What EXCAR is, the design philosophy, and the interfaces that make everything swappable.
The mental model. Read this before the dated reports.
EXCAR is a conversational AI co-pilot that gives a car a voice - you wake it, talk to it, and it talks back, in the spirit of KITT from Knight Rider. The target hardware is a Raspberry Pi 4 (64-bit) wired into the car; today it runs on a laptop for development. A companion iOS app (Expo / React Native) pairs with the device to configure it, watch its status, and connect services like Spotify.
It is built for commercialisation from day one, which drove every architectural decision below.
The problem: a voice assistant in a car has hard, conflicting constraints - it must feel instant (people won't tolerate lag while driving), it must keep working when the car loses signal in a tunnel, it must not babble at passengers, and it must run on cheap, low-power hardware. No single off-the-shelf component solves all of this, and the components we'd want (the LLM, the wake word, the voice) will change as the market changes.
The design answer: *every component sits behind a small interface and is selected from configuration.* Whisper can become a cloud STT; Claude can become a local GGUF model; Piper can become a realtime speech-to-speech model - without touching the core loop. This is what makes the product survivable as the underlying AI stack churns.
src/excar/core/interfaces.py)Five Protocols define the entire contract of the system. Engines depend on these, never on each other:
| Protocol | Method | Responsibility |
|---|---|---|
STTEngine | transcribe(audio) -> Transcript | Speech → text |
LLMEngine | stream_reply(text, history) -> Iterator[str] | Text → streamed reply deltas |
TTSEngine | synthesize(text) -> AudioData | Text → speech |
AudioSource | record_utterance() -> AudioData | None | Mic capture (one utterance) |
AudioSink | play(audio, interrupt) | Speaker output, interruptible |
Why Protocols (structural typing) and not base classes: a new backend only has to *have the right shape* to drop in - no inheritance, no registration ceremony. The LLMEngine returning an Iterator[str] is the single most important design choice in the project: it forces every brain to stream, which is what makes overlapped low-latency playback possible (see Report 01).
src/excar/factory.py)There is exactly one place in the codebase that maps a config string to a concrete class: factory.py. build_llm turns llm.provider: claude into a ClaudeEngine; build_location_provider turns location.provider: gpsd into a GpsdLocationProvider, and so on.
Why this matters: the rest of the code never does if provider == .... Swapping a backend is a one-line config change, and the per-component builders are public so diagnostics can stand up *just one layer* (e.g. only the LLM for text chat) without booting the whole pipeline.
excar-v2/
├── src/excar/ # The Python device software
│ ├── core/ # Provider-agnostic: interfaces, types, pipeline, chunker, addressing, intent_gate
│ ├── audio/ # Mic capture (VAD), speaker playback, ducking, diarization
│ ├── stt/ # faster-whisper adapter
│ ├── llm/ # Claude / local GGUF / hybrid adapters
│ ├── tts/ # Piper adapter
│ ├── memory/ # Session buffer + persistent cross-session memory
│ ├── services/ # Tools: maps, clock, location, gps, spotify
│ ├── connectivity/ # BT-tether detector (online/offline decision)
│ ├── wake/ # OpenWakeWord detector
│ ├── api/ # FastAPI REST server + routes (the mobile bridge)
│ ├── personality/ # EXCAR's persona (system prompt)
│ ├── config.py # Typed, validated YAML config
│ ├── factory.py # Composition root: config → wired Pipeline
│ └── app.py # CLI entrypoint (run / doctor / listen / say / chat)
├── mobile/ # Expo / React Native companion app
├── config/ # config.yaml (laptop dev) + config.pi.yaml (car)
├── scripts/ # RPi install + systemd boot scripts
├── tests/ # Logic tests (no hardware/network needed)
└── docs/ # STATUS.md + sessions/ (this build journal)
The ConversationStore (session memory) was Atilla's; PersistentMemory is the layer added on top so memory survives the car powering off between drives.
mic → VAD endpoint → [diarization] → STT → [intent gate] → LLM (streamed, tools) → sentence chunker → TTS → [ducking] → speaker
↑ barge-in interrupt
Brackets are gates/filters that can short-circuit the turn. Everything in this chain is covered in detail across reports 01–06.