macOS · iPhone · iPad · GPL-3.0

companion-forge

A companion you talk with out loud, with a protogen visor for a face. It listens, waits until you have finished rather than until a timer runs out, answers in a voice, and learns how you pause. It is one person's companion: it knows their voice, keeps a diary of their days, and catches up on what they told it. Everything runs locally, and what it remembers is encrypted on the device.

M1 Mac · iPhone 17 · iPad M4 0.9–1.8 s to the first sound, Gemma 4 E4B on the M1 Sealed on this device. The key is never synced
mic → echo canceller → VAD → turn taker → ASR → language model → voice → speakers

VAD              Silero VAD v5, ONNX
ASR              Parakeet Ultra through FluidAudio, on the Neural Engine
language model   Gemma 4 E4B, 4-bit MLX
                 Qwen3 4B or Gemma 4 E2B where the phone has 8 GB
voice            the polyglot snepssen, on voice-core's Piper
                 English included, the same voice
face             protoke's visor, ported to Swift, drawn as an LED panel
memory           bge-small-en-v1.5 on MLX, over notes the model writes
whose voice      CAM++ voiceprints through FluidAudio
dictionary       English Wiktionary, cut down to SQLite

One chain, on the machine in front of you. The voice the build bundles is voice-polyglot's snepssen, English included, on voice-core's Piper.

Turns, not timers

It waits for a silence longer than yours

interpreter-forge cuts speech on pauses, punctuation and a fixed 5 second ceiling. A conversation can do better. Every time you go quiet and then carry on, that gap was a pause inside your turn, and the companion keeps the last 200 of them.

It waits for a silence longer than nearly all of those: the 90th percentile, shortened after a question, lengthened when the words so far trail off on “and” or “um”, and pushed out further each time a reply cut you off. It starts at 0.9 seconds and moves from there. What it has learned is shown in Settings.

The reply is drafted while you are still pausing, as soon as the pause is long enough to be worth a guess: transcribed, written and turned into speech, but held. If you carry on, it is thrown away. If the pause runs to the learned wait, what is ready plays at once. Most of the model's time is spent inside the silence you were leaving anyway.

The face

Protoke's visor, drawn as an LED panel

Protoke's face is continuous dials: eyes and a mouth that are one shape each, at different settings, so any expression is part way to any other and a viseme is just a mouth. The dials, the outline geometry and the LED grid are ported number for number.

The face eases between idle, listening, thinking, speaking and surprised, and blinks and breathes on its own. A sentence Apple's on-device sentiment score finds glad is said with Protoke's happy eyes. The sad side is not used: the score treats a plain question as sadly as sympathy.

The mouth follows what the voice says, phoneme by phoneme. The voice graph reports how long it spent on each one, and espeak's IPA maps to Protoke's mouth shapes, so “my mom” is mouthed m-ah-ee, m-ah-m. The polyglot voice, whose layout differs, falls back to timing words by their spelling.

Nothing to press

It listens as soon as its models are loaded, for as long as the app is open. On a phone or tablet, that is while it is on screen. Quitting is how you stop it, and closing the Mac window quits. The face fills the window, with the last thing each of you said underneath, dimming once the conversation goes quiet. While it listens, the panel's unlit cells brighten with your voice. While it loads, it is half asleep. Double-click or double-tap the face to leave only the face, for a tablet on a stand.

Settings has the visor's colour and style, an LED panel or Protoke's smooth outlines, the model, echo cancellation, what it remembers and what it has learned about your pauses. The speech bubble shows the whole conversation.

When it's serious

It stays, and it says so plainly

If what they say sounds like they might be thinking about suicide or hurting themselves, the reply follows what crisis lines train their volunteers to do rather than what makes a product feel covered: stay, listen, take it seriously, ask directly, and don't judge, argue, minimise, joke or tell them what to do. Asking does not put the idea there. Clear words get all of that. Words that might only mean a bad day (“I can't cope anymore”) get a gentle “are you okay? Like, really?”.

In the first reply it mentions, once, the national line where they are. Twenty-four countries' lines, checked on 2026-10-21 against Wikipedia's list and findahelpline.com, with the emergency number, as something for them and not a way of ending the conversation. It keeps talking for as long as they want. If two replies go by and the model still has not said it, the companion says it itself. Numbers are read digit by digit. Where it cannot be sure of a line, it points to findahelpline.com. The next day it asks how they are doing, not how it went.

Getting it

Built from this checkout

The package resolves from this checkout, plus voice-core by URL. Nothing else has to sit beside it. The voice the build bundles is voice-polyglot's snepssen, English included: its newest export where that checkout is beside this one, otherwise its latest GitHub release, checked against the release's SHA256SUMS and fetched again when a newer one is out. No separate English voice is fetched. The first build also fetches English Wiktionary's extract, 523 MB, and cuts the dictionary from it. The language model is downloaded from Settings into Application Support. Where interpreter-forge's models/ sits beside this checkout, or WORKSHOP_ROOT names the folder that holds both, the fetch script symlinks each model the store doesn't already have, so the gigabytes stay where they are. One build is enough: the app finds them after that. A model already in the store is left as it is. A machine without interpreter-forge downloads as usual.

scripts/fetch-models.sh # Silero VAD, and links to shared models
./build.sh               # checks, then Companion.app and ccrun
swift run cccheck        # the model-free parts, held to their cases

iOS/build-ios.sh         # build, install on a connected iPhone or iPad
iOS/build-ios.sh --no-install

The iPhone and iPad app is the same engine and views, generated with xcodegen as interpreter-forge's is. It offers the models that fit beside recognition, the embedder and the voice: Qwen3 4B or Gemma 4 E2B on an 8 GB iPhone, with the increased memory entitlement, and Gemma 4 E4B from 12 GB. It listens while it is on screen. Leaving the app stops it and writes the conversation up, and coming back starts it again. The screen stays on while it is in front. MLX needs a real GPU, so there is no Simulator build. It has run on an iPhone 17 and an iPad with M4.

In depth

How it works

The microphone and the voice share one engineOne audio engine plays the voice and hears the room, so the echo canceller can subtract what it says, and why a Mac differs.

On an iPhone or iPad, Apple's voice processing is on, which cancels the voice's echo. It can only subtract what it plays itself, so nothing is played any other way. On a Mac it is not used. On the M1, on 2026-10-20, its microphone side delivered nothing at all, whatever devices it was pointed at, so the microphone is taken as it is. The checks below keep the voice out, and the first words of a turn are also checked against the end of the last reply.

Some echo always gets through, so talking over a reply has to pass two checks. It has to sound like confident speech for 0.35 seconds. Then it is compared two ways: by its words, because the reply's own voice coming back says the words being said, and by its level. The companion knows what it played, learns how loud that comes back in this room, and a person talking over it is louder than twice that. Either check can call it echo. Only something that passes both stops the reply, and then only the part actually said is kept.

Where the voice sits

With the spatial voice on, which it is in Settings by default, the voice is placed half a metre in front of you and a little below, where a phone or tablet held at the chest is. On AirPods that track the head, on iOS 18 and macOS 15 and later, it stays there when you turn your head. On speakers it sounds as before. If that path will not start on a device, it falls back to the plain voice and says so. It has not yet been heard on AirPods.

It looks at whoever is speaking, and lags when you move itIt looks at whoever is talking, through the front camera if you turn that on, and its head turns a little after its eyes.

The face looks straight at them while it listens, mostly at them while it answers, and only glances while it thinks. Its head turns a little after its eyes. The front camera does this if you turn on “Look at whoever is talking” in Settings, which is off by default. Vision finds the faces and their lips about ten times a second. The one whose mouth keeps opening and shutting is talking, and with nobody talking it looks at the nearest face. Each picture is looked at and let go. On a Mac it uses the built-in camera, never a virtual one. This works with the echo canceller on.

On an iPhone or iPad with the echo canceller off, and headphones, the level difference between the front microphones' two sides says left or right. The canceller gives one channel, so out loud this is the camera's job. A Mac's microphone is one channel. ccrun --from-right speaks from one side of a stereo microphone and then the other: the face turned to 0.84 across, then to −0.84. Neither the camera nor the phone's stereo microphones have been tried on a device yet. If the face turns the wrong way, the picture or the microphone is mirrored.

The face hangs behind the glass on a spring. Move the phone and it lags and swings back; turn it and the face tilts after it. On a Mac, which has no motion sensors, dragging the window does the same. Shake it hard for a second and its eyes go wide and it says so (“Woah there! This ain't a rodeo. Slow down, please.”, and four others in turn), at most once every 25 seconds, and never over somebody talking. Walking measures about 0.05 on the shake meter, a shake 2 and up, and it speaks at 1.2. With nothing else going on, its eyes look around: a quick dart, a hold of one to five seconds, often back to the middle. It looks at you while it listens and up to one side while it thinks.

Three layers, then a diary it keeps for itselfConversations kept in full, short notes about the person, and then a diary it keeps for itself.

Conversations are kept in full, one JSON line per turn, and nothing is decided there about what matters. Notes are short sentences about the person, written by the language model once a conversation has been quiet for 45 seconds, or when Talk is stopped. Each note names the conversation and the turns it came from. Small talk, greetings and general questions are left out. A note that says the same thing as an older one replaces it.

Notes about who you are, your name, your work, the people and pets in your life, go into every reply. A 4B model asked to mark these seldom does, so the companion recognises them too. Before each reply, what you just said is compared with the notes by meaning, using bge-small-en-v1.5 on MLX, 134 MB, downloaded on first launch. Apple's built-in sentence embedding ranked too poorly to use. A note must score 0.45 and be within 0.12 of the best match to be brought up. What comes to mind goes in just before your words, so the rest of the conversation stays in the model's cache. When you ask about the past, the original turns behind the best note are read back word for word.

You can also ask out loud. “Remember that my sister's birthday is on the third of March” is noted at once, in your own words. “Forget what I said about my sister's birthday” finds the closest note and deletes it, and it says which it did. Both happen only once your turn is over, so a half-heard sentence changes nothing, and “remember when” is a question, not a command. Writing notes gives way to talking: it stops the moment you speak and picks up where it left off. Settings shows everything it remembers, with a button to forget any of it, or all of it and the logs.

ccrun --memory   Gemma 4 E4B
  two earlier conversations, 16 turns
  7 notes in 9.6 s, three of them lasting
  recalling a note: 7 to 25 ms

  “Do you remember what I told you about my sister?”
  brought up the note and the turns behind it
  “You mentioned your sister Ana is visiting.”

  “What's the weather like?”
  brought up nothing

The sister, and the weather, are that run. “Work was exhausting” stays with the note about night shifts, which is what the 0.45 and 0.12 cut is for.

A diary, a calendar, and catching up

Under the notes are two layers the person never sets up. Once a conversation is written up, the companion writes a few lines about it. Once a week is over, those entries are summed up in a few more. Anything said with a date on it goes on a calendar, each entry pointing at the note and the diary line it came from. Dates are read as people say them: tonight, on Friday, this weekend, the 3rd, 12 March, in two weeks, counted from the day they were said. Birthdays and anniversaries come round every year. Something on today, asked before evening, or tomorrow, is brought up in passing, at most once every five days.

A note about something with a time on it becomes worth asking about once it has happened or had a few days to go somewhere: an evening thing the next morning, a weekend on the Monday, something under way after a few days. From the fourth turn, at a calm moment, it asks how it went. What they say is written up like anything else. Something they told it after the fact is not asked about again. Opening the app after two hours or more, it says hello: a few words that fit the time of day and how long it has been, with anything to catch up on in the same breath. The first time, it says it is glad to meet them and asks their name. Back within two hours, it says nothing.

Sealed on this device

What it remembers, the logs and the notes, the diary and the calendar, is sealed with AES-GCM under a key made on first use and kept in the Keychain on this device only. The key is never synced, and never put in a backup that could be restored elsewhere. On an iPhone or iPad the folder is also unreadable while the device is locked. A Mac relies on the sealing, and on FileVault. Files written before sealing still open, and are sealed the next time they are written. If the Keychain will not give the key, it says so and runs unsealed. It never makes a new key over an old one, which would lock away everything sealed with it.

The first voice it hears for long enough is its personThe first voice it hears for long enough is its person; anyone else is a guest, and is told nothing about them.

It keeps that voice as the average of CAM++ voiceprints, through FluidAudio, of what they have said, steadier the more it hears. Anyone else is a guest, met kindly, told nothing it knows about its person, and kept out of its memory. Each sees only their own side of the conversation. Told everything, it once told a guest about its person's nerves over a visit mentioned minutes before.

ccrun --speakers   thirteen system voices, eight clips each
  against an averaged print at 0.80
  every clip over 1.5 s went to the right voice
  none went to a wrong one
  short turns (“yeah”, “hm”) decide nothing

Out of a conversation, only its person starts one. Left on all night, it had answered a television and the street. In a conversation, a guest joins in if they are about as near as its person. “Know my voice” is on by default. The model is fetched the first time, and without it everyone is its person, as before.

It looks the word up before it answersAsked how to say a word, it looks it up first, in an offline dictionary of fifteen languages.

Asked how to say breakfast in Romanian, what “köszönöm” means, or “and in Polish?”, it looks the word up before the model answers, in an offline dictionary built from English Wiktionary, about 130 MB: translations between English and fifteen languages, and what English words mean. None of the three models knew “micul dejun”. Told nothing is there, it says it does not know rather than guess.

A sentence can mix languages. A quoted word in the language they named, or one Apple's detector is sure of, and a whole sentence in another language, are each spoken in their own language by the polyglot voice and joined. English is that same voice. The eight languages the voice was trained on sound like it, and the rest go through plain espeak-ng, accent and all.

Who it is

Three personalities, chosen in Settings. Playful is the default: jokes, wordplay, plays along. Gentle is warm and calm. Deadpan is dry wit, delivered straight. Playful and deadpan drop the act when someone is having a hard time. Small models follow examples far better than descriptions, so each comes with a few lines in its voice. Told only the rules, it was correct and flat.

On an M1 with 16 GB, recognition and memory loadedFootprint and reply time on an M1 with recognition, the voice and memory all loaded.

Footprint is what iOS holds against an app, and on Apple silicon it includes what MLX keeps on the GPU. The run is ccrun on 2026-10-20, with recognition, the embedder, the voice and memory all loaded. The same day, with Gemma 4 E4B, the reply started 0.9 to 1.8 seconds after you stop talking.

ModelFootprintAt peakFrom the end of your speech to the first sound
Qwen3 4B, for 8 GB iPhones3.0 GB3.0 GB0.89–1.72 s
Gemma 4 E2B3.2 GB4.2 GB0.81–1.05 s
Gemma 4 E4B, Mac and 12 GB iPhones4.8 GB5.8 GB1.07–1.76 s

Most of that time is the learned wait itself. A reply is drafted halfway into the pause, transcribed in 0.14 seconds, its first words written 0.3 seconds later and its first sentence voiced by about 0.8 seconds, so it is ready when the wait ends. The rest is a long first sentence.

With the reply's echo leaking back at 0.05, roughly what voice processing leaves, echo measured 1.0 times its learned level and an interruption 11 to 18 times, so none was stopped falsely and the interruption was caught on its first check. At 0.3, barely cancelled, 17 echo fragments were let go and the interruption was caught on its third check.

The model is shown the last 16 to 23 turns. The window moves 8 turns at a time rather than one, and after each reply the model reads the next prompt in while you talk. Sliding a turn at a time made every reply past the sixteenth re-read about 230 tokens, and its first words came 1.86 seconds after you finished instead of 0.31. With both changes, every turn of a 22-turn conversation started in 0.31 to 0.37 seconds.

What has not been measured yetWhere it has run so far, and what is still to be measured on the phone, the tablet and AirPods.

It has run on an M1 Mac, an iPhone 17 and an iPad with M4. The numbers above are the Mac's. Memory and lag on the phone and the tablet are still to be measured the same way.

The spatial voice has not been heard on AirPods. The camera, and the phone's stereo microphones, have not been tried on a device. Desktop beyond the Mac, and Android, are not this app: the README's next step is llama.cpp and sherpa-onnx, as interpreter-forge's Android port already runs Silero, Parakeet and Piper.

The sad side of the visor is unused on purpose. If the Keychain will not give the key, it says so and runs unsealed rather than inventing a new key. Without the voiceprint model, everyone is treated as its person.

The workshop

Made from the pieces beside it

Speaks every language, English included, with the polyglot snepssen Piper voice through voice-core.

Uses voice-polyglot's snepssen voice, the one the build bundles.

Takes ObjCExceptions, SileroVAD, SampleBuffer, ModelStore and PromptCache from speech-core, at 0.5.0.

Reuses Interpreter's Voice, copied with small changes.

Ports Protoke's protogen visor to Swift, drawn as an LED panel.

The wider workshop

Nine projects, five foundations, one local-first practice.

Move between guided-meditation authoring, measured speech, live interpreting, a voice companion, lyric video, delivery validation, keeping a Downloads folder empty, and the small field tools that support all of them, on one voice, one speech engine and one hearing layer they share.

01 / JOURNEYSmacOS

Gateway Forge

Assemble guided sessions and keep published maps beside what was actually found.

Explore Gateway Forge →
02 / SPEECHmacOS · Linux · untested: Windows

Voice Forge

A transparent Piper workbench for timing, pronunciation, and honest audio export.

Explore Voice Forge →
03 / VIDEOmacOS · tested on Linux, Windows

Protoke

Karaoke and narration video with a vector visor face that mouths every word.

Explore Protoke →
04 / DELIVERYmacOS · tested on Linux, Windows

Media Preflight

Check a finished file against where it is going, and correct it without touching the original.

Explore Media Preflight →
05 / INTAKEmacOS · tested on Linux · untested: Windows

siphon

Fetch from a link or convert what you already have, without re-encoding what it does not have to.

Explore siphon →
06 / FILINGmacOS · Linux · tested on Windows

auto-sort

Empty the Downloads funnel into structure the folder taught it, and undo any of it.

Explore auto-sort →
07 / INTERPRETINGmacOS · iPhone · iPad · untested: Android

Interpreter

Hear the table in English a few seconds behind, cut at the end of each thought, on your own phone.

Explore Interpreter →
08 / COMPANIONmacOS · iPhone · iPad

companion-forge

A local voice companion for Mac, iPhone and iPad, with a protogen LED-visor face.

Explore companion-forge →
09 / FIELD TOOLSPython · shell

tools-core

Small utilities for audio inspection, corpus work, authoring, and repeatable checks.

Open the toolbox →

Get in touch

If something looks wrong, say so

There is no analytics here and no crash reporter, so a failure has no way of reaching me on its own.