11 KiB
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_idau décroché, capture (réutilise/voice/captureinterne), 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(0–9) + 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✅ (modeltts-1, voixnova/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:8300ou la voice-bridge/voice/transcribe(studio:8200, actuellement down).
8. Dépendances bloquantes (honnêtes)
- 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. - Endpoint whisper vivant : relancer la voice-bridge sur studio OU whisper.cpp
:8300OU 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/sayet leMAX_BODY256K 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 alimententconversation; 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)
- Tonalités + dialer + machine à états jusqu'à RINGBACK — testable au son, zéro IA.
- Gateway
/v1/voice/turnvoie greeting + annuaire → composer un numéro = entendre le PNJ parler (sens unique, sans micro/whisper). Livrable jouable. - Vérif micro (SLIC décroché) + whisper → conversation deux sens.
- 3615 Minitel (handshake + agent robotique).
13. Tests
- Gateway (unit) :
/v1/voice/turnavec 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/dialpour CI. - Dégradé : STT down → accueil + fallback fonctionnent.