Skip to content

Architecture Reference

System overview

Mirach runs as a background daemon on your Linux desktop and exposes an HTTP/SSE API used by the web widget and the Android app. A single hotkey triggers the voice pipeline; the same pipeline can be driven remotely from the mobile app.

[Hotkey] ──► trigger.py ──socket──► Assistant (FSM)
                          ┌──────────────┼──────────────┐
                          ▼              ▼               ▼
                   AudioRecorder   WhisperTranscriber  PiperSpeaker
                    (sounddevice)   (faster-whisper)   (piper-tts)
                                    LLMBackend (protocol)
                                    ┌────┴────┐
                              opencode_serve  native
                              (opencode CLI) (Ollama/vLLM…)
                                    ConversationBus ──► HTTP/SSE server
                                                    ┌────────┴────────┐
                                                  Widget        Android app
                                                (browser)    (flutter/mobile)

State machine — assistant.py

Assistant owns a three-state FSM. toggle() is the only public entry point, called by the Unix socket server on every "toggle" message.

IDLE ──[toggle]──► RECORDING ──[toggle]──► PROCESSING ──[done]──► IDLE
                                           [toggle]  interrupts pipeline
                                             IDLE (re-enter)
State Description
IDLE Waiting for hotkey press. No audio capture.
RECORDING Microphone is open; audio frames collected in a buffer.
PROCESSING Background thread running: AudioRecorder.stop()WhisperTranscriber.transcribe()LLMBackend.query()PiperSpeaker.speak().

A second toggle() during PROCESSING interrupts: LLM.interrupt() + TTS.interrupt() are called concurrently; the pipeline thread sets the FSM back to IDLE.

LLM backends — mirach/harness/

Two backends implement the LLMBackend protocol (llm_types.py). Select via MIRACH_BACKEND.

opencode_serve (default)mirach/harness/providers/opencode.py

Spawns and supervises opencode serve. Creates or reuses a session, translates the SSE event stream (message.part.delta, permission.updated, session.idle) into ConversationBus events, and enforces PolicyEngine on every permission.updated. Session resets after MIRACH_SESSION_IDLE_TIMEOUT idle seconds.

nativemirach/harness/native_backend.py

Runs a full inner tool-use REPL against any OpenAI-compatible endpoint (Ollama, llama.cpp, vLLM…). Tool invocation protocol: auto | native | prompted. Policy enforced before every tool execution. History in-memory; session resets after the same idle timeout.

Policy engine — mirach/harness/policy/

PolicyEngine evaluates every tool call before execution against policy.yaml. Rules are allow or deny with glob patterns on tool name and arguments. Matched deny rules block execution and surface a permission.updated event with status: denied. Unmatched calls that require confirmation trigger status: awaiting_confirmation.

ConversationBus — mirach/harness/events.py

An in-process publish/subscribe channel. The active backend publishes typed events (queued, user_turn, text_delta, tool_call, tool_result, awaiting_confirmation, done, error, cost). The HTTP server fans them out to SSE subscribers; the UI thread drives TTS from the same bus.

HTTP/SSE server — mirach/harness/server.py

A Python stdlib ThreadingHTTPServer that exposes the REST + SSE API. Enabled by default; disable with MIRACH_SERVER_ENABLED=0.

See HTTP/SSE API Reference for the full endpoint contract.

STT — stt.py

WhisperTranscriber captures at MIRACH_SAMPLE_RATE (default 48 kHz), downsamples to Whisper's required 16 kHz via polyphase resampling (scipy) or a boxcar fallback, and runs inference on GPU or CPU. A warmup pass on a silent buffer runs at startup to avoid cold-start latency.

TTS — tts.py

PiperSpeaker synthesizes chunks that stream directly into a sounddevice.OutputStream — playback begins before synthesis finishes. A _stream_lock serializes concurrent speak() calls (main response + filler loop). Short filler phrases are pre-baked to WAV files at startup (prebake_fillers) and played via sd.play().

i18n — i18n.py

Desktop locale chosen at import time via MIRACH_LOCALE. Add a new locale by extending the STRINGS and FILLERS dicts. Strings not present in the current locale fall back to English.

The Android app uses a separate ARB-based system (mobile/lib/l10n/) managed by flutter gen-l10n. The app locale is persisted under the mirach_lang key in flutter_secure_storage.

ConversationLog — conversation.py

Each session writes one Markdown file under logs/conversations/ and updates a latest.md symlink. A new session starts when the idle timeout elapses.

Source layout

mirach/
  __main__.py          — entry point: `python -m mirach`
  assistant.py         — orchestrator, FSM, shutdown hooks
  audio.py             — thread-safe microphone capture (sounddevice)
  stt.py               — WhisperTranscriber with warmup + downsampling
  tts.py               — PiperSpeaker with streaming + pre-baked fillers
  llm_types.py         — LLMBackend protocol + _strip_markdown()
  ipc.py               — Unix socket server (toggle / ping)
  conversation.py      — Markdown logs + latest.md symlink
  conversation_html.py — styled HTML viewer (dark theme, chat layout)
  obsidian_cache.py    — in-memory Obsidian vault reader (session context)
  config.py            — all MIRACH_* env vars with defaults
  i18n.py              — desktop locale strings + filler phrases
  langpack.py          — locale helper used by i18n
  notify.py            — desktop notifications + beep WAV generation
  logging_setup.py     — rotating file logger + stdout for journalctl
  cli.py               — `mirach` CLI entry point
  harness/
    events.py          — ConversationBus + typed event schema
    server.py          — HTTP/SSE server (REST API + widget)
    _widget.py         — embedded web widget HTML
    loop.py            — native backend tool-use REPL (AgentLoop)
    native_backend.py  — NativeBackend: OpenAI-compatible endpoint
    context.py         — ContextManager (compaction strategy)
    build.py           — conversation history builder
    tool_protocol.py   — tool call format detection / normalization
    providers/
      base.py          — LLMBackend abstract base
      opencode.py      — OpenCodeServeBackend (opencode CLI)
      openai_compat.py — raw OpenAI-compatible provider
    policy/
      engine.py        — PolicyEngine: allow/deny/confirm rules
      schema.py        — policy.yaml schema
    tools/
      registry.py      — tool registration
      shell.py         — bash tool
      files.py         — file read/write tools
      web.py           — web search tool
      memory.py        — Obsidian memory tool

trigger.py             — hotkey client (sends "toggle" to socket)
run_daemon.sh          — CUDA 12 library path setup + daemon launcher
pyproject.toml         — package metadata and dependencies
install.py             — interactive setup wizard
bootstrap.sh           — one-liner installer (system deps → clone → wizard)
policy.yaml            — tool permission rules (gitignored; personal)
system_prompt.md       — LLM system prompt (gitignored; personal)
mirach.env             — local config overrides (gitignored; personal)

mobile/                — Android companion app (Flutter)
voices/                — Piper voice models (gitignored; downloaded by installer)
logs/                  — daemon logs + conversation Markdown files

Beep frequencies

Beep Frequency Duration Purpose
Start recording 1320 Hz 60 ms Signals mic is open
Begin processing 660 Hz 80 ms Signals transcription started
Shutdown 660 Hz → 330 Hz 120 ms + 40 ms gap + 120 ms Daemon stopping