Whisper recoit le vocabulaire metier en amorce
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) <noreply@anthropic.com>
This commit is contained in:
parent
3ad274a774
commit
f1cff4239d
59
core.py
59
core.py
@ -24,9 +24,65 @@ CONFIG = {
|
|||||||
"level_release": 0.15, # lissage à la descente (plus petit = retombée douce)
|
"level_release": 0.15, # lissage à la descente (plus petit = retombée douce)
|
||||||
"wave_rings": 3, # nombre d'anneaux d'onde concentriques
|
"wave_rings": 3, # nombre d'anneaux d'onde concentriques
|
||||||
"wave_speed": 0.04, # vitesse de propagation des anneaux par frame
|
"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):
|
def validate_config(config):
|
||||||
"""Valide la config ; lève ValueError si une valeur est aberrante."""
|
"""Valide la config ; lève ValueError si une valeur est aberrante."""
|
||||||
if config["min_duration_s"] < 0:
|
if config["min_duration_s"] < 0:
|
||||||
@ -75,3 +131,6 @@ def acquire_single_instance_lock(path=LOCK_PATH):
|
|||||||
handle.close()
|
handle.close()
|
||||||
return None
|
return None
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG["initial_prompt"] = build_initial_prompt()
|
||||||
|
|||||||
@ -108,5 +108,6 @@ class Transcriber:
|
|||||||
audio,
|
audio,
|
||||||
path_or_hf_repo=self.model,
|
path_or_hf_repo=self.model,
|
||||||
language=self.language,
|
language=self.language,
|
||||||
|
initial_prompt=CONFIG["initial_prompt"],
|
||||||
)
|
)
|
||||||
return clean_transcript(result.get("text", ""))
|
return clean_transcript(result.get("text", ""))
|
||||||
|
|||||||
50
tests/test_vocabulaire.py
Normal file
50
tests/test_vocabulaire.py
Normal file
@ -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"]
|
||||||
Loading…
Reference in New Issue
Block a user