Repository navigation
QVAC-27970 feat[api]: expose the tts-ggml 0.15 backend, Core ML, sampling and replay controls - #5684
Conversation
…ling and replay controls
The SDK pins @qvac/tts-ggml ^0.15.0 but its schema and plugin never
accepted the addon's explicit backend selector, the Fabric controls
(Supertonic precision / environment / in-memory voice tensors, Parler
Core ML sidecars, CosyVoice3 greedy decoding and forced trajectories,
MOSS text and audio sampling with context / generation budgets), the
LavaSR Core ML models, or the CosyVoice3 speechTokenIds the engine hands
back, and dropped the four Parler Core ML stats.
- `backend` on every engine arm, forwarded as config.backend and on the
fit request; WebGPU refused where the native library has no support.
- Supertonic: precision, f16Attn, f16Weights, kvAttnType,
f16WeightsDenyList, vulkanEnvOverrides, voiceStyleTtl / voiceStyleDp
(paired); precision and f16Weights reach the fitter.
- Parler: coremlDecoder, coremlComputeUnits, coremlStrict (+ fit) and
the decoderSidecarLoaded / dacSidecarLoaded / decoderOnCoreml /
dacOnCoreml stats.
- CosyVoice3: greedy, forceSpeechTokens; speechTokenIds on both
response frames, forwarded by the ops (the empty final chunk that
carries them is no longer skipped), concatenated on the result.
- MOSS: nCtx, maxNewTokens (bounding durationTokens), textTemperature /
textTopP / textTopK, audioTemperature / audioTopP / audioTopK /
audioRepetitionPenalty; nCtx and maxNewTokens reach the fitter.
- LavaSR: lavasr{Enhancer,Denoiser}Coreml{ModelPath,ComputeUnits} on the
four engines that run LavaSR, resolved into the addon's enhancer /
denoiser options.
- qvac serve: the sample-rate fallback table knows every tts-ggml engine.
- Docs, contracts (schema.json, Python models, Kotlin SchemaTypes.kt).
Pocket TTS stays out: it is a separate engine arm with no registry model
and belongs in its own PR.
Review StatusCurrent Status: ✅ APPROVED |
License compliance — cleanNo new dependency license findings in this PR. Warn-only (shadow) mode — this check does not block merges yet. Updated automatically by the canonical license compliance workflow. NOTICE presence (advisory)Missing NOTICE (advisory, does not block):
|
[Medium impact] The PR appears safe to merge; no new actionable issue was established. SummaryExposes the tts-ggml 0.15 controls through inference and the SDK.
Both earlier findings are addressed: replay docs now limit replay to one native synthesis, and both clients append tokens into one owned array. The supplied threads have no comment numbers, so they have no numbered previous-finding entries.
|
… append tokens in place Review follow-ups on the CosyVoice3 speech-token surface: - `forceSpeechTokens` is fixed at load, so a sentence stream's concatenated `speechTokenIds` cannot be replayed as one run: every sentence would start its own synthesis from the same joined array. The result, frame, chunkUpdate and `forceSpeechTokens` descriptions and the docs page now scope replay to one native synthesis (batch run, native chunk stream, or one sentence's array) and say so; contracts and Python models regenerated. - Both TTS side channels appended each token-bearing frame by spreading every token collected so far, quadratic over a long sentence stream. They now push into one owned array; the frames' own arrays are left untouched (tests).
GustavoA1604
left a comment
There was a problem hiding this comment.
[medium] packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/_internal.py — line 26738 (speech_token_ids: Annotated[)
Responses expose list[SpeechTokenId], but force_speech_tokens expects list[ForceSpeechToken]. Passing response.speech_token_ids directly into the CosyVoice3 replay configuration raises a Pydantic ValidationError for every token. Use a compatible scalar representation for both fields and add a Python regression test that feeds returned tokens into the replay configuration.
…-tts-ggml-gaps # Conflicts: # packages/sdk-kotlin/src/commonMain/kotlin/io/tether/qvac/sdk/generated/schema/SchemaTypes.kt
…TokenIds and forceSpeechTokens The response arrays and the CosyVoice3 load option carried differently constrained integer items (safe-integer vs int32), so the generated Python client wrapped them in two unrelated RootModel classes (`SpeechTokenId`, `ForceSpeechToken`) and feeding a returned trajectory into the replay configuration failed validation on every token. Both arrays now take one titled int32 scalar, `TtsSpeechTokenId`, defined once in the TTS schema. datamodel-code-generator emits a single shared class for it, so `response.speech_token_ids` is accepted by `forceSpeechTokens` as is; Kotlin stays `List<Long>` and TypeScript `number[]` on both sides. Contracts, Python models regenerated (Kotlin types unchanged). Adds `tests/test_tts_speech_tokens.py`: a returned trajectory from both response models feeds the replay config unchanged, plain ints still work, and both fields share one item annotation.
|
Thanks, valid: the response arrays carried safe-integer items while Fixed at the schema source in 7b37565: both arrays now take one titled int32 scalar, Regression test: The branch also carries the |
🎯 What problem does this PR solve?
@qvac/tts-ggml ^0.15.0, but the inference schema and the tts-ggml plugin never accepted or forwarded what the 0.15 addon added on top of what QVAC-27586 feat[api]: expose the tts-ggml controls and stats the SDK dropped #5554 exposed, so SDK users could not reach it while the addon's own API could:config.backend:auto,cpu,opencl,webgpu,MTL0,CUDA0, …), also taken byassessFit.precision, thef16Attn/f16Weights/kvAttnTypeprecision overrides,f16WeightsDenyList,vulkanEnvOverrides, and in-memory voice tensors (voiceStyleTtl/voiceStyleDp).coremlDecoder,coremlComputeUnits,coremlStrict, and the four stats they produce (decoderSidecarLoaded,dacSidecarLoaded,decoderOnCoreml,dacOnCoreml), whichcollectTtsStatsdropped.greedydecoding,forceSpeechTokens(replay a trajectory), and thespeechTokenIdsthe engine hands back on its output, which the ops dropped; under native chunk streaming they arrive on an empty final chunk the streaming paths skipped as an end marker.textTemperature/textTopP/textTopK,audioTemperature/audioTopP/audioTopK/audioRepetitionPenalty) and thenCtx/maxNewTokensbudgets;durationTokenswas bounded by the default budget only.enhancer/denoisercoremlModelPath+coremlComputeUnits).instructand Audio8 reference voice landed in QVAC-27586 feat[api]: expose the tts-ggml controls and stats the SDK dropped #5554; Audio8 incremental output,chunkStreamand the binary PCM transport in QVAC-27326 feat[api]: add chunked Int16Array output to textToSpeech #5348 / QVAC-27325 feat[api]: replace JSON PCM transport with binary audio transport #5380; the addon-side fixes (unconditioned MOSS output, backend fields on streamed stats, hardware-acceleration memory) arrive through the existing pin.📝 How does it solve it?
packages/inference/src/schemas/text-to-speech.ts: each row above is a described schema key on the engine arm the addon scopes it to (lib/fabricOptions.jsENGINE_OPTION_KEYS,lib/engines/*), with the addon's ranges (int32 knobs,topPin (0, 1], positive counts).backendrides the shared backend shape onto all eight arms; awebgpuselector is refused on Chatterbox and the MOSS engines, which the native library has no WebGPU support for (validateBackendOption). Supertonic's voice tensors come as a pair; MOSSdurationTokensmust fitmaxNewTokens − 33(2015 by default, as before); the LavaSR Core ML path and policy come together and need the stage's GGUF source (validateLavaSRCoreML). Three new vocabularies (TTS_PARLER_COREML_COMPUTE_UNITS,TTS_LAVASR_COREML_COMPUTE_UNITS,TTS_SUPERTONIC_PRECISIONS) are exported from the SDK root and registered for codegen.plugin.tsforwards the new constructor options; the LavaSR Core ML fields are load-only (compiled.mlmodelcdirectories on the host, not sources), soresolveConfigmoves them into artifacts andcreateModelbuilds the addon'senhancer/denoiseroption objects beside the GGUFfiles.fit.tsforwardsbackend, Supertonicprecision/f16Weights, ParlercoremlDecoderand MOSSnCtx/maxNewTokens, so the projection matches the load.speechTokenIds: added to both response schemas and toTtsOutputChunk/TtsOpYield/chunkMetadata/ttsFrameMetadata. The batch paths collect it with the PCM; the chunked paths keep an empty chunk that carries it instead of skipping it as an end marker (Audio8's empty markers, which carry nothing, are still skipped). The SDK client and the in-process@qvac/inferenceclient expose it asresult.speechTokenIds(the arrays of a run concatenated, one per native synthesis) and on eachchunkUpdatesentry.ttsStatsSchema, the addon stats type and the table-drivenTTS_FORWARDED_STATSgain the four Parler fields (the exhaustiveness type required all three).backendIddocuments5= WebGPU.qvac serve/qvac configure: the new load keys reach both through the sharedmodelConfigschema with no CLI code.speechTokenIdshas no OpenAI analogue, so it is library-only. The route's sample-rate fallback table (used only when a run produced no audio) now knows every tts-ggml engine instead of Chatterbox and Supertonic alone._unpublished/sdk/next/ai-capabilities/text-to-speech.mdx(the development line) is first brought in line with the published(v0.23)page, which QVAC-27326 feat[api]: add chunked Int16Array output to textToSpeech #5348 / QVAC-27329 fix[api]: keep backend fields on streamed TTS stats and put the audiogen GPU fallback code on the wire #5384 / QVAC-27586 feat[api]: expose the tts-ggml controls and stats the SDK dropped #5554 had edited directly, then gains a Backend selection section, the Supertonic, Parler, CosyVoice3, MOSS and LavaSR additions, and the new stats rows.packages/sdk/contract/schema.json, the Python models, the KotlinSchemaTypes.kt.Not in this PR
textfit workload andmemoryBudgetBytes; the addon's own docs say SDK integration follows separately, QVAC-27586 feat[api]: expose the tts-ggml controls and stats the SDK dropped #5554 deferred it for the same reason, QVAC-24306 feat[api]: expose Pocket TTS through inference, SDK and CLI #4698 (the earlier attempt) is closed and no Pocket SDK PR is open. The CLI'sqvac configuretemplates are keyed byTtsEngineand need a registry model per engine, which Pocket does not have yet. It stays a PR of its own.warmUp(text)is a new RPC method rather than a pass-through option; the load-timeprewarmTextalready warms the engine up, so the callable form is left out (noted in the docs).TTSGgml.getVoiceControls()(not in the epic).🧪 How was it tested?
packages/inference:tsc -p tsconfig.build.jsonreports the same 13 pre-existing environmental errors asmain(stale addon installs, none in TTS files);lunteandprettier --checkclean on the touched files. Under Bare:tts-schemas(119 tests: backend on every arm and the WebGPU rule, Supertonic Fabric controls and the voice-tensor pair, Parler Core ML, CosyVoice3greedy/forceSpeechTokens, MOSS samplers and thedurationTokens/maxNewTokensbudget, LavaSR Core ML pairing rules,speechTokenIdson both response schemas, Parler stats),tts-resolve-config(38: every forwarded option read back from the addon's_setup, LavaSR Core ML fields stripped into artifacts and rebuilt asenhancer/denoiseroptions),tts-fit-request(12),tts-stats(13), the newtts-speech-token-ids(8: batch collect, batch-over-native-stream, and the plain / sentence / duplex paths forwarding the empty token chunk while still skipping Audio8's empty end markers),tts-side-channel(6), plustts-voice-options,tts-audio8-stream,tts-chunk-stream,tts-chunk-stream-public,tts-moss,tts-moss-stream,tts-handler-pcm,tts-cancel,tts-multicast,tts-parler-options,config-description-guard,registry-schema-coverageall pass.packages/sdk:tsc --noEmiton sources clean, on tests only the known localbare-linktyping gap;lunteandprettierclean;tts-constants-exports(new vocabularies at the root and in the constants registry), the newtts-speech-token-ids(batch, plain stream, sentence streamchunkUpdates, engines without tokens),text-to-speech-stream-client,tts-chunk-stream,tts-batch-terminal,tts-chunk-stream-public,tts-multicast,tts-pcm-wire,contract-schemas,contract-export,plugin-contractpass;export-contract.ts --checkpasses.packages/cli:test/unit/extensions/openai/audio.test.ts(41, with the new per-engine sample-rate case);lunteandprettierclean; typecheck is CI-only locally.packages/sdk-python:generate.pythen--check;pytest testspasses 512 tests, the 23 that spawn a real worker fail locally on the known Bare 1.28.1 vs>=1.32.0engine gap (CI covers them);mypy -p tetherto.qvac_sdkreports the same two pre-existing errors asmain(sdcppmode, vector-indexstorage).packages/sdk-kotlin:generate-contract.pythen--check, the generator'sunittestsuite (16) andcheck-wire-literals.pypass;vulkanEnvOverridesrenders asMap<String, String>, the token arrays asList<Long>._setupor the forwarded job / frame instead.🔌 API Changes
New root exports:
TTS_PARLER_COREML_COMPUTE_UNITS,TTS_LAVASR_COREML_COMPUTE_UNITS,TTS_SUPERTONIC_PRECISIONS.TextToSpeechStreamResultandTtsSentenceChunkUpdategainspeechTokenIds; both response frames carryspeechTokenIds.Replay is scoped to one native synthesis:
forceSpeechTokensis fixed at load, so a sentence stream's concatenatedspeechTokenIdscannot be replayed as one (replay it sentence by sentence fromchunkUpdates). The token item is one titled int32 scalar (TtsSpeechTokenId) on both the response andforceSpeechTokens, so a returned list feeds the replay config unchanged in every generated client: Pythonlist[TtsSpeechTokenId]on both sides (one shared class), KotlinList<Long>, TypeScriptnumber[].