The mental model. Read this before the dated reports.


What EXCAR is

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.


Why it is built the way it is

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.


The core abstraction (src/excar/core/interfaces.py)

Five Protocols define the entire contract of the system. Engines depend on these, never on each other:

ProtocolMethodResponsibility
STTEnginetranscribe(audio) -> TranscriptSpeech → text
LLMEnginestream_reply(text, history) -> Iterator[str]Text → streamed reply deltas
TTSEnginesynthesize(text) -> AudioDataText → speech
AudioSourcerecord_utterance() -> AudioData | NoneMic capture (one utterance)
AudioSinkplay(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).


The composition root (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.


Repository layout

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 team & ownership

  • Selim - product, device software, mobile app, integration, builds.
  • Atilla - local LLM model (the GGUF that powers offline mode); repo owner.
  • Mete - collaborator following the project via these reports.

The ConversationStore (session memory) was Atilla's; PersistentMemory is the layer added on top so memory survives the car powering off between drives.


The data flow in one line

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.