Files
le-mystere-professeur-zacus/docs/superpowers/specs/2026-06-14-plip-telephone-conversation-design.md
T
clemsail 904a77a08b
Repo State / repo-state (pull_request) Failing after 39s
Repo State / repo-state (push) Failing after 30s
Validate Zacus refactor / validate (pull_request) Failing after 5m43s
docs(plip): design — téléphone conversationnel France Télécom (annuaire 12/3615/15/17/18, /v1/voice/turn)
2026-06-14 21:13:40 +02:00

11 KiB
Raw Blame History

PLIP — Téléphone conversationnel France Télécom — Design

Date : 2026-06-14 Statut : design validé, prêt pour plan d'implémentation Carte : PLIP (téléphone rétro), ESP32_ZACUS/plip_voice/, ESP32-A1S Audio Kit + SLIC classe AG1171, codec ES8388, IP 192.168.0.138

1. Objectif

Faire du PLIP un vrai téléphone France Télécom interactif dans l'escape room « Mystère du Professeur Zacus » : le joueur décroche, entend la tonalité, compose un numéro sur le cadran, et tombe sur un PNJ vocal (Professeur Zacus ou un service d'époque) avec qui il converse (reconnaissance vocale → réponse IA en personnage). Raccroché = fin + silence.

2. Architecture — Approche retenue (A)

Le firmware PLIP possède la machine à états (la « boucle » téléphonique) ; la gateway expose un endpoint stateless-par-tour qui enchaîne STT → LLM → TTS. Le PLIP ne fait ni TLS, ni prompt, ni resample.

COMBINÉ PLIP (firmware = machine à états)
  raccroché → décroché → TONALITÉ → numérotation → routage annuaire
           → ringback → accueil PNJ → [ ÉCOUTE(VAD) → /v1/voice/turn → JOUE ]* → raccroché→IDLE+mute
        │
        └─ HTTP simple (plain, LAN) ─────────────► ZACUS-GATEWAY  /v1/voice/turn (stateless/tour)
                                                     stt(whisper) → chat(ailiance + persona + historique) → tts(ailiance) → resample 16k
                                                        │                │                                       │
                                                     whisper :8300   gateway.ailiance.fr /v1/chat/completions   .../v1/audio/speech
                                                     (À RELANCER)         ✅ Ministral & co                       ✅ tts-1

Frontière :

  • PLIP firmware : machine à états, session_id au décroché, capture (réutilise /voice/capture interne), POST le WAV, joue la réponse, reboucle ; raccroché = abort + mute (réutilise le mute-on-hook existant). Aucun TLS / prompt / resample sur l'ESP32.
  • Gateway : pipeline IA d'un tour + historique court par session_id (mémoire, TTL 10 min). Persona + voix + annuaire = côté serveur, éditables sans reflasher.

Décision : un seul appel /v1/voice/turn par tour (PLIP envoie le WAV capté, reçoit le WAV de réponse) plutôt que 3 appels stt/chat/tts séparés → firmware bien plus simple, le PLIP garde la maîtrise du séquencement (quand écouter, stop VAD, jouer, reboucler, abort).

3. Séquence téléphonique (machine à états)

IDLE (raccroché, PA coupé, muet)
 └─ DÉCROCHÉ (SHK haut)
     → DIAL_TONE : tonalité 440 Hz continu (FR) en boucle, attend le 1er chiffre
     → DIALING : collecte les chiffres (coupe la tonalité au 1er), fin = numéro complet (match annuaire)
                  OU timeout inter-chiffre ~3 s
     → ROUTING : recherche du numéro dans l'annuaire
         ├─ numéro connu → RINGBACK (440 Hz 1,5 s/3,5 s) → CONNECT
         │      → GREET (kind=greeting) → [ LISTEN(VAD) → /v1/voice/turn(kind=reply) → SPEAK ]*
         └─ numéro inconnu → tonalité « non attribué »/occupé (440 Hz 0,5/0,5) ~5 s → IDLE
     → RACCROCHÉ (SHK bas) à tout instant → IDLE + mute (abort tour en cours)

Tonalités France Télécom (générées localement, pas de fichier)

  • Tonalité (invitation à numéroter) : 440 Hz continu.
  • Occupé / non attribué : 440 Hz, 0,5 s on / 0,5 s off.
  • Retour d'appel (ringback) : 440 Hz, 1,5 s on / 3,5 s off.
  • Handshake Minitel (pour 3615) : sample audio embarqué (le « bip strident » de connexion).

La tonalité/ringback passe par l'ampli PA → forcer le PA on dès le décroché (cohérent avec le mute-on-hook : décroché ⇒ PA on).

4. Numérotation — module dialer

Abstraction qui émet des événements digit(09) + dial_complete, 3 sources combinables (Kconfig) :

  • Impulsions (cadran)CONFIG_PLIP_DIAL_PULSE : comptage d'impulsions sur le pin SHK du SLIC (la boucle s'ouvre/ferme pendant la numérotation au cadran ; 1 impulsion=1 … 10=0), gap inter-chiffre. Bench-testable avec un téléphone à cadran vintage branché sur le SLIC.
  • DTMF (touches)CONFIG_PLIP_DIAL_DTMF : décodage Goertzel des 8 fréquences DTMF sur l'audio capté via le SLIC. Bench-testable (dépend du chemin micro).
  • Debug/banc — toujours dispo : POST /debug/dial?number=NNNN (+ option bouton BOOT = 1 chiffre) pour CI/validation sans téléphone physique.

Le SLIC classe AG1171 fournit : hook + impulsions sur SHK (le hook GPIO actuel = SHK), audio combiné via le codec ES8388, sonnerie via les pins RM/F-R. Le mute-on-hook existant mappe exactement (SHK bas = raccroché = muet).

5. Annuaire (game/scenarios/phone_directory.yaml)

Table côté gateway : numéro → { persona, voice, minitel? }. Numéros supportés (réf. France Télécom) :

Numéro Service Comportement
12 Renseignements (l'annuaire) opératrice/standard : oriente, indice « quel numéro appeler »
3615 Minitel (kiosque Télétel) handshake modem Minitel en intro, puis agent vocal spécial « service Minitel » (persona datée/robotique), même boucle /voice/turn
15 SAMU PNJ secours médical en personnage
17 Police secours PNJ police
18 Sapeurs-pompiers PNJ pompiers

Le numéro de Professeur Zacus (persona principale) est une entrée d'annuaire supplémentaire, numéro configurable. Le contenu des personas (caractère, ton, voix, anti-triche) est rempli par l'auteur après implémentation ; l'annuaire est extensible (autres PNJ = ajout d'entrées, sans reflasher). Numéro absent de l'annuaire → tonalité « non attribué ».

6. Contrat /v1/voice/turn

POST /v1/voice/turn (sous token) :

Entrée Type Rôle
session_id string généré par le PLIP au décroché ; clé de l'historique
number string le numéro composé → choix persona/voix dans l'annuaire
kind "greeting" | "reply" greeting = ouverture (pas d'audio) ; reply = tour normal
audio (corps) WAV 16k mono la parole captée (absent si kind=greeting)

Sortie : 200, Content-Type: audio/wav = WAV 16k prêt à jouer. En-têtes debug : X-Zacus-Heard (transcription) + X-Zacus-Said (réponse texte).

POST /v1/voice/end {session_id} → purge l'historique (best-effort ; sinon TTL 10 min).

Historique serveur : dict[session_id] → messages[] (system persona du numéro + alternance user/assistant), borné aux ~8 derniers tours, purgé sur /v1/voice/end ou TTL.

Flux d'un appel

1. numéro routé → POST /turn {session_id, number, kind:greeting}
   → serveur : chat(persona accueil) → TTS → WAV → PLIP joue l'accueil
2. PLIP capture le micro (VAD : démarre à la voix, stop au silence ~800 ms, cap ~8 s)
3. POST /turn {session_id, number, kind:reply, audio:WAV}
   → serveur : whisper(audio)→texte ; append historique ; chat(persona+historique)→réponse ; TTS→resample 16k → WAV
4. PLIP joue la réponse → retour étape 2
5. raccroché → abort + mute ; POST /v1/voice/end (best-effort)

7. Services IA (état constaté 2026-06-14)

  • LLM (NPC brain) : gateway.ailiance.fr /v1/chat/completions (Ministral & autres modèles ailiance).
  • TTS : gateway.ailiance.fr /v1/audio/speech (model tts-1, voix nova/alloy ; sortie WAV 22050 Hz → resample 16k côté gateway, réutilise _wav_to_16k_mono).
  • STT : absent d'ailiance (/v1/audio/transcriptions → 404). À fournir par whisper.cpp :8300 ou la voice-bridge /voice/transcribe (studio:8200, actuellement down).

8. Dépendances bloquantes (honnêtes)

  1. Micro = vrai audio : la capture renvoie aujourd'hui des zéros. Hypothèse forte : le SLIC ne connecte le chemin audio du combiné que décroché, et/ou l'entrée ES8388 utilisée n'est pas celle câblée à la sortie audio du SLIC. Vérif à coût quasi nul : téléphone branché + décroché → /voice/capture → RMS ≠ 0. Sinon, basculer l'entrée ES8388 (LIN2/RIN2 vs LIN1/RIN1) selon le câblage SLIC.
  2. Endpoint whisper vivant : relancer la voice-bridge sur studio OU whisper.cpp :8300 OU exposer un endpoint STT sur la gateway.

9. Mode dégradé (sans micro / sans STT)

Le téléphone marche en sens unique sans ces deux dépendances : tonalité, numérotation, routage annuaire, ringback, et chaque PNJ débite sa réplique d'accueil (TTS). La boucle d'écoute (LISTEN→STT) est sautée. → un téléphone-annuaire France Télécom jouable est livrable AVANT le micro/whisper.

10. Gestion d'erreur

  • STT vide/échec → la gateway renvoie un WAV de relance (« Allô ? je vous entends mal, répétez ? », par persona) ; la boucle continue.
  • Erreur réseau/ailiance → WAV de repli (« la ligne est mauvaise… »).
  • Raccroché en plein tour → le PLIP abort le HTTP en cours (best-effort), stoppe l'audio, mute, IDLE.
  • Numéro inconnu → tonalité « non attribué » → IDLE.
  • Taille réponse capée ~7 s (240 KiB), cohérent avec /v1/voice/say et le MAX_BODY 256K du PLIP (upload/playback streaming).

11. Composants & fichiers

Firmware PLIP (ESP32_ZACUS/plip_voice/main/) :

  • dialer.c/.h (neuf) : impulsions SHK + DTMF Goertzel + /debug/dial → événements digit/dial_complete.
  • tones.c/.h (neuf) : tonalités FR + sample handshake Minitel.
  • conversation.c/.h (neuf) : machine à états complète, session_id, capture→POST /v1/voice/turn→play, abort au raccroché.
  • extension phone.c : les événements hook alimentent conversation ; SHK pulse → dialer.
  • client HTTP : POST du WAV capté à la gateway, réception du WAV de réponse.
  • réutilise : /voice/capture (mic+VAD), play streaming, mute-on-hook, ring.

Gateway (tools/zacus-gateway/) :

  • POST /v1/voice/turn, POST /v1/voice/end.
  • chargement game/scenarios/phone_directory.yaml (annuaire → persona/voix).
  • store de sessions en mémoire (TTL).
  • réutilise _synthesise_wav(ailiance) + _wav_to_16k_mono.

12. Étapes de build (incrémental, chaque étape livrable)

  1. Tonalités + dialer + machine à états jusqu'à RINGBACK — testable au son, zéro IA.
  2. Gateway /v1/voice/turn voie greeting + annuaire → composer un numéro = entendre le PNJ parler (sens unique, sans micro/whisper). Livrable jouable.
  3. Vérif micro (SLIC décroché) + whisperconversation deux sens.
  4. 3615 Minitel (handshake + agent robotique).

13. Tests

  • Gateway (unit) : /v1/voice/turn avec WAV fixture → routage persona par numéro, accumulation historique, voie greeting, fallbacks d'erreur (ailiance mocké ou live) ; chargement annuaire.
  • Firmware (banc, téléphone à cadran via SLIC) : décroché→tonalité ; composer 17→ringback→accueil police ; parler→réponse (gated micro) ; 3615→handshake Minitel→agent robotique ; numéro inconnu→occupé ; raccroché→silence. Impulsions via SHK ; DTMF Goertzel ; /debug/dial pour CI.
  • Dégradé : STT down → accueil + fallback fonctionnent.