Repository navigation
feat: online voice engines: provider registry, streaming, offline fallback modes - #167
Open
hongkongkiwi wants to merge 21 commits into
Open
hongkongkiwi wants to merge 21 commits into
hongkongkiwi wants to merge 21 commits into
Conversation
hongkongkiwi
force-pushed
the
voice-online
branch
3 times, most recently
from
October 26, 2026 11:09
a9131f8 to
1e326f5
Compare
hongkongkiwi
force-pushed
the
voice-online
branch
6 times, most recently
from
October 31, 2026 12:17
47fb4e3 to
fd5da56
Compare
8 tasks done
hongkongkiwi
force-pushed
the
voice-online
branch
from
November 1, 2026 04:48
34da824 to
ac37140
Compare
Pluggable engine layer for speech-to-text: Apple Speech on macOS (streaming punctuated partials, on-device preferred) and sherpa-onnx everywhere (streaming zipformer live partials + offline quality pass). Models are never bundled: Settings will offer a pinned catalog (Moonshine, SenseVoice, Parakeet, Whisper, live streamers, punctuation) with size caps, SHA-256 verification and progress events. Commands: voice_state/save_settings/models/download_model/start/stop/cancel.
src-tauri/Info.plist is merged by Tauri into the generated bundle, so voice dictation prompts for microphone and speech recognition with the right copy. The engine refuses (instead of crashing) when the running binary has no bundle — the tauri dev case.
The mic sits beside the Saver leaf once Voice is on: tap or hold (per setting) records natively, live partial text streams into the prompt replacing the previous partial, and Done settles the final transcript for review — never auto-send, Esc cancels, and the recording bar (level meter, timer) survives composer re-renders from body. Settings → Voice lists the pinned model catalog with sizes, licenses, download progress and a quality-model picker; preview mocks cover the whole flow.
expo-speech-recognition drives the phone's own recognizer: tap the mic to dictate, live partial text streams into the composer, and only the final transcript is sent — as ordinary text over Connect. On-device recognition is preferred (iOS requiresOnDeviceRecognition, Android's on-device service); Settings gains an On-device only switch that forbids the OS's network service. The mic hides on devices without a speech service. Nothing is uploaded to PandaCode.
docs/voice.md covers engines, the pinned model catalog with licenses, commands/events, interaction and mobile behavior, with a Verification section. docs/ipc.md gains the voice_* contract; the README privacy table lists the model download and the mobile OS speech service. Adds the PANDACODE_MIC live capture test the doc references.
Settings → Voice gains a mode: offline (default), online, or online-with-fallback. A provider registry presets OpenAI, Groq, OpenRouter, Together, Deepgram, OpenAI Realtime (Together-compatible) and any custom OpenAI-compatible endpoint; keys live in the secret store (voice_set_key/voice_keys, never the webview), model and base URL are settings, voice_test_provider exercises the whole path with a half second of silence. Streaming providers (Deepgram's WebSocket, OpenAI Realtime at 24 kHz) give live partials while speaking; multipart providers send one WAV on stop. A failed online attempt settles on the local quality model (finalize_buffer) when fallback is on, with the result marked fallback. Loopback WebSocket mocks cover both streaming clients end to end (session config, PCM/base64 payloads, interim and final composition, error paths).
The mode selector (offline / online / online with fallback) sits above the engine; online shows the provider picker, API-key field (secure store, write-only from the webview), model list and endpoint override with a Test button that probes the real service. Preview mocks cover the whole flow (providers, keys, test, mode normalization keeps online reachable until a provider is picked); the voice spec asserts key saving and the connectivity test. PW_PORT lets a second worktree run the suite against its own dev server instead of port 1420's.
The online provider forms render .voice-key and .voice-base-url inputs that had no stylesheet, so they showed as bare browser fields inside the styled setting rows.
…ing the recording
The Linux runner has pkg-config but no ALSA headers, so alsa-sys's build script panics before the tests compile.
Drop a Copy clone, replace a dropped spawn handle with drop(), take &Path in setup, and build test Settings with struct literals.
The picked dictation language rides along as an ISO-639-1 hint on every transport: the multipart language field, Deepgram's language query parameter, and the realtime session's transcription config. Detect automatically sends nothing and leaves detection to the provider.
Named in their own language like the rest, and the voice spec is now registered in the WebKit config (it silently ran zero voice tests there).
hongkongkiwi
force-pushed
the
voice-online
branch
from
November 1, 2026 05:03
ac37140 to
5e29cef
Compare
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.
What and why
Closes #169.
What
Settings → Voice gains a transcription mode, exactly the requested set:
fallback: trueand a toast.Language hint on every transport
The dictation language from Settings → Voice rides along when a specific language is picked (ISO-639-1, region stripped): the
languagemultipart field for the OpenAI-compatible endpoints (verified against the official SDK docs: applies to the gpt-4o-transcribe models as well as whisper-1), Deepgram'slanguagequery parameter (omit → the provider defaults toen), and the realtime transcription session'slanguageconfig. Detect automatically sends nothing and lets the provider detect. The dictation picker itself gains Русский and Polski, named natively like the rest.Provider registry
Presets cover the APIs people actually use; Custom points at any OpenAI-compatible endpoint, self-hosted included (a local whisper.cpp server on localhost works):
POST /v1/audio/transcriptionsgpt-4o-mini-transcribewhisper-large-v3-turbo/api/v1/audio/transcriptions)openai/gpt-4o-mini-transcribeopenai/whisper-large-v3Tokenauth)nova-3gpt-4o-mini-transcribeSpeed was the design constraint: streaming providers send 16 kHz PCM (Deepgram) or 24 kHz PCM16 (OpenAI Realtime, upsampled from the capture) and partial text lands in the composer while you speak; multipart providers send one WAV on stop and usually return within a second on the turbo models.
voice_test_providerexercises the whole configured path with half a second of silence so "Test" in Settings is a real connectivity check.Keys and settings
voice_set_key/voice_keys— ids only ever reach the webview, never values).voice.json); a provider switch adopts the preset's default model.voice_providers) drives the Settings picker, marked live vs on-stop.voice_startrefuses with a clear message until provider + key exist.Fallback semantics
Recording always captures locally. Online failures mid-take emit a toast and keep buffering; on stop the buffer is decoded by the offline engine (
sherpa::finalize_buffer— the quality pass without the streaming stage). Without fallback mode, the online error is the outcome. Multipart providers can't fail mid-take.How it was tested
cargo clippy --all-targets -- -D warningsclean;cargo test --lib voice::— 37 passed, 1 ignored (provider URL shapes per preset, WAV encode round-trip, PCM16/upsample math, settings mode normalization, language-hint normalization, Deepgram URL carrying the hint, and loopback WebSocket mocks covering both streaming clients end to end — session config, binary/base64 payloads, interim→final composition, error paths).npx tsc --noEmit,npm run build(typecheck + bundle budget) clean.npx playwright test tests/voice.spec.ts— 5/5 in Chrome.tests/voice.spec.tswas missing from the WebKit config's testMatch and grep (zero voice tests ran there); both are now registered. A WebKit run completed 3/5 with 2 timeouts while the machine was under heavy memory pressure (25 GB swap in use from a concurrent perf run); Chrome passes all 5. Treat the WebKit verdict as partial pending a calm-machine rerun.npm testhas one pre-existing failure (tool-cards, reproduces on the clean fork tip; verified this session); the three bundle-budget failures in a fresh worktree clear afternpm run build.Also includes a small DX fix:
PW_PORTon the Playwright config lets a second worktree run the suite against its own dev server instead of reusing whatever serves 1420 (default behavior unchanged).npm run build(typecheck + bundle)npm testcargo test --manifest-path src-tauri/Cargo.toml(if Rust changed)Checklist
<type>: <description>docs/updated if behavior changedsrc/preview.tsfor new IPC commands