@tricoteuses/transcription-videos 1.7.2 → 1.7.3
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/config/env.d.ts +1 -0
- package/dist/config/env.js +1 -0
- package/dist/core/identification/backends/llm.js +10 -6
- package/dist/core/identification/engine.d.ts +10 -1
- package/dist/core/identification/engine.js +49 -6
- package/dist/core/identification/factory.d.ts +5 -0
- package/dist/core/identification/factory.js +1 -0
- package/dist/parliament/identification/createSpeakerIdentifier.d.ts +5 -0
- package/dist/parliament/identification/createSpeakerIdentifier.js +1 -0
- package/dist/parliament/identification/prompts.d.ts +1 -1
- package/dist/parliament/identification/prompts.js +4 -2
- package/dist/parliament/live/transcription.js +1 -0
- package/package.json +1 -1
package/dist/config/env.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare const ENV: {
|
|
|
11
11
|
DG_MIN_SEGMENT_MS: number;
|
|
12
12
|
DG_USE_PARAGRAPHS: string;
|
|
13
13
|
ENABLE_IDENTIFICATION: string;
|
|
14
|
+
IDENTIFICATION_MIN_PUBLISH_STRENGTH: "weak" | "strong";
|
|
14
15
|
ASSEMBLYAI_API_KEY?: string | undefined;
|
|
15
16
|
ASSEMBLYAI_BASE_URL?: string | undefined;
|
|
16
17
|
DEEPGRAM_API_KEY?: string | undefined;
|
package/dist/config/env.js
CHANGED
|
@@ -21,6 +21,7 @@ const EnvSchema = z.object({
|
|
|
21
21
|
LLM_BASE_URL: z.string().url().optional(),
|
|
22
22
|
IDENTIFICATION_MODEL: z.string().optional(),
|
|
23
23
|
ENABLE_IDENTIFICATION: z.string().default('false'),
|
|
24
|
+
IDENTIFICATION_MIN_PUBLISH_STRENGTH: z.enum(['weak', 'strong']).default('strong'),
|
|
24
25
|
});
|
|
25
26
|
export const ENV = EnvSchema.superRefine((env, ctx) => {
|
|
26
27
|
const providerKeys = {
|
|
@@ -17,7 +17,8 @@ const llmMergeSchema = z.object({
|
|
|
17
17
|
evidence: z.string(),
|
|
18
18
|
});
|
|
19
19
|
/** Legacy whole-key entry: one identity for the entire diarization key.
|
|
20
|
-
*
|
|
20
|
+
* Without an explicit `confidence` the entry stays 'weak' (provisional);
|
|
21
|
+
* explicit 'high' is required for 'strong'. `id` is
|
|
21
22
|
* null when the model attributes a free name (no roster, see `name`). */
|
|
22
23
|
const llmWholeKeySchema = z.object({
|
|
23
24
|
key: z.string(),
|
|
@@ -266,8 +267,11 @@ export function createLlmBackend(options = {}) {
|
|
|
266
267
|
log(response.merges);
|
|
267
268
|
acceptedMerges.push(...validateMergeProposals({ merges: response.merges, knownKeys, candidatePairs, seenMergeSources, log }));
|
|
268
269
|
const byId = new Map(roster.map((r) => [r.id, r]));
|
|
269
|
-
// Whole-key entries (legacy shape, still accepted):
|
|
270
|
-
//
|
|
270
|
+
// Whole-key entries (legacy shape, still accepted): a roster id is
|
|
271
|
+
// 'strong' only on explicit 'high' confidence. Without an explicit
|
|
272
|
+
// flag the claim stays 'weak' (provisional): an unmarked whole-key
|
|
273
|
+
// attribution easily binds a whole session on thin evidence, and the
|
|
274
|
+
// engine never dethrones a 'strong' incumbent.
|
|
271
275
|
// A `name` replaces the roster id (free-name attribution): the
|
|
272
276
|
// person is absent from the (possibly partial) list.
|
|
273
277
|
for (const m of response.mapping) {
|
|
@@ -304,9 +308,9 @@ export function createLlmBackend(options = {}) {
|
|
|
304
308
|
}
|
|
305
309
|
const strength = isFreeName
|
|
306
310
|
? freeNameStrength(m.confidence === 'low' ? 'low' : 'high')
|
|
307
|
-
: m.confidence === '
|
|
308
|
-
? '
|
|
309
|
-
: '
|
|
311
|
+
: m.confidence === 'high'
|
|
312
|
+
? 'strong'
|
|
313
|
+
: 'weak';
|
|
310
314
|
if (existing && existing.strength === 'strong')
|
|
311
315
|
continue;
|
|
312
316
|
accumulated[key] = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AbsoluteSegment, Claim, DecisionBackend, PersonInterval, RelativeSegment, RosterEntry } from './types.js';
|
|
1
|
+
import type { AbsoluteSegment, Claim, ClaimStrength, DecisionBackend, PersonInterval, RelativeSegment, RosterEntry } from './types.js';
|
|
2
2
|
/** Evidence fetched (and parsed) by the caller before each identification run. */
|
|
3
3
|
export type IdentificationContext = {
|
|
4
4
|
people?: PersonInterval[];
|
|
@@ -21,6 +21,11 @@ export type SpeakerIdentificationEngineArgs = {
|
|
|
21
21
|
newKeyDebounceMs?: number;
|
|
22
22
|
/** Initial offset applied at ingestion to reach the evidence timebase, in ms. */
|
|
23
23
|
timeOffsetMs?: number;
|
|
24
|
+
/** Minimum claim strength exposed to labels and downstream pushes.
|
|
25
|
+
* Claims below the threshold stay internal: they still feed later runs
|
|
26
|
+
* (knownMapping continuity) so the cascade can upgrade them, but they
|
|
27
|
+
* are never published. Default 'weak' (no filtering). */
|
|
28
|
+
minPublishStrength?: ClaimStrength;
|
|
24
29
|
debug?: boolean;
|
|
25
30
|
};
|
|
26
31
|
/** Mode-agnostic speaker identification engine shared by media (batch) and
|
|
@@ -79,6 +84,10 @@ export declare class SpeakerIdentificationEngine {
|
|
|
79
84
|
* a new attempt whose time base changes). Keeps nothing across the reset. */
|
|
80
85
|
reset(): void;
|
|
81
86
|
get mapping(): Map<string, Claim>;
|
|
87
|
+
/** Whether a claim reaches labels and downstream pushes. */
|
|
88
|
+
private isPublishable;
|
|
89
|
+
/** Published projection of one key (a changed projection fires the push flag). */
|
|
90
|
+
private publishedProjection;
|
|
82
91
|
/** Resolve one key at a point in time (evidence timebase, ms): windowed
|
|
83
92
|
* claims (heterogeneous-key splits) take precedence over the flat mapping,
|
|
84
93
|
* mirroring relabelSegments (the matching window contains the segment
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { fallbackSpeakerLabel } from './identify.js';
|
|
2
2
|
import { collapseSplitSpeakers } from './identify.js';
|
|
3
3
|
import { normalizeDecideResult } from './types.js';
|
|
4
|
+
/** Publication rank: provisional ('weak') claims stay internal until a
|
|
5
|
+
* backend upgrades them. */
|
|
6
|
+
const PUBLISH_RANK = { weak: 1, strong: 2 };
|
|
4
7
|
function normalizeOffsetMs(ms) {
|
|
5
8
|
return ms != null && Number.isFinite(ms) ? Math.max(0, Math.round(ms)) : 0;
|
|
6
9
|
}
|
|
@@ -157,7 +160,28 @@ export class SpeakerIdentificationEngine {
|
|
|
157
160
|
this.lastTriggerEndMs = 0;
|
|
158
161
|
}
|
|
159
162
|
get mapping() {
|
|
160
|
-
|
|
163
|
+
const min = this.args.minPublishStrength ?? 'weak';
|
|
164
|
+
if (min === 'weak')
|
|
165
|
+
return this.currentMapping;
|
|
166
|
+
const out = new Map();
|
|
167
|
+
for (const [key, entry] of this.currentMapping) {
|
|
168
|
+
if (PUBLISH_RANK[entry.strength] >= PUBLISH_RANK[min])
|
|
169
|
+
out.set(key, entry);
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/** Whether a claim reaches labels and downstream pushes. */
|
|
174
|
+
isPublishable(entry) {
|
|
175
|
+
if (!entry)
|
|
176
|
+
return false;
|
|
177
|
+
const min = this.args.minPublishStrength ?? 'weak';
|
|
178
|
+
return PUBLISH_RANK[entry.strength] >= PUBLISH_RANK[min];
|
|
179
|
+
}
|
|
180
|
+
/** Published projection of one key (a changed projection fires the push flag). */
|
|
181
|
+
publishedProjection(entry) {
|
|
182
|
+
if (!this.isPublishable(entry))
|
|
183
|
+
return undefined;
|
|
184
|
+
return `${entry.speakerId}|${entry.label}|${entry.strength}|${entry.source ?? ''}`;
|
|
161
185
|
}
|
|
162
186
|
/** Resolve one key at a point in time (evidence timebase, ms): windowed
|
|
163
187
|
* claims (heterogeneous-key splits) take precedence over the flat mapping,
|
|
@@ -171,9 +195,10 @@ export class SpeakerIdentificationEngine {
|
|
|
171
195
|
const mid = (startMs + (endMs ?? startMs)) / 2;
|
|
172
196
|
const windows = this.windowedMapping.get(canonical);
|
|
173
197
|
const hit = windows?.find((w) => (w.startMs ?? Number.NEGATIVE_INFINITY) <= mid && mid <= (w.endMs ?? Number.POSITIVE_INFINITY));
|
|
174
|
-
if (hit)
|
|
198
|
+
if (hit && this.isPublishable(hit))
|
|
175
199
|
return hit;
|
|
176
|
-
|
|
200
|
+
const flat = this.currentMapping.get(canonical);
|
|
201
|
+
return this.isPublishable(flat) ? flat : undefined;
|
|
177
202
|
}
|
|
178
203
|
/** Read-once: returns true when the mapping changed since the previous read. */
|
|
179
204
|
get hasMappingChanged() {
|
|
@@ -290,6 +315,7 @@ export class SpeakerIdentificationEngine {
|
|
|
290
315
|
// runs already established for the same key.
|
|
291
316
|
const existing = this.windowedMapping.get(key) ?? [];
|
|
292
317
|
const merged = [...existing];
|
|
318
|
+
let addedPublished = false;
|
|
293
319
|
for (const incoming of claims) {
|
|
294
320
|
const conflict = merged.find((k) => k.speakerId !== incoming.speakerId &&
|
|
295
321
|
(k.startMs ?? Number.NEGATIVE_INFINITY) < (incoming.endMs ?? Number.POSITIVE_INFINITY) &&
|
|
@@ -301,6 +327,8 @@ export class SpeakerIdentificationEngine {
|
|
|
301
327
|
const idx = merged.indexOf(conflict);
|
|
302
328
|
merged[idx] = incoming;
|
|
303
329
|
this.log(`[engine:${backend.name}] windowed claim REPLACED weaker incumbent for ${key}`);
|
|
330
|
+
if (this.isPublishable(incoming))
|
|
331
|
+
addedPublished = true;
|
|
304
332
|
}
|
|
305
333
|
else {
|
|
306
334
|
this.log(`[engine:${backend.name}] windowed claim REJECTED (overlaps stronger incumbent for ${key})`);
|
|
@@ -308,9 +336,12 @@ export class SpeakerIdentificationEngine {
|
|
|
308
336
|
continue;
|
|
309
337
|
}
|
|
310
338
|
merged.push(incoming);
|
|
339
|
+
if (this.isPublishable(incoming))
|
|
340
|
+
addedPublished = true;
|
|
311
341
|
}
|
|
312
342
|
this.windowedMapping.set(key, merged);
|
|
313
|
-
|
|
343
|
+
if (addedPublished)
|
|
344
|
+
this._hasMappingChanged = true;
|
|
314
345
|
}
|
|
315
346
|
}
|
|
316
347
|
for (const merge of decided.merges ?? []) {
|
|
@@ -330,8 +361,11 @@ export class SpeakerIdentificationEngine {
|
|
|
330
361
|
// above (beatsIncumbent) is the only gate.
|
|
331
362
|
const existing = this.currentMapping.get(key);
|
|
332
363
|
const enriched = this.enrich(entry);
|
|
364
|
+
const before = this.publishedProjection(existing);
|
|
333
365
|
this.currentMapping.set(key, { ...enriched, source: backend.name });
|
|
334
|
-
|
|
366
|
+
const after = this.publishedProjection(this.currentMapping.get(key));
|
|
367
|
+
if (before !== after)
|
|
368
|
+
this._hasMappingChanged = true;
|
|
335
369
|
this.log(`[engine:${backend.name}] ${key} → ${enriched.label} [${enriched.strength}]${enriched.evidence ? ` (${enriched.evidence})` : ''}${existing ? ' [remapped]' : ''}`);
|
|
336
370
|
}
|
|
337
371
|
}
|
|
@@ -445,13 +479,22 @@ export class SpeakerIdentificationEngine {
|
|
|
445
479
|
// A claim held on the loser moves to the canonical key (per-key
|
|
446
480
|
// arbitration still applies: it must beat the canonical incumbent).
|
|
447
481
|
const loserClaim = this.currentMapping.get(loser);
|
|
482
|
+
const before = [
|
|
483
|
+
this.publishedProjection(this.currentMapping.get(canonical)),
|
|
484
|
+
this.publishedProjection(loserClaim),
|
|
485
|
+
];
|
|
448
486
|
if (loserClaim) {
|
|
449
487
|
this.currentMapping.delete(loser);
|
|
450
488
|
if (this.beatsIncumbent(canonical, loserClaim)) {
|
|
451
489
|
this.currentMapping.set(canonical, loserClaim);
|
|
452
490
|
}
|
|
453
491
|
}
|
|
454
|
-
|
|
492
|
+
const after = [
|
|
493
|
+
this.publishedProjection(this.currentMapping.get(canonical)),
|
|
494
|
+
this.publishedProjection(this.currentMapping.get(loser)),
|
|
495
|
+
];
|
|
496
|
+
if (before.join() !== after.join())
|
|
497
|
+
this._hasMappingChanged = true;
|
|
455
498
|
this.log(`[engine:${backendName}] merge ${source} → ${canonical}${merge.kind ? ` [${merge.kind}]` : ''}${merge.evidence ? ` (${merge.evidence})` : ''}`);
|
|
456
499
|
}
|
|
457
500
|
/** Sole arbitration rule (per-key only — many-to-one allowed across keys):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SpeakerIdentificationEngine, type IdentificationContext } from './engine.js';
|
|
2
2
|
import type { MaybePromise } from './types.js';
|
|
3
|
+
import type { ClaimStrength } from './types.js';
|
|
3
4
|
import { type DecisionBackendKind } from './backends/selection.js';
|
|
4
5
|
import type { DeterministicBackendOptions } from './backends/deterministic.js';
|
|
5
6
|
import type { LlmBackendOptions } from './backends/llm.js';
|
|
@@ -16,6 +17,10 @@ export type CreateSpeakerIdentifierArgs = {
|
|
|
16
17
|
* No default patterns: business layers inject their locale pack
|
|
17
18
|
* (or leave it unset to disable floor-announcement matching). */
|
|
18
19
|
deterministic?: DeterministicBackendOptions;
|
|
20
|
+
/** Minimum claim strength exposed to labels and downstream pushes.
|
|
21
|
+
* Below-threshold claims stay internal (later runs see them via
|
|
22
|
+
* knownMapping and can upgrade them). Default 'weak' (no filtering). */
|
|
23
|
+
minPublishStrength?: ClaimStrength;
|
|
19
24
|
/** Decision layer(s). Default 'cascade' (deterministic → llm); 'deterministic' keeps tests offline. */
|
|
20
25
|
backendKind?: DecisionBackendKind;
|
|
21
26
|
debug?: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IdentificationContext } from '../../core/identification/engine.js';
|
|
2
2
|
import type { SpeakerIdentificationEngine } from '../../core/identification/engine.js';
|
|
3
3
|
import type { MaybePromise } from '../../core/identification/types.js';
|
|
4
|
+
import type { ClaimStrength } from '../../core/identification/types.js';
|
|
4
5
|
import type { DecisionBackendKind } from '../../core/identification/backends/selection.js';
|
|
5
6
|
import type { AgendaChapter } from '../nvs/types.js';
|
|
6
7
|
export type CreateSpeakerIdentifierArgs = {
|
|
@@ -18,6 +19,10 @@ export type CreateSpeakerIdentifierArgs = {
|
|
|
18
19
|
timeOffsetMs?: number;
|
|
19
20
|
/** LLM transcript selection: 'last' (rolling live) or 'all' (batch chunks). Default 'last'. */
|
|
20
21
|
llmChunkMode?: 'last' | 'all';
|
|
22
|
+
/** Minimum claim strength exposed to labels and downstream pushes.
|
|
23
|
+
* Below-threshold claims stay internal (later runs see them and can
|
|
24
|
+
* upgrade them). Default 'weak' (no filtering). */
|
|
25
|
+
minPublishStrength?: ClaimStrength;
|
|
21
26
|
/** Decision layer(s). Default 'cascade' (deterministic → llm); 'deterministic' keeps tests offline. */
|
|
22
27
|
backendKind?: DecisionBackendKind;
|
|
23
28
|
debug?: boolean;
|
|
@@ -65,6 +65,7 @@ export function createSpeakerIdentifier(args) {
|
|
|
65
65
|
triggerMs: args.triggerMs,
|
|
66
66
|
timeOffsetMs: args.timeOffsetMs,
|
|
67
67
|
backendKind: args.backendKind,
|
|
68
|
+
minPublishStrength: args.minPublishStrength,
|
|
68
69
|
llm: {
|
|
69
70
|
systemPrompt: parliamentSystemPromptFr,
|
|
70
71
|
buildUserPrompt: buildParliamentUserPrompt,
|
|
@@ -4,6 +4,6 @@ import type { LlmPromptParts } from '../../core/identification/backends/prompt.j
|
|
|
4
4
|
* Injected by the parliament adapter via `LlmBackendOptions`; the core
|
|
5
5
|
* default stays domain-agnostic.
|
|
6
6
|
*/
|
|
7
|
-
export declare const parliamentSystemPromptFr = "Tu analyses la transcription en direct d'une s\u00E9ance parlementaire fran\u00E7aise.\nLes locuteurs sont \u00E9tiquet\u00E9s A, B, C... par un syst\u00E8me de diarisation automatique.\n\nTa t\u00E2che : d\u00E9terminer quel participant de la liste officielle correspond \u00E0 chaque \u00E9tiquette de locuteur, en utilisant le texte transcrit comme preuve.\n\nR\u00C8GLES IMPORTANTES:\n1. Si une liste officielle est fournie, attribue EN PRIORIT\u00C9 des identifiants de cette liste : si la personne y figure, copie son \"id\" EXACTEMENT depuis la liste.\nN'utilise un NOM LIBRE (champ \"name\", avec \"id\": null) que si l'intervenant est identifiable avec certitude mais ABSENT de la liste (NVS partiel : intervenant annonc\u00E9 \u00E0 l'ordre du jour ou nomm\u00E9 dans la transcription mais non list\u00E9). Un nom libre doit \u00EAtre attest\u00E9 mot pour mot (transcription, ordre du jour) et appuy\u00E9 par une preuve citable : JAMAIS un nom devin\u00E9 ou d\u00E9duit indirectement.\nSi la liste officielle est VIDE ou ABSENTE (d\u00E9but de s\u00E9ance, NVS non encore renseign\u00E9), attribue directement le NOM R\u00C9EL de l'intervenant (champ \"name\", avec \"id\": null), d\u00E9duit de la transcription et de l'ordre du jour (octroi de parole par nom, ouverture de s\u00E9ance, auto-pr\u00E9sentation) \u2014 avec la m\u00EAme exigence de certitude absolue.\n\n2. Toute identification doit reposer sur une PREUVE CITABLE : recopie dans \"evidence\" l'indice exact entendu ou lu (octroi de parole, ouverture de s\u00E9ance, th\u00E8me + ordre du jour). Une r\u00E9ponse sans preuve v\u00E9rifiable sera rejet\u00E9e. En cas de doute, NE RENDS RIEN pour ce locuteur : un orateur laiss\u00E9 sans identit\u00E9 est acceptable, une identit\u00E9 erron\u00E9e ne l'est pas.\n\n2 bis. Les listes officielles contiennent parfois des ENTR\u00C9ES DE R\u00D4LE G\u00C9N\u00C9RIQUE (nom \u00AB pr\u00E9sident de s\u00E9ance \u00BB, \u00AB rapporteur \u00BB, \u00AB intervenant \u00BB, sans nom de personne). N'attribue JAMAIS l'identifiant d'une telle entr\u00E9e : c'est un r\u00F4le, pas une personne. Si la liste contient aussi une personne r\u00E9elle qui peut l'assumer (m\u00EAme fonction, m\u00EAme intervention), r\u00E9sous le r\u00F4le vers cette personne r\u00E9elle avec sa preuve citable ; sinon abstains-toi. De m\u00EAme, un locuteur provisoirement \u00E9tiquet\u00E9 par un r\u00F4le g\u00E9n\u00E9rique doit \u00EAtre r\u00E9-identifi\u00E9 vers la personne r\u00E9elle d\u00E8s que le texte le permet (ouverture de s\u00E9ance, adresses \u00AB Madame la pr\u00E9sidente / Monsieur le pr\u00E9sident \u00BB, octroi de parole par un nom coh\u00E9rent avec la fonction).\n\n3. La diarisation peut couper un m\u00EAme orateur en plusieurs \u00E9tiquettes (ex. A et C sont la m\u00EAme personne). Plusieurs cl\u00E9s PEUVENT donc pointer vers le m\u00EAme identifiant : mappe chaque \u00E9tiquette ind\u00E9pendamment.\n\n4. Inversement, une M\u00CAME \u00E9tiquette peut couvrir PLUSIEURS personnes (la diarisation regroupe parfois deux voix proches, typiquement en questions au gouvernement). Si le texte montre deux prises de parole distinctes sous la m\u00EAme cl\u00E9 (annonces diff\u00E9rentes, sujets disjoints), rends une entr\u00E9e FEN\u00CATR\u00C9E par intervalle (champ \"claims\") au lieu d'une identit\u00E9 unique.\n\n5. Abstention explicite : pour un intervalle que tu ne peux pas identifier, rends une entr\u00E9e \"claims\" avec \"confidence\": \"abstain\" et \"id\": null. Cela vaut mieux qu'un nom devin\u00E9.\n\n6. Fusions : une section \"Paires suspectes\" peut lister des cl\u00E9s adjacentes possiblement issues du m\u00EAme tour de parole (ex. \"B \u2192 A ?\"). Si et seulement si le texte prouve manifestement la continuit\u00E9 (phrase coup\u00E9e, m\u00EAme annonce, m\u00EAme orateur poursuivant son propos), propose une fusion. Ne propose JAMAIS de fusion hors de cette liste. En cas de doute, ne propose rien.\n\nTypes de preuves \u00E0 rechercher dans la transcription :\n- Octroi de parole : \"La parole est \u00E0 M./Mme [NOM]\", \"Je donne/c\u00E8de/passe la parole \u00E0...\", \"[NOM], vous avez la parole\"\n- Octroi de parole par R\u00D4LE : \"La parole est \u00E0 M. le rapporteur\", \"M. le pr\u00E9sident, vous avez la parole\", \"j'appelle Mme la rapporteure\" \u2192 r\u00E9sous le r\u00F4le via les \"R\u00F4les\" de la liste officielle des participants. Un seul intervenant porte ce r\u00F4le \u2192 confidence \"high\" ; plusieurs candidats \u2192 confidence \"low\" seulement si un autre indice (ordre du jour, contexte) les d\u00E9partage, sinon abstention. Attention aux variantes entendues (rapporteure, la pr\u00E9sidente, pr\u00E9sident de la commission vs pr\u00E9sident de s\u00E9ance).\n- Auto-r\u00E9f\u00E9rence de r\u00F4le : \"En tant que rapporteur/rapporteure...\", \"En ma qualit\u00E9 de...\"\n- Adresse au pr\u00E9sident : \"Madame/Monsieur la/le pr\u00E9sident(e)...\" \u2192 permet d'identifier le pr\u00E9sident par d\u00E9duction\n- Ouverture de s\u00E9ance : \"La s\u00E9ance est ouverte\", \"L'ordre du jour appelle...\" \u2192 ce locuteur est le/la pr\u00E9sident(e)\n- Contexte de la s\u00E9ance : le titre peut confirmer l'identit\u00E9 d'un auditeur\n- Ordre du jour : si une s\u00E9quence d'intervention pr\u00E9vue est fournie, utilise-la comme signal d'appui pour confirmer l'identit\u00E9 d'un locuteur lorsque la s\u00E9quence de prise de parole correspond. Attention : des interventions hors-ordre sont possibles, ne l'utilise jamais seul sans autre indice.\n\nR\u00E9ponds UNIQUEMENT avec ce type de JSON :\n{\n \"claims\": [\n { \"key\": \"A\", \"from\": \"00:01:23\", \"to\": \"00:03:10\", \"id\": 18960, \"confidence\": \"high\", \"kind\": \"ouverture de s\u00E9ance\", \"evidence\": \"le locuteur d\u00E9clare la s\u00E9ance ouverte\" },\n { \"key\": \"H\", \"from\": \"00:56:32\", \"to\": \"00:58:25\", \"id\": 7, \"confidence\": \"high\", \"kind\": \"octroi de parole\", \"evidence\": \"le pr\u00E9sident annonce \u00AB la parole est \u00E0 monsieur X \u00BB juste avant\" },\n { \"key\": \"B\", \"from\": \"01:12:00\", \"to\": \"01:13:40\", \"id\": null, \"confidence\": \"abstain\", \"kind\": \"\", \"evidence\": \"\" }\n ],\n \"mapping\": [],\n \"merges\": [\n { \"key\": \"B\", \"into\": \"A\", \"kind\": \"continuit\u00E9 syntaxique\", \"evidence\": \"la phrase de A se poursuit en B\" }\n ]\n}\n- \"key\" : l'\u00E9tiquette EXACTE de la transcription.\n- \"from\"/\"to\" : bornes temporelles du tour de parole, COPI\u00C9ES depuis les horodatages [HH:MM:SS] de la transcription. Utilise \"claims\" quand une cl\u00E9 couvre plusieurs personnes ; sinon une seule entr\u00E9e couvrant toute la cl\u00E9.\n- \"id\" : la valeur \"ID\" de l'intervenant, copi\u00E9e EXACTEMENT depuis la liste ; null avec \"abstain\", ou null avec \"name\": \"Nom attest\u00E9\" quand l'intervenant est absent de la liste ou que la liste est vide/absente.\n- \"confidence\" : \"high\" si la preuve est citable mot pour mot, \"low\" si elle est indirecte (th\u00E8me + ordre du jour), \"abstain\" si aucune.\n- \"mapping\" (format historique, une identit\u00E9 par cl\u00E9 enti\u00E8re) reste accept\u00E9
|
|
7
|
+
export declare const parliamentSystemPromptFr = "Tu analyses la transcription en direct d'une s\u00E9ance parlementaire fran\u00E7aise.\nLes locuteurs sont \u00E9tiquet\u00E9s A, B, C... par un syst\u00E8me de diarisation automatique.\n\nTa t\u00E2che : d\u00E9terminer quel participant de la liste officielle correspond \u00E0 chaque \u00E9tiquette de locuteur, en utilisant le texte transcrit comme preuve.\n\nR\u00C8GLES IMPORTANTES:\n1. Si une liste officielle est fournie, attribue EN PRIORIT\u00C9 des identifiants de cette liste : si la personne y figure, copie son \"id\" EXACTEMENT depuis la liste.\nN'utilise un NOM LIBRE (champ \"name\", avec \"id\": null) que si l'intervenant est identifiable avec certitude mais ABSENT de la liste (NVS partiel : intervenant annonc\u00E9 \u00E0 l'ordre du jour ou nomm\u00E9 dans la transcription mais non list\u00E9). Un nom libre doit \u00EAtre attest\u00E9 mot pour mot (transcription, ordre du jour) et appuy\u00E9 par une preuve citable : JAMAIS un nom devin\u00E9 ou d\u00E9duit indirectement.\nSi la liste officielle est VIDE ou ABSENTE (d\u00E9but de s\u00E9ance, NVS non encore renseign\u00E9), attribue directement le NOM R\u00C9EL de l'intervenant (champ \"name\", avec \"id\": null), d\u00E9duit de la transcription et de l'ordre du jour (octroi de parole par nom, ouverture de s\u00E9ance, auto-pr\u00E9sentation) \u2014 avec la m\u00EAme exigence de certitude absolue.\n\n2. Toute identification doit reposer sur une PREUVE CITABLE : recopie dans \"evidence\" l'indice exact entendu ou lu (octroi de parole, ouverture de s\u00E9ance, th\u00E8me + ordre du jour). Une r\u00E9ponse sans preuve v\u00E9rifiable sera rejet\u00E9e. En cas de doute, NE RENDS RIEN pour ce locuteur : un orateur laiss\u00E9 sans identit\u00E9 est acceptable, une identit\u00E9 erron\u00E9e ne l'est pas.\n\n2 bis. Les listes officielles contiennent parfois des ENTR\u00C9ES DE R\u00D4LE G\u00C9N\u00C9RIQUE (nom \u00AB pr\u00E9sident de s\u00E9ance \u00BB, \u00AB rapporteur \u00BB, \u00AB intervenant \u00BB, sans nom de personne). N'attribue JAMAIS l'identifiant d'une telle entr\u00E9e : c'est un r\u00F4le, pas une personne. Si la liste contient aussi une personne r\u00E9elle qui peut l'assumer (m\u00EAme fonction, m\u00EAme intervention), r\u00E9sous le r\u00F4le vers cette personne r\u00E9elle avec sa preuve citable ; sinon abstains-toi. De m\u00EAme, un locuteur provisoirement \u00E9tiquet\u00E9 par un r\u00F4le g\u00E9n\u00E9rique doit \u00EAtre r\u00E9-identifi\u00E9 vers la personne r\u00E9elle d\u00E8s que le texte le permet (ouverture de s\u00E9ance, adresses \u00AB Madame la pr\u00E9sidente / Monsieur le pr\u00E9sident \u00BB, octroi de parole par un nom coh\u00E9rent avec la fonction).\n\n3. La diarisation peut couper un m\u00EAme orateur en plusieurs \u00E9tiquettes (ex. A et C sont la m\u00EAme personne). Plusieurs cl\u00E9s PEUVENT donc pointer vers le m\u00EAme identifiant : mappe chaque \u00E9tiquette ind\u00E9pendamment.\n\n4. Inversement, une M\u00CAME \u00E9tiquette peut couvrir PLUSIEURS personnes (la diarisation regroupe parfois deux voix proches, typiquement en questions au gouvernement). Si le texte montre deux prises de parole distinctes sous la m\u00EAme cl\u00E9 (annonces diff\u00E9rentes, sujets disjoints), rends une entr\u00E9e FEN\u00CATR\u00C9E par intervalle (champ \"claims\") au lieu d'une identit\u00E9 unique.\n\n4 bis. ATTENTION \u2014 lier une cl\u00E9 ENTI\u00C8RE (section \"mapping\") engage toute la s\u00E9ance pour cette \u00E9tiquette. Un discours d'ouverture ou de bienvenue (\"je vous souhaite la bienvenue\", \"je vous remercie\", annonce des invit\u00E9s) ne prouve que le locuteur au moment o\u00F9 il parle : il ne permet JAMAIS \u00E0 lui seul de lier la cl\u00E9 ENTI\u00C8RE \u2014 la m\u00EAme cl\u00E9 couvre ensuite souvent d'autres intervenants (discours d'ouverture du pr\u00E9sident PUIS expos\u00E9 d'un invit\u00E9). En cas de doute sur l'homog\u00E9n\u00E9it\u00E9 d'une cl\u00E9, rends des entr\u00E9es FEN\u00CATR\u00C9ES (\"claims\") avec des bornes exactes, jamais \"mapping\".\n\n5. Abstention explicite : pour un intervalle que tu ne peux pas identifier, rends une entr\u00E9e \"claims\" avec \"confidence\": \"abstain\" et \"id\": null. Cela vaut mieux qu'un nom devin\u00E9.\n\n6. Fusions : une section \"Paires suspectes\" peut lister des cl\u00E9s adjacentes possiblement issues du m\u00EAme tour de parole (ex. \"B \u2192 A ?\"). Si et seulement si le texte prouve manifestement la continuit\u00E9 (phrase coup\u00E9e, m\u00EAme annonce, m\u00EAme orateur poursuivant son propos), propose une fusion. Ne propose JAMAIS de fusion hors de cette liste. En cas de doute, ne propose rien.\n\nTypes de preuves \u00E0 rechercher dans la transcription :\n- Octroi de parole : \"La parole est \u00E0 M./Mme [NOM]\", \"Je donne/c\u00E8de/passe la parole \u00E0...\", \"[NOM], vous avez la parole\"\n- Octroi de parole par R\u00D4LE : \"La parole est \u00E0 M. le rapporteur\", \"M. le pr\u00E9sident, vous avez la parole\", \"j'appelle Mme la rapporteure\" \u2192 r\u00E9sous le r\u00F4le via les \"R\u00F4les\" de la liste officielle des participants. Un seul intervenant porte ce r\u00F4le \u2192 confidence \"high\" ; plusieurs candidats \u2192 confidence \"low\" seulement si un autre indice (ordre du jour, contexte) les d\u00E9partage, sinon abstention. Attention aux variantes entendues (rapporteure, la pr\u00E9sidente, pr\u00E9sident de la commission vs pr\u00E9sident de s\u00E9ance).\n- Auto-r\u00E9f\u00E9rence de r\u00F4le : \"En tant que rapporteur/rapporteure...\", \"En ma qualit\u00E9 de...\"\n- Adresse au pr\u00E9sident : \"Madame/Monsieur la/le pr\u00E9sident(e)...\" \u2192 permet d'identifier le pr\u00E9sident par d\u00E9duction\n- Ouverture de s\u00E9ance : \"La s\u00E9ance est ouverte\", \"L'ordre du jour appelle...\" \u2192 ce locuteur est le/la pr\u00E9sident(e)\n- Contexte de la s\u00E9ance : le titre peut confirmer l'identit\u00E9 d'un auditeur\n- Ordre du jour : si une s\u00E9quence d'intervention pr\u00E9vue est fournie, utilise-la comme signal d'appui pour confirmer l'identit\u00E9 d'un locuteur lorsque la s\u00E9quence de prise de parole correspond. Attention : des interventions hors-ordre sont possibles, ne l'utilise jamais seul sans autre indice.\n\nR\u00E9ponds UNIQUEMENT avec ce type de JSON :\n{\n \"claims\": [\n { \"key\": \"A\", \"from\": \"00:01:23\", \"to\": \"00:03:10\", \"id\": 18960, \"confidence\": \"high\", \"kind\": \"ouverture de s\u00E9ance\", \"evidence\": \"le locuteur d\u00E9clare la s\u00E9ance ouverte\" },\n { \"key\": \"H\", \"from\": \"00:56:32\", \"to\": \"00:58:25\", \"id\": 7, \"confidence\": \"high\", \"kind\": \"octroi de parole\", \"evidence\": \"le pr\u00E9sident annonce \u00AB la parole est \u00E0 monsieur X \u00BB juste avant\" },\n { \"key\": \"B\", \"from\": \"01:12:00\", \"to\": \"01:13:40\", \"id\": null, \"confidence\": \"abstain\", \"kind\": \"\", \"evidence\": \"\" }\n ],\n \"mapping\": [],\n \"merges\": [\n { \"key\": \"B\", \"into\": \"A\", \"kind\": \"continuit\u00E9 syntaxique\", \"evidence\": \"la phrase de A se poursuit en B\" }\n ]\n}\n- \"key\" : l'\u00E9tiquette EXACTE de la transcription.\n- \"from\"/\"to\" : bornes temporelles du tour de parole, COPI\u00C9ES depuis les horodatages [HH:MM:SS] de la transcription. Utilise \"claims\" quand une cl\u00E9 couvre plusieurs personnes ; sinon une seule entr\u00E9e couvrant toute la cl\u00E9.\n- \"id\" : la valeur \"ID\" de l'intervenant, copi\u00E9e EXACTEMENT depuis la liste ; null avec \"abstain\", ou null avec \"name\": \"Nom attest\u00E9\" quand l'intervenant est absent de la liste ou que la liste est vide/absente.\n- \"confidence\" : \"high\" si la preuve est citable mot pour mot, \"low\" si elle est indirecte (th\u00E8me + ordre du jour), \"abstain\" si aucune.\n- \"mapping\" (format historique, une identit\u00E9 par cl\u00E9 enti\u00E8re) reste accept\u00E9 UNIQUEMENT avec \"confidence\": \"high\" explicite ; sans \"confidence\": \"high\" l'entr\u00E9e est trait\u00E9e comme PROVISOIRE (non publi\u00E9e). Voir r\u00E8gle 4 bis avant de lier une cl\u00E9 enti\u00E8re.\n- Le champ \"kind\" doit nommer le type de preuve utilis\u00E9 (ex. \"octroi de parole\", \"ouverture de s\u00E9ance\", \"auto-r\u00E9f\u00E9rence de r\u00F4le\", \"adresse au pr\u00E9sident\", \"ordre du jour\").";
|
|
8
8
|
/** Parliament user prompt: "séance / liste officielle / ordre du jour" wording. */
|
|
9
9
|
export declare function buildParliamentUserPrompt(parts: LlmPromptParts): string;
|
|
@@ -21,6 +21,8 @@ Si la liste officielle est VIDE ou ABSENTE (début de séance, NVS non encore re
|
|
|
21
21
|
|
|
22
22
|
4. Inversement, une MÊME étiquette peut couvrir PLUSIEURS personnes (la diarisation regroupe parfois deux voix proches, typiquement en questions au gouvernement). Si le texte montre deux prises de parole distinctes sous la même clé (annonces différentes, sujets disjoints), rends une entrée FENÊTRÉE par intervalle (champ "claims") au lieu d'une identité unique.
|
|
23
23
|
|
|
24
|
+
4 bis. ATTENTION — lier une clé ENTIÈRE (section "mapping") engage toute la séance pour cette étiquette. Un discours d'ouverture ou de bienvenue ("je vous souhaite la bienvenue", "je vous remercie", annonce des invités) ne prouve que le locuteur au moment où il parle : il ne permet JAMAIS à lui seul de lier la clé ENTIÈRE — la même clé couvre ensuite souvent d'autres intervenants (discours d'ouverture du président PUIS exposé d'un invité). En cas de doute sur l'homogénéité d'une clé, rends des entrées FENÊTRÉES ("claims") avec des bornes exactes, jamais "mapping".
|
|
25
|
+
|
|
24
26
|
5. Abstention explicite : pour un intervalle que tu ne peux pas identifier, rends une entrée "claims" avec "confidence": "abstain" et "id": null. Cela vaut mieux qu'un nom deviné.
|
|
25
27
|
|
|
26
28
|
6. Fusions : une section "Paires suspectes" peut lister des clés adjacentes possiblement issues du même tour de parole (ex. "B → A ?"). Si et seulement si le texte prouve manifestement la continuité (phrase coupée, même annonce, même orateur poursuivant son propos), propose une fusion. Ne propose JAMAIS de fusion hors de cette liste. En cas de doute, ne propose rien.
|
|
@@ -50,7 +52,7 @@ Réponds UNIQUEMENT avec ce type de JSON :
|
|
|
50
52
|
- "from"/"to" : bornes temporelles du tour de parole, COPIÉES depuis les horodatages [HH:MM:SS] de la transcription. Utilise "claims" quand une clé couvre plusieurs personnes ; sinon une seule entrée couvrant toute la clé.
|
|
51
53
|
- "id" : la valeur "ID" de l'intervenant, copiée EXACTEMENT depuis la liste ; null avec "abstain", ou null avec "name": "Nom attesté" quand l'intervenant est absent de la liste ou que la liste est vide/absente.
|
|
52
54
|
- "confidence" : "high" si la preuve est citable mot pour mot, "low" si elle est indirecte (thème + ordre du jour), "abstain" si aucune.
|
|
53
|
-
- "mapping" (format historique, une identité par clé entière) reste accepté
|
|
55
|
+
- "mapping" (format historique, une identité par clé entière) reste accepté UNIQUEMENT avec "confidence": "high" explicite ; sans "confidence": "high" l'entrée est traitée comme PROVISOIRE (non publiée). Voir règle 4 bis avant de lier une clé entière.
|
|
54
56
|
- Le champ "kind" doit nommer le type de preuve utilisé (ex. "octroi de parole", "ouverture de séance", "auto-référence de rôle", "adresse au président", "ordre du jour").`;
|
|
55
57
|
/** Parliament user prompt: "séance / liste officielle / ordre du jour" wording. */
|
|
56
58
|
export function buildParliamentUserPrompt(parts) {
|
|
@@ -77,6 +79,6 @@ Réponds uniquement avec ce type de JSON :
|
|
|
77
79
|
- "from"/"to" : bornes COPIÉES depuis les horodatages [HH:MM:SS] de la transcription.
|
|
78
80
|
- "id" : la valeur "ID" de l'intervenant, copiée EXACTEMENT depuis la liste ; null avec "abstain", ou null avec "name": "Nom attesté" quand l'intervenant est absent de la liste ou que la liste est vide/absente.
|
|
79
81
|
- "confidence" : "high" si la preuve est citable mot pour mot, "low" si elle est indirecte, "abstain" si aucune.
|
|
80
|
-
- "mapping" (format historique) reste
|
|
82
|
+
- "mapping" (format historique) reste accepté UNIQUEMENT avec "confidence": "high" explicite ; sans "confidence": "high" l'entrée est traitée comme PROVISOIRE (non publiée).
|
|
81
83
|
- Le champ "kind" doit nommer le type de preuve utilisé (ex. "octroi de parole", "ouverture de séance", "auto-référence de rôle", "ordre du jour").`;
|
|
82
84
|
}
|
|
@@ -149,6 +149,7 @@ export async function runForeverPush(args) {
|
|
|
149
149
|
finalPlayer: () => fetchText(livePlayerNvsUrl, { headers: PARLIAMENT_HEADERS }),
|
|
150
150
|
sessionTitle,
|
|
151
151
|
triggerMs: 30_000,
|
|
152
|
+
minPublishStrength: ENV.IDENTIFICATION_MIN_PUBLISH_STRENGTH,
|
|
152
153
|
debug,
|
|
153
154
|
})
|
|
154
155
|
: null;
|
package/package.json
CHANGED