Repository navigation
Speech output: text to speech through Ollama (phonon) - #298
Merged
Merged
Conversation
Adds the other half of voice: answers read aloud. A speech client in zorp-voice (OllamaSpeech) speaks text through a local Ollama server's OpenAI-compatible POST /v1/audio/speech with the phonon model, and lists a model's voices from POST /api/show. zorp-web exposes it behind the existing voice feature at GET /api/speech/status, GET /api/speech/voices, POST /api/speech/chunks and POST /api/speech, registered in every build and answering 501 without it, the way the voice routes do. Rules kept (CLAUDE.md, ADR 0077/0078/0137): - The text goes to a loopback address or nowhere: same zorp-loopback guard as recorded audio and recall, redirects off, proxy off, no cloud TTS fallback. A connection-counting canary in zorp-voice/tests/no_remote.rs proves it. - Speaking is something a person turns on: a speaker button under every answer and a read-aloud toggle are the only starts; no tool starts or stops speech. - Spoken text is only ever the rendered answer, taken from the live streaming path so reopening a conversation does not read it aloud. The TTS model and voice are saved settings (config.rs), resolved like the chat model, with Settings pickers in the browser. Long text is split on sentence boundaries under phonon's per-request limit by the one chunker, zorp_voice::chunk_for_speech, exposed at /api/speech/chunks so playback starts on the first chunk. The /api/show voices field name could not be confirmed against Ollama's docs from this environment, so extract_voices searches for the first string array under a voices or *.voices key and falls back to the default voice; it is the one place to pin once the field is confirmed. The Mac app routes and a fully hands-free speaking mode (auto listen, send and speak) are follow-ups. cargo test -p zorp-voice, cargo test -p zorp-web --features voice, and the web/ check, test and build all pass. Closes #293 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K7qLzxWmLqkYQQLiQqJtiu
Ported from the #294 branch. Spawning a just-written executable can briefly fail with Text file busy (errno 26) when a sibling thread's fork holds a write handle on it, which flaked test_supervisor_spawn_lifecycle_and_events on Linux CI (a pre-existing flake in zorp-train, which this TTS change does not otherwise touch). Retry the spawn a couple of times on that one errno. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K7qLzxWmLqkYQQLiQqJtiu
This was referenced Oct 28, 2026
Three endpoints were built as format!("{}{path}", url) with a leading
slash on the path, so a ZORP_SPEECH_URL ending in "/" produced a "//".
Ollama tolerates it, but a configured trailing slash should not change
the request. Add a base() helper that trims one trailing slash and a
test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7qLzxWmLqkYQQLiQqJtiu
# Conflicts: # docs/DECISIONS.md # web/index.html # web/src/api.ts # web/src/main.ts # zorp-agent/src/config.rs # zorp-web/src/api.rs # zorp-web/src/settings.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #293
What this does
The other half of voice: answers read aloud. A speech client in
zorp-voice(OllamaSpeech) speaks text through a local Ollama server's OpenAI-compatiblePOST /v1/audio/speechwith thephononmodel, and lists a model's voices fromPOST /api/show.zorp-webexposes it behind the existingvoicefeature:GET /api/speech/status— reachable, model present, voices.GET /api/speech/voices?model=POST /api/speech/chunks— splits long text on sentence boundaries (the one chunker, in Rust), so the browser starts playback on the first chunk.POST /api/speech— synthesizes one chunk, returns the audio.All four exist in every build and answer 501 without the feature, the way the voice routes do. Browser: a speaker button under every answer, a read-aloud toggle in the toolbar, and model + voice pickers in Settings. The TTS model/voice are saved settings (
config.rs), resolved like the chat model.Rules kept (CLAUDE.md, ADR 0077 / 0078 / 0137)
zorp-loopback'sLoopbackUrl/LoopbackResolver(worded for spoken text viaSpeechWording), redirects off, proxy-from-env off, no cloud TTS fallback. A connection-counting canary (zorp-voice/tests/no_remote.rs) proves it by counting hits on a canary, not by trusting an error.speak/voicestake the model by name; the voice picker is driven by the model's/api/show.Where the code and the issue differ
/api/showvoices field name (issue flagged "confirm against the docs"):docs.ollama.comis not in this cloud session's allowed network, so I could not confirm the exact field.extract_voicessearches the answer for the first string array under avoicesor*.voiceskey and falls back to the default voice — it never fails the call, and it's the one place to pin once the field is confirmed. Called out per the instruction to say where I diverged.Tests
cargo test -p zorp-voice(incl. speech unit tests, theno_remotespeech canary cases, and a scripted request-body test) passes.cargo test -p zorp-web --features voicepasses (speech routes: chunks split, empty body rejected, 501 without the feature).web/:npm run check,npm test(incl. 7 newspeech-outputcases),npm run build:sitepass.CLAUDE.md/AGENTS.mdbullet added beside the voice one;docs/DECISIONS.mdupdated.Generated by Claude Code