<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://fabiencappelli.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fabiencappelli.com/" rel="alternate" type="text/html" /><updated>2026-04-15T19:50:35+00:00</updated><id>https://fabiencappelli.com/feed.xml</id><title type="html">Fabien Cappelli</title><subtitle>Portfolio &amp; Technical Notes</subtitle><entry xml:lang="en"><title type="html">Robie V1 — Wake Word, STT and TTS on Raspberry Pi</title><link href="https://fabiencappelli.com/robie/raspberry-pi/ai/audio/2026/04/12/robie-entendetparle-en.html" rel="alternate" type="text/html" title="Robie V1 — Wake Word, STT and TTS on Raspberry Pi" /><published>2026-04-12T00:00:00+00:00</published><updated>2026-04-12T00:00:00+00:00</updated><id>https://fabiencappelli.com/robie/raspberry-pi/ai/audio/2026/04/12/robie-entendetparle-en</id><content type="html" xml:base="https://fabiencappelli.com/robie/raspberry-pi/ai/audio/2026/04/12/robie-entendetparle-en.html"><![CDATA[<h2 id="a-real-local-voice-loop">A Real Local Voice Loop</h2>

<p>Today’s session had a simple goal: turn Robie into something more than a blinking prototype.</p>

<p>The idea was to validate a complete voice pipeline running locally on a Raspberry Pi:</p>

<ul>
  <li>waiting for a wake word</li>
  <li>listening to the command</li>
  <li>speech transcription in French</li>
  <li>spoken playback using text-to-speech</li>
  <li>returning to idle mode</li>
</ul>

<p>In other words: a first fully local conversational loop, without relying on any cloud service.</p>

<hr />

<h2 id="tested-architecture">Tested Architecture</h2>

<p>The selected pipeline relies on lightweight components suitable for a Raspberry Pi:</p>

<ul>
  <li><strong>OpenWakeWord</strong> for wake word detection</li>
  <li><strong>SoundDevice</strong> for audio capture</li>
  <li><strong>Vosk</strong> for offline speech recognition</li>
  <li><strong>Pico TTS</strong> for speech synthesis</li>
  <li><strong>DotStar LEDs</strong> for visual feedback</li>
</ul>

<p>The behavior is intentionally simple:</p>

<ul>
  <li>LEDs off while idle</li>
  <li>red while listening</li>
  <li>yellow while processing</li>
  <li>playback of the response</li>
  <li>lights off and return to standby</li>
</ul>

<hr />

<h2 id="pleasant-surprise-pico-tts">Pleasant Surprise: Pico TTS</h2>

<p>The most positive surprise of the day was the speed of <strong>Pico TTS</strong>.</p>

<p>The voice is clearly synthetic, almost retro, but generation is immediate and perfectly usable on modest hardware. In Robie’s case, that limitation almost becomes a strength: the robotic sound fits the project’s identity.</p>

<p>The real challenge is therefore not the voice itself, but the quality of the audio chain (speakers, volume, mixing, output quality).</p>

<hr />

<h2 id="speech-recognition-results">Speech Recognition Results</h2>

<p>The tests confirmed that local transcription works, but with predictable limitations:</p>

<ul>
  <li>noticeable latency</li>
  <li>variable results depending on speech clarity</li>
  <li>weaker performance with children’s voices</li>
  <li>more difficulty with younger users</li>
</ul>

<p>One interesting lesson already emerged: the system performs better when the speaker talks clearly and without hesitation. That means part of the user experience will also involve learning how to interact with it effectively.</p>

<hr />

<h2 id="what-this-session-validates">What This Session Validates</h2>

<p>Even imperfect, the prototype proves several important points:</p>

<ul>
  <li>a local voice assistant on Raspberry Pi is realistic</li>
  <li>open-source building blocks are enough for a credible V1</li>
  <li>the full wake word → STT → TTS loop genuinely works</li>
  <li>current limitations are more ergonomic than conceptual</li>
</ul>

<p>This is a bigger milestone than it may seem: Robie is no longer just an assembly of components, but an object that listens, sometimes understands, and responds.</p>

<hr />

<h2 id="improvement-paths">Improvement Paths</h2>

<p>Future iterations could focus on:</p>

<ul>
  <li>better recognition of children’s voices</li>
  <li>lower latency</li>
  <li>improved audio output quality</li>
  <li>more natural dialogues</li>
  <li>interruption during playback</li>
  <li>handling stories, voice notes, and multiple commands</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>The result is not perfect. Robie is sometimes slow, sometimes hesitant, sometimes clumsy.</p>

<p>But it works — and the children were wildly excited.</p>]]></content><author><name></name></author><category term="robie" /><category term="raspberry-pi" /><category term="ai" /><category term="audio" /><summary type="html"><![CDATA[A Real Local Voice Loop]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fabiencappelli.com/assets/images/blog/robie_2.png" /><media:content medium="image" url="https://fabiencappelli.com/assets/images/blog/robie_2.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Robie V1 — Wake Word, STT et TTS sur Raspberry Pi</title><link href="https://fabiencappelli.com/robie/raspberry-pi/ia/audio/2026/04/12/robie-entendetparle-fr.html" rel="alternate" type="text/html" title="Robie V1 — Wake Word, STT et TTS sur Raspberry Pi" /><published>2026-04-12T00:00:00+00:00</published><updated>2026-04-12T00:00:00+00:00</updated><id>https://fabiencappelli.com/robie/raspberry-pi/ia/audio/2026/04/12/robie-entendetparle-fr</id><content type="html" xml:base="https://fabiencappelli.com/robie/raspberry-pi/ia/audio/2026/04/12/robie-entendetparle-fr.html"><![CDATA[<h2 id="une-vraie-boucle-vocale-locale">Une vraie boucle vocale locale</h2>

<p>La session du jour avait un objectif simple : transformer Robie en quelque chose de plus qu’un prototype qui clignote.</p>

<p>L’idée était de valider une chaîne complète, embarquée sur Raspberry Pi :</p>

<ul>
  <li>attente d’un mot-clé (<em>wake word</em>)</li>
  <li>écoute de la commande</li>
  <li>transcription vocale en français</li>
  <li>restitution de la phrase en synthèse vocale</li>
  <li>retour en veille</li>
</ul>

<p>Autrement dit : une première boucle conversationnelle locale, sans dépendre d’un service cloud.</p>

<hr />

<h2 id="architecture-testée">Architecture testée</h2>

<p>Le pipeline retenu repose sur des composants légers et adaptés au Raspberry Pi :</p>

<ul>
  <li><strong>OpenWakeWord</strong> pour la détection du mot d’activation</li>
  <li><strong>SoundDevice</strong> pour la capture audio</li>
  <li><strong>Vosk</strong> pour la reconnaissance vocale hors ligne</li>
  <li><strong>Pico TTS</strong> pour la synthèse vocale</li>
  <li><strong>DotStar LEDs</strong> pour le feedback visuel</li>
</ul>

<p>Le comportement est volontairement simple :</p>

<ul>
  <li>LED éteintes en veille</li>
  <li>rouge pendant l’écoute</li>
  <li>jaune pendant le traitement</li>
  <li>lecture de la réponse</li>
  <li>extinction et reprise de la veille</li>
</ul>

<hr />

<h2 id="bonne-surprise--pico-tts">Bonne surprise : Pico TTS</h2>

<p>La découverte la plus positive de la journée a été la rapidité de <strong>Pico TTS</strong>.</p>

<p>La voix est clairement synthétique, presque rétro, mais la génération est immédiate et parfaitement exploitable sur une machine modeste. Dans le cadre de Robie, ce défaut devient presque une qualité : le rendu robotique colle bien à l’identité du projet.</p>

<p>Le vrai enjeu n’est donc pas tant la voix que la qualité de la chaîne audio (haut-parleurs, volume, mixage, sortie sonore).</p>

<hr />

<h2 id="résultats-sur-la-reconnaissance-vocale">Résultats sur la reconnaissance vocale</h2>

<p>Les tests ont confirmé que la transcription locale fonctionne, mais avec des limites prévisibles :</p>

<ul>
  <li>latence perceptible</li>
  <li>résultats variables selon la clarté de la diction</li>
  <li>performances plus fragiles avec les voix d’enfants</li>
  <li>davantage de difficulté avec les plus jeunes utilisateurs</li>
</ul>

<p>Un point intéressant est déjà apparu : l’outil fonctionne mieux quand la personne parle de manière nette et sans hésitation. Cela signifie qu’une partie de l’expérience utilisateur passera aussi par l’apprentissage du bon usage du système.</p>

<hr />

<h2 id="ce-que-cette-session-valide">Ce que cette session valide</h2>

<p>Même imparfait, le prototype prouve plusieurs choses importantes :</p>

<ul>
  <li>un assistant vocal local sur Raspberry Pi est réaliste</li>
  <li>des briques open source suffisent pour une V1 crédible</li>
  <li>la boucle complète wake word → STT → TTS fonctionne réellement</li>
  <li>les limites actuelles sont davantage ergonomiques que conceptuelles</li>
</ul>

<p>C’est un cap plus important qu’il n’y paraît : Robie n’est plus seulement un assemblage de composants, mais un objet qui écoute, comprend parfois, et répond.</p>

<hr />

<h2 id="pistes-damélioration">Pistes d’amélioration</h2>

<p>Les prochaines itérations pourront viser :</p>

<ul>
  <li>meilleure reconnaissance des voix enfantines</li>
  <li>réduction de la latence</li>
  <li>amélioration de la sortie audio</li>
  <li>dialogues plus naturels</li>
  <li>interruption pendant la lecture</li>
  <li>gestion d’histoires, notes vocales et commandes multiples</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Le résultat n’est pas parfait. Robie est parfois lent, parfois hésitant, parfois maladroit.</p>

<p>Mais il fonctionne, et les enfants étaient surexcités.</p>]]></content><author><name></name></author><category term="robie" /><category term="raspberry-pi" /><category term="ia" /><category term="audio" /><summary type="html"><![CDATA[Une vraie boucle vocale locale]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fabiencappelli.com/assets/images/blog/robie_2.png" /><media:content medium="image" url="https://fabiencappelli.com/assets/images/blog/robie_2.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Architecture of v2</title><link href="https://fabiencappelli.com/robie/2026/04/04/robie-v2archi-post-en.html" rel="alternate" type="text/html" title="Architecture of v2" /><published>2026-04-04T00:00:00+00:00</published><updated>2026-04-04T00:00:00+00:00</updated><id>https://fabiencappelli.com/robie/2026/04/04/robie-v2archi-post-en</id><content type="html" xml:base="https://fabiencappelli.com/robie/2026/04/04/robie-v2archi-post-en.html"><![CDATA[<p>While trying to think through the behavior I really want, I realized my naïve approach was incomplete. Robie will actually need to <strong>listen while it is reading</strong>. Because we’ll want to interrupt it:</p>

<ul>
  <li>to change the story</li>
  <li>to switch to another action</li>
  <li>to adjust the volume, since right now there is nothing in place to control Robie’s volume</li>
</ul>

<p>To kick off the thinking process, nothing beats a small diagram.</p>

<h2 id="flow">Flow</h2>

<div class="mermaid">

flowchart TB
Start([Start]) --&gt; Idle[Idle: waiting for wake word]

    Idle --&gt; Wake[/Wake word said/]
    Wake --&gt; Listen[/Record or listen live/]
    Listen --&gt; DetectIntent[Process intent]
    DetectIntent --&gt; ConfirmIntent[/Confirm intent/]
    ConfirmIntent --&gt; IsConfirmIntent{Intent confirmed?}

    IsConfirmIntent -- No --&gt; Listen
    IsConfirmIntent -- Yes --&gt; IsIntent{Which intent?}

    IsIntent -- Read a story --&gt; ReadingInit[Enter reading mode]
    IsIntent -- Take a note --&gt; NotePrompt[/Please record the note/]
    IsIntent -- Other request --&gt; HandleOther[Handle other intent]

    NotePrompt --&gt; NoteListen[/Record note/]
    NoteListen --&gt; NoteSave[Save note]
    NoteSave --&gt; Idle

    HandleOther --&gt; Idle

    subgraph ReadingMode [Reading mode]
        ReadingInit --&gt; StartPlayback[Start audio playback]
        StartPlayback --&gt; ReadingLoop[Reading active]

        ReadingLoop --&gt; CheckCommand{Command detected?}
        ReadingLoop --&gt; CheckTime{Is it midnight?}
        ReadingLoop --&gt; EndOfStory{Story finished?}

        CheckCommand -- No --&gt; ReadingLoop
        CheckCommand -- Stop --&gt; StopPlayback[Stop playback]
        CheckCommand -- Volume up --&gt; VolumeUp[Increase volume]
        CheckCommand -- Volume down --&gt; VolumeDown[Decrease volume]

        VolumeUp --&gt; ReadingLoop
        VolumeDown --&gt; ReadingLoop

        CheckTime -- No --&gt; ReadingLoop
        CheckTime -- Yes --&gt; Shutdown[Shutdown device]

        EndOfStory -- No --&gt; ReadingLoop
        EndOfStory -- Yes --&gt; ExitReading[Exit reading mode]
    end

    StopPlayback --&gt; Idle
    ExitReading --&gt; Idle

</div>

<h2 id="consequences">Consequences</h2>

<p>The central point is that <strong>Reading is not a one-shot action</strong>.<br />
It is a <strong>long-running active mode</strong>, during which several things must exist at the same time:</p>

<ul>
  <li>continuous audio playback</li>
  <li>listening for control commands</li>
  <li>monitoring the time</li>
  <li>the ability to interrupt playback cleanly</li>
</ul>

<p>In other words, my system can no longer be designed as a simple linear chain such as:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wake → listen → STT → action → end
</code></pre></div></div>

<p>It must become a system with <strong>persistent activity + concurrent events</strong>.</p>

<h3 id="first-constraint-concurrency">First Constraint: Concurrency</h3>

<p>Since I do not want to split playback into tiny chunks, the reading must continue <strong>while something else is happening</strong>.</p>

<p>That implies some form of concurrency, typically:</p>

<ul>
  <li>multithreading</li>
  <li>separate processes</li>
  <li>or a more advanced event loop</li>
</ul>

<p>In all cases, we move beyond the logic of “one loop doing everything in order”.</p>

<p>Concretely, I will probably need at least:</p>

<ul>
  <li>one component managing playback</li>
  <li>one component listening to the microphone</li>
  <li>one component processing commands</li>
  <li>one component monitoring the clock</li>
  <li>one orchestrator deciding what to do</li>
</ul>

<h3 id="second-constraint-clean-inter-component-communication">Second Constraint: Clean Inter-Component Communication</h3>

<p>As soon as multiple activities run in parallel, I need to define how they communicate.</p>

<p>For example:</p>

<ul>
  <li>the microphone module detects “stop”</li>
  <li>it must notify the playback module</li>
  <li>the clock module detects midnight</li>
  <li>it must trigger a global shutdown</li>
  <li>the playback module reaches the end of the file</li>
  <li>it must notify the system to return to <code class="language-plaintext highlighter-rouge">Idle</code></li>
</ul>

<p>So I can no longer rely on simple functions calling one another directly.
I need logic such as:</p>

<ul>
  <li>events</li>
  <li>message queues</li>
  <li>state flags</li>
  <li>synchronization objects</li>
</ul>

<p>Otherwise, I’ll quickly end up with spaghetti code.</p>

<h3 id="third-constraint-a-real-state-model">Third Constraint: A Real State Model</h3>

<p>My diagram implicitly says that we are no longer only in “do an action”, but in “be in a state”.</p>

<p>For example:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Idle</code></li>
  <li><code class="language-plaintext highlighter-rouge">Listening</code></li>
  <li><code class="language-plaintext highlighter-rouge">Reading</code></li>
  <li><code class="language-plaintext highlighter-rouge">Note recording</code></li>
  <li>maybe later <code class="language-plaintext highlighter-rouge">Thinking</code></li>
  <li>maybe <code class="language-plaintext highlighter-rouge">Shutting down</code></li>
</ul>

<p>And while in <code class="language-plaintext highlighter-rouge">Reading</code>, some commands are allowed:</p>

<ul>
  <li>stop</li>
  <li>volume up</li>
  <li>volume down</li>
</ul>

<p>while others may not be allowed, or not handled the same way.</p>

<p>So I need to explicitly model:</p>

<ul>
  <li>the current state</li>
  <li>allowed transitions</li>
  <li>what happens when an event arrives in a given state</li>
</ul>

<p>Otherwise I’ll get fuzzy behaviors like:</p>

<p>“What does Robie do if someone talks while it is reading?”
“What happens if midnight occurs during a volume command?”</p>

<h3 id="fourth-constraint-clean-interruption">Fourth Constraint: Clean Interruption</h3>

<p>Continuous audio playback means I must be able to:</p>

<ul>
  <li>stop immediately</li>
  <li>possibly pause</li>
  <li>change volume on the fly</li>
  <li>exit without leaving the audio system in a broken state</li>
</ul>

<p>So the audio player cannot be a simple blocking command launched without control.
It must be a controllable component, with commands such as:</p>

<ul>
  <li>start</li>
  <li>stop</li>
  <li>pause</li>
  <li>set_volume</li>
</ul>

<p>And those commands must remain safe no matter when they arrive.</p>

<h3 id="fifth-constraint-speech-recognition-can-no-longer-be-designed-the-same-way">Fifth Constraint: Speech Recognition Can No Longer Be Designed the Same Way</h3>

<p>In a classic conversational loop, we do:</p>

<ul>
  <li>record</li>
  <li>transcribe</li>
  <li>act</li>
</ul>

<p>But here, during reading, I need to detect <strong>very short commands continuously</strong>.</p>

<p>So I am no longer doing only “classic” STT.
Instead, I need continuous control listening, probably with:</p>

<ul>
  <li>reduced vocabulary</li>
  <li>limited command logic</li>
  <li>fast and robust detection</li>
</ul>

<p>So the problem is no longer:</p>

<p>“transcribe an open request”</p>

<p>but rather:</p>

<p>“quickly and reliably detect a few critical commands”</p>

<p>That is a different kind of need.</p>

<h3 id="sixth-constraint-risk-of-robie-hearing-itself">Sixth Constraint: Risk of Robie Hearing Itself</h3>

<p>This is probably one of the hidden big challenges of reading mode.</p>

<p>If Robie reads aloud while listening, the microphone may capture:</p>

<ul>
  <li>its own playback</li>
  <li>reverberation</li>
  <li>children’s voices</li>
  <li>ambient noise</li>
</ul>

<p>So I’ll need safeguards such as:</p>

<ul>
  <li>short and highly specific commands</li>
  <li>adapted thresholds / detection logic</li>
  <li>maybe a secondary wake word in reading mode</li>
  <li>or microphone / volume / physical placement adjustments</li>
</ul>

<p>The diagram does not mention it, but formalizing <code class="language-plaintext highlighter-rouge">Reading</code> as an interactive mode directly creates this problem.</p>

<h3 id="seventh-constraint-priority-logic">Seventh Constraint: Priority Logic</h3>

<p>Not all events have the same weight.</p>

<p>For example:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Shutdown at midnight</code> is probably highest priority</li>
  <li><code class="language-plaintext highlighter-rouge">Stop playback</code> is very high priority</li>
  <li><code class="language-plaintext highlighter-rouge">Volume up</code> is less critical</li>
  <li><code class="language-plaintext highlighter-rouge">Story finished</code> is a normal event</li>
</ul>

<p>So I will need to define a policy:</p>

<ul>
  <li>what interrupts what</li>
  <li>who wins in case of collision</li>
  <li>in what order events are processed</li>
</ul>

<p>Without that, strange behaviors are likely.</p>

<h3 id="eighth-constraint-separate-behavior-from-implementation">Eighth Constraint: Separate Behavior from Implementation</h3>

<p>The diagram is excellent because it formalizes the expected behavior.
But it also forces an important distinction:</p>

<ul>
  <li><strong>functional level</strong>: what Robie must do</li>
  <li><strong>technical level</strong>: how it is implemented</li>
</ul>

<p>In this case, the formalization already says I will probably need:</p>

<ul>
  <li>a controllable audio player</li>
  <li>parallel listening</li>
  <li>autonomous time monitoring</li>
  <li>an event system</li>
  <li>explicit state management</li>
</ul>

<p>Even if I have not yet chosen between:</p>

<ul>
  <li>thread</li>
  <li>callback</li>
  <li>queue</li>
  <li>process</li>
</ul>

<h3 id="summary">Summary</h3>

<p>This formalization leads to one clear conclusion:</p>

<p>Robie can no longer be developed as a simple sequential pipeline.
Reading mode requires a <strong>concurrent, event-driven architecture with explicit state management</strong>.</p>

<p>More concretely, that means:</p>

<ul>
  <li>several activities must run at the same time</li>
  <li>they must communicate cleanly</li>
  <li>the system must know its current state</li>
  <li>playback must be interruptible at any moment</li>
  <li>voice detection during playback becomes a specific problem</li>
  <li>priorities and transitions must be handled properly</li>
</ul>]]></content><author><name></name></author><category term="robie" /><summary type="html"><![CDATA[While trying to think through the behavior I really want, I realized my naïve approach was incomplete. Robie will actually need to listen while it is reading. Because we’ll want to interrupt it:]]></summary></entry><entry xml:lang="fr"><title type="html">Architecture de la v2</title><link href="https://fabiencappelli.com/robie/2026/04/04/robie-v2archi-post-fr.html" rel="alternate" type="text/html" title="Architecture de la v2" /><published>2026-04-04T00:00:00+00:00</published><updated>2026-04-04T00:00:00+00:00</updated><id>https://fabiencappelli.com/robie/2026/04/04/robie-v2archi-post-fr</id><content type="html" xml:base="https://fabiencappelli.com/robie/2026/04/04/robie-v2archi-post-fr.html"><![CDATA[<p>En essayant de réfléchir au comportement que je veux vraiment avoir, je comprends que mon approche naïve était incomplète. Il va falloir que Robie arrive en fait à écouter en même temps qu’il “lit”. Parce qu’on va vouloir l’interrompre :</p>

<ul>
  <li>pour qu’il change d’histoire</li>
  <li>pour qu’il change d’action</li>
  <li>pour régler le volume, même, car pour l’instant il n’y a rien pour pouvoir contrôler le volume de Robie</li>
</ul>

<p>Pour amorcer la réflexion, rien de tel qu’un petit schéma</p>

<h2 id="flow">Flow</h2>

<div class="mermaid">

flowchart TB
Start([Start]) --&gt; Idle[Idle: waiting for wake word]

    Idle --&gt; Wake[/Wake word said/]
    Wake --&gt; Listen[/Record or listen live/]
    Listen --&gt; DetectIntent[Process intent]
    DetectIntent --&gt; ConfirmIntent[/Confirm intent/]
    ConfirmIntent --&gt; IsConfirmIntent{Intent confirmed?}

    IsConfirmIntent -- No --&gt; Listen
    IsConfirmIntent -- Yes --&gt; IsIntent{Which intent?}

    IsIntent -- Read a story --&gt; ReadingInit[Enter reading mode]
    IsIntent -- Take a note --&gt; NotePrompt[/Please record the note/]
    IsIntent -- Other request --&gt; HandleOther[Handle other intent]

    NotePrompt --&gt; NoteListen[/Record note/]
    NoteListen --&gt; NoteSave[Save note]
    NoteSave --&gt; Idle

    HandleOther --&gt; Idle

    subgraph ReadingMode [Reading mode]
        ReadingInit --&gt; StartPlayback[Start audio playback]
        StartPlayback --&gt; ReadingLoop[Reading active]

        ReadingLoop --&gt; CheckCommand{Command detected?}
        ReadingLoop --&gt; CheckTime{Is it midnight?}
        ReadingLoop --&gt; EndOfStory{Story finished?}

        CheckCommand -- No --&gt; ReadingLoop
        CheckCommand -- Stop --&gt; StopPlayback[Stop playback]
        CheckCommand -- Volume up --&gt; VolumeUp[Increase volume]
        CheckCommand -- Volume down --&gt; VolumeDown[Decrease volume]

        VolumeUp --&gt; ReadingLoop
        VolumeDown --&gt; ReadingLoop

        CheckTime -- No --&gt; ReadingLoop
        CheckTime -- Yes --&gt; Shutdown[Shutdown device]

        EndOfStory -- No --&gt; ReadingLoop
        EndOfStory -- Yes --&gt; ExitReading[Exit reading mode]
    end

    StopPlayback --&gt; Idle
    ExitReading --&gt; Idle

</div>

<h2 id="conséquences">Conséquences</h2>

<p>Le point central, c’est que <strong>Reading n’est pas une action ponctuelle</strong>.
C’est un <strong>mode actif long</strong>, pendant lequel plusieurs choses doivent exister en même temps :</p>

<ul>
  <li>la lecture audio continue</li>
  <li>l’écoute de commandes de contrôle</li>
  <li>la surveillance de l’heure</li>
  <li>la capacité à interrompre proprement la lecture</li>
</ul>

<p>Autrement dit, mon système ne peut plus être pensé comme une simple chaîne linéaire du type :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wake → écoute → STT → action → fin
</code></pre></div></div>

<p>Il doit devenir un système avec <strong>activité persistante + événements concurrents</strong>.</p>

<h3 id="première-contrainte--concurrence">Première contrainte : Concurrence</h3>

<p>Comme je ne veux pas découper la lecture en petits bouts, la lecture doit pouvoir continuer <strong>pendant qu’autre chose se passe</strong>.</p>

<p>Ça implique une forme de concurrence, typiquement :</p>

<ul>
  <li>multithread</li>
  <li>ou processus séparés</li>
  <li>ou boucle événementielle plus élaborée</li>
</ul>

<p>Mais dans tous les cas, on quitte la logique “une seule boucle qui fait tout dans l’ordre”.</p>

<p>Concrètement, il faudra probablement au minimum distinguer :</p>

<ul>
  <li>un composant qui gère la lecture</li>
  <li>un composant qui écoute le micro</li>
  <li>un composant qui traite les commandes</li>
  <li>un composant qui surveille l’heure</li>
  <li>un orchestrateur qui décide quoi faire</li>
</ul>

<h3 id="deuxième-contrainte--communication-inter-composants-propre">Deuxième contrainte : Communication inter-composants propre</h3>

<p>Dès qu’on a plusieurs activités en parallèle, on doit définir comment elles communiquent.</p>

<p>Par exemple :</p>

<ul>
  <li>le module micro détecte “stop”</li>
  <li>il doit prévenir le module lecture</li>
  <li>le module horloge détecte minuit</li>
  <li>il doit déclencher un arrêt global</li>
  <li>le module lecture termine le fichier</li>
  <li>il doit notifier le système pour revenir à <code class="language-plaintext highlighter-rouge">Idle</code></li>
</ul>

<p>Donc je ne peux plus me contenter de fonctions qui s’appellent les unes les autres de manière simple.
Il faut une logique de type :</p>

<ul>
  <li>événements</li>
  <li>files de messages</li>
  <li>drapeaux d’état</li>
  <li>objets de synchronisation</li>
</ul>

<p>Sinon je vais très vite entrer dans du spaghetti.</p>

<h3 id="troisième-contrainte--vrai-modèle-détat">Troisième contrainte : Vrai modèle d’état</h3>

<p>Mon schéma dit implicitement qu’on n’est plus seulement dans “faire une action”, mais dans “être dans un état”.</p>

<p>Par exemple :</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Idle</code></li>
  <li><code class="language-plaintext highlighter-rouge">Listening</code></li>
  <li><code class="language-plaintext highlighter-rouge">Reading</code></li>
  <li><code class="language-plaintext highlighter-rouge">Note recording</code></li>
  <li>peut-être plus tard <code class="language-plaintext highlighter-rouge">Thinking</code></li>
  <li>peut-être <code class="language-plaintext highlighter-rouge">Shutting down</code></li>
</ul>

<p>Et pendant <code class="language-plaintext highlighter-rouge">Reading</code>, certaines commandes sont autorisées :</p>

<ul>
  <li>stop</li>
  <li>volume up</li>
  <li>volume down</li>
</ul>

<p>alors que d’autres ne le sont peut-être pas, ou pas de la même manière.</p>

<p>Donc il faut modéliser explicitement :</p>

<ul>
  <li>l’état courant</li>
  <li>les transitions autorisées</li>
  <li>ce qui se passe quand un événement arrive dans tel ou tel état</li>
</ul>

<p>Sinon j’aurai des comportements flous du genre :
“que fait Robie si on lui parle pendant qu’il lit ?”
“qu’arrive-t-il si minuit tombe pendant une commande volume ?”</p>

<h3 id="quatrième-contrainte--interruption-propre">Quatrième contrainte : Interruption propre</h3>

<p>Une lecture audio continue, ça veut dire qu’il faudra savoir :</p>

<ul>
  <li>arrêter immédiatement</li>
  <li>mettre en pause éventuellement</li>
  <li>changer le volume à chaud</li>
  <li>sortir sans laisser le système audio dans un état bancal</li>
</ul>

<p>Donc le lecteur audio ne pourra pas être une simple commande bloquante lancée sans contrôle.
Il faudra un composant pilotable, avec des commandes du type :</p>

<ul>
  <li>start</li>
  <li>stop</li>
  <li>pause</li>
  <li>set_volume</li>
</ul>

<p>Et ces commandes devront être sûres même si elles arrivent à n’importe quel moment.</p>

<h3 id="cinquième-contrainte--la-reconnaissance-vocale-ne-peut-plus-être-pensée-comme-avant">Cinquième contrainte : la reconnaissance vocale ne peut plus être pensée comme avant</h3>

<p>Dans la boucle conversationnelle classique, on fait :</p>

<ul>
  <li>on enregistre</li>
  <li>puis on transcrit</li>
  <li>puis on agit</li>
</ul>

<p>Mais ici, pendant la lecture, il faut détecter <strong>en continu</strong> des commandes très courtes.</p>

<p>Donc je ne fais plus de la STT “classique” seulement.
Je fais plutôt une écoute de contrôle en continu, probablement avec :</p>

<ul>
  <li>vocabulaire réduit</li>
  <li>logique de commandes limitées</li>
  <li>détection robuste et rapide</li>
</ul>

<p>Donc le problème n’est plus :
“transcrire une demande ouverte”</p>

<p>mais plutôt :
“repérer vite et proprement quelques ordres critiques”</p>

<p>C’est un autre type de besoin.</p>

<h3 id="sixième-contrainte--risque-que-robie-sentende-lui-même">Sixième contrainte : Risque que Robie s’entende lui-même</h3>

<p>C’est sans doute l’un des gros défis cachés du mode lecture.</p>

<p>Si Robie lit à voix haute pendant qu’il écoute, alors le micro risque de capter :</p>

<ul>
  <li>sa propre lecture</li>
  <li>de la réverbération</li>
  <li>des voix d’enfants</li>
  <li>du bruit ambiant</li>
</ul>

<p>Donc je devrai penser à des garde-fous :</p>

<ul>
  <li>commandes courtes très spécifiques</li>
  <li>seuils / détection adaptée</li>
  <li>peut-être wake word secondaire en mode lecture</li>
  <li>ou réglages micro/volume/placement physique</li>
</ul>

<p>Le diagramme n’en parle pas, mais la formalisation de <code class="language-plaintext highlighter-rouge">Reading</code> comme mode interactif implique directement ce problème.</p>

<h3 id="septième-contrainte--logique-de-priorités">Septième contrainte : Logique de priorités</h3>

<p>Tous les événements n’ont pas le même poids.</p>

<p>Par exemple :</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Shutdown at midnight</code> doit sans doute être prioritaire</li>
  <li><code class="language-plaintext highlighter-rouge">Stop playback</code> est très prioritaire</li>
  <li><code class="language-plaintext highlighter-rouge">Volume up</code> est moins critique</li>
  <li><code class="language-plaintext highlighter-rouge">Story finished</code> est un événement normal</li>
</ul>

<p>Donc il faudra définir une politique :</p>

<ul>
  <li>qui interrompt quoi</li>
  <li>qui gagne en cas de collision</li>
  <li>dans quel ordre on traite les événements</li>
</ul>

<p>Sans ça, risques de comportements bizarres.</p>

<h3 id="huitième-contrainte--séparer-comportement-et-implémentation">Huitième contrainte : Séparer comportement et implémentation</h3>

<p>Le schéma est excellent parce qu’il formalise le comportement attendu.
Mais il force aussi une distinction importante :</p>

<ul>
  <li><strong>niveau fonctionnel</strong> : ce que Robie doit faire</li>
  <li><strong>niveau technique</strong> : comment on le réalise</li>
</ul>

<p>Dans ce cas, la formalisation dit déjà qu’il faudra probablement :</p>

<ul>
  <li>un lecteur audio contrôlable</li>
  <li>une écoute en parallèle</li>
  <li>une surveillance horaire autonome</li>
  <li>un système d’événements</li>
  <li>une gestion d’état explicite</li>
</ul>

<p>Même si on n’a pas encore choisi :</p>

<ul>
  <li>thread</li>
  <li>callback</li>
  <li>queue</li>
  <li>process</li>
</ul>

<h2 id="en-résumé">En résumé</h2>

<p>La formalisation entraîne ceci :</p>

<p>Robie ne peut plus être développé comme un simple pipeline séquentiel.
Le mode lecture impose une architecture <strong>concurrente, pilotée par événements, avec gestion d’état explicite</strong>.</p>

<p>Plus concrètement, ça veut dire :</p>

<ul>
  <li>plusieurs activités doivent vivre en même temps</li>
  <li>elles doivent communiquer proprement</li>
  <li>le système doit connaître son état courant</li>
  <li>la lecture doit être interrompable à tout moment</li>
  <li>la détection vocale pendant lecture devient un problème spécifique</li>
  <li>il faut gérer les priorités et les transitions proprement</li>
</ul>]]></content><author><name></name></author><category term="robie" /><summary type="html"><![CDATA[En essayant de réfléchir au comportement que je veux vraiment avoir, je comprends que mon approche naïve était incomplète. Il va falloir que Robie arrive en fait à écouter en même temps qu’il “lit”. Parce qu’on va vouloir l’interrompre :]]></summary></entry><entry xml:lang="en"><title type="html">Continuation of v1</title><link href="https://fabiencappelli.com/robie/audio/2026/04/03/robie-second-post-en.html" rel="alternate" type="text/html" title="Continuation of v1" /><published>2026-04-03T00:00:00+00:00</published><updated>2026-04-03T00:00:00+00:00</updated><id>https://fabiencappelli.com/robie/audio/2026/04/03/robie-second-post-en</id><content type="html" xml:base="https://fabiencappelli.com/robie/audio/2026/04/03/robie-second-post-en.html"><![CDATA[<p><img src="/assets/images/blog/robie_1.jpg" alt="image" /></p>

<hr />

<h2 id="rebuilding-a-clean-virtual-environment">Rebuilding a Clean Virtual Environment</h2>

<p>And… crash.</p>

<p>Everything broke, especially the Adafruit Voice Bonnet handling.</p>

<p>I had to start over to make audio input and output work again.</p>

<hr />

<h3 id="step-1--the-real-wall-low-level-audio">Step 1 — The Real Wall: Low-Level Audio</h3>

<p>Before even talking about AI, the first challenge was… the microphone.</p>

<h4 id="problems-encountered">Problems encountered:</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">RPi.GPIO</code> errors → conflict between Python environment and system libraries</li>
  <li><code class="language-plaintext highlighter-rouge">sounddevice</code> unable to open the audio stream</li>
  <li>PulseAudio / PipeWire locking the device</li>
  <li>ALSA detects the card… but rejects every format</li>
</ul>

<h4 id="typical-symptoms">Typical symptoms:</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">PortAudioError: Invalid number of channels</code></li>
  <li><code class="language-plaintext highlighter-rouge">device or resource busy</code></li>
  <li><code class="language-plaintext highlighter-rouge">Unable to install hw params</code></li>
</ul>

<h4 id="important-lessons">Important lessons:</h4>

<ul>
  <li>On Raspberry Pi, <strong>avoid high-level audio layers</strong></li>
  <li>Go directly through <strong>ALSA (<code class="language-plaintext highlighter-rouge">arecord</code>)</strong></li>
  <li>Disable PipeWire/PulseAudio if needed</li>
  <li>Check codec configuration with <code class="language-plaintext highlighter-rouge">alsamixer</code></li>
</ul>

<p>Once this step is solved, everything becomes much easier.</p>

<hr />

<h3 id="step-2--working-audio-pipeline">Step 2 — Working Audio Pipeline</h3>

<p>After stabilization, we finally get:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>microphone → ALSA → recording → processing → playback
</code></pre></div></div>

<p>And on the UX side:</p>

<ul>
  <li>LED off → standby</li>
  <li>red LED → listening</li>
  <li>yellow LED → processing</li>
  <li>sound → response</li>
</ul>

<p>At this stage, the robot already feels “alive”.</p>

<hr />

<h3 id="step-3--whisper-attempt-and-failure">Step 3 — Whisper Attempt (and Failure)</h3>

<p>The next logical step was transcription with <code class="language-plaintext highlighter-rouge">faster-whisper</code>.</p>

<h4 id="result">Result:</h4>

<ul>
  <li>huge latency (several seconds, sometimes tens of seconds)</li>
  <li>poor quality with the <code class="language-plaintext highlighter-rouge">tiny</code> model</li>
  <li>impossible to improve quality without exploding compute time</li>
</ul>

<h4 id="why-it-fails">Why it fails:</h4>

<ul>
  <li>Raspberry Pi 4 is too limited for modern STT</li>
  <li>Whisper is optimized for GPUs or powerful CPUs</li>
  <li>impossible to maintain a good quality/speed tradeoff</li>
</ul>

<p>Conclusion:
<strong>Whisper is excellent… but not for this use case on Pi.</strong></p>

<hr />

<h3 id="step-4--pivot-to-vosk">Step 4 — Pivot to Vosk</h3>

<p>Strategy shift: test Vosk.</p>

<h4 id="immediate-result">Immediate result:</h4>

<ul>
  <li>much better latency</li>
  <li>almost correct transcription</li>
  <li>stable pipeline</li>
</ul>

<p>Big improvement.</p>

<p>But…</p>

<h4 id="new-problem">New problem:</h4>

<ul>
  <li>~10 seconds to process 4 seconds of audio</li>
  <li>still too slow for natural interaction</li>
</ul>

<hr />

<h4 id="key-insight-wrong-problem">Key Insight: Wrong Problem</h4>

<p>The issue was not the engine.</p>

<p>The issue was the task.</p>

<p>We were asking:</p>

<blockquote>
  <p>“Freely transcribe everything I say”</p>
</blockquote>

<p>When the real need was:</p>

<blockquote>
  <p>“Recognize a few simple commands”</p>
</blockquote>

<hr />

<h3 id="step-5--paradigm-shift">Step 5 — Paradigm Shift</h3>

<p>Instead of voice dictation, move to <strong>voice command recognition</strong>.</p>

<h4 id="example">Example:</h4>

<p>```python id=”a1r7kp”
if “hello” in text:
    play(“hello.mp3”)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
Or even better: restrict the vocabulary directly in Vosk:

```python id="w2m5dz"
rec = KaldiRecognizer(
    model,
    16000,
    '["hello", "story", "music", "stop"]'
)
</code></pre></div></div>

<h4 id="result-1">Result:</h4>

<ul>
  <li>faster</li>
  <li>more reliable</li>
  <li>much more robust</li>
</ul>

<hr />

<h3 id="final-architecture-v1">Final Architecture (V1)</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Wake word
    ↓
Red LED (listening)
    ↓
Short recording (2–3s)
    ↓
Vosk (limited vocabulary)
    ↓
Simple intent
    ↓
Audio response
    ↓
Back to standby
</code></pre></div></div>

<hr />

<h3 id="what-really-made-the-difference">What Really Made the Difference</h3>

<h4 id="what-does-not-work-well">What does not work well</h4>

<ul>
  <li>Whisper on Raspberry Pi</li>
  <li>abstracted audio layers (<code class="language-plaintext highlighter-rouge">sounddevice</code>, PulseAudio)</li>
  <li>free transcription on a weak CPU</li>
</ul>

<h4 id="what-works">What works</h4>

<ul>
  <li>direct ALSA (<code class="language-plaintext highlighter-rouge">arecord</code>)</li>
  <li>simple and deterministic pipeline</li>
  <li>Vosk with restricted vocabulary</li>
  <li>intent logic rather than full NLP</li>
</ul>

<hr />

<h3 id="result-2">Result</h3>

<p>We move from:</p>

<blockquote>
  <p>a slow and frustrating prototype</p>
</blockquote>

<p>to:</p>

<blockquote>
  <p>a fast, responsive voice assistant usable by children</p>
</blockquote>

<hr />

<h3 id="what-comes-next">What Comes Next?</h3>

<p>Once this solid base is ready:</p>

<ul>
  <li>add end-of-speech detection (VAD)</li>
  <li>improve responses (TTS or sounds)</li>
  <li>add simple memory</li>
  <li>possibly connect an LLM (later)</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>The key issue in this project was not an AI problem.</p>

<p>It was an <strong>architecture choice</strong> problem.</p>

<p>On limited hardware:</p>

<ul>
  <li>you must <strong>simplify the problem</strong></li>
  <li>not just optimize the solution</li>
</ul>

<hr />]]></content><author><name></name></author><category term="robie" /><category term="audio" /><summary type="html"><![CDATA[]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fabiencappelli.com/assets/images/blog/robie_1.jpg" /><media:content medium="image" url="https://fabiencappelli.com/assets/images/blog/robie_1.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Continuation de la v1</title><link href="https://fabiencappelli.com/robie/audio/2026/04/03/robie-second-post-fr.html" rel="alternate" type="text/html" title="Continuation de la v1" /><published>2026-04-03T00:00:00+00:00</published><updated>2026-04-03T00:00:00+00:00</updated><id>https://fabiencappelli.com/robie/audio/2026/04/03/robie-second-post-fr</id><content type="html" xml:base="https://fabiencappelli.com/robie/audio/2026/04/03/robie-second-post-fr.html"><![CDATA[<p><img src="/assets/images/blog/robie_1.jpg" alt="image" /></p>

<hr />

<h2 id="reconstitution-dun-environnement-virtuel-sain">Reconstitution d’un environnement virtuel sain</h2>

<p>Et… patatra…
Tout se brise, et notamment la gestion du bonnet voice Adafruit.</p>

<p>Je dois tout recommencer pour faire en sorte que le son entre et sorte.</p>

<hr />

<h3 id="étape-1--le-vrai-mur--laudio-bas-niveau">Étape 1 — Le vrai mur : l’audio bas niveau</h3>

<p>Avant même de parler d’IA, le premier défi a été… le micro.</p>

<h4 id="problèmes-rencontrés-">Problèmes rencontrés :</h4>

<ul>
  <li>erreurs <code class="language-plaintext highlighter-rouge">RPi.GPIO</code> → conflit entre environnement Python et libs système</li>
  <li><code class="language-plaintext highlighter-rouge">sounddevice</code> incapable d’ouvrir le flux audio</li>
  <li>PulseAudio / PipeWire qui monopolisent le device</li>
  <li>ALSA qui voit la carte… mais refuse tous les formats</li>
</ul>

<h4 id="symptômes-typiques-">Symptômes typiques :</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">PortAudioError: Invalid number of channels</code></li>
  <li><code class="language-plaintext highlighter-rouge">device or resource busy</code></li>
  <li><code class="language-plaintext highlighter-rouge">Unable to install hw params</code></li>
</ul>

<h4 id="leçons-importantes-">Leçons importantes :</h4>

<ul>
  <li>Sur Raspberry Pi, <strong>éviter les couches audio haut niveau</strong></li>
  <li>Aller directement vers <strong>ALSA (<code class="language-plaintext highlighter-rouge">arecord</code>)</strong></li>
  <li>Désactiver PipeWire/PulseAudio si nécessaire</li>
  <li>Vérifier la config du codec via <code class="language-plaintext highlighter-rouge">alsamixer</code></li>
</ul>

<p>Une fois cette étape passée, tout devient beaucoup plus simple.</p>

<hr />

<h3 id="étape-2--pipeline-audio-fonctionnel">Étape 2 — Pipeline audio fonctionnel</h3>

<p>Après stabilisation, on obtient enfin :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>micro → ALSA → enregistrement → traitement → playback
</code></pre></div></div>

<p>Et côté UX :</p>

<ul>
  <li>LED éteinte → veille</li>
  <li>LED rouge → écoute</li>
  <li>LED jaune → traitement</li>
  <li>son → réponse</li>
</ul>

<p>À ce stade, le robot “vit” déjà.</p>

<hr />

<h3 id="étape-3--tentative-avec-whisper-et-échec">Étape 3 — Tentative avec Whisper (et échec)</h3>

<p>L’étape suivante logique était la transcription avec <code class="language-plaintext highlighter-rouge">faster-whisper</code>.</p>

<h4 id="résultat-">Résultat :</h4>

<ul>
  <li>latence énorme (plusieurs secondes voire dizaines de secondes)</li>
  <li>mauvaise qualité avec modèle <code class="language-plaintext highlighter-rouge">tiny</code></li>
  <li>impossible de monter en qualité sans exploser le temps de calcul</li>
</ul>

<h4 id="pourquoi-ça-échoue-">Pourquoi ça échoue :</h4>

<ul>
  <li>Raspberry Pi 4 trop limité pour du STT moderne</li>
  <li>Whisper optimisé pour GPU ou CPU puissants</li>
  <li>compromis qualité / vitesse impossible à tenir</li>
</ul>

<p>Conclusion :
<strong>Whisper est excellent… mais pas pour ce cas d’usage sur Pi.</strong></p>

<hr />

<h3 id="étape-4--pivot-vers-vosk">Étape 4 — Pivot vers Vosk</h3>

<p>Changement de stratégie : tester Vosk.</p>

<h4 id="résultat-immédiat-">Résultat immédiat :</h4>

<ul>
  <li>latence bien meilleure</li>
  <li>transcription presque correcte</li>
  <li>pipeline stable</li>
</ul>

<p>Grosse amélioration.</p>

<p>Mais…</p>

<h4 id="nouveau-problème-">Nouveau problème :</h4>

<ul>
  <li>~10 secondes pour traiter 4 secondes d’audio</li>
  <li>encore trop lent pour une interaction naturelle</li>
</ul>

<hr />

<h4 id="compréhension-clé--mauvais-problème">Compréhension clé : mauvais problème</h4>

<p>Le problème n’était pas le moteur.</p>

<p>Le problème était la tâche.</p>

<p>On demandait :</p>

<blockquote>
  <p>“Transcris librement tout ce que je dis”</p>
</blockquote>

<p>Alors que le vrai besoin était :</p>

<blockquote>
  <p>“Reconnais quelques commandes simples”</p>
</blockquote>

<hr />

<h3 id="étape-5--changement-de-paradigme">Étape 5 — Changement de paradigme</h3>

<p>Au lieu de faire de la dictée vocale, on passe à <strong>reconnaissance de commandes vocales</strong></p>

<h4 id="exemple-">Exemple :</h4>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="sh">"</span><span class="s">bonjour</span><span class="sh">"</span> <span class="ow">in</span> <span class="n">text</span><span class="p">:</span>
    <span class="nf">play</span><span class="p">(</span><span class="sh">"</span><span class="s">bonjour.mp3</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Ou mieux encore : limiter le vocabulaire directement dans Vosk :</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rec</span> <span class="o">=</span> <span class="nc">KaldiRecognizer</span><span class="p">(</span>
    <span class="n">model</span><span class="p">,</span>
    <span class="mi">16000</span><span class="p">,</span>
    <span class="sh">'</span><span class="s">[</span><span class="sh">"</span><span class="s">bonjour</span><span class="sh">"</span><span class="s">, </span><span class="sh">"</span><span class="s">histoire</span><span class="sh">"</span><span class="s">, </span><span class="sh">"</span><span class="s">musique</span><span class="sh">"</span><span class="s">, </span><span class="sh">"</span><span class="s">stop</span><span class="sh">"</span><span class="s">]</span><span class="sh">'</span>
<span class="p">)</span>
</code></pre></div></div>

<h4 id="résultat--1">Résultat :</h4>

<ul>
  <li>plus rapide</li>
  <li>plus fiable</li>
  <li>beaucoup plus robuste</li>
</ul>

<hr />

<h3 id="architecture-finale-v1">Architecture finale (V1)</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Wake word
    ↓
LED rouge (écoute)
    ↓
Enregistrement court (2–3s)
    ↓
Vosk (vocabulaire limité)
    ↓
Intent simple
    ↓
Réponse audio
    ↓
Retour veille
</code></pre></div></div>

<hr />

<h3 id="ce-qui-a-vraiment-fait-la-différence">Ce qui a vraiment fait la différence</h3>

<h4 id="ce-qui-ne-marche-pas-bien">Ce qui ne marche pas bien</h4>

<ul>
  <li>Whisper sur Raspberry Pi</li>
  <li>audio abstrait (sounddevice, PulseAudio)</li>
  <li>transcription libre sur CPU faible</li>
</ul>

<h4 id="ce-qui-marche">Ce qui marche</h4>

<ul>
  <li>ALSA direct (<code class="language-plaintext highlighter-rouge">arecord</code>)</li>
  <li>pipeline simple et déterministe</li>
  <li>Vosk avec vocabulaire restreint</li>
  <li>logique d’intentions plutôt que NLP complet</li>
</ul>

<hr />

<h3 id="résultat">Résultat</h3>

<p>On passe de :</p>

<blockquote>
  <p>un prototype lent et frustrant</p>
</blockquote>

<p>à :</p>

<blockquote>
  <p>un assistant vocal rapide, réactif et utilisable par des enfants</p>
</blockquote>

<hr />

<h3 id="et-après-">Et après ?</h3>

<p>Une fois cette base solide :</p>

<ul>
  <li>ajouter détection de fin de parole (VAD)</li>
  <li>améliorer les réponses (TTS ou sons)</li>
  <li>ajouter mémoire simple</li>
  <li>éventuellement brancher un LLM (mais plus tard)</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Le point clé de ce projet n’a pas été un problème d’IA.</p>

<p>C’était un problème de <strong>choix d’architecture</strong>.</p>

<p>Sur du matériel limité :</p>

<ul>
  <li>il faut <strong>simplifier le problème</strong></li>
  <li>pas juste optimiser la solution</li>
</ul>

<hr />]]></content><author><name></name></author><category term="robie" /><category term="audio" /><summary type="html"><![CDATA[]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fabiencappelli.com/assets/images/blog/robie_1.jpg" /><media:content medium="image" url="https://fabiencappelli.com/assets/images/blog/robie_1.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Project Kickoff</title><link href="https://fabiencappelli.com/robie/audio/2026/03/28/robie-first-post-en.html" rel="alternate" type="text/html" title="Project Kickoff" /><published>2026-03-28T00:00:00+00:00</published><updated>2026-03-28T00:00:00+00:00</updated><id>https://fabiencappelli.com/robie/audio/2026/03/28/robie-first-post-en</id><content type="html" xml:base="https://fabiencappelli.com/robie/audio/2026/03/28/robie-first-post-en.html"><![CDATA[<p><img src="/assets/images/blog/robie_1.jpg" alt="image" /></p>

<h2 id="goal">Goal</h2>

<p>Build a system able to:</p>

<ul>
  <li>listen continuously</li>
  <li>detect a wake word</li>
  <li>record a voice command</li>
  <li>understand the intent</li>
  <li>trigger an action</li>
  <li>respond with sound or voice</li>
</ul>

<p>All of it <strong>locally</strong>, with no cloud dependency.</p>

<hr />

<h2 id="overall-architecture">Overall Architecture</h2>

<p>The system is split into several blocks:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Microphone
→ Wake word
→ Recording
→ Speech-to-Text (Whisper)
→ Interpretation (LLM)
→ Action
→ Response (sounds / TTS / LEDs)
</code></pre></div></div>

<p>Each block will be explored and validated separately.</p>

<hr />

<h2 id="1-audio-and-hardware">1. Audio and Hardware</h2>

<p>The project relies on:</p>

<ul>
  <li>Raspberry Pi (Debian Bookworm)</li>
  <li>Adafruit Voice Bonnet (microphones + LEDs + speakers)</li>
  <li>audio output tested with pink noise</li>
</ul>

<p>Important points:</p>

<ul>
  <li>correctly identify the right audio device</li>
  <li>properly handle simultaneous input/output</li>
  <li>implement clean LED management (cleanup)</li>
</ul>

<hr />

<h2 id="2-wake-word-the-first-challenge">2. Wake Word: the First Challenge</h2>

<h3 id="-picovoice-porcupine">❌ Picovoice (Porcupine)</h3>

<p>Initially considered, but dropped:</p>

<ul>
  <li>now requires a pro account</li>
  <li>external dependency</li>
  <li>less suitable for a long-term personal project</li>
</ul>

<h3 id="-openwakeword">✅ openWakeWord</h3>

<p>Chosen solution:</p>

<ul>
  <li>open source</li>
  <li>runs locally</li>
  <li>based on TFLite models</li>
</ul>

<h4 id="issues-encountered">Issues encountered:</h4>

<ul>
  <li>missing models → <code class="language-plaintext highlighter-rouge">download_models()</code></li>
  <li>NumPy / SciPy conflicts → downgrade and version alignment</li>
  <li>false positives → filtering required</li>
</ul>

<h4 id="solutions-implemented">Solutions implemented:</h4>

<ul>
  <li>high threshold (<code class="language-plaintext highlighter-rouge">~0.95</code>)</li>
  <li>several consecutive frames</li>
  <li>refractory period (10s)</li>
  <li>stop audio stream during actions</li>
</ul>

<p>👉 Key takeaway: <strong>a wake word is not reliable “raw” — it needs control logic</strong></p>

<hr />

<h2 id="3-classic-problem-the-robot-triggers-itself">3. Classic Problem: the Robot Triggers Itself</h2>

<p>Robie was detecting its own audio output (feedback loop).</p>

<p>Solution:</p>

<ul>
  <li>pause listening during:
    <ul>
      <li>recording</li>
      <li>sound response</li>
    </ul>
  </li>
  <li>add a grace delay</li>
</ul>

<hr />

<h2 id="4-stt--understanding">4. STT ≠ Understanding</h2>

<p>Testing <code class="language-plaintext highlighter-rouge">faster-whisper</code></p>

<hr />

<h2 id="5-introducing-a-local-llm">5. Introducing a Local LLM</h2>

<p>Test with Ollama + Qwen2.5 (1.5B)</p>

<p>Result:</p>

<ul>
  <li>~2 second latency</li>
  <li>stable behavior</li>
  <li>viable for embedded usage</li>
</ul>

<p>👉 Conclusion: <strong>a small local LLM on Raspberry Pi is usable</strong></p>

<hr />

<h2 id="the-key-metric-latency">The Key Metric: Latency</h2>

<p>What matters is not total speed, but the time before the response starts.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">&lt; 3 seconds</code> : good</li>
  <li><code class="language-plaintext highlighter-rouge">3–6</code> : acceptable</li>
  <li><code class="language-plaintext highlighter-rouge">&gt; 6</code> : frustrating</li>
</ul>

<p>UX trick:</p>

<ul>
  <li>yellow LED = “thinking”</li>
  <li>intermediate sound cue</li>
</ul>

<p>👉 turns lag into natural behavior</p>

<hr />

<h2 id="6-role-of-the-llm-in-robie">6. Role of the LLM in Robie</h2>

<p>The LLM should not be used for open-ended chatting.</p>

<p>It will be used to:</p>

<ul>
  <li>transform a sentence into an intent</li>
  <li>structure the command</li>
</ul>

<p>Example:</p>

<p>Input:</p>

<blockquote>
  <p>“Robie, note that Thomas needs to bring his coat tomorrow”</p>
</blockquote>

<p>Output:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"intent"</span><span class="p">:</span><span class="w"> </span><span class="s2">"take_note"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"content"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Thomas needs to bring his coat tomorrow"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"answer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Noted."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The Python code then executes the action.</p>

<hr />

<h2 id="7-french-language-support">7. French Language Support</h2>

<p>Important constraint: French-speaking children.</p>

<p>Solutions:</p>

<ul>
  <li>multilingual Whisper (not <code class="language-plaintext highlighter-rouge">.en</code>)</li>
  <li>language forced to <code class="language-plaintext highlighter-rouge">fr</code></li>
  <li>multilingual LLM (Qwen works well)</li>
  <li>prompts in French</li>
  <li>intents in French</li>
</ul>

<p>⚠️ Note: children’s voices are harder to recognize → tolerance will be needed.</p>

<hr />

<h2 id="8-what-about-my-coral-tpu">8. What About My Coral TPU?</h2>

<p>Not usable for LLMs.</p>

<p>Why:</p>

<ul>
  <li>Coral = quantized TFLite models</li>
  <li>LLM = incompatible architecture</li>
</ul>

<p>Relevant future uses:</p>

<ul>
  <li>vision (camera)</li>
  <li>object detection</li>
  <li>environmental perception</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>This first session shows that building a local embedded assistant is probably achievable. Next step: finish testing each block, then start designing the overall architecture for version 2.</p>

<h3 id="v1">V1</h3>

<p>Component testing and performance stability</p>

<h3 id="v2">V2</h3>

<p>Pipeline construction and first real-world tests</p>]]></content><author><name></name></author><category term="robie" /><category term="audio" /><summary type="html"><![CDATA[]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fabiencappelli.com/assets/images/blog/robie_1.jpg" /><media:content medium="image" url="https://fabiencappelli.com/assets/images/blog/robie_1.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Démarrage du projet</title><link href="https://fabiencappelli.com/robie/audio/2026/03/28/robie-first-post-fr.html" rel="alternate" type="text/html" title="Démarrage du projet" /><published>2026-03-28T00:00:00+00:00</published><updated>2026-03-28T00:00:00+00:00</updated><id>https://fabiencappelli.com/robie/audio/2026/03/28/robie-first-post-fr</id><content type="html" xml:base="https://fabiencappelli.com/robie/audio/2026/03/28/robie-first-post-fr.html"><![CDATA[<p><img src="/assets/images/blog/robie_1.jpg" alt="image" /></p>

<h2 id="objectif">Objectif</h2>

<p>Construire un système capable de :</p>

<ul>
  <li>écouter en permanence</li>
  <li>détecter un mot d’éveil (wake word)</li>
  <li>enregistrer une commande vocale</li>
  <li>comprendre l’intention</li>
  <li>déclencher une action</li>
  <li>répondre avec du son ou de la voix</li>
</ul>

<p>Le tout <strong>localement</strong>, sans dépendance cloud.</p>

<hr />

<h2 id="architecture-globale">Architecture globale</h2>

<p>Le système se décompose en plusieurs briques :</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Micro
→ Wake word
→ Enregistrement
→ Speech-to-Text (Whisper)
→ Interprétation (LLM)
→ Action
→ Réponse (sons / TTS / LEDs)
</code></pre></div></div>

<p>Chaque brique va être explorée et validée séparément.</p>

<hr />

<h2 id="1-audio-et-matériel">1. Audio et matériel</h2>

<p>Le projet s’appuie sur :</p>

<ul>
  <li>Raspberry Pi (Debian Bookworm)</li>
  <li>Adafruit Voice Bonnet (micros + LEDs + haut-parleurs)</li>
  <li>sortie audio testée via bruit rose</li>
</ul>

<p>Points importants :</p>

<ul>
  <li>bien identifier le bon device audio</li>
  <li>gérer correctement l’entrée/sortie simultanée</li>
  <li>prévoir une gestion propre des LEDs (cleanup)</li>
</ul>

<hr />

<h2 id="2-wake-word--le-premier-défi">2. Wake word : le premier défi</h2>

<h3 id="-picovoice-porcupine">❌ Picovoice (Porcupine)</h3>

<p>Initialement envisagé, mais abandonné :</p>

<ul>
  <li>demande désormais un compte pro</li>
  <li>dépendance externe</li>
  <li>moins adapté à un projet perso long terme</li>
</ul>

<h3 id="-openwakeword">✅ openWakeWord</h3>

<p>Choix retenu :</p>

<ul>
  <li>open source</li>
  <li>fonctionne en local</li>
  <li>basé sur des modèles TFLite</li>
</ul>

<h4 id="difficultés-rencontrées-">Difficultés rencontrées :</h4>

<ul>
  <li>modèles manquants → <code class="language-plaintext highlighter-rouge">download_models()</code></li>
  <li>conflits NumPy / SciPy → downgrade et alignement versions</li>
  <li>faux positifs → nécessité de filtrage</li>
</ul>

<h4 id="solutions-mises-en-place-">Solutions mises en place :</h4>

<ul>
  <li>seuil élevé (<code class="language-plaintext highlighter-rouge">~0.95</code>)</li>
  <li>plusieurs frames consécutives</li>
  <li>période réfractaire (10s)</li>
  <li>arrêt du stream pendant l’action</li>
</ul>

<p>👉 Le point clé : <strong>un wake word n’est pas fiable “brut” — il faut le cadrer</strong></p>

<hr />

<h2 id="3-problème-classique--le-robot-sauto-déclenche">3. Problème classique : le robot s’auto-déclenche</h2>

<p>Robie détectait son propre son (feedback audio).</p>

<p>Solution :</p>

<ul>
  <li>couper l’écoute pendant :
    <ul>
      <li>enregistrement</li>
      <li>réponse sonore</li>
    </ul>
  </li>
  <li>ajouter un délai de grâce</li>
</ul>

<hr />

<h2 id="4-stt--compréhension">4. STT ≠ compréhension</h2>

<p>On essaye <code class="language-plaintext highlighter-rouge">faster-whisper</code></p>

<hr />

<h2 id="5-introduction-dun-llm-local">5. Introduction d’un LLM local</h2>

<p>Test avec Ollama + Qwen2.5 (1.5B)</p>

<p>Résultat :</p>

<ul>
  <li>latence ~2 secondes</li>
  <li>fonctionnement stable</li>
  <li>viable pour un usage embarqué</li>
</ul>

<p>👉 Conclusion : <strong>un petit LLM local sur Pi est exploitable</strong></p>

<hr />

<h2 id="la-métrique-clé--la-latence">La métrique clé : la latence</h2>

<p>Ce qui compte n’est pas la vitesse globale, mais le temps avant que la réponse commence.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">&lt; 3 secondes</code> : bon</li>
  <li><code class="language-plaintext highlighter-rouge">3–6</code> : acceptable</li>
  <li><code class="language-plaintext highlighter-rouge">&gt; 6</code> : frustrant</li>
</ul>

<p>Astuce UX :</p>

<ul>
  <li>LED jaune = “réflexion”</li>
  <li>son intermédiaire</li>
</ul>

<p>👉 transforme un lag en comportement naturel</p>

<hr />

<h2 id="6-rôle-du-llm-dans-robie">6. Rôle du LLM dans Robie</h2>

<p>Le LLM ne peut pas être utilisé pour “discuter”.</p>

<p>Il servira à :</p>

<ul>
  <li>transformer une phrase en intention</li>
  <li>structurer la commande</li>
</ul>

<p>Exemple :</p>

<p>Entrée :</p>

<blockquote>
  <p>“Robie, note que Thomas doit prendre son manteau demain”</p>
</blockquote>

<p>Sortie :</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"intent"</span><span class="p">:</span><span class="w"> </span><span class="s2">"take_note"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"content"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Thomas doit prendre son manteau demain"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"answer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"C’est noté."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Le code Python exécute ensuite l’action.</p>

<hr />

<h2 id="7-support-du-français">7. Support du français</h2>

<p>Contrainte importante : enfants francophones.</p>

<p>Solutions :</p>

<ul>
  <li>Whisper multilingue (pas <code class="language-plaintext highlighter-rouge">.en</code>)</li>
  <li>langue forcée en <code class="language-plaintext highlighter-rouge">fr</code></li>
  <li>LLM multilingue (Qwen OK)</li>
  <li>prompts en français</li>
  <li>intents en français</li>
</ul>

<p>⚠️ Attention : la reconnaissance des voix d’enfants est plus difficile → prévoir tolérance.</p>

<hr />

<h2 id="8-et-ma-coral-tpu-">8. Et ma Coral TPU ?</h2>

<p>Non utilisable pour les LLM.</p>

<p>Pourquoi :</p>

<ul>
  <li>Coral = modèles TFLite quantifiés</li>
  <li>LLM = architecture incompatible</li>
</ul>

<p>Usage futur pertinent :</p>

<ul>
  <li>vision (caméra)</li>
  <li>détection d’objets</li>
  <li>perception environnement</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Cette première session montre qu’il est probablement possible de construire un assistant embarqué local. Pour la suite, je dois finir de tester chaque brique, et commencer à envisager l’architecture globale de ma v2.</p>

<h3 id="v1">V1</h3>

<p>Test des briques et stabilité des performances</p>

<h3 id="v2">V2</h3>

<p>Construction du pipeline et premiers tests en conditions réelles</p>]]></content><author><name></name></author><category term="robie" /><category term="audio" /><summary type="html"><![CDATA[]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fabiencappelli.com/assets/images/blog/robie_1.jpg" /><media:content medium="image" url="https://fabiencappelli.com/assets/images/blog/robie_1.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>