@qvac/tts-ggml 0.10.0 → 0.10.2
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 +13 -3
- package/package.json +8 -6
- package/test/integration/addon.test.js +8 -7
- package/test/integration/chatterbox-mtl.test.js +14 -9
- package/test/integration/chatterbox-speed.test.js +4 -2
- package/test/integration/cosyvoice3-clone.test.js +3 -3
- package/test/integration/cosyvoice3-lavasr.test.js +15 -11
- package/test/integration/cosyvoice3.test.js +14 -8
- package/test/integration/lavasr-enhancer.test.js +3 -2
- package/test/integration/moss.test.js +21 -16
- package/test/integration/multiple-runs.test.js +69 -58
- package/test/integration/output-sample-rate.test.js +4 -2
- package/test/integration/parler-wer.test.js +7 -8
- package/test/integration/parler.test.js +27 -21
package/README.md
CHANGED
|
@@ -1275,9 +1275,10 @@ the demos still run and write the concatenated wav.
|
|
|
1275
1275
|
## Testing
|
|
1276
1276
|
|
|
1277
1277
|
```bash
|
|
1278
|
-
npm run test:unit
|
|
1279
|
-
npm run test:integration
|
|
1280
|
-
npm run test
|
|
1278
|
+
npm run test:unit # mocked binding; fast
|
|
1279
|
+
npm run test:integration # spins up the real engine; needs models
|
|
1280
|
+
npm run test:integration:smoke # Supertonic suite only; the darwin-x64 CI leg runs this
|
|
1281
|
+
npm run test # both
|
|
1281
1282
|
```
|
|
1282
1283
|
|
|
1283
1284
|
Integration tests scan a few candidate `models/` directories for the
|
|
@@ -1298,6 +1299,15 @@ files are absent. They cover, across the engines:
|
|
|
1298
1299
|
for WER against the synthesized audio (whisper-small via the
|
|
1299
1300
|
`@qvac/asr-ggml` development dependency).
|
|
1300
1301
|
|
|
1302
|
+
On CPU the suite keeps to brief inference: single short syntheses, plus
|
|
1303
|
+
Supertonic's sub-second multi-sentence checks and a few capped CPU-backend
|
|
1304
|
+
checks (Audio8's 16-frame runs, the LavaSR denoiser composition). Tests that
|
|
1305
|
+
synthesize more (several runs or loads, longer text, WER) run on the GPU and
|
|
1306
|
+
are skipped when `NO_GPU=true`. So are the CosyVoice3, Chatterbox MTL and
|
|
1307
|
+
Parler f16/large tests, where one CPU synthesis already takes about a minute;
|
|
1308
|
+
the CPU smokes in `gpu-smoke.test.js` still run everywhere. The multi-run
|
|
1309
|
+
Parler tests need Metal, so off Apple they are skipped too.
|
|
1310
|
+
|
|
1301
1311
|
The MOSS integration test (`moss.test.js`) does not download its GGUFs:
|
|
1302
1312
|
set `QVAC_TEST_MOSS_MODEL_DIR` to a directory holding them, otherwise it is
|
|
1303
1313
|
skipped. The C++ suite runs its MOSS synthesis cases when
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qvac/tts-ggml",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Text to Speech (TTS) addon for qvac (ggml backend, wrapping the chatterbox + supertonic + parler + cosyvoice3 + audio8 + moss + Pocket engines from tts-cpp)",
|
|
5
5
|
"addon": true,
|
|
6
6
|
"engines": {
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"test:node": "node --test scripts/__tests__/*.test.js",
|
|
90
90
|
"test": "npm run test:types && npm run test:unit && npm run test:integration",
|
|
91
91
|
"test:integration": "npm run build:ts && brittle-bare test/integration/**/*.test.js",
|
|
92
|
+
"test:integration:smoke": "npm run build:ts && brittle-bare test/integration/supertonic.test.js",
|
|
92
93
|
"test:benchmark:rtf": "npm run build:ts && brittle-bare test/benchmark/rtf-benchmark.test.js",
|
|
93
94
|
"test:benchmark:rtf:matrix": "node scripts/run-rtf-benchmark-matrix.js",
|
|
94
95
|
"test:benchmark:streaming": "npm run build:ts && brittle-bare test/benchmark/streaming-benchmark.test.js",
|
|
@@ -107,6 +108,7 @@
|
|
|
107
108
|
"convert-models": "bash scripts/convert-models.sh",
|
|
108
109
|
"setup-models": "bash scripts/setup-venv.sh && bash scripts/convert-models.sh",
|
|
109
110
|
"download-models:registry": "node scripts/download-tts-ggml-models.js",
|
|
111
|
+
"download-models:smoke": "node scripts/download-tts-ggml-models.js --group supertonic",
|
|
110
112
|
"example:pocket": "bare examples/pocket-tts.js",
|
|
111
113
|
"test:pocket": "npm run build:ts && brittle-bare test/integration/pocket.integration.test.js"
|
|
112
114
|
},
|
|
@@ -214,10 +216,10 @@
|
|
|
214
216
|
},
|
|
215
217
|
"types": "index.d.ts",
|
|
216
218
|
"optionalDependencies": {
|
|
217
|
-
"@qvac/tts-ggml-linux-x64": "0.10.
|
|
218
|
-
"@qvac/tts-ggml-linux-arm64": "0.10.
|
|
219
|
-
"@qvac/tts-ggml-darwin-arm64": "0.10.
|
|
220
|
-
"@qvac/tts-ggml-darwin-x64": "0.10.
|
|
221
|
-
"@qvac/tts-ggml-win32-x64": "0.10.
|
|
219
|
+
"@qvac/tts-ggml-linux-x64": "0.10.2",
|
|
220
|
+
"@qvac/tts-ggml-linux-arm64": "0.10.2",
|
|
221
|
+
"@qvac/tts-ggml-darwin-arm64": "0.10.2",
|
|
222
|
+
"@qvac/tts-ggml-darwin-x64": "0.10.2",
|
|
223
|
+
"@qvac/tts-ggml-win32-x64": "0.10.2"
|
|
222
224
|
}
|
|
223
225
|
}
|
|
@@ -16,11 +16,11 @@ const { ensureChatterboxModels, ensureWhisperModel } = require('../utils/downloa
|
|
|
16
16
|
const { loadWhisper, runWhisper } = require('../utils/runWhisper')
|
|
17
17
|
const { recordTtsStats } = require('../utils/perf-helper')
|
|
18
18
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
19
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
19
20
|
|
|
20
21
|
const platform = os.platform()
|
|
21
22
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
22
23
|
const isDarwin = platform === 'darwin'
|
|
23
|
-
const forceNoGpu = os.getEnv('NO_GPU') === 'true'
|
|
24
24
|
|
|
25
25
|
const INPUT_SENTENCES = (isMobile ? 'short' : os.getEnv('INPUT_SENTENCES')) || 'short'
|
|
26
26
|
const useSplit = INPUT_SENTENCES !== 'short'
|
|
@@ -42,7 +42,7 @@ function getEnglishSentences() {
|
|
|
42
42
|
|
|
43
43
|
test(
|
|
44
44
|
'Chatterbox TTS (ggml): English synthesis + optional WER verification',
|
|
45
|
-
{ timeout: 1800000 },
|
|
45
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
46
46
|
async (t) => {
|
|
47
47
|
const baseDir = getBaseDir()
|
|
48
48
|
const modelsDir = path.join(baseDir, 'models')
|
|
@@ -86,7 +86,8 @@ test(
|
|
|
86
86
|
const model = await loadChatterboxTTS({
|
|
87
87
|
threads: TTS_TEST_THREADS,
|
|
88
88
|
modelDir: resolvedModelDir,
|
|
89
|
-
language: 'en'
|
|
89
|
+
language: 'en',
|
|
90
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
90
91
|
})
|
|
91
92
|
t.ok(model, 'Chatterbox (ggml) model should be loaded')
|
|
92
93
|
|
|
@@ -254,7 +255,7 @@ test(
|
|
|
254
255
|
const model = new TTSGgml({
|
|
255
256
|
threads: TTS_TEST_THREADS,
|
|
256
257
|
files: { modelDir: download.targetDir },
|
|
257
|
-
config: { language: 'en', ...(
|
|
258
|
+
config: { language: 'en', ...(NO_GPU ? { useGPU: false } : {}) },
|
|
258
259
|
opts: { stats: true }
|
|
259
260
|
})
|
|
260
261
|
|
|
@@ -315,7 +316,7 @@ test(
|
|
|
315
316
|
threads: TTS_TEST_THREADS,
|
|
316
317
|
files: { modelDir: download.targetDir },
|
|
317
318
|
referenceAudio: resolveRefWavPath({}),
|
|
318
|
-
config: { language: 'en', outputSampleRate: 16000, ...(
|
|
319
|
+
config: { language: 'en', outputSampleRate: 16000, ...(NO_GPU ? { useGPU: false } : {}) },
|
|
319
320
|
opts: { stats: true }
|
|
320
321
|
})
|
|
321
322
|
await model.load()
|
|
@@ -344,7 +345,7 @@ test(
|
|
|
344
345
|
|
|
345
346
|
test(
|
|
346
347
|
'Chatterbox TTS (ggml): native C++ chunk streaming via streamChunkTokens',
|
|
347
|
-
{ timeout: 600000 },
|
|
348
|
+
{ timeout: 600000, skip: NO_GPU },
|
|
348
349
|
async (t) => {
|
|
349
350
|
const baseDir = getBaseDir()
|
|
350
351
|
const modelsDir = path.join(baseDir, 'models')
|
|
@@ -371,7 +372,7 @@ test(
|
|
|
371
372
|
streamChunkTokens: 25,
|
|
372
373
|
streamFirstChunkTokens: 10,
|
|
373
374
|
cfmSteps: 1,
|
|
374
|
-
config: { language: 'en',
|
|
375
|
+
config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
|
|
375
376
|
opts: { stats: true }
|
|
376
377
|
})
|
|
377
378
|
await model.load()
|
|
@@ -26,13 +26,15 @@ const {
|
|
|
26
26
|
} = require('../utils/downloadModel')
|
|
27
27
|
const { recordTtsStats } = require('../utils/perf-helper')
|
|
28
28
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
29
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
29
30
|
|
|
30
31
|
const platform = os.platform()
|
|
31
32
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
32
33
|
|
|
33
|
-
// Language coverage
|
|
34
|
-
//
|
|
35
|
-
// Tests that *are* about GPU live
|
|
34
|
+
// Language coverage, not a GPU policy test. A single MTL synthesis already
|
|
35
|
+
// takes close to a minute on the CPU-only runners, so these tests run on the
|
|
36
|
+
// GPU only and are skipped there (NO_GPU=true). Tests that *are* about GPU live
|
|
37
|
+
// in gpu-smoke.test.js.
|
|
36
38
|
|
|
37
39
|
function getBaseDir() {
|
|
38
40
|
return isMobile && global.testDir ? global.testDir : '.'
|
|
@@ -83,7 +85,7 @@ async function loadChatterboxMtlTTS(params) {
|
|
|
83
85
|
|
|
84
86
|
test(
|
|
85
87
|
'Chatterbox MTL TTS (ggml): synthesizes across es/fr/de with shared engine',
|
|
86
|
-
{ timeout: 1800000 },
|
|
88
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
87
89
|
async (t) => {
|
|
88
90
|
const baseDir = getBaseDir()
|
|
89
91
|
const download = await ensureChatterboxMtlModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -98,7 +100,8 @@ test(
|
|
|
98
100
|
modelDir: download.targetDir,
|
|
99
101
|
t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
|
|
100
102
|
s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
|
|
101
|
-
language: MTL_SENTENCES[0].lang
|
|
103
|
+
language: MTL_SENTENCES[0].lang,
|
|
104
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
102
105
|
})
|
|
103
106
|
try {
|
|
104
107
|
for (let i = 0; i < MTL_SENTENCES.length; i++) {
|
|
@@ -166,7 +169,7 @@ test(
|
|
|
166
169
|
|
|
167
170
|
test(
|
|
168
171
|
'Chatterbox MTL TTS (ggml): synthesizes Japanese with MeCab dictionary',
|
|
169
|
-
{ timeout: 1800000 },
|
|
172
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
170
173
|
async (t) => {
|
|
171
174
|
const baseDir = getBaseDir()
|
|
172
175
|
const download = await ensureChatterboxMtlModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -188,7 +191,8 @@ test(
|
|
|
188
191
|
t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
|
|
189
192
|
s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
|
|
190
193
|
mecabDictDir: mecab.dir,
|
|
191
|
-
language: 'ja'
|
|
194
|
+
language: 'ja',
|
|
195
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
192
196
|
})
|
|
193
197
|
try {
|
|
194
198
|
const t0 = Date.now()
|
|
@@ -233,7 +237,7 @@ test(
|
|
|
233
237
|
|
|
234
238
|
test(
|
|
235
239
|
'Chatterbox MTL TTS (ggml): synthesizes Chinese with Cangjie table',
|
|
236
|
-
{ timeout: 1800000 },
|
|
240
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
237
241
|
async (t) => {
|
|
238
242
|
const baseDir = getBaseDir()
|
|
239
243
|
const download = await ensureChatterboxMtlModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -255,7 +259,8 @@ test(
|
|
|
255
259
|
t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
|
|
256
260
|
s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
|
|
257
261
|
cangjieTsvPath: cangjie.path,
|
|
258
|
-
language: 'zh'
|
|
262
|
+
language: 'zh',
|
|
263
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
259
264
|
})
|
|
260
265
|
try {
|
|
261
266
|
const t0 = Date.now()
|
|
@@ -12,6 +12,7 @@ const path = require('bare-path')
|
|
|
12
12
|
const { loadChatterboxTTS, runChatterboxTTS } = require('../utils/runChatterboxTTS')
|
|
13
13
|
const { ensureChatterboxModels } = require('../utils/downloadModel')
|
|
14
14
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
15
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
15
16
|
|
|
16
17
|
function getBaseDir() {
|
|
17
18
|
const platform = os.platform()
|
|
@@ -30,7 +31,7 @@ const EXPECTATION = {
|
|
|
30
31
|
|
|
31
32
|
test(
|
|
32
33
|
'Chatterbox TTS (ggml): speed config adjusts duration, preserves backward compat',
|
|
33
|
-
{ timeout: 1800000 },
|
|
34
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
34
35
|
async (t) => {
|
|
35
36
|
const modelsDir = path.join(getBaseDir(), 'models')
|
|
36
37
|
|
|
@@ -52,7 +53,8 @@ test(
|
|
|
52
53
|
threads: TTS_TEST_THREADS,
|
|
53
54
|
modelDir,
|
|
54
55
|
language: 'en',
|
|
55
|
-
speed
|
|
56
|
+
speed,
|
|
57
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
56
58
|
})
|
|
57
59
|
try {
|
|
58
60
|
const r = await runChatterboxTTS(model, { text: TEXT }, EXPECTATION)
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
const fs = require('bare-fs')
|
|
9
9
|
const os = require('bare-os')
|
|
10
10
|
const path = require('bare-path')
|
|
11
|
-
const proc = require('bare-process')
|
|
12
11
|
const test = require('brittle')
|
|
13
12
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
14
13
|
|
|
@@ -20,10 +19,11 @@ const {
|
|
|
20
19
|
} = require('../utils/downloadModel')
|
|
21
20
|
const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
|
|
22
21
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
22
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
23
23
|
|
|
24
24
|
const platform = os.platform()
|
|
25
25
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
26
|
-
const useGPU =
|
|
26
|
+
const useGPU = GPU_ONLY_USE_GPU
|
|
27
27
|
|
|
28
28
|
function getBaseDir() {
|
|
29
29
|
return isMobile && global.testDir ? global.testDir : '.'
|
|
@@ -99,7 +99,7 @@ async function synthOnce(t, loadParams, label) {
|
|
|
99
99
|
|
|
100
100
|
test(
|
|
101
101
|
'CosyVoice3 cloning (ggml): baked, zero-shot and cross-lingual all differ at one seed',
|
|
102
|
-
{ timeout: 1800000 },
|
|
102
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
103
103
|
async (t) => {
|
|
104
104
|
const modelDir = await ensureCloneReadyModelDir(t)
|
|
105
105
|
if (!modelDir) return
|
|
@@ -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
|
-
//
|
|
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,
|
|
@@ -25,6 +27,7 @@ const {
|
|
|
25
27
|
ensureCosyvoiceModel
|
|
26
28
|
} = require('../utils/downloadModel')
|
|
27
29
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
30
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
28
31
|
|
|
29
32
|
const platform = os.platform()
|
|
30
33
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
@@ -118,7 +121,7 @@ async function stageCosyvoice(t, baseDir) {
|
|
|
118
121
|
|
|
119
122
|
test(
|
|
120
123
|
'CosyVoice3 + LavaSR enhancer (batch) reports 48 kHz enhanced output',
|
|
121
|
-
{ timeout: 900000, skip: SKIP_COSYVOICE },
|
|
124
|
+
{ timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
|
|
122
125
|
async (t) => {
|
|
123
126
|
const baseDir = getBaseDir()
|
|
124
127
|
const enh = await stageEnhancer(t, baseDir)
|
|
@@ -130,7 +133,7 @@ test(
|
|
|
130
133
|
threads: TTS_TEST_THREADS,
|
|
131
134
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
132
135
|
files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
|
|
133
|
-
config: { language: 'en', useGPU:
|
|
136
|
+
config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
|
|
134
137
|
opts: { stats: true }
|
|
135
138
|
})
|
|
136
139
|
await model.load()
|
|
@@ -141,8 +144,8 @@ test(
|
|
|
141
144
|
t.ok(r.stats, 'runtimeStats returned (constructed with stats:true)')
|
|
142
145
|
t.is(
|
|
143
146
|
r.stats.enhancerBackendDevice,
|
|
144
|
-
|
|
145
|
-
'enhancer loaded and ran on
|
|
147
|
+
r.stats.backendDevice,
|
|
148
|
+
'enhancer loaded and ran on the engine device (not -1 = never loaded)'
|
|
146
149
|
)
|
|
147
150
|
} finally {
|
|
148
151
|
try {
|
|
@@ -154,7 +157,7 @@ test(
|
|
|
154
157
|
|
|
155
158
|
test(
|
|
156
159
|
'CosyVoice3 + LavaSR enhancer + native chunk streaming emits 48 kHz chunks',
|
|
157
|
-
{ timeout: 900000, skip: SKIP_COSYVOICE },
|
|
160
|
+
{ timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
|
|
158
161
|
async (t) => {
|
|
159
162
|
const baseDir = getBaseDir()
|
|
160
163
|
const enh = await stageEnhancer(t, baseDir)
|
|
@@ -167,7 +170,7 @@ test(
|
|
|
167
170
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
168
171
|
files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
|
|
169
172
|
streamChunkTokens: 25,
|
|
170
|
-
config: { language: 'en', useGPU:
|
|
173
|
+
config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
|
|
171
174
|
opts: { stats: true }
|
|
172
175
|
})
|
|
173
176
|
await model.load()
|
|
@@ -191,7 +194,7 @@ test(
|
|
|
191
194
|
|
|
192
195
|
test(
|
|
193
196
|
'CosyVoice3 + enhancer + streaming resamples every chunk to outputSampleRate',
|
|
194
|
-
{ timeout: 900000, skip: SKIP_COSYVOICE },
|
|
197
|
+
{ timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
|
|
195
198
|
async (t) => {
|
|
196
199
|
// A non-native rate while streaming is only legal because the enhancer's
|
|
197
200
|
// overlap-reprocess window folds the resample in seam-free. Without this the
|
|
@@ -208,7 +211,7 @@ test(
|
|
|
208
211
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
209
212
|
files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
|
|
210
213
|
streamChunkTokens: 25,
|
|
211
|
-
config: { language: 'en', useGPU:
|
|
214
|
+
config: { language: 'en', useGPU: GPU_ONLY_USE_GPU, outputSampleRate: 16000 },
|
|
212
215
|
opts: { stats: true }
|
|
213
216
|
})
|
|
214
217
|
await model.load()
|
|
@@ -228,7 +231,7 @@ test(
|
|
|
228
231
|
|
|
229
232
|
test(
|
|
230
233
|
'CosyVoice3 + LavaSR denoiser changes the emitted PCM',
|
|
231
|
-
{ timeout: 900000, skip: SKIP_COSYVOICE },
|
|
234
|
+
{ timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
|
|
232
235
|
async (t) => {
|
|
233
236
|
const baseDir = getBaseDir()
|
|
234
237
|
const dl = await stageCosyvoice(t, baseDir)
|
|
@@ -247,7 +250,7 @@ test(
|
|
|
247
250
|
threads: TTS_TEST_THREADS,
|
|
248
251
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
249
252
|
files: Object.assign({ cosyvoiceModelDir: dl.modelDir }, files),
|
|
250
|
-
config: { language: 'en', useGPU:
|
|
253
|
+
config: { language: 'en', useGPU: GPU_ONLY_USE_GPU, seed: SEED },
|
|
251
254
|
opts: { stats: true }
|
|
252
255
|
})
|
|
253
256
|
await model.load()
|
|
@@ -294,6 +297,7 @@ test(
|
|
|
294
297
|
if (!dl) return
|
|
295
298
|
|
|
296
299
|
const badEnhancer = path.join(baseDir, 'models', 'lavasr', 'invalid-enhancer.gguf')
|
|
300
|
+
fs.mkdirSync(path.dirname(badEnhancer), { recursive: true })
|
|
297
301
|
fs.writeFileSync(badEnhancer, 'not a gguf')
|
|
298
302
|
|
|
299
303
|
const model = new TTSGgml({
|
|
@@ -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
|
|
7
|
-
//
|
|
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')
|
|
@@ -13,6 +15,7 @@ const test = require('brittle')
|
|
|
13
15
|
const { loadCosyvoiceTTS, runCosyvoiceTTS } = require('../utils/runCosyvoiceTTS')
|
|
14
16
|
const { ensureCosyvoiceModel } = require('../utils/downloadModel')
|
|
15
17
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
18
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
16
19
|
|
|
17
20
|
const platform = os.platform()
|
|
18
21
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
@@ -27,7 +30,7 @@ const MODEL_MISSING =
|
|
|
27
30
|
|
|
28
31
|
test(
|
|
29
32
|
'CosyVoice3 TTS (ggml): outputSampleRate=16000 resamples and reports 16 kHz',
|
|
30
|
-
{ timeout: 600000 },
|
|
33
|
+
{ timeout: 600000, skip: NO_GPU },
|
|
31
34
|
async (t) => {
|
|
32
35
|
const baseDir = getBaseDir()
|
|
33
36
|
const download = await ensureCosyvoiceModel({
|
|
@@ -43,7 +46,8 @@ test(
|
|
|
43
46
|
const model = await loadCosyvoiceTTS({
|
|
44
47
|
threads: TTS_TEST_THREADS,
|
|
45
48
|
cosyvoiceModelDir: download.modelDir,
|
|
46
|
-
outputSampleRate: 16000
|
|
49
|
+
outputSampleRate: 16000,
|
|
50
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
47
51
|
})
|
|
48
52
|
try {
|
|
49
53
|
const text = 'Hello from CosyVoice.'
|
|
@@ -70,7 +74,7 @@ function samplesEqual(a, b) {
|
|
|
70
74
|
|
|
71
75
|
test(
|
|
72
76
|
'CosyVoice3 TTS (ggml): emotion changes the audio; per-call switch needs no reload',
|
|
73
|
-
{ timeout: 900000 },
|
|
77
|
+
{ timeout: 900000, skip: NO_GPU },
|
|
74
78
|
async (t) => {
|
|
75
79
|
const baseDir = getBaseDir()
|
|
76
80
|
const download = await ensureCosyvoiceModel({
|
|
@@ -87,7 +91,8 @@ test(
|
|
|
87
91
|
const model = await loadCosyvoiceTTS({
|
|
88
92
|
threads: TTS_TEST_THREADS,
|
|
89
93
|
cosyvoiceModelDir: download.modelDir,
|
|
90
|
-
seed: 42
|
|
94
|
+
seed: 42,
|
|
95
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
91
96
|
})
|
|
92
97
|
try {
|
|
93
98
|
const text = 'Hello from CosyVoice.'
|
|
@@ -172,7 +177,7 @@ test(
|
|
|
172
177
|
|
|
173
178
|
test(
|
|
174
179
|
'CosyVoice3 TTS (ggml): instruct conditioning produces audio',
|
|
175
|
-
{ timeout: 600000 },
|
|
180
|
+
{ timeout: 600000, skip: NO_GPU },
|
|
176
181
|
async (t) => {
|
|
177
182
|
const baseDir = getBaseDir()
|
|
178
183
|
const download = await ensureCosyvoiceModel({
|
|
@@ -186,7 +191,8 @@ test(
|
|
|
186
191
|
const model = await loadCosyvoiceTTS({
|
|
187
192
|
threads: TTS_TEST_THREADS,
|
|
188
193
|
cosyvoiceModelDir: download.modelDir,
|
|
189
|
-
emotion: 'happy'
|
|
194
|
+
emotion: 'happy',
|
|
195
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
190
196
|
})
|
|
191
197
|
try {
|
|
192
198
|
const text = 'Hello from CosyVoice.'
|
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
} = require('../utils/downloadModel')
|
|
34
34
|
const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
|
|
35
35
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
36
|
+
const { GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
36
37
|
|
|
37
38
|
const platform = os.platform()
|
|
38
39
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
@@ -632,7 +633,7 @@ test(
|
|
|
632
633
|
|
|
633
634
|
test(
|
|
634
635
|
'Chatterbox + LavaSR enhancer + native chunk streaming emits 48 kHz chunks',
|
|
635
|
-
{ timeout: 900000 },
|
|
636
|
+
{ timeout: 900000, skip: NO_GPU },
|
|
636
637
|
async (t) => {
|
|
637
638
|
const baseDir = getBaseDir()
|
|
638
639
|
const enh = await ensureLavaSREnhancerGguf({
|
|
@@ -662,7 +663,7 @@ test(
|
|
|
662
663
|
},
|
|
663
664
|
referenceAudio: resolveRefWavPath({}),
|
|
664
665
|
streamChunkTokens: 25, // native chunk streaming + enhancer (the path)
|
|
665
|
-
config: { language: 'en', useGPU:
|
|
666
|
+
config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
|
|
666
667
|
opts: { stats: true }
|
|
667
668
|
})
|
|
668
669
|
await model.load()
|
|
@@ -11,6 +11,7 @@ const { recordTtsStats } = require('../utils/perf-helper')
|
|
|
11
11
|
const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
|
|
12
12
|
const { readWavAsFloat32, createWav, resampleLinear } = require('../utils/wav-helper')
|
|
13
13
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
14
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
14
15
|
|
|
15
16
|
const MOSS_SAMPLE_RATE = 24000
|
|
16
17
|
const MOSS_STREAM_FRAMES = 10
|
|
@@ -34,7 +35,6 @@ const GPU_DEVICE = 1
|
|
|
34
35
|
|
|
35
36
|
const modelDir = (proc.env && proc.env[MODEL_DIR_ENV]) || ''
|
|
36
37
|
const skipWithoutModels = modelDir === ''
|
|
37
|
-
const noGpu = proc.env && proc.env.NO_GPU === 'true'
|
|
38
38
|
|
|
39
39
|
function createMossModel(extra = {}, useGPU = false) {
|
|
40
40
|
return new TTSGgml({
|
|
@@ -184,7 +184,7 @@ test(
|
|
|
184
184
|
|
|
185
185
|
test(
|
|
186
186
|
'MOSS TTS: useGPU=true synthesizes on a GPU backend',
|
|
187
|
-
{ timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels ||
|
|
187
|
+
{ timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels || NO_GPU },
|
|
188
188
|
async (t) => {
|
|
189
189
|
await withLoadedModel(
|
|
190
190
|
{},
|
|
@@ -231,25 +231,29 @@ test(
|
|
|
231
231
|
|
|
232
232
|
test(
|
|
233
233
|
'MOSS TTS: durationTokens steers the length of the synthesized speech',
|
|
234
|
-
{ timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels },
|
|
234
|
+
{ timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels || NO_GPU },
|
|
235
235
|
async (t) => {
|
|
236
|
-
await withLoadedModel(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
long
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
236
|
+
await withLoadedModel(
|
|
237
|
+
{ durationTokens: SHORT_DURATION_TOKENS, seed: 7 },
|
|
238
|
+
async (model) => {
|
|
239
|
+
const short = await synthesize(model, SYNTHESIS_TEXT)
|
|
240
|
+
assertAudio(t, 'MOSS short target', short)
|
|
241
|
+
await model.reload({ durationTokens: LONG_DURATION_TOKENS })
|
|
242
|
+
const long = await synthesize(model, SYNTHESIS_TEXT)
|
|
243
|
+
assertAudio(t, 'MOSS long target', long)
|
|
244
|
+
t.ok(
|
|
245
|
+
long.samples.length > short.samples.length,
|
|
246
|
+
`a longer target yields longer speech (${short.samples.length} -> ${long.samples.length} samples)`
|
|
247
|
+
)
|
|
248
|
+
},
|
|
249
|
+
GPU_ONLY_USE_GPU
|
|
250
|
+
)
|
|
247
251
|
}
|
|
248
252
|
)
|
|
249
253
|
|
|
250
254
|
test(
|
|
251
255
|
'MOSS TTS: dialogue synthesis clones one reference per speaker',
|
|
252
|
-
{ timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels },
|
|
256
|
+
{ timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels || NO_GPU },
|
|
253
257
|
async (t) => {
|
|
254
258
|
if (!hasDialogueBackbone() || !hasEncoder()) {
|
|
255
259
|
t.comment(`skipping: the TTSD backbone or the codec encoder is not in ${modelDir}`)
|
|
@@ -273,7 +277,8 @@ test(
|
|
|
273
277
|
const result = await synthesize(model, DIALOGUE_TEXT)
|
|
274
278
|
assertAudio(t, 'MOSS dialogue', result)
|
|
275
279
|
recordMoss(t, 'moss dialogue', result, Date.now() - started)
|
|
276
|
-
}
|
|
280
|
+
},
|
|
281
|
+
GPU_ONLY_USE_GPU
|
|
277
282
|
)
|
|
278
283
|
} finally {
|
|
279
284
|
removeFiles(references)
|
|
@@ -30,13 +30,15 @@ const {
|
|
|
30
30
|
const { loadSupertonicTTS, runSupertonicTTS } = require('../utils/runSupertonicTTS')
|
|
31
31
|
const { ensureChatterboxModels, ensureSupertonicModel } = require('../utils/downloadModel')
|
|
32
32
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
33
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
33
34
|
|
|
34
35
|
const platform = os.platform()
|
|
35
36
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
36
37
|
|
|
37
|
-
// Lifecycle / sequential-run
|
|
38
|
-
//
|
|
39
|
-
//
|
|
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.
|
|
40
42
|
|
|
41
43
|
function getBaseDir() {
|
|
42
44
|
return isMobile && global.testDir ? global.testDir : '.'
|
|
@@ -50,7 +52,7 @@ const PHRASES = [
|
|
|
50
52
|
|
|
51
53
|
test(
|
|
52
54
|
'Chatterbox: multiple sequential runs reuse the same engine instance',
|
|
53
|
-
{ timeout: 1800000 },
|
|
55
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
54
56
|
async (t) => {
|
|
55
57
|
const baseDir = getBaseDir()
|
|
56
58
|
const download = await ensureChatterboxModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -77,7 +79,8 @@ test(
|
|
|
77
79
|
threads: TTS_TEST_THREADS,
|
|
78
80
|
modelDir: download.targetDir,
|
|
79
81
|
refWavPath,
|
|
80
|
-
language: 'en'
|
|
82
|
+
language: 'en',
|
|
83
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
81
84
|
})
|
|
82
85
|
try {
|
|
83
86
|
const timings = []
|
|
@@ -111,7 +114,7 @@ test(
|
|
|
111
114
|
|
|
112
115
|
test(
|
|
113
116
|
'Chatterbox: fresh instance per run (app-restart simulation)',
|
|
114
|
-
{ timeout: 1800000 },
|
|
117
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
115
118
|
async (t) => {
|
|
116
119
|
const baseDir = getBaseDir()
|
|
117
120
|
const download = await ensureChatterboxModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -142,7 +145,8 @@ test(
|
|
|
142
145
|
threads: TTS_TEST_THREADS,
|
|
143
146
|
modelDir: download.targetDir,
|
|
144
147
|
refWavPath,
|
|
145
|
-
language: 'en'
|
|
148
|
+
language: 'en',
|
|
149
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
146
150
|
})
|
|
147
151
|
const loadMs = Date.now() - t0
|
|
148
152
|
try {
|
|
@@ -175,58 +179,63 @@ test(
|
|
|
175
179
|
}
|
|
176
180
|
)
|
|
177
181
|
|
|
178
|
-
test(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
}
|
|
187
194
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
199
206
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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')
|
|
213
221
|
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
await model.reload({ language: 'en' })
|
|
223
|
+
t.pass('reload() resolved')
|
|
216
224
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
}
|
|
228
237
|
}
|
|
229
|
-
|
|
238
|
+
)
|
|
230
239
|
|
|
231
240
|
test('Supertonic: reload() between runs preserves stability', { timeout: 1800000 }, async (t) => {
|
|
232
241
|
const baseDir = getBaseDir()
|
|
@@ -272,7 +281,7 @@ test('Supertonic: reload() between runs preserves stability', { timeout: 1800000
|
|
|
272
281
|
|
|
273
282
|
test(
|
|
274
283
|
'Engine swap: chatterbox -> supertonic -> chatterbox in separate instances',
|
|
275
|
-
{ timeout: 1800000 },
|
|
284
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
276
285
|
async (t) => {
|
|
277
286
|
const baseDir = getBaseDir()
|
|
278
287
|
const cb = await ensureChatterboxModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -300,7 +309,8 @@ test(
|
|
|
300
309
|
threads: TTS_TEST_THREADS,
|
|
301
310
|
modelDir: cb.targetDir,
|
|
302
311
|
refWavPath,
|
|
303
|
-
language: 'en'
|
|
312
|
+
language: 'en',
|
|
313
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
304
314
|
})
|
|
305
315
|
try {
|
|
306
316
|
const r = await runChatterboxTTS(c1, { text: 'Hello from chatterbox.' }, { minSamples: 5000 })
|
|
@@ -316,7 +326,7 @@ test(
|
|
|
316
326
|
supertonicModelPath: st.path,
|
|
317
327
|
voice: 'F1',
|
|
318
328
|
language: 'en',
|
|
319
|
-
useGPU:
|
|
329
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
320
330
|
})
|
|
321
331
|
try {
|
|
322
332
|
const r = await runSupertonicTTS(s1, { text: 'Hello from supertonic.' }, { minSamples: 5000 })
|
|
@@ -331,7 +341,8 @@ test(
|
|
|
331
341
|
threads: TTS_TEST_THREADS,
|
|
332
342
|
modelDir: cb.targetDir,
|
|
333
343
|
refWavPath,
|
|
334
|
-
language: 'en'
|
|
344
|
+
language: 'en',
|
|
345
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
335
346
|
})
|
|
336
347
|
try {
|
|
337
348
|
const r = await runChatterboxTTS(
|
|
@@ -13,6 +13,7 @@ const TTSGgml = require('@qvac/tts-ggml')
|
|
|
13
13
|
|
|
14
14
|
const { ensureSupertonicModel, ensureParlerModel } = require('../utils/downloadModel')
|
|
15
15
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
16
|
+
const { 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'
|
|
@@ -123,7 +124,7 @@ test(
|
|
|
123
124
|
|
|
124
125
|
test(
|
|
125
126
|
'Parler: outputSampleRate=16000 resamples and reports 16 kHz',
|
|
126
|
-
{ timeout: 600000 },
|
|
127
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
127
128
|
async (t) => {
|
|
128
129
|
const baseDir = getBaseDir()
|
|
129
130
|
const dl = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -140,6 +141,7 @@ test(
|
|
|
140
141
|
files: { parlerModel: dl.path },
|
|
141
142
|
voice: 'Laura',
|
|
142
143
|
seed: 42,
|
|
144
|
+
config: { useGPU: PARLER_GPU_ONLY_USE_GPU },
|
|
143
145
|
opts: { stats: true }
|
|
144
146
|
})
|
|
145
147
|
await native.load()
|
|
@@ -160,7 +162,7 @@ test(
|
|
|
160
162
|
files: { parlerModel: dl.path },
|
|
161
163
|
voice: 'Laura',
|
|
162
164
|
seed: 42,
|
|
163
|
-
config: { outputSampleRate: 16000 },
|
|
165
|
+
config: { outputSampleRate: 16000, useGPU: PARLER_GPU_ONLY_USE_GPU },
|
|
164
166
|
opts: { stats: true }
|
|
165
167
|
})
|
|
166
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')
|
|
@@ -27,10 +27,9 @@ const isApple = platform === 'darwin' || platform === 'ios'
|
|
|
27
27
|
const NO_GPU = proc.env && proc.env.NO_GPU === 'true'
|
|
28
28
|
const LARGE_MIN_RAM_BYTES = 16 * 1024 ** 3
|
|
29
29
|
|
|
30
|
-
// Calibrated 2026-07-24:
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// 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%).
|
|
34
33
|
const WER_THRESHOLD = 0.35
|
|
35
34
|
|
|
36
35
|
// Clear English sentences, all >= 9 words (a single ASR slip stays <= ~0.11,
|
|
@@ -51,7 +50,7 @@ function getBaseDir() {
|
|
|
51
50
|
for (const { variant } of PARLER_WER_TIERS) {
|
|
52
51
|
test(
|
|
53
52
|
`Parler WER (${variant}): English synthesis transcribes within ${(WER_THRESHOLD * 100).toFixed(0)}%`,
|
|
54
|
-
{ timeout: 1800000, skip: !isDarwin },
|
|
53
|
+
{ timeout: 1800000, skip: !isDarwin || NO_GPU },
|
|
55
54
|
async (t) => {
|
|
56
55
|
if (variant === 'large' && os.totalmem() < LARGE_MIN_RAM_BYTES) {
|
|
57
56
|
t.pass(
|
|
@@ -7,18 +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')
|
|
16
15
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
16
|
+
const { NO_GPU, SKIP_PARLER_GPU_ONLY, PARLER_GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
17
17
|
|
|
18
18
|
const platform = os.platform()
|
|
19
19
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
20
20
|
const isApple = platform === 'darwin' || platform === 'ios'
|
|
21
|
-
const NO_GPU = proc.env && proc.env.NO_GPU === 'true'
|
|
22
21
|
|
|
23
22
|
function getBaseDir() {
|
|
24
23
|
return isMobile && global.testDir ? global.testDir : '.'
|
|
@@ -137,7 +136,7 @@ test(
|
|
|
137
136
|
|
|
138
137
|
test(
|
|
139
138
|
'Parler TTS (ggml): description matrix — conflicts throw, per-call wins, defaults work',
|
|
140
|
-
{ timeout: 600000 },
|
|
139
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
141
140
|
async (t) => {
|
|
142
141
|
const baseDir = getBaseDir()
|
|
143
142
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -191,7 +190,8 @@ test(
|
|
|
191
190
|
threads: TTS_TEST_THREADS,
|
|
192
191
|
parlerModelPath: download.path,
|
|
193
192
|
voice: 'Laura',
|
|
194
|
-
seed: 42
|
|
193
|
+
seed: 42,
|
|
194
|
+
useGPU: PARLER_GPU_ONLY_USE_GPU
|
|
195
195
|
})
|
|
196
196
|
try {
|
|
197
197
|
// Per-call description conflict (same level) rejects.
|
|
@@ -234,7 +234,8 @@ test(
|
|
|
234
234
|
threads: TTS_TEST_THREADS,
|
|
235
235
|
parlerModelPath: download.path,
|
|
236
236
|
description: 'A calm female voice, very clear audio.',
|
|
237
|
-
seed: 42
|
|
237
|
+
seed: 42,
|
|
238
|
+
useGPU: PARLER_GPU_ONLY_USE_GPU
|
|
238
239
|
})
|
|
239
240
|
try {
|
|
240
241
|
await t.exception(
|
|
@@ -252,7 +253,7 @@ test(
|
|
|
252
253
|
|
|
253
254
|
test(
|
|
254
255
|
'Parler TTS (ggml): emotion flag changes the audio; per-call switch needs no reload',
|
|
255
|
-
{ timeout: 600000 },
|
|
256
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
256
257
|
async (t) => {
|
|
257
258
|
const baseDir = getBaseDir()
|
|
258
259
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -265,7 +266,8 @@ test(
|
|
|
265
266
|
threads: TTS_TEST_THREADS,
|
|
266
267
|
parlerModelPath: download.path,
|
|
267
268
|
voice: 'Laura',
|
|
268
|
-
seed: 42
|
|
269
|
+
seed: 42,
|
|
270
|
+
useGPU: PARLER_GPU_ONLY_USE_GPU
|
|
269
271
|
})
|
|
270
272
|
try {
|
|
271
273
|
const text = 'The weather is wonderful today.'
|
|
@@ -300,7 +302,7 @@ test(
|
|
|
300
302
|
|
|
301
303
|
test(
|
|
302
304
|
'Parler TTS (ggml): fixed seed is deterministic across runs and reload',
|
|
303
|
-
{ timeout: 600000 },
|
|
305
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
304
306
|
async (t) => {
|
|
305
307
|
const baseDir = getBaseDir()
|
|
306
308
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -314,7 +316,8 @@ test(
|
|
|
314
316
|
parlerModelPath: download.path,
|
|
315
317
|
voice: 'Laura',
|
|
316
318
|
emotion: 'neutral',
|
|
317
|
-
seed: 42
|
|
319
|
+
seed: 42,
|
|
320
|
+
useGPU: PARLER_GPU_ONLY_USE_GPU
|
|
318
321
|
})
|
|
319
322
|
try {
|
|
320
323
|
const text = 'Deterministic synthesis check.'
|
|
@@ -345,7 +348,7 @@ test(
|
|
|
345
348
|
|
|
346
349
|
test(
|
|
347
350
|
'Parler TTS (ggml): runStream emits per-sentence chunks with one pinned description',
|
|
348
|
-
{ timeout: 600000 },
|
|
351
|
+
{ timeout: 600000, skip: SKIP_PARLER_GPU_ONLY },
|
|
349
352
|
async (t) => {
|
|
350
353
|
const baseDir = getBaseDir()
|
|
351
354
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -360,6 +363,7 @@ test(
|
|
|
360
363
|
engine: TTSGgml.ENGINE_PARLER,
|
|
361
364
|
files: { parlerModel: download.path },
|
|
362
365
|
seed: 42,
|
|
366
|
+
config: { useGPU: PARLER_GPU_ONLY_USE_GPU },
|
|
363
367
|
opts: { stats: true }
|
|
364
368
|
})
|
|
365
369
|
await model.load()
|
|
@@ -393,7 +397,7 @@ test(
|
|
|
393
397
|
|
|
394
398
|
test(
|
|
395
399
|
'Parler TTS (ggml): native streaming emits monotonic chunks equal to batch',
|
|
396
|
-
{ timeout: 900000 },
|
|
400
|
+
{ timeout: 900000, skip: SKIP_PARLER_GPU_ONLY },
|
|
397
401
|
async (t) => {
|
|
398
402
|
const baseDir = getBaseDir()
|
|
399
403
|
const download = await ensureParlerModel({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -526,22 +530,24 @@ test(
|
|
|
526
530
|
|
|
527
531
|
// Desktop quant + backend coverage: q8_0 + f16 for indic/mini, q8_0 for large.
|
|
528
532
|
// Runs on the platform's GPU-if-available backend (Metal on Apple GPU runners,
|
|
529
|
-
// CPU otherwise).
|
|
530
|
-
//
|
|
531
|
-
//
|
|
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.
|
|
532
538
|
const PARLER_QUANT_MATRIX = [
|
|
533
|
-
{ variant: 'indic', quant: 'q8_0' },
|
|
534
|
-
{ variant: 'indic', quant: 'f16' },
|
|
535
|
-
{ variant: 'mini', quant: 'q8_0' },
|
|
536
|
-
{ variant: 'mini', quant: 'f16' },
|
|
537
|
-
{ 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 }
|
|
538
544
|
]
|
|
539
545
|
const LARGE_MIN_RAM_BYTES = 16 * 1024 ** 3
|
|
540
546
|
|
|
541
|
-
for (const { variant, quant } of PARLER_QUANT_MATRIX) {
|
|
547
|
+
for (const { variant, quant, gpuOnly } of PARLER_QUANT_MATRIX) {
|
|
542
548
|
test(
|
|
543
549
|
`Parler TTS (ggml): ${variant} ${quant} synthesizes 44.1 kHz audio on the target backend`,
|
|
544
|
-
{ timeout: 900000, skip: isMobile },
|
|
550
|
+
{ timeout: 900000, skip: isMobile || (gpuOnly && SKIP_PARLER_GPU_ONLY) },
|
|
545
551
|
async (t) => {
|
|
546
552
|
if (variant === 'large' && os.totalmem() < LARGE_MIN_RAM_BYTES) {
|
|
547
553
|
t.pass(
|