@qvac/tts-ggml 0.9.2 → 0.10.1

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.
Files changed (42) hide show
  1. package/README.md +330 -36
  2. package/docs/pocket-tts.md +201 -0
  3. package/examples/moss-dialogue-tts.js +113 -0
  4. package/examples/moss-tts.js +154 -0
  5. package/examples/pocket-tts.js +51 -0
  6. package/examples/supertonic-chunk-stream-tts.js +156 -0
  7. package/examples/supertonic-sentence-stream-tts.js +4 -4
  8. package/index.d.ts +352 -29
  9. package/index.js +698 -52
  10. package/lib/fit.d.ts +127 -0
  11. package/lib/fit.js +29 -0
  12. package/lib/pocketConfig.d.ts +10 -0
  13. package/lib/pocketConfig.js +145 -0
  14. package/package.json +16 -9
  15. package/test/integration/addon.test.js +13 -7
  16. package/test/integration/audio8.test.js +7 -0
  17. package/test/integration/chatterbox-mtl.test.js +20 -9
  18. package/test/integration/chatterbox-speed.test.js +16 -3
  19. package/test/integration/cosyvoice3-clone.test.js +17 -2
  20. package/test/integration/cosyvoice3-lavasr.test.js +21 -11
  21. package/test/integration/cosyvoice3.test.js +33 -8
  22. package/test/integration/fit.test.js +174 -0
  23. package/test/integration/gpu-smoke.test.js +13 -0
  24. package/test/integration/lavasr-enhancer.test.js +107 -5
  25. package/test/integration/moss.test.js +287 -0
  26. package/test/integration/multiple-runs.test.js +84 -57
  27. package/test/integration/output-sample-rate.test.js +10 -2
  28. package/test/integration/parler-wer.test.js +14 -9
  29. package/test/integration/parler.test.js +54 -24
  30. package/test/integration/pocket.integration.test.js +156 -0
  31. package/test/integration/supertonic-mtl.test.js +2 -0
  32. package/test/integration/supertonic.test.js +70 -0
  33. package/test/integration/supertonic3-quant.test.js +2 -0
  34. package/test/integration/voice-controls.test.js +56 -0
  35. package/test/mobile/integration.auto.cjs +16 -1
  36. package/test/mobile/pocket-worklet-host.cpp +92 -0
  37. package/test/mobile/pocket-worklet.cjs +47 -0
  38. package/test/mobile/test-groups.json +8 -4
  39. package/test/utils/runCosyvoiceTTS.js +1 -0
  40. package/test/utils/runTTS.js +6 -11
  41. package/tts.d.ts +13 -2
  42. package/tts.js +3 -2
@@ -18,9 +18,12 @@ const {
18
18
  cosyvoiceBaseFileNames
19
19
  } = require('../utils/downloadModel')
20
20
  const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
21
+ const { TTS_TEST_THREADS } = require('../utils/testThreads')
22
+ const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
21
23
 
22
24
  const platform = os.platform()
23
25
  const isMobile = platform === 'ios' || platform === 'android'
26
+ const useGPU = GPU_ONLY_USE_GPU
24
27
 
25
28
  function getBaseDir() {
26
29
  return isMobile && global.testDir ? global.testDir : '.'
@@ -66,7 +69,12 @@ function samplesDiffer(a, b) {
66
69
  }
67
70
 
68
71
  async function synthOnce(t, loadParams, label) {
69
- const model = await loadCosyvoiceTTS({ ...loadParams, seed: CLONE_SEED })
72
+ const model = await loadCosyvoiceTTS({
73
+ threads: TTS_TEST_THREADS,
74
+ ...loadParams,
75
+ seed: CLONE_SEED,
76
+ useGPU
77
+ })
70
78
  try {
71
79
  const result = await runCosyvoiceTTS(
72
80
  model,
@@ -76,6 +84,11 @@ async function synthOnce(t, loadParams, label) {
76
84
  console.log(result.output)
77
85
  t.ok(result.passed, `${label} synth passes expectations`)
78
86
  t.ok(result.data.sampleCount > 0, `${label} produced audio`)
87
+ t.is(
88
+ result.data.stats?.backendDevice,
89
+ useGPU ? 1 : 0,
90
+ `${label} runs on the ${useGPU ? 'GPU' : 'CPU'} selected by the CI lane`
91
+ )
79
92
  return result.data
80
93
  } finally {
81
94
  try {
@@ -86,7 +99,7 @@ async function synthOnce(t, loadParams, label) {
86
99
 
87
100
  test(
88
101
  'CosyVoice3 cloning (ggml): baked, zero-shot and cross-lingual all differ at one seed',
89
- { timeout: 1800000 },
102
+ { timeout: 1800000, skip: NO_GPU },
90
103
  async (t) => {
91
104
  const modelDir = await ensureCloneReadyModelDir(t)
92
105
  if (!modelDir) return
@@ -127,6 +140,7 @@ test(
127
140
  t.exception(
128
141
  () =>
129
142
  new TTSGgml({
143
+ threads: TTS_TEST_THREADS,
130
144
  engine: TTSGgml.ENGINE_COSYVOICE3,
131
145
  referenceAudio: resolveRefWavPath({}),
132
146
  files: {
@@ -167,6 +181,7 @@ test(
167
181
  }
168
182
 
169
183
  const model = new TTSGgml({
184
+ threads: TTS_TEST_THREADS,
170
185
  engine: TTSGgml.ENGINE_COSYVOICE3,
171
186
  files: { cosyvoiceModelDir: baseOnlyDir },
172
187
  referenceAudio: resolveRefWavPath({})
@@ -7,7 +7,9 @@
7
7
  // CosyVoice3's ~2.4 GB model dir, while this one is listed on the dedicated
8
8
  // 'cosyvoice' row that pre-stages it (see test/mobile/test-groups.json).
9
9
  //
10
- // Text is kept short to bound CPU LM-decode time in CI, as in cosyvoice3.test.js.
10
+ // As in cosyvoice3.test.js, the model-backed tests run on the GPU only and are
11
+ // skipped on CPU-only runners (NO_GPU=true): even one short CosyVoice3 CPU
12
+ // synthesis costs minutes there.
11
13
  //
12
14
  // Stage the enhancer GGUF via scripts/convert-lavasr-enhancer-to-gguf.py (from
13
15
  // the public LavaSRcpp ONNX release) into models/lavasr/lavasr-enhancer.gguf,
@@ -24,6 +26,8 @@ const {
24
26
  ensureLavaSRDenoiserGguf,
25
27
  ensureCosyvoiceModel
26
28
  } = require('../utils/downloadModel')
29
+ const { TTS_TEST_THREADS } = require('../utils/testThreads')
30
+ const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
27
31
 
28
32
  const platform = os.platform()
29
33
  const isMobile = platform === 'ios' || platform === 'android'
@@ -117,7 +121,7 @@ async function stageCosyvoice(t, baseDir) {
117
121
 
118
122
  test(
119
123
  'CosyVoice3 + LavaSR enhancer (batch) reports 48 kHz enhanced output',
120
- { timeout: 900000, skip: SKIP_COSYVOICE },
124
+ { timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
121
125
  async (t) => {
122
126
  const baseDir = getBaseDir()
123
127
  const enh = await stageEnhancer(t, baseDir)
@@ -126,9 +130,10 @@ test(
126
130
  if (!dl) return
127
131
 
128
132
  const model = new TTSGgml({
133
+ threads: TTS_TEST_THREADS,
129
134
  engine: TTSGgml.ENGINE_COSYVOICE3,
130
135
  files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
131
- config: { language: 'en', useGPU: false },
136
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
132
137
  opts: { stats: true }
133
138
  })
134
139
  await model.load()
@@ -139,8 +144,8 @@ test(
139
144
  t.ok(r.stats, 'runtimeStats returned (constructed with stats:true)')
140
145
  t.is(
141
146
  r.stats.enhancerBackendDevice,
142
- 0,
143
- 'enhancer loaded and ran on CPU (enhancerBackendDevice=0, not -1 = never loaded)'
147
+ r.stats.backendDevice,
148
+ 'enhancer loaded and ran on the engine device (not -1 = never loaded)'
144
149
  )
145
150
  } finally {
146
151
  try {
@@ -152,7 +157,7 @@ test(
152
157
 
153
158
  test(
154
159
  'CosyVoice3 + LavaSR enhancer + native chunk streaming emits 48 kHz chunks',
155
- { timeout: 900000, skip: SKIP_COSYVOICE },
160
+ { timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
156
161
  async (t) => {
157
162
  const baseDir = getBaseDir()
158
163
  const enh = await stageEnhancer(t, baseDir)
@@ -161,10 +166,11 @@ test(
161
166
  if (!dl) return
162
167
 
163
168
  const model = new TTSGgml({
169
+ threads: TTS_TEST_THREADS,
164
170
  engine: TTSGgml.ENGINE_COSYVOICE3,
165
171
  files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
166
172
  streamChunkTokens: 25,
167
- config: { language: 'en', useGPU: false },
173
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
168
174
  opts: { stats: true }
169
175
  })
170
176
  await model.load()
@@ -188,7 +194,7 @@ test(
188
194
 
189
195
  test(
190
196
  'CosyVoice3 + enhancer + streaming resamples every chunk to outputSampleRate',
191
- { timeout: 900000, skip: SKIP_COSYVOICE },
197
+ { timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
192
198
  async (t) => {
193
199
  // A non-native rate while streaming is only legal because the enhancer's
194
200
  // overlap-reprocess window folds the resample in seam-free. Without this the
@@ -201,10 +207,11 @@ test(
201
207
  if (!dl) return
202
208
 
203
209
  const model = new TTSGgml({
210
+ threads: TTS_TEST_THREADS,
204
211
  engine: TTSGgml.ENGINE_COSYVOICE3,
205
212
  files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
206
213
  streamChunkTokens: 25,
207
- config: { language: 'en', useGPU: false, outputSampleRate: 16000 },
214
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU, outputSampleRate: 16000 },
208
215
  opts: { stats: true }
209
216
  })
210
217
  await model.load()
@@ -224,7 +231,7 @@ test(
224
231
 
225
232
  test(
226
233
  'CosyVoice3 + LavaSR denoiser changes the emitted PCM',
227
- { timeout: 900000, skip: SKIP_COSYVOICE },
234
+ { timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
228
235
  async (t) => {
229
236
  const baseDir = getBaseDir()
230
237
  const dl = await stageCosyvoice(t, baseDir)
@@ -240,9 +247,10 @@ test(
240
247
 
241
248
  const synth = async (files) => {
242
249
  const model = new TTSGgml({
250
+ threads: TTS_TEST_THREADS,
243
251
  engine: TTSGgml.ENGINE_COSYVOICE3,
244
252
  files: Object.assign({ cosyvoiceModelDir: dl.modelDir }, files),
245
- config: { language: 'en', useGPU: false, seed: SEED },
253
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU, seed: SEED },
246
254
  opts: { stats: true }
247
255
  })
248
256
  await model.load()
@@ -289,9 +297,11 @@ test(
289
297
  if (!dl) return
290
298
 
291
299
  const badEnhancer = path.join(baseDir, 'models', 'lavasr', 'invalid-enhancer.gguf')
300
+ fs.mkdirSync(path.dirname(badEnhancer), { recursive: true })
292
301
  fs.writeFileSync(badEnhancer, 'not a gguf')
293
302
 
294
303
  const model = new TTSGgml({
304
+ threads: TTS_TEST_THREADS,
295
305
  engine: TTSGgml.ENGINE_COSYVOICE3,
296
306
  files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: badEnhancer },
297
307
  config: { language: 'en', useGPU: false },
@@ -3,8 +3,10 @@
3
3
  // CosyVoice3 engine integration smoke: real synthesis in the desktop
4
4
  // `run-integration-tests` lane. Mirrors parler.test.js (download model → build
5
5
  // TTSGgml → run → assert) but for the directory-consuming CosyVoice3 engine
6
- // (Qwen2 speech LM + DiT flow + CausalHiFT vocoder; native 24 kHz, CPU-only).
7
- // Text is kept SHORT to bound CPU LM-decode time in CI.
6
+ // (Qwen2 speech LM + DiT flow + CausalHiFT vocoder; native 24 kHz).
7
+ // Even one short CPU synthesis costs minutes on the CPU-only runners, so these
8
+ // tests run on the GPU only and are skipped there (NO_GPU=true); the CPU crash
9
+ // check is the CosyVoice3 CPU smoke in gpu-smoke.test.js.
8
10
 
9
11
  const os = require('bare-os')
10
12
  const path = require('bare-path')
@@ -12,6 +14,8 @@ const test = require('brittle')
12
14
 
13
15
  const { loadCosyvoiceTTS, runCosyvoiceTTS } = require('../utils/runCosyvoiceTTS')
14
16
  const { ensureCosyvoiceModel } = require('../utils/downloadModel')
17
+ const { TTS_TEST_THREADS } = require('../utils/testThreads')
18
+ const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
15
19
 
16
20
  const platform = os.platform()
17
21
  const isMobile = platform === 'ios' || platform === 'android'
@@ -26,7 +30,7 @@ const MODEL_MISSING =
26
30
 
27
31
  test(
28
32
  'CosyVoice3 TTS (ggml): outputSampleRate=16000 resamples and reports 16 kHz',
29
- { timeout: 600000 },
33
+ { timeout: 600000, skip: NO_GPU },
30
34
  async (t) => {
31
35
  const baseDir = getBaseDir()
32
36
  const download = await ensureCosyvoiceModel({
@@ -40,8 +44,10 @@ test(
40
44
  // Second model with outputSampleRate wired through the config surface, so the
41
45
  // whole resample path (engine → addon → reported chunk) is exercised end-to-end.
42
46
  const model = await loadCosyvoiceTTS({
47
+ threads: TTS_TEST_THREADS,
43
48
  cosyvoiceModelDir: download.modelDir,
44
- outputSampleRate: 16000
49
+ outputSampleRate: 16000,
50
+ useGPU: GPU_ONLY_USE_GPU
45
51
  })
46
52
  try {
47
53
  const text = 'Hello from CosyVoice.'
@@ -68,7 +74,7 @@ function samplesEqual(a, b) {
68
74
 
69
75
  test(
70
76
  'CosyVoice3 TTS (ggml): emotion changes the audio; per-call switch needs no reload',
71
- { timeout: 900000 },
77
+ { timeout: 900000, skip: NO_GPU },
72
78
  async (t) => {
73
79
  const baseDir = getBaseDir()
74
80
  const download = await ensureCosyvoiceModel({
@@ -83,8 +89,10 @@ test(
83
89
  // logits, and cosyvoice reseeds per synthesis, so pinning it is the
84
90
  // difference between measuring the conditioning and measuring the sampler.
85
91
  const model = await loadCosyvoiceTTS({
92
+ threads: TTS_TEST_THREADS,
86
93
  cosyvoiceModelDir: download.modelDir,
87
- seed: 42
94
+ seed: 42,
95
+ useGPU: GPU_ONLY_USE_GPU
88
96
  })
89
97
  try {
90
98
  const text = 'Hello from CosyVoice.'
@@ -119,6 +127,16 @@ test(
119
127
  'plain cosyvoice reports 24 kHz native sample rate'
120
128
  )
121
129
  t.ok(plain.data.durationMs > 0, 'plain cosyvoice audio duration is > 0 ms')
130
+ // Engine StageTimings: each stage did work, the zero-shot baked voice
131
+ // prompts the LM with its speech tokens, and no denoiser is loaded.
132
+ const stats = plain.data.stats
133
+ t.ok(stats.speechTokens > 0, 'stats report the speech tokens generated')
134
+ t.ok(stats.decodeSteps > 0, 'stats report the LM decode steps')
135
+ t.ok(stats.promptSpeechTokens > 0, 'zero-shot prompts the LM with speech tokens')
136
+ for (const key of ['lmDecodeMs', 'ditEulerMs', 'hiftDecodeMs', 'stageTotalMs']) {
137
+ t.ok(stats[key] > 0, `stats report ${key}`)
138
+ }
139
+ t.is(stats.denoiserBackendDevice, -1, 'no denoiser loaded -> denoiserBackendDevice=-1')
122
140
  const neutral = await runCosyvoiceTTS(
123
141
  model,
124
142
  { text, perCallEmotion: 'neutral' },
@@ -159,7 +177,7 @@ test(
159
177
 
160
178
  test(
161
179
  'CosyVoice3 TTS (ggml): instruct conditioning produces audio',
162
- { timeout: 600000 },
180
+ { timeout: 600000, skip: NO_GPU },
163
181
  async (t) => {
164
182
  const baseDir = getBaseDir()
165
183
  const download = await ensureCosyvoiceModel({
@@ -171,8 +189,10 @@ test(
171
189
  }
172
190
 
173
191
  const model = await loadCosyvoiceTTS({
192
+ threads: TTS_TEST_THREADS,
174
193
  cosyvoiceModelDir: download.modelDir,
175
- emotion: 'happy'
194
+ emotion: 'happy',
195
+ useGPU: GPU_ONLY_USE_GPU
176
196
  })
177
197
  try {
178
198
  const text = 'Hello from CosyVoice.'
@@ -182,6 +202,11 @@ test(
182
202
  t.ok(result.passed, 'cosyvoice instruct synth passes expectations')
183
203
  t.ok(result.data.sampleCount > 0, 'cosyvoice instruct produced audio')
184
204
  t.is(result.data.reportedSampleRate, 24000, 'cosyvoice instruct reports 24 kHz')
205
+ t.is(
206
+ result.data.stats.promptSpeechTokens,
207
+ 0,
208
+ 'instruct mode drops the prompt speech tokens (StageTimings)'
209
+ )
185
210
  } finally {
186
211
  try {
187
212
  await model.unload()
@@ -0,0 +1,174 @@
1
+ 'use strict'
2
+
3
+ const fs = require('bare-fs')
4
+ const os = require('bare-os')
5
+ const path = require('bare-path')
6
+ const test = require('brittle')
7
+
8
+ const TTSGgml = require('@qvac/tts-ggml')
9
+ const { ensureSupertonicModel } = require('../utils/downloadModel')
10
+
11
+ const TEST_TIMEOUT_MS = 900000
12
+
13
+ let modelPath = null
14
+
15
+ async function supertonicPath(t) {
16
+ if (modelPath === null) {
17
+ const resolved = await ensureSupertonicModel()
18
+ modelPath = resolved.success ? resolved.path : ''
19
+ }
20
+ if (modelPath === '') {
21
+ t.pass('Supertonic GGUF unavailable on this runner')
22
+ return null
23
+ }
24
+ return modelPath
25
+ }
26
+
27
+ /**
28
+ * The supertonic fitter covers the fused graph path: a validated GPU, or a CPU
29
+ * without the Accelerate pointwise kernels. A runner outside that answers
30
+ * `compute-path-not-supported`. That is a refusal, and it carries no figures.
31
+ */
32
+ function projected(t, fit) {
33
+ if (fit.reason === 'compute-path-not-supported') {
34
+ t.pass('this runner’s compute path is not modelled')
35
+ return false
36
+ }
37
+ return true
38
+ }
39
+
40
+ function tempDir(t) {
41
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tts-fit-'))
42
+ t.teardown(() => fs.rmSync(dir, { recursive: true, force: true }))
43
+ return dir
44
+ }
45
+
46
+ /**
47
+ * The registry's weightless copy of a GGUF: the header and tensor table as
48
+ * written, and a hole where the tensor data was. The file keeps its length so
49
+ * every offset in the header still resolves.
50
+ */
51
+ function weightlessCopy(dir, sourcePath, headerBytes) {
52
+ const target = path.join(dir, 'weightless.gguf')
53
+ const source = fs.openSync(sourcePath, 'r')
54
+ const header = Buffer.alloc(headerBytes)
55
+ fs.readSync(source, header, 0, headerBytes, 0)
56
+ fs.closeSync(source)
57
+
58
+ fs.writeFileSync(target, header)
59
+ fs.truncateSync(target, fs.statSync(sourcePath).size)
60
+ return target
61
+ }
62
+
63
+ test('a projection is internally consistent', { timeout: TEST_TIMEOUT_MS }, async (t) => {
64
+ const model = await supertonicPath(t)
65
+ if (model === null) return
66
+
67
+ const fit = TTSGgml.assessFit({
68
+ engineType: 'supertonic',
69
+ supertonicModelPath: model,
70
+ useGPU: true
71
+ })
72
+
73
+ t.comment(`status=${fit.status} reason=${fit.reason} device=${fit.deviceName}`)
74
+ if (!projected(t, fit)) return
75
+ t.ok(fit.status === 'fits' || fit.status === 'does-not-fit', 'a verdict, not an error')
76
+ t.ok(fit.deviceTotalBytes > 0, 'the device reported its capacity')
77
+ t.ok(fit.deviceFreeBytes <= fit.deviceTotalBytes, 'free never exceeds total')
78
+ t.ok(fit.weightsBytes > 0, 'the weights were measured')
79
+ t.ok(fit.deviceBytes >= fit.weightsBytes, 'weights are part of the device demand')
80
+ t.is(fit.lavasrFileBytes, 0, 'no LavaSR stage was named')
81
+ })
82
+
83
+ test('a weightless copy projects the same', { timeout: TEST_TIMEOUT_MS }, async (t) => {
84
+ const model = await supertonicPath(t)
85
+ if (model === null) return
86
+
87
+ const full = TTSGgml.assessFit({
88
+ engineType: 'supertonic',
89
+ supertonicModelPath: model,
90
+ useGPU: true
91
+ })
92
+ if (!projected(t, full)) return
93
+ const stub = TTSGgml.assessFit({
94
+ engineType: 'supertonic',
95
+ supertonicModelPath: weightlessCopy(tempDir(t), model, 1024 * 1024),
96
+ useGPU: true
97
+ })
98
+
99
+ t.is(stub.status, full.status, 'the same verdict')
100
+ t.is(stub.weightsBytes, full.weightsBytes, 'the same weight footprint')
101
+ t.is(stub.deviceBytes, full.deviceBytes, 'the same device demand')
102
+ t.is(stub.hostBytes, full.hostBytes, 'the same host demand')
103
+ })
104
+
105
+ test('the load’s offload keys reach the projection', { timeout: TEST_TIMEOUT_MS }, async (t) => {
106
+ const model = await supertonicPath(t)
107
+ if (model === null) return
108
+
109
+ const cpu = TTSGgml.assessFit({
110
+ engineType: 'supertonic',
111
+ supertonicModelPath: model,
112
+ useGPU: false
113
+ })
114
+ const pinned = TTSGgml.assessFit({
115
+ engineType: 'supertonic',
116
+ supertonicModelPath: model,
117
+ nGpuLayers: 0
118
+ })
119
+
120
+ t.ok(cpu.deviceIsCpu, 'useGPU false projects on the CPU device')
121
+ t.is(pinned.deviceIsCpu, cpu.deviceIsCpu, 'nGpuLayers 0 agrees with useGPU false')
122
+ })
123
+
124
+ test('a LavaSR stage is sized beside the projection', { timeout: TEST_TIMEOUT_MS }, async (t) => {
125
+ const model = await supertonicPath(t)
126
+ if (model === null) return
127
+
128
+ const dir = tempDir(t)
129
+ const enhancer = path.join(dir, 'enhancer.gguf')
130
+ fs.writeFileSync(enhancer, Buffer.alloc(4096, 1))
131
+
132
+ const fit = TTSGgml.assessFit({
133
+ engineType: 'supertonic',
134
+ supertonicModelPath: model,
135
+ useGPU: true,
136
+ lavasrEnhancerPath: enhancer
137
+ })
138
+
139
+ t.is(fit.lavasrFileBytes, 4096, 'the enhancer is reported at its size on disk')
140
+ if (!projected(t, fit)) return
141
+ t.ok(fit.lavasrFileBytes < fit.deviceBytes, 'reported beside the projection')
142
+ })
143
+
144
+ test('a voice with no fitter is an outcome, not a throw', (t) => {
145
+ const fit = TTSGgml.assessFit({
146
+ engineType: 'moss',
147
+ mossBackbonePath: '/models/moss-tts-delay.gguf'
148
+ })
149
+
150
+ t.is(fit.status, 'error')
151
+ t.is(fit.reason, 'unsupported-engine')
152
+ t.is(fit.modelVariant, 'moss')
153
+ })
154
+
155
+ test('a model that cannot be read is an outcome, not a throw', (t) => {
156
+ const fit = TTSGgml.assessFit({
157
+ engineType: 'supertonic',
158
+ supertonicModelPath: '/nonexistent/model.gguf'
159
+ })
160
+
161
+ t.is(fit.status, 'error')
162
+ })
163
+
164
+ test('a count that is not a count is refused', (t) => {
165
+ t.exception(
166
+ () =>
167
+ TTSGgml.assessFit({
168
+ engineType: 'supertonic',
169
+ supertonicModelPath: '/models/supertonic.gguf',
170
+ textTokens: -1
171
+ }),
172
+ /non-negative count/
173
+ )
174
+ })
@@ -45,6 +45,7 @@ const {
45
45
  ensureCosyvoiceModel
46
46
  } = require('../utils/downloadModel')
47
47
  const { recordTtsStats } = require('../utils/perf-helper')
48
+ const { TTS_TEST_THREADS } = require('../utils/testThreads')
48
49
 
49
50
  const platform = os.platform()
50
51
  const isMobile = platform === 'ios' || platform === 'android'
@@ -228,6 +229,7 @@ test(
228
229
  }
229
230
 
230
231
  const model = await loadChatterboxTTS({
232
+ threads: TTS_TEST_THREADS,
231
233
  modelDir: download.targetDir,
232
234
  refWavPath,
233
235
  language: 'en',
@@ -287,6 +289,7 @@ test(
287
289
  }
288
290
 
289
291
  const model = await loadChatterboxTTS({
292
+ threads: TTS_TEST_THREADS,
290
293
  modelDir: download.targetDir,
291
294
  t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
292
295
  s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
@@ -338,6 +341,7 @@ test(
338
341
  const supertonicPath = download.path || path.join(modelsDir, 'supertonic.gguf')
339
342
 
340
343
  const model = await loadSupertonicTTS({
344
+ threads: TTS_TEST_THREADS,
341
345
  supertonicModelPath: supertonicPath,
342
346
  language: 'en',
343
347
  voice: 'F1',
@@ -385,6 +389,7 @@ test(
385
389
  }
386
390
 
387
391
  const model = await loadSupertonicTTS({
392
+ threads: TTS_TEST_THREADS,
388
393
  supertonicModelPath: download.path,
389
394
  language: 'en',
390
395
  voice: 'F1',
@@ -431,6 +436,7 @@ test(
431
436
  }
432
437
 
433
438
  const model = await loadSupertonicTTS({
439
+ threads: TTS_TEST_THREADS,
434
440
  supertonicModelPath: download.path,
435
441
  language: 'en',
436
442
  voice: 'F1',
@@ -488,6 +494,7 @@ test(
488
494
  }
489
495
 
490
496
  const model = await loadChatterboxTTS({
497
+ threads: TTS_TEST_THREADS,
491
498
  modelDir: download.targetDir,
492
499
  refWavPath,
493
500
  language: 'en',
@@ -532,6 +539,7 @@ test(
532
539
  const supertonicPath = download.path || path.join(modelsDir, 'supertonic.gguf')
533
540
 
534
541
  const model = await loadSupertonicTTS({
542
+ threads: TTS_TEST_THREADS,
535
543
  supertonicModelPath: supertonicPath,
536
544
  language: 'en',
537
545
  voice: 'F1',
@@ -583,6 +591,7 @@ for (const v of [
583
591
  return
584
592
  }
585
593
  const model = await loadParlerTTS({
594
+ threads: TTS_TEST_THREADS,
586
595
  parlerModelPath: download.path,
587
596
  seed: 42,
588
597
  useGPU: true
@@ -625,6 +634,7 @@ for (const v of [
625
634
  return
626
635
  }
627
636
  const model = await loadParlerTTS({
637
+ threads: TTS_TEST_THREADS,
628
638
  parlerModelPath: download.path,
629
639
  seed: 42,
630
640
  useGPU: false
@@ -678,6 +688,7 @@ async function probeAndroidGpuVendor(t) {
678
688
  })
679
689
  if (!download || !download.success) return null
680
690
  const model = await loadSupertonicTTS({
691
+ threads: TTS_TEST_THREADS,
681
692
  supertonicModelPath: download.path,
682
693
  language: 'en',
683
694
  voice: 'F1',
@@ -730,6 +741,7 @@ test(
730
741
  }
731
742
  }
732
743
  const model = await loadCosyvoiceTTS({
744
+ threads: TTS_TEST_THREADS,
733
745
  cosyvoiceModelDir: download.modelDir,
734
746
  useGPU: true
735
747
  })
@@ -788,6 +800,7 @@ test(
788
800
  return
789
801
  }
790
802
  const model = await loadCosyvoiceTTS({
803
+ threads: TTS_TEST_THREADS,
791
804
  cosyvoiceModelDir: download.modelDir,
792
805
  useGPU: false
793
806
  })