Files
voice-assistant/Makefile
T
2026-10-06 18:17:48 +02:00

277 lines
14 KiB
Makefile

# Makefile — toutes les opérations système du projet assistant vocal.
# Cible par défaut : help.
SHELL := /bin/bash
VENV := server/.venv
PY := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
# PlatformIO vit dans son propre environnement : ses dépendances entrent sinon en
# conflit avec celles du serveur vocal (constaté avec click / esptool).
PIO_VENV := .pio-venv
PIO := $(PIO_VENV)/bin/pio
FW_DIR := firmware
SERIAL_PORT ?= /dev/ttyUSB0
BAUD ?= 921600
OUT ?= recordings/test.wav
TONE ?= 440
TEXT ?= Quelle heure est-il ?
LAST ?= 5
JITTER ?= 0
VOICE ?= fr_FR-tom-medium
SPEAKER ?=
SAY_OUT ?= recordings/voix-test/phrase.wav
WAKE ?=
WAKE_VOICE ?= en_GB-alan-medium
# Adresse du broker : UN seul réglage, mqtt.host de server/config.yaml, relu ici pour les
# cibles mqtt-* (surcharge ponctuelle : make mqtt-watch MQTT_HOST=...).
MQTT_HOST ?= $(shell awk '/^mqtt:/{m=1;next} m && $$1=="host:"{print $$2; exit}' server/config.yaml)
MSG ?= {"cmd":"ping"}
MOSQ_DIR ?= $(HOME)/srv/mosquitto
MOSQ_COMPOSE ?= $(HOME)/srv/docker-compose.mosquitto.yml
# Clients MQTT : ceux du système s'ils sont installés, sinon ceux de l'image Docker.
MOSQ_RUN := $(shell command -v mosquitto_sub >/dev/null 2>&1 || echo "docker run --rm --network host eclipse-mosquitto:2.0")
# Comptes MQTT (étape 15) : fichier de mots de passe hachés, jamais versionné.
MOSQ_PASSWD := $(shell command -v mosquitto_passwd >/dev/null 2>&1 && echo "mosquitto_passwd mosquitto/passwd" \
|| echo "docker run --rm -it --user $$(id -u):$$(id -g) -v $(CURDIR)/mosquitto:/work eclipse-mosquitto:2.0 mosquitto_passwd /work/passwd")
# Les cibles mqtt-* prennent les identifiants du serveur dans server/.env. Leurs
# recettes commencent par @ : make n'affiche pas la ligne, donc pas le mot de passe.
-include server/.env
MQTT_AUTH = $(if $(MQTT_USERNAME),-u '$(MQTT_USERNAME)' -P '$(MQTT_PASSWORD)')
MQTT_AS = @echo "(compte MQTT : $(if $(MQTT_USERNAME),$(MQTT_USERNAME) de server/.env,anonyme))"
.DEFAULT_GOAL := help
.PHONY: help install-system mosquitto-config mosquitto-docker mqtt-user agent-run agent-install agent-log mqtt-agents mqtt-watch mqtt-transcript mqtt-home mqtt-control venv install install-dev install-speech install-diarization install-wakeword models \
say speech-check transcribe vad vad-all diarize diarize-demo enroll profiles forget run test lint record play inspect play-esp tone-esp play-mqtt tone-mqtt run-echo pio-venv fw-build fw-check fw-upload \
fw-monitor clean
## ------------------------------------------------------------------ aide
help: ## Affiche cette aide
@echo "Assistant vocal local ESP32 — cibles disponibles :"
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) \
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[1m%-18s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo "Variables : SERIAL_PORT=$(SERIAL_PORT) BAUD=$(BAUD) OUT=$(OUT) TONE=$(TONE) MQTT_HOST=$(MQTT_HOST) JITTER=$(JITTER)"
@echo " TEXT=\"$(TEXT)\""
## ------------------------------------------------------- installation système
install-system: ## Installe les paquets Debian nécessaires (sudo)
sudo apt-get update
sudo apt-get install -y \
mosquitto mosquitto-clients \
ffmpeg \
python3 python3-venv python3-pip \
git
@echo "Ajout de l'utilisateur au groupe dialout (accès au port série)."
sudo usermod -aG dialout $$USER
@echo "→ Déconnectez/reconnectez votre session pour activer le groupe dialout."
mosquitto-config: mosquitto/passwd ## Installe voice.conf, acl et passwd dans /etc/mosquitto et redémarre le broker
sed 's|/mosquitto/config/|/etc/mosquitto/|' mosquitto/voice.conf | sudo tee /etc/mosquitto/conf.d/voice.conf >/dev/null
sudo install -o mosquitto -g mosquitto -m 0600 mosquitto/acl /etc/mosquitto/acl
sudo install -o mosquitto -g mosquitto -m 0600 mosquitto/passwd /etc/mosquitto/passwd
sudo systemctl restart mosquitto
sudo systemctl --no-pager status mosquitto | head -n 5
mosquitto-docker: mosquitto/passwd ## Installe voice.conf, acl et passwd dans le conteneur Mosquitto et le relance
@test -f $(MOSQ_COMPOSE) || { echo "Fichier compose introuvable : MOSQ_COMPOSE=$(MOSQ_COMPOSE)"; exit 1; }
-sudo cp $(MOSQ_DIR)/config/mosquitto.conf $(MOSQ_DIR)/config/mosquitto.conf.bak
sudo install -o 1883 -g 1883 -m 0644 mosquitto/voice.conf $(MOSQ_DIR)/config/mosquitto.conf
sudo install -o 1883 -g 1883 -m 0600 mosquitto/acl $(MOSQ_DIR)/config/acl
sudo install -o 1883 -g 1883 -m 0600 mosquitto/passwd $(MOSQ_DIR)/config/passwd
docker compose -f $(MOSQ_COMPOSE) restart
@sleep 2
docker compose -f $(MOSQ_COMPOSE) logs --tail 5
mosquitto/passwd:
@echo "mosquitto/passwd absent : creez d'abord les comptes (make mqtt-user NAME=voice-server, puis NAME=esp32-01)"; exit 1
mqtt-user: ## Crée ou change le compte MQTT NAME (voice-server, ou le DEVICE_ID d'une carte) dans mosquitto/passwd
@test -n "$(NAME)" || { echo "usage : make mqtt-user NAME=voice-server puis make mqtt-user NAME=esp32-01"; exit 1; }
@touch mosquitto/passwd && chmod 600 mosquitto/passwd
@echo "mot de passe de $(NAME) (idee : openssl rand -hex 16), a reporter dans $(if $(filter voice-server,$(NAME)),server/.env,$(if $(findstring esp32,$(NAME)),firmware/include/secrets.h,/etc/voice-assistant/agent.yaml)) :"
@$(MOSQ_PASSWD) $(NAME)
@echo "compte $(NAME) enregistre, hache, dans mosquitto/passwd. Installation : make mosquitto-docker (ou mosquitto-config)."
## ------------------------------------------------------------- agents Linux (étape 15b)
agent-run: agent/agent.yaml ## Lance l'agent de CETTE machine au premier plan (agent/agent.yaml, essai)
$(PY) agent/voice_agent.py --config agent/agent.yaml
agent/agent.yaml:
sed 's/^dry_run: false/dry_run: true/' agent/agent.yaml.example > $@
@echo "agent/agent.yaml cree en MODE ESSAI (dry_run: true) : verifiez id et mqtt.host"
agent-install: ## Installe l'agent comme service sur CETTE machine (autre machine : copier agent/, make -C agent install)
$(MAKE) -C agent install
agent-log: ## Journal de l'agent installé sur cette machine
$(MAKE) -C agent log
mqtt-agents: ## Affiche états, commandes et comptes rendus des agents : agent/#
$(MQTT_AS)
@$(MOSQ_RUN) mosquitto_sub -h $(MQTT_HOST) $(MQTT_AUTH) -v -t 'agent/#'
mqtt-watch: ## Affiche les messages texte voice/+/... et home/# (sans l'audio binaire), Ctrl-C pour quitter
$(MQTT_AS)
@$(MOSQ_RUN) mosquitto_sub -h $(MQTT_HOST) $(MQTT_AUTH) -v -t 'voice/+/state' -t 'voice/+/event' \
-t 'voice/+/control' -t 'voice/+/transcript' -t 'voice/server/status' -t 'home/#'
mqtt-home: ## Affiche seulement les commandes domotiques home/# (étape 11)
$(MQTT_AS)
@$(MOSQ_RUN) mosquitto_sub -h $(MQTT_HOST) $(MQTT_AUTH) -v -t 'home/#'
mqtt-transcript: ## Affiche seulement « qui a dit quoi » : voice/+/transcript (étape 10)
$(MQTT_AS)
@$(MOSQ_RUN) mosquitto_sub -h $(MQTT_HOST) $(MQTT_AUTH) -v -t 'voice/+/transcript'
mqtt-control: ## Publie MSG sur voice/esp32-01/control
$(MQTT_AS)
@$(MOSQ_RUN) mosquitto_pub -h $(MQTT_HOST) $(MQTT_AUTH) -q 1 -t voice/esp32-01/control -m '$(MSG)'
## ----------------------------------------------------------- environnement Python
venv: ## Crée l'environnement virtuel Python
python3 -m venv $(VENV)
$(PIP) install --upgrade pip setuptools wheel
install: venv ## Installe le serveur Python (dépendances de base)
$(PIP) install -e server
@test -f server/.env || { cp server/.env.example server/.env; echo "server/.env créé depuis .env.example — renseigner MQTT_USERNAME/PASSWORD et HF_TOKEN si besoin."; }
install-dev: install pio-venv ## Installe en plus les outils de développement (pyright, pytest, platformio)
$(PIP) install -e "server[dev]"
install-speech: install ## Installe faster-whisper et Piper (étape 6)
$(PIP) install -e "server[speech]"
install-diarization: install-speech ## Installe pyannote et torch, version CPU (étape 8, ~1,3 Go)
$(PIP) install --index-url https://download.pytorch.org/whl/cpu \
--extra-index-url https://pypi.org/simple -e "server[diarization]"
install-wakeword: install-speech ## Installe openWakeWord, en mode ONNX (étape 13)
$(PIP) install -e "server[wakeword]"
# Sans dépendances : openwakeword exige tflite-runtime, absent pour Python 3.13 (inutile en ONNX).
$(PIP) install --no-deps "openwakeword==0.6.0"
models: ## Télécharge les modèles de config.yaml dans server/models (étapes 6, 8 et 13)
$(PY) tools/download_models.py
say: ## Fait dire [WAKE (voix anglaise) puis] TEXT par VOICE (SPEAKER) : WAV + MP3 dans SAY_OUT, pour un téléphone
$(PY) tools/say.py "$(TEXT)" --voice $(VOICE) $(if $(SPEAKER),--speaker $(SPEAKER)) \
$(if $(WAKE),--wake "$(WAKE)" --wake-voice $(WAKE_VOICE)) --out $(SAY_OUT)
speech-check: ## Synthèse, transcription et RÉPONSE de l'assistant à TEXT, sans ESP32 (étape 6)
$(PY) tools/speech_check.py "$(TEXT)"
transcribe: ## VAD, transcription et réponse du WAV OUT, sans ESP32 (étapes 6-7)
$(PY) tools/speech_check.py --wav $(OUT)
vad: ## Montre la parole trouvée par la VAD dans OUT, écrit la parole seule (étape 7)
$(PY) tools/vad_check.py $(OUT)
vad-all: ## Passe la VAD sur tous les enregistrements du serveur (étape 7)
$(PY) tools/vad_check.py server/recordings/*.wav
diarize: ## Qui parle quand dans OUT, qui est-ce, qui a dit quoi (étapes 8-10)
$(PY) tools/diarize_check.py $(OUT)
diarize-demo: ## Diarise une séquence à deux voix : la vôtre et celle de Piper (étape 8)
$(PY) tools/diarize_check.py --demo
enroll: ## Crée le profil vocal de NAME avec ses LAST dernières sessions (étape 9)
@test -n "$(NAME)" || { echo "Usage : make enroll NAME=Denis [LAST=5]"; exit 1; }
$(PY) tools/enroll.py --name "$(NAME)" --last $(LAST)
profiles: ## Liste les profils vocaux (étape 9)
$(PY) tools/enroll.py --list
forget: ## Supprime le profil vocal de NAME : donnée biométrique (étape 9)
@test -n "$(NAME)" || { echo "Usage : make forget NAME=Denis"; exit 1; }
$(PY) tools/enroll.py --forget "$(NAME)"
## ----------------------------------------------------------------- serveur
run: ## Lance l'assistant vocal : répond à chaque session (étape 6, Ctrl-C pour arrêter)
cd server && .venv/bin/python -m voice_server --config config.yaml
run-echo: ## Serveur en mode echo : chaque session est rejouee par l'ESP32 (etape 5)
cd server && .venv/bin/python -m voice_server --config config.yaml --echo
test: ## Exécute les tests Python
$(VENV)/bin/pytest server/tests -v
lint: ## Vérifie le typage Python avec pyright
$(VENV)/bin/pyright --project server/pyrightconfig.json
## ----------------------------------------------------------------- outils
play: ## Joue OUT en cherchant un lecteur qui fonctionne
@for player in paplay pw-play ffplay aplay; do \
command -v $$player >/dev/null 2>&1 || continue; \
echo "-> $$player $(OUT)"; \
case $$player in \
ffplay) ffplay -autoexit -nodisp -loglevel error "$(OUT)" && exit 0 ;; \
*) $$player "$(OUT)" && exit 0 ;; \
esac; \
done; \
echo "Aucun lecteur n'a abouti. Utilisez 'make inspect' pour verifier sans ecouter."; \
exit 1
inspect: ## Analyse OUT sans le jouer : enveloppe ASCII et niveaux
$(PY) tools/wav_inspect.py $(OUT)
play-esp: ## Joue OUT sur le haut-parleur de l'ESP32, au rythme reel (etape 2)
$(PY) tools/wav_to_serial.py --port $(SERIAL_PORT) --baud $(BAUD) $(OUT)
tone-esp: ## Joue un son pur de TONE Hz sur le haut-parleur de l'ESP32 (etape 2)
$(PY) tools/wav_to_serial.py --port $(SERIAL_PORT) --baud $(BAUD) --tone $(TONE) --seconds 3
play-mqtt: ## Joue OUT sur l'ESP32, par MQTT, avec bilan ; JITTER=ms simule un Wi-Fi irrégulier (étapes 5, 12)
$(PY) tools/wav_to_mqtt.py --jitter $(JITTER) $(OUT)
tone-mqtt: ## Joue un son pur de TONE Hz sur l'ESP32, par MQTT ; JITTER=ms simule un Wi-Fi irrégulier (étapes 5, 12)
$(PY) tools/wav_to_mqtt.py --tone $(TONE) --seconds 5 --jitter $(JITTER)
record: ## Enregistre une session depuis l'ESP32 vers un WAV (étape 1)
@mkdir -p $$(dirname $(OUT))
$(PY) tools/serial_to_wav.py --port $(SERIAL_PORT) --baud $(BAUD) --output $(OUT)
## ---------------------------------------------------------------- firmware
pio-venv: ## Installe PlatformIO dans son propre environnement virtuel
python3 -m venv $(PIO_VENV)
$(PIO_VENV)/bin/pip install --upgrade pip
$(PIO_VENV)/bin/pip install platformio
# click 8.3+ casse l'esptool 5.0 livré avec le core Arduino 3.x : on épingle 8.1.
$(PIO_VENV)/bin/pip install "click==8.1.8"
fw-build: ## Compile le firmware ESP32
@test -x $(PIO) || { echo "PlatformIO absent : lancez 'make pio-venv'."; exit 1; }
$(PIO) run -d $(FW_DIR)
fw-check: ## Compile sans secrets.h personnel (secrets factices hors du dépôt)
@test -x $(PIO) || { echo "PlatformIO absent : lancez 'make pio-venv'."; exit 1; }
@if [ -f $(FW_DIR)/include/secrets.h ]; then \
echo "secrets.h present : compilation normale"; $(PIO) run -d $(FW_DIR); \
else \
tmp=$$(mktemp -d); cp $(FW_DIR)/include/secrets.h.example $$tmp/secrets.h; \
echo "secrets.h absent : secrets factices dans $$tmp (firmware/include/ n'est pas touche)"; \
PLATFORMIO_BUILD_FLAGS="-I$$tmp" $(PIO) run -d $(FW_DIR); status=$$?; rm -rf $$tmp; exit $$status; \
fi
fw-upload: ## Compile et téléverse le firmware
$(PIO) run -d $(FW_DIR) -t upload --upload-port $(SERIAL_PORT)
fw-monitor: ## Ouvre le moniteur série (Ctrl-C pour quitter)
$(PIO) device monitor -p $(SERIAL_PORT) -b $(BAUD)
## ------------------------------------------------------------------ nettoyage
clean: ## Supprime les artefacts de compilation et les caches
rm -rf $(FW_DIR)/.pio
find . -name '__pycache__' -type d -prune -exec rm -rf {} +
rm -rf server/*.egg-info .pytest_cache