mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 15:08:13 +02:00
fix(voix): refuser les lectures Gemini qui sautent des mots
Le contrôle lettre à lettre acceptait une lecture dès 85 %, alors qu'une lecture complète atteint ~97 % (petites erreurs de Whisper) : une lecture à 86 % (mots sautés) passait sans nouvelle génération. Seuil porté à 92 %. Les mots non entendus sont nommés dans les journaux et l'avertissement, pour savoir ce qui manquait. Le modèle Whisper, déjà dans l'image, est chargé sans interroger Hugging Face (HF_HUB_OFFLINE). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
This commit is contained in:
3 files changed
+34
-2
No files matched your search
@@ -30,6 +30,8 @@ ENV WHISPER_MODEL=${WHISPER_MODEL} \
|
|||||||
HOME=/tmp \
|
HOME=/tmp \
|
||||||
XDG_CACHE_HOME=/tmp/.cache
|
XDG_CACHE_HOME=/tmp/.cache
|
||||||
RUN python -c "from faster_whisper import WhisperModel; WhisperModel('${WHISPER_MODEL}', device='cpu', compute_type='int8')"
|
RUN python -c "from faster_whisper import WhisperModel; WhisperModel('${WHISPER_MODEL}', device='cpu', compute_type='int8')"
|
||||||
|
# Modèle déjà présent : plus aucune requête vers Hugging Face au démarrage
|
||||||
|
ENV HF_HUB_OFFLINE=1
|
||||||
|
|
||||||
COPY main.py .
|
COPY main.py .
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ pas rogner une syllabe.
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import difflib
|
import difflib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -17,16 +17,21 @@ import numpy as np
|
|||||||
from . import proc
|
from . import proc
|
||||||
from .align import Word, normalize
|
from .align import Word, normalize
|
||||||
|
|
||||||
|
MIN_COVERAGE = 0.92
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ReadingCheck:
|
class ReadingCheck:
|
||||||
coverage: float # part des mots attendus effectivement entendus
|
coverage: float # part des mots attendus effectivement entendus
|
||||||
extra: float # mots entendus en trop (consigne lue, ajouts), rapportés au texte
|
extra: float # mots entendus en trop (consigne lue, ajouts), rapportés au texte
|
||||||
ending_ok: bool # la fin du texte est bien lue
|
ending_ok: bool # la fin du texte est bien lue
|
||||||
|
missing: list[str] = field(default_factory=list) # mots attendus non entendus (diagnostic)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def acceptable(self) -> bool:
|
def acceptable(self) -> bool:
|
||||||
return self.coverage >= 0.85 and self.extra <= 0.25 and self.ending_ok
|
# Comparaison lettre à lettre : une lecture complète atteint ~97 %
|
||||||
|
# (petites erreurs de Whisper) ; sous 92 %, des mots ont été sautés.
|
||||||
|
return self.coverage >= MIN_COVERAGE and self.extra <= 0.25 and self.ending_ok
|
||||||
|
|
||||||
def describe(self) -> str:
|
def describe(self) -> str:
|
||||||
parts = [f"{self.coverage:.0%} des mots lus"]
|
parts = [f"{self.coverage:.0%} des mots lus"]
|
||||||
@@ -34,6 +39,8 @@ class ReadingCheck:
|
|||||||
parts.append("fin tronquée")
|
parts.append("fin tronquée")
|
||||||
if self.extra > 0.25:
|
if self.extra > 0.25:
|
||||||
parts.append("mots ajoutés")
|
parts.append("mots ajoutés")
|
||||||
|
if self.missing and self.coverage < MIN_COVERAGE:
|
||||||
|
parts.append("non entendus : " + " ".join(self.missing[:12]))
|
||||||
return ", ".join(parts)
|
return ", ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
@@ -59,9 +66,21 @@ def check_reading(expected_text: str, spoken: list[Word]) -> ReadingCheck:
|
|||||||
coverage=matched / len(expected),
|
coverage=matched / len(expected),
|
||||||
extra=max(0, len(heard) - matched) / len(expected),
|
extra=max(0, len(heard) - matched) / len(expected),
|
||||||
ending_ok=tail_matched >= min(4, len(expected) - tail_start),
|
ending_ok=tail_matched >= min(4, len(expected) - tail_start),
|
||||||
|
missing=_missing_words(expected_text, spoken),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _missing_words(expected_text: str, spoken: list[Word]) -> list[str]:
|
||||||
|
"""Mots attendus sans équivalent proche dans ce qui a été entendu."""
|
||||||
|
heard = {normalize(w.text) for w in spoken}
|
||||||
|
missing = []
|
||||||
|
for word in expected_text.split():
|
||||||
|
token = normalize(word)
|
||||||
|
if token and not any(difflib.SequenceMatcher(a=token, b=h).ratio() >= 0.7 for h in heard):
|
||||||
|
missing.append(word.strip(".,!?;:…"))
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
# Enveloppe sonore : niveau de la voix toutes les 10 ms
|
# Enveloppe sonore : niveau de la voix toutes les 10 ms
|
||||||
FRAME = 0.01
|
FRAME = 0.01
|
||||||
# Est « audible » ce qui dépasse le niveau de crête moins 38 dB : un seuil
|
# Est « audible » ce qui dépasse le niveau de crête moins 38 dB : un seuil
|
||||||
|
|||||||
@@ -60,3 +60,14 @@ def test_small_recognition_errors_are_not_missing_words():
|
|||||||
heard = _heard("Découvrez nos plans de saison à petit prix. On vous attend ce week -end.")
|
heard = _heard("Découvrez nos plans de saison à petit prix. On vous attend ce week -end.")
|
||||||
check = check_reading(expected, heard)
|
check = check_reading(expected, heard)
|
||||||
assert check.acceptable, check
|
assert check.acceptable, check
|
||||||
|
|
||||||
|
|
||||||
|
def test_skipped_words_are_detected_and_named():
|
||||||
|
expected = (
|
||||||
|
"Pour Halloween découvrez nos costumes nos bonbons et toute notre décoration effrayante en magasin."
|
||||||
|
)
|
||||||
|
heard = _heard("Pour Halloween découvrez nos costumes et toute notre décoration en magasin.")
|
||||||
|
check = check_reading(expected, heard)
|
||||||
|
assert not check.acceptable
|
||||||
|
assert "bonbons" in check.missing and "effrayante" in check.missing
|
||||||
|
assert "non entendus" in check.describe()
|
||||||
Reference in new issue
Block a user