# Pixies

In opdracht · 2025–2026 · Scape Agency · tags: ai, audio, hardware

URL: https://jim.vanhekke.nl/pixies/  
Bijgewerkt: 2026-10-07

Personages die luisteren, nadenken en terugpraten, volledig op hardware die in de installatie zelf staat.

Een Pixi is een digitaal mens op een scherm in de publieke ruimte: de AI Tuinman in een duurzaamheidscentrum, Futura op Dutch Design Week, Zoë de Duif als gastvrouw van het AI in de Stad-congres. Loop erheen, druk op de knop en stel een vraag. Niets verlaat de kast: geen cloud, geen gesprek bij een derde partij.

Ik heb de server gebouwd, de spraak sneller gemaakt en de installatiecode geschreven. De eerste versie was een Django-applicatie die microfoon, spraakdetectie, Whisper, een klein taalmodel via Ollama, Chatterbox en een kennisbank aan elkaar knoopte. Omdat het doel is om dit op kleine lokale hardware te draaien, is in de tweede versie alles naar Rust geport. Het meertalige stemmodel van Chatterbox heb ik gedistilleerd naar een kleiner Nederlands model, van 30 naar 16 lagen, getraind op een eigen GPU. Voor de lipsync schreven we als team een eigen module: die vertaalt vooraf alle fonemen naar visemen, zodat de live applicatie alleen op het juiste moment het juiste viseem hoeft in te laden. Alle Pixies worden aangestuurd door een klein 3B-taalmodel van Mistral. Hun specifieke kennis halen ze uit een hybride vector- en graafdatabase.

## Colofon

- **Getoond:** Dutch Design Week 2025 (Futura); AI in de Stad-congres, Breda, 2026 (Zoë de Duif); duurzaamheidscentrum (AI Tuinman)
- **Studio:** Pixi Systems, Lovelace Labs
- **Mijn aandeel:** projectmanagement, klantcontact, server, spraakketen, Nederlands stemmodel, lipsynchronisatie, installatie

## Code

Het begin van de spraakketen, ingekort: hoe "25" een gesproken getal wordt voordat het stemmodel het ziet. De tekst wordt eerst door een telwoordenspeller gehaald, die per taal werkt en één keer per proces wordt ingesteld; daarna volgen de uitspraaklijst, tijden en afkortingen.

```rust
pub type NumberSpeller = fn(&str, &str) -> String;

static NUMBER_SPELLER: OnceLock<NumberSpeller> = OnceLock::new();

/// Eén keer, vroeg, vóór het eerste TTS-verzoek. De eerste wint: twee keer
/// installeren is een bedradingsfout die je wilt horen, geen stille overschrijving.
pub fn set_number_speller(speller: NumberSpeller) -> Result<(), NumberSpeller> {
    NUMBER_SPELLER.set(speller)
}

impl TextNormalizer for LexilaceNormalizer {
    fn normalize(&self, text: &str) -> String {
        let spelled = match NUMBER_SPELLER.get() {
            // "25" wordt "vijfentwintig", in de taal van deze Pixi
            Some(spell) => Cow::Owned(spell(text, &self.language)),
            // zonder speller gaan cijfers ongelezen door naar het stemmodel
            None => Cow::Borrowed(text),
        };
        self.inner.normalize(&spelled)
    }
}
```

## Beelden

- ![Futura in de studio, juli 2025 — foto: Scape Agency](https://jim.vanhekke.nl/pixies/images/pixies-03-futura-studio.webp)
- ![Het brein: de kennisgraaf van een Pixi in Coco Biblio, hier die van Digibied Breda](https://jim.vanhekke.nl/pixies/images/pixies-08-kennisgraaf.webp)
- ![Jim en Zoë de Duif op het AI in de Stad-congres, Breda, september 2026 — foto: Eshuis Fotografie](https://jim.vanhekke.nl/pixies/images/pixies-09-zoe-congres-featured_image.webp)
