Cet article fait partie de la série Assistant vocal sur Raspberry Pi.
En six articles et deux soirées de travail, on a assemblé un assistant vocal franco-québécois qui tourne entièrement en local sur deux Raspberry Pi 4.
Ce qu’on a construit
L’architecture finale :
┌─────────────────────────────────────────────────────┐
│ Pi Client (2GB) │
│ Bouton GPIO → arecord → ffmpeg → Whisper.net │
│ .NET 10 Worker Service │
│ Piper TTS → ffmpeg → aplay │
└──────────────────────┬──────────────────────────────┘
│ HTTP (réseau local)
┌──────────────────────▼──────────────────────────────┐
│ Pi Cerveau (4GB) │
│ Ollama + llama3.2:1b │
└─────────────────────────────────────────────────────┘
En mode cloud : Pi Client → Claude API → Function calling → outils locaux
La stack complète :
| Composant | Technologie | Décision |
|---|---|---|
| Runtime | .NET 10 Worker Service | C# sur Pi, ça marche |
| Speech-to-text | Whisper.net (modèle base) | Gratuit, local, FR supporté |
| LLM local | Ollama + llama3.2:1b | Rapide sur 4GB RAM |
| LLM cloud | Claude API (Sonnet) | Meilleure qualité fr-CA |
| Text-to-speech | Piper TTS | Léger, voix française correcte |
| Audio | arecord + ffmpeg + aplay | Conversion 48kHz stéréo requise |
| GPIO | System.Device.Gpio | Simple, bien documenté |
| Météo | Open-Meteo API | Gratuit, sans clé |
| Tools | Anthropic.SDK function calling | Pattern extensible |
| Démarrage | systemd | Boot automatique |
Ce qui a bien fonctionné
Honnêtement, .NET sur Pi ARM64 m’a surpris par sa facilité. Je m’attendais à de la friction (un runtime qui manque, un truc qui compile pas), mais non. Une commande, ça roule. Le Worker Service et le DI container donnent une architecture propre que j’aurais du mal à faire aussi nettement en Python.
Séparer les deux Pi était aussi la bonne décision. Le Pi Client fait l’audio et la transcription, le Pi Cerveau fait le LLM. Ça donne la flexibilité de swapper un composant sans toucher à l’autre, ce qu’on a fait exactement à l’article #5 quand on a basculé d’Ollama à Claude API.
L’abstraction ILlmService payait pas de mine au départ, mais c’est elle qui a rendu ce swap trivial : une ligne dans appsettings.json. C’est le genre de décision qu’on fait “au cas où” et qui finit toujours par valoir la peine.
Open-Meteo mérite aussi une mention : une API météo gratuite, sans clé, sans inscription. Rare.
Les galères
L’adaptateur USB audio AB13X m’a pris pas mal de temps. Je m’attendais à brancher un micro et que ça marche, mais l’AB13X enregistre en 48kHz stéréo uniquement et Whisper veut du 16kHz mono. Pour la sortie, aplay accélérait l’audio parce que le taux d’échantillonnage ne correspondait pas. La solution passe par ffmpeg dans les deux sens, ce qui fonctionne bien maintenant, mais j’aurais gagné du temps avec un adaptateur qui supporte plusieurs formats dès le départ.
Le bouton GPIO m’a aussi joué un tour : un bouton tactile 4 pattes a ses pattes en deux paires, et si tu câbles les deux fils sur la même paire, rien ne se passe. Il faut câbler sur des côtés opposés. J’ai perdu une bonne heure là-dessus avant de comprendre.
La qualité de transcription est une autre limite visible. Whisper base s’en sort bien dans le calme, mais avec du bruit ambiant ou en parlant un peu vite en québécois, ça donne des perles :
- “Comment tu t’appelles?” → “Comment je te pète?”
- “Quel est ton nom?” → “Quelle est-on non?”
Et llama3.2:1b en français québécois, c’est créatif. Quand je lui demandais comment s’habiller par temps frais, il répondait “la crinière devra s’abuffer”. Claude API règle ça, mais au coût d’une dépendance cloud.
Si c’était à refaire
Je commencerais avec Whisper small plutôt que base, la différence de qualité justifie la latence. Je choisirais un adaptateur audio qui supporte le mono et plusieurs taux d’échantillonnage dès le départ, ce qui éviterait la complexité ffmpeg. Et je validerais le GPIO avec un script Python avant d’intégrer dans le code .NET.
La v2
Wake word. Remplacer le bouton par “Hey Alex” serait le changement le plus visible. La librairie openWakeWord tourne sur Pi ARM64 et est gratuite. C’est un projet en soi.
Écran d’état 4 pouces. J’ai un petit écran HDMI qui traîne. Afficher “À l’écoute…”, “Traitement…”, “En train de répondre…” via une interface minimale ASP.NET Core sur le Pi Client, ce serait simple et utile.
Transcription via API. Envoyer le WAV à l’API Whisper d’OpenAI plutôt que de transcrire localement. Bien meilleure qualité, latence réduite, ~0,006$/minute pour un usage personnel.
Mémoire persistante. L’historique disparaît au redémarrage. SQLite via EF Core réglerait ça proprement.
Home Assistant. Un HomeAssistantTool pour les lumières, les capteurs, la présence : quelques dizaines de lignes avec l’API REST.
Google Calendar. Un CalendarTool pour les événements du jour. “Qu’est-ce que j’ai aujourd’hui?” deviendrait réellement utile.
Récapitulatif de la série
| Article | Sujet | Effort estimé |
|---|---|---|
| #1 | Setup des deux Pi | ~2h |
| #2 | Worker Service .NET 10 + pipeline audio | ~3h |
| #3 | Intégration Ollama | ~1h |
| #4 | Mémoire, silence auto, systemd | ~2h |
| #5 | Météo temps réel + swap Claude API | ~2h |
| #6 | Function calling + outils extensibles | ~3h |
Environ deux soirées de travail pour un assistant vocal complet, en .NET, sur du matériel à moins de 150$.
Le code source complet de la série est disponible sur GitHub.
Articles de la série
- Setup des deux Raspberry Pi
- Worker Service .NET 10 et pipeline audio
- Intégration Ollama et contexte maison
- Mémoire, détection de silence et systemd
- Météo en temps réel et swap Claude API
- Function Calling : enseigner des outils à l’assistant
- Bilan, leçons apprises et perspectives v2 (cet article)
Ce qui m’a le plus surpris : que .NET roule aussi bien sur Pi ARM64, et qu’un LLM local sur un Raspberry Pi à moins de 100$ soit déjà utilisable. Le maillon faible reste Whisper base. Une fois ça réglé, l’assistant tient la route au quotidien.
Cet article a été rédigé avec l’aide de l’IA et révisé par moi.