mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 05:34:43 +02:00
fix(stt): détecte « Asa » même découpé par l'ASR (« a so »/« à ça ») + log diagnostic (#46)
Test sur poste après #45 : Parakeet FR ne transcrit pas « Asa » comme un mot mais comme « a so » / « à ça » / « ah ça » (deux tokens) → la détection mot-entier ne matchait jamais (« quand je dis Asa, rien ne se passe »). - _detect_wake : glisse une fenêtre de 1-2 mots, joint (sans espace) + déaccentue, compare en égalité exacte à des alias COMPACTS (asa, aso, aca, ahca…). L'exact évite les faux positifs (« à cause » → « acause » ≠ alias). Extrait la commande après le wake. - wake_aliases (engine + config) : formes compactes reflétant les rendus ASR réels. - Log diagnostic [stt.voice] sur stderr/journalctl (transcription + décision), coupable via STT_VOICE_LOG=0 — précieux pour ajuster les alias selon l'ASR. - .gitignore : build/ + *.egg-info (artefacts d'install locale du client). Validé en live (poste) : « Asa » réveille, « Asa stop » endort, partiels en direct. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d83091a350
commit
1e01caf634
5 changed files with 58 additions and 13 deletions
|
|
@ -40,8 +40,11 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||
"voice": {
|
||||
# Wake word principal : « Asa ». Dire « Asa » la réveille ; « Asa stop » la rendort.
|
||||
"wake_word": "asa",
|
||||
# Variantes tolérées (l'ASR FR rend « Asa » diversement). Mots ENTIERS, déaccentués.
|
||||
"wake_aliases": ["asa", "aza", "assa", "aca", "haza", "hassa"],
|
||||
# Variantes tolérées du wake word « Asa » — formes COMPACTES (déaccentuées, sans
|
||||
# espace). L'ASR découpe « Asa » en « a so » / « à ça » / « ah ça »… → on joint les
|
||||
# 1-2 premiers mots avant de comparer. Ajuste cette liste si ton ASR rend autrement.
|
||||
"wake_aliases": ["asa", "aza", "assa", "aca", "aco", "aso", "asso", "azo",
|
||||
"ahca", "ahsa", "aica", "haza", "hassa", "aha"],
|
||||
# Mots/phrases qui remettent Asa en veille quand elle est éveillée (sous-chaîne).
|
||||
"sleep_words": ["stop", "dors", "au revoir", "bonne nuit", "fin", "silence", "tais-toi"],
|
||||
# ASR : "whisper" (faster-whisper CPU, défaut) | "onnx" (Parakeet/Canary/Nemotron)
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ Dépendances lourdes (sounddevice, faster_whisper, webrtcvad) importées à la v
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import queue
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
|
@ -28,6 +30,15 @@ from typing import Any, Callable
|
|||
|
||||
from .asr import make_backend
|
||||
|
||||
# Journalise les transcriptions + décisions (veille/réveil) sur stderr → journalctl.
|
||||
# Activé par défaut (volume faible : une ligne par énoncé) ; STT_VOICE_LOG=0 pour couper.
|
||||
_VOICE_LOG = os.environ.get("STT_VOICE_LOG", "1") not in ("0", "false", "")
|
||||
|
||||
|
||||
def _log(msg: str) -> None:
|
||||
if _VOICE_LOG:
|
||||
print(f"[stt.voice] {msg}", file=sys.stderr, flush=True)
|
||||
|
||||
SAMPLE_RATE = 16000
|
||||
PRE_ROLL = 15
|
||||
# Mots de mise en veille (« Asa stop ») — déaccentués, cherchés en sous-chaîne quand
|
||||
|
|
@ -35,9 +46,14 @@ PRE_ROLL = 15
|
|||
DEFAULT_SLEEP_WORDS = (
|
||||
"stop", "dors", "au revoir", "bonne nuit", "fin", "silence", "tais-toi", "chut",
|
||||
)
|
||||
# Variantes du wake word tolérées (l'ASR FR rend « Asa » diversement). Surchargeable
|
||||
# via [voice].wake_aliases ; le wake word principal ([voice].wake_word) s'y ajoute.
|
||||
DEFAULT_WAKE_ALIASES = ("asa", "aza", "assa", "aca", "aza", "haza", "hassa")
|
||||
# Variantes du wake word « Asa » tolérées — formes COMPACTES (déaccentuées, sans espace).
|
||||
# L'ASR (Parakeet FR) découpe et rend « Asa » très diversement : « a so », « à ça », « ah ça »,
|
||||
# « aza »… On compare donc la jonction des 1-2 premiers mots à ces formes. Surchargeable via
|
||||
# [voice].wake_aliases ; le wake word principal ([voice].wake_word) s'y ajoute.
|
||||
DEFAULT_WAKE_ALIASES = (
|
||||
"asa", "aza", "assa", "aca", "aco", "aso", "asso", "azo",
|
||||
"ahca", "ahsa", "aica", "haza", "hassa", "aha",
|
||||
)
|
||||
|
||||
PIPER_BIN = Path.home() / ".local/share/piper-runtime/piper"
|
||||
PIPER_VOICE_DIR = Path.home() / ".local/share/piper"
|
||||
|
|
@ -116,14 +132,22 @@ class VoiceEngine:
|
|||
return {_deaccent(w).strip() for w in (*base, *extra) if w}
|
||||
|
||||
def _detect_wake(self, text: str) -> tuple[bool, str]:
|
||||
"""Cherche le wake word comme **mot entier** (pas sous-chaîne : « asa » est court
|
||||
et matcherait « phrase », « casa »…). Renvoie (trouvé, reste de la phrase)."""
|
||||
"""Repère le wake word et renvoie (trouvé, reste de la phrase).
|
||||
|
||||
L'ASR découpe souvent « Asa » en deux tokens (« a ça », « a so »…). On glisse une
|
||||
fenêtre de 1 puis 2 mots, on **joint** (sans espace) et déaccentue, et on compare
|
||||
en **égalité exacte** à l'ensemble d'alias compacts (l'exact évite les faux positifs
|
||||
type « à cause » → « acause »). La fenêtre de 2 d'abord → capte « a so » avant « a »."""
|
||||
aliases = self._wake_aliases()
|
||||
words = text.strip().split()
|
||||
low = [_deaccent(w).strip(",.!?«»…:;\"'()") for w in words]
|
||||
for i, w in enumerate(low):
|
||||
if w in aliases:
|
||||
return True, " ".join(words[i + 1:]).strip()
|
||||
low = [_deaccent(w).strip(",.!?«»…:;\"'()-") for w in words]
|
||||
for i in range(len(low)):
|
||||
for n in (2, 1):
|
||||
if i + n > len(low):
|
||||
continue
|
||||
head = "".join(low[i:i + n])
|
||||
if head in aliases:
|
||||
return True, " ".join(words[i + n:]).strip()
|
||||
return False, ""
|
||||
|
||||
def _is_sleep_command(self, text: str) -> bool:
|
||||
|
|
@ -337,6 +361,7 @@ class VoiceEngine:
|
|||
self.emit({"type": "state", "state": "asleep" if asleep else "listening"})
|
||||
continue
|
||||
text = self._transcribe(audio)
|
||||
_log(f"({'veille' if asleep else 'eveil'}) entendu: {text!r}")
|
||||
if not text:
|
||||
self.emit({"type": "state", "state": "asleep" if asleep else "listening"})
|
||||
continue
|
||||
|
|
@ -344,8 +369,10 @@ class VoiceEngine:
|
|||
if asleep:
|
||||
found, command = self._detect_wake(text)
|
||||
if not found:
|
||||
_log(f" pas de wake word (alias={sorted(self._wake_aliases())})")
|
||||
self.emit({"type": "state", "state": "asleep"})
|
||||
continue
|
||||
_log(f" RÉVEIL (commande={command!r})")
|
||||
asleep = False
|
||||
last_activity = time.time()
|
||||
self.emit({"type": "state", "state": "listening"})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue