@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.
- package/dist/core/identification/engine.d.ts +3 -0
- package/dist/core/identification/engine.js +13 -2
- package/dist/core/media/resolve.d.ts +2 -0
- package/dist/core/media/resolve.js +20 -0
- package/dist/parliament/live/transcription.d.ts +20 -0
- package/dist/parliament/live/transcription.js +57 -14
- package/dist/scripts/transcribe_live.js +20 -1
- package/dist/src/config/env.js +6 -32
- package/dist/src/{parliament/live → direct}/api.js +40 -57
- package/dist/src/{core/ndjson.js → direct/ndjson_tail.js} +5 -16
- package/dist/src/direct/transcription.js +252 -0
- package/dist/src/{parliament → formatters}/compteRendu.js +14 -6
- package/dist/src/formatters/formatTranscript.js +72 -0
- package/dist/src/git.js +167 -0
- package/dist/src/identification/identifySpeakers.js +378 -0
- package/dist/src/identification/live.js +190 -0
- package/dist/src/identification/scoring.js +61 -0
- package/dist/src/identification/speakerText.js +88 -0
- package/dist/src/{parliament/nvs → nvs}/nvsChapters.js +7 -0
- package/dist/src/providers/assemblyai.js +116 -0
- package/dist/src/providers/deepgram.js +397 -0
- package/dist/src/providers/index.js +16 -0
- package/dist/src/providers/mistral.js +75 -0
- package/dist/src/{parliament → scripts}/transcribeOne.js +68 -77
- package/dist/src/scripts/transcribe_audio.js +80 -0
- package/dist/src/scripts/transcribe_live.js +95 -82
- package/dist/src/scripts/transcribe_podcast.js +78 -0
- package/dist/src/scripts/transcribe_replay.js +89 -0
- package/dist/src/scripts/transcribe_reunions.js +140 -88
- package/dist/src/utils/copy.js +349 -0
- package/dist/src/utils/date.js +34 -0
- package/dist/src/utils/ffmpeg.js +88 -0
- package/dist/src/utils/filters.js +27 -0
- package/dist/src/utils/identifySpeakers.js +482 -0
- package/dist/src/{parliament → utils}/logging.js +4 -4
- package/dist/src/{parliament/nvs/parseNvs.js → utils/parse.js} +97 -22
- package/dist/src/{parliament/nvs/nvsSummary.js → utils/parseSummary.js} +2 -14
- package/dist/src/utils/path.js +74 -0
- package/dist/src/{core → utils}/roman.js +2 -2
- package/dist/src/utils/transcribeVideo.js +5 -0
- package/dist/src/utils/utlis.js +73 -0
- package/dist/tests/generate-expected.js +32 -30
- package/dist/tests/integration/providers.fixtures.benchmark.test.js +3 -2
- package/dist/tests/integration/transcribeOne.offline-e2e.test.js +7 -22
- package/dist/tests/unit/runForeverPush.resolveUrl.test.js +11 -29
- package/package.json +1 -1
- package/dist/src/core/cli.js +0 -82
- package/dist/src/core/diarization/segments.js +0 -233
- package/dist/src/core/fs.js +0 -23
- package/dist/src/core/http.js +0 -73
- package/dist/src/core/identification/assignment.js +0 -82
- package/dist/src/core/identification/backends/debug.js +0 -11
- package/dist/src/core/identification/backends/deterministic.js +0 -510
- package/dist/src/core/identification/backends/keys.js +0 -56
- package/dist/src/core/identification/backends/llm-open.js +0 -377
- package/dist/src/core/identification/backends/llm-retry.js +0 -121
- package/dist/src/core/identification/backends/llm.js +0 -482
- package/dist/src/core/identification/backends/prompt.js +0 -89
- package/dist/src/core/identification/backends/selection.js +0 -36
- package/dist/src/core/identification/candidates.js +0 -66
- package/dist/src/core/identification/display.js +0 -34
- package/dist/src/core/identification/engine.js +0 -476
- package/dist/src/core/identification/factory.js +0 -32
- package/dist/src/core/identification/identify.js +0 -143
- package/dist/src/core/identification/open-world.js +0 -39
- package/dist/src/core/identification/scoring.js +0 -90
- package/dist/src/core/identification/stats.js +0 -36
- package/dist/src/core/identification/types.js +0 -9
- package/dist/src/core/media/extract.js +0 -61
- package/dist/src/core/media/ffmpeg.js +0 -296
- package/dist/src/core/media/pdt.js +0 -147
- package/dist/src/core/media/probe.js +0 -119
- package/dist/src/core/media/resolve.js +0 -212
- package/dist/src/core/media/stream.js +0 -28
- package/dist/src/core/providers/TranscriptionProvider.js +0 -3
- package/dist/src/core/providers/assemblyai.js +0 -54
- package/dist/src/core/providers/deepgram.js +0 -337
- package/dist/src/core/providers/index.js +0 -33
- package/dist/src/core/providers/mistral.js +0 -212
- package/dist/src/core/stream/audioPipe.js +0 -28
- package/dist/src/core/stream/failures.js +0 -49
- package/dist/src/core/stream/session.js +0 -440
- package/dist/src/core/text/captions.js +0 -88
- package/dist/src/core/text/french.js +0 -131
- package/dist/src/core/text/html.js +0 -18
- package/dist/src/core/text/normalize.js +0 -33
- package/dist/src/core/time.js +0 -81
- package/dist/src/core/transcriber.js +0 -212
- package/dist/src/parliament/audioStart.js +0 -40
- package/dist/src/parliament/chambers/an.js +0 -64
- package/dist/src/parliament/chambers/index.js +0 -10
- package/dist/src/parliament/chambers/sn.js +0 -58
- package/dist/src/parliament/chambers/types.js +0 -7
- package/dist/src/parliament/constants.js +0 -8
- package/dist/src/parliament/copy.js +0 -140
- package/dist/src/parliament/filters.js +0 -19
- package/dist/src/parliament/identification/createSpeakerIdentifier.js +0 -69
- package/dist/src/parliament/identification/prompts.js +0 -80
- package/dist/src/parliament/live/transcription.js +0 -257
- package/dist/src/parliament/nvs/nvsBundle.js +0 -21
- package/dist/src/parliament/nvs/nvsPaths.js +0 -16
- package/dist/src/parliament/odj.js +0 -68
- package/dist/src/parliament/parse.js +0 -82
- package/dist/src/parliament/paths.js +0 -65
- package/dist/src/parliament/types/parliament.js +0 -1
- package/dist/src/parliament/uid.js +0 -3
- package/dist/src/scripts/cli_common.js +0 -32
- package/dist/src/scripts/replay.js +0 -106
- package/dist/src/scripts/transcribe_media.js +0 -172
- package/dist/src/scripts/transcribe_stream.js +0 -101
- package/dist/tests/compat/public-interface.test.js +0 -80
- package/dist/tests/integration/media.extract.real.test.js +0 -97
- package/dist/tests/unit/assignment.solve.test.js +0 -126
- package/dist/tests/unit/cli.parse.test.js +0 -107
- package/dist/tests/unit/diarization.segments.test.js +0 -176
- package/dist/tests/unit/http.retry.test.js +0 -40
- package/dist/tests/unit/identification.candidates.test.js +0 -67
- package/dist/tests/unit/identification.display.test.js +0 -43
- package/dist/tests/unit/identification.engine.test.js +0 -665
- package/dist/tests/unit/identification.identify.test.js +0 -345
- package/dist/tests/unit/identification.llm-open.test.js +0 -179
- package/dist/tests/unit/identification.llm-retry.test.js +0 -103
- package/dist/tests/unit/identification.llm.test.js +0 -557
- package/dist/tests/unit/identification.open-world.test.js +0 -76
- package/dist/tests/unit/identification.scoring.test.js +0 -69
- package/dist/tests/unit/identification.selection.test.js +0 -48
- package/dist/tests/unit/identification.stats.test.js +0 -48
- package/dist/tests/unit/identification.wiring.test.js +0 -84
- package/dist/tests/unit/media.broadcast-start.test.js +0 -67
- package/dist/tests/unit/media.formats.test.js +0 -200
- package/dist/tests/unit/media.intake.test.js +0 -186
- package/dist/tests/unit/media.pdt.test.js +0 -128
- package/dist/tests/unit/media.resolve.ytdlp.test.js +0 -119
- package/dist/tests/unit/media.streamPrepare.test.js +0 -56
- package/dist/tests/unit/parliament.audioStart.test.js +0 -42
- package/dist/tests/unit/parliament.odj.test.js +0 -85
- package/dist/tests/unit/providers.deepgram-partial.test.js +0 -139
- package/dist/tests/unit/providers.mistral-partial.test.js +0 -84
- package/dist/tests/unit/providers.words.test.js +0 -129
- package/dist/tests/unit/stream.failures.test.js +0 -32
- package/dist/tests/unit/stream.session.test.js +0 -654
- package/dist/tests/unit/text.captions.test.js +0 -60
- package/dist/tests/unit/text.french.test.js +0 -107
- package/dist/tests/unit/text.html.test.js +0 -36
- package/dist/tests/unit/time.test.js +0 -97
- package/dist/tests/unit/transcriber.test.js +0 -137
- package/dist/vitest.config.js +0 -9
- /package/dist/src/{core/stream/types.js → providers/TranscriptionProvider.js} +0 -0
- /package/dist/src/{core/types/transcription.js → types/identification.js} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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)
|
package/dist/src/config/env.js
CHANGED
|
@@ -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'
|
|
5
|
-
ASSEMBLYAI_API_KEY: z.string().
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
47
|
-
export async function fetchDirectReunionUids(uid) {
|
|
41
|
+
export async function postParagraphe(uid, p) {
|
|
48
42
|
const { baseUrl, token } = getApiConfig();
|
|
49
|
-
const res = await
|
|
50
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
7
|
+
let stat;
|
|
18
8
|
try {
|
|
19
9
|
stat = await fs.promises.stat(path);
|
|
20
10
|
}
|
|
21
11
|
catch {
|
|
22
|
-
|
|
12
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
13
|
+
continue;
|
|
23
14
|
}
|
|
24
|
-
if (stat
|
|
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
|
-
|
|
46
|
-
return;
|
|
47
|
-
await sleep(pollMs);
|
|
36
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
48
37
|
}
|
|
49
38
|
}
|