6 September 2026 · Linux i7-1255U · PR #8 at 97ba0eb

Pocket is promising. The integration needs work.

Keep Pocket and Pipecat for the next iteration. Restore streaming, fix model lifecycle and text handling, then tune Parakeet. The measured evidence does not support removing Pipecat as the first performance fix.

Request changes · 1/5 merge readiness

Five actionable findings, four major. PR scope: +2682 −982 across 55 files. Review comments posted to GitHub: zero. No merge or product changes made.

PR #8 · Machine-readable review · Earlier model/runtime study

Walkthrough and scope

This PR replaces Kokoro with the native Pocket daemon and mixed-precision English model. I reviewed the Python adapters, daemon protocol, model ownership, metrics and Jarvis client/server speech paths, then exercised real models in an isolated checkout. This is a voice performance review, not an exhaustive audit of every packaging or CI change.

The controlled comparison

Five alternating trials per mode in one process, using the same resident daemon and the same 25-word report. The experimental mode only consumes chunks incrementally; it keeps Pipecat, the models, WAV-file IPC, onset filter and output transport. These are medians with observed ranges, not production p95 estimates.

Boundary, millisecondsPR #8 unchangedIncremental experiment
Native first raw chunk32 (28–69)37 (30–45)
First post-onset service PCM1,176 (1,076–1,307)79 (38–128)
First PCM at Pipecat sink1,180 (1,076–1,308)80 (39–129)
Native synthesis complete1,159 (1,059–1,285)1,153 (1,088–1,454)
First remote audio event1,247 (1,146–1,417)1,156 (1,090–1,457)

About 15× earlier first service PCM with Pipecat retained. Native completion time is almost unchanged. The remote event barely improves because it is emitted after the full utterance. Service-to-sink delay in the incremental run was 0.4–0.7 ms. Alternating-run data · Driver · Experimental adapter.

An earlier independent nine-utterance pass of the unchanged PR was slower: typical first service PCM 1.69–2.15 seconds, long 2.36–3.21 seconds, and the first cold short request 3.98 seconds through the remote result. This machine is an active developer laptop; scheduling, thermals and stochastic synthesis matter. Both passes demonstrate the same terminal-before-PCM ordering. I cannot reproduce the PR body's claimed typical 150–500 ms using its exact Python source. Initial measurements.

Comparison with the earlier Kokoro

Pocket clearly improves generation throughput and cancellation. Its current adapter still wastes the main streaming benefit. Historical results are context, not a newly repeated, identical-condition head-to-head: earlier native benchmarks pinned two CPUs, used a different onset definition, and excluded the production Pipecat path.

MeasurementEarlier KokoroCurrent Pocket evidence
Warm typical first audible PCM, model-level historical1,924 msCurrent PR first PCM: 1,176 ms median; different endpoint. Experimental Pocket first PCM: 79 ms.
Typical full native generation10,155 ms median; RTF 1.071,159 ms median in alternating runs; output duration varies by draw.
Cancellation after 250 ms3,978 ms median to return48 ms median; 30–53 ms in three full-runtime cancellation probes.
Peak memory accountingHistorical native benchmark maximum ~845 MiBInitial PR run: host HWM ~175 MiB, daemon HWM ~546 MiB. Their sum ~722 MiB is an upper-bound sum of separate peaks, not a simultaneous peak.
Production model-swap baseline from earlier docsFirst PCM 3.08–3.82 s; remote WAV including prep 4.23–5.20 sModel preparation still costs ~1.0 s; warm remote output varied ~1.15–2.29 s across these runs. Different prompts and load, so no precise speedup ratio.

Earlier native benchmark data · Earlier production baseline and its definitions. The earlier model study reported Kokoro WER 6.5%; PR #8 claims Pocket 5.2% on a different corpus. Those percentages do not establish a quality improvement by themselves.

Where the full pipeline spends time

StageObserved behaviorAssessment
Capture and uploadDesktop forwards PCM to its voice worker. Mobile buffers the complete recording and uploads it after release.No microphone or LAN/tailnet RTT measured. Mobile adds whole-upload latency before recognition.
Parakeet recognitionOne-second fixture: 33–49 ms release to result. 7.76-second 24 kHz sample: 337–348 ms.Actual Runtime commands with 20 ms PCM chunks, submitted faster than real time. Decode dominates; resampling ~3.8–4.2 ms on the long sample.
Pipecat STT framingWarm capture start ~1–2 ms. Internal release-to-transcript minus decode/resampling ~1–5 ms.Not the main steady-state delay. First capture had ~688 ms additional lazy imports outside its ~0.7 ms internal pipelineReady metric.
Typed Jarvis routingTranscripts pass through target grounding, clarification, and ordinary provider dispatch.Source traced. User confirmation and provider execution are variable and were not benchmarked or bypassed.
STT → TTS preparation985–1,056 ms in three swaps.Recognizer is destroyed before Pocket is loaded. Warm speech-only benchmarks exclude this cost.
Pocket generation and adapterNative stream is collected until terminal, then converted to Pipecat PCM.New PR bottleneck. Incremental delivery removes most of this first-PCM delay.
Local outputPipecat sends frames to PipeWire on Linux, native local output elsewhere.Fake sink tested, no real speaker/DAC/hotkey measurements. Hardware buffering remains unknown.
Remote outputPython buffers all PCM; TS concatenates chunks; server encodes a complete WAV; mobile writes a file and plays it.Existing whole-WAV contract defeats streaming even after fixing the new adapter. Requires coordinated Jarvis contract/server/client change.
TTS → listening1,921–1,948 ms in three swaps.Pocket is closed and Parakeet reconstructed. This is a latency/memory policy decision, not Pipecat overhead.
Web and presetsBrowser fallback uses browser speechSynthesis; Full and Controller own native speech; plain Headless server service remains unavailable without the desktop broker.Pocket results do not describe browser voices. Capability and client paths reviewed; cross-platform packaged behavior not device-tested.

STT, cancellation and idle data · Driver. Nine cancellation probes at 25/250/1,000 ms completed in 29–129 ms. This measures return to a cancellation result with a PCM buffer sink, not the last physical speaker sample.

Parakeet tuning produced a real improvement

The default four-thread recognizer used about 2.57 CPU-seconds to decode the 7.76-second sample, then another 3.78 CPU-seconds in the first two idle seconds. The following two-second interval was quiet. This is a short post-decode spin tail, not continuous idle polling.

Recognizer configurationReport decode wall medianReport CPU medianCPU in first 2 idle seconds
4 threads, existing defaults263 ms2,573 ms3.783 s
4 threads, spin disabled215 ms531 ms0.00015 s
2 threads, spin disabled227 ms373 ms0.00019 s
1 thread, spin disabled305 ms304 ms0.00010 s

All three transcripts per configuration matched. Four threads without spin was fastest in this small sample; two without spin used less CPU for a modest wall-time tradeoff. This does not yet choose a universal thread count. Disable spin first, then benchmark the realistic workload and target devices. Data · Reproduction · Tested config.

Sherpa 1.13.6 supports passing cpu:/path/to/config with SessionConfig.session.intra_op.allow_spinning=0 and the corresponding inter-op key. This experiment used that public mechanism; changing Python onnxruntime options alone would not configure Sherpa's native sessions. Sherpa implementation · ONNX Runtime threading documentation.

Residency, quality, and acceptance

A separate co-resident experiment retained both recognizer and Pocket. Sampled current RSS was ~799 MiB when ready, then 966–990 MiB after decoding and synthesis. That is roughly 0.94–0.97 GiB while used, excluding Electron, Node, clients and transient unsampled peaks. It is too close to the existing 1 GiB voice gate to blindly make both models permanent on every machine. A memory-budgeted resident policy is worth testing because it can remove the measured reloads. Both model thread pools must remain quiet when idle. Memory and quality evidence.

The nine-sample quality screen used audio from the incremental experiment: three short, three typical and three long samples, 216 reference words total. ASR screening found 4 errors, all on two of the long sentences. In one, “unchanged” disappeared from the transcript. ASR can mishear valid speech; listen to the samples before making a perceptual verdict. This small screen is not a replacement for the previous 42-sample corpus or real-device acceptance.

Long sentence, draw 1. Same text and model; incremental adapter experiment.

Long sentence, draw 2. Same text and model; incremental adapter experiment.

Long sentence, draw 3. Same text and model; incremental adapter experiment.

Suggested acceptance targets for the next implementation, not claims of current compliance: warm post-onset first PCM p95 ≤200 ms on the reference laptop; cancellation p95 ≤100 ms with a hard observed maximum below 200 ms; negligible post-decode idle CPU; no missing negation, number or clause in the expanded semantic corpus; bounded total process memory under the selected residency policy. Run at least 30 representative trials per boundary before estimating p95. Measure release-to-transcript, routing/confirmation, text-ready-to-first-PCM, actual playback onset and remote transfer separately.

PR findings

FindingSeverityLocation
Deliver PCM as the daemon produces itP1apps/desktop/pipecat/src/jarvis_voice_runtime/pocket.py:425
Use a Windows-compatible readiness readerP1apps/desktop/pipecat/src/jarvis_voice_runtime/pocket.py:157
Close a newly created daemon when preparation is abandonedP1apps/desktop/pipecat/src/jarvis_voice_runtime/runtime.py:295
Enforce the model's text bound without losing wordsP1apps/desktop/pipecat/src/jarvis_voice_runtime/pocket.py:425
Include daemon CPU and memory in performance telemetryP2apps/desktop/pipecat/src/jarvis_voice_runtime/pocket.py:453
P1 · logic · performance Deliver PCM as the daemon produces it

PocketDaemon.synthesize collects every raw chunk and returns only after the terminal event. run_tts then sorts and reads the files. The queue is bounded only after generation has already completed, so the adapter removes the native runtime's streaming advantage.

In five alternating runs of the same 25-word report, original first service PCM had a median of 1,176 ms. An experimental incremental iterator, retaining the same daemon, model, Pipecat pipeline and sink, reduced that median to 79 ms. Median native synthesis completion stayed about 1.16 seconds in both modes.

The existing tests pass because their fake daemon returns a completed list. A new regression withholding the terminal event while a playable chunk is available fails against the PR. This directly affects local speech latency; remote buffering is a separate issue.

Fix: change PocketDaemon and JarvisPocketTTSService.run_tts to consume ordered chunks incrementally through the onset filter and bounded queue before synthesis finishes; propagate terminal failures, cancel the active request independently of blocked reads, and retain temporary files only until their PCM has been consumed.

Prompt for an implementation agent
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/desktop/pipecat/src/jarvis_voice_runtime/pocket.py` around line 425, change PocketDaemon and JarvisPocketTTSService.run_tts to consume ordered chunks incrementally through the onset filter and bounded queue before synthesis finishes; propagate terminal failures, cancel the active request independently of blocked reads, and retain temporary files only until their PCM has been consumed. Start with a failing regression at the affected output or lifecycle boundary. Preserve cancellation, request identity, PCM ordering, and model ownership. Apply the correction to every local and remote caller that shares this path, and verify the complete behavior with focused tests and real-model measurements. Do not modify unrelated T3 internals, the live install, or merge the PR. Report measured latency and resource use separately from untested device behavior.
P1 · logic · correctness Use a Windows-compatible readiness reader

PocketDaemon.start calls select.select on a subprocess stdout pipe. Python on Windows supports select only for sockets. The OSError is swallowed and the loop retries until the 30-second deadline.

A daemon can already have emitted ready and still be killed as a startup timeout. The portable regression supplies a ready line and Windows's not-a-socket error; startup fails. This is a source and simulated-platform finding, not a claim that a Windows device was tested.

Fix: replace select-based pipe readiness with a portable reader and bounded startup wait, surface EOF and startup-failed messages, and always reap failed startup processes. Validate real Windows startup separately.

Python documents that Windows select accepts sockets, not file pipes.

Prompt for an implementation agent
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/desktop/pipecat/src/jarvis_voice_runtime/pocket.py` around line 157, replace select-based pipe readiness with a portable reader and bounded startup wait, surface EOF and startup-failed messages, and always reap failed startup processes. Validate real Windows startup separately. Start with a failing regression at the affected output or lifecycle boundary. Preserve cancellation, request identity, PCM ordering, and model ownership. Apply the correction to every local and remote caller that shares this path, and verify the complete behavior with focused tests and real-model measurements. Do not modify unrelated T3 internals, the live install, or merge the PR. Report measured latency and resource use separately from untested device behavior.
P1 · logic · stability Close a newly created daemon when preparation is abandoned

When shutdown or capture wins while Pocket is being constructed, _prepare_speech deletes native_tts and returns. PocketNativeHandle owns a subprocess and has no destructor that closes it. The new explicit close in _dispose_tts only covers handles already attached to a service.

A regression changes the desired lifecycle state during the factory call and verifies that the newly created handle is closed. It fails. The orphan daemon can keep its model resident after the runtime has moved back to listening, undermining the single-model memory policy.

Fix: explicitly close any created or retained Pocket handle on every abandoned preparation and pipeline-start failure path. Ownership must transfer to the service only after successful startup; cleanup must not depend on garbage collection.

Prompt for an implementation agent
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/desktop/pipecat/src/jarvis_voice_runtime/runtime.py` around line 295, explicitly close any created or retained Pocket handle on every abandoned preparation and pipeline-start failure path. Ownership must transfer to the service only after successful startup; cleanup must not depend on garbage collection. Start with a failing regression at the affected output or lifecycle boundary. Preserve cancellation, request identity, PCM ordering, and model ownership. Apply the correction to every local and remote caller that shares this path, and verify the complete behavior with focused tests and real-model measurements. Do not modify unrelated T3 internals, the live install, or merge the PR. Report measured latency and resource use separately from untested device behavior.
P1 · logic · correctness Enforce the model's text bound without losing words

The PR documents a 50-token operating bound and acknowledges missing clauses in longer input, but forwards the entire finalized text to the daemon. TextAggregationMode.SENTENCE does not split these TTSSpeakFrame submissions; the measured 25-word, four-sentence report is one native request. The public host contract permits up to 32,000 characters.

In three generated 46-word long-sentence samples, Parakeet produced two imperfect transcripts, including omission of “unchanged”. This is an ASR screening result, not a listening verdict, but it agrees with the PR's acknowledged limitation. Short and typical samples in this nine-sample screen transcribed without errors.

An aggregate WER can hide a dropped negation or important clause. Production text handling should preserve the full meaning for all accepted inputs rather than rely on most reports being short.

Fix: add tokenizer-aware bounded text segmentation before native generation, preserving every word and punctuation needed for meaning. Stream successive segments, support interruption across boundaries, and validate long clauses, names, numbers, negation, and one-word responses with transcripts and listening checks.

Prompt for an implementation agent
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/desktop/pipecat/src/jarvis_voice_runtime/pocket.py` around line 425, add tokenizer-aware bounded text segmentation before native generation, preserving every word and punctuation needed for meaning. Stream successive segments, support interruption across boundaries, and validate long clauses, names, numbers, negation, and one-word responses with transcripts and listening checks. Start with a failing regression at the affected output or lifecycle boundary. Preserve cancellation, request identity, PCM ordering, and model ownership. Apply the correction to every local and remote caller that shares this path, and verify the complete behavior with focused tests and real-model measurements. Do not modify unrelated T3 internals, the live install, or merge the PR. Report measured latency and resource use separately from untested device behavior.
P2 · logic · performance Include daemon CPU and memory in performance telemetry

GenerationMetrics measures Python process CPU, and Runtime reports the Python process RSS. Native synthesis now happens in a separate daemon. Its terminal event already supplies native CPU and peak RSS, but the adapter discards those fields.

One measured report used 3,560 ms of native CPU while the published timing reported only 159 ms. Native peak RSS was about 375 MiB while the reported host peak was about 168 MiB. The model-swap benchmark reads this incomplete telemetry, so its memory gate cannot establish the whole voice footprint.

Fix: carry daemon timings into the typed metrics and report host and native CPU separately plus their sum. Track process-specific current RSS and a simultaneous aggregate peak rather than silently equating host RSS or a sum of independent historical peaks with total voice memory.

Prompt for an implementation agent
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/desktop/pipecat/src/jarvis_voice_runtime/pocket.py` around line 453, carry daemon timings into the typed metrics and report host and native CPU separately plus their sum. Track process-specific current RSS and a simultaneous aggregate peak rather than silently equating host RSS or a sum of independent historical peaks with total voice memory. Start with a failing regression at the affected output or lifecycle boundary. Preserve cancellation, request identity, PCM ordering, and model ownership. Apply the correction to every local and remote caller that shares this path, and verify the complete behavior with focused tests and real-model measurements. Do not modify unrelated T3 internals, the live install, or merge the PR. Report measured latency and resource use separately from untested device behavior.

Recommended implementation order

  1. Fix incremental Pocket delivery, abandoned-handle cleanup and Windows startup. Keep the current native runtime and Pipecat.
  2. Add bounded text segmentation and quality checks that preserve every clause. Repeat the whole corpus with correct onset measurements.
  3. Disable Parakeet spin using the supported provider configuration, and expose complete process metrics.
  4. Evaluate memory-budgeted model residency. Retain the existing low-memory path where a resident pair fails the budget.
  5. Design progressive remote audio across the existing Jarvis wire contracts and every affected client. Preserve request identity, cancellation and bounded buffers.
  6. Run real local and remote device acceptance. Consider removing Pipecat only if a later controlled test identifies material remaining overhead that simpler integration cannot remove.

Reproduction and limits

Source: isolated detached worktree /tmp/jarvis-pr8-perf-audit, exact PR head. Models and already-built native binary were reused from the implementation checkout with its pinned provenance: PocketTTS.cpp e801e7d, stephvax fc68ee7 English 2026-04, mixed precision, Alba three-second reference, ORT 1.23.2, two inference threads, 1/3-frame chunks. Python 3.12 environment matches PR pins: Pipecat 1.7.0, Sherpa 1.13.6, Python ORT 1.24.4. The native binary was not rebuilt by this audit. No product source was modified; the incremental adapter exists only in benchmark-process monkeypatches.

One heavy experiment ran at a time on the active Linux i7-1255U laptop. CPU affinity was not forced. Exact JSON timings use monotonic clocks. Instrumentation records daemon stdout arrival, first post-onset service frame, fake PCM sink write and remote message emission. First PCM is not acoustic onset: the gate retains up to 40 ms preroll and the OS/DAC/network are excluded. Initial and streaming runs were sequential; the alternating experiment reduces drift but does not establish identical random draws or statistical p95.

The production Python entrypoint eagerly loads Parakeet before ready. The cold first-request probe constructed Runtime in-process and excluded interpreter imports and this entrypoint load, so it is not a full application cold-start measurement. STT tests submitted fixture packets in a burst, not through a live microphone. Network, Electron IPC, native permission, physical key release, mobile playback, Mac/Windows packages, provider time, and large node catalogs remain unmeasured. No browser or simulator was launched, no live user database was used, and no unrelated processes were stopped.

The existing Pocket runtime suite passes 24 tests. Three new ordering/lifecycle/Windows-semantics regression tests fail against PR #8. Passing suite output · Failing regression output · Regression source. These identify why passing mocked tests did not establish production readiness.

All changed files in PR scope
PathChange
.github/workflows/jarvis-desktop-linux.yml+19 −10
.github/workflows/jarvis-desktop-mac.yml+19 −8
.github/workflows/jarvis-setup-windows.yml+11 −9
apps/desktop/pipecat/src/jarvis_voice_runtime/__main__.py+10 −10
apps/desktop/pipecat/src/jarvis_voice_runtime/kokoro.py+0 −290
apps/desktop/pipecat/src/jarvis_voice_runtime/onset.py+91 −0
apps/desktop/pipecat/src/jarvis_voice_runtime/output.py+1 −1
apps/desktop/pipecat/src/jarvis_voice_runtime/pocket.py+528 −0
apps/desktop/pipecat/src/jarvis_voice_runtime/runtime.py+34 −27
apps/desktop/pipecat/tests/test_native_runtime.py+1 −1
apps/desktop/pipecat/tests/test_onset.py+98 −0
apps/desktop/pipecat/tests/{test_tts_runtime.py => test_pocket_runtime.py}+293 −445
apps/desktop/src/voice/DesktopJarvisVoice.ts+2 −2
apps/desktop/src/voice/DesktopVoiceWorkerProtocol.ts+1 −1
apps/desktop/src/voice/desktopVoiceWorker.ts+5 −5
apps/server/src/environment/ServerEnvironment.test.ts+1 −0
apps/web/src/components/jarvis/JarvisControlCenter.tsx+6 −3
apps/web/src/components/jarvis/JarvisOnboarding.logic.test.ts+1 −1
apps/web/src/components/jarvis/JarvisOnboarding.logic.ts+3 −3
apps/web/src/components/jarvis/JarvisOnboarding.tsx+3 −2
docs/internals/glossary.md+1 −1
docs/internals/jarvis-controller.md+1 −1
docs/internals/jarvis-mobile-voice.md+3 −3
docs/internals/jarvis-t3-boundary.md+3 −3
docs/internals/jarvis-task-desk.md+1 −1
docs/internals/linux-full-native-voice.md+24 −25
docs/internals/pocket-tts.md+93 −0
docs/operations/jarvis-acceptance-checklist.md+3 −3
docs/operations/release.md+1 −1
docs/user/install.md+1 −1
docs/user/jarvis.md+5 −5
packages/contracts/src/environment.ts+14 −1
packages/jarvis-native-voice/.gitignore+2 −1
packages/jarvis-native-voice/native/pocket/CMakeLists.txt+89 −0
packages/jarvis-native-voice/native/pocket/PINNED_REVISIONS.json+58 −0
packages/jarvis-native-voice/native/pocket/daemon_main.cpp+492 −0
packages/jarvis-native-voice/package.json+3 −3
packages/jarvis-native-voice/resources/THIRD_PARTY_NOTICES.md+14 −3
packages/jarvis-native-voice/scripts/benchmark-model-swap.mjs+8 −8
packages/jarvis-native-voice/scripts/{benchmark-kokoro.mjs => benchmark-pocket.mjs}+1 −1
packages/jarvis-native-voice/scripts/build-pocket-runtime.mjs+376 −0
packages/jarvis-native-voice/scripts/ensure-kokoro-resources.mjs+0 −79
packages/jarvis-native-voice/scripts/ensure-pocket-resources.mjs+322 −0
packages/jarvis-native-voice/scripts/smoke-speech-runtime.mjs+1 −1
packages/jarvis-native-voice/src/desktop-native-voice.ts+1 −1
packages/jarvis-voice-runtime/src/pipecat-protocol.test.ts+1 −1
packages/jarvis-voice-runtime/src/pipecat-protocol.ts+2 −2
packages/jarvis-voice-runtime/src/pipecat-sidecar.test.ts+2 −2
packages/jarvis-voice-runtime/src/pipecat-sidecar.ts+3 −3
scripts/build-desktop-artifact.test.ts+10 −7
scripts/jarvis-release-workflow.test.ts+2 −2
scripts/jarvis-voice-packaging.ts+14 −5
scripts/smoke-headless-node-runtime.test.ts+2 −0
scripts/smoke-headless-node-runtime.ts+1 −0
scripts/windows-setup.ts+1 −0
Copy the complete fix handoff
Re-run this review