Skip to content

feat(omnivoice-cpp): add OmniVoice TTS backend (file + streaming, voice cloning + voice design)#10310

Merged
mudler merged 12 commits into
masterfrom
feat/omnivoice-cpp-backend
Jun 13, 2026
Merged

feat(omnivoice-cpp): add OmniVoice TTS backend (file + streaming, voice cloning + voice design)#10310
mudler merged 12 commits into
masterfrom
feat/omnivoice-cpp-backend

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

What

Adds omnivoice-cpp, a Go + purego TTS backend wrapping ServeurpersoCom/omnivoice.cpp (GGML-based; Qwen3 612M backbone + RVQ audio codec, 24 kHz mono, 646 languages). Mirrors the structure of the existing qwen3-tts-cpp / crispasr backends.

Capabilities wired:

  • File TTS (TTS) and streaming TTS (TTSStream) - OmniVoice's on_chunk callback is bridged through gRPC TTSStream. The backend emits a self-describing WAV (header chunk first, then PCM16LE chunks), since the gRPC chan []byte path never sets Message.
  • Voice cloning - voice = reference audio path (decoded to float in Go and passed as ref_audio_24k), params.ref_text = transcript.
  • Voice design - instructions = attribute keywords (gender, age, pitch, style, volume, emotion).

How it's structured

  • Thin C wrapper (cpp/gomnivoicecpp.cpp) exposing flat omni_* symbols over OmniVoice's ov_* C ABI (no by-value structs across the purego boundary). Links the upstream omnivoice-core static lib.
  • Pure-Go helpers (language normalization, option parsing, WAV framing) with Ginkgo/Gomega unit tests that run without a model.
  • Load/TTS/TTSStream over base.SingleThread; a single shared purego.NewCallback (serialized by SingleThread) avoids leaking a callback per request. The streaming callback fires synchronously on the calling thread (verified against upstream pipeline-tts.cpp), so the purego path is safe.

Registration surfaces

  • backend/index.yaml: meta &omnivoicecpp (capabilities map, no uri) + 20 image entries (production + development), tag-suffixes aligned with the CI matrix.
  • .github/backend-matrix.yml: 12 build variants (cpu/cuda12/cuda13/intel-sycl/metal/rocm/vulkan/l4t), all Dockerfile.golang. bump_deps.yaml pins OMNIVOICE_VERSION.
  • Root Makefile wiring; pref-only importer entry (core/http/endpoints/localai/backend.go) + a guard test confirming a bare GGUF URI never auto-resolves to omnivoice-cpp.
  • Gallery: omnivoice-cpp (Q8_0 default, ~945 MB) and omnivoice-cpp-hq (BF16), real sha256 from the HF LFS pointers.
  • Docs: features/text-to-audio.md section + reference/compatibility-table.md row.

Testing

  • Unit specs (12) pass without a model; make lint reports 0 issues; go vet clean.
  • e2e verified on the real model (CPU, Q4_K_M): model loads, TTS writes a valid 24 kHz WAV, and TTSStream emits a WAV header chunk followed by PCM chunks during MaskGIT decode (rc=0). Env-gated (OMNIVOICE_MODEL/OMNIVOICE_CODEC) so CI unit runs are unaffected.

Notes

  • Voice-design streaming (no reference) runs ~6-12 dB quieter than the buffered path (upstream skips peak normalization when the global peak is unknowable before the last sample). Acceptable for v1.
  • The omnivoice-codec RVQ pre-compute (.rvq) path is deferred; reference audio is passed as WAV per call.

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

mudler added 10 commits June 13, 2026 16:09
…ice ov_* ABI

Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
…s with tests

Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

// writeWAV24k writes samples as a finalized 24 kHz mono 16-bit WAV at dst.
func writeWAV24k(dst string, samples []float32) error {
f, err := os.Create(dst)
// float32 slice in [-1,1] for use as reference audio. OmniVoice expects 24 kHz;
// callers should supply 24 kHz reference clips.
func readWAVAsFloat(path string) ([]float32, error) {
f, err := os.Open(path)
}
var refPtr unsafe.Pointer
if len(ref) > 0 {
refPtr = unsafe.Pointer(&ref[0])

var n int32
ptr := CppTTS(req.Text, lang, instruct, refPtr, len(ref), refText, seed,
boolToInt(o.opts.denoise), unsafe.Pointer(&n))
return fmt.Errorf("omnivoice: synthesis failed")
}
defer CppPCMFree(ptr)
src := unsafe.Slice((*float32)(unsafe.Pointer(ptr)), int(n)) //nolint:govet // C-allocated PCM, copied out before free
return fmt.Errorf("omnivoice: synthesis failed")
}
defer CppPCMFree(ptr)
src := unsafe.Slice((*float32)(unsafe.Pointer(ptr)), int(n)) //nolint:govet // C-allocated PCM, copied out before free
if nSamples <= 0 || samples == nil || streamChan == nil {
return 1 // continue
}
src := unsafe.Slice(samples, int(nSamples))
}
var refPtr unsafe.Pointer
if len(ref) > 0 {
refPtr = unsafe.Pointer(&ref[0])
mudler added 2 commits June 13, 2026 17:02
… cloning reference

The model config tts.audio_path (ModelOptions.AudioPath) and tts.voice now
provide a default voice-cloning reference used when a request omits Voice, so a
cloned voice can be pinned in the model YAML instead of passed per request. A
per-request voice still overrides. Paths resolve relative to the model dir.

Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Mirrors the whisper/vibevoice convention: a -development meta aggregating the
master-tagged image variants (the production meta and per-variant prod+dev image
entries already existed; only the development meta aggregator was missing).

Assisted-by: claude:claude-opus-4-8 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
@mudler
mudler merged commit 0854932 into master Jun 13, 2026
69 of 70 checks passed
@mudler
mudler deleted the feat/omnivoice-cpp-backend branch June 13, 2026 19:28
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