diff --git a/README.md b/README.md index 6c1d447..bdf70b3 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Le robot ne fait **aucun traitement lourd** — il capture, streame, et restitue | [Mémoire conversationnelle](docs/memory-system.md) | Système de mémoire à 3 niveaux | | [Hardware](docs/hardware.md) | Propositions de composants et modules | | [Roadmap](docs/roadmap.md) | Phases de développement et priorités | +| [Simulateur](docs/simulator.md) | Simulateur web et CLI pour développer sans le robot physique | ## Équipe diff --git a/docs/simulator.md b/docs/simulator.md new file mode 100644 index 0000000..23cd4d1 --- /dev/null +++ b/docs/simulator.md @@ -0,0 +1,272 @@ +# Simulateur Robot + +## Pourquoi un simulateur ? + +Le robot physique Ti-Pote prendra du temps à assembler (impression 3D, soudure, firmware). En attendant, on a besoin de développer et tester le core backend sans être bloqué par le hardware. Le simulateur remplace le robot physique en parlant exactement le même protocole WebSocket — du point de vue du core, il est indiscernable d'un vrai Ti-Pote. + +Principe fondamental : **le core ne doit jamais savoir s'il parle à un vrai robot ou à un simulateur.** C'est l'archi hexagonale en action — le robot (réel ou simulé) n'est qu'un adaptateur entrant. + +## Les deux simulateurs + +### 1. Simulateur Web (usage quotidien) + +Une page web qui transforme le navigateur en robot Ti-Pote. Le développeur ouvre la page, parle dans son micro, et interagit avec le core exactement comme le ferait le robot. + +``` +┌──────────────────────────────────────────────┐ +│ Navigateur (Simulateur Web) │ +│ │ +│ ┌─────────────┐ ┌──────────────────┐ │ +│ │ Micro PC │ │ Interface │ │ +│ │ (WebRTC │ │ - Statut LEDs │ │ +│ │ getUserMedia) │ - Transcript │ │ +│ └──────┬──────┘ │ - Logs WS │ │ +│ │ │ - Bouton wake │ │ +│ ▼ │ word │ │ +│ ┌─────────────┐ └──────────────────┘ │ +│ │ WebSocket │ │ +│ │ Client │◄────── Audio TTS ─────── │ +│ │ │─────► Audio chunks ─────► │ +│ └─────────────┘ │ +│ │ ┌──────────────────┐ │ +│ │ │ Speaker PC │ │ +│ │ │ (Web Audio API) │ │ +│ └─────────►│ │ │ +│ └──────────────────┘ │ +└──────────────────────────────────────────────┘ + │ WSS + ▼ + ┌──────────────┐ + │ Core Backend │ + │ (NestJS) │ + └──────────────┘ +``` + +#### Fonctionnalités de l'interface + +L'interface web du simulateur affiche : + +**Zone de statut** — un indicateur visuel qui simule les LEDs du robot. Un cercle coloré qui change selon l'état : bleu pulsant (écoute), jaune clignotant (réflexion/traitement), vert (Ti-Pote parle), gris (veille). L'état est piloté par les messages `status` reçus du core via WebSocket. + +**Bouton "Hey Ti-Pote"** — un bouton qui simule la détection du wake word. Un clic envoie le message `{ type: 'wake_word_detected' }` au core et commence la capture audio du micro. En option, on peut aussi implémenter une vraie détection wake word dans le navigateur via un modèle TensorFlow.js, mais le bouton suffit pour le développement. + +**Transcript en temps réel** — affiche les transcriptions STT (ce que l'utilisateur a dit) et les réponses du LLM (ce que Ti-Pote répond). Utile pour debugger sans écouter l'audio à chaque fois. + +**Logs WebSocket** — un panneau dépliant qui affiche tous les messages WebSocket échangés (type, taille, timestamp). Indispensable pour le debug du protocole. + +**Contrôles audio** — indicateur de niveau du micro (pour vérifier que la capture fonctionne), contrôle du volume de sortie, et bouton mute. + +#### Flux audio + +**Capture (micro → core)** : +1. L'utilisateur clique sur "Hey Ti-Pote" (ou le wake word est détecté). +2. Le navigateur demande l'accès au micro via `navigator.mediaDevices.getUserMedia({ audio: true })`. +3. Un `AudioWorklet` ou `ScriptProcessorNode` capture les samples audio en PCM. +4. Les samples sont rééchantillonnés à 16kHz 16bit mono (format attendu par le core). +5. Les chunks sont envoyés au core via WebSocket en binaire. +6. La VAD (Voice Activity Detection) détecte le silence et envoie `{ type: 'speech_end' }`. + +**Playback (core → speaker)** : +1. Le core envoie des chunks audio TTS via WebSocket. +2. Le simulateur les bufferise et les decode via `AudioContext.decodeAudioData()`. +3. Les chunks sont joués séquentiellement via des `AudioBufferSourceNode`. +4. Le streaming permet de commencer la lecture avant de recevoir toute la réponse. + +#### Technologies + +Le simulateur web est une simple page HTML avec du JavaScript vanilla (ou un mini React si on veut). Pas besoin de build, pas de dépendances lourdes. Il peut être servi directement par le core NestJS en tant que fichier statique. + +``` +apps/simulator/ +├── index.html # Page principale +├── css/ +│ └── simulator.css # Styles +├── js/ +│ ├── websocket.js # Gestion WebSocket + protocole +│ ├── audio-capture.js # Capture micro (AudioWorklet) +│ ├── audio-playback.js # Lecture audio TTS +│ ├── vad.js # Voice Activity Detection +│ └── ui.js # Interface (LEDs, transcript, logs) +└── worklets/ + └── pcm-processor.js # AudioWorklet pour le resampling +``` + +### 2. CLI Client (tests automatisés) + +Un script TypeScript en ligne de commande qui se connecte au core en WebSocket. Pas de micro, pas de speaker — il envoie des données pré-enregistrées ou du texte brut. + +```bash +# Envoyer un fichier audio pré-enregistré +tipote-cli send-audio ./test-audio/bonjour.wav + +# Envoyer du texte directement (bypass STT) +tipote-cli send-text "Mets un rendez-vous demain à 15h" + +# Mode interactif (tape du texte, reçois du texte) +tipote-cli interactive + +# Lancer un scénario de test +tipote-cli test ./scenarios/calendar-test.json +``` + +#### Modes d'utilisation + +**Mode audio** — Envoie un fichier `.wav` au core comme si c'était du streaming micro. Le core passe par STT normalement. Utile pour tester la chaîne complète avec des inputs reproductibles. + +**Mode texte** — Bypasse le STT et envoie du texte directement au ConversationService. Le core skip la phase STT et va directement au LLM. Utile pour tester le function calling et les services métier sans dépendre du STT. + +**Mode interactif** — Un REPL où le développeur tape des messages et voit les réponses en texte. Comme un chatbot en CLI. Pratique pour le dev quotidien quand on veut juste tester une feature. + +**Mode scénario** — Exécute un fichier JSON qui décrit une séquence de messages et vérifie les réponses attendues. Utile pour les tests de régression et le CI. + +```json +// Exemple : scenarios/calendar-test.json +{ + "name": "Création de rendez-vous", + "steps": [ + { + "input": "Mets un rendez-vous demain à 15h avec le dentiste", + "expect": { + "tool_call": "create_calendar_event", + "params_contain": { + "title": "dentiste" + }, + "response_contains": ["rendez-vous", "créé", "15h"] + } + }, + { + "input": "Qu'est-ce que j'ai demain ?", + "expect": { + "tool_call": "list_calendar_events", + "response_contains": ["dentiste", "15h"] + } + } + ] +} +``` + +#### Structure + +``` +apps/cli/ +├── src/ +│ ├── index.ts # Point d'entrée +│ ├── commands/ +│ │ ├── send-audio.ts # Commande send-audio +│ │ ├── send-text.ts # Commande send-text +│ │ ├── interactive.ts # Mode interactif +│ │ └── test.ts # Exécution de scénarios +│ ├── websocket.ts # Client WebSocket +│ └── audio.ts # Lecture et conversion de fichiers audio +├── scenarios/ # Fichiers de test +│ ├── basic-conversation.json +│ ├── calendar-test.json +│ └── timer-test.json +├── test-audio/ # Fichiers audio de test +│ ├── bonjour.wav +│ └── quelle-heure.wav +├── package.json +└── tsconfig.json +``` + +## Protocole WebSocket : contrat partagé + +Le simulateur et le vrai robot parlent exactement le même protocole. Ce contrat est défini une seule fois et implémenté des deux côtés. + +### Connexion + +1. Le client (robot ou simulateur) ouvre une connexion WebSocket vers `wss://api.tipote.dev/ws/`. +2. Le premier message est une authentification : + +```json +{ + "type": "auth", + "device_token": "eyJhbGciOiJIUzI1NiIs...", + "client_type": "robot" | "simulator_web" | "simulator_cli" +} +``` + +Le champ `client_type` permet au core de savoir quel type de client est connecté (utile pour les logs et le monitoring), mais ne change rien au traitement. Le core répond : + +```json +{ + "type": "auth_success", + "device_id": "uuid-du-device", + "user_id": "uuid-du-user", + "config": { /* config du device */ } +} +``` + +### Messages client → core + +| Type | Payload | Description | +|------|---------|-------------| +| `wake_word_detected` | `{}` | Le wake word a été détecté, le core passe en mode écoute | +| `audio_chunk` | `{ data: Buffer, sampleRate: 16000, encoding: 'pcm_s16le' }` | Chunk audio du micro | +| `speech_end` | `{}` | La VAD a détecté une fin de parole | +| `user_interrupt` | `{}` | L'utilisateur parle pendant que Ti-Pote répond (barge-in) | +| `text_input` | `{ text: string }` | Texte direct, bypass STT (simulateur CLI uniquement) | +| `heartbeat` | `{ timestamp: number }` | Ping pour maintenir la connexion | + +### Messages core → client + +| Type | Payload | Description | +|------|---------|-------------| +| `status` | `{ state: 'idle' \| 'listening' \| 'thinking' \| 'speaking' }` | État du core pour piloter les LEDs | +| `audio_chunk` | `{ data: Buffer, format: 'pcm_s16le' \| 'mp3' \| 'opus' }` | Chunk audio TTS à jouer | +| `response_start` | `{}` | Début de la réponse (le TTS va commencer) | +| `response_end` | `{}` | Fin de la réponse audio | +| `transcript` | `{ text: string, final: boolean }` | Transcription STT (partielle ou finale) | +| `response_text` | `{ text: string }` | Texte de la réponse du LLM (pour affichage) | +| `notification` | `{ title: string, body: string, priority: 'low' \| 'normal' \| 'urgent' }` | Notification proactive | +| `error` | `{ code: string, message: string }` | Erreur | + +### Encodage audio + +Les chunks audio sont envoyés en messages binaires WebSocket (pas en JSON). Le header de chaque chunk contient un identifiant de type (1 byte) suivi des données audio brutes. Les messages de contrôle (wake_word, speech_end, etc.) sont en JSON texte. + +Convention : messages binaires = audio, messages texte = contrôle JSON. + +## Workflow de développement + +### Développement quotidien + +1. Lancer le core en local : `docker compose up -d && pnpm dev` +2. Ouvrir le simulateur web : `http://localhost:3000/simulator` +3. Cliquer "Hey Ti-Pote", parler dans le micro du PC +4. Debugger dans la console du navigateur + les logs NestJS + +### Tests automatisés (CI) + +1. Le CI lance le core en mode test (mock des services externes STT/TTS/LLM) +2. Le CLI client exécute les scénarios de test +3. Chaque scénario vérifie que les function calls et les réponses sont correctes +4. Pas besoin de vrai micro, ni de vrai LLM — tout est mocké + +### Test de performance (latence) + +Le simulateur web affiche les timings de chaque étape : + +``` +[14:32:01.000] Wake word → audio stream start +[14:32:01.050] First audio chunk sent (50ms) +[14:32:03.200] Speech end detected +[14:32:03.250] STT transcript received (50ms after speech end) +[14:32:03.800] LLM first token (550ms after transcript) +[14:32:04.100] TTS first audio chunk (300ms after LLM start) +[14:32:04.100] Total latency: 900ms (speech end → first audio playback) +``` + +Ces timings permettent d'identifier les goulots d'étranglement et d'optimiser la latence. + +## Évolution progressive + +Le simulateur évolue avec le projet : + +**Phase 1 (MVP)** — Simulateur web avec micro + speaker uniquement. Bouton wake word. Transcript et logs. CLI avec mode texte interactif. + +**Phase 2** — Ajout de la webcam pour simuler le module caméra. Interface pour afficher ce que le "robot voit". CLI avec mode audio (envoi de fichiers .wav). + +**Phase 3** — Simulation visuelle des LEDs et de l'écran du robot. Canvas 2D pour simuler la position du robot si le module mobile est actif. Scénarios de test automatisés dans le CI. + +**Phase 4** — Simulateur multi-device (ouvrir plusieurs onglets = plusieurs robots). Test de la reconnaissance vocale multi-utilisateur. Dashboard de monitoring intégré.