mirror of
https://github.com/deunix-educ/voice-assistant.git
synced 2026-10-13 00:11:57 +02:00
Assistant vocal local ESP32 : étapes 1 à 15, et 15b écrite
État du projet au premier commit : - firmware ESP32 : micro I2S, lecture anti-gigue, MQTT (état retenu, testament), mains libres, domotique de démonstration, chien de garde, présence du serveur ; - serveur Python : sessions, VAD, faster-whisper, pyannote, identification, droits par profil, domotique, mot de réveil, Piper ; - broker Mosquitto : configuration, comptes et ACL (non activés en développement) ; - étape 15b écrite, en attente d'essai : agent MQTT pour machines Linux. Étapes 1 à 15 validées. 203 tests, pyright sans erreur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+768
@@ -0,0 +1,768 @@
|
||||
# CONTEXT.md — état d'avancement du projet
|
||||
|
||||
> Fichier de reprise. À relire **en premier** en cas d'interruption, et à mettre à jour
|
||||
> **à la fin de chaque étape**. Il décrit ce qui est fait, ce qui est en cours, et les
|
||||
> décisions techniques déjà prises (pour ne pas les rediscuter).
|
||||
|
||||
---
|
||||
|
||||
## 1. Projet
|
||||
|
||||
Assistant vocal **100 % local** : ESP32 (micro INMP441) → MQTT → serveur Python
|
||||
(VAD, faster-whisper, diarisation, Piper) → MQTT → ESP32 (ampli MAX98357A).
|
||||
|
||||
Prompt de référence : `prompt-assistant-vocal-esp32.md` (rôle, style, feuille de route, conventions).
|
||||
|
||||
---
|
||||
|
||||
## 2. Feuille de route et avancement
|
||||
|
||||
| # | Étape | État | Validé par l'utilisateur |
|
||||
|---|---|---|---|
|
||||
| 0 | Ossature du dépôt (arborescence, Makefile, git, README) | **fait** | en attente |
|
||||
| 1 | Entrée audio ESP32 (I2S RX) + `tools/serial_to_wav.py` | **fait** | **oui (2026-09-29)** |
|
||||
| 2 | Sortie audio ESP32 (I2S TX) + `tools/wav_to_serial.py` | **fait** | **oui (2026-09-29)** |
|
||||
| 3 | Mosquitto + topics (Wi-Fi, MQTT, état retenu, testament) | **fait** | **oui (2026-09-29)** |
|
||||
| 4 | ESP32 → MQTT → serveur (sessions reconstruites en WAV) | **fait** | **oui (2026-09-29)** |
|
||||
| 5 | Serveur → MQTT → ESP32 (WAV serveur joué par l'ESP32, mode écho) | **fait** | **oui (2026-09-29)** |
|
||||
| 6 | MVP : PTT → faster-whisper → réponse → Piper → ESP32 | **fait** (serveur seul, firmware inchangé) | **oui (2026-09-29)** |
|
||||
| 7 | VAD (Silero) | **fait** (serveur seul) | **oui (2026-09-29)** |
|
||||
| 8 | Diarisation (pyannote) | **fait**, test réel à deux voix réussi | **oui (2026-09-29)** |
|
||||
| 9 | Identification (embeddings + cosinus + seuil) | **fait** (serveur seul) | **oui (2026-09-30)** |
|
||||
| 10 | Association locuteur + texte | **fait** (serveur seul) | **oui (2026-09-30)** |
|
||||
| 11 | Assistant / domotique | **fait** (serveur + firmware) | **oui (2026-09-30)** |
|
||||
| 12 | Buffer de sortie anti-jitter | **fait** (firmware + banc de gigue) | **oui (2026-09-30)** |
|
||||
| 13 | Wake word (openWakeWord) | **fait** (serveur + firmware) | **oui (2026-09-30)** |
|
||||
| 14 | Multi-utilisateurs | **fait** (serveur seul) | **oui (2026-09-30)** |
|
||||
| 15 | Fiabilisation | **fait** (broker + serveur + firmware) | **oui (2026-09-30)** |
|
||||
| 15b | **Agents Linux** (ajout utilisateur) : commander des machines du réseau | **fait** (serveur + agent) | en attente |
|
||||
| 16 | Optimisation | à faire | — |
|
||||
|
||||
**Règle absolue** : ne jamais démarrer l'étape N+1 avant validation explicite de l'étape N.
|
||||
|
||||
**Exigence ajoutée par l'utilisateur (2026-09-30)** : l'assistant doit aussi pouvoir **commander des
|
||||
machines Linux du réseau**. Absente de la feuille de route du prompt. Proposition en discussion :
|
||||
un agent MQTT par machine (liste blanche d'actions définie sur la machine), Wake-on-LAN par le
|
||||
serveur, état retenu + testament comme l'ESP32 ; actions sensibles seulement après les droits par
|
||||
profil (étape 14) et l'authentification MQTT (étape 15). **Placement décidé : étape 15b** (après droits par profil et authentification MQTT). Liste d'actions et machines à préciser.
|
||||
|
||||
---
|
||||
|
||||
## 3. Décisions techniques figées
|
||||
|
||||
| Sujet | Décision | Raison |
|
||||
|---|---|---|
|
||||
| Format audio unique | PCM `s16le`, 16 kHz, mono | compatible Whisper/Silero, simple pour l'ESP32 |
|
||||
| Taille de chunk | 50 ms = 800 échantillons = **1600 octets** | compromis latence / surcoût protocole |
|
||||
| Encodage MQTT | bytes bruts, **jamais de Base64** | −33 % de débit, pas de CPU perdu |
|
||||
| Core Arduino-ESP32 | **3.x** (ESP-IDF 5.x), driver `driver/i2s_std.h` | ancien `driver/i2s.h` déprécié ; plateforme `pioarduino` |
|
||||
| Plateforme PlatformIO | `pioarduino` 54.03.21 (fork maintenu pour core 3.x) | le paquet officiel `espressif32` est resté en core 2.0.x |
|
||||
| PlatformIO isolé | venv dédié `.pio-venv`, avec `click==8.1.8` | click ≥ 8.3 casse l'esptool 5.0 du core 3.x (erreur `get_metavar() missing ctx`) ; évite aussi tout conflit avec les dépendances du serveur |
|
||||
| Périphériques I2S | I2S0 = micro (RX), I2S1 = ampli (TX) | duplex séparé, pas de conflit d'horloges |
|
||||
| Broches | voir `firmware/include/config.h` | aucune broche de strapping (0, 2, 5, 12, 15) sur l'audio |
|
||||
| Broker de dev : conteneur `mosquitto_services-mosquitto-1`, compose `~/srv/docker-compose.mosquitto.yml`, config montée `~/srv/mosquitto/config/mosquitto.conf` ; **IP du PC = 192.168.1.104** (et non .0.104) | 2026-09-29 | constaté sur la machine |
|
||||
| MQTT : état JSON **retenu** + testament `{"status":"offline"}` retenu QoS 1, keepalive 10 s | 2026-09-29 | `offline` en 15 s max après coupure ; serveur informé même s'il démarre après l'ESP32 |
|
||||
| Wi-Fi : `setSleep(false)` | 2026-09-29 | le modem endormi ajoute 100 ms+ de latence : incompatible avec le flux audio |
|
||||
| Partitions `huge_app.csv` (3 Mo d'application, pas d'OTA) | 2026-09-29 | Wi-Fi + MQTT = 74 % de la partition par défaut |
|
||||
| Sortie par MQTT : START/END sur `control` (QoS 1), PCM sur `audio/out` (QoS 0), **300 ms d'avance** puis 1 chunk / 50 ms | 2026-09-29 | FIFO ESP32 de 512 ms ; la carte publie `played` (reçus/annoncés, sous-alimentations) |
|
||||
| Tout audio envoyé à l'ESP32 : **16 kHz mono, crête à −3 dBFS** (`AudioResampler`, soxr) | 2026-09-29 | ESP32 simple ; règle le volume faible noté à l'étape 2 ; Piper (22,05 kHz) passera par là |
|
||||
| Firmware : lecture regroupée dans `PlaybackStream` (série ou MQTT), commandes dans `CommandHandler` (ArduinoJson 7.2.1) | 2026-09-29 | un seul chemin de lecture ; les commandes s'enrichiront à l'étape 11 |
|
||||
| Capture : micro → **tâche de capture** → FIFO 1 s → `loop()` → MQTT | 2026-09-29 | `publish()` peut bloquer sur un Wi-Fi faible ; le DMA du micro n'a que 75 ms de réserve |
|
||||
| Perte détectée par le **nombre de chunks du END** (pas de numéro de séquence avant l'étape 16) | 2026-09-29 | audio en QoS 0 ; le serveur compare reçus / annoncés après 0,5 s de grâce |
|
||||
| **Limite connue** : PubSubClient ne publie qu'en **QoS 0**, `event` compris (le prompt demande QoS 1) | 2026-09-29 | serveur tolérant (session sans END close après 5 s d'inactivité). **Revu à l'étape 15 : conservé** (aucune perte mesurée sur le réseau local ; esp-mqtt = réécriture de `MqttLink` et de ses rappels, sans gain observable) |
|
||||
| Session sans broker → liaison série, format de l'étape 1 | 2026-09-29 | `make record` reste un outil de dépannage |
|
||||
| Sortie : **stéréo, même échantillon sur les deux voies** | 2026-09-29 | le MAX98357A joue (G+D)/2 : niveau exact, inversion G/D sans effet ; on ne mise plus sur un mode mono implicite |
|
||||
| Sortie : canal I2S1 **toujours actif**, `auto_clear` → silence quand rien à jouer | 2026-09-29 | broches de l'ampli jamais flottantes (cause du souffle initial) |
|
||||
| Lecture : réception → **FIFO** (StreamBuffer statique 512 ms) → **tâche audio** → I2S | 2026-09-29 | architecture exigée pour MQTT (callback court) ; la tâche est rythmée par le DMA |
|
||||
| FIFO : écriture **tout ou rien** par chunk | 2026-09-29 | un chunk coupé = octet impair = tout le reste désaligné |
|
||||
| Protocole série **partagé** : `SerialProtocol.h` ↔ `tools/serial_protocol.py` | 2026-09-29 | une seule définition par langage ; test croisé encodeur Python → décodeur C++ |
|
||||
| Passe-haut micro **80 Hz**, 1er ordre, calculé en **24 bits** (`MIC_HPF_SHIFT = 5`) | 2026-09-29 | 87 % de l'énergie d'un enregistrement était infrasonore (plaque qui fléchit sous le doigt, souffle) ; 0,6 % après filtre, voix 99 % |
|
||||
| Cadrage micro : **MSB** (24 bits utiles), mesuré, pas supposé | 2026-09-29 | en Philips, le bit de signe déborde dans le mot précédent (23 bits + `00000001`) ; l'auto-test bascule seul si un autre module est Philips |
|
||||
| Les horloges du micro ne s'arrêtent **jamais** après le démarrage | 2026-09-29 | l'INMP441 s'endort sans horloge ; chaque réveil = dérive lente de la composante continue (+23 % FS encore à 600 ms) |
|
||||
| Lecture I2S stéréo : la **phase de trame** est suivie entre les appels | corrigé le 2026-09-29 | `i2s_channel_read()` peut rendre un nombre **impair** de mots ; sans suivi, les deux moitiés s'échangent en cours de route et on enregistre la moitié muette |
|
||||
| Micro : SCK → **GPIO 25**, WS → **GPIO 26** | corrigé le 2026-09-29 | le schéma initial les avait inversés ; aucune raison technique ne départage 25 et 26, la doc a été alignée sur le banc réel |
|
||||
| Liaison série étape 1 | **921 600 baud**, trames `VSA1` + longueur | 32 kB/s d'audio impossible à 115 200 baud |
|
||||
| Conversion micro | mot I2S 32 bits → `>> 16`, gain ×4, offset DC glissant | l'INMP441 place 24 bits utiles en tête de mot |
|
||||
| Licence | MIT | choix par défaut du prompt |
|
||||
| Extra `speech` : **faster-whisper 1.2.1**, **piper-tts 1.8.0** (au lieu de 1.0.3 / 1.2.0) | 2026-09-29 | Python 3.13 : faster-whisper 1.0 exige `av<13` (compilation depuis les sources, échoue sans les en-têtes ffmpeg) ; piper-tts 1.2 dépend de `piper-phonemize` sans roue 3.13. piper-tts ≥ 1.3 embarque espeak-ng. API Piper : `PiperVoice.load()`, `synthesize()` → `AudioChunk.audio_float_array` |
|
||||
| Modèles **locaux** dans `server/models/` : `whisper/<model>/`, `piper/<voice>.onnx(.json)`, via `make models` (`tools/download_models.py`) | 2026-09-29 | aucun accès réseau au démarrage du serveur ; ~550 Mo ; ignorés par git |
|
||||
| STT : `small`, `int8`, `beam_size 5`, `condition_on_previous_text=False`, `vad_filter=False` (VAD = étape 7) | 2026-09-29 | mesuré sur ce PC (20 cœurs) : **≈ 1,0 s quelle que soit la phrase (1 à 3 s)**, le nombre de fils (0/8/16) ou le beam (1/5) : Whisper encode toujours une fenêtre de 30 s |
|
||||
| Session de crête < **−45 dBFS** (`stt.min_peak_dbfs`) : Whisper n'est pas appelé, la carte dit « Je n'ai rien entendu. » | 2026-09-29 | mesuré : 2 s de silence numérique = **4,8 s** de calcul (repli sur les températures) |
|
||||
| Hallucinations connues écartées (`KNOWN_HALLUCINATIONS` : Amara.org, sous-titres…) | 2026-09-29 | mesuré : 1 s de bruit gaussien → « Sous-titres réalisés par la communauté d'Amara.org » |
|
||||
| Imports faster-whisper / Piper **différés** (dans `__init__`) | 2026-09-29 | `make run-echo` et les tests fonctionnent sans l'extra `speech` |
|
||||
| Assistant à règles (`assistant.py`) : heure, date, nom, bonjour, merci ; sinon « Vous avez dit : … » ; heure écrite « une heure », « midi », « 19 heures 5 » | 2026-09-29 | Piper lit « 1 heure » = « un heure » ; Whisper écrit parfois « Quel heure » (mots-clés tolérants) |
|
||||
| VAD : **Silero ONNX livré par faster-whisper** (`faster_whisper.vad.get_vad_model`), segmentation écrite dans `voice_activity.py` (hystérésis 0,5 / 0,35, pause 500 ms, parole ≥ 250 ms, marge 150 ms) | 2026-09-29 | pas de torch ; segmentation testable sans modèle ; 2 à 4 ms par session |
|
||||
| VAD **sur la session entière** (après le END), pas en flux | 2026-09-29 | suffisant en PTT ; la détection de fin de parole en flux servira au mode mains libres (étape 13) |
|
||||
| La VAD **remplace le seuil de crête** `stt.min_peak_dbfs` (supprimé) | 2026-09-29 | l'appui muet de l'étape 6 (crête −30,7 dBFS) passait le seuil ; la VAD le rejette |
|
||||
| Diarisation : **pyannote.audio 4.0.7**, torch **2.14.0+cpu** / torchaudio 2.11.0+cpu (index pytorch.org), modèle `pyannote/speaker-diarization-community-1` (33 Mo) | 2026-09-29 | Python 3.13 ; sans l'index CPU, pip tire torch CUDA (plusieurs Go de `nvidia-*`) ; pyannote 3.3.2 antérieur à ce torch |
|
||||
| **numpy 2.1.3 → 2.5.3** (dépendance de base) | 2026-09-29 | pyannote-metrics 4 exige numpy ≥ 2.2.2 ; 78 tests repassés avec 2.5.3 (soxr, onnxruntime, ctranslate2 OK) |
|
||||
| **Télémétrie pyannote coupée** (`PYANNOTE_METRICS_ENABLED=false`, forcé par `disable_telemetry()` avant l'import) | 2026-09-29 | activée par défaut en 4.x : envoie durée et nombre de locuteurs à `otel.pyannote.ai` ; variable relue à chaque appel |
|
||||
| Modèle pyannote **copié dans `models/pyannote/<nom>/`** par `make models` (`snapshot_download`, `HF_TOKEN` de `server/.env`), chargé depuis ce dossier | 2026-09-29 | les sous-modèles sont des `$model/…` relatifs : aucun appel au hub à l'exécution. `HF_HUB_OFFLINE` non utilisé : lu à l'import de huggingface_hub, que faster-whisper a déjà fait |
|
||||
| Diarisation **sur `vad.speech_pcm`**, le même audio que Whisper, **en séquence** avant Whisper | 2026-09-29 | instants communs pour l'association locuteur/texte (étape 10) ; parallélisation → étape 16 |
|
||||
| `diarization.enabled` dans config.yaml ; activée mais absente → le serveur refuse de démarrer avec la marche à suivre | 2026-09-29 | Raspberry Pi 4 : `false` ; pas de repli silencieux |
|
||||
| pyannote 4 rend **une empreinte vocale par locuteur** (`speaker_embeddings`, WeSpeaker ResNet34), conservée dans `Diarization.embeddings` | 2026-09-29 | candidate pour l'identification (étape 9), à comparer à SpeechBrain ECAPA prévu par le prompt ; `speechbrain` retiré de l'extra `diarization` |
|
||||
| Empreintes : **modèle `embedding/` du pipeline pyannote (WeSpeaker ResNet34, 256 dim.)** via `Inference(window="whole")` sur toute la parole de chaque locuteur (tours de `exclusive_speaker_diarization` mis bout à bout), 66 ms | 2026-09-29 | **les `speaker_embeddings` de pyannote sont NaN sous ~2 s de parole** : il ne garde que les fenêtres de 10 s où le locuteur parle seul ≥ 20 % (`filter_embeddings`, `min_active_ratio=0.2`) ; toutes les questions PTT étaient NaN |
|
||||
| Tours de parole : version **exclusive** (sans chevauchement) | 2026-09-29 | une voix par empreinte ; prête pour le découpage du texte (étape 10) |
|
||||
| Seuil cosinus **0,45** (remplace 0,75) | 2026-09-29 | mesuré : profil de 5 sessions contre 23 autres sessions de l'utilisateur **0,52–0,83** (0,52 = 0,9 s de parole) ; Piper direct ou par haut-parleur **−0,08 à 0,11**. À recalibrer avec de vraies autres personnes (étape 14) |
|
||||
| Profils : `server/profiles/<nom>.json` (nom, modèle, nb de sessions, date, empreinte normalisée), écriture atomique, **relus quand le dossier change** | 2026-09-29 | JSON lisible (on sait ce qu'on stocke) ; un profil d'un autre modèle est ignoré ; enrôlement pris en compte sans redémarrer |
|
||||
| Enrôlement (`make enroll NAME=`, `tools/enroll.py`) : ≥ 3 sessions de ≥ 1 s, une seule voix, **écart des sessions qui ne ressemblent pas aux autres** (cosinus avec la moyenne des autres < seuil) | 2026-09-29 | la session mélangée `093463` (voix + haut-parleur, vue comme 1 locuteur par pyannote) est écartée à 0,27–0,32 |
|
||||
| Identification **seulement avec la diarisation** (pas d'empreinte sinon) | 2026-09-29 | choix de l'utilisateur (empreintes pyannote plutôt que SpeechBrain) |
|
||||
| Qui a dit quoi : **`word_timestamps=True`** dans Whisper, chaque mot au tour de diarisation qui le recouvre le plus (sinon le plus proche), mots consécutifs d'un même locuteur regroupés | 2026-09-30 | mesuré : +0,07 à 0,14 s (5 à 10 %) ; les bornes des mots tombent sur celles des tours (même audio pour les deux) |
|
||||
| Topic **`voice/<carte>/transcript`** (serveur → abonnés, QoS 1, non retenu) : `{"session", "utterances": [{"speaker", "text", "start", "end", "score"}]}` | 2026-09-30 | format de la feuille de route + instants et score pour la domotique (étape 11) et les droits (étape 14) ; le serveur ne s'y abonne pas |
|
||||
| L'assistant reçoit le **nom de la dernière personne qui a parlé** (`reply(text, speaker)`) : « Bonjour Denis ! », « Avec plaisir, Denis. » | 2026-09-30 | c'est elle qui attend la réponse ; `None` si inconnue |
|
||||
| Domotique : commandes sur **`home/<pièce>/<appareil>/set`** (QoS 1, **non retenu**) **et**, si la pièce est celle de la carte, `voice/<carte>/control` `{"cmd":"device",...}` exécuté par l'ESP32 (choix de l'utilisateur : « les deux », en simulation) | 2026-09-30 | toute domotique peut s'abonner ; l'ESP32 « exécute la commande éventuelle » (prompt) ; une commande retenue serait rejouée à un abonné tardif |
|
||||
| Analyse par **mots-clés** (`home_control.py`) : action × appareil × pièce ; « allume/éteins » seuls = lumière ; sans pièce = pièce de la carte (`home.boards`) ; appareil/action incompatibles ou incomplets → question | 2026-09-30 | suffisant pour 3 appareils ; tolère « fermé » (Whisper) ; limites : 1 appareil et 1 pièce par phrase, faux positifs possibles (« je porte… ») |
|
||||
| Identifiants en anglais dans les topics et le JSON (`light`, `shutter`, `door`, `on/off/open/close`) ; pièces = noms de config.yaml, en slug (`salle-de-bain`) | 2026-09-30 | mêmes chaînes côté Python et firmware |
|
||||
| Firmware : **`DemoDevices`**, LED sur **GPIO 21** (330 Ω), volets et porte simulés ; confirmation `{"event":"device","ok":...,"state":...}` journalisée par le serveur | 2026-09-30 | GPIO 21 libre, hors strapping ; la boucle commande → exécution → confirmation est visible |
|
||||
| La commande part **avant** la réponse parlée | 2026-09-30 | la lumière s'allume pendant que la carte le dit |
|
||||
| `normalize` déplacé dans `text_normalize.py` | 2026-09-30 | commun à l'assistant et à la domotique, sans import circulaire |
|
||||
| Météo : réponse honnête « je fonctionne sans Internet » | 2026-09-30 | plutôt que répéter la question |
|
||||
| Anti-gigue : **amorçage à 300 ms** (`PLAYBACK_START_MS`) avant le premier échantillon, **réamorçage** après une famine (pause propre plutôt que bribes), son plus court que la réserve joué dès le END | 2026-09-30 | 300 ms = 6 chunks = avance envoyée d'emblée par le serveur : la lecture démarre dès qu'elle est arrivée ; valeur de l'exemple du prompt |
|
||||
| `underruns` du bilan = **réamorçages** (famines en plein flux) ; nouveaux champs `start_ms` (délai du premier son) et `min_margin_ms` (plus petite réserve mesurée) | 2026-09-30 | l'ancien compteur comptait chaque bloc incomplet, y compris au démarrage : 6 à 10 par lecture sans aucune perte |
|
||||
| Banc de gigue : `--jitter MS` / `make tone-mqtt JITTER=MS` retient **1 chunk sur 10** (toutes les 500 ms) pendant MS ms, puis rattrapage en rafale | 2026-09-30 | scénario déterministe : marge attendue ≈ 300 − MS ms, réamorçages seulement au-delà de 300 ms ; prouver plutôt qu'attendre un mauvais Wi-Fi |
|
||||
| Mot de réveil : **openWakeWord 0.6.0 en ONNX**, installé `--no-deps` (exige `tflite-runtime`, absent pour Python 3.13) ; modèles `alexa`, `hey_jarvis`, `hey_mycroft` v0.1 + melspectrogram/embedding, depuis les releases GitHub v0.5.1 | 2026-09-30 | mesuré : « Alexa » (Piper FR) 0,92–1,0 ; « Hé Maïcrofte » 1,0 ; « Hey Djâr-viss » 0,74 ; lus à la française Jarvis/Mycroft ≈ 0 ; **0 fausse alerte sur 90 enregistrements réels** (max 0,07) ; ~1 ms par chunk |
|
||||
| Flux continu **`voice/<carte>/audio/stream`** (QoS 0), activé par le serveur (`cmd listen`) à la connexion/redémarrage de la carte, coupé à l'arrêt du serveur ; **jamais pendant une réponse** (écho, Wi-Fi) ni pendant un appui (le bouton reprend la main) | 2026-09-30 | le serveur décide ; la carte reste simple |
|
||||
| `HandsFreeListener` : VEILLE (préroll 500 ms **en mémoire seulement**) → COMMANDE (fin = 800 ms de silence après de la parole qui dépasse le mot de 0,3 s ; abandon après 4 s sans commande ; max 10 s) → RÉPONSE (jusqu'au bilan `played`, garde 20 s) ; la commande devient une `SessionResult` ordinaire | 2026-09-30 | aucune autre partie de la chaîne ne change |
|
||||
| Délais d'une commande mesurés en **audio reçu**, pas à l'horloge | 2026-09-30 | trouvé en simulation : en rafale Wi-Fi, 2 s de son peuvent arriver en 10 ms ; l'horloge ne sert qu'aux gardes (réponse sans bilan, flux perdu 2 s) |
|
||||
| LED : éclair 100 ms / 2 s = écoute continue ; allumée = commande en cours (`cmd capture`) ; `StatusLed::blink(period, onMs)` | 2026-09-30 | l'utilisateur voit quand le micro part sur le réseau |
|
||||
| Droits : **complet > standard > limite** (`access_control.py`) ; inconnu = limite, enrôlé sans niveau = standard, `users: {Denis: complet}` | 2026-09-30 | niveaux du prompt ; conversation toujours permise |
|
||||
| Niveau complet seulement si **score ≥ 0,6** (`full_min_score`), sinon rétrogradé en standard | 2026-09-30 | la voix n'est pas une preuve : une identification de justesse ne doit pas tout ouvrir |
|
||||
| Règles par appareil/action ; **action absente = niveau complet** | 2026-09-30 | on ne permet jamais par oubli |
|
||||
| Actions sensibles (`confirm: [door.open]`) : question, puis « oui »/« non » **de la même personne**, sur la même carte, en 15 s ; toute autre phrase abandonne la demande | 2026-09-30 | garde-fou pour la porte et pour les agents Linux (15b) |
|
||||
| Droits appliqués au **dernier locuteur** de la session | 2026-09-30 | c'est lui qui attend la réponse (cohérent avec l'étape 10) |
|
||||
| Broker : `allow_anonymous false` + `password_file` + `acl_file` ; comptes `voice-server` (voice/# et home/# en lecture-écriture) et **un par carte, nom = DEVICE_ID** (`pattern ... voice/%u/...`) | 2026-09-30 | micro inaudible et commandes impossibles pour le reste du réseau ; une carte ne peut ni écouter ni usurper une autre (vérifié sur broker jetable) |
|
||||
| Outils `make mqtt-*` : identifiants du serveur relus dans `server/.env` (`-include`), recettes en `@` | 2026-09-30 | pas de compte supplémentaire ; mot de passe jamais affiché par make ; mot de passe hexadécimal (`#`, `$` cassent make) |
|
||||
| Présence serveur : `voice/server/status` retenu, `online` à chaque connexion, testament `offline`, `offline` publié à l'arrêt propre ; **seulement `MqttLink(presence=True)`** du vrai serveur | 2026-09-30 | ESP32 : serveur absent → `listenWanted=false`, plus de flux micro ; les outils (wav_to_mqtt) ne doivent pas faire croire au départ du serveur |
|
||||
| Topic de présence hors motif `voice/+/<suffixe>` des abonnements serveur | 2026-09-30 | sinon le serveur se prendrait pour une carte (écoute activée sur voice/server/control) |
|
||||
| Chien de garde ESP32 : TWDT reconfiguré **15 s, panique** ; loop (`enableLoopWDT`), tâches capture et audio inscrites ; réarmé entre deux envois réseau | 2026-09-30 | `NetworkClient::write` peut attendre 10 × 1 s (lu dans le core 3.x) : 5 s par défaut redémarrerait sur un Wi-Fi faible |
|
||||
| MQTT reste dans `loop()` (pas de tâche dédiée) ; connexion TCP 1 s (`setConnectionTimeout`) + CONNACK 2 s = 3 s de blocage au pire, broker absent seulement | 2026-09-30 | PubSubClient pas prévu pour plusieurs tâches ; audio dans ses propres tâches |
|
||||
| Wi-Fi relancé (`disconnect` + `begin`) après 30 s d'absence | 2026-09-30 | filet si la reconnexion automatique n'aboutit pas |
|
||||
| Journal serveur copié dans `logging.file` (`RotatingFileHandler` 1 Mo × 4), `logs/` ignoré par git | 2026-09-30 | Raspberry Pi sans écran ; les transcriptions y figurent = données personnelles |
|
||||
| Machines : **agent MQTT par machine** (`agent/voice_agent.py`, autonome : paho-mqtt + PyYAML), config YAML sur la machine | 2026-09-30 | demande utilisateur (config YAML) ; MQTT = transport unique (§3.7) ; SSH écarté : clés sur le serveur, liste blanche côté serveur seulement |
|
||||
| Liste blanche **sur la machine** : action = commande fixe en **liste** (pas de shell), le serveur n'envoie qu'un **nom** ; nom inconnu refusé | 2026-09-30 | un message forgé ne peut rien lancer d'autre ; config refusée si `command` est une chaîne |
|
||||
| Topics `agent/<id>/state` (retenu, **actions annoncées**, testament), `command` (QoS 1, non retenu, **session propre**), `result` | 2026-09-30 | le serveur sait si la machine répond et ce qu'elle accepte ; aucune commande rejouée au redémarrage |
|
||||
| « Allume » = **Wake-on-LAN** envoyé par le serveur (paquet magique UDP port 9, `broadcast` configurable), si agent hors ligne et MAC connue | 2026-09-30 | machine éteinte = pas d'agent |
|
||||
| Phrase qui **nomme une machine** traitée avant la domotique ; noms de plusieurs mots (« pc du bureau ») | 2026-09-30 | « éteins le PC du bureau » ≠ « éteins la lumière du bureau » (bureau est une pièce) |
|
||||
| Droits : `access.rules.machine` (wake/lock standard, shutdown/reboot complet), `confirm: [machine.shutdown, machine.reboot]` ; `MachineCommand` porte ses phrases (`said`, `asked`) | 2026-09-30 | réutilise étape 14 sans changer `DeviceCommand` ; `CommandRouter` aiguille vers Home/MachineController |
|
||||
| Service systemd **root** (`/opt/voice-agent`, `/etc/voice-agent/agent.yaml` 0600), `Restart=always` ; `agent/` autonome (`make -C agent install`) | 2026-09-30 | poweroff/reboot exigent root ; il suffit de copier le dossier sur la machine |
|
||||
| `dry_run` dans agent.yaml ; `make agent-run` crée `agent/agent.yaml` en mode essai | 2026-09-30 | tester « éteins le PC » sur le PC de développement sans l'éteindre |
|
||||
| **TLS non activé** (option de l'étape 15) ; **numéros de séquence** reportés à l'étape 16 (§3.4 du prompt : « phase 16 ») | 2026-09-30 | comptes + ACL ferment l'accès au micro ; TLS = certificats + ~40 Ko de RAM ESP32, à décider par l'utilisateur |
|
||||
| `make run` = **mode assistant** ; `make run-echo` = étape 5 | 2026-09-29 | une file + un fil de travail : les sessions sont traitées une à une, hors du fil réseau |
|
||||
|
||||
---
|
||||
|
||||
## 4. Fichiers existants (et qui les utilise)
|
||||
|
||||
```
|
||||
docs/BROCHAGE.md brochage, consommation, vérifications multimètre
|
||||
docs/wiring.svg schéma de câblage complet (source versionnable)
|
||||
firmware/include/config.h constantes matérielles + audio (étapes 1→16)
|
||||
firmware/include/secrets.h.example modèle Wi-Fi / MQTT (utilisé à partir de l'étape 3)
|
||||
firmware/src/MicCapture.* I2S RX stéréo + conversion 32→16 bits [étape 1]
|
||||
firmware/src/MicSelfTest.* auto-test du micro au démarrage [étape 1]
|
||||
firmware/src/PushButton.* bouton PTT anti-rebond [étape 1]
|
||||
firmware/src/StatusLed.* LED d'état [étape 1]
|
||||
firmware/src/SerialProtocol.h format des trames, partagé [étapes 1-2]
|
||||
firmware/src/SerialFramer.* émission des trames vers le PC [étape 1]
|
||||
firmware/src/SerialFrameReader.* réception des trames du PC [étape 2]
|
||||
firmware/src/AudioFifo.* FIFO réception → tâche audio (StreamBuffer) [étape 2]
|
||||
firmware/src/AudioOutput.* I2S1 TX vers MAX98357A, bip de test [étape 2]
|
||||
firmware/src/WifiLink.* Wi-Fi station, non bloquant [étape 3]
|
||||
firmware/src/MqttLink.* PubSubClient, testament, reconnexion [étape 3]
|
||||
firmware/src/VoiceSession.* START / chunks / END, MQTT ou série [étape 4]
|
||||
firmware/src/PlaybackStream.* flux de lecture + tâche audio + bilan [étape 5]
|
||||
firmware/src/CommandHandler.* JSON du topic control (start/end/ping) [étape 5]
|
||||
server/voice_server/audio_resampler.py WAV quelconque -> 16 kHz mono -3 dBFS [étape 5]
|
||||
server/voice_server/audio_sender.py START / chunks rythmés / END vers une carte [étape 5]
|
||||
tools/wav_to_mqtt.py WAV ou son pur -> carte, avec bilan [étape 5]
|
||||
server/voice_server/settings.py config.yaml + .env [étape 4]
|
||||
server/voice_server/mqtt_link.py paho-mqtt 2.x, abonnement voice/+/... [étape 4]
|
||||
server/voice_server/audio_buffer.py PCM borné, niveaux, écriture WAV [étape 4]
|
||||
server/voice_server/session_manager.py sessions, pertes, délais, WAV [étape 4]
|
||||
server/voice_server/__main__.py point d'entrée : make run [étape 4]
|
||||
server/voice_server/speech_to_text.py SpeechToText (faster-whisper) + Transcript [étape 6]
|
||||
server/voice_server/text_to_speech.py TextToSpeech (Piper → 16 kHz) + Speech [étape 6]
|
||||
server/voice_server/assistant.py Assistant à règles (heure, date…) [étape 6]
|
||||
server/voice_server/voice_pipeline.py VoicePipeline : STT → réponse → TTS → envoi [étape 6]
|
||||
tools/download_models.py make models [étape 6]
|
||||
tools/speech_check.py make speech-check / make transcribe [étapes 6-7]
|
||||
server/voice_server/voice_activity.py VoiceActivityDetector (Silero), find_segments [étape 7]
|
||||
tools/vad_check.py make vad / make vad-all [étape 7]
|
||||
server/voice_server/diarization.py Diarizer (pyannote), SpeakerTurn, timeline [étape 8]
|
||||
tools/diarize_check.py make diarize / make diarize-demo [étapes 8-10]
|
||||
server/voice_server/speaker_identifier.py SpeakerIdentifier, profils, cosinus [étape 9]
|
||||
server/voice_server/speaker_attribution.py attribute, Utterance, transcript_json [étape 10]
|
||||
server/voice_server/home_control.py parse_intent, decide, HomeController [étape 11]
|
||||
server/voice_server/text_normalize.py normalize (commun) [étape 11]
|
||||
firmware/src/DemoDevices.* LED GPIO 21 + volets/porte simulés [étape 11]
|
||||
server/voice_server/wake_word.py WakeWordDetector (openWakeWord ONNX) [étape 13]
|
||||
server/voice_server/hands_free.py HandsFreeListener : veille / commande / réponse [étape 13]
|
||||
server/voice_server/access_control.py AccessPolicy : niveaux, refus, confirmation [étape 14]
|
||||
tools/enroll.py make enroll / profiles / forget [étape 9]
|
||||
tools/say.py make say : phrase Piper en WAV + MP3 (téléphone) [étape 14]
|
||||
docs/MOSQUITTO.md broker : installation, pièges, vérification [étape 3]
|
||||
firmware/src/main.cpp boucle PTT → I2S → série [étape 1]
|
||||
tools/serial_protocol.py format des trames, partagé [étapes 1-2]
|
||||
tools/serial_to_wav.py réception des trames série → WAV [étape 1]
|
||||
tools/wav_inspect.py enveloppe ASCII et niveaux d'un WAV [étape 1]
|
||||
tools/wav_to_serial.py WAV ou son pur → ESP32, au rythme réel [étape 2]
|
||||
mosquitto/voice.conf configuration du broker [étape 3]
|
||||
mosquitto/acl droits par topic : serveur, une carte = ses topics [étape 15]
|
||||
mosquitto/passwd comptes hachés (make mqtt-user), NON versionné [étape 15]
|
||||
server/tests/test_mqtt_link.py présence, refus du broker, journal fichier [étape 15]
|
||||
server/voice_server/machine_control.py MachineController, phrases, Wake-on-LAN [étape 15b]
|
||||
server/voice_server/command_router.py CommandRouter : domotique / machines [étape 15b]
|
||||
agent/voice_agent.py agent Linux autonome (liste blanche, dry_run) [étape 15b]
|
||||
agent/agent.yaml.example modèle de /etc/voice-agent/agent.yaml [étape 15b]
|
||||
agent/voice-agent.service, agent/Makefile service systemd, make -C agent install [étape 15b]
|
||||
server/tests/test_machine_control.py, test_voice_agent.py [étape 15b]
|
||||
server/ squelette Python (modules à partir de l'étape 3)
|
||||
Makefile toutes les opérations système
|
||||
```
|
||||
|
||||
**Non encore écrits** (volontairement, par étape) : optimisation et numéros de séquence (16),
|
||||
TLS (option de l'étape 15, non activée).
|
||||
|
||||
---
|
||||
|
||||
## 5. Reprise après incident
|
||||
|
||||
1. Lire ce fichier, puis `README.md`.
|
||||
2. `make help` liste toutes les cibles.
|
||||
3. Vérifier l'étape en cours dans le tableau du §2, et **ne rien faire au-delà**.
|
||||
4. Contrôle rapide de l'environnement : `make test` (98 tests), `make lint`, `make fw-check`.
|
||||
|
||||
### État au 2026-09-29 au soir (fin de session)
|
||||
|
||||
- (état du 2026-09-29 au soir, dépassé : l'étape 9 a été validée le 2026-09-30, voir le journal.)
|
||||
- Étape 9 alors écrite et testée hors ligne. Étapes 1 à 8 validées.
|
||||
- Tout était vert à l'arrêt : 98 tests, pyright 0 erreur, `make fw-check` SUCCESS (RAM 36,7 %,
|
||||
Flash 31,3 %), `pip check` propre. Aucun serveur lancé.
|
||||
- `server/.venv` contient les extras `speech` et `diarization` (torch CPU) ; `server/models/`
|
||||
contient whisper `small`, la voix Piper et le pipeline pyannote (accès HF réglé, `HF_TOKEN` dans
|
||||
`server/.env` — **secret de l'utilisateur, ne jamais le modifier ni l'afficher**).
|
||||
- **Aucun profil vocal** : `server/profiles/` n'existe pas encore (mes essais étaient dans le scratchpad).
|
||||
- Firmware inchangé depuis l'étape 5 (déjà sur la carte). Broker : conteneur Docker de l'utilisateur, 192.168.1.104.
|
||||
|
||||
### Reprise de l'étape 9 (actions de l'utilisateur)
|
||||
|
||||
1. `make run`, puis 5 questions variées de 1 à 4 s, seul.
|
||||
2. Dans un autre terminal, sans arrêter le serveur : `make enroll NAME=Denis`.
|
||||
3. Nouvelles questions → le journal doit afficher `SPEAKER_00 = Denis (0.7x)`.
|
||||
4. Une autre voix (personne ou haut-parleur) → `Inconnu (…, proche de Denis)`.
|
||||
5. `make diarize-demo` → votre voix reconnue, Piper « Inconnu ».
|
||||
6. Analyser le journal collé, puis demander la validation de l'étape 9 avant l'étape 10
|
||||
(association locuteur + texte : `{"speaker": "Denis", "text": "..."}`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Vérifications déjà passées (sans matériel)
|
||||
|
||||
| Contrôle | Commande | Résultat |
|
||||
|---|---|---|
|
||||
| Compilation du firmware | `make fw-build` | **SUCCESS**, 0 warning avec `-Wall` ; RAM 7,2 % (23 536 o), Flash 24,7 % (324 182 o) |
|
||||
| Tests Python | `make test` | **162 tests** passés (dont le vrai pipeline pyannote et openWakeWord) (trames, sessions, sortie, assistant, boucle, VAD, diarisation, vrais modèles) |
|
||||
| Typage Python | `make lint` | 0 erreur, 0 warning (pyright, mode `standard`) |
|
||||
| Chaîne série → WAV | port série simulé, sinus 440 Hz 1 s | WAV 16 kHz / 16 bits / mono, 16 000 trames, crête 48,8 % |
|
||||
|
||||
Reste à valider **avec le matériel** : câblage, niveau du micro, intelligibilité.
|
||||
|
||||
## 7. Git
|
||||
|
||||
Dépôt initialisé le 2026-09-30 (branche `main`, aucun dépôt distant). Identité **locale au dépôt** :
|
||||
Denis Defolie <denis.defolie@gmail.com>. Commits et push seulement à la demande de l'utilisateur.
|
||||
Vérifié avant le premier commit : `secrets.h`, `.env`, `mosquitto/passwd`, `agent/agent.yaml`,
|
||||
profils vocaux, enregistrements (wav/mp3), modèles, journaux et environnements virtuels sont ignorés.
|
||||
`docs/test-suivi-perso.txt` (notes personnelles) ignoré à la demande de l'utilisateur ; ancien doublon
|
||||
`firmware/secrets.h.example` (modèle périmé, sans vrai secret) supprimé à sa demande.
|
||||
|
||||
## 8. Journal
|
||||
|
||||
- **2026-09-27** — Création du dépôt, ossature complète, étape 1 écrite (I2S RX + `serial_to_wav.py`).
|
||||
Compilation, tests et typage vérifiés. En attente de validation matérielle par l'utilisateur.
|
||||
- **2026-09-27** — Ajout de `docs/BROCHAGE.md` et `docs/wiring.svg` (schéma de câblage
|
||||
complet : micro, ampli, HP, bouton, LED, rails d'alimentation). Point ouvert reporté à
|
||||
l'étape 2 : si le niveau sonore est deux fois trop faible, forcer le mode « canal gauche »
|
||||
du MAX98357A par un 100 kΩ de `SD` vers GND.
|
||||
- **2026-09-28** — `docs/BROCHAGE.md` §6 : types et valeurs des condensateurs
|
||||
(C1 = 470 µF 16 V électrolytique faible ESR, C2 = 100 nF X7R, C3 = 100 nF anti-rebond
|
||||
facultatif), bornes mini/maxi justifiées par le calcul et lecture du marquage à 3 chiffres.
|
||||
Schéma mis à jour : C1 et C2 dessinés côte à côte sur le `Vin` de l'ampli.
|
||||
- **2026-09-29** — Matériel branché par l'utilisateur : le HP soufflait en permanence. Cause : à l'étape 1 les broches BCLK/LRC/DIN sont flottantes et le MAX98357A (SD tiré au + par 100 kΩ) amplifie ce bruit. Correctif : `silenceAmplifier()` dans `main.cpp`, appelé en tout premier dans `setup()`, force les trois broches à 0 → veille automatique de l'ampli. Firmware recompilé (SUCCESS). Reste le bruit pendant les ~300 ms de boot : option matérielle `SD` → GND, ou piloter `SD` par un GPIO à l'étape 2 (résout aussi le « pop » d'activation).
|
||||
- **2026-09-29** — Premier enregistrement réel : crête sous 2 %. `MIC_GAIN` passé de 4 à **16** (dimensionnement : INMP441 à -26 dBFS/94 dB SPL, voix à 30 cm ≈ 80 dB SPL crête → -40 dBFS ≈ 1 % ; ×16 → ~16 %, soit 16 dB de marge). `tools/serial_to_wav.py` affiche désormais crête + RMS + offset continu en dBFS, distingue « aucun signal » (câblage) de « niveau faible » (gain) et propose le facteur correctif. 6 tests, pyright propre, firmware recompilé.
|
||||
- **2026-09-29** — L'utilisateur confirme le câblage. Ajout d'un **auto-test matériel** dans le firmware : `MicCapture::probe()` / `stats()` relèvent min/max/zéros des **mots I2S bruts**, `reportMicHealth()` (main.cpp) imprime le verdict sur la série au démarrage (bruit ambiant) et à la fin de chaque session (parole, avec la valeur de `MIC_GAIN` conseillée). Distingue : aucun mot lu (I2S muet) / mots figés (L/R ou SD) / amplitude mesurable (question de gain). `make fw-upload && make fw-monitor` suffit désormais à diagnostiquer sans le PC côté WAV.
|
||||
- **2026-09-29** — **Cause trouvée** : l'auto-test renvoyait 39 200 mots I2S *tous exactement nuls*, avec deux micros différents et un câblage vérifié. Signature de la lecture de la mauvaise moitié de trame : l'INMP441 sort des zéros pendant la moitié qu'il n'utilise pas, et la correspondance `L/R` ↔ `I2S_STD_SLOT_LEFT/RIGHT` n'est pas celle attendue sur l'ESP32 d'origine. Correctif : `MicCapture::useSlot()` (reconfiguration à chaud via `i2s_channel_reconfig_std_slot`) et `selectMicSlot()` dans `main.cpp`, qui écoute les deux moitiés au démarrage et garde celle qui porte du signal. **Décision figée : ne jamais supposer le mapping des slots I2S sur ESP32, le mesurer.**
|
||||
- **2026-09-29** — Les deux moitiés de trame sont nulles → la piste « slot » ne suffit pas. Vérification faite dans les en-têtes ESP-IDF installés (`framework-arduinoespressif32-libs/esp32/include/esp_driver_i2s/.../i2s_std.h`) : la configuration Philips 32 bits mono est correcte pour l'ESP32, le bug n'est pas là. Deux corrections : (1) l'auto-test affichait `amplitude 0` sans distinguer « mots nuls » de « aucun mot lu » → il imprime désormais le **nombre de mots** ; (2) `selectMicConfiguration()` explore les **4 combinaisons** {SCK/WS normaux, permutés} × {slot gauche, droite}, car une permutation SCK/WS donne exactement le même symptôme (le micro reçoit 16 kHz au lieu de 1 MHz et n'émet rien). En cas d'échec des 4, le firmware imprime un protocole de mesure au voltmètre et laisse les horloges tourner sur les broches du schéma.
|
||||
- **2026-09-29** — **Le micro capte.** Session réelle : 24 000 mots, amplitude 1 547 300 LSB24 (**-14 dBFS brut**), soit 6 044 LSB sur 16 bits avant gain. `MIC_GAIN` passé de 16 à **1** (crête ≈ 18 % de pleine échelle). Le calcul théorique datasheet (-26 dBFS @ 94 dB SPL → -40 dBFS attendus) sous-estimait le niveau réel de **26 dB**. **Décision figée : pour les niveaux audio, mesurer sur le banc, ne pas extrapoler du datasheet.** Point à confirmer : quelle combinaison a été retenue par `selectMicConfiguration()` (SCK/WS normaux ou permutés) — si permutés, corriger le câblage ou le schéma.
|
||||
- **2026-09-29** — **Fausse piste corrigée.** L'auto-test avait retenu « SCK/WS permutés » avec une amplitude de 16,7 M LSB24, soit ~100 % de la pleine échelle 24 bits (8 388 608) **au repos** : ce n'était pas du signal mais du **bruit pleine échelle d'une ligne de données flottante**. Mon critère « garder la plus grande amplitude » était faux. Trois corrections : (1) `MicCapture` lit désormais en **stéréo** (`I2S_SLOT_MODE_STEREO`) et choisit la moitié de trame en logiciel — la sélection de slot par le périphérique est ambiguë sur l'ESP32 et testait probablement deux fois la même moitié ; (2) convention d'amplitude unifiée (crête = demi-écart crête à crête, en LSB24) ; (3) garde-fou `NOISE_FLOOR_LIMIT` = 50 % de pleine échelle : au repos, au-delà c'est une ligne flottante, la candidate est rejetée et le message désigne `SD`/alimentation. **Décision figée : un niveau proche de la pleine échelle au repos est un défaut, jamais un succès.**
|
||||
- **2026-09-29** — Mesures utilisateur : **VDD = 3,29 V**, **GPIO 25 et 26 = 1,64 V** (≈ VDD/2, signature d'un carré 50 % → les deux horloges tournent et arrivent). Alimentation et horloges donc écartées ; seule reste la ligne `SD` → GPIO 33. Ajout de `MicCapture::probeDataLine()` : applique le pull-up puis le pull-down interne (~45 kΩ) et échantillonne la broche 4000 fois, horloges actives. Une sortie logique (quelques ohms) écrase le pull : si les lectures **suivent** le pull (100 % puis 0 %), la ligne flotte ; sinon elle est pilotée. `gpio_get_level()` fonctionne même quand la broche est routée vers l'I2S, d'où un test sans démonter la configuration.
|
||||
- **2026-09-29** — Test électrique : « ligne PILOTEE ». Mais ce verdict confondait deux cas très différents : *ligne active* et *ligne figée à un niveau fixe* (un fil posé sur GND ou L/R donne aussi « pilotée »). Verdicts désormais séparés en quatre : flottante / tenue à 0 / tenue à 1 / porte des transitions. Ajout de `MicCapture::readRaw()` + `dumpRawFrames()` : vidage hexadécimal de 12 trames (les deux moitiés côte à côte). C'est la seule observation qui distingue un décalage de bits d'une ligne morte. **Les pourcentages pull-up/pull-down sont l'information clé et doivent être relevés.**
|
||||
- **2026-09-29** — Lecture de la **structure** des relevés : en mode « permuté », une moitié de trame porte des données et l'autre est *exactement* nulle — signature d'un INMP441 correctement cadencé (`L/R` fixe la moitié d'émission). Du bruit de ligne flottante serait présent dans les deux moitiés. Donc le micro fonctionne, et les fils `SCK`/`WS` ne sont pas sur les broches du schéma. Deux corrections : (1) `MIC_SETTLE_MS = 250` — le transitoire de mise en route de l'INMP441 faussait toutes les mesures d'ambiance (0, puis -8, puis -15 dBFS d'un essai à l'autre) ; `begin()` jette désormais ce début ; (2) le firmware annonce les **GPIO réels** retenus pour SCK et WS au lieu du mot « permuté », et affiche les mots bruts si le niveau au repos dépasse 10 % de la pleine échelle. **Point ouvert : déterminer si c'est le câblage ou le schéma du dépôt qui doit changer.**
|
||||
- **2026-09-29** — **Brochage corrigé.** Le banc est câblé `SCK`→GPIO 25 et `WS`→GPIO 26, soit l'inverse du schéma initial. GPIO 25 et 26 étant équivalents (aucune fonction de strapping, tous deux en sortie), la documentation a été alignée sur le matériel plutôt que l'inverse. Modifiés : `config.h`, `docs/wiring.svg`, `docs/wiring.png`, `docs/BROCHAGE.md` (§1.1, ASCII, §9, §10), `README.md`, `README-EN.md`, `tools/serial_to_wav.py`. Vérifs : firmware SUCCESS, 6 tests, pyright 0 erreur, SVG re-rendu et relu.
|
||||
- **2026-09-29** — `aplay` échouait avec « unable to open slave » : rien à voir avec le WAV. Le PCM ALSA `default` passe par `dmix`, dont l'esclave est la **carte 0** = `C110`, un périphérique USB de **capture seule** (`pcm0c`, aucun `pcm*p`). D'où l'ENOENT. `paplay` (PulseAudio) fonctionne ; `aplay -D plughw:1,0` est refusé car PulseAudio tient la carte, et `aplay -D pulse` échoue (plugin `libasound2-plugins` absent). Ajout de `make play` (essaie paplay, pw-play, ffplay, aplay) et surtout de **`tools/wav_inspect.py`** + `make inspect` : enveloppe ASCII, crête/RMS/offset et détection d'alternance silence/parole — **valider un enregistrement sans écouter**, ce qui servira aussi sur le Raspberry Pi sans écran. 8 tests, pyright 0 erreur.
|
||||
- **2026-09-29** — **Bug trouvé : perte d'alignement de trame.** L'auto-test voyait du signal mais le WAV enregistré était fait de zéros exacts. Cause : en lecture stéréo, je déduisais la moitié de trame de la **parité de l'indice dans le tampon** (`_scratch[i*2]` / `[i*2+1]`). Or `i2s_channel_read()` rend ce qui est disponible, pas forcément un nombre pair de mots — et l'ancien `flush()` (lectures à vide) pouvait s'arrêter au milieu d'une trame. Les deux moitiés s'échangeaient alors, et `read()` prenait la moitié muette. Corrections : (1) `_phase` mémorisée entre les appels et avancée **mot par mot** dans `read()`, `probe()` et `readRaw()` ; (2) `flush()` fait désormais `i2s_channel_disable()` + `enable()`, ce qui vide le DMA **et** garantit que le prochain mot est un début de trame ; (3) `begin()` termine par `flush()`. **Décision figée : ne jamais déduire un canal d'une parité d'indice sur une API en flux d'octets.**
|
||||
- **2026-09-29** — Diagnostic enfin net et cohérent : test électrique **100 % / 0 %** → la ligne suit exactement le pull interne, donc **GPIO 33 flotte, rien ne le pilote**. Le « bruit pleine échelle » vu sur une moitié de trame est du couplage capacitif depuis les pistes d'horloge voisines, capté par une entrée non connectée — d'où son rejet par `NOISE_FLOOR_LIMIT`. Alimentation (3,29 V) et horloges (1,64 V) déjà validées au voltmètre : le seul maillon restant est le fil `SD` micro → GPIO 33. Correction d'un défaut de mon diagnostic : le vidage hexa s'exécutait sur la configuration *permutée* (dernière de la boucle) et montrait donc des zéros non représentatifs ; on revient maintenant au câblage documenté avant de diagnostiquer. Ajout d'un protocole de **continuité** (§8 de BROCHAGE.md), à mesurer sur la **pastille** du module et non au bout du fil.
|
||||
- **2026-09-29** — Les 3 continuités sont bonnes (`SD` ↔ GPIO 33, `L/R` ↔ GND, pas de court-circuit). Toutes les hypothèses de câblage côté connexions sont donc épuisées. Deux pistes restantes : (a) les signaux atteignent l'embase ESP32 mais **pas les pastilles du module** (soudure sèche) — à vérifier en mesurant SCK/WS/VDD **sur le module** ; (b) l'horloge bit de 1,024 MHz (16 kHz × 64) est trop lente pour ce module : certains INMP441 de contrefaçon ne démarrent qu'au-dessus de ~2 MHz. L'auto-test balaie désormais **2 fréquences × 2 câblages × 2 moitiés**, et `startSession()` annonce la **fréquence réellement retenue** dans l'en-tête de session (le WAV reste donc correct si 48 kHz est nécessaire).
|
||||
- **2026-09-29** — **Relecture complète de l'historique : j'avais tiré une conclusion fausse.**
|
||||
Le verdict « FLOTTE » (ligne SD) avait été mesuré alors que la configuration active était la
|
||||
dernière de la boucle d'essais, c'est-à-dire horloges **inversées** pour le câblage réel : micro
|
||||
non cadencé, donc muet. Le verdict « PILOTÉE » de l'essai précédent avait, lui, été mesuré
|
||||
horloges **correctes**. Conclusion juste : **le micro pilote bien sa ligne SD**. Les tests de
|
||||
continuité demandés ensuite étaient superflus (tous bons). J'avais repéré ce défaut pour le
|
||||
vidage hexa, pas pour le test électrique qui avait le même.
|
||||
Ce que tous les essais montrent de façon constante : **horloges correctes → une moitié de trame
|
||||
porte des valeurs énormes et erratiques au repos (0, -8, -14, -15 dBFS), l'autre est exactement
|
||||
nulle ; horloges inversées → tout est nul.** Hypothèses restantes, par ordre :
|
||||
(H1) **mot décalé d'un bit** (cadrage Philips vs MSB) : le bit 31 devient aléatoire et un signal
|
||||
faible ressemble à un bruit pleine échelle ;
|
||||
(H2) **quelques mots parasites** qui font exploser la mesure crête (max − min), métrique fragile ;
|
||||
(H3) **pull-down de 100 kΩ absent** sur SD, pourtant exigé par le datasheet (sortie haute impédance
|
||||
hors moitié de trame).
|
||||
Réponse firmware : nouvelle classe **`MicSelfTest`** (main.cpp passe de 437 à ~150 lignes) ;
|
||||
balayage {16 kHz, 48 kHz} × {Philips, MSB} × {2 moitiés} ; critère = **valeur efficace au repos**
|
||||
(< -20 dBFS) + **cohérence du bit de signe** (bit31 = bit30 dans > 99 % des mots), plus jamais
|
||||
l'amplitude crête ; **pull-down interne activé sur SD** (`MIC_SD_PULLDOWN`) ; analyse **bit à bit**
|
||||
(taux de 1 par position) + vidage hexa en configuration nominale ; test électrique refait
|
||||
**horloges correctes**. Le balayage SCK/WS permutés est supprimé : câblage vérifié.
|
||||
- **2026-09-29** — **H1 observée directement : mot décalé d'un bit en cadrage Philips.**
|
||||
Analyse bit à bit (16 kHz Philips) : 1re moitié = `F806B200`… avec **9 zéros** en bas au lieu de 8
|
||||
(bit 8 à 0 sur 8 mots sur 8), 2e moitié = `00000001` en permanence. Lecture : le premier bit émis
|
||||
par le micro (signe) est échantillonné un coup d'horloge trop tard et **déborde dans le dernier bit
|
||||
du mot précédent** ; les 23 bits restants sont décalés d'un rang. Confirmation croisée : à 48 kHz
|
||||
en cadrage **MSB**, signe incohérent seulement 0,5 % et 2e moitié exactement nulle.
|
||||
**Bug de mon critère** : la 2e moitié constante `00000001` avait été retenue comme « plausible »
|
||||
(non nulle, RMS nulle après `>> 12`, signe cohérent) → un enregistrement aurait été silencieux.
|
||||
Corrections : refus des constantes (`minWord == maxWord`) ; statistiques séparant **composante
|
||||
continue** (`sum`) et **variations** (écart type) — seul l'écart type juge le bruit de repos ;
|
||||
plancher à -110 dBFS ; `MIC_SETTLE_MS` 250 → **600 ms** (l'INMP441 s'endort à chaque arrêt des
|
||||
horloges et chaque réveil laisse une composante continue lente, -30 dBFS encore visible à 250 ms) ;
|
||||
analyse bit à bit faite sur la configuration **retenue**, en régime établi (trames du milieu).
|
||||
**À confirmer par le prochain essai : cadrage MSB attendu comme configuration retenue.**
|
||||
- **2026-09-29** — Essai suivant : **16 kHz Philips encore retenu à tort**, toujours décalé (9 zéros,
|
||||
`00000001` dans l'autre moitié). Mon test de cohérence du signe (bit31 = bit30) est **aveugle sur un
|
||||
signal faible** : toute la partie haute du mot vaut `1`, en perdre un ne change rien. Il ne détecte le
|
||||
décalage que sur un signal fort. Nouveau critère, indépendant de l'amplitude : **nombre de bits
|
||||
utiles** = 32 − ctz(OU de tous les mots) ; avec le pull-down, un INMP441 bien cadré donne
|
||||
**exactement 24**. Validé hors carte sur les mots réels du banc (g++) : Philips → 23 (décalé), mêmes
|
||||
échantillons recadrés MSB → 24. Valeurs recadrées ≈ −227 000 LSB24 (composante continue de réveil,
|
||||
≈ −31 dBFS) avec des écarts de quelques milliers : audio sain. Mesures au repos en MSB : continu
|
||||
−43 dBFS, variations −44 dBFS (16 kHz). **Décision attendue : cadrage MSB retenu automatiquement.**
|
||||
- **2026-09-29** — **Cadrage MSB confirmé** sur le banc : 24 bits utiles (8 zéros exacts), 2e moitié
|
||||
exactement nulle. Mais l'auto-test échouait : les mots au repos valaient +1 950 000 (**23 % de pleine
|
||||
échelle**) en montant d'échantillon en échantillon. Cause : **le micro se réveillait à chaque essai**.
|
||||
L'INMP441 s'endort dès que ses horloges s'arrêtent, et mon auto-test les arrêtait entre chaque
|
||||
configuration ; chaque mesure « au repos » mesurait donc un réveil, pas la pièce (d'où −43 dBFS un jour,
|
||||
−13 dBFS le suivant). Pire : `flush()` faisait `disable()`/`enable()`, donc **chaque session
|
||||
d'enregistrement aurait démarré par ce transitoire** (« boum » grave).
|
||||
Refonte : (1) **un seul réveil** au démarrage, lu en MSB, `MIC_SETTLE_MS` = 1500 ; (2) décision
|
||||
**purement structurelle** : moitié ni nulle ni constante, **24 bits utiles** → MSB ; **25** → micro
|
||||
Philips, bascule et revérification ; le niveau au repos n'est plus qu'une information ; (3) `flush()`
|
||||
vide le DMA **par lecture**, sans arrêter les horloges, en suivant la phase, et **amorce le filtre de
|
||||
continu** avec la moyenne mesurée ; (4) toutes les lectures traitent les mots reçus **avant** de tester
|
||||
l'erreur (une lecture interrompue ne décale plus la phase) ; (5) `readRaw()` rend toujours des trames
|
||||
entières ; (6) suppression du balayage 48 kHz et de l'incohérence de signe (détecteur aveugle au repos) ;
|
||||
(7) `delay(1500)` au démarrage pour que le moniteur série ne perde plus le début de l'auto-test.
|
||||
Hypothèse de robustesse notée : quand personne ne lit entre deux sessions, le DMA déborde ; les
|
||||
descripteurs contiennent des trames entières (200 × 2 mots) et les lectures sont de taille paire, la
|
||||
parité — donc l'alignement — est conservée.
|
||||
- **2026-09-29** — **Auto-test réussi sur le banc** : 16 kHz, cadrage MSB, 1re moitié, 24 bits utiles,
|
||||
2e moitié strictement nulle, ligne SD pilotée. Mots bruts `FE466E00`… → échantillons ≈ −111 000 ± 2 000
|
||||
LSB24 (continu ≈ −37 dBFS). Niveau au repos mesuré −37 dBFS de variations pendant l'auto-test, mais
|
||||
l'analyse bit à bit faite juste après montre des échantillons confinés dans [−131 000 ; −98 000], soit
|
||||
≈ −54 dBFS : le micro finissait de se stabiliser, 1,5 s après son réveil. Sans conséquence pour les
|
||||
sessions (horloges jamais arrêtées ensuite). **Reste à valider : enregistrement réel + `make inspect`.**
|
||||
- **2026-09-29** — **Premier enregistrement audible** (entendu au casque ; `paplay` fonctionne, sortie
|
||||
analogique par défaut). Analyse spectrale du WAV (numpy) : **87 % de l'énergie sous 20 Hz**, voix
|
||||
réelle mais à −42 dBFS dans la bande 300–3 400 Hz. Chaîne de capture saine : aucun saut > 382 LSB,
|
||||
aucune longue suite de valeurs identiques, donc aucun glissement de phase. Cause la plus probable :
|
||||
mécanique (plaque d'essai qui fléchit sous le doigt tenant le bouton) + souffle. L'ancien filtre
|
||||
(`MIC_DC_SHIFT = 12`, 0,6 Hz) ne retirait que le continu. Simulation de plusieurs réglages sur le WAV
|
||||
réel : 1 étage à 80 Hz suffit (infrasons 0,6 %, voix 99,1 %) ; 2 étages n'apportent presque rien.
|
||||
Implémenté : `MIC_HPF_SHIFT = 5` (80 Hz), filtre calculé **en 24 bits** (en 16 bits, zone morte de
|
||||
31 LSB), gain appliqué avant la réduction 24 → 16 bits avec arrondi, **`MIC_GAIN = 4`** (mesuré après
|
||||
filtre). La recommandation de gain en fin de session porte désormais sur la **crête après passe-haut**
|
||||
(`RawStats::filteredPeak`), plus sur les mots bruts. Vérifié hors carte (g++, arithmétique identique au
|
||||
firmware) : écart à la théorie du 1er ordre < 0,3 dB, −29 dB à 3 Hz, < 0,4 dB au-dessus de 300 Hz.
|
||||
**Étape 1 : critère « audible, sans saturation » atteint ; confirmation attendue sur le firmware final.**
|
||||
- **2026-09-29** — **Étape 1 validée par l'utilisateur** (« le son est vraiment bien »). Le WAV validé
|
||||
provient encore de l'ancien firmware (dérive continue +0,3 % → +4,6 %, impossible avec le passe-haut
|
||||
à 80 Hz) : le filtre 80 Hz sera vérifié sur le matériel avec le firmware de l'étape 2.
|
||||
- **2026-09-29** — **Étape 2 écrite.** Firmware : `AudioOutput` (I2S1 TX, 16 bits stéréo dupliqué,
|
||||
Philips par défaut via `AMP_FORMAT_PHILIPS`, `auto_clear`, bip 440 Hz −12 dBFS au démarrage = test de
|
||||
la sortie seule), `AudioFifo` (StreamBuffer statique 16 Ko, tout ou rien), `SerialFrameReader`
|
||||
(machine à états), tâche audio statique cœur 1 priorité 5, `SerialProtocol.h` partagé ;
|
||||
`silenceAmplifier()` supprimé (l'I2S tient les broches dès la 1re ligne de `setup()`) ; `loop()` sert
|
||||
la lecture même si le micro est en défaut. PC : `serial_protocol.py` extrait, `wav_to_serial.py`
|
||||
(WAV ou `--tone`, avance 200 ms puis 1 chunk / 50 ms), cibles `make play-esp` et `make tone-esp`.
|
||||
Vérifs : firmware SUCCESS 0 warning (RAM 17,4 %) ; **test croisé** encodeur Python → décodeur C++
|
||||
compilé sur PC (texte parasite, faux mot magique, somme fausse, longueur excessive : 4 trames
|
||||
valides, 2 rejetées) ; outil PC sur pseudo-terminal : 4 chunks d'avance puis 50 ± 1 ms ; 15 tests
|
||||
pytest (dont un qui a trouvé un vrai bug : dernier chunk de longueur impaire), pyright 0 erreur.
|
||||
**Non vérifiable sans matériel** : cadrage Philips réel du MAX98357A côté ESP32, bruit de
|
||||
l'ampli cadencé en permanence sur le micro, absence de coupure sur la carte.
|
||||
- **2026-09-29** — **Étape 2 validée par l'utilisateur** : bip, son pur sans clic, lecture de la voix,
|
||||
contrôle du micro — tout passe. Seul point : **niveau du HP faible mais audible**. Explication :
|
||||
bip à −12 dBFS par choix, et le WAV rejoué (étape 1, ancien firmware, gain 1) avait sa voix vers
|
||||
−30 dBFS crête. Pistes, sans code : mesurer `Vin` sur la pastille de l'ampli (≥ 4,5 V), rejouer un
|
||||
enregistrement fait avec le firmware actuel (gain 4 → +12 dB), `GAIN` à GND (+6 dB, 15 dB),
|
||||
enceinte pour le HP. Correctif de fond prévu : **normaliser côté serveur** l'audio envoyé (étape 5,
|
||||
puis la voix Piper à l'étape 6), l'ESP32 restant simple. Prochaine étape : 3 (Mosquitto + topics).
|
||||
- **2026-09-29** — **Étape 3 écrite.** Constat en arrivant : le broker du conteneur
|
||||
`mosquitto_services-mosquitto-1` redémarrait en boucle (**378 redémarrages**). Sa configuration,
|
||||
écrite à la main hors du dépôt, déclarait **deux `listener 1883`** : `Error: Address in use`,
|
||||
reproduit dans un conteneur jetable. Le journal restait muet car `log_dest syslog` (dans MA
|
||||
`voice.conf`) n'écrit nulle part dans un conteneur ; `message_size_limit` était aussi obsolète.
|
||||
L'IP donnée par l'utilisateur (192.168.0.104) ne correspond pas à la machine : **192.168.1.104**.
|
||||
`voice.conf` corrigé (`log_dest stdout`, `max_packet_size`, pièges documentés) et **testé dans un
|
||||
broker jetable** : état `online` retenu, testament `offline` publié à la coupure brutale du
|
||||
client, et reçu par un abonné arrivé après coup. Le conteneur de l'utilisateur n'a **pas** été
|
||||
modifié (hors dépôt, fichiers de l'uid 1883) : cible `make mosquitto-docker` fournie.
|
||||
Firmware : `WifiLink`, `MqttLink` (PubSubClient 2.8 épinglé, tampon 2048, keepalive 10 s, socket
|
||||
2 s, essais toutes les 5 s), état JSON retenu toutes les 30 s (IP, RSSI, uptime, micro, ampli),
|
||||
événement `boot` avec la cause du redémarrage (dont `brownout`), abonnement `control` journalisé ;
|
||||
secrets transmis par constructeur, `#error` explicite si `secrets.h` manque ; Wi-Fi démarré
|
||||
**après** l'auto-test du micro. Partitions `huge_app` (Flash 74,5 % → 31 %). Build SUCCESS
|
||||
0 warning (avec un `secrets.h` temporaire, supprimé). Cibles `mqtt-watch`, `mqtt-control`
|
||||
(clients de l'image Docker si `mosquitto-clients` absent). `docs/MOSQUITTO.md` créé.
|
||||
Correction faite dans la doc : un reset d'ESP32 ne ferme PAS la connexion TCP (testament en ≤ 15 s).
|
||||
- **2026-09-29** — Étape 3, premiers résultats : broker réparé par l'utilisateur **avec sa propre
|
||||
config** (pas `voice.conf` : `log_dest file`, `log_type all`, `message_size_limit`) — fonctionne,
|
||||
mais `log_type all` journalisera chaque chunk audio à l'étape 4 → passer à `voice.conf` avant.
|
||||
ESP32 : Wi-Fi « BPI_R3-2G » en 4,8 s, IP 192.168.1.23, **RSSI −76 à −78 dBm (faible)**, MQTT connecté.
|
||||
Vérifié directement sur le broker : un abonné tardif reçoit aussitôt l'état retenu (uptime 158 s),
|
||||
republié 30 s plus tard (188 s). Auto-test micro : variations au repos −46 dBFS (Wi-Fi démarré
|
||||
après). Restent à confirmer : message `control` reçu, `offline` après coupure d'alimentation.
|
||||
- **2026-09-29** — **Étape 3 validée par l'utilisateur.** `mqtt-watch` : état périodique, `control`
|
||||
relayé par le broker, `offline` publié par le testament à la coupure d'alimentation, puis `online`
|
||||
(uptime 12 s) et `event boot reason=poweron` au retour. Avant l'étape 4 : passer le broker sur
|
||||
`voice.conf` (`make mosquitto-docker`) et améliorer le RSSI (−75 à −79 dBm, viser > −70).
|
||||
- **2026-09-29** — **Étape 3 validée ; étape 4 écrite.** Firmware : `VoiceSession` (START / chunks /
|
||||
END en JSON sur `event`, PCM brut sur `audio/in`, id de session 6 hex), **tâche de capture** dédiée
|
||||
+ FIFO de capture 32 Ko (1 s) : `loop()` ne lit plus le micro, il vide la FIFO vers MQTT ; `AudioFifo`
|
||||
à stockage fourni ; `MqttLink::publishBinary`. RAM 36,7 %, Flash 31,1 %, 0 warning.
|
||||
Serveur (premier code) : `settings`, `mqtt_link`, `audio_buffer`, `session_manager`, `__main__` ;
|
||||
`make run`. Verdicts : complete, lost_chunks, timeout, interrupted. 30 tests pytest, pyright 0.
|
||||
**Essai de bout en bout sur le vrai broker** avec une carte simulée (paho, rythme réel) :
|
||||
complete 40/40 ✓, timeout 20/? ✓, mais la session à 3 chunks perdus était classée `interrupted`
|
||||
(nouveau START arrivé pendant le délai de grâce) → **bug corrigé**, test de régression ajouté,
|
||||
scénario rejoué : `lost_chunks 37/40` + alerte, puis session suivante `complete`.
|
||||
Pas de `tools/mqtt_to_wav.py` : le serveur remplit ce rôle ; à réévaluer avec `wav_to_mqtt.py` (étape 5).
|
||||
- **2026-09-29** — **Incident causé par moi : `firmware/include/secrets.h` de l'utilisateur détruit.**
|
||||
Mes compilations de contrôle faisaient `cp secrets.h.example secrets.h ; make fw-build ; rm -f secrets.h`.
|
||||
Sans conséquence à l'étape 3 (fichier pas encore créé) ; à l'étape 4, la copie a écrasé le vrai
|
||||
fichier puis `rm` l'a supprimé (dossier modifié à 17:02). Symptôme chez l'utilisateur :
|
||||
`'WIFI_SSID' was not declared` (le `#error` s'affiche bien, mais GCC continue et ajoute cette
|
||||
erreur). **Correctif de méthode** : cible `make fw-check`, secrets factices dans un `mktemp -d`
|
||||
via `PLATFORMIO_BUILD_FLAGS=-I<tmp>`, `firmware/include/` jamais touché. Règle aussi notée en
|
||||
mémoire. L'utilisateur doit recréer son `secrets.h` (SSID relevé dans ses journaux : « BPI_R3-2G »,
|
||||
broker 192.168.1.104 déjà dans le modèle).
|
||||
- **2026-09-29** — **Étape 4 validée** : 4 phrases de 4 à 6 mots, intelligibles au casque. Analyse des
|
||||
4 WAV serveur : 1,8 à 4,35 s, nombres entiers de chunks (36, 50, 87, 56), crête −10,4 à −15,3 dBFS
|
||||
(gain ×4 dans la cible), **offset ≈ 0 %** et infrasons 0 à 0,3 % (contre +3 % et 87 % avant) :
|
||||
**passe-haut 80 Hz validé sur le matériel**. Voix 99,4 à 99,9 % de l'énergie.
|
||||
Prochaine étape : 5 (serveur → MQTT → ESP32).
|
||||
- **2026-09-29** — **Étape 4 validée ; étape 5 écrite.** Firmware : `PlaybackStream` (lecture série ou
|
||||
MQTT, tâche audio, bilan `played` publié sur `event`), `CommandHandler` (start/end/ping, ArduinoJson
|
||||
7.2.1), abonnement `audio/out`. Build 0 warning, RAM 36,7 %. Serveur : `AudioResampler` (8/16/24/32
|
||||
bits, toute fréquence, mono, soxr HQ, crête −3 dBFS), `AudioSender` (300 ms d'avance, rythme réel),
|
||||
`MqttLink.publish/wait_connected`, `SessionManager(on_finished=...)`, `--echo` (fil de lecture dédié,
|
||||
hors fil paho). Outil `tools/wav_to_mqtt.py`, cibles `tone-mqtt`, `play-mqtt`, `run-echo`.
|
||||
41 tests (dont un qui a trouvé qu'un WAV tronqué levait `EOFError` au lieu du message ffmpeg), pyright 0.
|
||||
**Essais de bout en bout sur le broker réel** (carte simulée « esp32-test ») : son pur 40/40, écart max
|
||||
51 ms, crête −3,0 dBFS, bilan reçu ; écho : session 30/30 à −24,3 dBFS renvoyée à −3 dBFS, 30/30.
|
||||
**Incident causé par moi** : mon serveur de test a repris l'identifiant `voice-server` du `make run`
|
||||
de l'utilisateur (lancé à 17:16) → ~16 s d'éjections mutuelles ; son serveur a enregistré
|
||||
`server/recordings/esp32-test_20260929-173525_voix01.wav` (artefact de test, 0,5 s, supprimable).
|
||||
Correctifs : identifiant de test distinct, indice « identifiant en double ? » dans le journal de
|
||||
déconnexion, règle mémorisée. Le `make run` de l'utilisateur tourne encore l'ancien code (étape 4).
|
||||
- **2026-09-29** — Étape 5, premier essai utilisateur : écho une fois (phrase coupée), puis rien. Journal
|
||||
serveur : session `39ecba` ouverte et close en **72 ms, 0 chunk** → le bouton est lu relâché pendant
|
||||
l'appui (contact intermittent sur la plaque ? carte déplacée pour le Wi-Fi ?). Le serveur **ignorait en
|
||||
silence** les sessions < 0,2 s pour l'écho (défaut de ma part). Ligne `en ligne` 12 s après la
|
||||
session : republication périodique ou **redémarrage** — indécidable, le journal n'affichait pas
|
||||
l'uptime. Instrumentation ajoutée : raison de fin de session (« bouton relache » / « duree
|
||||
maximale »), alerte si session < 300 ms, purge des événements de bouton périmés (un appui mémorisé
|
||||
pendant une session relançait une session fantôme), uptime dans le journal d'état serveur,
|
||||
événement `boot` journalisé avec sa cause (WARNING si brownout/panic/watchdog), écho ignoré annoncé.
|
||||
- **2026-09-29** — Étape 5, second essai (instrumenté) : **diagnostic Wi-Fi confirmé par les chiffres**.
|
||||
RSSI −81 à −75 dBm. Session 833429 : ~108 chunks captés, 42 envoyés, **66 perdus faute de place**
|
||||
(FIFO de capture 1 s débordée : envoi Wi-Fi bloqué plusieurs secondes) ; écho 11/42 chunks, END
|
||||
perdu (reconnexion MQTT pendant l'écho : `offline` puis `online` 130 ms après, uptime continu 102→107 s,
|
||||
donc **pas de redémarrage**). Sessions suivantes 55/55 et 76/76 (4 perdus), échos complets mais 35 et
|
||||
107 sous-alimentations (≈ 2 s de trous sur 3,8 s). Serveur toujours à l'heure (retard d'envoi 0 ms).
|
||||
Bouton : plus aucun problème (appuis de 2 à 5 s). **Décision : ne pas masquer côté logiciel** (le
|
||||
tampon anti-gigue est l'étape 12) ; expérience demandée : ESP32 à 1–2 m du point d'accès. Cibles :
|
||||
> −65 dBm fiable, −65 à −72 sous-alimentations ponctuelles, < −72 pertes. Piste matérielle si
|
||||
l'emplacement ne peut pas changer : ESP32-WROOM-32U + antenne externe.
|
||||
- **2026-09-29** — **Étape 5 validée** (« Echo est bon maintenant ») après **changement de SSID** par
|
||||
l'utilisateur. RSSI −69 à −72 dBm (à peine mieux que −75/−81), même IP 192.168.1.23 (même réseau :
|
||||
autre point d'accès ou autre canal). État stable 7 min, aucune reconnexion. Leçon : le canal / la
|
||||
congestion 2,4 GHz compte autant que le RSSI. Chiffres de lecture (`played`) non fournis.
|
||||
Prochaine étape : 6 (MVP : faster-whisper → réponse → Piper → ESP32).
|
||||
- **2026-09-29** — **Étape 6 écrite** (serveur seul, firmware inchangé). `speech_to_text.py`,
|
||||
`text_to_speech.py`, `assistant.py`, `voice_pipeline.py` ; `__main__.py` : mode assistant par
|
||||
défaut, écho avec `--echo`. `config.yaml` : `stt.model small`, `beam_size`, `no_speech_threshold`,
|
||||
`min_peak_dbfs`, `tts.length_scale` (clé `output_sample_rate` retirée : c'est `audio.sample_rate`).
|
||||
Extra `speech` installé dans `server/.venv` et modèles téléchargés (549 Mo) par moi.
|
||||
Mesures : Piper 0,04 à 0,10 s pour 1 à 3 s de parole ; Whisper ≈ 1,0 s ; `make speech-check` →
|
||||
« Quelle heure est-il ? » relu exactement, réponse prête en 1,04 s de calcul. Les 3 derniers
|
||||
enregistrements réels de l'ESP32 (`server/recordings/`) transcrits sans faute (« Fermez le volet
|
||||
de la salle de bain. »). 65 tests (dont 3 avec les vrais modèles, ignorés sans `make models`),
|
||||
pyright 0 erreur. Délai perçu attendu ≈ 1,1 s de calcul + 0,3 s d'avance d'envoi. Pas d'essai
|
||||
bout en bout sur le broker réel (éviterait deux serveurs qui répondent à la même carte).
|
||||
En attente de validation matérielle par l'utilisateur (`make run`).
|
||||
- **2026-09-29** — **Étape 6 validée** par l'utilisateur (« Dialogue très correct »). 9 questions
|
||||
d'affilée, toutes les sessions `complete`, toutes les lectures `chunks N/N` :
|
||||
heure, date, bonjour, nom → bonnes réponses ; phrases libres répétées. Réponse prête **0,95 à
|
||||
1,11 s** après la fin de session (Whisper 0,89–1,06 s, Piper 0,05–0,11 s), pire retard d'envoi 0 ms.
|
||||
Appui sans parler : crête −30,7 dBFS (bruit du bouton, au-dessus du seuil de −45) → Whisper appelé,
|
||||
hallucination « Amara.org » **écartée par le filtre**, réponse « Je n'ai rien entendu. ».
|
||||
Points notés pour plus tard :
|
||||
(a) **sous-alimentations 6 à 9 par lecture même sans perte**, 25 et 35 quand le RSSI tombe à −83 dBm.
|
||||
La base 6–9 vient probablement du démarrage : la tâche audio vide chaque chunk dès son arrivée
|
||||
(DMA encore vide) et compte chaque bloc incomplet ; il n'y a pas de seuil de démarrage → **étape 12**
|
||||
(lecture lancée seulement après l'avance de 300 ms, sous-alimentations comptées après) ;
|
||||
(b) session muette : RMS −46,8 dBFS contre −22 à −28 pour la parole → la VAD de l'**étape 7**
|
||||
remplacera le seuil de crête ; (c) « fermé la porte » pour « fermez » : homophone, l'**étape 11**
|
||||
devra tolérer ce genre d'écart ; (d) 7 chunks perdus faute de place à la capture (Wi-Fi).
|
||||
Prochaine étape : 7 (VAD Silero). Silero est déjà livré par faster-whisper
|
||||
(`faster_whisper/assets/silero_vad_v6.onnx`, exécuté par onnxruntime) : pas besoin de torch.
|
||||
- **2026-09-29** — **Étape 7 écrite** (serveur seul, firmware inchangé). `voice_activity.py`
|
||||
(`find_segments`, `pad_segments`, `VoiceActivityDetector`, `envelope`), branché dans `VoicePipeline`
|
||||
avant Whisper ; `tools/vad_check.py` (`make vad`, `make vad-all`) ; `speech_check --wav` passe par la VAD.
|
||||
Config : section `vad` (clé `window_samples` retirée : 512 est imposé, constante `WINDOW_SAMPLES`),
|
||||
`stt.min_peak_dbfs` supprimé. Mesures sur les 27 enregistrements du serveur : 6 sans parole, tous
|
||||
à juste titre (3 sessions vides de l'étape 5, l'appui muet `0137fe`, mes 2 sons purs `esp32-test`) ;
|
||||
pour les 9 questions de l'étape 6, 8 à 37 % de silence retiré, l'utilisateur parle ~0,5 s après
|
||||
l'appui. **Gain inattendu** : `c5a22c`, transcrit « fermé la porte » à l'étape 6, devient
|
||||
« fermez la porte » une fois le silence retiré. Appui muet : réponse prête en 0,03 s au lieu de 1,11 s.
|
||||
`c5a22c` commence à 0,00 s : parole dès l'appui, début de mot possiblement coupé (piège à signaler).
|
||||
78 tests, pyright 0 erreur. Pas d'essai sur le broker : le `make run` de l'utilisateur (étape 6,
|
||||
lancé à 19:26) tournait encore ; vérification hors ligne de `make_assistant` sur 2 vraies sessions.
|
||||
En attente de validation par l'utilisateur.
|
||||
- **2026-09-29** — **Étape 7 validée** par l'utilisateur (« Semble fonctionner »). 6 sessions :
|
||||
segments de parole trouvés partout (jusqu'à 3 segments, pauses de 0,6 à 0,95 s conservées en une
|
||||
seule transcription), appui muet (crête −32,4 dBFS) rejeté par la VAD, réponse prête en **0,07 s**.
|
||||
VAD 7 à 11 ms (25 ms au premier appel). Erreur « 4 jours, sommes-nous ? » pour « Quel jour
|
||||
sommes-nous ? » (`ce69b6`, pause de 0,6 s après « jour ») : **pas due à la VAD** — sur l'audio brut,
|
||||
Whisper écrit « qu'à jour, sommes-nous ». Essai `initial_prompt` (phrases types de l'assistant) :
|
||||
corrige `ce69b6` mais dégrade `a2428a` (« quelles jours ») → **non retenu** ; à reconsidérer à
|
||||
l'étape 11 (vocabulaire des intentions, règles tolérantes : « jour » + « sommes ») ou modèle
|
||||
`medium` à l'étape 16. Prochaine étape : 8 (diarisation pyannote).
|
||||
- **2026-09-29** — **Étape 8 écrite** (serveur seul). Installé par moi dans `server/.venv` :
|
||||
pyannote.audio 4.0.7 + torch CPU (`.venv` = 1,9 Go), numpy monté à 2.5.3 ; `pip check` propre.
|
||||
`diarization.py` (`Diarizer`, `Diarization`, `SpeakerTurn`, `timeline`, `disable_telemetry`),
|
||||
branché dans `VoicePipeline` après la VAD ; `make install-diarization`, `make diarize`,
|
||||
`make diarize-demo` (voix de l'utilisateur + Piper + voix de l'utilisateur) ; `make models`
|
||||
télécharge le pipeline si `HF_TOKEN` est dans `server/.env`, sinon explique la démarche et rend 1.
|
||||
**Découverte** : pyannote 4 a une télémétrie active par défaut vers otel.pyannote.ai → coupée.
|
||||
**Non vérifié** : le vrai pipeline n'a pas tourné, `server/.env` n'existe pas (pas de jeton). J'ai
|
||||
relu le code de pyannote là où le mien s'y appuie : entrée `{"waveform": (1, n), "sample_rate"}`
|
||||
acceptée, silence → `DiarizeOutput` vide sans erreur, `speaker_embeddings` (n, dim).
|
||||
84 tests + 1 ignoré (modèle réel), pyright 0 erreur. En attente : jeton de l'utilisateur, puis validation.
|
||||
- **2026-09-29** — Modèle pyannote obtenu : premier refus `GatedRepoError` car le compte HF du jeton
|
||||
(type « read », valide) n'avait pas accepté les conditions ; réglé par l'utilisateur. `make models`,
|
||||
`make test` (85, vrai pipeline compris) et `make diarize-demo` OK : **2 locuteurs** trouvés dans
|
||||
l'ordre vous / Piper / vous (0,08-1,55 · 1,75-5,58 · 5,82-7,25 s), **0,47 s** pour 7,35 s de parole.
|
||||
Avertissement torch `std(): degrees of freedom` (morceau trop court) masqué dans `Diarizer.diarize`.
|
||||
Question seule en `make run` : OK selon l'utilisateur. Reste : une session réelle à deux voix.
|
||||
- **2026-09-29** — **Test réel à deux voix réussi** (session `453cce`, 11,85 s, au micro de l'ESP32) :
|
||||
l'utilisateur, une voix Piper jouée par un haut-parleur, l'utilisateur. VAD : 3 segments, 6,34 s de
|
||||
parole. Diarisation : SPEAKER_00 0,08-1,50 · SPEAKER_01 1,57-4,69 · SPEAKER_00 4,92-6,17 — les deux
|
||||
phrases de l'utilisateur portent la même étiquette, et les bornes coïncident avec celles de la VAD
|
||||
(1,61 et 4,89 s dans l'audio sans silences). Diarisation 0,46 s. **Délai : réponse prête en 2,04 s**
|
||||
(VAD 0,02 + diarisation 0,46 + Whisper 1,34 pour 6,3 s + Piper 0,21) : la diarisation s'ajoute à
|
||||
Whisper → candidate à la parallélisation (étape 16). « la roi de synthèse » : voix passée par un
|
||||
haut-parleur puis le micro. Validation explicite de l'étape 8 non encore donnée.
|
||||
- **2026-09-29** — **Étape 8 validée** par l'utilisateur. Étape 9 lancée avec **les empreintes de
|
||||
pyannote** (choix de l'utilisateur, après comparaison avec SpeechBrain ECAPA : précision publiée
|
||||
du même ordre ; SpeechBrain n'apporterait que l'indépendance vis-à-vis de la diarisation, au prix
|
||||
d'un modèle, d'une dépendance et d'un risque de compatibilité avec torchaudio 2.11).
|
||||
- **2026-09-29** — **Étape 9 écrite** (serveur seul). Mesure préalable : les empreintes rendues par
|
||||
pyannote sont **NaN pour toutes les questions courtes** (cf. décisions) → empreinte recalculée par
|
||||
locuteur avec le sous-modèle WeSpeaker. `speaker_identifier.py` (`SpeakerIdentifier`,
|
||||
`VoiceProfile`, `Identification`, `save_profile`, `load_profiles`), `tools/enroll.py`,
|
||||
`make enroll/profiles/forget`, identification affichée par `make diarize` ; `VoicePipeline` journalise
|
||||
`SPEAKER_00 = Nom (score)`. `diarize-demo` choisissait `453cce` (deux voix) puis `093463` (mélange) :
|
||||
il prend maintenant la question la plus récente de 1 à 4 s à une seule voix (`--voice` pour imposer).
|
||||
Avertissements numpy « Mean of empty slice » (empreintes NaN internes de pyannote) masqués.
|
||||
Essai complet **dans le scratchpad** (aucun profil écrit dans `server/profiles/`) : profil « Essai »
|
||||
sur 10 sessions → 6 retenues (2 à deux voix, 1 muette, 1 mélangée écartées), cohérence 0,79–0,86 ;
|
||||
`c5a22c` → Essai 0,74 ; `453cce` → Essai 0,72 / haut-parleur Inconnu −0,05 ; démo → Essai 0,84 /
|
||||
Piper Inconnu −0,02. Délai : + ~0,5 s de diarisation et d'empreinte (réponse prête 1,47 s pour une
|
||||
question de 1,3 s). 98 tests, pyright 0 erreur. En attente : enrôlement réel et validation.
|
||||
- **2026-09-30** — Fichiers de test pour l'étape 9 (demande de l'utilisateur, à jouer depuis son
|
||||
téléphone) : `recordings/voix-test/{1-tom,2-pierre,3-jessica,4-gilles,5-siwis}.{wav,mp3}`, 5 voix
|
||||
Piper différentes (tom-medium, upmc-medium pierre et jessica, gilles-low, siwis-medium), 0,5 s de
|
||||
silence avant et après, crête −3 dBFS, toutes relues correctement par Whisper. Voix téléchargées
|
||||
dans le scratchpad puis supprimées (rien ajouté à `server/models/`). `fr_FR-mls-medium` écartée :
|
||||
aucun de ses 125 locuteurs n'est transcrit correctement. **Mesure à retenir** : Jessica et Siwis
|
||||
(deux voix féminines de synthèse) se ressemblent à **0,60** (fichier contre fichier), au-dessus du
|
||||
seuil de 0,45 ; les autres paires 0,01–0,37. Le seuil devra peut-être monter si une voix étrangère
|
||||
est reconnue comme l'utilisateur (ses propres sessions : 0,52–0,83, marge faible).
|
||||
- **2026-09-30** — **Étape 9 validée** par l'utilisateur (« Tout semble marcher super bien »).
|
||||
6 profils enrôlés par l'utilisateur : Denis (5 sessions) et les 5 voix de synthèse jouées au
|
||||
téléphone (Gilles 3, Jessica 3, Pierre 4, Siwis 3, Tom 5). Essai réel : Denis 0,74 et 0,78 ;
|
||||
Tom 0,95, Gilles 0,96, Siwis 0,96, Jessica 0,93, Pierre 0,92 — chaque voix trouvée, Jessica et Siwis
|
||||
(0,60 entre elles) bien départagées par le meilleur score. Réponse prête en 1,5 à 1,8 s
|
||||
(diarisation + empreinte ≈ 0,45 s). **Limites à garder en tête** : (1) les scores de 0,92–0,96 des
|
||||
voix de synthèse sont gonflés — même fichier rejoué à l'enrôlement et au test, audio quasi identique ;
|
||||
le vrai test de généralisation est Denis (phrases nouvelles, 0,74–0,78) ; (2) le cas « Inconnu »
|
||||
n'a pas été rejoué en réel ce matin (toutes les voix étaient enrôlées) ; il a été vérifié hier
|
||||
(haut-parleur −0,05, Piper −0,02) ; (3) seuil 0,45 à recalibrer avec de vraies personnes (étape 14).
|
||||
« Quel ton fera-t-il » pour « temps » : Whisper sur une voix de téléphone. Prochaine étape : 10.
|
||||
- **2026-09-30** — **Étape 10 écrite** (serveur seul). `Word` et `Transcript.words` (Whisper,
|
||||
`word_timestamps`), `speaker_attribution.py` (`turn_of`, `attribute`, `Utterance`,
|
||||
`transcript_json`), `VoicePipeline` publie sur `voice/<carte>/transcript` et passe le nom de la
|
||||
dernière personne à l'assistant ; `make diarize` affiche qui a dit quoi et le message MQTT.
|
||||
Essai hors ligne sur `453cce` avec les profils réels de l'utilisateur : Denis « Ouvrir la porte. »,
|
||||
**Siwis** (juste : c'était sa voix au haut-parleur) « Je suis la roi de synthèse… », Denis « Fermez la
|
||||
porte. », bornes 0,00-1,26 / 1,50-4,46 / 4,92-5,86 s. 110 tests, pyright 0 erreur.
|
||||
En attente : essai sur la carte et validation.
|
||||
- **2026-09-30** — **Étape 10 validée** par l'utilisateur (« continuer maintenant »). Essai carte :
|
||||
Denis 0,68 / 0,79, Jessica 0,92, interventions et textes justes ; « Bonjour » seul (1,06 s de parole)
|
||||
→ Inconnu 0,32 → salutation générique (piège annoncé : phrase trop courte pour une empreinte).
|
||||
Session à plusieurs voix non refaite sur la carte (vérifiée hors ligne sur `453cce`).
|
||||
`make mqtt-watch` affichait l'audio binaire (`voice/#`) : limité aux topics texte (state, event,
|
||||
control, transcript) ; ajout de `make mqtt-transcript`. Prochaine étape : 11 (intentions + domotique).
|
||||
- **2026-09-30** — **Étape 11 écrite** (serveur + firmware). Choix utilisateur : commandes vers
|
||||
`home/…` **et** exécution par la carte pour sa pièce ; pas de domotique réelle (simulation).
|
||||
Serveur : `home_control.py`, `Assistant.respond()` → `Reply(text, command)`, `HomeController`,
|
||||
`MqttLink.publish_topic`, section `home` de config.yaml (pièces + `boards: esp32-01: salon`),
|
||||
journal des confirmations `device`. Firmware : `DemoDevices`, `CommandHandler` gère `cmd: device`,
|
||||
`PIN_DEMO_LIGHT = 21` ; `make fw-check` SUCCESS, 0 warning (RAM 36,7 %, Flash 31,4 %).
|
||||
`make mqtt-watch` inclut `home/#` ; `make mqtt-home`. `docs/BROCHAGE.md` : LED GPIO 21 + 330 Ω
|
||||
(non dessinée sur `wiring.svg`). Essai hors ligne sur 3 enregistrements réels : volets du salon
|
||||
fermés/ouverts → `home/salon/shutter/set` + `esp32-01/control` ; salle de bain → `home/…` seul.
|
||||
130 tests, pyright 0 erreur. **Firmware non téléversé** (à faire par l'utilisateur). En attente de validation.
|
||||
- **2026-09-30** — **Étape 11 validée** par l'utilisateur (« Semble fonctionner correctement »),
|
||||
firmware téléversé. « Éteint la lumière » (graphie Whisper) et « Allume la lumière » →
|
||||
`home/salon/light/set` + `esp32-01/control`, **confirmation de la carte 40 ms après la commande**
|
||||
(`{"event":"device","ok":true,"state":"off"}`), puis la réponse parlée ; « Quel temps fait-il ? » →
|
||||
réponse « sans Internet ». Réponse prête en 1,41 à 1,55 s. `mqtt-watch` lisible : transcript,
|
||||
commande, confirmation, START/END de lecture, `played`. **Sous-alimentations toujours 6 à 10 par
|
||||
lecture, sans aucune perte** (`chunks 34/34`) : c'est l'objet de l'étape 12. Prochaine étape : 12.
|
||||
- **2026-09-30** — Exigence utilisateur : commander des machines Linux → **étape 15b** (décidé).
|
||||
- **2026-09-30** — **Étape 12 écrite**. Firmware : `PlaybackStream` devient une machine à états
|
||||
(Idle / Priming / Playing) dans la tâche audio ; `PLAYBACK_START_MS = 300` ; bilan `played` enrichi
|
||||
(`start_ms`, `min_margin_ms`, `underruns` = réamorçages) ; `make fw-check` SUCCESS, 0 warning
|
||||
(RAM 36,7 %, Flash 31,4 %). Cause probable des 6 à 10 « sous-alimentations » : la tâche jouait dès
|
||||
le premier chunk, sans attendre l'avance envoyée par le serveur, et comptait chaque bloc incomplet.
|
||||
Serveur : journal `played` enrichi (WARNING s'il y a des réamorçages) ; banc `JitteryLink` dans
|
||||
`tools/wav_to_mqtt.py`, `JITTER` dans le Makefile, `tone-mqtt` passe à 5 s. 131 tests, pyright
|
||||
0 erreur. **Non vérifié sur la carte** (je n'envoie pas de son sur le matériel de l'utilisateur) :
|
||||
firmware à téléverser, puis essais `JITTER=0/200/400`. En attente de validation.
|
||||
- **2026-09-30** — Premiers essais de l'étape 12 sur la carte (`tone-mqtt` 5 s, JITTER 0 / 200 / 400) :
|
||||
marge mini **204 / 142 / 2 ms** (suit bien la gigue), réamorçages **1 / 1 / 2**, premier son à
|
||||
**214–218 ms**. Deux écarts avec la prévision : (1) **un réamorçage systématique**, même sans gigue,
|
||||
hors de la marge mesurée ; (2) l'avance de 300 ms n'arrive pas d'un bloc mais en ~200 ms (hypothèse :
|
||||
fenêtre TCP de lwIP ~5,7 Ko < 6 chunks de 1,6 Ko, la carte lit au rythme de PubSubClient).
|
||||
Instrumentation ajoutée plutôt que supposition : `gaps_ms` (instant de chaque réamorçage depuis le
|
||||
premier son) dans le bilan `played`, la série et le journal serveur. En attente d'un nouvel essai.
|
||||
- **2026-09-30** — **Cause du réamorçage systématique trouvée grâce à `gaps_ms`** : 1er essai après
|
||||
démarrage 0 réamorçage ; 2e essai `gaps_ms:[28043]` = uptime de la carte, donc compté avant le premier
|
||||
son du flux (`_firstSoundMs` = 0). **Faux compte, inaudible** : après un flux, la tâche restait en
|
||||
Playing, bloquée ≤ 20 ms dans `read()` ; le START suivant arrivait pendant ce temps, le `read()`
|
||||
rendait 0 octet, `_endOfStream` venait d'être remis à faux → famine comptée pour un flux pas encore
|
||||
commencé. Correctif : passage à **Idle** quand la FIFO est vide après le END, famine comptée
|
||||
seulement par **compare-and-swap Playing → Priming** (échoue si `open()` a déjà changé l'état), et
|
||||
`open()` met l'état à Priming **avant** de remettre les compteurs à zéro. `make fw-check` SUCCESS.
|
||||
Premier son à 214 ms : l'avance arrive étalée (fenêtre TCP de lwIP probable) ; à revoir à l'étape 16
|
||||
(latence), sans effet sur la fluidité. Marge mini 188–204 ms sans gigue.
|
||||
- **2026-09-30** — Essai final de l'étape 12 (firmware corrigé) : `tone-mqtt` ×3 → **0 réamorçage,
|
||||
`gaps_ms` vide** à chaque fois (le cas qui échouait) ; JITTER=200 → **0**, marge mini 162 ms ;
|
||||
JITTER=400 → 1 réamorçage **réel** à 364 ms du premier son (1er blocage), marge 2 ms. Premier son
|
||||
212–221 ms. Critères de l'étape 12 atteints ; validation explicite de l'utilisateur en attente.
|
||||
- **2026-09-30** — **Étape 12 validée** par l'utilisateur. Étape 13 lancée : mots de réveil anglais
|
||||
prêts à l'emploi **alexa, hey_jarvis, hey_mycroft** (les trois ; alexa si un seul).
|
||||
- **2026-09-30** — **Étape 13 écrite** (serveur + firmware). openWakeWord installé `--no-deps` dans
|
||||
`server/.venv`, modèles dans `server/models/openwakeword/` (`make models`). Serveur : `wake_word.py`,
|
||||
`hands_free.py`, `make_listener` dans `__main__.py` (état/événements composés, balayage, arrêt qui
|
||||
coupe l'écoute), section `wake` de config.yaml, extra `wakeword` + `make install-wakeword`.
|
||||
Firmware : `cmd listen` / `cmd capture`, flux continu dans `loop()` (`updateStreaming`,
|
||||
`stopStreaming`), LED `blink(2000, 100)` ; `make fw-check` SUCCESS, 0 warning. Simulation complète
|
||||
hors ligne (flux : phrase sans mot puis « Alexa, allume la lumière », Piper) : 1re phrase ignorée,
|
||||
aucun fichier ; mot 0,99 ; commande close 2,75 s après ; `home/salon/light/set` + carte ; réponse.
|
||||
142 tests (dont vie privée : rien écrit en veille ; rafale Wi-Fi), pyright 0 erreur.
|
||||
**Non vérifié sur la carte** : firmware à téléverser. Limite connue : broker anonyme → flux audio
|
||||
lisible par tout le réseau local (étape 15) ; si le serveur meurt sans `listen false`, la carte
|
||||
continue d'émettre (testament serveur à prévoir à l'étape 15). En attente de validation.
|
||||
- **2026-09-30** — Retour utilisateur (essai réel de l'étape 13) : « Hey Jarvis » et « Alexa » pas
|
||||
toujours reconnus (prononciation anglaise nécessaire), **« Hey Mycroft » fiable en français**.
|
||||
Ajouts : **seuil propre à chaque mot** (`wake.thresholds`, vide par défaut) et **journal des mots
|
||||
presque reconnus** (pic de score ≥ 0,15 retombé sans atteindre le seuil) pour régler sur mesures.
|
||||
143 tests, pyright 0 erreur. Aucun seuil changé tant qu'on n'a pas les pics réels.
|
||||
- **2026-09-30** — **Étape 13 validée** par l'utilisateur, qui retient **« Hey Mycroft »** comme mot
|
||||
principal (fiable prononcé en français) ; alexa et hey_jarvis restent actifs. Réglage des seuils par
|
||||
mot reporté (outil prêt : « presque reconnu »). Étape 14 lancée : droits par profil.
|
||||
- **2026-09-30** — **Étape 14 écrite** (serveur seul, firmware inchangé). `access_control.py`
|
||||
(`AccessPolicy`, `Level`, `Verdict`, `Resolution`), section `access` de config.yaml,
|
||||
`VoicePipeline._decide` (confirmation en attente d'abord, puis droits, puis exécution),
|
||||
`home_control.announce/request`, élision « d'ouvrir ». Essai hors ligne avec les vrais profils :
|
||||
Siwis (standard, 0,59) « ouvre la porte » → refusé ; Denis (complet, 0,69) → confirmation demandée ;
|
||||
phrase suivante sans oui/non → demande abandonnée ; Denis ferme les volets → exécuté.
|
||||
162 tests, pyright 0 erreur. En attente de l'essai réel et de la validation.
|
||||
- **2026-09-30** — Ajout de **`make say`** (`tools/say.py`) : fait dire `TEXT` par une voix Piper
|
||||
(`VOICE`, `SPEAKER` pour upmc : 0 = Jessica, 1 = Pierre), 0,5 s de silence avant/après, crête −3 dBFS,
|
||||
WAV + MP3 (ffmpeg) dans `SAY_OUT` (défaut `recordings/voix-test/phrase.wav`) ; voix téléchargée au
|
||||
premier usage. Pour tester droits et confirmation sans seconde personne (≠ `speech-check`, qui fait
|
||||
entendre la RÉPONSE). Générés : `recordings/voix-test/tom-porte` (« Hey Mycroft, ouvre la porte. »)
|
||||
et `pierre-lumiere` (« Hey Mycroft, allume la lumière. »).
|
||||
- **2026-09-30** — Retour utilisateur : « Hey Mycroft » doit être prononcé à l'anglaise, sinon rien.
|
||||
Mesures (détecteur hey_mycroft réel, scratchpad) : voix FR (tom, upmc, siwis) en texte ou en graphies
|
||||
francisées → **0,00** ; en phonèmes bruts Piper `[[ˈɛj mˈajkʁɔft]]` → ~1 synthèse sur 2 atteint 0,5
|
||||
(Piper est aléatoire), mais retombe à 0 au moindre bruit/filtrage → inutilisable au téléphone. Voix
|
||||
anglaise **en_GB-alan-medium** → 1,00 partout, même bruit 15 dB + bande 300-4000 Hz + −12 dB (ryan et
|
||||
lessac moins stables). D'où `make say WAKE="Hey Mycroft"` (`--wake`, `--wake-voice`) : Alan dit le mot,
|
||||
0,6 s de pause, puis VOICE dit la commande ; chaque morceau normalisé à −3 dBFS, sortie **16 kHz** ;
|
||||
fichier vérifié par `WakeWordDetector` (échec → code 1). Identification : le serveur prend la
|
||||
**dernière** intervention, mais la pré-écoute (0,5 s avant la détection, qui tombe ~0,15 s avant la fin
|
||||
du mot) mêle la voix d'Alan : commande courte → Tom 0,42-0,46 (sous le seuil) ; commande ≥ 2 s
|
||||
(« …du salon, s'il te plaît. ») → Tom 0,57, Pierre 0,77 sur la découpe exacte du serveur. Refaits :
|
||||
`tom-porte` (« Ouvre la porte du salon, s'il te plaît. », détecteur 1,00) et `pierre-lumiere`
|
||||
(« Allume la lumière du salon, s'il te plaît. », 0,95). 162 tests, pyright 0 erreur.
|
||||
- **2026-09-30** — **Essai réel de l'étape 14** (« satisfaisant, ma voix Denis OK »). Denis : lumière
|
||||
on/off exécutées (0,71-0,84), porte → confirmation → « oui » (bouton, ou « croft ? Oui. » en mains
|
||||
libres) → exécutée ; Tom (fichier téléphone, 0,68) porte → refusé ; Pierre (0,83) identifié.
|
||||
**Bug trouvé** : Pierre (standard) REFUSÉ pour allumer la lumière, « requis complet ». Cause : YAML 1.1
|
||||
lit `on`/`off` nus comme des **booléens** → règles `light.True/False`, action `on` absente → défaut
|
||||
complet. Corrigé : guillemets dans config.yaml, `settings._key()` rend True/False → on/off, et
|
||||
`AccessPolicy` **refuse au démarrage** une action inconnue (plus d'erreur silencieuse). Aussi : Denis
|
||||
à 0,59 (< full_min_score 0,6) recevait « votre profil ne permet pas » — faux ; message désormais
|
||||
« je ne reconnais pas assez nettement votre voix… Répétez, plus près du micro. ». Autres constats,
|
||||
non corrigés : 1re session hallucinée par Whisper (0,82 s de parole → « Je vous remercie de votre
|
||||
soutien… », 5,5 s de calcul) ; un « Ouvre » perdu en tête de commande (« la porte du salon… » → « Que
|
||||
dois-je faire avec la porte ? »), réussi au 2e essai. 165 tests, pyright 0 erreur.
|
||||
- **2026-09-30** — **Étape 14 validée** par l'utilisateur. Étape 15 lancée.
|
||||
- **2026-09-30** — **Étape 15 écrite** (broker + serveur + firmware). Broker : `voice.conf`
|
||||
(`allow_anonymous false`, `password_file`, `acl_file`), `mosquitto/acl`, `make mqtt-user`,
|
||||
`mosquitto-docker`/`mosquitto-config` installent acl + passwd (0600). Vérifié sur un broker **jetable**
|
||||
(conteneur `voice-acl-test`, port 18830, supprimé ensuite ; le broker de l'utilisateur n'a pas été
|
||||
touché) : anonyme et mauvais mot de passe refusés ; carte qui publie sous le nom d'une autre, lit le
|
||||
micro ou les commandes d'une autre, publie sur home/ → jeté ; carte lit ses commandes et la présence.
|
||||
Un broker **ouvert** accepte un client qui envoie des identifiants (vérifié) → migration : comptes,
|
||||
.env + secrets.h, firmware + serveur, broker en dernier. Serveur : `MqttLink(presence=True)`,
|
||||
`refusal_hint`, `setup_logging` ; vérifié : arrêt propre → `offline`, `kill -9` → testament
|
||||
immédiat, mauvais mot de passe → message clair, `make run-echo` avec config de test → journal
|
||||
fichier. Firmware : TWDT 15 s, `setConnectionTimeout(1000)`, Wi-Fi relancé à 30 s, présence
|
||||
serveur (micro coupé si absent), utilisateur MQTT = DEVICE_ID par défaut, diagnostic codes 4/5.
|
||||
`make fw-check` SUCCESS 0 avertissement (RAM 36,7 %, Flash 31,5 %). 170 tests, pyright 0 erreur.
|
||||
Non vérifié sur la carte : déclenchement réel du chien de garde (pas de commande de test).
|
||||
En attente de l'essai réel et de la validation.
|
||||
- **2026-09-30** — **Étape 15 validée** par l'utilisateur, qui **garde le broker de développement
|
||||
anonyme** (`make mosquitto-docker` non lancé ; comptes et ACL à activer en production). Conséquence
|
||||
à rappeler : tant que le broker est anonyme, tout appareil du réseau peut publier sur les topics de
|
||||
commande (cartes, et agents Linux de l'étape 15b). Étape 15b lancée : agent MQTT sur chaque
|
||||
machine, configuration YAML.
|
||||
- **2026-09-30** — **Étape 15b écrite** (serveur + agent ; firmware inchangé). Serveur :
|
||||
`machine_control.py` (parse/decide, `MachineController` : états des agents, interprétation, commande
|
||||
ou Wake-on-LAN), `command_router.py`, `MqttLink.on_topic` (filtres hors voice/), section `machines`
|
||||
de config.yaml (actions : mots + phrases ; `pc-bureau`), `AccessPolicy` et `Assistant` généralisés
|
||||
(`Command = DeviceCommand | MachineCommand`). Agent : `agent/voice_agent.py`, `agent.yaml.example`,
|
||||
service systemd, `agent/Makefile` ; cibles `agent-run`, `agent-install`, `agent-log`, `mqtt-agents` ;
|
||||
règles agents dans `mosquitto/acl` (pour la production). Essai de bout en bout sur broker **jetable**
|
||||
anonyme (port 18832, supprimé) : agent en `dry_run` annoncé avec 3 actions ; « éteins » et
|
||||
« verrouille » → agent → compte rendu ; « allume » → « déjà en marche » ; agent arrêté → hors ligne →
|
||||
« ne répond pas ». 202 tests, pyright 0 erreur (agent/ inclus). En attente de l'essai réel.
|
||||
- **2026-09-30** — **Git initialisé** à la demande de l'utilisateur, avant l'essai de l'étape 15b :
|
||||
premier commit (108 fichiers) sur `main` ; contrôle des fichiers sensibles fait (§7).
|
||||
Reference in New Issue
Block a user