@qvac/tts-ggml 0.10.1 → 0.11.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 +238 -20
- package/examples/moss-sfx-fit.js +18 -0
- package/examples/moss-sound-effect.js +125 -0
- package/examples/moss-speech.js +136 -0
- package/examples/pcm-chunk-player.js +35 -10
- package/index.d.ts +116 -6
- package/index.js +391 -2
- package/lib/fit.d.ts +22 -2
- package/package.json +31 -16
- package/test/integration/fit.test.js +7 -3
- package/test/integration/moss-fit.test.js +57 -0
- package/test/integration/moss-sfx-fit.test.js +63 -0
- package/test/integration/moss-sfx.test.js +96 -0
- package/test/integration/moss-speech.test.js +110 -0
- package/test/mobile/integration.auto.cjs +15 -0
- package/tts.d.ts +20 -0
- /package/test/{mobile → pocket-worklet}/pocket-worklet-host.cpp +0 -0
- /package/test/{mobile → pocket-worklet}/pocket-worklet.cjs +0 -0
package/README.md
CHANGED
|
@@ -10,7 +10,9 @@ Metal on Apple, CUDA/Vulkan on Linux, Vulkan on Windows, and OpenCL/Adreno
|
|
|
10
10
|
GPU offload on Android), **Audio8**
|
|
11
11
|
(DualAR + neural codec, in-process voice cloning, desktop GPU), and **MOSS**
|
|
12
12
|
(MOSS-TTS Delay, 24 kHz, in-process voice cloning, native chunk streaming,
|
|
13
|
-
desktop),
|
|
13
|
+
desktop), **MOSS-SoundEffect** (text-to-sound-effects, 48 kHz, up to 30 s,
|
|
14
|
+
desktop), and **MOSS-Speech** (speech-to-speech: a spoken question in, a
|
|
15
|
+
spoken answer out, 24 kHz, desktop), plus optional
|
|
14
16
|
LavaSR neural denoise + 48 kHz bandwidth-extension enhancement. Unsure
|
|
15
17
|
which checkpoint to stage? Start with [Choosing a model](#choosing-a-model).
|
|
16
18
|
|
|
@@ -74,6 +76,8 @@ obvious for each job.
|
|
|
74
76
|
| Voice cloning at 44.1 kHz with nothing pre-baked | `audio8-lm-q8_0.gguf` + `audio8-codec-decoder-q8_0.gguf` (+ `audio8-codec-encoder-q8_0.gguf` to clone) | Clones from a reference wav **and its transcript**, encoded in-process — no enrolment step, no voice profile. Whole-utterance only (no native chunk streaming). |
|
|
75
77
|
| Multilingual speech with native chunk streaming and cloning on a desktop | `moss-tts-delay-f16.gguf` + `moss-codec-decoder-f16.gguf` (+ `moss-codec-encoder-f16.gguf` to clone) | 8B backbone, desktop only. Clones from a reference wav encoded in-process (no transcript needed); native `streamChunkTokens` chunk streaming. Native 24 kHz. |
|
|
76
78
|
| Multi-speaker dialogue that continues each speaker's reference voice | `moss-ttsd-f16.gguf` + `moss-codec-decoder-f16.gguf` + `moss-codec-encoder-f16.gguf` | 8B backbone, desktop only. One 24 kHz reference per speaker; the text opens with their transcripts. Native 24 kHz. |
|
|
79
|
+
| Sound effects from a text description (not speech) | `moss-sfx-v2-q8_0.gguf` (or `moss-sfx-v2-f16.gguf`) | Diffusion model, desktop GPU recommended. Up to 30 s per clip, `seconds` per call; no streaming. Native 48 kHz. |
|
|
80
|
+
| Answer a spoken question with speech (voice assistant) | `moss-speech-q8_0.gguf` + `moss-speech-codec-f16.gguf` | 9B model, desktop GPU. Audio in, 24 kHz audio (plus the reply text) out; conversation history, reply voice and reply length per call; no streaming. |
|
|
77
81
|
| Indic languages | `parler-indic-q8_0.gguf` | 21 Indic languages; voice / emotion templates. Emotion is officially tested on 10 languages — see [Parler descriptions & emotions](#parler-descriptions--emotions) (or the upstream model card). Native 44.1 kHz. |
|
|
78
82
|
| Chinese dialects (Cantonese, Sichuan, Shanghai, …) | CosyVoice3 dir (`cosyvoice3-llm-*.gguf` + flow / hift / `voice.gguf`) | Instruct-conditioned; 17 dialects via `instruct: { dialect: '…' }`. CPU, with opt-in GPU offload (Metal on Apple, Vulkan on Linux/Windows, OpenCL/Adreno on Android); native 24 kHz. |
|
|
79
83
|
| Zero-shot / cross-lingual cloning (multilingual) | CosyVoice3 dir + `cosyvoice3-s3tok-*.gguf` + `cosyvoice3-campplus-*.gguf` | Clones from a reference wav; its transcript (`promptText`) selects zero-shot, omitting it selects cross-lingual (timbre only, any target language). Composes with `instruct`. |
|
|
@@ -98,6 +102,8 @@ here, so this guide does not go stale when backends change.
|
|
|
98
102
|
| `audio8-lm-q8_0` + codec halves | Text-led (no `language` option) | ~0.7 GB (+ ~120 MB to clone) | 44.1 kHz | Reference wav + transcript, per call too | Sentence streaming |
|
|
99
103
|
| `moss-tts-delay-f16` + codec halves | Multilingual (`language` hint) | ~18.8 GB (+ ~1.8 GB to clone) | 24 kHz | Reference wav, fixed per instance | Sentence + native chunk |
|
|
100
104
|
| `moss-ttsd-f16` + codec halves | Multilingual (`language` hint) | ~20.5 GB | 24 kHz | One reference wav per speaker, fixed per instance | Native chunk |
|
|
105
|
+
| `moss-sfx-v2-q8_0` (or `-f16`) | Sound effects from a text prompt | ~3.5 GB (`q8_0`) / ~6.4 GB (`f16`) | 48 kHz | — | None |
|
|
106
|
+
| `moss-speech-q8_0` (or `-bf16`) + `moss-speech-codec-f16` | Speech-to-speech replies | ~9.7 GB (`q8_0`) / ~18.2 GB (`bf16`), + 1.3 GB codec | 24 kHz | Codec default, or a per-call reference | None |
|
|
101
107
|
|
|
102
108
|
### Legacy Supertonic v1 / v2
|
|
103
109
|
|
|
@@ -172,8 +178,9 @@ dependency, pinned to the exact `@qvac/tts-ggml` version:
|
|
|
172
178
|
|
|
173
179
|
## Model files
|
|
174
180
|
|
|
175
|
-
|
|
176
|
-
CosyVoice3, Audio8, MOSS), each with its own GGUF layout
|
|
181
|
+
Eight engine families are wrapped (Chatterbox, Supertonic, Parler,
|
|
182
|
+
CosyVoice3, Audio8, MOSS, MOSS-SoundEffect, MOSS-Speech), each with its own GGUF layout
|
|
183
|
+
under `models/`:
|
|
177
184
|
|
|
178
185
|
```
|
|
179
186
|
# Chatterbox turbo (English)
|
|
@@ -233,6 +240,13 @@ moss-tts-delay-f16.gguf (~17 GB)
|
|
|
233
240
|
moss-ttsd-f16.gguf (~17 GB; MOSS-TTSD dialogue backbone, optional)
|
|
234
241
|
moss-codec-decoder-f16.gguf (~1.8 GB)
|
|
235
242
|
moss-codec-encoder-f16.gguf (~1.8 GB; cloning and dialogue only)
|
|
243
|
+
|
|
244
|
+
# MOSS-SoundEffect (OpenMOSS MOSS-SoundEffect-v2; 48 kHz text-to-sound-effects)
|
|
245
|
+
moss-sfx-v2-q8_0.gguf (~3.5 GB; or moss-sfx-v2-f16.gguf, ~6.4 GB)
|
|
246
|
+
|
|
247
|
+
# MOSS-Speech (fnlp/MOSS-Speech; 24 kHz speech-to-speech)
|
|
248
|
+
moss-speech-q8_0.gguf (~9.7 GB; or moss-speech-bf16.gguf, ~18.2 GB)
|
|
249
|
+
moss-speech-codec-f16.gguf (~1.3 GB; carries the default reply voice)
|
|
236
250
|
```
|
|
237
251
|
|
|
238
252
|
Download the registry-published Chatterbox, Supertonic, and Parler models into
|
|
@@ -244,7 +258,7 @@ npm run download-models:registry -- --group chatterbox,supertonic3
|
|
|
244
258
|
npm run download-models:registry -- --output /path/to/models
|
|
245
259
|
```
|
|
246
260
|
|
|
247
|
-
CosyVoice3, Audio8 and MOSS are not currently included in that registry command.
|
|
261
|
+
CosyVoice3, Audio8, MOSS, MOSS-SoundEffect and MOSS-Speech are not currently included in that registry command.
|
|
248
262
|
Stage their layouts shown above from local converted artifacts. CosyVoice3
|
|
249
263
|
resolves each component by filename prefix and does not rank quantizations, so
|
|
250
264
|
stage one file per component or name it explicitly with
|
|
@@ -270,7 +284,9 @@ qvac-fabric-speech.cpp (`engines/tts/scripts/convert-audio8-lm-to-gguf.py`
|
|
|
270
284
|
and `convert-audio8-codec-to-gguf.py`) until they are published to the model
|
|
271
285
|
registry alongside the other engines. The MOSS GGUFs come from
|
|
272
286
|
`convert-moss-delay-to-gguf.py` and `convert-moss-codec-to-gguf.py` in the same
|
|
273
|
-
directory.
|
|
287
|
+
directory, the MOSS-SoundEffect GGUF from `convert-moss-sfx-to-gguf.py`, and the
|
|
288
|
+
MOSS-Speech GGUFs from `convert-moss-speech-to-gguf.py` and
|
|
289
|
+
`convert-moss-speech-codec-to-gguf.py`.
|
|
274
290
|
|
|
275
291
|
Point the addon at a custom location via `files.modelDir` (engine
|
|
276
292
|
auto-detected from the gguf filenames present), or pass explicit
|
|
@@ -281,7 +297,9 @@ auto-detected from the gguf filenames present), or pass explicit
|
|
|
281
297
|
`files.audio8Lm` + `files.audio8CodecDecoder` (+ `files.audio8CodecEncoder`
|
|
282
298
|
to clone) (Audio8) /
|
|
283
299
|
`files.mossBackbone` + `files.mossCodecDecoder` (+ `files.mossCodecEncoder`
|
|
284
|
-
to clone) (MOSS)
|
|
300
|
+
to clone) (MOSS) /
|
|
301
|
+
`files.mossSoundEffect` (MOSS-SoundEffect) /
|
|
302
|
+
`files.mossSpeechModel` + `files.mossSpeechCodec` (MOSS-Speech).
|
|
285
303
|
|
|
286
304
|
## Quick start
|
|
287
305
|
|
|
@@ -688,8 +706,8 @@ published model set yet; supply your own to opt in.
|
|
|
688
706
|
| Model | Sidecar next to the GGUF | Stage on Core ML | Runs on ggml instead | Force ggml |
|
|
689
707
|
| --- | --- | --- | --- | --- |
|
|
690
708
|
| Supertonic 1 / 2 / 3 | `<model>-vocoder.mlmodelc` | vocoder, in 64-latent-frame windows | GGUFs whose vocoder weights are stored below 8 bits (`q4_0`) | `SUPERTONIC_COREML_DISABLE=1` |
|
|
691
|
-
| Audio8 | `audio8-codec-decoder.mlmodelc`, beside the codec decoder GGUF | codec synthesis stack (upsampling + DAC decoder), in 64-post-frame windows; the language model stays on the ggml backend | a call that fails on the sidecar, which also retires it for every later call on that instance | `AUDIO8_COREML_DISABLE=1` |
|
|
692
|
-
| Chatterbox, Parler, CosyVoice3, MOSS, LavaSR | none | — | always | — |
|
|
709
|
+
| Audio8 | `audio8-codec-decoder.mlmodelc`, beside the codec decoder GGUF | codec synthesis stack (upsampling + DAC decoder), in 64-post-frame windows synthesised while the language model is still generating, so only the last one is left after it; the language model stays on the ggml backend | a call that fails on the sidecar, which also retires it for every later call on that instance | `AUDIO8_COREML_DISABLE=1` |
|
|
710
|
+
| Chatterbox, Parler, CosyVoice3, MOSS, MOSS-SoundEffect, MOSS-Speech, LavaSR | none | — | always | — |
|
|
693
711
|
|
|
694
712
|
Set the force-ggml variables in the process environment before `load()`.
|
|
695
713
|
Audio8 reports its codec path in `response.stats`: `codecSidecarLoaded` is 1
|
|
@@ -797,6 +815,8 @@ each engine's subset straight from the native library, without loading a model
|
|
|
797
815
|
| Chatterbox | not supported | not supported | — | `speed` |
|
|
798
816
|
| Audio8 | not supported | not supported | — | — |
|
|
799
817
|
| MOSS | not supported | not supported | — | — |
|
|
818
|
+
| MOSS-SoundEffect | not supported | not supported | — | — |
|
|
819
|
+
| MOSS-Speech | not supported | not supported | — | — |
|
|
800
820
|
|
|
801
821
|
The 12 canonical emotions (case-insensitive): `command`, `anger`, `narration`,
|
|
802
822
|
`conversation`, `disgust`, `fear`, `happy`, `neutral`, `proper noun`, `news`,
|
|
@@ -1014,6 +1034,104 @@ falls back to CPU and sets `response.stats.gpuUnsupported`. `backendsDir`
|
|
|
1014
1034
|
reaches MOSS like every other engine, so builds that load ggml backends at
|
|
1015
1035
|
runtime find them.
|
|
1016
1036
|
|
|
1037
|
+
## MOSS-SoundEffect
|
|
1038
|
+
|
|
1039
|
+
MOSS-SoundEffect-v2 generates a sound effect from a text description: a Qwen3
|
|
1040
|
+
text encoder reads the prompt, a DiT denoises a latent over a number of
|
|
1041
|
+
flow-matching steps, and a DAC decoder turns it into 48 kHz mono audio of up
|
|
1042
|
+
to 30 seconds. It is not a speech model, and it ships as one GGUF. Select it
|
|
1043
|
+
with `engine: TTSGgml.ENGINE_MOSS_SFX` or by staging `moss-sfx-*.gguf` in the
|
|
1044
|
+
`modelDir`.
|
|
1045
|
+
|
|
1046
|
+
```js
|
|
1047
|
+
const sfx = new TTSGgml({
|
|
1048
|
+
engine: TTSGgml.ENGINE_MOSS_SFX,
|
|
1049
|
+
files: { modelDir: './models' },
|
|
1050
|
+
config: { useGPU: true },
|
|
1051
|
+
opts: { stats: true }
|
|
1052
|
+
})
|
|
1053
|
+
await sfx.load()
|
|
1054
|
+
const response = await sfx.run({
|
|
1055
|
+
input: 'Heavy rain falling on a tin roof with distant thunder.',
|
|
1056
|
+
seconds: 10, // (0, 30], rounded to 0.1 s; defaults to 10
|
|
1057
|
+
negativePrompt: 'music', // optional
|
|
1058
|
+
steps: 100, // optional; model default 100
|
|
1059
|
+
guidance: 4, // optional; model default 4, 1 skips the negative branch
|
|
1060
|
+
shift: 5 // optional; model default 5
|
|
1061
|
+
})
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
The generation controls are per call and moss-sfx-only (`steps` in the
|
|
1065
|
+
constructor is rejected); the output lasts `seconds` rounded to 0.1 s. `seed` (constructor) makes a clip reproducible. Every clip
|
|
1066
|
+
costs about the same whatever its length, because the model always denoises a
|
|
1067
|
+
30-second latent and crops it: about two minutes for 100 steps on an Apple M1
|
|
1068
|
+
Ultra GPU, and much longer on CPU, so fewer `steps` trade quality for time.
|
|
1069
|
+
There is no streaming (`runStream()`, `runStreaming()` and
|
|
1070
|
+
`run({ streamOutput: true })` are rejected), `config.outputSampleRate` must be
|
|
1071
|
+
unset or `48000`, and the LavaSR stages and voice options are not supported.
|
|
1072
|
+
|
|
1073
|
+
## MOSS-Speech
|
|
1074
|
+
|
|
1075
|
+
MOSS-Speech answers a spoken question with speech, with no text step in
|
|
1076
|
+
between: a 9B language model with separate text and audio branches reads the
|
|
1077
|
+
question's speech tokens and writes the reply's, and the codec speaks them in a
|
|
1078
|
+
24 kHz voice. It ships as two GGUFs, the language model and the codec (which
|
|
1079
|
+
also carries the default reply voice). Select it with
|
|
1080
|
+
`engine: TTSGgml.ENGINE_MOSS_SPEECH` or by staging `moss-speech-*.gguf` and
|
|
1081
|
+
`moss-speech-codec-*.gguf` in the `modelDir`; `q8_0` is preferred over `bf16`
|
|
1082
|
+
when both are there.
|
|
1083
|
+
|
|
1084
|
+
```js
|
|
1085
|
+
const assistant = new TTSGgml({
|
|
1086
|
+
engine: TTSGgml.ENGINE_MOSS_SPEECH,
|
|
1087
|
+
files: { modelDir: './models' },
|
|
1088
|
+
config: { useGPU: true },
|
|
1089
|
+
opts: { stats: true }
|
|
1090
|
+
})
|
|
1091
|
+
await assistant.load()
|
|
1092
|
+
const response = await assistant.run({
|
|
1093
|
+
input: '', // or a typed question instead of `audio`
|
|
1094
|
+
audio: questionPcm, // Int16Array or Float32Array, mono
|
|
1095
|
+
sampleRate: 16000, // of `audio`, 8000..192000
|
|
1096
|
+
messages: [ // optional earlier turns, oldest first
|
|
1097
|
+
{ role: 'user', text: 'Hi, I am planning a trip.' },
|
|
1098
|
+
{ role: 'assistant', text: 'Great, where to?' }
|
|
1099
|
+
],
|
|
1100
|
+
systemPrompt: 'Answer briefly.', // optional; replaces the default system turn
|
|
1101
|
+
replyVoice: voicePcm, // optional reply voice (up to 60 s of mono PCM)
|
|
1102
|
+
replyVoiceSampleRate: 24000,
|
|
1103
|
+
maxReplySeconds: 20, // optional cut of the spoken reply
|
|
1104
|
+
maxNewTokens: 1000, // optional, 1..4096
|
|
1105
|
+
textReply: false, // true: answer in text only, no audio
|
|
1106
|
+
greedy: false, temperature: 0.7, topP: 0.95, topK: 20 // optional sampling
|
|
1107
|
+
})
|
|
1108
|
+
await response.onUpdate((data) => {
|
|
1109
|
+
// data.outputArray: 24 kHz Int16 PCM of the spoken reply
|
|
1110
|
+
// data.text: the reply's text channel (the whole answer with textReply)
|
|
1111
|
+
}).await()
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
When `maxNewTokens` is omitted, MOSS-Speech uses the remaining model context
|
|
1115
|
+
after the prompt instead of an implicit 1000-token budget. The JavaScript API
|
|
1116
|
+
currently accepts explicit budgets from 1 to 4096; the native engine also
|
|
1117
|
+
checks that the requested budget fits the remaining context. `maxReplySeconds`
|
|
1118
|
+
is optional and defaults to no time cut.
|
|
1119
|
+
|
|
1120
|
+
The user turn is either `audio` (with `sampleRate`) or the run `input` text,
|
|
1121
|
+
not both; each entry of `messages` carries exactly one of `text` or `audio`
|
|
1122
|
+
(with its `sampleRate`). Every call is one exchange: pass the earlier turns in
|
|
1123
|
+
`messages` to keep a conversation going. `seed` (constructor) makes sampled
|
|
1124
|
+
replies reproducible. The reply comes back in one piece: there is no streaming
|
|
1125
|
+
(`runStream()`, `runStreaming()`, `run({ streamOutput: true })` and
|
|
1126
|
+
`streamChunkTokens` are rejected), `config.outputSampleRate` must be unset or
|
|
1127
|
+
`24000`, and the LavaSR stages and `referenceAudio` are not supported. On an
|
|
1128
|
+
Apple M1 Ultra GPU the model generates about 47 reply tokens per second in
|
|
1129
|
+
`q8_0` (32 in `bf16`) against the 12.5 tokens per second of speech it produces;
|
|
1130
|
+
preparing the default voice adds a few seconds to the first reply of an
|
|
1131
|
+
instance. Runtime stats add `promptTokens`, `generatedTokens`, `replyTokens`,
|
|
1132
|
+
`truncated` and the per-stage `encodeMs` / `prefillMs` / `generateMs` /
|
|
1133
|
+
`decodeMs`.
|
|
1134
|
+
|
|
1017
1135
|
## API overview
|
|
1018
1136
|
|
|
1019
1137
|
### Constructor — `new TTSGgml(options)`
|
|
@@ -1036,15 +1154,18 @@ runtime find them.
|
|
|
1036
1154
|
| `files.mossBackbone` | string | — | MOSS Delay backbone GGUF: MOSS-TTS or the MOSS-TTSD dialogue checkpoint (overrides `modelDir`) |
|
|
1037
1155
|
| `files.mossCodecDecoder` | string | — | MOSS codec synthesis half — codes to wav (overrides `modelDir`) |
|
|
1038
1156
|
| `files.mossCodecEncoder` | string | — | MOSS codec analysis half — wav to codes; only needed to clone a voice or for `dialogueReferences` |
|
|
1157
|
+
| `files.mossSoundEffect` | string | — | MOSS-SoundEffect GGUF (`moss-sfx-*.gguf`); routes to the `moss-sfx` engine |
|
|
1158
|
+
| `files.mossSpeechModel` | string | — | MOSS-Speech language model GGUF (`moss-speech-*.gguf`); routes to the `moss-speech` engine |
|
|
1159
|
+
| `files.mossSpeechCodec` | string | — | MOSS-Speech codec GGUF (`moss-speech-codec-*.gguf`), required with `files.mossSpeechModel` |
|
|
1039
1160
|
| `files.lavasrEnhancer` | string | — | LavaSR enhancer GGUF — supplying it turns on 48 kHz enhancement |
|
|
1040
1161
|
| `files.lavasrDenoiser` | string | — | LavaSR denoiser GGUF — supplying it turns on denoising (batch only) |
|
|
1041
|
-
| `engine` | string | auto | Force `'chatterbox'`, `'supertonic'`, `'cosyvoice3'`, `'parler'`, `'audio8'` or `'moss'` (`TTSGgml.ENGINE_CHATTERBOX` / `ENGINE_SUPERTONIC` / `ENGINE_COSYVOICE3` / `ENGINE_PARLER` / `ENGINE_AUDIO8` / `ENGINE_MOSS`); auto-detected from the GGUFs present otherwise |
|
|
1162
|
+
| `engine` | string | auto | Force `'chatterbox'`, `'supertonic'`, `'cosyvoice3'`, `'parler'`, `'audio8'`, `'moss'`, `'moss-sfx'` or `'moss-speech'` (`TTSGgml.ENGINE_CHATTERBOX` / `ENGINE_SUPERTONIC` / `ENGINE_COSYVOICE3` / `ENGINE_PARLER` / `ENGINE_AUDIO8` / `ENGINE_MOSS` / `ENGINE_MOSS_SFX` / `ENGINE_MOSS_SPEECH`); auto-detected from the GGUFs present otherwise |
|
|
1042
1163
|
| `referenceAudio` | string | — | Wav to clone (Chatterbox: mono, ≥ 5 s; CosyVoice3: 0.5-30 s, multichannel downmixed to mono, needs the s3tok + campplus GGUFs; Audio8: also needs `referenceText`; MOSS: needs `files.mossCodecEncoder`). Audio8 accepts it per call too |
|
|
1043
1164
|
| `referenceText` | string | — | Audio8-only: what `referenceAudio` says, verbatim. Required whenever a reference is set; accepted per call |
|
|
1044
1165
|
| `dialogueReferences` | string[] | — | MOSS-only: one 24 kHz recording per speaker (`[S1]`, `[S2]`, ...) for dialogue synthesis; the text must open with each recording's transcript under its tag; needs `files.mossCodecEncoder`, excludes `referenceAudio`, fixed per instance |
|
|
1045
1166
|
| `durationTokens` | number | `0` | MOSS-only: target length in codec frames (12.5 per second); `0` keeps the length free; reloadable |
|
|
1046
1167
|
| `voiceDir` | string | — | Pre-baked voice profile |
|
|
1047
|
-
| `seed` | number | 42 | RNG seed (CFM noise + sampling); MOSS defaults to its engine's 1234 |
|
|
1168
|
+
| `seed` | number | 42 | RNG seed (CFM noise + sampling); MOSS defaults to its engine's 1234, MOSS-SoundEffect and MOSS-Speech to 0, so an unseeded prompt gives the same clip or reply every call |
|
|
1048
1169
|
| `nGpuLayers` | number | 0 | Layers offloaded to GPU (mirrors `useGPU`; pass `99` to offload all) |
|
|
1049
1170
|
| `nCtx` | number | 4096 | Chatterbox T3 context limit for the prompt plus generated speech tokens (25 tokens ≈ 1 s of audio). The KV cache is allocated up front at this length, so the 4096-token default directly bounds memory. Pass `0` to use the GGUF metadata value |
|
|
1050
1171
|
| `kvCacheType` | string | `f16` | T3 KV-cache dtype: `f32` \| `f16` \| `q8_0`. `f16` (~50% of f32) is the safe cross-backend default. `q8_0` stores the cache at ~27% of f32 and decodes 20-30% faster on Metal, but only works on backends with a q8_0 CONT op (CPU, CUDA) — it hard-aborts the multilingual model on Metal, so it is opt-in. Turbo greedy decoding is byte-identical across all three (upstream-validated). Pass `f32` for bit-exact pre-quantisation behaviour |
|
|
@@ -1065,7 +1186,7 @@ runtime find them.
|
|
|
1065
1186
|
| `voice` / `voiceName` | string | — | Supertonic voice id (e.g. `'F1'`, `'M1'`); Parler template speaker name (e.g. `'Laura'`, `'Rohit'`) |
|
|
1066
1187
|
| `voiceJsonPath` | string | — | Supertonic-only: external voice JSON (`{ style_ttl, style_dp }`, e.g. a cloned voice) that overrides `voice`; the engine checks the tensor sizes against the model |
|
|
1067
1188
|
| `prewarmText` | string | — | Supertonic-only: text synthesized once at load so GPU pipelines compile, and a Core ML vocoder sidecar specializes, before the first `run()` (skipped on a plain CPU run); use a representative length. Wins over the `vulkanCacheDir` default pre-warm |
|
|
1068
|
-
| `vulkanDevice` | number | 0 | Supertonic / CosyVoice3 Vulkan adapter, also used by their LavaSR enhancer: `0` = first, `N` = the Nth, `-1` = auto-pick by free VRAM preferring a discrete GPU |
|
|
1189
|
+
| `vulkanDevice` | number | 0 | Supertonic / CosyVoice3 Vulkan adapter, also used by their LavaSR enhancer: `0` = first, `N` = the Nth, `-1` = auto-pick by free VRAM preferring a discrete GPU. Audio8 always auto-picks |
|
|
1069
1190
|
| `flowCutPrompt` | boolean | `false` | CosyVoice3-only: attention-only prompt frames in the flow — faster, with output that deviates slightly from the reference |
|
|
1070
1191
|
| `steps` / `numInferenceSteps` | number | GGUF default | Supertonic vector-estimator CFM steps (`0` = GGUF default) |
|
|
1071
1192
|
| `noiseNpyPath` | string | — | Supertonic: optional fixed CFM noise `.npy` for reproducibility |
|
|
@@ -1085,8 +1206,8 @@ runtime find them.
|
|
|
1085
1206
|
| `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 |
|
|
1086
1207
|
| `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 |
|
|
1087
1208
|
| `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`, ...) |
|
|
1088
|
-
| `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 GPU offload is validated on Apple/Metal. Unsupported backends fall back to CPU. See [Backends & GPU acceleration](#backends--gpu-acceleration) |
|
|
1089
|
-
| `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, enhancer 48 kHz). Parler native chunk streaming accepts a non-native rate only with the enhancer active; MOSS
|
|
1209
|
+
| `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) |
|
|
1210
|
+
| `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 |
|
|
1090
1211
|
| `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`) |
|
|
1091
1212
|
| `exclusiveRun` | boolean | `false` | **Top-level** option (not under `opts`): serialize overlapping streaming runs |
|
|
1092
1213
|
|
|
@@ -1103,6 +1224,14 @@ runtime find them.
|
|
|
1103
1224
|
- `TTSGgml.getVoiceControls()` — static: tts-cpp's emotion / pace vocabulary
|
|
1104
1225
|
and each engine's supported subset (see [Emotion & pace](#emotion--pace-cross-engine)).
|
|
1105
1226
|
- `model.run({ input, type: 'text' })` → `QvacResponse`.
|
|
1227
|
+
- `model.run({ input, seconds, negativePrompt, steps, guidance, shift })` →
|
|
1228
|
+
moss-sfx: one sound effect of `seconds` length; the controls are optional
|
|
1229
|
+
and moss-sfx-only (see [MOSS-SoundEffect](#moss-soundeffect)).
|
|
1230
|
+
- `model.run({ input, audio, sampleRate, messages, systemPrompt, replyVoice,
|
|
1231
|
+
replyVoiceSampleRate, maxReplySeconds, maxNewTokens, textReply, greedy,
|
|
1232
|
+
temperature, topP, topK })` → moss-speech: one spoken (or text) reply with
|
|
1233
|
+
its text in `data.text`; the fields are moss-speech-only (see
|
|
1234
|
+
[MOSS-Speech](#moss-speech)).
|
|
1106
1235
|
- `model.run({ input, streamOutput: true })` → sentence-chunked
|
|
1107
1236
|
synthesis driven by the JS-side sentence splitter (see
|
|
1108
1237
|
`lib/textChunker.js`). Equivalent to `runStream(input)`.
|
|
@@ -1128,7 +1257,8 @@ All `run*` methods return a `QvacResponse` (from `@qvac/infer-base`):
|
|
|
1128
1257
|
```js
|
|
1129
1258
|
response.onUpdate(data => {
|
|
1130
1259
|
data.outputArray // Int16Array — mono PCM
|
|
1131
|
-
data.sampleRate // actual rate: Chatterbox/CosyVoice3/MOSS 24000, Supertonic/Parler/Audio8 44100, enhancer 48000
|
|
1260
|
+
data.sampleRate // actual rate: Chatterbox/CosyVoice3/MOSS/MOSS-Speech 24000, Supertonic/Parler/Audio8 44100, MOSS-SoundEffect and enhancer 48000
|
|
1261
|
+
data.text // MOSS-Speech only: the reply's text
|
|
1132
1262
|
data.chunkIndex // present on sentence-streaming events only
|
|
1133
1263
|
data.sentenceChunk // present on sentence-streaming events only
|
|
1134
1264
|
})
|
|
@@ -1228,6 +1358,8 @@ fit.report
|
|
|
1228
1358
|
| `chatterbox` | `t3ModelPath`, `s3genModelPath` | `textTokens`, `predictTokens` |
|
|
1229
1359
|
| `audio8` | `audio8LmPath`, `audio8CodecDecoderPath`, `audio8CodecEncoderPath` | `promptTokens`, `maxFrames`, `referenceSeconds` |
|
|
1230
1360
|
| `cosyvoice3` | `cosyvoiceLlmModelPath`, `cosyvoiceFlowModelPath`, `cosyvoiceHiftModelPath`, `cosyvoiceVoiceModelPath` | `textTokens`, `speechTokens` |
|
|
1361
|
+
| `moss` | `mossBackbonePath`, `mossCodecDecoderPath`, optional `mossCodecEncoderPath` | Required `promptRows`, `referenceSamples`, `streaming`; load controls `durationTokens`, `streamChunkTokens`, `threads` |
|
|
1362
|
+
| `moss-sfx` | Required `mossSoundEffectPath` | Required `prompt`, `seconds`; optional `negativePrompt`, `steps`, `guidance`, `shift`, `threads` |
|
|
1231
1363
|
|
|
1232
1364
|
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.
|
|
1233
1365
|
|
|
@@ -1235,10 +1367,60 @@ Everything else comes from the load config: `nGpuLayers` and `useGPU` carry the
|
|
|
1235
1367
|
|
|
1236
1368
|
`deviceSharesHostMemory` reports that the device pool is system RAM, so host bytes compete with device bytes.
|
|
1237
1369
|
|
|
1238
|
-
A model the engine cannot read is `status: "error"`, as is a voice with no fitter, which reports `reason: "unsupported-engine"`. MOSS
|
|
1370
|
+
A model the engine cannot read is `status: "error"`, as is 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, throws.
|
|
1371
|
+
|
|
1372
|
+
MOSS-TTS and MOSS-TTSD share the `moss` fitter. `promptRows` is the complete
|
|
1373
|
+
native prompt length, including special tokens, encoded speaker references and
|
|
1374
|
+
TTSD continuation rows. `referenceSamples` is the total mono reference sample
|
|
1375
|
+
count (0 without a reference); a positive count requires the encoder path.
|
|
1376
|
+
`streaming` explicitly selects native chunk streaming or batch synthesis.
|
|
1377
|
+
The fitter does not open reference recordings. Use the TTSD backbone to assess
|
|
1378
|
+
dialogue, supplying its complete prompt and total reference workload.
|
|
1379
|
+
|
|
1380
|
+
```js
|
|
1381
|
+
const fit = TTSGgml.assessFit({
|
|
1382
|
+
engineType: 'moss',
|
|
1383
|
+
mossBackbonePath: './moss-tts-delay-f16.gguf',
|
|
1384
|
+
mossCodecDecoderPath: './moss-codec-decoder-f16.gguf',
|
|
1385
|
+
promptRows: 128,
|
|
1386
|
+
referenceSamples: 0,
|
|
1387
|
+
streaming: false,
|
|
1388
|
+
useGPU: true
|
|
1389
|
+
})
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
The result includes LM weights and KV state, codec weights and compute, streaming
|
|
1393
|
+
caches when applicable, and host staging/audio memory. It sums component peaks
|
|
1394
|
+
as a conservative upper bound, including the reference encoder when requested;
|
|
1395
|
+
this may reject a workload whose actual peak is smaller. The fit follows the
|
|
1396
|
+
load's generation settings and does not change synthesis behavior.
|
|
1239
1397
|
|
|
1240
1398
|
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.
|
|
1241
1399
|
|
|
1400
|
+
### MOSS-SoundEffect fit
|
|
1401
|
+
|
|
1402
|
+
```js
|
|
1403
|
+
const fit = TTSGgml.assessFit({
|
|
1404
|
+
engineType: 'moss-sfx',
|
|
1405
|
+
mossSoundEffectPath: './moss-sfx-v2-q8_0.gguf',
|
|
1406
|
+
prompt: 'Rain falling on a tin roof.',
|
|
1407
|
+
seconds: 8,
|
|
1408
|
+
useGPU: false
|
|
1409
|
+
})
|
|
1410
|
+
console.log(fit.report)
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
The fitter uses generation's tokenizer, request validation and graph builders,
|
|
1414
|
+
without loading weights or generating audio. It supports full and metadata-only
|
|
1415
|
+
GGUF files. `negativePrompt`, `steps`, `guidance` and `shift` keep their generation
|
|
1416
|
+
semantics; `marginBytes` keeps the shared 256 MiB default. DiT processes the model's
|
|
1417
|
+
full latent duration even for a short output. Requested seconds size decoder
|
|
1418
|
+
windows and audio buffers. The shared compute arena is counted at its peak:
|
|
1419
|
+
`lmComputeBytes` covers text/DiT, `codecComputeBytes` any additional VAE demand.
|
|
1420
|
+
Host memory includes conditioning, temporary arrays and CPU fallback buffers.
|
|
1421
|
+
|
|
1422
|
+
Run `bare examples/moss-sfx-fit.js ./moss-sfx-v2-q8_0.gguf "Rain on a roof." 8`.
|
|
1423
|
+
|
|
1242
1424
|
## Examples
|
|
1243
1425
|
|
|
1244
1426
|
Runnable demos under `examples/`:
|
|
@@ -1264,16 +1446,25 @@ Runnable demos under `examples/`:
|
|
|
1264
1446
|
| `cosyvoice-enhanced.js` | CosyVoice3 + LavaSR 48 kHz enhancement (add `--denoise` for the denoiser). `bare examples/cosyvoice-enhanced.js "Hello"` |
|
|
1265
1447
|
| `audio8-tts.js` | Audio8 batch synth, optionally cloning a reference. Set `QVAC_TTS_AUDIO8_GPU=1` for the desktop GPU backend (CUDA or Vulkan on linux, Vulkan on Windows). `bare examples/audio8-tts.js "Hello" voice.wav "What it says."` |
|
|
1266
1448
|
| `moss-tts.js` | MOSS batch synth, optionally cloning a reference. Set `QVAC_TTS_MOSS_STREAM_FRAMES=25` for native chunk streaming, `QVAC_TTS_MOSS_DURATION=38` for a target length and `QVAC_TTS_MOSS_GPU=1` for the GPU backend. `bare examples/moss-tts.js "Hello" voice.wav` |
|
|
1449
|
+
| `moss-sound-effect.js` | MOSS-SoundEffect text-to-sound-effects at 48 kHz. Set `QVAC_TTS_MOSS_SFX_GPU=1` for the GPU backend and `QVAC_TTS_MOSS_SFX_STEPS` to trade quality for time. `bare examples/moss-sound-effect.js "Rain on a tin roof." 5` |
|
|
1450
|
+
| `moss-speech.js` | MOSS-Speech speech-to-speech: answers a spoken question WAV with a 24 kHz spoken reply (optionally in the voice of a second WAV). Set `QVAC_TTS_MOSS_SPEECH_GPU=1` for the GPU backend and `QVAC_TTS_MOSS_SPEECH_TEXT=1` for a text-only answer. `bare examples/moss-speech.js test/reference-audio/jfk.wav` |
|
|
1267
1451
|
| `moss-dialogue-tts.js` | MOSS-TTSD multi-speaker dialogue from one 24 kHz reference per speaker; the text opens with each reference's transcript. Set `QVAC_TTS_MOSS_GPU=1` for the GPU backend. `bare examples/moss-dialogue-tts.js "[S1] What alice.wav says. [S2] What bob.wav says. [S1] Hi. [S2] Hello." alice.wav bob.wav` |
|
|
1268
1452
|
|
|
1269
|
-
The
|
|
1453
|
+
The streaming examples feed PCM into a single long-running
|
|
1270
1454
|
`sox play` / `ffplay` process so chunks play back-to-back without any
|
|
1271
|
-
per-chunk spawn gaps
|
|
1272
|
-
`brew install ffmpeg` on macOS)
|
|
1273
|
-
|
|
1455
|
+
per-chunk spawn gaps. Playback needs one of those players (`brew install sox`
|
|
1456
|
+
or `brew install ffmpeg` on macOS) and `bare-subprocess`, a development
|
|
1457
|
+
dependency: run `npm install bare-subprocess` when you run the examples from
|
|
1458
|
+
an installed package. Without them the demos still run and write the
|
|
1459
|
+
concatenated wav.
|
|
1274
1460
|
|
|
1275
1461
|
## Testing
|
|
1276
1462
|
|
|
1463
|
+
The consolidated C++ CI lane uses persistent vcpkg binaries, a package-specific
|
|
1464
|
+
compiler cache, and two build workers. The deterministic C++ tier remains a
|
|
1465
|
+
required gate. See [C++ CI configuration](../../docs/ci/nx-ci-consolidation.md#optionsci-cheat-sheet)
|
|
1466
|
+
for cache warming and resource settings.
|
|
1467
|
+
|
|
1277
1468
|
```bash
|
|
1278
1469
|
npm run test:unit # mocked binding; fast
|
|
1279
1470
|
npm run test:integration # spins up the real engine; needs models
|
|
@@ -1311,12 +1502,39 @@ Parler tests need Metal, so off Apple they are skipped too.
|
|
|
1311
1502
|
The MOSS integration test (`moss.test.js`) does not download its GGUFs:
|
|
1312
1503
|
set `QVAC_TEST_MOSS_MODEL_DIR` to a directory holding them, otherwise it is
|
|
1313
1504
|
skipped. The C++ suite runs its MOSS synthesis cases when
|
|
1314
|
-
`QVAC_TEST_MOSS_BACKBONE_GGUF` and `QVAC_TEST_MOSS_DECODER_GGUF` are set.
|
|
1505
|
+
`QVAC_TEST_MOSS_BACKBONE_GGUF` and `QVAC_TEST_MOSS_DECODER_GGUF` are set. The
|
|
1506
|
+
MOSS-SoundEffect integration test (`moss-sfx.test.js`) likewise needs
|
|
1507
|
+
`QVAC_TEST_MOSS_SFX_MODEL_DIR`, and the C++ suite generates a short clip when
|
|
1508
|
+
`QVAC_TEST_MOSS_SFX_GGUF` is set. The MOSS-Speech integration test
|
|
1509
|
+
(`moss-speech.test.js`) needs `QVAC_TEST_MOSS_SPEECH_MODEL_DIR` (the model and
|
|
1510
|
+
its codec), and the C++ suite answers a short spoken turn when
|
|
1511
|
+
`QVAC_TEST_MOSS_SPEECH_GGUF` and `QVAC_TEST_MOSS_SPEECH_CODEC_GGUF` are set.
|
|
1315
1512
|
|
|
1316
1513
|
To stress-test long inputs, set `INPUT_SENTENCES=medium` (or `long`)
|
|
1317
1514
|
and re-run the integration suite — `addon.test.js` reads the env var to
|
|
1318
1515
|
pick its sentence corpus from `test/data/sentences-{medium,long}.js`.
|
|
1319
1516
|
|
|
1517
|
+
### Fuzzing
|
|
1518
|
+
|
|
1519
|
+
The JS-adapter config string parsers have a [Google FuzzTest][fuzztest]
|
|
1520
|
+
target, `tts-config-parse-fuzz`. Fuzzing is Linux-only: it needs clang with
|
|
1521
|
+
libFuzzer and AddressSanitizer, plus the [build-from-source](#build-from-source)
|
|
1522
|
+
prerequisites. The target does not link tts-cpp, so it runs with full ASan and
|
|
1523
|
+
LeakSanitizer.
|
|
1524
|
+
|
|
1525
|
+
```bash
|
|
1526
|
+
npm run fuzz # bounded run of every FUZZ_TEST, as Linux CI does
|
|
1527
|
+
npm run fuzz:continuous # coverage-guided, one FUZZ_TEST at a time
|
|
1528
|
+
npm run fuzz:continuous -- TtsConfigParseFuzz.ParseFloatNeverCrashes --fuzz_for=30m
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
Flags other than `--continuous` and `--build-dir` go to the fuzz binary. See
|
|
1532
|
+
[`docs/architecture/ADDON-FUZZING.md`](../../docs/architecture/ADDON-FUZZING.md)
|
|
1533
|
+
for the fuzzing design, the vcpkg-supplied FuzzTest stack, and how to add a
|
|
1534
|
+
target.
|
|
1535
|
+
|
|
1536
|
+
[fuzztest]: https://github.com/google/fuzztest
|
|
1537
|
+
|
|
1320
1538
|
## Build from source
|
|
1321
1539
|
|
|
1322
1540
|
Prerequisites: `clang` with C++20 support, CMake ≥ 3.25,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const process = require('bare-process')
|
|
4
|
+
const TTSGgml = require('@qvac/tts-ggml')
|
|
5
|
+
|
|
6
|
+
const [, , model, prompt, seconds] = process.argv
|
|
7
|
+
if (!model || !prompt || seconds === undefined) {
|
|
8
|
+
throw new Error('Usage: bare examples/moss-sfx-fit.js <model.gguf> <prompt> <seconds>')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const fit = TTSGgml.assessFit({
|
|
12
|
+
engineType: 'moss-sfx',
|
|
13
|
+
mossSoundEffectPath: model,
|
|
14
|
+
prompt,
|
|
15
|
+
seconds: Number(seconds),
|
|
16
|
+
useGPU: false
|
|
17
|
+
})
|
|
18
|
+
console.log(fit.report || `${fit.status}: ${fit.reason}`)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MOSS-SoundEffect text-to-sound-effects generation for @qvac/tts-ggml.
|
|
5
|
+
*
|
|
6
|
+
* MOSS-SoundEffect-v2 is a flow-matching diffusion model: a Qwen3 text
|
|
7
|
+
* encoder reads the prompt, a DiT denoises a latent over a fixed number of
|
|
8
|
+
* steps, and a DAC decoder turns it into 48 kHz mono audio of up to 30
|
|
9
|
+
* seconds. It is not a speech model: the prompt describes a sound.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* bare examples/moss-sound-effect.js "description of the sound" [seconds]
|
|
13
|
+
*
|
|
14
|
+
* Examples:
|
|
15
|
+
* bare examples/moss-sound-effect.js "Heavy rain on a tin roof with distant thunder." 10
|
|
16
|
+
* QVAC_TTS_MOSS_SFX_GPU=1 bare examples/moss-sound-effect.js "A dog barking twice." 5
|
|
17
|
+
* QVAC_TTS_MOSS_SFX_STEPS=50 bare examples/moss-sound-effect.js "Fast typing on a keyboard." 5
|
|
18
|
+
*
|
|
19
|
+
* Expects the MOSS-SoundEffect GGUF (moss-sfx-v2-f16.gguf or
|
|
20
|
+
* moss-sfx-v2-q8_0.gguf) under:
|
|
21
|
+
* models/
|
|
22
|
+
* Produce it with engines/tts/scripts/convert-moss-sfx-to-gguf.py in
|
|
23
|
+
* qvac-fabric-speech.cpp until it is published to the model registry. Every
|
|
24
|
+
* clip costs the same whatever its length (the model always denoises a
|
|
25
|
+
* 30-second latent); use a GPU for the default 100 steps.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const path = require('bare-path')
|
|
29
|
+
const proc = require('bare-process')
|
|
30
|
+
const TTSGgml = require('../')
|
|
31
|
+
const { createWav } = require('./wav-helper')
|
|
32
|
+
const { setLogger, releaseLogger } = require('../addonLogging')
|
|
33
|
+
|
|
34
|
+
const SFX_SAMPLE_RATE = 48000
|
|
35
|
+
const DEFAULT_SECONDS = 10
|
|
36
|
+
|
|
37
|
+
const argv = global.Bare ? global.Bare.argv : process.argv
|
|
38
|
+
const promptArg = argv[2]
|
|
39
|
+
const seconds = Number(argv[3] || DEFAULT_SECONDS)
|
|
40
|
+
const steps = Number(proc.env.QVAC_TTS_MOSS_SFX_STEPS || 0)
|
|
41
|
+
|
|
42
|
+
function fail(message) {
|
|
43
|
+
console.error(message)
|
|
44
|
+
if (global.Bare) global.Bare.exit(1)
|
|
45
|
+
else process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!promptArg || typeof promptArg !== 'string' || promptArg.trim().length === 0) {
|
|
49
|
+
fail('Usage: moss-sound-effect.js "<description of the sound>" [seconds]')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pkgRoot = path.join(__dirname, '..')
|
|
53
|
+
const modelDir = path.join(pkgRoot, 'models')
|
|
54
|
+
|
|
55
|
+
function buildModel() {
|
|
56
|
+
return new TTSGgml({
|
|
57
|
+
engine: TTSGgml.ENGINE_MOSS_SFX,
|
|
58
|
+
files: { modelDir },
|
|
59
|
+
config: { useGPU: proc.env.QVAC_TTS_MOSS_SFX_GPU === '1' },
|
|
60
|
+
logger: console,
|
|
61
|
+
opts: { stats: true }
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function reportStats(stats) {
|
|
66
|
+
if (!stats) return
|
|
67
|
+
console.log(
|
|
68
|
+
`Generation stats: totalTime=${stats.totalTime.toFixed(2)}s, ` +
|
|
69
|
+
`realTimeFactor=${stats.realTimeFactor.toFixed(3)}, ` +
|
|
70
|
+
`audioDuration=${stats.audioDurationMs}ms, totalSamples=${stats.totalSamples}, ` +
|
|
71
|
+
`backendDevice=${stats.backendDevice}, backendId=${stats.backendId}`
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function collectPcm(response) {
|
|
76
|
+
let buffer = []
|
|
77
|
+
await response
|
|
78
|
+
.onUpdate((data) => {
|
|
79
|
+
if (data && data.outputArray) buffer = buffer.concat(Array.from(data.outputArray))
|
|
80
|
+
})
|
|
81
|
+
.await()
|
|
82
|
+
return buffer
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
setLogger((priority, message) => {
|
|
87
|
+
if (priority > 1) return
|
|
88
|
+
const names = { 0: 'ERROR', 1: 'WARNING', 2: 'INFO', 3: 'DEBUG', 4: 'OFF' }
|
|
89
|
+
const name = names[priority] || 'UNKNOWN'
|
|
90
|
+
console.log(`[${new Date().toISOString()}] [C++ log] [${name}]: ${message}`)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const outputFile = path.join(__dirname, 'moss-sound-effect.wav')
|
|
94
|
+
const model = buildModel()
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
console.log('Loading MOSS-SoundEffect model...')
|
|
98
|
+
await model.load()
|
|
99
|
+
console.log('Model loaded.')
|
|
100
|
+
|
|
101
|
+
console.log(`Generating ${seconds}s of: "${promptArg}"`)
|
|
102
|
+
const response = await model.run({
|
|
103
|
+
input: promptArg,
|
|
104
|
+
type: 'text',
|
|
105
|
+
seconds,
|
|
106
|
+
...(steps > 0 ? { steps } : {})
|
|
107
|
+
})
|
|
108
|
+
const buffer = await collectPcm(response)
|
|
109
|
+
|
|
110
|
+
console.log('Generation finished!')
|
|
111
|
+
reportStats(response.stats)
|
|
112
|
+
|
|
113
|
+
createWav(buffer, SFX_SAMPLE_RATE, outputFile)
|
|
114
|
+
console.log(`Finished writing to ${outputFile}`)
|
|
115
|
+
} finally {
|
|
116
|
+
await model.unload()
|
|
117
|
+
releaseLogger()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch((err) => {
|
|
122
|
+
console.error(err)
|
|
123
|
+
if (global.Bare) global.Bare.exit(1)
|
|
124
|
+
else process.exit(1)
|
|
125
|
+
})
|