Skip to content

feat(vibevoice-cpp): true streaming TTS (TTSStream via vv_capi_tts_stream)#10764

Merged
mudler merged 5 commits into
masterfrom
feat/vibevoice-cpp-streaming
Jul 9, 2026
Merged

feat(vibevoice-cpp): true streaming TTS (TTSStream via vv_capi_tts_stream)#10764
mudler merged 5 commits into
masterfrom
feat/vibevoice-cpp-streaming

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

True streaming for the vibevoice-cpp TTS backend

The vibevoice-cpp backend advertised streaming (TTSStream) but faked it: it synthesized the entire clip to a tempfile via the file-only vv_capi_tts, then chunked the finished PCM. Time-to-first-audio equaled full-synthesis time, defeating the point of a "Realtime" model.

This PR wires the backend to real streaming. The heavy lifting is in the engine (localai-org/vibevoice.cpp#8), which adds a bit-exact streaming acoustic decoder and a vv_capi_tts_stream callback ABI. Here we:

  • Bind vv_capi_tts_stream via a single reusable purego callback created once at init (purego has a process-global callback limit and cannot free them). CGO_ENABLED=0-safe: no runtime/cgo.
  • Rewrite TTSStream: emit the streaming WAV header, then push each int16 PCM window (copied out of the transient C buffer, little-endian) into the gRPC results channel as the engine produces it. The synth-to-tempfile hack is gone.
  • Bump the engine pin to the streaming-decoder commit.

The non-streaming TTS() path is unchanged. Scope is the realtime-0.5B model; a 1.5B model routed to streaming returns a clean error.

Results (real model, this box is CPU-only)

End-to-end through the real Load -> TTSStream path with microsoft/VibeVoice-Realtime-0.5B (q8_0):

value
Time to first audio (streaming) 2.38 s
Time to first audio (batch, = full synth) 39.96 s
First-audio speedup ~17x
Stream chunks 18

CPU-only synthesis is not real-time for the whole clip (~40 s for the paragraph), but streaming cuts perceived latency from 40 s to 2.4 s; on GPU the per-window rate clears real-time. The win is time-to-first-audio, which is exactly what streaming is for.

Validation

  • Engine (in the linked PR): decoder chunked decode == single-shot bit-exact (rmse 0.0000); vv_capi_tts_stream int16 byte-identical to file PCM.
  • Here: purego callback framing unit test (little-endian, copy-before-push); gated real-model integration test (VIBEVOICE_IT=1) asserting valid streaming WAV header, >=2 chunks, and TTFA < total.

Note on the pin

VIBEVOICE_CPP_VERSION currently points at the engine PR's branch commit (fba2449). Re-pin to the merged master commit once localai-org/vibevoice.cpp#8 lands (or let the bump_deps workflow do it).

Assisted-by: Claude:claude-opus-4-8 [Claude Code]

🤖 Generated with Claude Code

mudler added 4 commits July 9, 2026 17:12
Replaces the synth-to-tempfile TTSStream hack with a real streaming path:
binds the new vv_capi_tts_stream callback ABI via a single reusable purego
callback (CGO_ENABLED=0-safe, no runtime/cgo), copies each int16 PCM window
into the gRPC results channel after the streaming WAV header.

Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
…measurement

Gated behind VIBEVOICE_IT=1, this Ginkgo spec dlopens the engine .so and
drives the exact Go->purego->C TTSStream/TTS path against the real
vibevoice-realtime-0.5B model. It measures time-to-first-audio for the
streaming path versus the batch path and asserts the streaming win:
44-byte WAV header first, >=2 PCM windows, non-silent audio, and
TTFA < total_stream. Without the env var the spec skips so CI and
normal go test are unaffected.

Measured: TTFA 2.38s vs batch deliver-time 39.96s (first audio in 5.9%
of the batch time, ~17x faster), 18 stream chunks, non-silent 24kHz PCM.

Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
Bumps VIBEVOICE_CPP_VERSION to the streaming-decoder engine commit that
adds vv_capi_tts_stream (localai-org/vibevoice.cpp#8). Re-pin to the
merged master commit once that PR lands.

Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
localai-org/vibevoice.cpp#8 merged to master as 000e372; move the pin
off the PR branch commit onto the merged master commit.

Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
if activeStream == nil || samples == nil || n <= 0 {
return 0
}
pcm := unsafe.Slice(samples, int(n))
golangci-lint errcheck flagged the unchecked fmt.Fprintf calls that
print the streaming TTFA headline. Build the report once with
fmt.Sprintf and write it per destination with an explicitly discarded
error, matching the GinkgoWriter reporting idiom used by the other
backend tests.

Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-fable-5 [Claude Code]
@mudler mudler enabled auto-merge (squash) July 9, 2026 23:34
@mudler mudler merged commit 3c2d85a into master Jul 9, 2026
68 of 69 checks passed
@mudler mudler deleted the feat/vibevoice-cpp-streaming branch July 9, 2026 23:48
mudler added a commit that referenced this pull request Jul 10, 2026
#10766)

The vibevoice transcription e2e hangs until the go test timeout when
the HF CDN is slow: the ASR Q4_K model is >10 GB, downloadFile capped
every curl attempt at --max-time 600 (needs a sustained ~17 MB/s to
fit), and curl's --retry restarts from byte zero, so no attempt ever
makes forward progress. This killed the job twice on PR #10764 and
previously forced skipping it on release tags (#10567).

Replace the wall-clock cap with stall detection (--speed-limit 1 MiB/s
over --speed-time 120s) and resume from the bytes already on disk with
-C -, retrying from Go because curl does not re-evaluate the resume
offset on its internal retries. Resume against the HF Xet CDN was
verified by killing a transfer mid-flight and confirming the next
invocation appended (114 MB -> 235 MB, GGUF magic intact).

Also parameterize the suite timeout (BACKEND_TEST_TIMEOUT, default
30m) and raise it to 120m for the vibevoice transcription wrapper: a
10 GB download plus 25 specs does not fit in 30m even on a good day,
and the job-level GHA timeout there is already 150m.


Assisted-by: Claude:claude-fable-5 [Claude Code]

Signed-off-by: Ettore Di Giacinto <[email protected]>
Co-authored-by: Ettore Di Giacinto <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants