Keep streaming TTS open; name the swappable-provider pattern

Utterances cross as chunks with an explicit terminator, so whole-utterance is
a stream of one chunk and a streaming TTS needs no protocol change. Records
what must not be built — a single-body download-then-play player — since that
forecloses streaming silently.

Also notes the transport is fully exercisable with espeak-ng or a canned WAV,
so audio transport need not wait on GPT-SoVITS.

The provider-behind-an-interface shape has now been picked three times
independently, so it is named as the project idiom rather than re-derived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kj58Jugkic783b8ZrtTVZ
This commit is contained in:
2026-09-04 14:02:41 +09:00
parent 1d24d90c36
commit 2fe9665664
2 changed files with 47 additions and 10 deletions
+20 -1
View File
@@ -86,6 +86,25 @@ Topic names and payload schemas are a **contract shared across repos** — Andro
and ESP32 clients depend on them — so they are versioned in their own repo and ESP32 clients depend on them — so they are versioned in their own repo
rather than duplicated per client. rather than duplicated per client.
### Swappable providers
The same shape has now been chosen independently three times, so it is the
project's idiom rather than three coincidences:
| Component | Interface | Real | Trivial |
|---|---|---|---|
| `svc-nav` | routing provider | local Valhalla | cloud routing API |
| `svc-gw` | input source | the bus | synthetic state |
| voice | TTS provider | GPT-SoVITS | `espeak-ng`, or a canned WAV |
The rule in each case: **one implementation of the contract, a swappable source
behind it.** That is what lets a service mock itself without a second codebase to
drift (decision 19), and what keeps a heavyweight dependency from blocking work on
everything downstream of it.
A new component that has an expensive or not-yet-existing dependency should assume
this shape rather than rediscovering it.
## Data flow and direction ## Data flow and direction
### Inside the Orin, direction belongs to topics, not links ### Inside the Orin, direction belongs to topics, not links
@@ -279,7 +298,7 @@ rewriting the document.
| 9 | Nav guidance published on the bus; route requests over HTTP via `telemetry-gw` | **Proposed** | 2026-09-03 | | 9 | Nav guidance published on the bus; route requests over HTTP via `telemetry-gw` | **Proposed** | 2026-09-03 |
| 10 | Map artifacts delivered by `rsync` over LAN only; Valhalla tiles kept as a directory, not a tar | Accepted | 2026-09-03 | | 10 | Map artifacts delivered by `rsync` over LAN only; Valhalla tiles kept as a directory, not a tar | Accepted | 2026-09-03 |
| 11 | Boot ordering is capture-first: acquisition reaches ready before serving or voice | Accepted | 2026-09-03 | | 11 | Boot ordering is capture-first: acquisition reaches ready before serving or voice | Accepted | 2026-09-03 |
| 12 | The **tablet** owns the car's aux input; TTS crosses as discrete utterances, not a stream | Accepted | 2026-09-04 | | 12 | The **tablet** owns the car's aux input; TTS crosses as chunked utterances, streaming-capable | Accepted | 2026-09-04 |
| 13 | Reasoning stays in the cloud — a local LLM does not fit beside the voice stack | Accepted | 2026-09-03 | | 13 | Reasoning stays in the cloud — a local LLM does not fit beside the voice stack | Accepted | 2026-09-03 |
| 14 | Recordings are bounded with rotation | Accepted | 2026-09-04 | | 14 | Recordings are bounded with rotation | Accepted | 2026-09-04 |
| 15 | Boundary is two simplex channels — lossy stream down, acknowledged request/response up | Accepted | 2026-09-03 | | 15 | Boundary is two simplex channels — lossy stream down, acknowledged request/response up | Accepted | 2026-09-03 |
+27 -9
View File
@@ -138,17 +138,35 @@ The tablet is the media source, so it holds the aux cable whatever is decided
about speech. Giving the Orin its own aux line would not free the display unit — about speech. Giving the Orin its own aux line would not free the display unit —
it would add a second permanent cable to the car and leave the first one in place. it would add a second permanent cable to the car and leave the first one in place.
### TTS crosses as utterances, not as a stream ### TTS crosses as chunked utterances
An utterance is short, finite, and fully synthesised before it is needed. So it An utterance travels as a **sequence of chunks with an explicit terminator**, over
ships as a **discrete object over the reliable channel**, not as a real-time audio the reliable channel. Not as one object, and not as a clock-synchronised audio
stream: no jitter buffer, no underrun handling, no clock synchronisation, and a stream.
dropped packet is a retry rather than an audible gap.
The cost is that speech begins only once synthesis *and* transfer complete. For a This is deliberately the shape that keeps both modes open, because whole-utterance
car assistant that is fine — nothing here is lip-synced. If streaming TTS is ever is simply a stream of one chunk:
wanted, so that speech starts before synthesis finishes, this is the decision to
revisit. | TTS capability | What it sends |
|---|---|
| Synthesises fully, then returns | one chunk, then terminator |
| Streams as it synthesises | chunks as produced, then terminator |
Same protocol, same receiver. The receiver starts playback on the first chunk,
appends as more arrive, and finishes on the terminator — which is what Android's
`AudioTrack` streaming mode does natively.
**What must not be built:** an utterance as a single response body with a
"download the file, then play the file" player. That works today and forecloses
streaming permanently. Whether the TTS we land on streams is still open, so the
transport must not assume either way.
### The transport is testable with no real TTS
Audio is the lowest-priority part of the system, but its transport does not have
to wait for GPT-SoVITS. A canned WAV, or `espeak-ng` — instant, tiny, and awful —
exercises chunking, ordering, the terminator, ducking and the no-sink path
completely. Voice quality is irrelevant to every one of those.
### Ducking is the tablet's job ### Ducking is the tablet's job