All work

Local-first AI companion

Mana

A desktop companion built to find out how close a local assistant can get to feeling like a person rather than a prompt box. It listens, answers out loud, remembers, watches the screen and has a face, with every model running on the machine itself.

v0.2.0 developer preview, actively developed

of default inference running on-device
100%
Electron apps sharing one Node backend
2

The problem

The goal was presence rather than convenience. Not a faster way to run a command, but something you talk to: it listens, replies out loud, remembers what you told it last week, and has a face on screen next to the chat window rather than instead of it.

The reference point I keep coming back to is Alice from Sword Art Online, and the useful part is not the science fiction. That series separates an AI scripted to respond to stimuli from one that grows out of what it has actually lived through, and the character who matters is the one shaped by a relationship with a single person rather than by more training. Mana is not an attempt at the second thing, and models available today cannot be. It is pointed in that direction, which in practice means one instance whose memory is built from one person's conversations instead of a service answering everybody at once.

A hosted assistant is the same assistant for everybody. Claude and ChatGPT answer millions of people at once, and whatever they come to know about you sits inside a service built for all of them. I wanted one instance belonging to one person, shaped by their conversations and nobody else's. I also did not want to hand my own information to a company to keep, and I did not want a monthly bill for the privilege. Running the models locally settles all three at once: it is mine, nothing said to it leaves the machine, and there is no subscription and no per-token cost.

Running it locally answers that and creates a harder engineering problem, because the transcription, the model, the voice and the vision all now have to fit and stay responsive on a single consumer machine.

How it works

Two Electron applications, a launcher and a desktop client, share one Node backend. Any feature has to land in both, which is a constraint the codebase enforces on itself.

Speech in runs through whisper.cpp, replies through llama.cpp, and speech out through a local synthesiser. A vision model plus OCR handle screen awareness, and local web search runs through a self-hosted SearXNG instance.

The avatar is Live2D, rendered in a transparent always-on-top overlay window.

Code is Apache-2.0. The artwork and avatar assets are not, and the Live2D runtime is fetched at setup under its own licence rather than vendored into the repository.

CLIENTSwindows-launcherElectron · mic capture · avatar overlaydesktop-clientElectron · packaged installersame local backend, port 5005BACKENDnode-bot, local backendrequest routing, tool-calling loop, memory, personainvoked as neededLOCAL RUNTIMEBrainllama.cpp, local LLMEarswhisper.cpp, local STTVoicelocal TTS + lip-syncPresenceLive2D + screen OCRexplicit opt-in onlyREMOTE, OFF BY DEFAULTRemote AIAPI keys are ignored unless the opt-in flag is set. Nothing leaves the machine otherwise.
Every request runs through models on the machine by default. Remote AI is wired in but ignored unless it is explicitly turned on, so nothing leaves the machine unless that switch is flipped.

Where it landed

The project is a working developer preview rather than a finished product. Voice conversation, local memory, screen reading and the avatar all function on a real Windows setup, and the release notes are honest about which parts are still rough.

What it taught me

The bug was in the window, not the animation

The avatar stuttered and snapped during playback, which looks exactly like an animation timing problem, so that is where I spent the first pass. The real cause was Electron throttling background rendering on the non-focusable overlay window the avatar lives in. No amount of animation tuning would have fixed it, because the frames were not being scheduled in the first place.

Two frontends punish every shortcut

Shipping a change into one of the two apps and not the other produces a drift that is invisible until someone uses the other one. Parity has to be part of the definition of done, not a follow-up.

Built with

  • Electron
  • Node.js
  • llama.cpp
  • whisper.cpp
  • Live2D
  • SearXNG