Skip to content

feat(genai,#19692): phase 1 -- routage narrateur independant du moteur + CosyVoice3 decoupe (drapeau, defaut inchange) + controle d'omission p7 - #19699

Merged
myia-ai-01 merged 10 commits into
mainfrom
feature/19692-narrator-pivot-phase1
Oct 7, 2026
Merged

myia-ai-01 merged 10 commits into
mainfrom
feature/19692-narrator-pivot-phase1

Conversation

@jsboige

@jsboige jsboige commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Grain: DEEP/genai — lane myia-po-2023:CoursIA — prev: MED/tooling #19618

See #19692 — phase 1 (cadrage coordinateur c.6034724293 : routage indépendant du moteur, CosyVoice3 découpé derrière un drapeau sans changement du défaut, contrôle d'omission p7, run complet mesuré ; pas d'audio.cpp). Claim posé par ai-01 au dispatch (c. du 07/10 09:08Z, paths: p5_tts, p7_verify, tests/**).

Ce que la PR livre

Livrable Fichier
Routage narrateur indépendant du moteur — _should_route_narrator_to(engine, seg) + _selected_narrator_engine() (acceptance #4) p5_tts.py
CosyVoice3 découpé par phrase derrière NARRATOR_COSYVOICE3_ROUTING (défaut 0, inchangé) p5_tts.py
Découpeur du banc A0C (#17586) : ≤280 c, frontières de phrase, fallback proposition, jamais au milieu d'un mot, pause 0,25 s, identité vérifiée par assert (le découpage ne peut pas perdre de texte) p5_tts.py
Contrôle d'omission p7 : alignement trigramme source/ASR, spans ≥3 mots, vote 2/3 ASR {tiny, large-v3, large-v3-turbo}, comptes dans quality_report.json + détail dans outputs/omission_report.json p7_verify.py
Correctif cache : le cache narrateur devient engine-aware (sentinel reference_id) — sans lui, flipper le drapeau servirait silencieusement les MP3 Qwen périmés p5_tts.py
Cause racine du rendu dégénéré fermée : masquage des trois tokens d'arrêt CosyVoice3 sur tout le préfixe min_len (voir ci-dessous) p5_tts.py
Cause racine n° 2 fermée — troncature héritée : le plafond S2-Pro de 500 caractères de _compose_tts_text s'appliquait à tous les moteurs ; le narrateur CV3 (découpé par phrase, sans plafond d'entrée) perdait la queue de chaque narration longue (seg 1 : 981 → ~440 caractères rendus). La troncation épargne désormais le narrateur routé CV3 ; défaut Qwen/FishAudio inchangé p5_tts.py
Cause racine n° 3 fermée — collage de mots : _strip_brackets_for_qwen avalait un tag et l'espace qui suit (« route,[short pause] quand » → « route,quand »). CosyVoice3 nourri d'un run-on collé omet tout ce qui précède le point de collage (seg 57 : l'audio démarre à « quand », la première proposition n'est jamais prononcée ; même signature segs 19/38/76 ; 16/20 segments échantillonnés porteurs d'omissions au vote 3/3 ASR). Le tag devient une espace p5_tts.py
Sentinel cache narrateur CV3 → -v3 : le fix de collage change l'entrée moteur après strip, donc le hash du texte composé (l'autre clé de cache) ne le voit pas — seul le bump du reference_id re-rend les 270 narrateurs p5_tts.py

Cause racine du rendu dégénéré, fermée à la source

Le décodeur non-vLLM de CosyVoice3 s'arrête sur n'importe lequel de trois tokens (stop_token_ids = [speech_token_size + i for i in range(3)]), mais n'en masque qu'un dans le préfixe protégé : weighted_scores[self.speech_token_size], qui est stop_token_ids[0]. Les deux autres restent échantillonnables sous le plancher min_len, le plancher fuit, et le rendu s'arrête après un token de parole — 1/25 s = 0,04 s, le token_hop_len de CosyVoice. min_len n'est jamais nul (le décodeur concatène prompt_text et rajoute sa longueur, donc min_len == 2 × tokens(tts_text)), donc ni « texte court » ni « seed malchanceuse » n'expliquent le défaut : il est structurel.

La branche vLLM n'a pas ce trou (SamplingParams(min_tokens=min_len) couvre tous les ids). Le correctif restaure sur la branche exécutée la forme que l'autre applique déjà — sampler patché sur l'objet chargé (organ-first, #17586), arbre amont inchangé.

Preuve sur le modèle réel (les 9 segments narrateur livrés à 0,10-0,14 s) : 9/9 rendus, 9/9 au premier essai, 0,74-4,26 s, 13,4-21,0 car/s — le centre de la distribution légitime (médiane 20,1). Les 3 que la garde de durée seule n'avait pas su récupérer (seg 93, 178, 383) passent du premier coup. Deux pièges mesurés fermés dans le même geste : le décodeur vit à model.model.llm (pas model.llm — une première version a levé sur 9/9, un run GPU entier pour l'apprendre), et un refus de patch est caché au lieu de recharger le modèle (~16 s) à chaque segment.

La garde de durée reste en seconde ligne, et sa justification est corrigée par la mesure : le plancher (40 car/s) tombe dans une bande vide du corpus (légitime le plus rapide 38,9 ; défectueux le plus lent 43,1 ; dégénérés francs 99-560), et l'ASR confirme la bande 43-69 (8/8 segments y sont défectueux, pas « rapides »). C'est un garde-fou grossier, pas l'organe de fidélité — l'organe de fidélité est le contrôle d'omission p7 que la gate lit.

Pourquoi le vote 2/3 dans p7

Le primitif trigramme a un halo : un mot substitué casse tous les trigrammes qui le touchent et produit un span ~5 mots autour d'une seule erreur d'ASR. Sur le banc A0C (#17586, c.6035598289), c'est le vote 2/3 qui séparait les omissions réelles (absentes sous les 3 ASRs) du bruit d'ASR (présent sous 2/3). p7 interroge donc trois modèles par segment échantillonné ; le WER garde sa convention large-v3-turbo seule (métrique inchangée). Le halo est épinglé par un test (test_single_word_substitution_produces_halo_span) pour que le rôle du vote reste visible.

Choix d'implémentation

  • Organ-first : chargement du modèle + constantes de prompt zero-shot réutilisés du client bakeoff prosody_lab/bakeoff_large/clients/cosyvoice3.py (mesuré [Audiobook #1028] Issue de recette UAT — Association Bibliothèques Sonores #17586) ; seule l'orchestration par chunk (graine seed+i, pause, concat, WAV→MP3 192 kbps comme le chemin Qwen) vit dans p5. Modèle chargé une fois par processus (mesuré : 3,29 Go de VRAM au pic, 13,9 s de chargement).
  • Échec dur, pas de fallback silencieux : NarratorCosyVoice3Unavailable (RuntimeError), même contrat que NarratorQwenUnavailable ([Audiobook #1028] Router le narrateur v4 vers Qwen VoiceDesign et prouver le gain mélodique #15002 acceptance 6).
  • Drapeaux exclusifs : NARRATOR_QWEN_ROUTING=1 ET NARRATOR_COSYVOICE3_ROUTING=1 → ValueError de configuration, jamais une précédence silencieuse.
  • Le défaut ne change pas : sans variable d'env, le narrateur reste sur Qwen VoiceDesign (tests existants inchangés et verts).

Tests — 171 passed (suite v4 entière, python -m pytest v4/tests -q)

  • test_p5_narrator_routing.py étendu : prédicat par moteur, non-narrateur jamais routé quel que soit le moteur, CV3 off par défaut, sélection exclusive, ValueError sur double drapeau, None quand tout est off.
  • test_p5_cv3_narrator.py : identité du découpage, ≤280 c, frontières de phrase, fallback proposition, refus des clauses indivisibles (jamais de coupe au milieu d'un mot), sentinel CV3 ≠ Qwen, garde d'entrée vide avant tout import torch (hermétique), cache narrateur engine-aware (Qwen→CV3 invalide, CV3→CV3 servi, dialogue insensible), re-roll d'un rendu dégénéré (graines espacées de _CV3_RETRY_SEED_STRIDE), échec dur quand tous les essais dégénèrent, masquage des trois tokens d'arrêt + idempotence + leçon du décodeur déplacé (l'échec nomme model.llm et #19692) + refus de patch caché + plancher dans la bande vide mesurée.
  • test_p7_omission_control.py : zéro span sur hypothèse fidèle, phrase entière tombée = 1 span, halo épinglé, normalisation cas/ponctuation, dérive d'inflexion documentée comme faux positif connu, vote 2/3 (bruit mono-ASR filtré, omission réelle gardée avec ses modèles, halos décalés clusterisés).

Run complet mesuré — VERDICT : porte la mesure, ne passe PAS la gate #17586

Config : NARRATOR_QWEN_ROUTING=0 NARRATOR_COSYVOICE3_ROUTING=1, RTX 3090, passe 3 (code be7aa641d5 + sentinel -v3 bf8bcf0dc5, re-rendu intégral des 270 narrateurs — le hash du texte composé ne voyait pas le fix de strip, seul le sentinel force le re-rendu).

Avant / après (instrument corpus_damage.py, plancher 40 car/s, même organe des deux côtés)

Métrique Avant (pré-patch) Après (passe 3)
Narrateurs sous le plancher 20/270 3/270
dont courts (< 2,5 s) 16 3 (segs 50/174/157)
dont tronqués longs 4 0
Bande légitime car/s (p05/médiane/max) 15,3 / 20,1 / 38,9 15,1 / 20,0 / 37,3

Les 3 résiduels sous le plancher sont ASR-propres (aucun span d'omission au vote 3/3) : parole rapide, pas du dégénéré — le plancher reste le garde grossier, l'organe de fidélité est p7.

p7 — WER et contrôle d'omission (vote ≥ 2/3 ASR, 20 segments échantillonnés)

Métrique Passe 2 (avant fix collage) Passe 3 (après) Gate #17586
WER moyen échantillon 0,253 0,235 ≤ 0,15 requis
omission_spans_3plus 31 24 0 requis
omission_words_3plus 180 145 —
Segments porteurs 16/20 14/20 —

Verdict : NON CONFORME — CV3 ne passe pas la gate de fidélité. Les trois correctifs de cause racine (plancher de tokens fea3688bf7, troncature 500 car c204fe3ea2, collage de tags be7aa641d5) ont chacun été validés par la mesure (20 → 3 sous plancher ; 31 → 24 spans), mais un résiduel moteur subsiste.

Le résiduel : onset-drop intrinsèque au moteur, prouvé par expérience

  • Texte propre en, queue seule dehors : seg 57 — le journal montre le texte complet corrigé envoyé au moteur (« Chacun guettait … la route, quand la diligence … »), l'ASR horodaté (large-v3, granularité mot) montre la parole démarrer à t = 0,0 s sur « Quand » : aucune coupe de montage, c'est le moteur qui ne dit pas le début.
  • Isolation par clause : la 2e clause seule (98 car) est rendue parfaitement ; la 1re clause seule (56 car) bafouille son attaque (« TQ, non, non, … ») et répète trois fois la fin — 12,6 s pour 56 caractères, un degénéré que le plancher de durée ne peut pas voir car il est trop long.
  • Dépendance à la graine : graines 4243/4244/4245 disent toutes l'attaque (« Chacun… », avec « guettait » écorché + artefact final). L'attaque loupée n'est donc pas déterministe — mais aucun signal de durée ne la détecte : les bandes car/s apparentes se recouvrent intégralement (segments propres jusqu'à 44,6 ; segments omettant de 16 à 35). Seul un contrôle de contenu (ASR) peut l'attraper — c'est le rôle de l'organe p7, qui l'attrape.
  • Cohérence bakeoff [Audiobook #1028] Issue de recette UAT — Association Bibliothèques Sonores #17586 : c'est exactement le défaut de fidélité qui avait fait refuser CV3 au banc (2 segments omis sur 252 mots). La phase 1 a corrigé ce qui était corrigeable côté pipeline (3 causes racines) et laissé au moteur ce qui est au moteur.

RTF agrégé (270 rendus)

Médiane 1,10 · p95 1,68 · max 4,85 · re-rolls 2 · 0 FAILED (avant le garde : seg 309 crashait dur). Histogramme d'essais : 1→207, 2→52, 3→6, 4→2, 5→3.

Résiduels rapportés tels quels

  • seg 0 (titre « BOULE DE SUIF », 13 caractères majuscules) : 0,88 s que l'ASR lit comme du japonais — attrapé par le vote p7, pas par le plancher.
  • Service de diarization (port 7860) arrêté — n'affecte que le champ Speakers, pas le WER.
  • Piste phase 2 ouverte par la mesure : garde d'attaque par chunk (ASR turbo par chunk + re-roll ciblé) — l'attaque loupée étant graines-dépendante, un re-roll informé par le contenu la récupérerait ; hors périmètre phase 1 (la gate décide, elle n'est pas contournée).

🤖 Generated with Claude Code

…r, CosyVoice3 decoupe derriere drapeau, controle d'omission p7

- p5: _should_route_narrator_to(engine, seg) remplace la predicat Qwen-only
  (#19692 acceptance 4); CosyVoice3 chunked par phrase derriere
  NARRATOR_COSYVOICE3_ROUTING (defaut 0, inchange), decoupeur banc A0C
  (#17586: <=280 c, frontieres de phrase, fallback proposition, pause 0,25 s,
  identite verifiee), modele charge une fois par processus via le client
  bakeoff (organ-first), echec dur NarratorCosyVoice3Unavailable sans
  fallback silencieux (#15002 acceptance 6)
- p5: cache narrateur engine-aware -- un MP3 d'un moteur ne se sert jamais
  pour un autre (sentinel reference_id), sinon le flip de drapeau servirait
  l'audio Qwen perime
- p7: controle d'omission -- alignement trigramme source/ASR, spans >=3 mots,
  vote 2/3 sur {tiny, large-v3, large-v3-turbo} (methode mesuree banc A0C,
  c.6035598289), comptes dans quality_report + detail omission_report.json
- tests: 159 passed (suite v4 entiere), 3 fichiers (routage etendu, chunker
  CV3, omission p7 avec vote)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-po-2023:CoursIA` voit ces signaux actifs sur les mergees du jour (UTC 2026-10-07) :

G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

@github-actions github-actions Bot added the pr-gate-missing PR gate absent du rollup: contexte requis jamais rapporte, PR bloquee, checks verts (#10928) label Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

PR gate absent du rollup (advisory, #10928)

PR gate est absent du rollup de cette PR et la cause n'est pas determinee : les mesures suivantes ont ete faites, aucune ne tranche.

  • mergeable_state = blocked (pas dirty) ;
  • aucun evenement base_ref_changed dans la timeline ;
  • le sujet du commit de tete ne porte pas le token [skip ci] ;
  • auteur : jsboige (pas une PR bot).

Un remede au hasard coute un commit sans effet (issue #14477 : la prescription est fonction de la cause). Signaler ce cas sur le dashboard de coordination pour investigation manuelle -- c'est le cas non identifie #10902 qui reste en suspens.

Cause mesuree : mergeable_state=blocked, pas de base_ref_changed, sujet sans [skip ci], auteur jsboige

jsboige and others added 3 commits October 7, 2026 12:22
…'exception d'origine

TTSResult(speaker='') viole le Literal du schema : le handler d'echec
crashe en ValidationError et tue tout le run au premier echec de
synthese, sans traceback utilisable. Le chemin n'avait jamais ete
exerce avant le routage CV3 (premier echec de synthese du pipeline).
Surfacer la cause (logger + print), puis construire l'enregistrement
d'echec avec le speaker REEL du segment.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…/__init__.py est du markdown, pas du Python

prosody_lab/bakeoff_large/__init__.py contient de la prose de
documentation sous un nom de module : tout import de paquet
(prosody_lab.bakeoff_large.clients...) meurt en SyntaxError
'unterminated string literal (line 3)'. Le banc A0C (#17586)
esquivait deja ce fichier en important clients.cosyvoice3 via
sys.path ; p5 fait pareil en plus propre : importlib sur le fichier
client direct, helper unique _get_cv3_client() mis en cache.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…_cv3_client

Run 4 traceback pinned it: _synthesize_narrator_cosyvoice3 still carried
'from .prosody_lab.bakeoff_large.clients import cosyvoice3' AFTER the
importlib loader refactor -- the package import traverses
bakeoff_large/__init__.py (markdown prose, SyntaxError line 3) and
crashed all 270 narrator segments. cv3_client now comes exclusively
from _get_cv3_client() which loads the client file directly.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the pr-gate-missing PR gate absent du rollup: contexte requis jamais rapporte, PR bloquee, checks verts (#10928) label Oct 7, 2026
jsboige and others added 5 commits October 7, 2026 13:36
…st plus livre en silence

Le modele peut revenir de `inference_zero_shot` en n'ayant emis qu'UN seul
token de parole : l'appelant recoit un WAV de exactement 1/25 s -- le
`token_hop_len` de CosyVoice -- avec `status="generated"` et aucun signal
d'erreur. Mesure sur la sortie du run 5 : 9 segments narrateur livres a
0,10-0,14 s, soit jusqu'a 560 caracteres/seconde (parole francaise ~15-20),
dont `BOULE DE SUIF` en tete de corpus.

Ce n'est pas une propriete du texte. Sur 8 textes x 6 seeds, un texte de
7 tokens degenere la ou un de 4 reussit, et le point final n'est pas le
declencheur (`C'etait un Bordelais` et sa variante ponctuee degenere
identiquement). La degenerecence est une fonction de (texte, seed) : sur
3 textes en echec x 5 seeds alternatifs, 17 rendus sur 18 reviennent a une
duree plausible pour le meme texte. D'ou un re-roll, et non une
transformation du texte -- aucune regle de decoupage n'aurait pu corriger
cela.

Le crash et le silence sont le MEME evenement : quand le mel produit est plus
court que le premier `conv1d` du predicteur f0, le rendu leve un RuntimeError
au lieu de rendre 0,04 s. Une garde de duree les attrape tous les deux, et le
meme re-roll les resout tous les deux.

Mesures (2026-10-07) :
- plancher : 40 car/s + 0,20 s absolu. Le 5e percentile des 257 segments
  narrateur legitimes est a 15,4 car/s (mediane 20,1) ; les 9 degeneres
  tournaient entre 108 et 560 car/s. Le plancher est donc 2,6x sous le
  segment legitime le plus lent et 2,7x au-dessus du degenere le plus rapide.
- sur le modele reel, les 9 segments connus-mauvais : 6 rendus (dont un des
  l'essai 0), 3 epuises aux 5 tentatives.
- la degenerecence est reproductible au bit pres dans un process donne, donc
  le re-roll rend le run reproductible.
- l'essai 0 conserve le seed d'avant la garde : un chunk sain est
  bit-identique, seul un chunk degenere est perturbe.

Les 3 segments non resolus ne sont PAS maquilles : ils levent
`NarratorCosyVoice3Unavailable` et le run les compte dans `Failed`. Un
`Failed: 3` visible vaut mieux que 9 MP3 muets annonces comme generes.

`_CV3_NARRATOR_REFERENCE_ID` passe a `-v2` : le sentinel indexe le cache
narrateur, et un segment cache n'entre jamais dans la fonction de synthese --
aucune garde placee la ne peut donc voir les MP3 ecrits avant elle. Le bump
est ce qui force leur re-rendu.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e du rendu degenere, pas son symptome

Le decodeur non-vLLM de CosyVoice3 protege ses `min_len` premiers pas contre un
token d'arret premature, mais n'en masque QU'UN sur les trois sur lesquels il
s'arrete :

    self.stop_token_ids = [speech_token_size + i for i in range(3)]
    def sampling_ids(...):
        if ignore_eos is True:
            weighted_scores[self.speech_token_size] = -float('inf')
    top_ids = self.sampling_ids(..., ignore_eos=True if i < min_len else False)
    if top_ids in self.stop_token_ids:   # n'importe lequel des TROIS
        break

`speech_token_size` EST `stop_token_ids[0]` : les ids 1 et 2 restent
echantillonnables dans le prefixe protege, le plancher fuit, et un rendu
s'arrete apres UN token de parole -- 1/25 s = 0,04 s, le `token_hop_len` de
CosyVoice. Ce n'est ni un effet de texte court ni un effet de seed : `min_len`
n'est jamais nul, le decodeur concatenant `prompt_text` et rajoutant sa longueur
(`text_len += prompt_text_len`), donc `min_len == 2 x tokens(tts_text)` -- soit
10 pour "Il demanda:", dont la mesure s'arretait a exactement un token.

Preuve sur le modele reel, sur les 9 segments narrateur livres a 0,10-0,14 s :
9/9 rendus, 9/9 au PREMIER essai, 0,74-4,26 s, 13,4-21,0 car/s -- le centre de
la distribution legitime (mediane 20,1). Les 3 que la garde seule n'avait pas su
recuperer (93, 178, 383) passent du premier coup.

La branche vLLM n'a pas ce trou (`SamplingParams(min_tokens=min_len)` couvre
tous les ids de `stop_token_ids`) : le patch restaure sur la branche qu'on
execute la forme que l'autre applique deja. Le sampler est patche sur l'objet
charge plutot que forke, pour garder la reutilisation organ-first (#17586) --
l'arbre amont reste tel qu'installe.

TROIS PIEGES MESURES, fermes dans le meme geste :
- le decodeur est a `model.model.llm`, pas `model.llm` : `AutoModel()` est une
  FABRIQUE qui rend `CosyVoice3`, dont `.model` est le `CosyVoice3Model` qui
  porte `.llm`. Une premiere version adressait `model.llm` et a leve sur les
  neuf segments -- un run GPU entier pour n'apprendre que "pas de decodeur".
  L'echec nomme desormais le type et les attributs OBSERVES.
- un patch qui echoue rechargeait le modele a chaque segment (~16 s) : mesure,
  six chargements sur neuf segments ; une passe de 269 segments aurait brule
  pres d'une heure avant de rien rapporter. Le refus est maintenant cache.
- la garde de duree reste en seconde ligne, et sa justification est corrigee :
  le plancher s'appuie sur une bande VIDE mesuree (38,9 car/s pour le legitime
  le plus rapide, 43,1 pour le defectueux le plus lent, 99-560 pour les
  degeneres francs), pas sur un "2,6x sous le plus lent" qui etait l'inverse de
  la mesure. L'ASR confirme la bande 43-69 car/s : 8 segments sur 8 y sont
  defectueux et non "parole rapide" (seg 158 : 3 mots transcrits pour 12 ;
  seg 227 : 79 pour 156).

Tests : 171 verts dans v4/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s le narrateur CosyVoice3

_compose_tts_text appliquait _MAX_TTS_CHARS (limite d'entree du FishAudio
S2-Pro) a TOUS les moteurs: le narrateur reroute vers CosyVoice3 heritait
la coupe. Mesure sur seg 1 (fishaudio_text 981 caracteres): ~440 rendus,
17,8 s d'audio pour un segment de 950 caracteres -- la queue de la
narration etait omise, exactement la classe de defaut que le controle
d'omission p7 (#17586) refuse.

Le narrateur CV3 est borne par chunk par _chunk_narration, pas par une
coupure globale: la troncature ne s'applique plus a lui. Defaut (Qwen)
et locuteurs non-narrateurs inchanges -- leurs moteurs ont vraiment cette
limite. Test de regression: CV3 garde le texte entier, locuteur non-narrateur
et defaut Qwen gardent la coupe.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
_strip_brackets_for_qwen remplaçait chaque tag par la chaîne vide en
avalant aussi l'espace qui suit ("route,[short pause] quand" ->
"route,quand"). Conséquence mesurée : CosyVoice3 nourri d'un run-on
collé omet tout ce qui précède le point de collage (seg 57 : l'audio
commence à "quand", la première proposition n'est jamais prononcée ;
même signature segs 19/38/76 ; 16/20 segments échantillonnés porteurs
d'omissions au vote 3/3 ASR), et le collage masque les frontières de
phrase au chunker CV3.

Le tag devient une espace (puis collapse des blancs). Le défaut
Qwen/FishAudio est inchangé (les deux ne lisaient jamais le texte
strippé en entrée moteur narrateur -- ce chemin est celui du
re-routage). Test de régression collé ajouté (47/47 verts).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Le correctif de collage (be7aa64) change l'entree moteur APRES le
strip des tags, donc le texte compose (avec tags) -- l'autre cle de
cache -- ne change pas : sans bump du reference_id, la passe suivante
servirait les MP3 colles depuis le cache. v3 force le re-rendu des 270
narrateurs au moteur CV3.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CONCERNS (mineures — structure solide, acceptation #17586 pas encore dans la PR)

[NanoClaw] structural review — lecture statique déclarée (python3 absent du conteneur ai-01, règle #16098). 5 fichiers lus au head bf8bcf0dc5 via contents API, tranches ciblées ≤150 lignes.

Vérifié (p5_tts.py)

  • Drapeau NARRATOR_COSYVOICE3_ROUTING défaut "0" (l.73) → inactif sans opt-in. Testé (test_cosyvoice3_off_by_default).
  • Exclusivité mutuelle des drapeaux moteurs : _selected_narrator_engine() lève ValueError si 2 actifs (l.1030) — pas de swap silencieux (#15002 acceptance 6). Testé.
  • Routage narrateur-seul : _should_route_narrator_to() (l.1046) ne déroute que speaker == "narrateur" ; les autres voix restent FishAudio. Testé (narrateur + non-narrateurs × 2 moteurs).
  • _chunk_narration (l.1291-1341) : split phrase → fallback clause → ValueError si clause indivisible >280 (jamais mid-word), grouping greedy, assert d'identité mot-à-mot (_norm_ws(join) == _norm_ws(text)). C'est la bonne primitive anti-perte.
  • Plancher durée CV3 calé sur les mesures A0C (38.9 car/s légitime vs 43.1 défaut) avec terme absolu — testé aux bornes (2.57 s accepté / 2.32 s rejeté) et re-roll dégénéré avec seeds déterministes, fail-loud après épuisement.

Vérifié (p7_verify.py)

  • _missing_word_spans (l.69) : trigrammes normalisés (NFC/lowercase/alnum) absents de l'hypothèse → spans maximaux ≥3 mots ; FP d'inflexion documentés dans la docstring ET couverts par un test dédié qui les assume.
  • _voted_missing_spans (l.114) : vote ≥2 ASR distincts avec recouvrement ≥3 mots, absent_sous trace les modèles contributeurs — le discriminateur bench #17586 est bien reproduit.
  • WER inchangé (convention large-v3-turbo seule) ; comptes dans le rapport numérique + omission_report.json séparé pour lecture avant action. Choix propre.

Tests : 0 skip, asserts réels — identité du chunker, vote filtre le bruit 1-ASR / garde l'omission réelle (2 modèles tracés), halos décalés clusterisés, sentinel CV3 distinct du sentinel Qwen et hors préfixe v4_.

Réserves

  1. _voted_missing_spans : clustering greedy premier-match — deux omissions réellement distinctes mais adjacentes peuvent fusionner via des halos qui se chevauchent en chaîne. Effet conservateur (covering plus grand, pas d'omission perdue), mais n_spans peut sous-compter. À connaître si le bench produit des compteurs inattendus.
  2. Cosmétique : test_cv3_unavailable_is_runtime_error porte l'assert identique en double (l.129-130).
  3. La passe 3 de rendu (fix collage + sentinel -v3) est en cours : l'acceptation #17586 (WER p7, omissions votées, avant/après) est promise mais pas encore publiée dans la PR — le body est transparent là-dessus (« aucune métrique revendiquée avant d'être lue »). Suggestion : merger après publication du avant/après, pour que la gate et le code arrivent ensemble.

— review structurelle ; exécution non testée depuis ce siège (statique).

…Claw)

Le test `test_cv3_unavailable_is_runtime_error` portait deux fois l'assert
identique `issubclass(NarratorCosyVoice3Unavailable, RuntimeError)`.
Reserve 2 de la review structurelle NanoClaw sur la PR #19699.

Suite re-executee apres le retrait : 47 passed (test_p5_cv3_narrator,
test_p5_narrator_routing, test_p7_omission_control).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner Author

Relecture des trois réserves de la review [NanoClaw] (2026-10-07T13:22:30Z, tête bf8bcf0dc5) : sont levées à la tête courante ea66b8562b. La review a été posée avant la livraison du run complet (14:35Z) — deux des trois points se sont résolus par cette livraison.

Réserve 2 — assert dupliqué (cosmétique). Corrigé : test_cv3_unavailable_is_runtime_error ne porte plus qu'une fois issubclass(NarratorCosyVoice3Unavailable, RuntimeError). Commit ea66b8562b. Suite ré-exécutée après le retrait : 47 passed (test_p5_cv3_narrator, test_p5_narrator_routing, test_p7_omission_control).

Réserve 3 — acceptation #17586 « promise mais pas encore publiée ». Elle est publiée depuis la livraison : le corps porte la section « Avant / après » (instrument corpus_damage.py, plancher 40 car/s — narrateurs sous le plancher 20/270 → 3/270) et la section « p7 — WER et contrôle d'omission » avec le tableau à deux passes (WER moyen 0,253 → 0,235 ; omission_spans_3plus 31 → 24 ; gate #17586 : WER ≤ 0,15 et 0 span requis, donc NON CONFORME — la porte est portée par la mesure, pas contournée). Les artefacts de la passe de vérification concordent avec ces valeurs (outputs/quality_report.json : WER moyen 0,235 ; omission_spans_3plus 24 ; 14/20 segments échantillonnés porteurs d'omissions).

Réserve 1 — clustering glouton premier-match de _voted_missing_spans. Je la traite en argument, pas en code. L'effet est celui que la review décrit : conservateur (le covering grandit, aucune omission réelle n'est perdue), avec un n_spans qui peut sous-compter quand deux omissions distinctes s'enchaînent par halos qui se chevauchent. Le rapport est structuré pour que ce cas soit inspectable plutôt que caché : chaque enregistrement porte spans (bornes + mots) et per_model_spans et absent_sous, donc un compteur inattendu se lit span par span dans outputs/omission_report.json sans re-exécuter l'ASR. Le halo lui-même est épinglé par test_single_word_substitution_produces_halo_span, et la dérive d'inflexion est documentée comme faux positif connu dans la docstring et couverte par un test dédié. Je ne durcis pas le clustering dans cette PR : le changer modifierait le discriminateur du banc, et la limite est aujourd'hui nommée, mesurée et inspectable.

Aucun point de la review ne reste ouvert ; la porte #17586 reste NON CONFORME par la mesure (onset-drop intrinsèque au moteur, démontré dans le corps), pas par un défaut de publication.

@jsboige

jsboige commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner Author

Issue de suivi #19739 — elle porte le reliquat que cette phase ne ferme pas : la garde d'attaque ASR par chunk (onset-drop intrinsèque du moteur, graine-dépendant, indétectable par la durée puisque les bandes car/s se recouvrent), plus la décision moteur de re-rendu. Elle est ouverte à la mesure, pas par confort : la porte #17586 reste NON CONFORME sur cette phase et l'issue dit pourquoi.

État des réserves de la review [NanoClaw] au head ea66b8562b : elles sont levées — réserve 2 corrigée (assert dupliqué retiré, 47 tests verts), réserve 3 satisfaite par le corps livré (avant/après 20/270 → 3/270 ; WER 0,253 → 0,235 ; spans 31 → 24), réserve 1 traitée en argument (clustering glouton conservateur, limite nommée et inspectable span par span dans outputs/omission_report.json). Détail au commentaire ci-dessus.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #19699 (feat(genai,#19692): phase 1 -- routage narrateur independant du moteur + CosyVoice3 decoupe (drapeau, defaut inchange) + controle d'omission p7) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

@jsboige

jsboige commented Oct 7, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2024:CoursIA
pr: 19699
head: ea66b85
complete: true
body: read
comments-reviewed: 6
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: e38badd79327e79c245fe23d429481dd546ca680d93a36bd80dd83b5ee125b32
diff-files: 5
diff-additions: 1669
diff-deletions: 50
checks: latest-wins-green
b0: clear
scope: pass
domain: not-applicable
verdict: READY
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 19699
organ-rc: 0
[/ADJOINT PREFLIGHT]

Lecture pour le merge (ai-01) :

  • checks : 0 jambe rouge au latest-wins sur la tete exacte ; composite PR gate success @18:22:49Z.
  • b0 : rc=0 verifie ce 07/10 au soir — la review NanoClaw CONCERNS (clusterManager-Myia COMMENTED, 13:22Z, « mineures ») est levee par la forme valide n°3 : issue de suivi NOMMEE feat(audio,#19692): phase 2 -- garde d'attaque ASR par chunk (onset-drop CV3 mesure, porte #17586 non conforme) #19739 (postee 16:02Z, avant tout merge, portant le reliquat ASR-guard) ; B.0 organ confirme aucun nit non leve.
  • scope : conforme au cadrage coordinateur (c.6034724293 : routage narrateur independant du moteur, CosyVoice3 derriere drapeau defaut inchange, controle omission p7, run complet mesure — pas d'audio.cpp). 5 fichiers, +1669/-50, Grain: DEEP/genai present, claim paths: pose par ai-01 au dispatch.
  • Advisories non bloquants lus : path-collision actif @17:24Z (a apprecier par ai-01 au merge — tranche phase 1 d'un plan coordonne), variation-genre-signals.

Aucun geste de lane requis. La phase 1 peut etre mergee ; le reliquat vit dans #19739.

@myia-ai-01 myia-ai-01 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[OVERRIDE] lane myia-ai-01:CoursIA

Je lève la review structurelle de clusterManager-Myia ([NanoClaw]) du 07/10 à 13:22:30Z, posée à la tête bf8bcf0. Les trois points sont traités à la tête ea66b85, et je l'ai vérifié sur pièces. Réserve 2 : l'assert en double est retiré (commit ea66b85, 47 tests passent). Réserve 3 : le body porte l'avant/après #17586 (WER 0,253 → 0,235 ; spans d'omission 31 → 24), et la porte reste déclarée NON CONFORME par la mesure. Le reliquat (garde d'attaque ASR par segment, choix du moteur de re-rendu) est porté par l'issue de suivi #19739, ouverte avant ce merge. Réserve 1 : traitée en argument. Le clustering glouton est conservateur, il reste inspectable span par span dans omission_report.json, et le durcir changerait le discriminateur du banc. Je retiens cet argument.

@myia-ai-01
myia-ai-01 merged commit 5de0858 into main Oct 7, 2026
20 of 22 checks passed
myia-ai-01 pushed a commit that referenced this pull request Oct 9, 2026
…ion (phase 2 CPU) (#19829)

* feat(audio,#19739): classifier onset/mid/end/spread des spans d'omission (phase 2 CPU)

PR #19699 MERGEE (phase 1) a livre l'organe p7 (vote 2/3 ASR) qui detecte
les spans >=3 mots sur 20 segments echantillonnes. Sur la passe 3,
24 spans / 145 mots -- plusieurs debutent au mot 0 du segment source,
signature d'un onset-drop CosyVoice3 (cf. corps de PR #19699).

Ce module classifier deterministe range chaque span d'omission par
position dans le segment source normalise :

- onset_drop : span_start == 0 (premiere proposition omise)
- end_drop : span_end >= seg_len (queue tombee)
- mid_omission : entre les deux
- spread : span couvre >= 80 % du segment (catastrophe)
- none : span < 3 mots ou seg < 3 mots (invariant p7)

Hierarchie de priorite : SPREAD > ONSET_DROP > END_DROP > MID_OMISSION
> NONE. CPU pur, deterministe, sans appel ASR. Consomme l'artefact
omission_report.json de p7_verify.py + annotated_v4.json pour les
longueurs normalisees.

Tests : 16/16 verts (test_onset_chunk.py).

Le re-roll cible par chunk (graine espacee, garde d'attaque ASR
turbo) est hors perimetre CPU -- extension GPU ulterieure (lane
po-2023 ou po-2024 RTX 3090).

Lane myia-po-2027:CoursIA-2 -- cycle c.1468.

* fix(audio,#19829): enregister audio-onset-chunk-detection.md dans docs/README

Le CI gate docs-index-guard detecte 1 NEW doc non reachable depuis
l'index : docs/genai/audio-onset-chunk-detection.md (nouveau fichier de la PR,
non reference dans docs/README.md, son point d'entree canonique).

Ajout d'une ligne dans la section GenAI de docs/README.md, rangee
apres audio-fading-remediation.md (meme sous-section audio, meme
famille GenAI). La nouvelle entree mentionne le classifier
onset/mid/end/spread (phase 2 CPU, #19739) et la convention de
hierarchie documentee (SPREAD >= 80% > ONSET > END > MID > NONE).

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants