@tricoteuses/transcription-videos 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/core/identification/engine.d.ts +3 -0
  2. package/dist/core/identification/engine.js +13 -2
  3. package/dist/core/media/resolve.d.ts +2 -0
  4. package/dist/core/media/resolve.js +20 -0
  5. package/dist/parliament/live/transcription.d.ts +20 -0
  6. package/dist/parliament/live/transcription.js +57 -14
  7. package/dist/scripts/transcribe_live.js +20 -1
  8. package/dist/src/config/env.js +6 -32
  9. package/dist/src/{parliament/live → direct}/api.js +40 -57
  10. package/dist/src/{core/ndjson.js → direct/ndjson_tail.js} +5 -16
  11. package/dist/src/direct/transcription.js +252 -0
  12. package/dist/src/{parliament → formatters}/compteRendu.js +14 -6
  13. package/dist/src/formatters/formatTranscript.js +72 -0
  14. package/dist/src/git.js +167 -0
  15. package/dist/src/identification/identifySpeakers.js +378 -0
  16. package/dist/src/identification/live.js +190 -0
  17. package/dist/src/identification/scoring.js +61 -0
  18. package/dist/src/identification/speakerText.js +88 -0
  19. package/dist/src/{parliament/nvs → nvs}/nvsChapters.js +7 -0
  20. package/dist/src/providers/assemblyai.js +116 -0
  21. package/dist/src/providers/deepgram.js +397 -0
  22. package/dist/src/providers/index.js +16 -0
  23. package/dist/src/providers/mistral.js +75 -0
  24. package/dist/src/{parliament → scripts}/transcribeOne.js +68 -77
  25. package/dist/src/scripts/transcribe_audio.js +80 -0
  26. package/dist/src/scripts/transcribe_live.js +95 -82
  27. package/dist/src/scripts/transcribe_podcast.js +78 -0
  28. package/dist/src/scripts/transcribe_replay.js +89 -0
  29. package/dist/src/scripts/transcribe_reunions.js +140 -88
  30. package/dist/src/utils/copy.js +349 -0
  31. package/dist/src/utils/date.js +34 -0
  32. package/dist/src/utils/ffmpeg.js +88 -0
  33. package/dist/src/utils/filters.js +27 -0
  34. package/dist/src/utils/identifySpeakers.js +482 -0
  35. package/dist/src/{parliament → utils}/logging.js +4 -4
  36. package/dist/src/{parliament/nvs/parseNvs.js → utils/parse.js} +97 -22
  37. package/dist/src/{parliament/nvs/nvsSummary.js → utils/parseSummary.js} +2 -14
  38. package/dist/src/utils/path.js +74 -0
  39. package/dist/src/{core → utils}/roman.js +2 -2
  40. package/dist/src/utils/transcribeVideo.js +5 -0
  41. package/dist/src/utils/utlis.js +73 -0
  42. package/dist/tests/generate-expected.js +32 -30
  43. package/dist/tests/integration/providers.fixtures.benchmark.test.js +3 -2
  44. package/dist/tests/integration/transcribeOne.offline-e2e.test.js +7 -22
  45. package/dist/tests/unit/runForeverPush.resolveUrl.test.js +11 -29
  46. package/package.json +1 -1
  47. package/dist/src/core/cli.js +0 -82
  48. package/dist/src/core/diarization/segments.js +0 -233
  49. package/dist/src/core/fs.js +0 -23
  50. package/dist/src/core/http.js +0 -73
  51. package/dist/src/core/identification/assignment.js +0 -82
  52. package/dist/src/core/identification/backends/debug.js +0 -11
  53. package/dist/src/core/identification/backends/deterministic.js +0 -510
  54. package/dist/src/core/identification/backends/keys.js +0 -56
  55. package/dist/src/core/identification/backends/llm-open.js +0 -377
  56. package/dist/src/core/identification/backends/llm-retry.js +0 -121
  57. package/dist/src/core/identification/backends/llm.js +0 -482
  58. package/dist/src/core/identification/backends/prompt.js +0 -89
  59. package/dist/src/core/identification/backends/selection.js +0 -36
  60. package/dist/src/core/identification/candidates.js +0 -66
  61. package/dist/src/core/identification/display.js +0 -34
  62. package/dist/src/core/identification/engine.js +0 -476
  63. package/dist/src/core/identification/factory.js +0 -32
  64. package/dist/src/core/identification/identify.js +0 -143
  65. package/dist/src/core/identification/open-world.js +0 -39
  66. package/dist/src/core/identification/scoring.js +0 -90
  67. package/dist/src/core/identification/stats.js +0 -36
  68. package/dist/src/core/identification/types.js +0 -9
  69. package/dist/src/core/media/extract.js +0 -61
  70. package/dist/src/core/media/ffmpeg.js +0 -296
  71. package/dist/src/core/media/pdt.js +0 -147
  72. package/dist/src/core/media/probe.js +0 -119
  73. package/dist/src/core/media/resolve.js +0 -212
  74. package/dist/src/core/media/stream.js +0 -28
  75. package/dist/src/core/providers/TranscriptionProvider.js +0 -3
  76. package/dist/src/core/providers/assemblyai.js +0 -54
  77. package/dist/src/core/providers/deepgram.js +0 -337
  78. package/dist/src/core/providers/index.js +0 -33
  79. package/dist/src/core/providers/mistral.js +0 -212
  80. package/dist/src/core/stream/audioPipe.js +0 -28
  81. package/dist/src/core/stream/failures.js +0 -49
  82. package/dist/src/core/stream/session.js +0 -440
  83. package/dist/src/core/text/captions.js +0 -88
  84. package/dist/src/core/text/french.js +0 -131
  85. package/dist/src/core/text/html.js +0 -18
  86. package/dist/src/core/text/normalize.js +0 -33
  87. package/dist/src/core/time.js +0 -81
  88. package/dist/src/core/transcriber.js +0 -212
  89. package/dist/src/parliament/audioStart.js +0 -40
  90. package/dist/src/parliament/chambers/an.js +0 -64
  91. package/dist/src/parliament/chambers/index.js +0 -10
  92. package/dist/src/parliament/chambers/sn.js +0 -58
  93. package/dist/src/parliament/chambers/types.js +0 -7
  94. package/dist/src/parliament/constants.js +0 -8
  95. package/dist/src/parliament/copy.js +0 -140
  96. package/dist/src/parliament/filters.js +0 -19
  97. package/dist/src/parliament/identification/createSpeakerIdentifier.js +0 -69
  98. package/dist/src/parliament/identification/prompts.js +0 -80
  99. package/dist/src/parliament/live/transcription.js +0 -257
  100. package/dist/src/parliament/nvs/nvsBundle.js +0 -21
  101. package/dist/src/parliament/nvs/nvsPaths.js +0 -16
  102. package/dist/src/parliament/odj.js +0 -68
  103. package/dist/src/parliament/parse.js +0 -82
  104. package/dist/src/parliament/paths.js +0 -65
  105. package/dist/src/parliament/types/parliament.js +0 -1
  106. package/dist/src/parliament/uid.js +0 -3
  107. package/dist/src/scripts/cli_common.js +0 -32
  108. package/dist/src/scripts/replay.js +0 -106
  109. package/dist/src/scripts/transcribe_media.js +0 -172
  110. package/dist/src/scripts/transcribe_stream.js +0 -101
  111. package/dist/tests/compat/public-interface.test.js +0 -80
  112. package/dist/tests/integration/media.extract.real.test.js +0 -97
  113. package/dist/tests/unit/assignment.solve.test.js +0 -126
  114. package/dist/tests/unit/cli.parse.test.js +0 -107
  115. package/dist/tests/unit/diarization.segments.test.js +0 -176
  116. package/dist/tests/unit/http.retry.test.js +0 -40
  117. package/dist/tests/unit/identification.candidates.test.js +0 -67
  118. package/dist/tests/unit/identification.display.test.js +0 -43
  119. package/dist/tests/unit/identification.engine.test.js +0 -665
  120. package/dist/tests/unit/identification.identify.test.js +0 -345
  121. package/dist/tests/unit/identification.llm-open.test.js +0 -179
  122. package/dist/tests/unit/identification.llm-retry.test.js +0 -103
  123. package/dist/tests/unit/identification.llm.test.js +0 -557
  124. package/dist/tests/unit/identification.open-world.test.js +0 -76
  125. package/dist/tests/unit/identification.scoring.test.js +0 -69
  126. package/dist/tests/unit/identification.selection.test.js +0 -48
  127. package/dist/tests/unit/identification.stats.test.js +0 -48
  128. package/dist/tests/unit/identification.wiring.test.js +0 -84
  129. package/dist/tests/unit/media.broadcast-start.test.js +0 -67
  130. package/dist/tests/unit/media.formats.test.js +0 -200
  131. package/dist/tests/unit/media.intake.test.js +0 -186
  132. package/dist/tests/unit/media.pdt.test.js +0 -128
  133. package/dist/tests/unit/media.resolve.ytdlp.test.js +0 -119
  134. package/dist/tests/unit/media.streamPrepare.test.js +0 -56
  135. package/dist/tests/unit/parliament.audioStart.test.js +0 -42
  136. package/dist/tests/unit/parliament.odj.test.js +0 -85
  137. package/dist/tests/unit/providers.deepgram-partial.test.js +0 -139
  138. package/dist/tests/unit/providers.mistral-partial.test.js +0 -84
  139. package/dist/tests/unit/providers.words.test.js +0 -129
  140. package/dist/tests/unit/stream.failures.test.js +0 -32
  141. package/dist/tests/unit/stream.session.test.js +0 -654
  142. package/dist/tests/unit/text.captions.test.js +0 -60
  143. package/dist/tests/unit/text.french.test.js +0 -107
  144. package/dist/tests/unit/text.html.test.js +0 -36
  145. package/dist/tests/unit/time.test.js +0 -97
  146. package/dist/tests/unit/transcriber.test.js +0 -137
  147. package/dist/vitest.config.js +0 -9
  148. /package/dist/src/{core/stream/types.js → providers/TranscriptionProvider.js} +0 -0
  149. /package/dist/src/{core/types/transcription.js → types/identification.js} +0 -0
  150. /package/dist/src/{parliament/nvs/types.js → types/transcription.js} +0 -0
@@ -47,6 +47,9 @@ export declare class SpeakerIdentificationEngine {
47
47
  * each cascade run; windowed claims take precedence over the flat mapping
48
48
  * for segments inside their [startMs, endMs] window. */
49
49
  private windowedMapping;
50
+ /** Consecutive runCascade() invocations whose fetchContext returned null
51
+ * (reset on success). Drives the throttled visibility warning. */
52
+ private contextFailCount;
50
53
  /** Source key → canonical key for applied merges (cumulative, first-seen wins). */
51
54
  private mergeRemap;
52
55
  private rosterById;
@@ -28,6 +28,9 @@ export class SpeakerIdentificationEngine {
28
28
  * each cascade run; windowed claims take precedence over the flat mapping
29
29
  * for segments inside their [startMs, endMs] window. */
30
30
  windowedMapping = new Map();
31
+ /** Consecutive runCascade() invocations whose fetchContext returned null
32
+ * (reset on success). Drives the throttled visibility warning. */
33
+ contextFailCount = 0;
31
34
  /** Source key → canonical key for applied merges (cumulative, first-seen wins). */
32
35
  mergeRemap = new Map();
33
36
  rosterById = new Map();
@@ -239,9 +242,15 @@ export class SpeakerIdentificationEngine {
239
242
  }
240
243
  const ctx = await this.args.fetchContext();
241
244
  if (!ctx) {
242
- this.log('No identification context fetched → skipping run');
245
+ // Visible outside debug: a persistently missing context silently
246
+ // disables identification (e.g. NVS unavailable on the remote source).
247
+ this.contextFailCount += 1;
248
+ if (this.contextFailCount % 10 === 1) {
249
+ console.warn('[SpeakerIdentificationEngine]', `No identification context fetched (×${this.contextFailCount}) → skipping run`);
250
+ }
243
251
  return;
244
252
  }
253
+ this.contextFailCount = 0;
245
254
  // Roster lookup for meta enrichment (url, role, …)
246
255
  this.rosterById = new Map((ctx.roster ?? []).map((r) => [r.id, r]));
247
256
  // Windowed claims are stateless per run: rebuilt from scratch so a key
@@ -268,7 +277,9 @@ export class SpeakerIdentificationEngine {
268
277
  decided = normalizeDecideResult(raw);
269
278
  }
270
279
  catch (err) {
271
- this.log(`Backend ${backend.name} error:`, err?.message ?? err);
280
+ // Visible outside debug: a repeatedly failing backend (bad API key,
281
+ // invalid model id, unreachable endpoint) silently disables the layer.
282
+ console.warn('[SpeakerIdentificationEngine]', `Backend ${backend.name} error:`, err?.message ?? err);
272
283
  continue;
273
284
  }
274
285
  for (const [key, claims] of Object.entries(decided.splits ?? {})) {
@@ -13,6 +13,8 @@ export type ResolveDeps = {
13
13
  exists: (p: string) => Promise<boolean>;
14
14
  getStreamUrl: (url: string) => Promise<string>;
15
15
  };
16
+ /** Watch-page URL yt-dlp can introspect for metadata (YouTube watch / live / youtu.be). */
17
+ export declare function isYouTubeWatchUrl(url: string | null | undefined): boolean;
16
18
  /** Local path usable as-is by ffmpeg. */
17
19
  export declare function resolvedTarget(input: ResolvedInput): string;
18
20
  /** Kill yt-dlp resolution after this long: an extractor hang must not hang the job. */
@@ -1,6 +1,26 @@
1
1
  import fs from 'node:fs';
2
2
  import { execa } from 'execa';
3
3
  const MEDIA_EXT_RE = /\.(m3u8|mpd|mp3|m4a|aac|wav|flac|ogg|opus|oga|mp4|m4v|mkv|webm|mov|ts|m2ts|avi|flv|wmv)(\?.*)?$/i;
4
+ /** Watch-page URL yt-dlp can introspect for metadata (YouTube watch / live / youtu.be). */
5
+ export function isYouTubeWatchUrl(url) {
6
+ if (!url)
7
+ return false;
8
+ try {
9
+ const parsed = new URL(url.trim());
10
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
11
+ return false;
12
+ const host = parsed.hostname.replace(/^www\./, '');
13
+ if (host === 'youtu.be')
14
+ return true;
15
+ if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'music.youtube.com') {
16
+ return parsed.pathname === '/watch' || parsed.pathname.startsWith('/live/');
17
+ }
18
+ return false;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
4
24
  /** Local path usable as-is by ffmpeg. */
5
25
  export function resolvedTarget(input) {
6
26
  return input.kind === 'file' ? input.path : input.url;
@@ -9,6 +9,26 @@ export type PushOpts = {
9
9
  preflightProbe?: boolean;
10
10
  };
11
11
  export declare function nvsStartTimeToEpochMs(startTimeNvs: string): number | null;
12
+ /** PostgreSQL INT4 max: the API stores paragraph timecodes as INT4. */
13
+ export declare const INT32_MAX_MS = 2147483647;
14
+ /**
15
+ * Video-relative timecode offset (ms) for a session attempt anchored on the
16
+ * HLS PROGRAM-DATE-TIME read position. Hierarchy: NVS broadcast start, then
17
+ * video broadcast start (YouTube), else null — the absolute epoch must never
18
+ * leak into a timecode (INT4 overflow API-side).
19
+ */
20
+ export declare function computePdtTimecodeOffsetMs(args: {
21
+ pdtAnchorMs: number;
22
+ startTimeNvs?: string | null;
23
+ broadcastStartMs?: number | null;
24
+ }): number | null;
25
+ export declare function toParagrapheApi(evt: any): {
26
+ start_ms: number;
27
+ end_ms: number;
28
+ speaker: string | null;
29
+ text: string;
30
+ confidence: number | null;
31
+ } | null;
12
32
  /**
13
33
  * DebatDirect adapter over the generic runStreamSession loop: holds every
14
34
  * parliament-specific concern (status polling, timecode offsets AN/SN, rolling
@@ -55,7 +55,31 @@ function computeTimecodeOffsetMs(startTimeNvs, sessionStartedAtMs) {
55
55
  // Offset is “current video timecode” at session start; never negative.
56
56
  return Math.max(0, Math.round(offset));
57
57
  }
58
- function toParagrapheApi(evt) {
58
+ /** PostgreSQL INT4 max: the API stores paragraph timecodes as INT4. */
59
+ export const INT32_MAX_MS = 2_147_483_647;
60
+ /**
61
+ * Video-relative timecode offset (ms) for a session attempt anchored on the
62
+ * HLS PROGRAM-DATE-TIME read position. Hierarchy: NVS broadcast start, then
63
+ * video broadcast start (YouTube), else null — the absolute epoch must never
64
+ * leak into a timecode (INT4 overflow API-side).
65
+ */
66
+ export function computePdtTimecodeOffsetMs(args) {
67
+ const { pdtAnchorMs, startTimeNvs, broadcastStartMs } = args;
68
+ if (!Number.isFinite(pdtAnchorMs))
69
+ return null;
70
+ const startEpochMs = nvsStartTimeToEpochMs(startTimeNvs ?? '');
71
+ if (startEpochMs != null)
72
+ return Math.max(0, Math.round(pdtAnchorMs - startEpochMs));
73
+ if (typeof broadcastStartMs === 'number' &&
74
+ Number.isFinite(broadcastStartMs) &&
75
+ broadcastStartMs > 0) {
76
+ return Math.max(0, Math.round(pdtAnchorMs - broadcastStartMs));
77
+ }
78
+ return null;
79
+ }
80
+ /** Logged once per process: an over-INT4 offset is a steady state (24/7 streams), not a one-off. */
81
+ let int4OffsetWarningShown = false;
82
+ export function toParagrapheApi(evt) {
59
83
  if (evt?.type !== 'utterance')
60
84
  return null;
61
85
  let start_ms = Number(evt.start);
@@ -69,11 +93,26 @@ function toParagrapheApi(evt) {
69
93
  : '';
70
94
  console.log(`[toParagrapheApi] start=${start_ms} end=${end_ms} offsetMs=${offsetMs}${absLabel}`);
71
95
  if (offsetMs != null && Number.isFinite(offsetMs)) {
72
- start_ms += offsetMs;
73
- end_ms += offsetMs;
96
+ if (offsetMs > INT32_MAX_MS) {
97
+ // Video position beyond what an INT4 timecode can hold (>24.8 days:
98
+ // epoch leak or permanent stream). Storing it would overflow INT4
99
+ // API-side; ignore the offset and keep relative timecodes flowing.
100
+ if (!int4OffsetWarningShown) {
101
+ int4OffsetWarningShown = true;
102
+ console.error(`[toParagrapheApi] timecodeOffsetMs=${offsetMs} exceeds INT4 (>24.8 days video position), offset ignored — timecodes stay relative to the read position`);
103
+ }
104
+ }
105
+ else {
106
+ start_ms += offsetMs;
107
+ end_ms += offsetMs;
108
+ }
74
109
  }
75
110
  if (!text || !Number.isFinite(start_ms) || !Number.isFinite(end_ms))
76
111
  return null;
112
+ if (start_ms > INT32_MAX_MS || end_ms > INT32_MAX_MS) {
113
+ console.error(`[toParagrapheApi] start_ms=${start_ms} end_ms=${end_ms} exceed INT4, paragraph dropped`);
114
+ return null;
115
+ }
77
116
  return {
78
117
  start_ms,
79
118
  end_ms,
@@ -207,17 +246,21 @@ export async function runForeverPush(args) {
207
246
  sessionStartMeta: ({ startedAtMs, restartOffsetSec, pdtAnchorMs }) => {
208
247
  const startSecOffset = startSec ? startSec * 1000 : 0;
209
248
  if (pdtAnchorMs != null) {
210
- // Absolute anchor measured on the actual read position (live edge
211
- // in stream mode, VOD start in media mode): video-relative when the
212
- // NVS epoch is known, absolute wall-clock otherwise. The empirical
213
- // AN/SN latency constants do not apply on this path.
214
- const startEpochMs = nvsStartTimeToEpochMs(startTimeNvs ?? '');
215
- const relative = startEpochMs != null ? pdtAnchorMs - startEpochMs : pdtAnchorMs;
216
- return {
217
- startTimeNvs,
218
- timecodeOffsetMs: Math.max(0, Math.round(relative)) + startSecOffset,
219
- timecodeSource: 'pdt',
220
- };
249
+ // Video-relative offset when a broadcast start is known (NVS epoch
250
+ // for AN/SN, video metadata epoch for YouTube); null otherwise.
251
+ // The empirical AN/SN latency constants do not apply on this path.
252
+ const offsetMs = computePdtTimecodeOffsetMs({ pdtAnchorMs, startTimeNvs, broadcastStartMs });
253
+ return offsetMs != null
254
+ ? {
255
+ startTimeNvs,
256
+ timecodeOffsetMs: offsetMs + startSecOffset,
257
+ timecodeSource: 'pdt',
258
+ }
259
+ : {
260
+ startTimeNvs,
261
+ timecodeOffsetMs: null,
262
+ timecodeSource: 'pdt-no-start',
263
+ };
221
264
  }
222
265
  const activeTs = activeStartTime(restartOffsetSec);
223
266
  const nvsOffset = computeTimecodeOffsetMs(activeTs ? String(activeTs) : (startTimeNvs ?? null), startedAtMs);
@@ -5,7 +5,7 @@ import { resolveSourceUrl, getStreamProvider, withSharedTranscribeOptions } from
5
5
  import { transcribeReplay } from './replay.js';
6
6
  import { fetchDebatDirect, resolveReunionSessionTitle } from '../parliament/live/api.js';
7
7
  import { nvsStartTimeToEpochMs, runForeverPush } from '../parliament/live/transcription.js';
8
- import { resolveVideoMetadata } from '../core/media/resolve.js';
8
+ import { resolveVideoMetadata, isYouTubeWatchUrl } from '../core/media/resolve.js';
9
9
  import { fetchText } from '../core/http.js';
10
10
  import { PARLIAMENT_HEADERS } from '../parliament/constants.js';
11
11
  async function transcribeLive(opts) {
@@ -73,6 +73,25 @@ async function transcribeLive(opts) {
73
73
  console.info(`[live] livePlayerNvs starttime: ${startTimeNvs}`);
74
74
  }
75
75
  }
76
+ // No NVS (e.g. GVT YouTube directs): anchor the video timebase on the
77
+ // YouTube broadcast start so live timecodes stay video-relative. Without
78
+ // it the PDT anchor would leak an absolute epoch into the timecode
79
+ // (INT4 overflow API-side). Best-effort: null on failure (degraded mode).
80
+ if (!livePlayerNvsUrl && isYouTubeWatchUrl(direct.m3u8Url)) {
81
+ try {
82
+ const metadata = await resolveVideoMetadata(direct.m3u8Url);
83
+ urlBroadcastStartMs = metadata?.timestampMs ?? null;
84
+ if (urlBroadcastStartMs != null) {
85
+ console.info(`[live] youtube broadcast start: ${new Date(urlBroadcastStartMs).toISOString()}`);
86
+ }
87
+ else {
88
+ console.warn('[live] youtube broadcast start unavailable, degraded timecode mode');
89
+ }
90
+ }
91
+ catch (err) {
92
+ console.warn(`[live] youtube broadcast start lookup failed: ${err?.message ?? err}`);
93
+ }
94
+ }
76
95
  }
77
96
  const resolveUrl = async () => {
78
97
  if (opts.url)
@@ -1,49 +1,23 @@
1
1
  import 'dotenv/config';
2
2
  import { z } from 'zod';
3
3
  const EnvSchema = z.object({
4
- TRANSCRIPTION_PROVIDER: z.enum(['assemblyai', 'deepgram', 'mistral']).default('deepgram'),
5
- ASSEMBLYAI_API_KEY: z.string().optional(),
4
+ TRANSCRIPTION_PROVIDER: z.enum(['assemblyai', 'deepgram']).default('deepgram'),
5
+ ASSEMBLYAI_API_KEY: z.string().min(1, 'ASSEMBLYAI_API_KEY is required'),
6
6
  ASSEMBLYAI_BASE_URL: z.string().url().optional(),
7
7
  DEFAULT_LANGUAGE: z.string().default('fr'),
8
8
  DEFAULT_DIARIZATION: z.string().default('true'),
9
9
  DEEPGRAM_API_KEY: z.string().optional(),
10
10
  DEEPGRAM_MODEL: z.string().default('nova-3'),
11
- DEEPGRAM_DIARIZE_MODEL: z.enum(['latest', 'v1', 'v2']).default('v2'),
11
+ DEEPGRAM_SMART_FORMAT: z.string().default('true'),
12
12
  MISTRAL_API_KEY: z.string().optional(),
13
13
  MISTRAL_TRANSCRIBE_MODEL: z.string().default('voxtral-mini-latest'),
14
- MISTRAL_REALTIME_MODEL: z.string().default('voxtral-mini-transcribe-realtime-2602'),
15
- MISTRAL_BASE_URL: z.string().optional(),
16
14
  DG_UTT_SPLIT_SEC: z.coerce.number().optional(),
17
15
  DG_MERGE_GAP_MS: z.coerce.number().default(800),
18
16
  DG_MIN_SEGMENT_MS: z.coerce.number().default(1200),
19
17
  DG_USE_PARAGRAPHS: z.string().default('true'),
20
- LLM_API_KEY: z.string().optional(),
21
- LLM_BASE_URL: z.string().url().optional(),
22
- IDENTIFICATION_MODEL: z.string().optional(),
18
+ ANTHROPIC_API_KEY: z.string().optional(),
19
+ IDENTIFICATION_MODEL: z.string().optional().default('claude-haiku-4-5'),
23
20
  ENABLE_IDENTIFICATION: z.string().default('false'),
24
21
  });
25
- export const ENV = EnvSchema.superRefine((env, ctx) => {
26
- const providerKeys = {
27
- assemblyai: { path: 'ASSEMBLYAI_API_KEY', label: 'ASSEMBLYAI_API_KEY' },
28
- deepgram: { path: 'DEEPGRAM_API_KEY', label: 'DEEPGRAM_API_KEY' },
29
- mistral: { path: 'MISTRAL_API_KEY', label: 'MISTRAL_API_KEY' },
30
- };
31
- for (const key of ['ASSEMBLYAI_API_KEY', 'DEEPGRAM_API_KEY', 'MISTRAL_API_KEY']) {
32
- if (env[key] !== undefined && env[key].length === 0) {
33
- ctx.addIssue({
34
- code: z.ZodIssueCode.custom,
35
- path: [key],
36
- message: `${key} must not be empty`,
37
- });
38
- }
39
- }
40
- const required = providerKeys[env.TRANSCRIPTION_PROVIDER];
41
- if (required && !env[required.path]) {
42
- ctx.addIssue({
43
- code: z.ZodIssueCode.custom,
44
- path: [required.path],
45
- message: `${required.label} is required when TRANSCRIPTION_PROVIDER=${env.TRANSCRIPTION_PROVIDER}`,
46
- });
47
- }
48
- }).parse(process.env);
22
+ export const ENV = EnvSchema.parse(process.env);
49
23
  export const DEFAULT_DIARIZATION = ENV.DEFAULT_DIARIZATION === 'true';
@@ -1,5 +1,3 @@
1
- import { fetchWithRetry } from '../../core/http.js';
2
- import { reunionOdjTitleFromDataset } from '../odj.js';
3
1
  function mustEnv(name) {
4
2
  const v = process.env[name];
5
3
  if (!v)
@@ -16,9 +14,7 @@ export async function fetchDebatDirect(uid) {
16
14
  const url = `${baseUrl}/directs/${encodeURIComponent(uid)}`;
17
15
  let res;
18
16
  try {
19
- res = await fetchWithRetry(`${baseUrl}/directs/${encodeURIComponent(uid)}?include=reunions`, {
20
- headers: { 'x-api-token': token },
21
- });
17
+ res = await fetch(url, { headers: { 'x-api-token': token } });
22
18
  }
23
19
  catch (err) {
24
20
  throw new Error(`Network error calling ${url}`, { cause: err });
@@ -40,70 +36,30 @@ export async function fetchDebatDirect(uid) {
40
36
  estReplay: d.estReplay ?? false,
41
37
  sourceDebatDirectUid: d.sourceDebatDirectUid ?? null,
42
38
  titre: d.titre ?? null,
43
- reunionUids: (d.reunions ?? []).map((r) => r?.reunionUid).filter(Boolean),
44
39
  };
45
40
  }
46
- /** UIDs des réunions liées à un direct (jointure direct→réunion). */
47
- export async function fetchDirectReunionUids(uid) {
41
+ export async function postParagraphe(uid, p) {
48
42
  const { baseUrl, token } = getApiConfig();
49
- const res = await fetchWithRetry(`${baseUrl}/directs/${encodeURIComponent(uid)}?include=reunions`, {
50
- headers: { 'x-api-token': token },
43
+ const res = await fetch(`${baseUrl}/paragraphes_directs/${encodeURIComponent(uid)}`, {
44
+ method: 'POST',
45
+ headers: {
46
+ 'content-type': 'application/json',
47
+ 'x-api-token': token,
48
+ },
49
+ body: JSON.stringify(p),
51
50
  });
52
- if (!res.ok)
53
- return [];
54
- const json = await res.json().catch(() => null);
55
- return (json?.data?.reunions ?? []).map((r) => r?.reunionUid).filter(Boolean);
56
- }
57
- /** Contexte LLM d'un direct : titre + ODJ complet de la réunion liée, sans
58
- * troncature (rapporteures, auditionnés…). La jointure direct→réunions est
59
- * une donnée API (le dataset ne porte pas l'UID du direct) ; l'ODJ lui-même
60
- * vient du dataset local (moisson continue — pas de fallback API).
61
- * `knownReunionUids` court-circuite la jointure API quand l'appelant les a
62
- * déjà (fetchDebatDirect les retourne gratuitement). undefined si la réunion
63
- * n'est pas trouvée localement. */
64
- export async function resolveReunionSessionTitle(uid, dataDir, knownReunionUids) {
65
- const reunionUids = knownReunionUids?.length ? knownReunionUids : await fetchDirectReunionUids(uid);
66
- const parts = [];
67
- for (const ruid of reunionUids.slice(0, 3)) {
68
- const title = await reunionOdjTitleFromDataset(ruid, dataDir);
69
- if (title)
70
- parts.push(title);
71
- }
72
- return parts.length ? parts.join(' — ') : undefined;
73
- }
74
- /** Shared POST against a paragraphes_directs subresource. Sends a JSON body
75
- * when provided (flush sends none), and throws with the given error label. */
76
- async function postDirectsJson(uid, suffix, opts) {
77
- const { baseUrl, token } = getApiConfig();
78
- const headers = { 'x-api-token': token };
79
- if (opts.body !== undefined)
80
- headers['content-type'] = 'application/json';
81
- let res;
82
- try {
83
- res = await fetchWithRetry(`${baseUrl}/paragraphes_directs/${encodeURIComponent(uid)}${suffix}`, {
84
- method: 'POST',
85
- headers,
86
- ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
87
- });
88
- }
89
- catch (err) {
90
- throw new Error(`${opts.errorLabel} network error`, { cause: err });
91
- }
92
51
  if (!res.ok) {
93
52
  const t = await res.text().catch(() => '');
94
- throw new Error(`${opts.errorLabel} failed ${res.status} ${t}`);
53
+ throw new Error(`API post paragraphe failed ${res.status} ${t}`);
95
54
  }
96
55
  return res.json().catch(() => ({}));
97
56
  }
98
- export async function postParagraphe(uid, p) {
99
- return postDirectsJson(uid, '', { body: p, errorLabel: 'API post paragraphe' });
100
- }
101
57
  export async function createReplayDebatDirect({ sourceUid, videoUrl, startSec, }) {
102
58
  const { baseUrl, token } = getApiConfig();
103
59
  const url = `${baseUrl}/v2/directs/${encodeURIComponent(sourceUid)}/replay`;
104
60
  let res;
105
61
  try {
106
- res = await fetchWithRetry(url, {
62
+ res = await fetch(url, {
107
63
  method: 'POST',
108
64
  headers: {
109
65
  'content-type': 'application/json',
@@ -133,14 +89,41 @@ export async function createReplayDebatDirect({ sourceUid, videoUrl, startSec, }
133
89
  titre: data.debatDirect.titre ?? null,
134
90
  estReplay: Boolean(data.debatDirect.estReplay),
135
91
  sourceDebatDirectUid: String(data.debatDirect.sourceDebatDirectUid),
136
- reunionUids: [],
137
92
  },
138
93
  };
139
94
  }
95
+ export async function postUtterance(uid, p) {
96
+ const { baseUrl, token } = getApiConfig();
97
+ const res = await fetch(`${baseUrl}/paragraphes_directs/${encodeURIComponent(uid)}/utterance`, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'content-type': 'application/json',
101
+ 'x-api-token': token,
102
+ },
103
+ body: JSON.stringify(p),
104
+ });
105
+ if (!res.ok) {
106
+ const t = await res.text().catch(() => '');
107
+ throw new Error(`API post utterance failed ${res.status} ${t}`);
108
+ }
109
+ return res.json().catch(() => ({}));
110
+ }
111
+ export async function flushUtterancesDraft(uid) {
112
+ const { baseUrl, token } = getApiConfig();
113
+ const res = await fetch(`${baseUrl}/paragraphes_directs/${encodeURIComponent(uid)}/flush`, {
114
+ method: 'POST',
115
+ headers: { 'x-api-token': token },
116
+ });
117
+ if (!res.ok) {
118
+ const t = await res.text().catch(() => '');
119
+ throw new Error(`API flush utterances draft failed ${res.status} ${t}`);
120
+ }
121
+ return res.json().catch(() => ({}));
122
+ }
140
123
  export async function updateDebatDirectIdentification({ uid, newMapping, }) {
141
124
  const { baseUrl, token } = getApiConfig();
142
125
  try {
143
- const res = await fetchWithRetry(`${baseUrl}/v2/directs/${encodeURIComponent(uid)}/identification`, {
126
+ const res = await fetch(`${baseUrl}/v2/directs/${encodeURIComponent(uid)}/identification`, {
144
127
  method: 'PUT',
145
128
  body: JSON.stringify({ mapping: newMapping }),
146
129
  headers: { 'x-api-token': token, 'Content-Type': 'application/json' },
@@ -1,27 +1,18 @@
1
1
  import fs from 'node:fs';
2
- import { sleep } from './time.js';
3
- /** Append one JSON object as a single NDJSON line. */
4
- export async function appendNdjson(filePath, obj) {
5
- await fs.promises.appendFile(filePath, JSON.stringify(obj) + '\n', 'utf8');
6
- }
7
- /**
8
- * Follow a growing NDJSON file, yielding one parsed line at a time (blank
9
- * lines skipped). Runs until `shouldStop()` returns true — checked after each
10
- * drain, so already-written lines are emitted before exiting.
11
- */
12
2
  export async function* tailNdjson(path, opts) {
13
3
  const pollMs = opts?.pollMs ?? 200;
14
4
  let pos = 0;
15
5
  let buf = '';
16
6
  while (true) {
17
- let stat = null;
7
+ let stat;
18
8
  try {
19
9
  stat = await fs.promises.stat(path);
20
10
  }
21
11
  catch {
22
- // Fichier pas encore créé : on retente au prochain tour.
12
+ await new Promise((r) => setTimeout(r, pollMs));
13
+ continue;
23
14
  }
24
- if (stat && stat.size > pos) {
15
+ if (stat.size > pos) {
25
16
  const fd = await fs.promises.open(path, 'r');
26
17
  try {
27
18
  const len = stat.size - pos;
@@ -42,8 +33,6 @@ export async function* tailNdjson(path, opts) {
42
33
  await fd.close();
43
34
  }
44
35
  }
45
- if (opts?.shouldStop?.())
46
- return;
47
- await sleep(pollMs);
36
+ await new Promise((r) => setTimeout(r, pollMs));
48
37
  }
49
38
  }