Repository navigation
QVAC-27586 feat[api]: expose the tts-ggml controls and stats the SDK dropped - #5554
Conversation
…dropped Chatterbox sampling, prosody, length and voiceDir controls; Supertonic native chunk streaming, external voices (voiceJsonSrc / noiseNpySrc), prewarmText and vulkanDevice; CosyVoice3 per-request instruct plus flowCutPrompt / vulkanDevice; Audio8 per-request referenceAudioSrc / referenceText; firstAudioMs and the engines' stage timings and counters in the stats. qvac serve forwards OpenAI `instructions` to CosyVoice3 as `instruct`. Contracts (TS, Python, Kotlin) regenerated. Pocket is left for its own PR.
Review StatusCurrent Status: ❌ PENDING Pending reviews: Needs 1 Management or Team Lead, and 1 more from Management, Team Lead, or Member. |
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 found. SummaryExposes more speech controls and engine statistics through the SDK.
The earlier download-cancellation, cached-reference cancellation, and stats-table findings are addressed in the current code. No new actionable issue was established. Synthesis with real models was not run during this review.
|
… and make the TTS stats table exhaustive
…-tts-ggml-gaps # Conflicts: # packages/inference/src/plugins/builtin/tts-ggml/plugin.ts # packages/sdk-kotlin/src/commonMain/kotlin/io/tether/qvac/sdk/generated/schema/SchemaTypes.kt
There was a problem hiding this comment.
the description is stale after the main merge. it says the SDK pins ^0.14.0 and that the parler Core ML sidecar stats wait for the next pin bump, but main now pins ^0.15.0 and those stats are in it. still fine to leave them out, since the SDK doesn't download parler sidecars or expose coremlDecoder. just update the reason.
…-tts-ggml-gaps # Conflicts: # packages/sdk-kotlin/src/commonMain/kotlin/io/tether/qvac/sdk/generated/schema/SchemaTypes.kt
…nAI instructions qvac serve forwarded `instructions` as a per-request `instruct` to every CosyVoice3 alias, which replaces the conditioning the operator configured. It is now forwarded only when the alias sets no instruct, emotion or non-moderate pace; otherwise it stays an ignored param. Also document that a request-level referenceAudioSrc from an uncached remote source downloads inside the request and holds the model, and point callers at downloadAsset(). Contracts regenerated (description text and the Kotlin contract after the main merge).
|
@opaninakuffo on the stale description: updated. It now says the SDK pins |
…ording resolved anyway resolveVoiceJobOptions returned null only when the source resolution threw after an abort. A cached referenceAudioSrc resolves through a file check no signal interrupts, so a cancel landing during it left the resolution successful: the ops went on to start the engine after their once-only model.cancel() had already fired on a model with no job. The signal is now checked after the resolution too. Tests cancel while a cached reference resolves, for textToSpeech and textToSpeechStream.
…rence changes SDK Pod Checks selects packages by changed path, and the cli entry had no depends_on, so an SDK type change never ran the CLI typecheck: #5604 added fields to AudioGenResult and main stayed green while every later PR that touched the CLI failed. The cli entry now depends on packages/sdk and packages/inference. A policy test pins the dependency and the selection rule; the CLI README says when the CLI checks run.
…aps' into feat/QVAC-27586-sdk-tts-ggml-gaps
…-tts-ggml-gaps # Conflicts: # packages/inference/src/utils/tts-stats.ts # packages/sdk-kotlin/src/commonMain/kotlin/io/tether/qvac/sdk/generated/schema/SchemaTypes.kt # packages/sdk-python/src/tetherto/qvac_sdk/_generated/models/_internal.py
🎯 What problem does this PR solve?
@qvac/tts-ggml0.10+ (the SDK pins^0.15.0) ships engine controls and runtime statistics that the inference schema and the tts-ggml plugin never accepted or forwarded, so SDK users could not reach them while the addon's own API could:temperature,topK,topP,repeatPenalty), the multilingual prosody controls (exaggeration,cfgWeight,minP), length / streaming-cost controls (nPredict,maxSentenceChars,crossfadeMs,batchCfmSteps,streamLeftContextTokens) and pre-baked voice profiles (voiceDir).streamChunkTokens,streamFirstChunkTokens,streamChunkTolerancePct,streamMinChunkTokens), external voices (voiceJsonPath) and the deployment controlsprewarmText,noiseNpyPath,vulkanDevice. The SDK only hadvulkanCacheDir.instructwas load-time only although the addon takes it per call;flowCutPromptandvulkanDevicewere absent.referenceAudio/referenceTextper call.firstAudioMs, the Chatterboxt3Ms/s3genMs/t3Tokens, the CosyVoice3 stage timings and work counters, the Audio8 stage timings andstageTotalMs.📝 How does it solve it?
packages/inference/src/schemas/text-to-speech.ts: every row above becomes a described schema key on the engine arm the addon's option validator (lib/optionValidation.ts,assertEngineScopedOptions) scopes it to, with the addon's ranges (int32 knobs,topPin (0, 1],minPin [0, 1],vulkanDevice >= -1). Supertonic moves to the shape + load-refinement pattern Parler and CosyVoice3 use, so its load schema can reject native chunk streaming together with either LavaSR stage, as the addon does (assertChunkStreamingSupport). Supertonic's voice JSON and noise tensor are single files, so they arevoiceJsonSrc/noiseNpySrcmodel sources resolved like the LavaSR GGUFs.Per-request voice controls (
instruct,referenceAudioSrc,referenceText) are added to both thetextToSpeechandtextToSpeechStreamrequest schemas with the engine-independent rules (a request-level recording needs its transcript;instructconflicts withemotion/ a non-moderatepace). The newops/voice-options.tsdoes the engine check once the model is known (CosyVoice3 forinstruct, Audio8 for the reference pair), resolvesreferenceAudioSrcthrough the same source resolution as the load-time field, and both ops forward the result onrun(),runStream()andrunStreaming(). The engine check runs before the op queues for the model; the recording is resolved inside the op's request context with that context as the download's cancellation binding, socancel({ modelId })andcancel({ requestId })stop an uncached remote recording mid-download instead of being followed by synthesis. The duplex request builders inpackages/inference/src/apiandpackages/sdk/src/client/apiforward the three fields (they whitelist).plugin.tsforwards the new constructor options;resolveSupertonicConfigresolves the two new sources intovoiceJsonPath/noiseNpyPathartifacts.toAddonInstructmoves toops/voice-options.tsso the load and request paths render the structured control the same way.Stats:
ttsStatsSchemaand the addon stats type gain the 28 fields;collectTtsStatsis table-driven over asatisfies (keyof AddonTtsStats & keyof TtsStats)[]list, and an exhaustiveness type requires every key of either type (apart from the renamedaudioDurationMs/audioDurationpair) to be in that list, so a field added to one side only, or to both without a table entry, fails to compile naming the field instead of being dropped.qvac serve:/v1/audio/speechforwards OpenAI'sinstructionsas the per-requestinstruct(a raw instruction string) to a CosyVoice3 alias that configures no conditioning of its own. A per-requestinstructreplaces the alias's conditioning, so an alias whose config setsinstruct,emotionor a non-moderatepacekeeps the operator's choice andinstructionsstays an ignored param there, as on the other engines; docs and the OpenAPI description updated. The Audio8 request-level reference pair has no OpenAI analogue (a server-side recording), so it is library-only; the load-time options reachqvac servethrough the model config as before.Docs (
(v0.23)/ai-capabilities/text-to-speech.mdx): Chatterbox, Supertonic, CosyVoice3 and Audio8 sections, the stats table and the LavaSR callout.Contracts regenerated:
packages/sdk/contract/*.json, the Python models, the KotlinSchemaTypes.kt.CI: two fixes for breakage this PR ran into rather than caused. The fake
AudioGenResultinpackages/cli/test/e2e/default/music.test.tsgets thechunkStreamandencodedfields QVAC-27588 feat[api]: add chunkStream, encoded output and MiniMax Core ML to audioGen #5604 added. And theclientry of.github/sdk-pod-checks.jsonnow hasdepends_on: [packages/sdk, packages/inference]: SDK Pod Checks selects packages by changed path, so an SDK type change never ran the CLI typecheck and could break main unnoticed, as QVAC-27588 feat[api]: add chunkStream, encoded output and MiniMax Core ML to audioGen #5604 did. A policy test in.github/scripts/test/ci-trust-policy.test.mjspins the dependency and the selection rule, and the CLI README says when the CLI checks run.Not in this PR
pocketengine is a separate engine arm (its own model files and lifecycle); exposing it needs a newttsEnginearm, model-source resolution for four companion files, plugin dispatch, CLI preset and docs. The earlier attempt was QVAC-24306 feat[api]: expose Pocket TTS through inference, SDK and CLI #4698 (closed). It is better as its own PR.decoderSidecarLoaded,dacSidecarLoaded,decoderOnCoreml,dacOnCoreml) are in the pinned 0.15 addon, but the SDK neither downloads Parler Core ML sidecars nor exposescoremlDecoder, so no SDK load could report them. They belong with the change that adds Parler Core ML to the SDK.referenceAudioSrcstays a model source (same spelling and resolver as the load-time field). A remote source that is not cached downloads inside the request, holding the model until it finishes; it is cancellable, and the docs and the schema description tell callers to pre-fetch withdownloadAsset()or pass a local path.streamLeftContextTokensis exposed on Chatterbox only; the addon accepts it on CosyVoice3 but documents it as not yet read by the pinned engine.🧪 How was it tested?
packages/inference:tsc -p tsconfig.build.jsonreports the same 42 pre-existing errors asmain(stale addon installs, none in TTS files);npm run lintandnpx prettier --check .clean;tts-schemas(105 tests),tts-stats,tts-resolve-config, the newtts-voice-options(per-request forwarding onrun()/runStream()/runStreaming(), Audio8 path resolution, engine rejections, and a model-scoped and a request-scoped cancel each stopping a reference download parked on a stalled loopback server, with no engine call and the model slot released; both fail against the pre-fix ops),tts-audio8-stream,tts-parler-options,tts-cancel,tts-moss,tts-moss-stream,tts-multicast,tts-side-channel,config-description-guard,registry-schema-coverageall pass.packages/sdk: lint,tsc --noEmit(both tsconfigs),contract-export/contract-schemas/plugin-contracttests,text-to-speech-stream-client(new per-request rule cases).packages/cli:test/unit/extensions/openai/audio.test.ts(newspeechInstructForcases); typecheck is CI-only locally.packages/sdk-python:generate.pythen--check,pytest tests/test_api.py.packages/sdk-kotlin:generate-contract.pythen--check,python3 -m unittest discover -s scripts -p 'test_*.py',check-wire-literals.py.tts-voice-optionscancels a request while a cachedreferenceAudioSrcresolves, throughtextToSpeechandtextToSpeechStream; both cases fail without the post-resolution abort check. Workflow policy tests: thepolicy-testsjob's 29 files pass locally (559 tests), and the workflow's ownjqselection pickssdk,inferenceandclifor a change underpackages/inference._setup.settings/ the job object instead.🔌 API Changes
qvac serve:POST /v1/audio/speechforwardsinstructionsasinstructto a CosyVoice3 alias without configured conditioning (instruct,emotionor a non-moderatepace); otherwise it stays an ignored param.