# 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(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` ✅ (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é) + whisper** → **conversation 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.
