@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.
@@ -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;
@@ -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
- * Absent confidence keeps the historical trust rule (→ 'strong'). `id` is
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): confidence
270
- // absent keeps the historical trust rule (strong) for roster ids.
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 === 'low'
308
- ? 'weak'
309
- : 'strong';
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
- return this.currentMapping;
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
- return this.currentMapping.get(canonical);
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
- this._hasMappingChanged = true;
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
- this._hasMappingChanged = true;
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
- this._hasMappingChanged = true;
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;
@@ -26,6 +26,7 @@ export function createSpeakerIdentifier(args) {
26
26
  fetchContext: async () => args.fetchContext(),
27
27
  triggerMs: args.triggerMs,
28
28
  timeOffsetMs: args.timeOffsetMs,
29
+ minPublishStrength: args.minPublishStrength,
29
30
  debug,
30
31
  });
31
32
  }
@@ -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 si tu pr\u00E9f\u00E8res ne pas segmenter.\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\").";
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é si tu préfères ne pas segmenter.
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 accepté.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tricoteuses/transcription-videos",
3
- "version": "1.7.2",
3
+ "version": "1.7.3",
4
4
  "description": "Permet d'obtenir la transcription des vidéos de l'assemblée/sénat en fournissant un lien vidéo m3u8 en entrée",
5
5
  "keywords": [
6
6
  "Assemblée nationale",