From f1cff4239db82cef986718e588bc4ad44ac58047 Mon Sep 17 00:00:00 2001 From: Ralph Mayola Date: Fri, 21 Aug 2026 00:28:52 +0200 Subject: [PATCH] Whisper recoit le vocabulaire metier en amorce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sans contexte, le modele rendait « Playwright » en « PlayWreck », « iCloud » en « Auriclaw », « Supabase » en « Supabass », « Vercel » en « Versell » : 4 termes sur 5 faux, mesure sur audio reel. Avec l'amorce (initial_prompt), les 5 sont corrects, sans changer de modele ni ralentir (1,3 s). Passer a large-v3-turbo a ete teste et ecarte : PIRE sur le meme echantillon (« commit sur Gitea » -> « commis sur JTA », « Stripe » -> « Trip »), pour 1,5 Go de plus. C'est l'amorce qui paie, pas la taille du modele. VOCABULAIRE est une simple liste a completer dans core.py. Whisper ne lit que ~224 tokens d'amorce et tronque le reste EN SILENCE : la longueur est donc bornee explicitement, pour qu'un ajout de termes ne soit jamais sans effet a l'insu de celui qui l'ecrit (47 termes = 416 caracteres sur 800). 34 tests passent (27 + 7 nouveaux). Co-Authored-By: Claude Opus 5 (1M context) --- core.py | 59 +++++++++++++++++++++++++++++++++++++++ engine.py | 1 + tests/test_vocabulaire.py | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 tests/test_vocabulaire.py diff --git a/core.py b/core.py index 66e28bf..c71600a 100644 --- a/core.py +++ b/core.py @@ -24,9 +24,65 @@ CONFIG = { "level_release": 0.15, # lissage à la descente (plus petit = retombée douce) "wave_rings": 3, # nombre d'anneaux d'onde concentriques "wave_speed": 0.04, # vitesse de propagation des anneaux par frame + "initial_prompt": None, # rempli plus bas depuis VOCABULAIRE } +# --- vocabulaire metier ------------------------------------------------- +# Whisper accepte une « amorce » (initial_prompt) qui conditionne le decodage. +# Sans elle, le modele rendait « Playwright » en « PlayWreck », « iCloud » en +# « Auriclaw », « Supabase » en « Supabass », « Vercel » en « Versell » +# (mesure du 2026-08-21 : 4 termes sur 5 faux ; 5 sur 5 corrects avec l'amorce). +# Passer a large-v3-turbo a ete teste et fait PIRE (« commit sur Gitea » -> +# « commis sur JTA ») : c'est bien l'amorce qui paie, pas la taille du modele. +# +# Pour ajouter tes propres termes : complete cette liste, puis ./build_app.sh. +VOCABULAIRE = [ + # agents et terminal + "Claude Code", "Codex", "OpenCode", "Warp", "Playwright", "MCP", + # web et hebergement + "Next.js", "TypeScript", "Vercel", "Convex", "Supabase", "Docker", "Caddy", + "Gitea", "GitHub", "Hetzner", "Hostinger", "iCloud", "Clerk", + # paiement, mail, automatisation + "Stripe", "PayPal", "Shopify", "Brevo", "n8n", "Airtable", "Yabetoo", + # projets + "FacturPro", "Forescale", "Reflow", "CiteGain", "Faramaya", "Soko", + "Moubote", "Yelessa", "Zonza", "MR TECH LAB", + # jargon courant + "repo", "commit", "webhook", "API", "npm", "build", "deploy", "conteneur", + "migration", "endpoint", "prompt", +] + +# Whisper ne lit que ~224 tokens d'amorce et tronque le reste EN SILENCE. +# On borne donc explicitement, pour qu'un ajout de termes ne soit jamais +# silencieusement sans effet. +PROMPT_MAX_CARACTERES = 800 + + +def build_initial_prompt(termes=None): + """Construit l'amorce Whisper a partir d'une liste de termes. + + Doublons retires (ordre conserve), une seule ligne, longueur bornee a + PROMPT_MAX_CARACTERES : au-dela, les derniers termes sont ecartes plutot + que de laisser Whisper tronquer au milieu d'un mot. + """ + if termes is None: + termes = VOCABULAIRE + vus, uniques = set(), [] + for t in termes: + if t not in vus: + vus.add(t) + uniques.append(t) + prefixe = "Vocabulaire : " + retenus = [] + for t in uniques: + candidat = prefixe + ", ".join(retenus + [t]) + "." + if len(candidat) > PROMPT_MAX_CARACTERES: + break + retenus.append(t) + return prefixe + ", ".join(retenus) + "." + + def validate_config(config): """Valide la config ; lève ValueError si une valeur est aberrante.""" if config["min_duration_s"] < 0: @@ -75,3 +131,6 @@ def acquire_single_instance_lock(path=LOCK_PATH): handle.close() return None return handle + + +CONFIG["initial_prompt"] = build_initial_prompt() diff --git a/engine.py b/engine.py index 4ee46a7..e271189 100644 --- a/engine.py +++ b/engine.py @@ -108,5 +108,6 @@ class Transcriber: audio, path_or_hf_repo=self.model, language=self.language, + initial_prompt=CONFIG["initial_prompt"], ) return clean_transcript(result.get("text", "")) diff --git a/tests/test_vocabulaire.py b/tests/test_vocabulaire.py new file mode 100644 index 0000000..0271861 --- /dev/null +++ b/tests/test_vocabulaire.py @@ -0,0 +1,50 @@ +"""Amorce de vocabulaire passee a Whisper. + +Contexte (bug du 2026-08-21) : sans contexte, le modele rend « Playwright » en +« PlayWreck », « iCloud » en « Auriclaw », « Supabase » en « Supabass » et +« Vercel » en « Versell » — mesure sur audio reel, 4 termes sur 5 faux. En +passant la liste des termes metier en initial_prompt, les 5 redeviennent +corrects sans changer de modele. + +Contrainte : Whisper ne lit que ~224 tokens d'amorce et TRONQUE le reste en +silence. La longueur est donc bornee, sinon les derniers termes ajoutes ne +servent a rien sans qu'on le sache. +""" +from core import CONFIG, VOCABULAIRE, PROMPT_MAX_CARACTERES, build_initial_prompt + + +def test_le_prompt_contient_les_termes_qui_avaient_echoue(): + p = build_initial_prompt() + for terme in ("Playwright", "iCloud", "Supabase", "Vercel", "Convex"): + assert terme in p + + +def test_le_prompt_tient_sur_une_seule_ligne(): + # un retour a la ligne coupe l'amorce cote Whisper + assert "\n" not in build_initial_prompt() + + +def test_le_prompt_respecte_la_borne(): + assert len(build_initial_prompt()) <= PROMPT_MAX_CARACTERES + + +def test_une_liste_trop_longue_est_tronquee_pas_ignoree(): + p = build_initial_prompt(["Terme%03d" % i for i in range(500)]) + assert len(p) <= PROMPT_MAX_CARACTERES + assert "Terme000" in p # les premiers termes survivent + + +def test_les_doublons_sont_retires_en_gardant_l_ordre(): + p = build_initial_prompt(["Convex", "Stripe", "Convex"]) + assert p.count("Convex") == 1 + assert p.index("Convex") < p.index("Stripe") + + +def test_le_vocabulaire_couvre_les_projets_de_ralph(): + for terme in ("Convex", "Supabase", "Stripe", "n8n", "Airtable"): + assert terme in VOCABULAIRE + + +def test_config_expose_le_prompt(): + assert CONFIG["initial_prompt"] + assert "Playwright" in CONFIG["initial_prompt"]