@qvac/tts-ggml 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,8 +29,27 @@ The LM, flow frontend/decoder, F0 predictor and HiFT vocoder use resident
29
29
  sidecars when present and fall back to ggml when unavailable. HiFT exports
30
30
  use FP32 for waveform parity. Core ML chooses operation placement, so a
31
31
  sidecar does not guarantee ANE execution or a speed improvement.
32
- When a sidecar is present, `assessFit` returns an error with reason
33
- `coreml-memory-estimate-unavailable`; its memory usage is not projected.
32
+ When a sidecar is present, `assessFit` answers `unknown`, since the sidecar's
33
+ memory usage is not projected.
34
+
35
+
36
+ ### Chatterbox Core ML sidecars
37
+
38
+ Apple builds support optional Chatterbox Turbo and Multilingual Core ML
39
+ sidecars for the S3Gen encoder, CFM estimator and HiFT vocoder through the
40
+ native `speech-cpp` 2026-10-30 dependency. Export and placement details are in
41
+ the [Chatterbox Core ML guide](https://github.com/tetherto/qvac-fabric-speech.cpp/blob/9ed06a668923b1ea3f68ad28614228aa1156223e/engines/tts/docs/chatterbox.md#core-ml-sidecars).
42
+
43
+ The three stages run on the Neural Engine when their sidecars sit next to the
44
+ S3Gen GGUF and fall back to ggml when unavailable; T3 keeps the GPU. Set
45
+ `maxSentenceChars` (the reference CLI uses 180) so S3Gen vocodes each sentence
46
+ on the Neural Engine while T3 decodes the next one: on an Apple M2 a
47
+ three-sentence prompt then synthesizes in 31% (Turbo) and 33% (Multilingual)
48
+ less time than on Metal. Without a split there is nothing to overlap, so a call
49
+ gains only the S3Gen time. The first use of each sidecar function by an
50
+ executable compiles it for the Neural Engine (3 to 26 s per function on an M2;
51
+ the OS caches the result). When a sidecar is present, `assessFit` answers
52
+ `unknown`, since the sidecar's memory usage is not projected.
34
53
 
35
54
 
36
55
  Runs in-process with a persistent native engine — the GGUFs, the S3Gen
@@ -67,6 +86,7 @@ and registry prerequisites described under [MOSS](#moss).
67
86
  `tts-cpp` selects the GPU backend per its per-vendor allowlist (see
68
87
  [Backends & GPU acceleration](#backends--gpu-acceleration)).
69
88
  - **Apple Core ML sidecars** for the Supertonic vocoder, the Audio8 codec,
89
+ the Chatterbox S3Gen encoder, CFM and HiFT,
70
90
  and one stage each of MOSS, MOSS-SoundEffect, and MOSS-Speech on macOS /
71
91
  iOS, picked up when staged next to the model (see
72
92
  [Core ML sidecars on Apple](#core-ml-sidecars-on-apple)).
@@ -204,10 +224,13 @@ under `models/`:
204
224
  # Chatterbox turbo (English)
205
225
  chatterbox-t3-turbo.gguf (~742 MB) — T3 GPT-2 Medium + BPE + VoiceEncoder
206
226
  chatterbox-s3gen.gguf (~1.0 GB) — S3Gen encoder/CFM + HiFT + CAMPPlus + S3TokenizerV2
227
+ chatterbox-s3gen-{encoder,cfm,hift}.mlmodelc/ (optional, macOS / iOS: Apple Core ML
228
+ sidecars for the S3Gen stages; see Core ML sidecars on Apple)
207
229
 
208
230
  # Chatterbox multilingual (en/es/fr/de/pt/it/zh/ja/ko/...)
209
231
  chatterbox-t3-mtl.gguf (~1.0 GB)
210
232
  chatterbox-s3gen-mtl.gguf (~1.0 GB)
233
+ chatterbox-s3gen-mtl-{encoder,cfm,hift}.mlmodelc/ (optional, macOS / iOS)
211
234
 
212
235
  # Supertonic 3 (Supertone/supertonic-3; 31 languages) — preferred Supertonic
213
236
  # checkpoint; published per quant tier (auto-detected from modelDir)
@@ -577,7 +600,7 @@ accepting a new one, so changing voices means constructing a new instance.
577
600
 
578
601
  Opt-in neural post-processing that bandwidth-extends the synthesized audio to
579
602
  **48 kHz** with a synthesised high band, using the LavaSR Vocos enhancer
580
- (ConvNeXt backbone + ISTFT spec head) converted to a single GGUF. It follows the
603
+ (ConvNeXt backbone + ISTFT spec head) converted to a single GGUF. Without an explicit Core ML selection, it follows the
581
604
  engine's GPU intent: with `config.useGPU: true` (or `nGpuLayers`) the enhancer
582
605
  runs on the GPU (Vulkan on Linux/Windows, Metal on macOS/iOS) and falls back to
583
606
  CPU otherwise. It is fully backward compatible — provide no enhancer GGUF and
@@ -698,8 +721,59 @@ Notes:
698
721
  (scalar CPU port, validated bit-close to the ONNX reference); it requires a
699
722
  `tts-cpp` build that includes that port — see the pinned version in `vcpkg.json`.
700
723
 
724
+
725
+ ### LavaSR Core ML
726
+
727
+ On Apple platforms, either stage can use a compiled Core ML model explicitly.
728
+ Provide `coremlModelPath` and `coremlComputeUnits` together inside `enhancer`
729
+ or `denoiser`, alongside the corresponding GGUF path (or its `files` alias).
730
+
731
+ ```js
732
+ const model = new TTSGgml({
733
+ engine: TTSGgml.ENGINE_SUPERTONIC,
734
+ files: { supertonicModel: 'models/supertonic.gguf' },
735
+ enhancer: {
736
+ type: 'lavasr',
737
+ enhancerPath: 'models/lavasr-enhancer.gguf',
738
+ coremlModelPath: 'models/enhancer.mlmodelc',
739
+ coremlComputeUnits: 'CPU_AND_NE'
740
+ },
741
+ denoiser: {
742
+ type: 'lavasr',
743
+ denoiserPath: 'models/lavasr-denoiser.gguf',
744
+ coremlModelPath: 'models/denoiser.mlmodelc',
745
+ coremlComputeUnits: 'CPU_AND_NE'
746
+ }
747
+ })
748
+ ```
749
+
750
+ The accepted policies are `CPU_ONLY`, `CPU_AND_GPU`, `CPU_AND_NE`, and `ALL`.
751
+ Each stage has its own policy; it does not change the synthesis engine's
752
+ `config.useGPU`. Models stay loaded with the instance. Omitting both options
753
+ keeps the existing ggml/scalar selection. An invalid, missing, incompatible,
754
+ or unsupported explicitly selected Core ML model fails loading; it does not
755
+ fall back to ggml. Existing streaming restrictions still apply.
756
+
757
+ Core ML policies select eligible devices; they do not guarantee that every
758
+ operation runs on ANE or GPU. Stage stats report device `2` for Core ML-managed
759
+ placement and backend id `99` (other), rather than claiming CPU or GPU.
760
+
761
+ Export matching models with the native Fabric
762
+ [`export-lavasr-coreml.py`](https://github.com/tetherto/qvac-fabric-speech.cpp/blob/8d9c6247ac18ff912bb3e1269da5f3fd679e77aa/engines/tts/scripts/export-lavasr-coreml.py)
763
+ script; precision is explicit (`float32` or `float16`). See the
764
+ [native LavaSR guide](https://github.com/tetherto/qvac-fabric-speech.cpp/blob/8d9c6247ac18ff912bb3e1269da5f3fd679e77aa/engines/tts/docs/lavasr.md#core-ml)
765
+ for export and compilation commands. A runnable addon example is
766
+ [`examples/lavasr-coreml.js`](examples/lavasr-coreml.js).
767
+
701
768
  ## Backends & GPU acceleration
702
769
 
770
+ `config.backend` selects the native backend explicitly. Accepted selectors
771
+ include `auto`, `cpu`, `opencl`, `webgpu`, and exact registered
772
+ device names such as `MTL0`, `CUDA0`, `Vulkan0`. Exact device names
773
+ depend on the host. Native selection reports an unavailable explicit backend
774
+ as an error; omitting this option preserves the existing `useGPU` policy.
775
+ The same selector is accepted as top-level `backend` by `assessFit()`.
776
+
703
777
  The addon delegates backend selection to `tts-cpp`'s registry-only
704
778
  init path. At `load()` time the engine walks the ggml-backend registry
705
779
  once and picks the first available accelerator that matches the
@@ -719,11 +793,67 @@ On hosts where more than one backend is usable, `TTS_CPP_GPU_BACKEND`
719
793
  and fails loudly when that backend cannot be resolved; unset (or empty)
720
794
  keeps the automatic preference above.
721
795
 
796
+ ### Optional WebGPU builds
797
+
798
+ Build with `bare-make generate -D TTS_WEBGPU=ON`, followed by
799
+ `bare-make build`, `bare-make install`, and `npm run link:platform` to include
800
+ and load the local Dawn backend.
801
+ WebGPU is exposed only for the native library's established model families:
802
+ Supertonic **3**, Parler, CosyVoice3 and Audio8. Explicit WebGPU requests
803
+ for other families, including Supertonic 1/2, fail before loading weights.
804
+ The manifest pins the compatible Dawn version; default builds stay unchanged.
805
+
806
+ Hexagon is not included in JavaScript package builds. Stats add `backendId: 5`
807
+ for WebGPU. LavaSR's existing `backendDevice: 2` continues to mean Core ML-managed
808
+ placement.
809
+
810
+ ## Native engine controls
811
+
812
+ These options are available at construction and through `reload()`. Leaving
813
+ them unset retains the native engine defaults.
814
+
815
+ | Engine | Options | Meaning |
816
+ | --- | --- | --- |
817
+ | MOSS / TTSD | `nCtx`, `maxNewTokens` | Context and generation budgets; positive integers. Duration validation uses the configured token budget, reserving the native 33 delay/EOS rows. |
818
+ | MOSS / TTSD | `textTemperature`, `textTopP`, `textTopK` | Text-channel sampling. |
819
+ | MOSS / TTSD | `audioTemperature`, `audioTopP`, `audioTopK`, `audioRepetitionPenalty` | Independent audio-channel sampling. |
820
+ | Parler | `coremlDecoder` | Explicit decoder-LM Core ML opt-in. The separate DAC sidecar remains presence-driven. |
821
+ | Parler | `coremlComputeUnits`, `coremlStrict` | Compute policy (`all`, `cpu_and_ane`, `cpu_and_gpu`, `cpu_only`) and optional strict/fallback selection. Omission preserves native/environment policy. |
822
+ | Supertonic | `precision` | `auto`, `f32`, `f16`, `q8_0`. |
823
+ | Supertonic | `f16Attn`, `f16Weights`, `kvAttnType` | Native attention/weight precision overrides; `-1` leaves automatic selection. |
824
+ | Supertonic | `f16WeightsDenyList` | Tensor-name patterns retained at their original precision. |
825
+ | Supertonic | `vulkanEnvOverrides` | String-valued Vulkan environment overrides passed to the engine. Explicit entries take precedence over the `vulkanCacheDir` convenience setting. |
826
+ | Supertonic | `voiceStyleTtl`, `voiceStyleDp` | Paired flattened `number[]` or `Float32Array` voice tensors; the engine validates model-specific sizes. They take precedence over JSON and baked voices. |
827
+ | CosyVoice3 | `greedy`, `forceSpeechTokens` | Greedy LM decoding or an explicit integer speech-token trajectory for replay. An empty trajectory restores generation. |
828
+
829
+ After loading Supertonic, `await model.warmUp(text)` invokes the engine's
830
+ warm-up directly, completes without returning PCM, and leaves the loaded
831
+ instance available for normal synthesis. It shares the instance's exclusive
832
+ execution queue. Other engines reject the method.
833
+
834
+ CosyVoice3 responses expose owned `speechTokenIds` arrays. Batch output
835
+ includes them with PCM; native streaming delivers them on a final
836
+ `isLast: true` empty-PCM chunk. Retain those IDs and pass them to
837
+ `reload({ forceSpeechTokens: ids })` to replay the trajectory. The existing
838
+ numeric `response.stats.speechTokens` remains a count.
839
+
840
+ [`examples/fabric-controls.js`](examples/fabric-controls.js) accepts a JSON
841
+ constructor configuration through `QVAC_TTS_OPTIONS`, optional preflight
842
+ through `QVAC_TTS_FIT`, and `QVAC_TTS_WARM_UP=1` for Supertonic. For example:
843
+
844
+ ```bash
845
+ QVAC_TTS_OPTIONS='{"engine":"pocket","files":{"modelDir":"/models/pocket"},"config":{"useGPU":true}}' \
846
+ npm run example:fabric-controls -- "Speech runs on the selected backend." pocket.wav
847
+ ```
848
+
849
+ The example saves a WAV and prints stats. For CosyVoice3 it also saves
850
+ `<output>.tokens.json` containing the returned speech token IDs.
851
+
722
852
  ### Core ML sidecars on Apple
723
853
 
724
854
  The macOS / iOS prebuilds carry the Apple Core ML (Neural Engine) sidecars
725
- for the Supertonic vocoder, the Audio8 codec, and the MOSS models' fixed-shape
726
- stages. They are presence-driven:
855
+ for the Supertonic vocoder, the Audio8 codec, the Chatterbox S3Gen stages, and
856
+ the MOSS models' fixed-shape stages. They are presence-driven:
727
857
  each stage runs on a compiled `.mlmodelc` found next to its model file
728
858
  (`supertonic3-q8_0.gguf` -> `supertonic3-vocoder.mlmodelc`) and falls back to
729
859
  the ggml graph when it is absent, so a model directory without sidecars
@@ -738,13 +868,20 @@ published model set yet; supply your own to opt in.
738
868
  | MOSS, MOSS-TTSD | `moss-codec-decoder.mlmodelc`, beside the codec decoder GGUF | codec decoder, 25 frames (2 s) per call, keeping its attention cache between calls (macOS 15 / iOS 18) | non-streaming synthesis and streams with `streamChunkTokens` above 25, which ggml decodes faster in one pass | `MOSS_COREML_DISABLE=1` |
739
869
  | MOSS-SoundEffect | `moss-sfx-v2-dit.mlmodelc` and `moss-sfx-v2-vae.mlmodelc` | diffusion transformer, one velocity per call; VAE decoder in 320-frame windows | a call that fails on the sidecar, which retires it | `MOSS_COREML_DISABLE=1` |
740
870
  | MOSS-Speech | `moss-speech-codec-tokenizer.mlmodelc`, beside the codec GGUF | speech tokenizer (Whisper-VQ encoder), one full 30 s segment per call | segments shorter than 30 s, which ggml encodes faster; so most spoken questions | `MOSS_COREML_DISABLE=1` |
741
- | Chatterbox, Parler, CosyVoice3, LavaSR | none | — | always | — |
871
+ | Parler | `<model>-dac.mlmodelc` and optional `<model>-decoder.mlmodelc` (drop the GGUF quant suffix) | DAC; decoder steps when `coremlDecoder: true` (macOS 15 / iOS 18); T5 and prefill stay on ggml | a failed sidecar, unless strict mode rejects it; see [Parler Core ML sidecars](#parler-core-ml-sidecars) | `PARLER_COREML_DISABLE=1` |
872
+ | LavaSR | explicit `enhancer.coremlModelPath` / `denoiser.coremlModelPath` | denoiser and enhancer neural cores | only when Core ML options are absent; explicit load errors fail | omit both Core ML options |
873
+ | Chatterbox Turbo / Multilingual | `<s3gen>-encoder.mlmodelc`, `<s3gen>-cfm.mlmodelc` and `<s3gen>-hift.mlmodelc`, beside the S3Gen GGUF (`chatterbox-s3gen-q8_0.gguf` -> `chatterbox-s3gen-{encoder,cfm,hift}.mlmodelc`) | S3Gen encoder and CFM estimator, one function per width up to 1536 mel frames (voice prompt included); HiFT in 128-frame windows. With `maxSentenceChars` set, each sentence is vocoded while T3 decodes the next; T3 stays on the ggml backend | inputs past 1536 mel frames (encoder and CFM), and a call that fails on a sidecar, which also retires it | `CHATTERBOX_COREML_DISABLE=1` |
874
+ | CosyVoice3 | none | — | always | — |
742
875
 
743
876
  Set the force-ggml variables in the process environment before `load()`.
744
877
  Audio8 reports its codec path in `response.stats`: `codecSidecarLoaded` is 1
745
878
  while the sidecar is attached and `codecOnCoreml` is 1 when that synthesis
746
879
  ran its codec on it (see [Response shape](#response-shape)). The Supertonic
747
- vocoder path is not reported in the stats, and neither are the MOSS paths.
880
+ vocoder path is not reported in the stats, and neither are the Chatterbox and
881
+ MOSS paths. The Chatterbox stages run on the CPU and Neural Engine, keeping the
882
+ GPU for T3, unless `CHATTERBOX_COREML_COMPUTE_UNITS` (`all`, `cpu_and_gpu`,
883
+ `cpu_only`) says otherwise; `CHATTERBOX_COREML_STRICT=1` turns a fallback into
884
+ an error.
748
885
  The MOSS sidecars run on the GPU unless `MOSS_COREML_COMPUTE_UNITS` (`all`,
749
886
  `cpu_and_ane`, `cpu_only`) says otherwise: none of their stages ran faster on
750
887
  the Neural Engine, and the speech tokenizer loses code accuracy there.
@@ -767,12 +904,14 @@ The MOSS sidecars, per stage against Metal with f16 models:
767
904
 
768
905
  Export the sidecars from the model GGUFs with
769
906
  `engines/tts/scripts/export-supertonic-coreml.py`,
770
- `engines/tts/scripts/export-audio8-codec-coreml.py`, and
907
+ `engines/tts/scripts/export-audio8-codec-coreml.py`,
908
+ `engines/tts/scripts/export-chatterbox-coreml.py`, and
771
909
  `engines/tts/scripts/export-moss-{codec,sfx,speech-tokenizer}-coreml.py` from the
772
910
  [`qvac-fabric-speech.cpp`](https://github.com/tetherto/qvac-fabric-speech.cpp)
773
911
  tree at the ref `speech-cpp` pins. Its
774
912
  [Supertonic](https://github.com/tetherto/qvac-fabric-speech.cpp/blob/master/engines/tts/docs/supertonic.md#core-ml-vocoder-sidecar),
775
913
  [Audio8](https://github.com/tetherto/qvac-fabric-speech.cpp/blob/master/engines/tts/docs/audio8.md#core-ml-codec-sidecar),
914
+ [Chatterbox](https://github.com/tetherto/qvac-fabric-speech.cpp/blob/master/engines/tts/docs/chatterbox.md#core-ml-sidecars),
776
915
  and
777
916
  [MOSS](https://github.com/tetherto/qvac-fabric-speech.cpp/blob/master/engines/tts/docs/moss.md#core-ml-codec-decoder)
778
917
  guides cover export, placement, and measurements.
@@ -929,6 +1068,55 @@ Android GPU backend (`useGPU: true` / `nGpuLayers`), including Vulkan on ARM
929
1068
  Mali. Unsupported or unavailable backends fall back to CPU. It emits native
930
1069
  44.1 kHz.
931
1070
 
1071
+ ### Parler Core ML sidecars
1072
+
1073
+ On Apple platforms, Parler can use optional Core ML decoder and DAC sidecars.
1074
+ Set these **top-level constructor options**, or pass them to `reload()`:
1075
+
1076
+ ```js
1077
+ const model = new TTSGgml({
1078
+ files: { parlerModel: './models/parler-mini-v1-q8_0.gguf' },
1079
+ coremlDecoder: true,
1080
+ coremlComputeUnits: 'cpu_and_ane',
1081
+ coremlStrict: false,
1082
+ opts: { stats: true }
1083
+ })
1084
+ await model.load()
1085
+ await model.reload({ coremlDecoder: false })
1086
+ ```
1087
+
1088
+ `coremlDecoder` is a boolean; omitting it keeps the engine's default.
1089
+ `coremlComputeUnits` accepts `all`, `cpu_and_ane`, `cpu_and_gpu`, or `cpu_only`;
1090
+ omitting it inherits the engine's environment/default policy. `coremlStrict`
1091
+ is a boolean: omit it to inherit strict mode, or pass `false` to explicitly
1092
+ permit fallback. These options are Parler-only load settings and are rejected
1093
+ on per-call APIs. Reload options that fail validation are rejected before
1094
+ replacing the current addon.
1095
+ `assessFit()` accepts `coremlDecoder` to match the load's decoder policy.
1096
+
1097
+ Strict mode requires a usable DAC sidecar even when `coremlDecoder` is false;
1098
+ enabling the decoder also requires its sidecar. A build without Core ML fails
1099
+ in strict mode. Combining strict mode with `PARLER_COREML_DISABLE` remains a
1100
+ fatal configuration under the engine's existing policy. On reload, omitted or
1101
+ `undefined` settings retain their current values; an empty `coremlComputeUnits`
1102
+ string is invalid. To return to inherited compute-unit or strict policy,
1103
+ construct a new instance with the corresponding options omitted.
1104
+
1105
+ If the replacement model fails to load, `reload()` rejects and leaves the
1106
+ instance unloaded with the requested settings retained. Recover by reloading
1107
+ with a usable configuration or constructing a new instance.
1108
+
1109
+ With stats enabled, `decoderSidecarLoaded` and `dacSidecarLoaded` report `1`
1110
+ while the corresponding sidecar remains attached. `decoderOnCoreml` and
1111
+ `dacOnCoreml` report `1` only when that synthesis used the corresponding Core ML
1112
+ stage; ggml fallback and mixed Core ML/ggml execution report `0`. Sentence
1113
+ streams report Core ML execution only if every reported sentence used it;
1114
+ sidecar availability retains the last report. These numeric flags describe
1115
+ execution paths; they do not
1116
+ identify whether Core ML scheduled work on the CPU, GPU, or Neural Engine.
1117
+ `backendDevice` / `backendId` continue to identify the ggml backend, so a mixed
1118
+ pipeline does not report all stages as Core ML.
1119
+
932
1120
  ## CosyVoice3 instruct
933
1121
 
934
1122
  Beyond `emotion` / `pace` above, CosyVoice3 accepts a natural-language
@@ -1348,7 +1536,7 @@ instance. Runtime stats add `promptTokens`, `generatedTokens`, `replyTokens`,
1348
1536
  | `cfmSteps` | number | 2 | Chatterbox native-streaming CFM steps: 1 = faster (halved CFM cost) |
1349
1537
  | `batchCfmSteps` | number | engine default | Chatterbox-only batch CFM steps (`0` = 2-step meanflow on Turbo, the model's full schedule on multilingual). The multilingual engine does not floor it, so values well below its schedule under-integrate |
1350
1538
  | `nPredict` | number | 1000 | Chatterbox-only cap on speech tokens per synthesis (25 ≈ 1 s, so ~40 s by default). Longer single calls also need `nCtx` to fit, or use `maxSentenceChars` |
1351
- | `maxSentenceChars` | number | 0 | Chatterbox-only sentence auto-split: T3 + S3Gen run per segment of at most this many bytes (the reference CLI uses 180), so `nPredict` applies per segment |
1539
+ | `maxSentenceChars` | number | 0 | Chatterbox-only sentence auto-split: T3 + S3Gen run per segment of at most this many bytes (the reference CLI uses 180), so `nPredict` applies per segment; with [Chatterbox Core ML sidecars](#chatterbox-core-ml-sidecars), S3Gen vocodes a segment while T3 decodes the next |
1352
1540
  | `crossfadeMs` | number | 30 | Chatterbox-only raised-cosine crossfade between auto-split batch segments; native streaming stays gapless |
1353
1541
  | `repeatPenalty` | number | 1.2 | Chatterbox-only T3 repetition penalty (`1` = off) |
1354
1542
  | `exaggeration` / `cfgWeight` / `minP` | number | 0.5 / 0.5 / 0 | Chatterbox multilingual only (Turbo ignores them): prosody intensity (> 0 more expressive, < 0 flatter), T3 classifier-free guidance scale (`0` = off; `cfgRate` is the separate S3Gen rate), and the min-p sampling threshold |
@@ -1365,7 +1553,7 @@ instance. Runtime stats add `promptTokens`, `generatedTokens`, `replyTokens`,
1365
1553
  | `pitch`, `expressivity`, `noise`, `reverb`, `quality` | string | — | Parler-only template fields (see [Parler descriptions](#parler-descriptions)); invalid values error listing the valid set |
1366
1554
  | `temperature` / `topK` / `topP` | number | engine defaults | Parler, Audio8 and Chatterbox sampling knobs (omit for the engine's own defaults — Parler temp 1.0 / top-k 50, Audio8 temp 0.7 / top-k 50 / top-p 0.9, Chatterbox temp 0.8 / top-k 1000 / top-p 0.95 where temp `0` is greedy and top-k `0` disables the cutoff; `topP` in `(0, 1]`) |
1367
1555
  | `maxFrames` | number | engine max | Parler + Audio8 generation cap in frames (Parler ~86/s of audio, Audio8 ~21.5/s); `0` = model default, Parler rejects 1–9 |
1368
- | `greedy` | boolean | `false` | Audio8-only: take the argmax instead of sampling; ignores `temperature` / `topK` / `topP` |
1556
+ | `greedy` | boolean | `false` | Audio8 / CosyVoice3: take the argmax instead of sampling; Audio8 ignores `temperature` / `topK` / `topP` |
1369
1557
  | `minNewTokens` | number | GGUF default | Parler-only minimum tokens before EOS (`-1` = model default) |
1370
1558
  | `normalizeNumbers` | boolean | `true` | Parler-only: expand digits before tokenization (English words; script-native digits on indic) — parler voices raw digits badly |
1371
1559
  | `instruct` | object \| string | — | CosyVoice3-only: instruction controls (`dialect` / `volume` / `style`, resolved by precedence in that order) or a raw instruction string; an unknown key or invalid value throws, and it counts toward the one-instruction rule (see [CosyVoice3 instruct](#cosyvoice3-instruct)). Also accepted per call, where it replaces the configured conditioning like a per-call `emotion` / `pace` |
@@ -1376,9 +1564,10 @@ instance. Runtime stats add `promptTokens`, `generatedTokens`, `replyTokens`,
1376
1564
  | `openclCacheDir` | string | unset | Android-only: directory where the OpenCL backend persists its compiled program-binary cache. Setting it across runs avoids re-JITing the kernels on every fresh process |
1377
1565
  | `vulkanCacheDir` | string | unset | Supertonic + `useGPU: true` only: writable directory where the Vulkan backend persists its compiled pipeline cache (`GGML_VK_PIPELINE_CACHE_DIR`). Moves the one-time first-dispatch pipeline-compile cost (seconds on Mali) off the first `run()` — paid once per install instead of once per process — and enables a load-time pre-warm. Fully opt-in: unset -> no cross-process cache, no pre-warm, behaviour unchanged |
1378
1566
  | `config.language` | string | `"en"` | Chatterbox MTL accepts `es/fr/de/pt/it/zh/ja/ko/...`; turbo & Supertonic are English; MOSS takes it as a prompt hint (`en`, `zh`, ...) |
1567
+ | `config.backend` | string | unset | Native backend selector or exact registered device name; see [Backends](#backends--gpu-acceleration) |
1379
1568
  | `config.useGPU` | boolean | `false` | Set to `true` to route through Metal / CUDA / Vulkan / OpenCL if available. Honored for Chatterbox/Supertonic on GPU-capable hosts (including Android, per `tts-cpp`'s per-vendor allowlist); Parler is validated on Apple/Metal, linux CUDA, and Android/ARM Mali Vulkan; CosyVoice3 and Audio8 offload on Apple/Metal, desktop linux CUDA/Vulkan, Windows Vulkan, and Android OpenCL/Adreno; MOSS, MOSS-SoundEffect and MOSS-Speech GPU offload is validated on Apple/Metal. Unsupported backends fall back to CPU. See [Backends & GPU acceleration](#backends--gpu-acceleration) |
1380
1569
  | `config.outputSampleRate` | number | — (engine-native) | Resample the output to this rate (8000–192000 Hz). Omit to keep the engine-native rate (Chatterbox 24 kHz, Supertonic / Parler / Audio8 44.1 kHz, CosyVoice3 24 kHz, MOSS 24 kHz, MOSS-SoundEffect 48 kHz, MOSS-Speech 24 kHz, enhancer 48 kHz). Parler native chunk streaming accepts a non-native rate only with the enhancer active; MOSS, MOSS-SoundEffect and MOSS-Speech accept only their native rate |
1381
- | `opts.stats` | boolean | `false` | Populate `response.stats` with RTF, `backendDevice` (0=CPU, 1=GPU), `backendId` (0=CPU, 1=Metal, 2=CUDA, 3=Vulkan, 4=OpenCL, 99=other), `enhancerBackendDevice` / `enhancerBackendId` and `denoiserBackendDevice` / `denoiserBackendId` (`-1` when that LavaSR stage is off), and the engine's own stage stats: Chatterbox `t3Ms` / `s3genMs` / `t3Tokens`; CosyVoice3 per-stage ms (`lmPrefillMs` … `hiftDecodeMs`, `stageTotalMs`) and work counters (`speechTokens`, `decodeSteps`, …); Audio8 per-stage ms (`prefillMs`, `fastDecodeMs`, `codecSynthMs`, …, `stageTotalMs`) |
1570
+ | `opts.stats` | boolean | `false` | Populate `response.stats` with RTF, `backendDevice` (0=CPU, 1=GPU), `backendId` (0=CPU, 1=Metal, 2=CUDA, 3=Vulkan, 4=OpenCL, 5=WebGPU, 99=other), `enhancerBackendDevice` / `enhancerBackendId` and `denoiserBackendDevice` / `denoiserBackendId` (`-1` when that LavaSR stage is off; device `2` and id `99` for Core ML-managed placement), and the engine's own stage stats: Chatterbox `t3Ms` / `s3genMs` / `t3Tokens`; CosyVoice3 per-stage ms (`lmPrefillMs` … `hiftDecodeMs`, `stageTotalMs`) and work counters (`speechTokens`, `decodeSteps`, …); Audio8 per-stage ms (`prefillMs`, `fastDecodeMs`, `codecSynthMs`, …, `stageTotalMs`) |
1382
1571
  | `exclusiveRun` | boolean | `false` | **Top-level** option (not under `opts`): serialize overlapping streaming runs |
1383
1572
 
1384
1573
  ### Methods
@@ -1391,6 +1580,7 @@ instance. Runtime stats add `promptTokens`, `generatedTokens`, `replyTokens`,
1391
1580
  config (`language`, `useGPU`, `outputSampleRate`, …).
1392
1581
  - `await model.destroy()` — `unload()` + mark this instance dead.
1393
1582
  - `await model.cancel()` — best-effort cancel of any in-flight run.
1583
+ - `await model.warmUp(text)` — Supertonic-only native warm-up without a PCM response.
1394
1584
  - `TTSGgml.getVoiceControls()` — static: tts-cpp's emotion / pace vocabulary
1395
1585
  and each engine's supported subset (see [Emotion & pace](#emotion--pace-cross-engine)).
1396
1586
  - `model.run({ input, type: 'text' })` → `QvacResponse`.
@@ -1444,14 +1634,16 @@ response.stats.generatedFrames // Audio8 and MOSS only: codec frames (Audio8 e
1444
1634
  response.stats.codecSidecarLoaded // Audio8 only, macOS / iOS: 1 while the Apple Core ML codec sidecar is attached
1445
1635
  response.stats.codecOnCoreml // Audio8 only: 1 when this synthesis ran the codec on that sidecar, 0 when on the ggml backend
1446
1636
  response.stats.backendDevice // 0=CPU, 1=GPU
1447
- response.stats.backendId // 0=CPU, 1=Metal, 2=CUDA, 3=Vulkan, 4=OpenCL, 99=other
1637
+ response.stats.backendId // 0=CPU, 1=Metal, 2=CUDA, 3=Vulkan, 4=OpenCL, 5=WebGPU, 99=other
1448
1638
  // present when a LavaSR enhancer is active:
1449
1639
  response.stats.enhancerBackendDevice // -1 none, 0 CPU, 1 GPU
1450
1640
  response.stats.enhancerBackendId
1451
1641
  ```
1452
1642
 
1453
- For `runStream()` and `runStreaming()`, `codecSidecarLoaded` and
1454
- `codecOnCoreml` retain the last chunk that reported each field, including zero
1643
+ For `runStream()` and `runStreaming()`, the backend fields (`backendDevice`,
1644
+ `backendId`, `gpuUnsupported`, and the LavaSR `enhancerBackend*` and
1645
+ `denoiserBackend*` pairs) and the Audio8 codec fields (`codecSidecarLoaded`,
1646
+ `codecOnCoreml`) retain the last chunk that reported each field, including zero
1455
1647
  when a sidecar is retired or synthesis falls back to ggml. They are not summed;
1456
1648
  if no chunk reports a field, it remains absent.
1457
1649
 
@@ -1507,20 +1699,17 @@ const fit = await TTSGgml.assessFit({
1507
1699
  audioSeconds: 30
1508
1700
  })
1509
1701
 
1510
- fit.status // 'fits' | 'does-not-fit' | 'error'
1511
- fit.reason // the engine's own wording, e.g. 'model-unreadable', 'workload-too-large'
1512
- fit.modelVariant // which pipeline was projected, e.g. 'chatterbox-t3-turbo'
1513
- fit.deviceName
1514
- fit.deviceBytes
1515
- fit.weightsBytes
1516
- fit.stateBytes
1517
- fit.lmComputeBytes // 0 for a pipeline with no language-model stage, such as supertonic
1518
- fit.codecComputeBytes // 0 for a pipeline with no separate codec stage
1519
- fit.hostBytes
1520
- fit.lavasrFileBytes // the LavaSR stages' size on disk, 0 when none was named
1521
- fit.report
1702
+ fit.status // 'fits' | 'does-not-fit' | 'unknown'
1703
+ fit.devices // [{ id, name, unified, usedBytes, freeBytes, totalBytes }]
1704
+ fit.stats // {}
1522
1705
  ```
1523
1706
 
1707
+ `devices` holds the device the load resolves to, then the host CPU, which holds the host-side buffers. A load on the CPU is one row. `id` is the PCI bus ID when the backend reports one, so the same GPU matches across addons built on different backends, and the device name otherwise. `unified` is true for memory the host owns: the CPU, an integrated GPU, a GPU that shares the host's memory, Android's OpenCL GPU, and a device that reports no memory of its own, which takes the host's figures. `usedBytes` is what the load takes on that device.
1708
+
1709
+ On an iOS device every unified row shares the app's own memory limit, `os_proc_available_memory()`. Each such row's free memory is capped at that limit, and a load whose unified rows together take more than the limit less the margin does not fit. A limit of 0 means the app has none left.
1710
+
1711
+ `unknown` is a load the fitter cannot size, and it carries no rows: supertonic outside the fused graph path it models, which is a validated GPU, or a CPU without the Accelerate pointwise kernels; a CosyVoice3 or Parler load with Core ML sidecars; or a load whose memory the engine fails to measure.
1712
+
1524
1713
  `engineType` picks the fitter, the same key a load uses, and is required: a fit request carries none of the file keys a load is inferred from. Each engine takes the load's own file keys, plus a workload the fit adds:
1525
1714
 
1526
1715
  | `engineType` | Files | Workload |
@@ -1530,16 +1719,15 @@ fit.report
1530
1719
  | `chatterbox` | `t3ModelPath`, `s3genModelPath` | `textTokens`, `predictTokens` |
1531
1720
  | `audio8` | `audio8LmPath`, `audio8CodecDecoderPath`, `audio8CodecEncoderPath` | `promptTokens`, `maxFrames`, `referenceSeconds` |
1532
1721
  | `cosyvoice3` | `cosyvoiceLlmModelPath`, `cosyvoiceFlowModelPath`, `cosyvoiceHiftModelPath`, `cosyvoiceVoiceModelPath` | `textTokens`, `speechTokens` |
1722
+ | `pocket` | `pocketFlowModelPath`, `pocketMimiModelPath`, `pocketFrontendPath`, optional `pocketVoicePath` / `referenceAudio` | Required `text`; optional `memoryBudgetBytes`, `marginBytes`, and native load controls |
1533
1723
  | `moss` | `mossBackbonePath`, `mossCodecDecoderPath`, optional `mossCodecEncoderPath` | Required `promptRows`, `referenceSamples`, `streaming`; load controls `durationTokens`, `streamChunkTokens`, `threads` |
1534
1724
  | `moss-sfx` | Required `mossSoundEffectPath` | Required `prompt`, `seconds`; optional `negativePrompt`, `steps`, `guidance`, `shift`, `threads` |
1535
1725
 
1536
1726
  Everything else comes from the load config: `nGpuLayers` and `useGPU` carry the offload intent, `nCtx` and `kvCacheType` size the chatterbox cache, `steps` takes the GGUF's own default at 0, and `vulkanDevice` and `backendsDir` place the backend. Supplying `audio8CodecEncoderPath` projects voice cloning, which the decoder alone cannot do. `marginBytes` sets the free memory that must remain for the projection to count as fitting.
1537
1727
 
1538
- `lavasrEnhancerPath` and `lavasrDenoiserPath` are reported as `lavasrFileBytes`, their size on disk. Those stages have no fitter, so the figure is a size on disk. It is counted in neither `deviceBytes` nor `hostBytes`.
1728
+ The LavaSR enhancer and denoiser have no fitter, and the request takes no LavaSR paths. The estimate for a load with LavaSR stages leaves those stages out.
1539
1729
 
1540
- `deviceSharesHostMemory` reports that the device pool is system RAM, so host bytes compete with device bytes.
1541
-
1542
- A model the engine cannot read resolves to `status: "error"`, as does a voice with no fitter, which reports `reason: "unsupported-engine"`. MOSS-TTS, MOSS-TTSD and MOSS-SoundEffect are supported. MOSS-Speech has no SDK fit projection. A broken request, or a host with no native binding, rejects.
1730
+ A model the engine cannot read, a workload it cannot project, a voice with no fitter, or a broken request rejects with an `InvalidArgument` error, and a host with no native binding rejects as well. MOSS-TTS, MOSS-TTSD and MOSS-SoundEffect are supported. MOSS-Speech has no fitter.
1543
1731
 
1544
1732
  MOSS-TTS and MOSS-TTSD share the `moss` fitter. `promptRows` is the complete
1545
1733
  native prompt length, including special tokens, encoded speaker references and
@@ -1567,8 +1755,6 @@ as a conservative upper bound, including the reference encoder when requested;
1567
1755
  this may reject a workload whose actual peak is smaller. The fit follows the
1568
1756
  load's generation settings and does not change synthesis behavior.
1569
1757
 
1570
- The supertonic fitter covers the fused graph path: a validated GPU, or a CPU without the Accelerate pointwise kernels. Elsewhere it answers `compute-path-not-supported` and projects nothing.
1571
-
1572
1758
  ### MOSS-SoundEffect fit
1573
1759
 
1574
1760
  ```js
@@ -1579,7 +1765,7 @@ const fit = await TTSGgml.assessFit({
1579
1765
  seconds: 8,
1580
1766
  useGPU: false
1581
1767
  })
1582
- console.log(fit.report)
1768
+ console.log(fit.devices)
1583
1769
  ```
1584
1770
 
1585
1771
  The fitter uses generation's tokenizer, request validation and graph builders,
@@ -1587,8 +1773,8 @@ without loading weights or generating audio. It supports full and metadata-only
1587
1773
  GGUF files. `negativePrompt`, `steps`, `guidance` and `shift` keep their generation
1588
1774
  semantics; `marginBytes` keeps the shared 256 MiB default. DiT processes the model's
1589
1775
  full latent duration even for a short output. Requested seconds size decoder
1590
- windows and audio buffers. The shared compute arena is counted at its peak:
1591
- `lmComputeBytes` covers text/DiT, `codecComputeBytes` any additional VAE demand.
1776
+ windows and audio buffers. The shared compute arena for text/DiT and any
1777
+ additional VAE demand is counted at its peak.
1592
1778
  Host memory includes conditioning, temporary arrays and CPU fallback buffers.
1593
1779
 
1594
1780
  Run `bare examples/moss-sfx-fit.js ./moss-sfx-v2-q8_0.gguf "Rain on a roof." 8`.
@@ -1611,6 +1797,7 @@ Runnable demos under `examples/`:
1611
1797
  | `supertonic-mtl-sweep-tts.js` | Multilingual Supertonic sweep across languages |
1612
1798
  | `supertonic-sentence-stream-tts.js` | Supertonic sentence-level streaming |
1613
1799
  | `supertonic-chunk-stream-tts.js` | Supertonic native chunk streaming via `streamChunkTokens`, with streaming playback |
1800
+ | `lavasr-coreml.js` | Supertonic + explicit Core ML denoiser/enhancer. Run without arguments for usage. |
1614
1801
  | `supertonic-enhanced.js` | Supertonic + LavaSR 48 kHz enhancement. `bare examples/supertonic-enhanced.js "Hello"` |
1615
1802
  | `parler-tts.js` | Parler batch synth with voice/emotion templates. `bare examples/parler-tts.js "Hello" Laura happy` |
1616
1803
  | `parler-enhanced.js` | Parler + LavaSR 48 kHz enhancement. `bare examples/parler-enhanced.js "Hello" Laura happy` |
@@ -1788,9 +1975,10 @@ Apache-2.0. See [LICENSE](./LICENSE).
1788
1975
 
1789
1976
  ## Pocket TTS
1790
1977
 
1791
- The native `speech-cpp` dependency includes Pocket Metal, CUDA, Vulkan and
1792
- OpenCL support in the native library. The JavaScript Pocket API continues
1793
- to require `config.useGPU: false`; updating the native dependency does not
1794
- remove that addon validation.
1978
+ Pocket supports CPU, Metal, CUDA, Vulkan and OpenCL through the addon.
1979
+ CPU remains the default. Set `config.useGPU: true`, `nGpuLayers`, or
1980
+ `config.backend` to select an accelerator; runtime stats identify the
1981
+ backend actually selected. Metadata-only `assessFit()` uses the same
1982
+ native configuration and accepts an optional application memory budget.
1795
1983
 
1796
1984
  See [Pocket TTS](docs/pocket-tts.md) for model conversion, addon/SDK usage, supported controls and validation commands.
@@ -1,8 +1,10 @@
1
1
  # Pocket TTS
2
2
 
3
3
  Pocket is an English TTS engine implemented in the Fabric speech library.
4
- The native library supports CPU, Metal, CUDA, Vulkan and OpenCL; the
5
- `@qvac/tts-ggml` JavaScript API currently accepts CPU inference only.
4
+ The native library and `@qvac/tts-ggml` addon support CPU, Metal, CUDA,
5
+ Vulkan and OpenCL. CPU remains the default; set `config.useGPU: true`
6
+ or a nonzero `nGpuLayers` to request GPU inference. `config.backend`
7
+ can select an exact registered device such as `MTL0`.
6
8
  It streams PCM16 audio through the addon. Python
7
9
  is used only to convert weights and benchmark the upstream reference; it is
8
10
  not needed for native synthesis. Inference plugin and public SDK integration
@@ -71,13 +73,39 @@ chunk with `isLast: true`; other engines may omit this metadata.
71
73
 
72
74
  ## Supported controls
73
75
 
74
- English only; CPU only; `threads` defaults to one per worker (two workers).
76
+ English only; `threads` defaults to one per worker (two workers).
75
77
  `seed` accepts unsigned 32-bit integers. `temperature`, `steps`, `nCtx`,
76
78
  `maxTokens`, `noiseClamp`, `eosThreshold`, `framesAfterEos` and
77
79
  `outputSampleRate` are validated before native loading. Output resampling
78
- supports 8–192 kHz. Other engines' sampling, emotional, speed, GPU and
80
+ supports 8–192 kHz. Other engines' sampling, emotional, speed and
79
81
  LavaSR settings are rejected instead of silently ignored.
80
82
 
83
+ `config.useGPU` and `nGpuLayers` must agree when supplied together.
84
+ Reloading preserves the selected backend unless it is explicitly changed.
85
+
86
+ ### Memory preflight
87
+
88
+ `assessFit()` reads GGUF metadata and frontend/voice metadata without loading
89
+ weights. Pass the same backend and workload that synthesis will use:
90
+
91
+ ```js
92
+ const fit = await TTSGgml.assessFit({
93
+ engineType: 'pocket',
94
+ pocketFlowModelPath: '/models/pocket/flow-lm.gguf',
95
+ pocketMimiModelPath: '/models/pocket/mimi.gguf',
96
+ pocketFrontendPath: '/models/pocket/frontend.json',
97
+ pocketVoicePath: '/models/pocket/voice.gguf',
98
+ text: 'Hello from Pocket.',
99
+ useGPU: true
100
+ })
101
+ ```
102
+
103
+ `marginBytes` leaves the requested memory headroom. Optional
104
+ `memoryBudgetBytes` applies an application ceiling to each memory pool;
105
+ zero or omission retains the native telemetry-based budget. Results use
106
+ the common `fits`, `does-not-fit` and `error` statuses. The projection
107
+ does not load a model or change a synthesis setting.
108
+
81
109
  Fabric defaults to one sampling step, matching the native CLI and upstream
82
110
  reference. An explicit `steps: 4` (or
83
111
  addon `numInferenceSteps: 4`) offers a quality option at additional compute
@@ -0,0 +1,71 @@
1
+ 'use strict'
2
+
3
+ const fs = require('bare-fs')
4
+ const proc = require('bare-process')
5
+ const TTSGgml = require('../')
6
+ const { createWav } = require('./wav-helper')
7
+
8
+ const DEFAULT_TEXT = 'Speech controls are available directly from the addon.'
9
+ const DEFAULT_OUTPUT = 'tts-controls.wav'
10
+
11
+ function readOptions() {
12
+ const value = proc.env.QVAC_TTS_OPTIONS
13
+ if (!value) throw new Error('Set QVAC_TTS_OPTIONS to a JSON constructor configuration')
14
+ return JSON.parse(value)
15
+ }
16
+
17
+ function concatenatePcm(chunks) {
18
+ const size = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
19
+ const result = new Int16Array(size)
20
+ let offset = 0
21
+ for (const chunk of chunks) {
22
+ result.set(chunk, offset)
23
+ offset += chunk.length
24
+ }
25
+ return result
26
+ }
27
+
28
+ async function collectAudio(response) {
29
+ const chunks = []
30
+ let sampleRate
31
+ let speechTokenIds
32
+ for await (const chunk of response.iterate()) {
33
+ chunks.push(chunk.outputArray)
34
+ sampleRate = chunk.sampleRate
35
+ if (chunk.speechTokenIds) speechTokenIds = chunk.speechTokenIds
36
+ }
37
+ return { pcm: concatenatePcm(chunks), sampleRate, speechTokenIds }
38
+ }
39
+
40
+ async function main() {
41
+ const argv = global.Bare ? global.Bare.argv : proc.argv
42
+ const text = argv[2] || DEFAULT_TEXT
43
+ const output = argv[3] || DEFAULT_OUTPUT
44
+ const options = readOptions()
45
+ if (proc.env.QVAC_TTS_FIT) {
46
+ console.log('Memory preflight:', await TTSGgml.assessFit(JSON.parse(proc.env.QVAC_TTS_FIT)))
47
+ }
48
+ const model = new TTSGgml({ ...options, opts: { ...options.opts, stats: true } })
49
+ try {
50
+ await model.load()
51
+ if (proc.env.QVAC_TTS_WARM_UP === '1') await model.warmUp(text)
52
+ const response = await model.run({ input: text })
53
+ const result = await collectAudio(response)
54
+ if (!result.pcm.length) throw new Error('No audio was returned')
55
+ createWav(result.pcm, result.sampleRate, output)
56
+ console.log('Saved:', output)
57
+ console.log('Stats:', response.stats)
58
+ if (result.speechTokenIds) {
59
+ fs.writeFileSync(output + '.tokens.json', JSON.stringify(result.speechTokenIds))
60
+ console.log('Speech token IDs:', result.speechTokenIds.length)
61
+ }
62
+ } finally {
63
+ await model.destroy()
64
+ }
65
+ }
66
+
67
+ main().catch((error) => {
68
+ console.error(error)
69
+ if (global.Bare) global.Bare.exit(1)
70
+ else proc.exitCode = 1
71
+ })