Assistant vocal local — ESP32 + Raspberry Pi
Assistant vocal 100 % local, sans aucun service cloud : un ESP32 capture la voix, l'envoie en PCM brut par MQTT à un serveur Python qui transcrit, interprète et répond en synthèse vocale ; l'ESP32 joue la réponse et exécute la commande.
English version: README-EN.md.
1. Architecture
INMP441 -> ESP32 -> MQTT -> Serveur Python (Raspberry Pi / PC)
|
+---------+---------+
| |
VAD Diarisation /
| identification
+---------+---------+
|
faster-whisper
|
Assistant
|
Piper
|
MQTT audio/out
|
ESP32 -> MAX98357A -> haut-parleur
Quatre traitements souvent confondus, à ne jamais mélanger :
| Bloc | Question |
|---|---|
| VAD | Est-ce que quelqu'un parle ? |
| Diarisation | Qui parle à quel moment ? (SPEAKER_00, SPEAKER_01…) |
| Identification | Qui est cette personne ? (Denis, Marie, Inconnu) |
| STT | Qu'est-ce qui est dit ? |
L'identification vocale est probabiliste. Ce n'est jamais un mécanisme d'authentification ni de sécurité.
2. Matériel
| Élément | Détail |
|---|---|
| ESP32-WROOM-32 | deux périphériques I2S : I2S0 en réception, I2S1 en émission |
| INMP441 | micro I2S, 3,3 V, broche L/R à GND (canal gauche) |
| MAX98357A | ampli classe D mono, 5 V, SD à VIN, GAIN laissé libre (9 dB) |
| Bouton | Push-To-Talk entre GPIO 4 et GND (pull-up interne) |
| LED | + résistance 330 Ω sur GPIO 2 |
| Serveur | Raspberry Pi 4/5 64 bits ou PC Debian |
Câblage
Schéma complet : docs/wiring.svg — brochage détaillé, bilan de consommation et vérifications au multimètre : docs/BROCHAGE.md.
ESP32 INMP441 (micro)
3V3 ----------------------- VDD
GND ----------------------- GND, L/R
GPIO 25 (SCK) ------------- SCK
GPIO 26 (WS) ------------- WS
GPIO 33 (SD) ------------- SD
ESP32 MAX98357A (ampli, étape 2)
5V (VIN) ------------------- VIN
GND ----------------------- GND
GPIO 27 (BCLK) ------------- BCLK
GPIO 14 (LRC) ------------- LRC
GPIO 22 (DIN) ------------- DIN
ESP32 Interface
GPIO 4 ------ bouton ------ GND
GPIO 2 --[330 Ω]-- LED ---- GND
Aucune broche de strapping (GPIO 0, 2, 5, 12, 15) n'est utilisée pour l'audio : leur niveau au démarrage change le mode de boot de l'ESP32. GPIO 2 est utilisé en sortie seule pour la LED, ce qui reste sans effet sur le boot.
3. Format audio, unique dans tout le système
| Paramètre | Valeur |
|---|---|
| Fréquence | 16 000 Hz |
| Résolution | 16 bits signés, little-endian (pcm_s16le) |
| Canaux | 1 (mono) |
| Débit | 32 000 octets/s |
| Chunk | 50 ms = 800 échantillons = 1 600 octets |
Pas de Base64 : les payloads MQTT contiennent des octets PCM bruts.
4. Installation
4.1 Firmware ESP32
Prérequis : câble USB avec les données (pas charge seule), port identifié
(ls /dev/ttyUSB* ou /dev/ttyACM* après branchement).
make pio-venv # PlatformIO dans son propre venv — une seule fois
make fw-build # compile sans flasher
make fw-upload SERIAL_PORT=/dev/ttyUSB0
Premier démarrage — portail de configuration WiFi
Si aucun identifiant n'a encore été configuré, la carte ouvre automatiquement un point d'accès :
- La LED clignote rapidement (100 ms on / 100 ms off).
- Connectez un téléphone ou un PC au réseau VoiceAssist-Config.
- Ouvrez
http://192.168.4.1dans un navigateur. - Renseignez le Wi-Fi (réseau 2,4 GHz obligatoire) et l'adresse IP du broker MQTT.
- Cliquez Enregistrer et redémarrer — les identifiants sont sauvegardés dans la mémoire permanente (NVS) et la carte redémarre.
Reconfiguration : tenez le bouton PTT appuyé 3 s à la mise sous tension pour rouvrir le portail, même si des identifiants sont déjà enregistrés.
Alternative — secrets.h (pour les compilations répétées en développement)
cp firmware/include/secrets.h.example firmware/include/secrets.h
# Éditer : WIFI_SSID, WIFI_PASSWORD, MQTT_HOST
make fw-upload SERIAL_PORT=/dev/ttyUSB0
La NVS reste prioritaire sur secrets.h. Pour repartir de zéro, forcez le
portail avec le bouton PTT (ou effacez la NVS avec esptool.py erase_flash).
Point d'accès Wi-Fi du Raspberry Pi 5 : WPA2 obligatoire
Si la carte se connecte au point d'accès du Pi (NetworkManager, ici RPI5-AP),
celui-ci doit être en WPA2. L'ESP32 refuse par défaut tout réseau en dessous
de WPA2, et un point d'accès créé par NetworkManager peut être en WPA1.
Correction recommandée, sur le Pi :
nmcli -t -f NAME,TYPE con show --active # nom de la connexion du point d'accès
sudo nmcli con modify "RPI5-AP" wifi-sec.key-mgmt wpa-psk wifi-sec.proto rsn \
wifi-sec.pairwise ccmp wifi-sec.group ccmp \
802-11-wireless.band bg 802-11-wireless.channel 6
sudo nmcli con down "RPI5-AP" && sudo nmcli con up "RPI5-AP"
proto rsn impose WPA2 ; le canal 6 évite les canaux 12 et 13, mal gérés par
l'ESP32. Le mot de passe du Wi-Fi ne change pas. Sur ce réseau, le Pi a
l'adresse 10.42.0.1 : c'est l'adresse du broker à saisir dans le portail.
Diagnostic au moniteur série (make fw-monitor, ou minicom à 921600 bauds) :
| Message de la carte | Cause | Correctif |
|---|---|---|
Reason: 211 |
point d'accès en WPA1 | le passer en WPA2 (ci-dessus) |
Reason: 15 - 4WAY_HANDSHAKE_TIMEOUT |
mot de passe Wi-Fi enregistré dans la carte faux | le ressaisir dans le portail ; le Pi le confirme : sudo journalctl -u wpa_supplicant | grep PSK-MISMATCH |
# mqtt : echec (code 5) |
compte MQTT absent du broker, ou mot de passe différent | make mqtt-user NAME=esp32-01 sur le Pi |
| des octets illisibles à l'appui du bouton | MQTT non connecté : la session part par le câble série | voir les lignes # wifi et # mqtt au démarrage |
Dans le portail, les deux champs de mot de passe (Wi-Fi et MQTT) s'affichent toujours vides : retapez-les à chaque enregistrement, un champ laissé vide efface le mot de passe enregistré.
4.2 Serveur Python
Prérequis : Python 3.10+, Docker (pour le broker) ou Mosquitto natif.
# 1. Paquets système (mosquitto, ffmpeg, venv) + groupe dialout (port série)
make install-system
# → se déconnecter/reconnecter pour activer le groupe « dialout »
# 2. Broker MQTT (choisir l'une ou l'autre)
make mosquitto-docker # conteneur Docker (recommandé)
make mosquitto-config # service Mosquitto Debian natif (déjà installé via make install-system)
# 3. Environnement Python + outils de développement (pytest, pyright, PlatformIO)
make install-dev
# PlatformIO est dans son propre venv (.pio-venv) : ses dépendances entrent
# en conflit avec celles du serveur vocal.
# 4. Transcription + synthèse vocale (~550 Mo)
make install-speech
make models
# 5. Diarisation + identification vocale (~1,3 Go) — optionnel, lourd sur RPi 4
# Prérequis : compte Hugging Face + accepter les conditions sur
# https://huggingface.co/pyannote/speaker-diarization-community-1
# Créer un jeton « Read » sur https://huggingface.co/settings/tokens
cp server/.env.example server/.env # puis HF_TOKEN=hf_…
make install-diarization
make models
# 6. Mot de réveil openWakeWord (~5 Mo) — optionnel
make install-wakeword
make models
# 7. Lancer l'assistant
make run
Fichiers à configurer :
| Fichier | Contenu |
|---|---|
server/.env |
MQTT_USERNAME, MQTT_PASSWORD, HF_TOKEN |
server/config.yaml |
pièces, voix, droits d'accès, liste des machines |
Pour l'aide sur toutes les cibles : make help.
4.3 Agent Linux (machines commandées par la voix)
Chaque machine Linux à commander (PC du bureau, NAS…) fait tourner un service léger qui n'exécute que les actions listées dans son fichier de configuration — aucun shell exposé.
Sur la machine cible :
# 1. Copier le répertoire agent/ (depuis le Raspberry Pi)
scp -r /home/rpi5/voice-assistant/agent/ user@pc-bureau:~/voice-agent/
# 2. Installer le service (crée /opt/voice-agent/, /etc/voice-assistant/, unité systemd)
cd ~/voice-agent
sudo make install
# 3. Créer le compte MQTT sur le serveur (si le broker est sécurisé, étape 15)
# (à exécuter sur le Raspberry Pi, pas sur la machine cible)
make mqtt-user NAME=pc-bureau # noter le mot de passe
make mosquitto-docker # ou mosquitto-config — recharge le broker
# 4. Configurer l'agent sur la machine cible
sudo nano /etc/voice-assistant/agent.yaml
# id: pc-bureau ← nom dans les topics MQTT
# mqtt.host: 192.168.1.200 ← adresse IP du broker
# mqtt.username: pc-bureau ← même valeur que id (vide si broker anonyme)
# mqtt.password: <mot-de-passe> ← généré à l'étape 3 (vide si broker anonyme)
# dry_run: false ← true pour tester sans rien exécuter
# 4. Activer et démarrer
sudo systemctl enable --now voice-agent
sudo systemctl status voice-agent
Sur le serveur, ajouter la machine dans server/config.yaml :
machines:
list:
- id: pc-bureau
name: "PC du bureau" # dit à voix haute : « le PC du bureau »
mac: "AA:BB:CC:DD:EE:FF" # adresse MAC pour Wake-on-LAN
ip: 192.168.1.50
Essai sans rien éteindre (dry_run: true dans agent.yaml) :
make run # serveur (terminal 1)
make agent-run # agent en mode essai sur cette machine (terminal 2)
# Dire : « Hey Mycroft, éteins le PC du bureau »
# → l'agent affiche : essai : systemctl poweroff (non exécuté)
Actions disponibles par défaut : shutdown, reboot, lock ; wake
(allumer depuis le réseau) est géré par le serveur via Wake-on-LAN.
5. Utilisation
make fw-build # compile le firmware
make fw-upload SERIAL_PORT=/dev/ttyUSB0
make record OUT=recordings/essai.wav # enregistre une session (étape 1)
make play OUT=recordings/essai.wav # joue le WAV (cherche un lecteur qui marche)
make inspect OUT=recordings/essai.wav # analyse le WAV sans l'écouter
make play-esp OUT=recordings/essai.wav # joue le WAV sur le haut-parleur de l'ESP32 (étape 2)
make tone-esp TONE=440 # son pur sur le haut-parleur de l'ESP32 (étape 2)
make run # assistant : répond à chaque question (étape 6)
make speech-check TEXT="Quelle heure est-il ?" # transcription et synthèse, sans ESP32
make say WAKE="Hey Mycroft" TEXT="Ouvre la porte du salon." VOICE=fr_FR-tom-medium # WAV + MP3 pour un téléphone
make latency # latence du serveur avant/après l'étape 16, sur de vrais enregistrements
make transcribe OUT=server/recordings/<fichier>.wav
make vad OUT=server/recordings/<fichier>.wav # parole trouvée par la VAD (étape 7)
make vad-all # VAD sur tous les enregistrements du serveur
make diarize OUT=server/recordings/<fichier>.wav # qui parle quand, qui, qui a dit quoi (étapes 8-10)
make diarize-demo # deux voix : la vôtre et celle de Piper
make enroll NAME=Denis # profil vocal depuis les 5 dernières sessions (étape 9)
make profiles # liste des profils ; make forget NAME=Denis pour en supprimer un
make mqtt-watch # messages texte voice/… et home/# (sans l'audio)
make mqtt-user NAME=esp32-01 # compte MQTT d'une carte, ou du serveur (étape 15)
make agent-run # agent Linux de ce PC, en mode essai (étape 15b)
make mqtt-agents # états, commandes et comptes rendus des agents
make fw-monitor # moniteur série
make test # tests Python
make lint # typage pyright
6. Avancement
L'état exact du projet, étape par étape, est dans CONTEXT.md.
Étape courante : 16 — optimisation. Les étapes 1 à 15b sont validées.
Étape 16 — optimisation
Mesurer d'abord, optimiser ensuite : make latency rejoue les derniers enregistrements
avec l'ancienne puis la nouvelle chaîne. Sur 54 commandes réelles (PC, 20 cœurs logiques) :
1,6 s → 1,1 s entre la fin de la phrase et le texte reconnu, même texte 54 fois sur 54.
| Optimisation | Gain mesuré | Réglage |
|---|---|---|
| Parole courte : empreinte vocale directe, sans diarisation | 0,40 s (0,42 → 0,02 s) | diarization.direct_below_s: 4.0 |
| Parole courte : Whisper sans horodatage des mots | 0,10 s | automatique |
| Réponses déjà dites gardées en mémoire (cache de synthèse) | 0,03 à 0,14 s | automatique, 64 phrases |
| Fin de commande cherchée toutes les 100 ms (250 avant) | 0,075 s en moyenne | — |
Envois réseau immédiats (TCP_NODELAY : serveur, broker) |
à mesurer : premier son a … ms |
set_tcp_nodelay true dans voice.conf |
Pistes mesurées puis écartées : diarisation et Whisper en parallèle (0,03 à 0,10 s seulement :
les deux calculs saturent le processeur, même dans deux processus), beam_size: 1 (0,05 s, et
un texte différent sur 16), 16 fils pour Whisper (plus lent que 4).
- Sur le Raspberry Pi (broker) :
git pull, puismake mosquitto-config. make run, quelques commandes : le journal affiche1 locuteur (parole courte, empreinte directe 20 ms)etreponse prete en ~1,1 s(1,5 s avant).make latency: avant, après, gain, et les identifications qui diffèrent.
Une parole de moins de 4 s est supposée venir d'une seule personne. Deux voix dans une phrase courte ne sont plus séparées (elles l'étaient à l'étape 10) ; au-delà de 4 s, rien ne change.
Étape 15b — machines Linux du réseau
« Hey Mycroft, éteins le PC du bureau » : chaque machine fait tourner un agent
(agent/voice_agent.py), petit client MQTT qui n'exécute que
les actions de sa liste (/etc/voice-assistant/agent.yaml), commandes fixes, sans
shell. Le serveur n'envoie qu'un nom d'action. « Allume » réveille une machine
éteinte par Wake-on-LAN (paquet réseau reçu par la carte réseau en veille).
| Action | Mots | Droits (config.yaml) |
|---|---|---|
wake (serveur) |
allume, démarre, réveille | standard |
lock |
verrouille | standard |
shutdown |
éteins, arrête | complet + confirmation |
reboot |
redémarre, relance | complet + confirmation |
- Essai sur ce PC, sans rien éteindre :
make agent-run(créeagent/agent.yamlen mode essai). make run, puis « Hey Mycroft, éteins le PC du bureau » → confirmation → « oui » : l'agent afficheessai : systemctl poweroff (non execute).- Machine réelle : copier
agent/dessus,make -C agent install, régler/etc/voice-assistant/agent.yaml, et la nommer dansserver/config.yaml(machines.list).
Broker anonyme : tout appareil du réseau peut alors publier sur agent/<machine>/command.
En production, les comptes et droits de l'étape 15 (mosquitto/acl contient les règles des agents).
Étape 15 — fiabilisation
| Quoi | Comment |
|---|---|
| Comptes MQTT et droits | broker sans accès anonyme ; une carte ne lit et n'écrit que ses topics (docs/MOSQUITTO.md §2) |
| Présence du serveur | voice/server/status : online retenu, testament offline ; serveur absent = micros des cartes coupés |
| Chien de garde (watchdog) | carte figée plus de 15 s → redémarrage, cause watchdog dans l'événement boot |
| Réseau | connexion TCP au broker en 1 s au plus ; Wi-Fi relancé après 30 s d'absence |
| Journal | copie dans server/logs/voice-server.log (fichiers tournants, 4 Mo au plus) |
Mise en place, dans cet ordre : un broker encore ouvert accepte déjà les clients qui ont un compte.
make mqtt-user NAME=voice-server, puismake mqtt-user NAME=esp32-01(mots de passe :openssl rand -hex 16).server/.env:MQTT_USERNAME=voice-server,MQTT_PASSWORD=...;secrets.h:MQTT_PASSWORD="...".make fw-uploadetmake run: tout marche comme avant, le serveur annonce sa présence.make mosquitto-docker: le broker exige désormais les comptes.
Étape 14 — droits par profil
| Niveau | Droits | Attribué à |
|---|---|---|
| complet | tout ; actions sensibles (ouvrir la porte) après confirmation « oui » | profils listés dans access.users, si la voix est reconnue nettement (score ≥ 0,6) |
| standard | lumières, volets, fermer la porte | profil enrôlé sans niveau (access.default) |
| limite | conversation seulement | voix inconnue (access.unknown) |
Règles par appareil et action dans config.yaml (section access) ; une action absente des
règles exige le niveau complet. Les noms de access.users sont ceux de make profiles.
make run(serveur seul, firmware inchangé).- « Hey Mycroft, allume la lumière » → exécuté (Denis, complet).
- « Hey Mycroft, ouvre la porte » → « Confirmez-vous : ouvrir la porte du salon ? » → « Hey Mycroft, oui » → « J'ouvre la porte du salon. » ; « non » → « D'accord, j'annule. »
- Une voix du téléphone (profil standard) : « ouvre la porte » → refus ; une voix non
enrôlée : « allume la lumière » → « Je ne vous ai pas reconnu… ». Phrases prêtes :
recordings/voix-test/tom-porte.mp3,pierre-lumiere.mp3; d'autres avecmake say. Le mot de réveil y est dit par une voix anglaise (WAKE) : prononcé à la française par une voix de synthèse, il n'est pas reconnu.
Rappel : la voix n'est jamais une preuve d'identité. Ces droits évitent les erreurs et les commandes d'un invité, pas une attaque (enregistrement, imitation).
Étape 13 — mot de réveil (mains libres)
Le serveur demande à la carte d'envoyer son micro en continu (voice/<carte>/audio/stream)
et guette le mot de réveil avec openWakeWord : « Alexa », « Hé Maïcrofte » (Hey
Mycroft) ou « Hey Djâr-viss » (Hey Jarvis), prononcés à l'anglaise. Une fois le mot
entendu, il garde la commande jusqu'au silence final, puis la traite comme une session du
bouton. Le bouton reste utilisable à tout moment.
LED : éclair bref toutes les 2 s = écoute continue ; allumée = mot entendu, commande en cours ; clignotement lent = prêt, sans écoute continue.
make install-wakewordpuismake models(5 modèles ONNX, 5 Mo).make fw-upload SERIAL_PORT=/dev/ttyUSB0(firmware de l'étape 13), puismake run.- « Alexa, allume la lumière » → LED allumée pendant la commande, puis réponse et commande.
- Une phrase sans le mot → rien ne se passe, rien n'est enregistré.
- « Alexa » seul, puis rien → retour en veille après 4 s.
Vie privée : avant le mot, seules les 500 dernières ms restent en mémoire, rien n'est
écrit sur disque (vérifié par un test). Mais le son circule en continu sur le réseau : tant
que le broker accepte les clients anonymes (étape 15), n'importe qui sur le réseau local peut
l'écouter. Couper l'écoute : wake.enabled: false, ou
make mqtt-control MSG='{"cmd":"listen","enabled":false}'.
Étape 12 — tampon de sortie anti-gigue
La carte ne joue rien tant qu'elle n'a pas 300 ms de son en réserve (amorçage) ; le
Wi-Fi peut alors livrer les chunks en retard ou en rafale sans que cela s'entende. Si la
réserve s'épuise, la lecture s'arrête proprement et reprend une fois la réserve refaite
(réamorçage), au lieu de hacher le son. Le bilan played donne le délai avant le premier
son, la marge minimale (retard encore absorbable) et le nombre de réamorçages.
make fw-upload SERIAL_PORT=/dev/ttyUSB0(firmware de l'étape 12).make tone-mqtt: attendureamorcages 0, marge mini ≈ 250 ms.make tone-mqtt JITTER=200: un blocage de 200 ms toutes les 500 ms ; son toujours continu,reamorcages 0, marge mini ≈ 100 ms.make tone-mqtt JITTER=400: au-delà de la réserve ; pauses franches (réamorçages > 0), jamais de grésillement.make run: les réponses de l'assistant, avecreamorcages 0au signal habituel.
Étape 11 — domotique
Une commande tient en trois mots : une action (allumer, éteindre, ouvrir, fermer), un
appareil (lumière, volets, porte) et une pièce (liste dans config.yaml, section home).
Sans pièce, c'est celle de la carte. Le serveur publie home/<pièce>/<appareil>/set pour
toute domotique ; si la pièce est celle de la carte, la carte exécute aussi la commande :
LED de démonstration sur GPIO 21 pour la lumière, volets et porte simulés (liaison série).
- Câbler la LED (facultatif) : GPIO 21 → 330 Ω → LED → GND (docs/BROCHAGE.md §10).
make fw-upload SERIAL_PORT=/dev/ttyUSB0(firmware de l'étape 11), puismake run.- Terminal à part :
make mqtt-watch(voice/… en texte et home/#). - « Allume la lumière » → « J'allume la lumière du salon. », LED allumée,
home/salon/light/setet la confirmation{"event":"device",...,"ok":true}. - « Ferme les volets de la cuisine » →
home/cuisine/shutter/setseulement (autre pièce). - « Allume les volets » → « Je ne sais pas allumer les volets. » ; « Ouvre » → « Que dois-je ouvrir ? ».
Sécurité : l'identification vocale n'est jamais une preuve. Ne branchez pas une gâche de porte ou une alarme sur ces commandes ; les droits par profil arrivent à l'étape 14.
Étape 10 — qui a dit quoi
Whisper donne l'instant de chaque mot, la diarisation celui de chaque tour de parole, sur
le même audio : chaque mot va au locuteur qui parlait à ce moment. Le serveur publie le
résultat sur voice/<carte>/transcript et l'assistant appelle la personne par son nom.
{"session": "453cce", "utterances": [
{"speaker": "Denis", "text": "Ouvrir la porte.", "start": 0.0, "end": 1.26, "score": 0.6},
{"speaker": "Siwis", "text": "Je suis la voix de synthèse...", "start": 1.5, "end": 4.46, "score": 0.6},
{"speaker": "Denis", "text": "Fermez la porte.", "start": 4.92, "end": 5.86, "score": 0.6}]}
make diarize OUT=server/recordings/<fichier>.wav: qui a dit quoi, et le message MQTT, sans carte.make mqtt-watchdans un terminal,make rundans un autre.- « Bonjour » : la carte répond « Bonjour Denis ! » ;
voice/esp32-01/transcriptapparaît. - Une session à deux voix (vous, puis une voix du téléphone, puis vous) : trois interventions.
Étape 9 — identification du locuteur
Chaque voix trouvée par la diarisation est résumée par une empreinte (256 nombres, modèle WeSpeaker du pipeline pyannote), comparée par similarité cosinus aux profils enrôlés. Au-dessus du seuil (0,45, mesuré sur ce banc), le nom ; sinon « Inconnu ». C'est une estimation, jamais une sécurité : un enregistrement ou une imitation peut tromper.
make run, puis 5 phrases variées de 1 à 4 s, seul, à distance normale.make enroll NAME=Denis: profil créé depuis ces 5 sessions ; les sessions trop courtes, à plusieurs voix ou qui ne ressemblent pas aux autres sont écartées.- Nouvelles questions (le serveur voit le profil sans redémarrer) : le journal affiche
SPEAKER_00 = Denis (0.74); une autre personne ou une voix de haut-parleur donneInconnu (0.05, proche de Denis). make diarize-demo: votre voix reconnue, la voix Piper « Inconnu ».
Les profils sont des données biométriques : server/profiles/, jamais versionné,
un fichier JSON lisible par personne ; make forget NAME=... le supprime.
Étape 8 — diarisation : qui parle quand
La parole trouvée par la VAD est découpée en tours de parole : SPEAKER_00 de 0,0 à
1,3 s, SPEAKER_01 de 1,4 à 2,9 s... Les étiquettes ne valent que pour une session :
mettre un nom sur une voix, c'est l'étape 9. Modèle pyannote/speaker-diarization-community-1
(33 Mo), exécuté sur CPU, télémétrie de pyannote coupée (active par défaut en version 4).
- Compte sur https://huggingface.co, puis accepter les conditions sur https://huggingface.co/pyannote/speaker-diarization-community-1.
- Jeton de type « Read » sur https://huggingface.co/settings/tokens, copié dans
server/.env:HF_TOKEN=hf_...(cp server/.env.example server/.envs'il n'existe pas). make install-diarizationpuismake models. Ensuite, plus aucun accès à Internet.make diarize-demo: votre voix, la voix Piper, votre voix → deux locuteurs attendus.make run: une session où deux personnes parlent à tour de rôle ; le journal affiche2 locuteur(s) : SPEAKER_00 0.00-1.30, SPEAKER_01 1.40-2.90.
Sur Raspberry Pi 4, trop lourd : diarization.enabled: false dans config.yaml.
Étape 7 — détection d'activité vocale (VAD)
Le serveur ne transcrit plus que la parole : silences de début et de fin retirés, sessions sans parole (appui muet, clic) écartées sans appeler Whisper. Silero est livré avec faster-whisper : rien de plus à installer.
make vad-all: une ligne par fenêtre de 32 ms (#parole,_silence) pour chaque enregistrement, et le nombre de sessions sans parole.make vad OUT=server/recordings/<fichier>.wavpuismake play OUT=recordings/parole.wav: la parole seule, sans les silences.make run(à relancer : le code a changé), puis quelques questions et un appui sans parler. Le journal afficheparole 1.36 s sur 2.00 s (segments 0.49-1.85 s), et pour l'appui muetsans parole (VAD), avec une réponse immédiate.
Réglages dans config.yaml, section vad : seuil, pause minimale, durée minimale de parole, marge.
Étape 6 — MVP : l'assistant répond
make install-speechpuismake models(faster-whispersmall, voix Piperfr_FR-siwis-medium, environ 550 Mo dansserver/models/, une seule fois).make speech-check: Piper dit « Quelle heure est-il ? », Whisper le relit, l'assistant répond ; aucun matériel nécessaire.make transcribe OUT=server/recordings/<fichier>.wav: transcrit une phrase enregistrée par l'ESP32 à l'étape 4.make run: appui sur le bouton, « Quelle heure est-il ? », relâcher ; la carte répond. Questions reconnues : l'heure, la date, bonjour, merci, ton nom ; sinon, la carte répète ce qu'elle a compris.
Le journal donne le temps de chaque maillon : transcription 1.0 s, synthese 0.1 s, reponse prete en 1.1 s. make run-echo garde le mode écho de l'étape 5,
sans modèles.
Étape 5 — le serveur fait parler l'ESP32, par MQTT
make fw-upload SERIAL_PORT=/dev/ttyUSB0(firmware de l'étape 5).make tone-mqtt: 3 s de son pur envoyées par MQTT ; le bilan de la carte s'affiche (chunks 60/60, underruns 0).make play-mqtt OUT=server/recordings/<fichier>.wav: n'importe quel WAV, converti en 16 kHz mono, crête à −3 dBFS.make run-echo: le serveur rejoue chaque phrase à la carte qui l'a dite.
Étape 4 — la voix vers le serveur, par MQTT
make install(dépendances du serveur : paho-mqtt, PyYAML, numpy...).make run: le serveur se connecte au broker et attend les sessions.make fw-upload SERIAL_PORT=/dev/ttyUSB0, puis appui sur le bouton, une phrase, relâcher.- Le serveur écrit
server/recordings/esp32-01_<date>_<session>.wavet affiche son bilan :complete, chunks reçus / annoncés, crête et RMS. make play OUT=server/recordings/<fichier>.wavoumake inspect OUT=....
Si le broker est injoignable à l'appui, la session part sur la liaison série :
make record reste utilisable pour le dépannage.
Étape 3 — Wi-Fi et MQTT
- Broker : voir docs/MOSQUITTO.md (service Debian ou conteneur).
cp firmware/include/secrets.h.example firmware/include/secrets.h, puis renseigner le Wi-Fi (2,4 GHz) et l'adresse IP du broker.make mqtt-watch MQTT_HOST=<ip du broker>dans un premier terminal.make fw-upload SERIAL_PORT=/dev/ttyUSB0dans un second.
Attendu dans le premier terminal : voice/esp32-01/state {"status":"online",...}
et voice/esp32-01/event {"event":"boot",...}. Après une coupure d'alimentation
de l'ESP32 : voice/esp32-01/state {"status":"offline"} en 15 s au plus.
Étape 2 — tester la sortie audio
- Câbler l'ampli MAX98357A et le haut-parleur (§2), sans oublier C1 et C2.
make fw-upload SERIAL_PORT=/dev/ttyUSB0- Au démarrage, un bip de 440 Hz retentit : il teste la sortie seule.
make tone-esp: 3 s de son pur envoyées par le PC, au rythme réel. La moindre coupure s'y entend comme un clic.make play-esp OUT=recordings/essai.wav: l'enregistrement de l'étape 1 rejoué sur le haut-parleur de l'ESP32.
Attendu : son pur, sans clic ni grésillement ; le bilan de l'ESP32 affiche
0 refuses et 0 sous-alimentations.
Étape 1 — tester l'entrée audio
- Câbler le micro, le bouton et la LED comme au §2.
make fw-upload SERIAL_PORT=/dev/ttyUSB0make record OUT=recordings/essai.wav- Appuyer sur le bouton, dire une phrase, relâcher.
- Écouter :
make play OUT=recordings/essai.wav - Sans sortie audio :
make inspect OUT=recordings/essai.wavdessine l'enveloppe du signal en ASCII et donne crête, RMS et offset continu.
Attendu : la phrase est intelligible, sans saturation ; le script affiche une
crête entre 10 % et 90 % de la pleine échelle. Si la crête dépasse 99 %,
diminuer MIC_GAIN dans firmware/include/config.h ;
si elle reste sous 2 %, l'augmenter.
7. Arborescence
voice-assistant/
├── firmware/ firmware ESP32 (PlatformIO, Arduino)
│ ├── include/ config.h, secrets.h.example
│ └── src/ main.cpp + une paire .h/.cpp par classe
├── server/ serveur Python (un module par classe)
├── tools/ outils de test hors serveur
├── mosquitto/ voice.conf
├── Makefile toutes les opérations système
├── CONTEXT.md état d'avancement et décisions techniques
└── README.md / README-EN.md
8. Licence
MIT — voir LICENSE.