@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.
- package/README.md +330 -36
- package/docs/pocket-tts.md +201 -0
- package/examples/moss-dialogue-tts.js +113 -0
- package/examples/moss-tts.js +154 -0
- package/examples/pocket-tts.js +51 -0
- package/examples/supertonic-chunk-stream-tts.js +156 -0
- package/examples/supertonic-sentence-stream-tts.js +4 -4
- package/index.d.ts +352 -29
- package/index.js +698 -52
- package/lib/fit.d.ts +127 -0
- package/lib/fit.js +29 -0
- package/lib/pocketConfig.d.ts +10 -0
- package/lib/pocketConfig.js +145 -0
- package/package.json +16 -9
- package/test/integration/addon.test.js +13 -7
- package/test/integration/audio8.test.js +7 -0
- package/test/integration/chatterbox-mtl.test.js +20 -9
- package/test/integration/chatterbox-speed.test.js +16 -3
- package/test/integration/cosyvoice3-clone.test.js +17 -2
- package/test/integration/cosyvoice3-lavasr.test.js +21 -11
- package/test/integration/cosyvoice3.test.js +33 -8
- package/test/integration/fit.test.js +174 -0
- package/test/integration/gpu-smoke.test.js +13 -0
- package/test/integration/lavasr-enhancer.test.js +107 -5
- package/test/integration/moss.test.js +287 -0
- package/test/integration/multiple-runs.test.js +84 -57
- package/test/integration/output-sample-rate.test.js +10 -2
- package/test/integration/parler-wer.test.js +14 -9
- package/test/integration/parler.test.js +54 -24
- package/test/integration/pocket.integration.test.js +156 -0
- package/test/integration/supertonic-mtl.test.js +2 -0
- package/test/integration/supertonic.test.js +70 -0
- package/test/integration/supertonic3-quant.test.js +2 -0
- package/test/integration/voice-controls.test.js +56 -0
- package/test/mobile/integration.auto.cjs +16 -1
- package/test/mobile/pocket-worklet-host.cpp +92 -0
- package/test/mobile/pocket-worklet.cjs +47 -0
- package/test/mobile/test-groups.json +8 -4
- package/test/utils/runCosyvoiceTTS.js +1 -0
- package/test/utils/runTTS.js +6 -11
- package/tts.d.ts +13 -2
- package/tts.js +3 -2
|
@@ -29,13 +29,16 @@ const {
|
|
|
29
29
|
} = require('../utils/runChatterboxTTS')
|
|
30
30
|
const { loadSupertonicTTS, runSupertonicTTS } = require('../utils/runSupertonicTTS')
|
|
31
31
|
const { ensureChatterboxModels, ensureSupertonicModel } = require('../utils/downloadModel')
|
|
32
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
33
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
32
34
|
|
|
33
35
|
const platform = os.platform()
|
|
34
36
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
35
37
|
|
|
36
|
-
// Lifecycle / sequential-run
|
|
37
|
-
//
|
|
38
|
-
//
|
|
38
|
+
// Lifecycle / sequential-run tests, not GPU policy tests. The Chatterbox ones
|
|
39
|
+
// run several syntheses, so they run on the GPU only and are skipped on CPU-only
|
|
40
|
+
// runners (NO_GPU=true); the Supertonic reload keeps the package default
|
|
41
|
+
// (`useGPU: false`). Tests that *are* about GPU live in gpu-smoke.test.js.
|
|
39
42
|
|
|
40
43
|
function getBaseDir() {
|
|
41
44
|
return isMobile && global.testDir ? global.testDir : '.'
|
|
@@ -49,7 +52,7 @@ const PHRASES = [
|
|
|
49
52
|
|
|
50
53
|
test(
|
|
51
54
|
'Chatterbox: multiple sequential runs reuse the same engine instance',
|
|
52
|
-
{ timeout: 1800000 },
|
|
55
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
53
56
|
async (t) => {
|
|
54
57
|
const baseDir = getBaseDir()
|
|
55
58
|
const download = await ensureChatterboxModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -73,9 +76,11 @@ test(
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
const model = await loadChatterboxTTS({
|
|
79
|
+
threads: TTS_TEST_THREADS,
|
|
76
80
|
modelDir: download.targetDir,
|
|
77
81
|
refWavPath,
|
|
78
|
-
language: 'en'
|
|
82
|
+
language: 'en',
|
|
83
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
79
84
|
})
|
|
80
85
|
try {
|
|
81
86
|
const timings = []
|
|
@@ -109,7 +114,7 @@ test(
|
|
|
109
114
|
|
|
110
115
|
test(
|
|
111
116
|
'Chatterbox: fresh instance per run (app-restart simulation)',
|
|
112
|
-
{ timeout: 1800000 },
|
|
117
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
113
118
|
async (t) => {
|
|
114
119
|
const baseDir = getBaseDir()
|
|
115
120
|
const download = await ensureChatterboxModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -137,9 +142,11 @@ test(
|
|
|
137
142
|
for (let i = 0; i < N; i++) {
|
|
138
143
|
const t0 = Date.now()
|
|
139
144
|
const model = await loadChatterboxTTS({
|
|
145
|
+
threads: TTS_TEST_THREADS,
|
|
140
146
|
modelDir: download.targetDir,
|
|
141
147
|
refWavPath,
|
|
142
|
-
language: 'en'
|
|
148
|
+
language: 'en',
|
|
149
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
143
150
|
})
|
|
144
151
|
const loadMs = Date.now() - t0
|
|
145
152
|
try {
|
|
@@ -172,57 +179,63 @@ test(
|
|
|
172
179
|
}
|
|
173
180
|
)
|
|
174
181
|
|
|
175
|
-
test(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
test(
|
|
183
|
+
'Chatterbox: reload() between runs preserves stability',
|
|
184
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
185
|
+
async (t) => {
|
|
186
|
+
const baseDir = getBaseDir()
|
|
187
|
+
const download = await ensureChatterboxModels({ targetDir: path.join(baseDir, 'models') })
|
|
188
|
+
if (!download.success) {
|
|
189
|
+
t.fail(
|
|
190
|
+
'Chatterbox GGUFs not available - registry fetch failed. Run `npm run download-models:registry` or stage models locally.'
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
184
194
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
195
|
+
// Mobile-aware resolution: on iOS / Android the asset is staged into
|
|
196
|
+
// `Library/Caches/jfk.wav` via `global.assetPaths`; on desktop falls
|
|
197
|
+
// back to the in-tree `test/reference-audio/jfk.wav`. The bundled
|
|
198
|
+
// path is not readable from native code on iOS, which would trip
|
|
199
|
+
// `ModelFileNotFound` the moment `model.load()` reaches the C++
|
|
200
|
+
// `ChatterboxModel::validateConfig` check.
|
|
201
|
+
const refWavPath = resolveRefWavPath({})
|
|
202
|
+
if (!fs.existsSync(refWavPath)) {
|
|
203
|
+
t.pass('Skipped: reference audio missing')
|
|
204
|
+
return
|
|
205
|
+
}
|
|
196
206
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
const model = await loadChatterboxTTS({
|
|
208
|
+
threads: TTS_TEST_THREADS,
|
|
209
|
+
modelDir: download.targetDir,
|
|
210
|
+
refWavPath,
|
|
211
|
+
language: 'en',
|
|
212
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
213
|
+
})
|
|
214
|
+
try {
|
|
215
|
+
const r1 = await runChatterboxTTS(
|
|
216
|
+
model,
|
|
217
|
+
{ text: 'First run before reload.' },
|
|
218
|
+
{ minSamples: 5000 }
|
|
219
|
+
)
|
|
220
|
+
t.ok(r1.passed, 'first run before reload should pass')
|
|
209
221
|
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
await model.reload({ language: 'en' })
|
|
223
|
+
t.pass('reload() resolved')
|
|
212
224
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
const r2 = await runChatterboxTTS(
|
|
226
|
+
model,
|
|
227
|
+
{ text: 'Second run after reload.' },
|
|
228
|
+
{ minSamples: 5000 }
|
|
229
|
+
)
|
|
230
|
+
t.ok(r2.passed, 'second run after reload should pass')
|
|
231
|
+
t.ok(r2.data.sampleCount > 0, 'reloaded model produces audio')
|
|
232
|
+
} finally {
|
|
233
|
+
try {
|
|
234
|
+
await model.unload()
|
|
235
|
+
} catch (_e) {}
|
|
236
|
+
}
|
|
224
237
|
}
|
|
225
|
-
|
|
238
|
+
)
|
|
226
239
|
|
|
227
240
|
test('Supertonic: reload() between runs preserves stability', { timeout: 1800000 }, async (t) => {
|
|
228
241
|
const baseDir = getBaseDir()
|
|
@@ -235,6 +248,7 @@ test('Supertonic: reload() between runs preserves stability', { timeout: 1800000
|
|
|
235
248
|
}
|
|
236
249
|
|
|
237
250
|
const model = await loadSupertonicTTS({
|
|
251
|
+
threads: TTS_TEST_THREADS,
|
|
238
252
|
supertonicModelPath: download.path,
|
|
239
253
|
voice: 'F1',
|
|
240
254
|
language: 'en',
|
|
@@ -267,7 +281,7 @@ test('Supertonic: reload() between runs preserves stability', { timeout: 1800000
|
|
|
267
281
|
|
|
268
282
|
test(
|
|
269
283
|
'Engine swap: chatterbox -> supertonic -> chatterbox in separate instances',
|
|
270
|
-
{ timeout: 1800000 },
|
|
284
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
271
285
|
async (t) => {
|
|
272
286
|
const baseDir = getBaseDir()
|
|
273
287
|
const cb = await ensureChatterboxModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -291,7 +305,13 @@ test(
|
|
|
291
305
|
return
|
|
292
306
|
}
|
|
293
307
|
|
|
294
|
-
const c1 = await loadChatterboxTTS({
|
|
308
|
+
const c1 = await loadChatterboxTTS({
|
|
309
|
+
threads: TTS_TEST_THREADS,
|
|
310
|
+
modelDir: cb.targetDir,
|
|
311
|
+
refWavPath,
|
|
312
|
+
language: 'en',
|
|
313
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
314
|
+
})
|
|
295
315
|
try {
|
|
296
316
|
const r = await runChatterboxTTS(c1, { text: 'Hello from chatterbox.' }, { minSamples: 5000 })
|
|
297
317
|
t.ok(r.passed, 'first chatterbox instance OK')
|
|
@@ -302,10 +322,11 @@ test(
|
|
|
302
322
|
}
|
|
303
323
|
|
|
304
324
|
const s1 = await loadSupertonicTTS({
|
|
325
|
+
threads: TTS_TEST_THREADS,
|
|
305
326
|
supertonicModelPath: st.path,
|
|
306
327
|
voice: 'F1',
|
|
307
328
|
language: 'en',
|
|
308
|
-
useGPU:
|
|
329
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
309
330
|
})
|
|
310
331
|
try {
|
|
311
332
|
const r = await runSupertonicTTS(s1, { text: 'Hello from supertonic.' }, { minSamples: 5000 })
|
|
@@ -316,7 +337,13 @@ test(
|
|
|
316
337
|
} catch (_e) {}
|
|
317
338
|
}
|
|
318
339
|
|
|
319
|
-
const c2 = await loadChatterboxTTS({
|
|
340
|
+
const c2 = await loadChatterboxTTS({
|
|
341
|
+
threads: TTS_TEST_THREADS,
|
|
342
|
+
modelDir: cb.targetDir,
|
|
343
|
+
refWavPath,
|
|
344
|
+
language: 'en',
|
|
345
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
346
|
+
})
|
|
320
347
|
try {
|
|
321
348
|
const r = await runChatterboxTTS(
|
|
322
349
|
c2,
|
|
@@ -12,6 +12,8 @@ const test = require('brittle')
|
|
|
12
12
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
13
13
|
|
|
14
14
|
const { ensureSupertonicModel, ensureParlerModel } = require('../utils/downloadModel')
|
|
15
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
16
|
+
const { SKIP_PARLER_GPU_ONLY, PARLER_GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
15
17
|
|
|
16
18
|
const platform = os.platform()
|
|
17
19
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
@@ -40,6 +42,7 @@ async function collect(model, text) {
|
|
|
40
42
|
// JS half, which must forward the combination rather than reject it up front.
|
|
41
43
|
test('CosyVoice3: enhancer + streaming forwards a non-native outputSampleRate', (t) => {
|
|
42
44
|
const model = new TTSGgml({
|
|
45
|
+
threads: TTS_TEST_THREADS,
|
|
43
46
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
44
47
|
files: {
|
|
45
48
|
cosyvoiceModelDir: './models/cosyvoice3',
|
|
@@ -72,6 +75,7 @@ test(
|
|
|
72
75
|
const text = 'Output rate selection resamples the synthesized audio.'
|
|
73
76
|
|
|
74
77
|
const native = new TTSGgml({
|
|
78
|
+
threads: TTS_TEST_THREADS,
|
|
75
79
|
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
76
80
|
files: { supertonicModel: dl.path },
|
|
77
81
|
voice: 'F1',
|
|
@@ -91,6 +95,7 @@ test(
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
const resampled = new TTSGgml({
|
|
98
|
+
threads: TTS_TEST_THREADS,
|
|
94
99
|
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
95
100
|
files: { supertonicModel: dl.path },
|
|
96
101
|
voice: 'F1',
|
|
@@ -119,7 +124,7 @@ test(
|
|
|
119
124
|
|
|
120
125
|
test(
|
|
121
126
|
'Parler: outputSampleRate=16000 resamples and reports 16 kHz',
|
|
122
|
-
{ timeout: 600000 },
|
|
127
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
123
128
|
async (t) => {
|
|
124
129
|
const baseDir = getBaseDir()
|
|
125
130
|
const dl = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -131,10 +136,12 @@ test(
|
|
|
131
136
|
const text = 'Output rate selection resamples the synthesized audio.'
|
|
132
137
|
|
|
133
138
|
const native = new TTSGgml({
|
|
139
|
+
threads: TTS_TEST_THREADS,
|
|
134
140
|
engine: TTSGgml.ENGINE_PARLER,
|
|
135
141
|
files: { parlerModel: dl.path },
|
|
136
142
|
voice: 'Laura',
|
|
137
143
|
seed: 42,
|
|
144
|
+
config: { useGPU: PARLER_GPU_ONLY_USE_GPU },
|
|
138
145
|
opts: { stats: true }
|
|
139
146
|
})
|
|
140
147
|
await native.load()
|
|
@@ -150,11 +157,12 @@ test(
|
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
const resampled = new TTSGgml({
|
|
160
|
+
threads: TTS_TEST_THREADS,
|
|
153
161
|
engine: TTSGgml.ENGINE_PARLER,
|
|
154
162
|
files: { parlerModel: dl.path },
|
|
155
163
|
voice: 'Laura',
|
|
156
164
|
seed: 42,
|
|
157
|
-
config: { outputSampleRate: 16000 },
|
|
165
|
+
config: { outputSampleRate: 16000, useGPU: PARLER_GPU_ONLY_USE_GPU },
|
|
158
166
|
opts: { stats: true }
|
|
159
167
|
})
|
|
160
168
|
await resampled.load()
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// chatterbox WER leg in addon.test.js (whisper ASR + wordErrorRate).
|
|
8
8
|
//
|
|
9
9
|
// macOS-desktop only (isDarwin): the whisper WER infra is desktop-only
|
|
10
|
-
// across the tts-ggml suite. Runs on
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// across the tts-ggml suite. Runs on the Metal GPU runner only and is skipped
|
|
11
|
+
// on CPU-only runners (NO_GPU=true). large-v1 is RAM-gated; both tiers skip
|
|
12
|
+
// cleanly if their GGUF isn't staged.
|
|
13
13
|
|
|
14
14
|
const os = require('bare-os')
|
|
15
15
|
const path = require('bare-path')
|
|
@@ -19,6 +19,7 @@ const test = require('brittle')
|
|
|
19
19
|
const { loadParlerTTS, runParlerTTS } = require('../utils/runParlerTTS')
|
|
20
20
|
const { ensureParlerModel, ensureWhisperModel } = require('../utils/downloadModel')
|
|
21
21
|
const { loadWhisper, runWhisper } = require('../utils/runWhisper')
|
|
22
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
22
23
|
|
|
23
24
|
const platform = os.platform()
|
|
24
25
|
const isDarwin = platform === 'darwin'
|
|
@@ -26,10 +27,9 @@ const isApple = platform === 'darwin' || platform === 'ios'
|
|
|
26
27
|
const NO_GPU = proc.env && proc.env.NO_GPU === 'true'
|
|
27
28
|
const LARGE_MIN_RAM_BYTES = 16 * 1024 ** 3
|
|
28
29
|
|
|
29
|
-
// Calibrated 2026-07-24:
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// leg stays effectively strict (greedy would be 50-100%, so this still asserts it).
|
|
30
|
+
// Calibrated 2026-07-24: Metal diverges by FP reorder — seed 42 is 0% on
|
|
31
|
+
// Apple-Silicon but 22.2% on Intel-Mac Metal — so 0.35 tolerates the GPU
|
|
32
|
+
// variance while still asserting the words (greedy would be 50-100%).
|
|
33
33
|
const WER_THRESHOLD = 0.35
|
|
34
34
|
|
|
35
35
|
// Clear English sentences, all >= 9 words (a single ASR slip stays <= ~0.11,
|
|
@@ -50,7 +50,7 @@ function getBaseDir() {
|
|
|
50
50
|
for (const { variant } of PARLER_WER_TIERS) {
|
|
51
51
|
test(
|
|
52
52
|
`Parler WER (${variant}): English synthesis transcribes within ${(WER_THRESHOLD * 100).toFixed(0)}%`,
|
|
53
|
-
{ timeout: 1800000, skip: !isDarwin },
|
|
53
|
+
{ timeout: 1800000, skip: !isDarwin || NO_GPU },
|
|
54
54
|
async (t) => {
|
|
55
55
|
if (variant === 'large' && os.totalmem() < LARGE_MIN_RAM_BYTES) {
|
|
56
56
|
t.pass(
|
|
@@ -77,7 +77,12 @@ for (const { variant } of PARLER_WER_TIERS) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const useGPU = isApple && !NO_GPU
|
|
80
|
-
const model = await loadParlerTTS({
|
|
80
|
+
const model = await loadParlerTTS({
|
|
81
|
+
threads: TTS_TEST_THREADS,
|
|
82
|
+
parlerModelPath: download.path,
|
|
83
|
+
seed: 42,
|
|
84
|
+
useGPU
|
|
85
|
+
})
|
|
81
86
|
const entries = []
|
|
82
87
|
try {
|
|
83
88
|
for (const text of WER_SENTENCES) {
|
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
|
|
8
8
|
const os = require('bare-os')
|
|
9
9
|
const path = require('bare-path')
|
|
10
|
-
const proc = require('bare-process')
|
|
11
10
|
const test = require('brittle')
|
|
12
11
|
|
|
13
12
|
const { loadParlerTTS, runParlerTTS } = require('../utils/runParlerTTS')
|
|
14
13
|
const { ensureParlerModel } = require('../utils/downloadModel')
|
|
15
14
|
const { recordTtsStats } = require('../utils/perf-helper')
|
|
15
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
16
|
+
const { NO_GPU, SKIP_PARLER_GPU_ONLY, PARLER_GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
16
17
|
|
|
17
18
|
const platform = os.platform()
|
|
18
19
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
19
20
|
const isApple = platform === 'darwin' || platform === 'ios'
|
|
20
|
-
const NO_GPU = proc.env && proc.env.NO_GPU === 'true'
|
|
21
21
|
|
|
22
22
|
function getBaseDir() {
|
|
23
23
|
return isMobile && global.testDir ? global.testDir : '.'
|
|
@@ -39,7 +39,11 @@ test(
|
|
|
39
39
|
|
|
40
40
|
// Deliberately NO description/voice/emotion: the engine renders the
|
|
41
41
|
// models' recommended fallback caption — everything works on defaults.
|
|
42
|
-
const model = await loadParlerTTS({
|
|
42
|
+
const model = await loadParlerTTS({
|
|
43
|
+
threads: TTS_TEST_THREADS,
|
|
44
|
+
parlerModelPath: download.path,
|
|
45
|
+
seed: 42
|
|
46
|
+
})
|
|
43
47
|
try {
|
|
44
48
|
const wavPath = isMobile ? undefined : path.join(baseDir, 'test', 'output', 'parler-en.wav')
|
|
45
49
|
const text = 'The parler engine speaks with a voice controlled by a text description.'
|
|
@@ -100,7 +104,11 @@ test(
|
|
|
100
104
|
return
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
const model = await loadParlerTTS({
|
|
107
|
+
const model = await loadParlerTTS({
|
|
108
|
+
threads: TTS_TEST_THREADS,
|
|
109
|
+
parlerModelPath: download.path,
|
|
110
|
+
voice: 'Laura'
|
|
111
|
+
})
|
|
104
112
|
try {
|
|
105
113
|
const response = await model.run({
|
|
106
114
|
type: 'text',
|
|
@@ -128,7 +136,7 @@ test(
|
|
|
128
136
|
|
|
129
137
|
test(
|
|
130
138
|
'Parler TTS (ggml): description matrix — conflicts throw, per-call wins, defaults work',
|
|
131
|
-
{ timeout: 600000 },
|
|
139
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
132
140
|
async (t) => {
|
|
133
141
|
const baseDir = getBaseDir()
|
|
134
142
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -143,6 +151,7 @@ test(
|
|
|
143
151
|
t.exception(
|
|
144
152
|
() =>
|
|
145
153
|
new TTSGgml({
|
|
154
|
+
threads: TTS_TEST_THREADS,
|
|
146
155
|
engine: TTSGgml.ENGINE_PARLER,
|
|
147
156
|
files: { parlerModel: download.path },
|
|
148
157
|
description: 'A calm female voice.',
|
|
@@ -157,6 +166,7 @@ test(
|
|
|
157
166
|
t.exception(
|
|
158
167
|
() =>
|
|
159
168
|
new TTSGgml({
|
|
169
|
+
threads: TTS_TEST_THREADS,
|
|
160
170
|
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
161
171
|
files: { supertonicModel: download.path },
|
|
162
172
|
emotion: 'happy'
|
|
@@ -167,6 +177,7 @@ test(
|
|
|
167
177
|
t.exception(
|
|
168
178
|
() =>
|
|
169
179
|
new TTSGgml({
|
|
180
|
+
threads: TTS_TEST_THREADS,
|
|
170
181
|
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
171
182
|
files: { supertonicModel: download.path },
|
|
172
183
|
pitch: 'high'
|
|
@@ -176,9 +187,11 @@ test(
|
|
|
176
187
|
)
|
|
177
188
|
|
|
178
189
|
const model = await loadParlerTTS({
|
|
190
|
+
threads: TTS_TEST_THREADS,
|
|
179
191
|
parlerModelPath: download.path,
|
|
180
192
|
voice: 'Laura',
|
|
181
|
-
seed: 42
|
|
193
|
+
seed: 42,
|
|
194
|
+
useGPU: PARLER_GPU_ONLY_USE_GPU
|
|
182
195
|
})
|
|
183
196
|
try {
|
|
184
197
|
// Per-call description conflict (same level) rejects.
|
|
@@ -218,9 +231,11 @@ test(
|
|
|
218
231
|
|
|
219
232
|
// Constructor-level free-text description + per-call template rejects.
|
|
220
233
|
const descModel = await loadParlerTTS({
|
|
234
|
+
threads: TTS_TEST_THREADS,
|
|
221
235
|
parlerModelPath: download.path,
|
|
222
236
|
description: 'A calm female voice, very clear audio.',
|
|
223
|
-
seed: 42
|
|
237
|
+
seed: 42,
|
|
238
|
+
useGPU: PARLER_GPU_ONLY_USE_GPU
|
|
224
239
|
})
|
|
225
240
|
try {
|
|
226
241
|
await t.exception(
|
|
@@ -238,7 +253,7 @@ test(
|
|
|
238
253
|
|
|
239
254
|
test(
|
|
240
255
|
'Parler TTS (ggml): emotion flag changes the audio; per-call switch needs no reload',
|
|
241
|
-
{ timeout: 600000 },
|
|
256
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
242
257
|
async (t) => {
|
|
243
258
|
const baseDir = getBaseDir()
|
|
244
259
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -248,9 +263,11 @@ test(
|
|
|
248
263
|
}
|
|
249
264
|
|
|
250
265
|
const model = await loadParlerTTS({
|
|
266
|
+
threads: TTS_TEST_THREADS,
|
|
251
267
|
parlerModelPath: download.path,
|
|
252
268
|
voice: 'Laura',
|
|
253
|
-
seed: 42
|
|
269
|
+
seed: 42,
|
|
270
|
+
useGPU: PARLER_GPU_ONLY_USE_GPU
|
|
254
271
|
})
|
|
255
272
|
try {
|
|
256
273
|
const text = 'The weather is wonderful today.'
|
|
@@ -285,7 +302,7 @@ test(
|
|
|
285
302
|
|
|
286
303
|
test(
|
|
287
304
|
'Parler TTS (ggml): fixed seed is deterministic across runs and reload',
|
|
288
|
-
{ timeout: 600000 },
|
|
305
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
289
306
|
async (t) => {
|
|
290
307
|
const baseDir = getBaseDir()
|
|
291
308
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -295,10 +312,12 @@ test(
|
|
|
295
312
|
}
|
|
296
313
|
|
|
297
314
|
const model = await loadParlerTTS({
|
|
315
|
+
threads: TTS_TEST_THREADS,
|
|
298
316
|
parlerModelPath: download.path,
|
|
299
317
|
voice: 'Laura',
|
|
300
318
|
emotion: 'neutral',
|
|
301
|
-
seed: 42
|
|
319
|
+
seed: 42,
|
|
320
|
+
useGPU: PARLER_GPU_ONLY_USE_GPU
|
|
302
321
|
})
|
|
303
322
|
try {
|
|
304
323
|
const text = 'Deterministic synthesis check.'
|
|
@@ -329,7 +348,7 @@ test(
|
|
|
329
348
|
|
|
330
349
|
test(
|
|
331
350
|
'Parler TTS (ggml): runStream emits per-sentence chunks with one pinned description',
|
|
332
|
-
{ timeout: 600000 },
|
|
351
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
333
352
|
async (t) => {
|
|
334
353
|
const baseDir = getBaseDir()
|
|
335
354
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -340,9 +359,11 @@ test(
|
|
|
340
359
|
|
|
341
360
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
342
361
|
const model = new TTSGgml({
|
|
362
|
+
threads: TTS_TEST_THREADS,
|
|
343
363
|
engine: TTSGgml.ENGINE_PARLER,
|
|
344
364
|
files: { parlerModel: download.path },
|
|
345
365
|
seed: 42,
|
|
366
|
+
config: { useGPU: PARLER_GPU_ONLY_USE_GPU },
|
|
346
367
|
opts: { stats: true }
|
|
347
368
|
})
|
|
348
369
|
await model.load()
|
|
@@ -376,7 +397,7 @@ test(
|
|
|
376
397
|
|
|
377
398
|
test(
|
|
378
399
|
'Parler TTS (ggml): native streaming emits monotonic chunks equal to batch',
|
|
379
|
-
{ timeout: 900000 },
|
|
400
|
+
{ timeout: 900000, skip: SKIP_PARLER_GPU_ONLY },
|
|
380
401
|
async (t) => {
|
|
381
402
|
const baseDir = getBaseDir()
|
|
382
403
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -392,6 +413,7 @@ test(
|
|
|
392
413
|
|
|
393
414
|
async function synth(extra) {
|
|
394
415
|
const model = new TTSGgml({
|
|
416
|
+
threads: TTS_TEST_THREADS,
|
|
395
417
|
engine: TTSGgml.ENGINE_PARLER,
|
|
396
418
|
files: { parlerModel: download.path },
|
|
397
419
|
seed: 42,
|
|
@@ -481,6 +503,7 @@ test(
|
|
|
481
503
|
}
|
|
482
504
|
|
|
483
505
|
const model = await loadParlerTTS({
|
|
506
|
+
threads: TTS_TEST_THREADS,
|
|
484
507
|
parlerModelPath: download.path,
|
|
485
508
|
voice: 'Rohit',
|
|
486
509
|
emotion: 'happy',
|
|
@@ -507,22 +530,24 @@ test(
|
|
|
507
530
|
|
|
508
531
|
// Desktop quant + backend coverage: q8_0 + f16 for indic/mini, q8_0 for large.
|
|
509
532
|
// Runs on the platform's GPU-if-available backend (Metal on Apple GPU runners,
|
|
510
|
-
// CPU otherwise).
|
|
511
|
-
//
|
|
512
|
-
//
|
|
533
|
+
// CPU otherwise). mini f16 and large are the slowest tiers on CPU (roughly
|
|
534
|
+
// 45-80 s per synthesis), so they run on Metal only. large self-gates on RAM; any tier that isn't
|
|
535
|
+
// staged skips (t.pass) so a not-yet-registered tier never fails CI. Registered
|
|
536
|
+
// per PR #3372. Desktop-only: the mobile Parler surface is covered by the
|
|
537
|
+
// gpu-smoke legs.
|
|
513
538
|
const PARLER_QUANT_MATRIX = [
|
|
514
|
-
{ variant: 'indic', quant: 'q8_0' },
|
|
515
|
-
{ variant: 'indic', quant: 'f16' },
|
|
516
|
-
{ variant: 'mini', quant: 'q8_0' },
|
|
517
|
-
{ variant: 'mini', quant: 'f16' },
|
|
518
|
-
{ variant: 'large', quant: 'q8_0' }
|
|
539
|
+
{ variant: 'indic', quant: 'q8_0', gpuOnly: false },
|
|
540
|
+
{ variant: 'indic', quant: 'f16', gpuOnly: false },
|
|
541
|
+
{ variant: 'mini', quant: 'q8_0', gpuOnly: false },
|
|
542
|
+
{ variant: 'mini', quant: 'f16', gpuOnly: true },
|
|
543
|
+
{ variant: 'large', quant: 'q8_0', gpuOnly: true }
|
|
519
544
|
]
|
|
520
545
|
const LARGE_MIN_RAM_BYTES = 16 * 1024 ** 3
|
|
521
546
|
|
|
522
|
-
for (const { variant, quant } of PARLER_QUANT_MATRIX) {
|
|
547
|
+
for (const { variant, quant, gpuOnly } of PARLER_QUANT_MATRIX) {
|
|
523
548
|
test(
|
|
524
549
|
`Parler TTS (ggml): ${variant} ${quant} synthesizes 44.1 kHz audio on the target backend`,
|
|
525
|
-
{ timeout: 900000, skip: isMobile },
|
|
550
|
+
{ timeout: 900000, skip: isMobile || (gpuOnly && SKIP_PARLER_GPU_ONLY) },
|
|
526
551
|
async (t) => {
|
|
527
552
|
if (variant === 'large' && os.totalmem() < LARGE_MIN_RAM_BYTES) {
|
|
528
553
|
t.pass(
|
|
@@ -542,7 +567,12 @@ for (const { variant, quant } of PARLER_QUANT_MATRIX) {
|
|
|
542
567
|
}
|
|
543
568
|
|
|
544
569
|
const useGPU = isApple && !NO_GPU
|
|
545
|
-
const model = await loadParlerTTS({
|
|
570
|
+
const model = await loadParlerTTS({
|
|
571
|
+
threads: TTS_TEST_THREADS,
|
|
572
|
+
parlerModelPath: download.path,
|
|
573
|
+
seed: 42,
|
|
574
|
+
useGPU
|
|
575
|
+
})
|
|
546
576
|
try {
|
|
547
577
|
const text =
|
|
548
578
|
variant === 'indic'
|