mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 04:34:41 +02:00
fix(stt): détecte « Asa » même découpé par l'ASR (« a so »/« à ça ») + log diagnostic
Test sur poste : 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 (« 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
4ef579405f
commit
2842446c15
5 changed files with 58 additions and 13 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,6 +2,9 @@
|
|||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
# artefacts de build Python (générés par pip/pipx install — ex. install locale du client stt)
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Ansible
|
||||
.vault_pass
|
||||
|
|
|
|||
|
|
@ -188,8 +188,12 @@ Machine à états du moteur vocal (`stt/client/stt/voice/engine.py`) :
|
|||
|
||||
- **Veille (`asleep`)** — état par défaut. Asa transcrit chaque énoncé **uniquement** pour y
|
||||
repérer le wake word ; elle ne répond à rien d'autre (HUD : portrait atténué, « SOMMEIL »).
|
||||
- **Réveil** — dire **« Asa »** (mot entier) la passe en éveil. « Asa, <question> » exécute
|
||||
directement la question. Variantes ASR tolérées via `[voice].wake_aliases`.
|
||||
- **Réveil** — dire **« Asa »** la passe en éveil. « Asa, <question> » exécute directement la
|
||||
question. ⚠️ L'ASR (Parakeet FR) ne transcrit **pas** « Asa » comme un mot : il l'entend
|
||||
« a so » / « à ça » / « ah ça »… La détection **joint les 1-2 premiers mots** (déaccentués,
|
||||
sans espace) et compare en égalité exacte à `[voice].wake_aliases` (formes compactes :
|
||||
`asa`, `aso`, `aca`, `ahca`…). Ajuster cette liste si l'ASR rend encore autrement — le log
|
||||
`[stt.voice] (veille) entendu: '…'` (journalctl, cf. `STT_VOICE_LOG`) montre la transcription réelle.
|
||||
- **Éveil (`listening`)** — elle répond à tout. **« Asa stop »** (ou « stop », « au revoir »,
|
||||
« dors »… — `[voice].sleep_words`) la **remet en veille** ; un timeout d'inactivité
|
||||
(`chat_timeout_sec`) aussi.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ Validé : tests unitaires (wake/sleep/alias) + Playwright (rendu `asleep`, bulle
|
|||
jour en place puis remplacée par le final, nettoyage du partiel au changement d'état).
|
||||
Bump client **0.14.0**. Doc : `admin/ia/stt.md` (section « Interaction vocale »).
|
||||
|
||||
> 🐛 **Trouvaille ASR (test sur poste)** : Parakeet FR ne transcrit **pas** « Asa » comme un mot
|
||||
> — il l'entend **« a so » / « à ça » / « ah ça »** (deux tokens). La détection mot-entier ne
|
||||
> matchait donc jamais → « rien ne se passe ». **Corrigé** : on **joint les 1-2 premiers mots**
|
||||
> (déaccentués, sans espace) et on compare en égalité exacte à des alias compacts (`aso`, `aca`,
|
||||
> `ahca`…). Faux positifs évités (« à cause » → `acause` ≠ alias). Ajout d'un log diagnostic
|
||||
> `[stt.voice]` (journalctl, `STT_VOICE_LOG=0` pour couper) qui montre la transcription réelle.
|
||||
> Validé en live : « Asa » réveille, « Asa stop » endort.
|
||||
|
||||
---
|
||||
|
||||
## (Rappel) Relevé d'état réel + RAG re-ingéré — fait la veille (2026-06-21, PR #44)
|
||||
|
|
|
|||
|
|
@ -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