mirror of
https://github.com/deunix-educ/voice-assistant.git
synced 2026-10-13 00:11:57 +02:00
correction agent
This commit is contained in:
@@ -142,8 +142,15 @@ class AccessPolicy:
|
||||
if speaker is not None:
|
||||
self._pending[board] = _Pending(command, speaker, self._clock() + self._access.confirm_timeout_s)
|
||||
|
||||
def resolve(self, board: str, text: str, speaker: str | None) -> Resolution | None:
|
||||
"""Réponse à une confirmation en attente ; None : pas de confirmation, requête ordinaire."""
|
||||
def resolve(self, board: str, text: str, speaker: str | None, closest: str | None = None,
|
||||
score: float | None = None) -> Resolution | None:
|
||||
"""Réponse à une confirmation en attente ; None : pas de confirmation, requête ordinaire.
|
||||
|
||||
Args:
|
||||
speaker: personne reconnue, None si la voix n'atteint pas le seuil d'identification.
|
||||
closest: profil le plus proche, même sous le seuil.
|
||||
score: similarité avec ce profil.
|
||||
"""
|
||||
pending = self._pending.pop(board, None)
|
||||
if pending is None or self._clock() > pending.expires:
|
||||
return None
|
||||
@@ -152,14 +159,30 @@ class AccessPolicy:
|
||||
said_no = any(word in NO for word in words)
|
||||
if not (said_yes or said_no):
|
||||
return None # autre chose : la demande en attente est abandonnée, on traite la nouvelle
|
||||
if speaker != pending.speaker:
|
||||
if speaker != pending.speaker and not self._probably(pending.speaker, speaker, closest, score):
|
||||
logger.warning("%s : confirmation de %s refusee, donnee par %s", board, pending.speaker,
|
||||
speaker or "une voix inconnue")
|
||||
speaker or f"une voix inconnue (proche de {closest}, {self._shown(score)})")
|
||||
return Resolution(f"Seul {pending.speaker} peut confirmer. J'annule.", None)
|
||||
if speaker != pending.speaker:
|
||||
logger.info("%s : confirmation de %s acceptee a %s : voix trop courte pour le seuil, mais "
|
||||
"profil le plus proche", board, pending.speaker, self._shown(score))
|
||||
if said_no:
|
||||
return Resolution("D'accord, j'annule.", None)
|
||||
return Resolution(self._announce(pending.command), pending.command)
|
||||
|
||||
def _probably(self, expected: str, speaker: str | None, closest: str | None, score: float | None) -> bool:
|
||||
"""Voix non reconnue, mais la personne attendue reste la plus proche : assez pour un « oui ».
|
||||
|
||||
Un « oui » dure moins d'une seconde, trop peu pour une empreinte nette. La demande,
|
||||
elle, a été reconnue au seuil normal quelques secondes plus tôt, sur la même carte.
|
||||
"""
|
||||
return (speaker is None and closest == expected and score is not None
|
||||
and score >= self._access.confirm_min_score)
|
||||
|
||||
@staticmethod
|
||||
def _shown(score: float | None) -> str:
|
||||
return "?" if score is None else f"{score:.2f}"
|
||||
|
||||
def _request(self, command: Command) -> str:
|
||||
"""« ouvrir la porte du salon », « éteindre le PC du bureau »."""
|
||||
return command.asked if isinstance(command, MachineCommand) else request(command, self._home)
|
||||
|
||||
@@ -136,6 +136,7 @@ class AccessSettings:
|
||||
rules: dict[str, dict[str, str]] # appareil → action → niveau minimal
|
||||
confirm: tuple[str, ...] # « door.open » : demande « oui » avant d'exécuter
|
||||
confirm_timeout_s: float
|
||||
confirm_min_score: float = 0.25 # « oui » trop court pour le seuil : accepté si le demandeur reste le plus proche
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -333,6 +334,7 @@ def load_settings(path: Path) -> Settings:
|
||||
for device, actions in dict(access["rules"]).items()},
|
||||
confirm=tuple(str(item) for item in access.get("confirm") or ()),
|
||||
confirm_timeout_s=float(access["confirm_timeout_s"]),
|
||||
confirm_min_score=float(access.get("confirm_min_score", 0.25)),
|
||||
),
|
||||
machines=_machines(data),
|
||||
recordings_dir=(base / str(storage["recordings_dir"])).resolve(),
|
||||
|
||||
@@ -57,7 +57,8 @@ class Policy(Protocol):
|
||||
|
||||
def ask(self, board: str, command: Command, speaker: str | None) -> None: ...
|
||||
|
||||
def resolve(self, board: str, text: str, speaker: str | None) -> Resolution | None: ...
|
||||
def resolve(self, board: str, text: str, speaker: str | None, closest: str | None = None,
|
||||
score: float | None = None) -> Resolution | None: ...
|
||||
|
||||
|
||||
class Transcriber(Protocol):
|
||||
@@ -191,7 +192,9 @@ class VoicePipeline:
|
||||
last = utterances[-1] if utterances else None
|
||||
speaker = last.speaker if last is not None and last.speaker != UNKNOWN else None
|
||||
score = last.score if last is not None else None
|
||||
reply, refused = self._decide(result.device, transcript.text, speaker, score)
|
||||
verdict = verdicts.get(last.label) if last is not None else None
|
||||
closest = verdict.closest if verdict is not None else None
|
||||
reply, refused = self._decide(result.device, transcript.text, speaker, score, closest)
|
||||
answer = reply.text
|
||||
if reply.command is not None and self._controller is not None:
|
||||
# La commande part avant la réponse parlée : la lumière s'allume pendant que la carte le dit.
|
||||
@@ -209,13 +212,14 @@ class VoicePipeline:
|
||||
stt_s=transcript.elapsed_s, tts_s=speech.elapsed_s, ready_s=ready_s,
|
||||
answer_s=speech.audio_s)
|
||||
|
||||
def _decide(self, board: str, text: str, speaker: str | None, score: float | None) -> tuple[Reply, bool]:
|
||||
def _decide(self, board: str, text: str, speaker: str | None, score: float | None,
|
||||
closest: str | None = None) -> tuple[Reply, bool]:
|
||||
"""Réponse et commande autorisée ; True si une commande a été refusée ou mise en attente."""
|
||||
if not text:
|
||||
return Reply(NOTHING_HEARD), False
|
||||
if self._policy is not None:
|
||||
# Une confirmation attendue passe avant tout : « oui » n'est pas une requête.
|
||||
resolution = self._policy.resolve(board, text, speaker)
|
||||
resolution = self._policy.resolve(board, text, speaker, closest, score)
|
||||
if resolution is not None:
|
||||
return Reply(resolution.answer, resolution.command), resolution.command is None
|
||||
reply = self._assistant.respond(text, speaker, board)
|
||||
|
||||
Reference in New Issue
Block a user