@qvac/tts-ggml 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +342 -40
- 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 +14 -11
- package/test/integration/addon.test.js +5 -0
- package/test/integration/audio8.test.js +7 -0
- package/test/integration/chatterbox-mtl.test.js +6 -0
- package/test/integration/chatterbox-speed.test.js +13 -2
- package/test/integration/cosyvoice3-clone.test.js +16 -1
- package/test/integration/cosyvoice3-lavasr.test.js +6 -0
- package/test/integration/cosyvoice3.test.js +19 -0
- package/test/integration/fit.test.js +174 -0
- package/test/integration/gpu-smoke.test.js +13 -0
- package/test/integration/lavasr-enhancer.test.js +104 -3
- package/test/integration/moss.test.js +282 -0
- package/test/integration/multiple-runs.test.js +18 -2
- package/test/integration/output-sample-rate.test.js +6 -0
- package/test/integration/parler-wer.test.js +7 -1
- package/test/integration/parler.test.js +27 -3
- 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
|
@@ -12,6 +12,7 @@ const test = require('brittle')
|
|
|
12
12
|
|
|
13
13
|
const { loadCosyvoiceTTS, runCosyvoiceTTS } = require('../utils/runCosyvoiceTTS')
|
|
14
14
|
const { ensureCosyvoiceModel } = require('../utils/downloadModel')
|
|
15
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
15
16
|
|
|
16
17
|
const platform = os.platform()
|
|
17
18
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
@@ -40,6 +41,7 @@ test(
|
|
|
40
41
|
// Second model with outputSampleRate wired through the config surface, so the
|
|
41
42
|
// whole resample path (engine → addon → reported chunk) is exercised end-to-end.
|
|
42
43
|
const model = await loadCosyvoiceTTS({
|
|
44
|
+
threads: TTS_TEST_THREADS,
|
|
43
45
|
cosyvoiceModelDir: download.modelDir,
|
|
44
46
|
outputSampleRate: 16000
|
|
45
47
|
})
|
|
@@ -83,6 +85,7 @@ test(
|
|
|
83
85
|
// logits, and cosyvoice reseeds per synthesis, so pinning it is the
|
|
84
86
|
// difference between measuring the conditioning and measuring the sampler.
|
|
85
87
|
const model = await loadCosyvoiceTTS({
|
|
88
|
+
threads: TTS_TEST_THREADS,
|
|
86
89
|
cosyvoiceModelDir: download.modelDir,
|
|
87
90
|
seed: 42
|
|
88
91
|
})
|
|
@@ -119,6 +122,16 @@ test(
|
|
|
119
122
|
'plain cosyvoice reports 24 kHz native sample rate'
|
|
120
123
|
)
|
|
121
124
|
t.ok(plain.data.durationMs > 0, 'plain cosyvoice audio duration is > 0 ms')
|
|
125
|
+
// Engine StageTimings: each stage did work, the zero-shot baked voice
|
|
126
|
+
// prompts the LM with its speech tokens, and no denoiser is loaded.
|
|
127
|
+
const stats = plain.data.stats
|
|
128
|
+
t.ok(stats.speechTokens > 0, 'stats report the speech tokens generated')
|
|
129
|
+
t.ok(stats.decodeSteps > 0, 'stats report the LM decode steps')
|
|
130
|
+
t.ok(stats.promptSpeechTokens > 0, 'zero-shot prompts the LM with speech tokens')
|
|
131
|
+
for (const key of ['lmDecodeMs', 'ditEulerMs', 'hiftDecodeMs', 'stageTotalMs']) {
|
|
132
|
+
t.ok(stats[key] > 0, `stats report ${key}`)
|
|
133
|
+
}
|
|
134
|
+
t.is(stats.denoiserBackendDevice, -1, 'no denoiser loaded -> denoiserBackendDevice=-1')
|
|
122
135
|
const neutral = await runCosyvoiceTTS(
|
|
123
136
|
model,
|
|
124
137
|
{ text, perCallEmotion: 'neutral' },
|
|
@@ -171,6 +184,7 @@ test(
|
|
|
171
184
|
}
|
|
172
185
|
|
|
173
186
|
const model = await loadCosyvoiceTTS({
|
|
187
|
+
threads: TTS_TEST_THREADS,
|
|
174
188
|
cosyvoiceModelDir: download.modelDir,
|
|
175
189
|
emotion: 'happy'
|
|
176
190
|
})
|
|
@@ -182,6 +196,11 @@ test(
|
|
|
182
196
|
t.ok(result.passed, 'cosyvoice instruct synth passes expectations')
|
|
183
197
|
t.ok(result.data.sampleCount > 0, 'cosyvoice instruct produced audio')
|
|
184
198
|
t.is(result.data.reportedSampleRate, 24000, 'cosyvoice instruct reports 24 kHz')
|
|
199
|
+
t.is(
|
|
200
|
+
result.data.stats.promptSpeechTokens,
|
|
201
|
+
0,
|
|
202
|
+
'instruct mode drops the prompt speech tokens (StageTimings)'
|
|
203
|
+
)
|
|
185
204
|
} finally {
|
|
186
205
|
try {
|
|
187
206
|
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
|
})
|
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
ensureParlerModel
|
|
33
33
|
} = require('../utils/downloadModel')
|
|
34
34
|
const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
|
|
35
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
35
36
|
|
|
36
37
|
const platform = os.platform()
|
|
37
38
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
@@ -159,6 +160,13 @@ async function runAndCollect(model, text) {
|
|
|
159
160
|
return { samples, sampleRate, stats: response.stats || null }
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
// A CPU engine keeps the denoiser on its scalar core, which runtimeStats
|
|
164
|
+
// reports as CPU (denoiserBackendDevice 0, denoiserBackendId 0).
|
|
165
|
+
function assertDenoiserOnCpu(t, stats, label) {
|
|
166
|
+
t.is(stats.denoiserBackendDevice, 0, `${label}: useGPU:false -> denoiser on CPU`)
|
|
167
|
+
t.is(stats.denoiserBackendId, 0, `${label}: denoiser backend reported as CPU`)
|
|
168
|
+
}
|
|
169
|
+
|
|
162
170
|
// Every chunk that carries audio must be tagged at the enhanced 48 kHz rate
|
|
163
171
|
// rather than the engine's native rate — the mislabel this feature prevents.
|
|
164
172
|
function assertStreamedChunksReportEnhancedRate(t, updates) {
|
|
@@ -202,6 +210,7 @@ function assertStreamTerminatesOnce(t, updates, terminal) {
|
|
|
202
210
|
|
|
203
211
|
async function runParlerBatch(files) {
|
|
204
212
|
const model = new TTSGgml({
|
|
213
|
+
threads: TTS_TEST_THREADS,
|
|
205
214
|
engine: TTSGgml.ENGINE_PARLER,
|
|
206
215
|
files,
|
|
207
216
|
voice: 'Laura',
|
|
@@ -225,6 +234,7 @@ test('Chatterbox: enhancer + streamChunkTokens constructs and forwards both', (t
|
|
|
225
234
|
// construct and forward both knobs to the addon (which runs the streaming
|
|
226
235
|
// enhancer per chunk).
|
|
227
236
|
const model = new TTSGgml({
|
|
237
|
+
threads: TTS_TEST_THREADS,
|
|
228
238
|
engine: TTSGgml.ENGINE_CHATTERBOX,
|
|
229
239
|
files: {
|
|
230
240
|
t3Model: './models/chatterbox-t3-turbo.gguf',
|
|
@@ -247,6 +257,7 @@ test('Parler: enhancer + streamChunkTokens constructs and forwards both', (t) =>
|
|
|
247
257
|
// Parler used to reject the enhancer outright; it now enhances both the batch
|
|
248
258
|
// path and native chunk streaming (seam-free via the streaming enhancer).
|
|
249
259
|
const model = new TTSGgml({
|
|
260
|
+
threads: TTS_TEST_THREADS,
|
|
250
261
|
engine: TTSGgml.ENGINE_PARLER,
|
|
251
262
|
files: {
|
|
252
263
|
parlerModel: './models/parler-mini-v1-q8_0.gguf',
|
|
@@ -266,6 +277,7 @@ test('Parler: enhancer + streamChunkTokens constructs and forwards both', (t) =>
|
|
|
266
277
|
|
|
267
278
|
test('Parler: denoiser forwards on the batch path and is rejected while streaming', (t) => {
|
|
268
279
|
const model = new TTSGgml({
|
|
280
|
+
threads: TTS_TEST_THREADS,
|
|
269
281
|
engine: TTSGgml.ENGINE_PARLER,
|
|
270
282
|
files: {
|
|
271
283
|
parlerModel: './models/parler-mini-v1-q8_0.gguf',
|
|
@@ -281,6 +293,7 @@ test('Parler: denoiser forwards on the batch path and is rejected while streamin
|
|
|
281
293
|
t.exception(
|
|
282
294
|
() =>
|
|
283
295
|
new TTSGgml({
|
|
296
|
+
threads: TTS_TEST_THREADS,
|
|
284
297
|
engine: TTSGgml.ENGINE_PARLER,
|
|
285
298
|
files: {
|
|
286
299
|
parlerModel: './models/parler-mini-v1-q8_0.gguf',
|
|
@@ -299,6 +312,7 @@ test('CosyVoice3: enhancer + streamChunkTokens constructs and forwards both', (t
|
|
|
299
312
|
// batch path and on native chunk streaming, so both knobs must reach the
|
|
300
313
|
// addon together.
|
|
301
314
|
const model = new TTSGgml({
|
|
315
|
+
threads: TTS_TEST_THREADS,
|
|
302
316
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
303
317
|
files: {
|
|
304
318
|
cosyvoiceModelDir: './models/cosyvoice3',
|
|
@@ -318,6 +332,7 @@ test('CosyVoice3: enhancer + streamChunkTokens constructs and forwards both', (t
|
|
|
318
332
|
|
|
319
333
|
test('CosyVoice3: denoiser is forwarded for batch but rejected with streaming', (t) => {
|
|
320
334
|
const batch = new TTSGgml({
|
|
335
|
+
threads: TTS_TEST_THREADS,
|
|
321
336
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
322
337
|
files: {
|
|
323
338
|
cosyvoiceModelDir: './models/cosyvoice3',
|
|
@@ -334,6 +349,7 @@ test('CosyVoice3: denoiser is forwarded for batch but rejected with streaming',
|
|
|
334
349
|
t.exception(
|
|
335
350
|
() =>
|
|
336
351
|
new TTSGgml({
|
|
352
|
+
threads: TTS_TEST_THREADS,
|
|
337
353
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
338
354
|
files: {
|
|
339
355
|
cosyvoiceModelDir: './models/cosyvoice3',
|
|
@@ -351,6 +367,7 @@ test('enhancer with an unknown type is rejected at construction', (t) => {
|
|
|
351
367
|
t.exception(
|
|
352
368
|
() =>
|
|
353
369
|
new TTSGgml({
|
|
370
|
+
threads: TTS_TEST_THREADS,
|
|
354
371
|
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
355
372
|
files: {
|
|
356
373
|
supertonicModel: './models/supertonic.gguf',
|
|
@@ -366,6 +383,7 @@ test('enhancer with an unknown type is rejected at construction', (t) => {
|
|
|
366
383
|
|
|
367
384
|
test('enhancer block with no GGUF path leaves enhancement off (no throw)', (t) => {
|
|
368
385
|
const model = new TTSGgml({
|
|
386
|
+
threads: TTS_TEST_THREADS,
|
|
369
387
|
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
370
388
|
files: { supertonicModel: './models/supertonic.gguf' },
|
|
371
389
|
enhancer: { type: 'lavasr' },
|
|
@@ -420,7 +438,12 @@ for (const [engineName, engine, files] of [
|
|
|
420
438
|
]
|
|
421
439
|
]) {
|
|
422
440
|
test(`${engineName}: enhancer + useGPU:true forwards useGPU alongside the enhancer path`, (t) => {
|
|
423
|
-
const model = new TTSGgml({
|
|
441
|
+
const model = new TTSGgml({
|
|
442
|
+
threads: TTS_TEST_THREADS,
|
|
443
|
+
engine,
|
|
444
|
+
files,
|
|
445
|
+
config: { language: 'en', useGPU: true }
|
|
446
|
+
})
|
|
424
447
|
const params = model._buildTtsParams()
|
|
425
448
|
t.is(params.useGPU, true, 'useGPU:true forwarded to the addon (drives EnhancerOptions.use_gpu)')
|
|
426
449
|
t.is(
|
|
@@ -431,7 +454,12 @@ for (const [engineName, engine, files] of [
|
|
|
431
454
|
})
|
|
432
455
|
|
|
433
456
|
test(`${engineName}: enhancer + useGPU:false keeps the enhancer on CPU`, (t) => {
|
|
434
|
-
const model = new TTSGgml({
|
|
457
|
+
const model = new TTSGgml({
|
|
458
|
+
threads: TTS_TEST_THREADS,
|
|
459
|
+
engine,
|
|
460
|
+
files,
|
|
461
|
+
config: { language: 'en', useGPU: false }
|
|
462
|
+
})
|
|
435
463
|
const params = model._buildTtsParams()
|
|
436
464
|
t.is(
|
|
437
465
|
params.useGPU,
|
|
@@ -446,7 +474,13 @@ for (const [engineName, engine, files] of [
|
|
|
446
474
|
})
|
|
447
475
|
|
|
448
476
|
test(`${engineName}: enhancer + nGpuLayers!=0 forwards the GPU layer count`, (t) => {
|
|
449
|
-
const model = new TTSGgml({
|
|
477
|
+
const model = new TTSGgml({
|
|
478
|
+
threads: TTS_TEST_THREADS,
|
|
479
|
+
engine,
|
|
480
|
+
files,
|
|
481
|
+
nGpuLayers: 99,
|
|
482
|
+
config: { language: 'en' }
|
|
483
|
+
})
|
|
450
484
|
const params = model._buildTtsParams()
|
|
451
485
|
t.is(params.nGpuLayers, 99, 'nGpuLayers forwarded (non-zero => enhancer requests the GPU)')
|
|
452
486
|
t.is(
|
|
@@ -479,6 +513,7 @@ test(
|
|
|
479
513
|
}
|
|
480
514
|
|
|
481
515
|
const model = new TTSGgml({
|
|
516
|
+
threads: TTS_TEST_THREADS,
|
|
482
517
|
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
483
518
|
files: { supertonicModel: dl.path, lavasrEnhancer: enh.path },
|
|
484
519
|
voice: 'F1',
|
|
@@ -525,6 +560,7 @@ test(
|
|
|
525
560
|
}
|
|
526
561
|
|
|
527
562
|
const model = new TTSGgml({
|
|
563
|
+
threads: TTS_TEST_THREADS,
|
|
528
564
|
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
529
565
|
files: { supertonicModel: dl.path },
|
|
530
566
|
voice: 'F1',
|
|
@@ -566,6 +602,7 @@ test(
|
|
|
566
602
|
const dir = dl.targetDir || modelsDir
|
|
567
603
|
|
|
568
604
|
const model = new TTSGgml({
|
|
605
|
+
threads: TTS_TEST_THREADS,
|
|
569
606
|
engine: TTSGgml.ENGINE_CHATTERBOX,
|
|
570
607
|
files: {
|
|
571
608
|
modelDir: dir,
|
|
@@ -615,6 +652,7 @@ test(
|
|
|
615
652
|
const dir = dl.targetDir || modelsDir
|
|
616
653
|
|
|
617
654
|
const model = new TTSGgml({
|
|
655
|
+
threads: TTS_TEST_THREADS,
|
|
618
656
|
engine: TTSGgml.ENGINE_CHATTERBOX,
|
|
619
657
|
files: {
|
|
620
658
|
modelDir: dir,
|
|
@@ -675,6 +713,7 @@ test(
|
|
|
675
713
|
}
|
|
676
714
|
|
|
677
715
|
const model = new TTSGgml({
|
|
716
|
+
threads: TTS_TEST_THREADS,
|
|
678
717
|
engine: TTSGgml.ENGINE_PARLER,
|
|
679
718
|
files: { parlerModel: dl.path, lavasrEnhancer: enh.path },
|
|
680
719
|
voice: 'Laura',
|
|
@@ -718,6 +757,7 @@ test(
|
|
|
718
757
|
}
|
|
719
758
|
|
|
720
759
|
const model = new TTSGgml({
|
|
760
|
+
threads: TTS_TEST_THREADS,
|
|
721
761
|
engine: TTSGgml.ENGINE_PARLER,
|
|
722
762
|
files: { parlerModel: dl.path },
|
|
723
763
|
voice: 'Laura',
|
|
@@ -731,6 +771,8 @@ test(
|
|
|
731
771
|
t.ok(r.samples > 0, 'synthesis produced audio')
|
|
732
772
|
t.ok(r.stats, 'runtimeStats returned (constructed with stats:true)')
|
|
733
773
|
t.is(r.stats.enhancerBackendDevice, -1, 'no enhancer loaded -> enhancerBackendDevice=-1')
|
|
774
|
+
t.is(r.stats.denoiserBackendDevice, -1, 'no denoiser loaded -> denoiserBackendDevice=-1')
|
|
775
|
+
t.is(r.stats.denoiserBackendId, -1, 'no denoiser loaded -> denoiserBackendId=-1')
|
|
734
776
|
} finally {
|
|
735
777
|
try {
|
|
736
778
|
await model.unload()
|
|
@@ -759,6 +801,7 @@ test(
|
|
|
759
801
|
}
|
|
760
802
|
|
|
761
803
|
const model = new TTSGgml({
|
|
804
|
+
threads: TTS_TEST_THREADS,
|
|
762
805
|
engine: TTSGgml.ENGINE_PARLER,
|
|
763
806
|
files: { parlerModel: dl.path, lavasrEnhancer: enh.path },
|
|
764
807
|
voice: 'Laura',
|
|
@@ -815,6 +858,7 @@ test(
|
|
|
815
858
|
}
|
|
816
859
|
|
|
817
860
|
const model = new TTSGgml({
|
|
861
|
+
threads: TTS_TEST_THREADS,
|
|
818
862
|
engine: TTSGgml.ENGINE_PARLER,
|
|
819
863
|
files: { parlerModel: dl.path, lavasrEnhancer: enh.path },
|
|
820
864
|
voice: 'Laura',
|
|
@@ -882,6 +926,7 @@ test(
|
|
|
882
926
|
-1,
|
|
883
927
|
'denoiser alone loads no enhancer (enhancerBackendDevice=-1)'
|
|
884
928
|
)
|
|
929
|
+
assertDenoiserOnCpu(t, denoisedOnly.stats, 'denoiser alone')
|
|
885
930
|
|
|
886
931
|
const enh = await ensureLavaSREnhancerGguf({
|
|
887
932
|
targetDir: path.join(baseDir, 'models', 'lavasr')
|
|
@@ -909,6 +954,7 @@ test(
|
|
|
909
954
|
0,
|
|
910
955
|
'useGPU:false -> enhancer on CPU (enhancerBackendDevice=0)'
|
|
911
956
|
)
|
|
957
|
+
assertDenoiserOnCpu(t, denoisedAndEnhanced.stats, 'denoiser + enhancer')
|
|
912
958
|
}
|
|
913
959
|
)
|
|
914
960
|
|
|
@@ -941,6 +987,7 @@ test(
|
|
|
941
987
|
}
|
|
942
988
|
|
|
943
989
|
const model = new TTSGgml({
|
|
990
|
+
threads: TTS_TEST_THREADS,
|
|
944
991
|
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
945
992
|
files: { supertonicModel: dl.path, lavasrEnhancer: enh.path },
|
|
946
993
|
voice: 'F1',
|
|
@@ -964,6 +1011,59 @@ test(
|
|
|
964
1011
|
}
|
|
965
1012
|
)
|
|
966
1013
|
|
|
1014
|
+
test(
|
|
1015
|
+
'Supertonic + LavaSR denoiser on GPU (useGPU:true) runs the denoiser on the engine device',
|
|
1016
|
+
{ timeout: 600000, skip: NO_GPU },
|
|
1017
|
+
async (t) => {
|
|
1018
|
+
const baseDir = getBaseDir()
|
|
1019
|
+
const den = await ensureLavaSRDenoiserGguf({
|
|
1020
|
+
targetDir: path.join(baseDir, 'models', 'lavasr')
|
|
1021
|
+
})
|
|
1022
|
+
if (!den.success) {
|
|
1023
|
+
t.comment('LavaSR denoiser GGUF not staged; skipping.')
|
|
1024
|
+
t.pass('skipped — no denoiser GGUF')
|
|
1025
|
+
return
|
|
1026
|
+
}
|
|
1027
|
+
const dl = await ensureSupertonicModel({ targetDir: path.join(baseDir, 'models') })
|
|
1028
|
+
if (!dl.success) {
|
|
1029
|
+
t.fail('Supertonic GGUF not available — registry fetch failed.')
|
|
1030
|
+
return
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const model = new TTSGgml({
|
|
1034
|
+
threads: TTS_TEST_THREADS,
|
|
1035
|
+
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
1036
|
+
files: { supertonicModel: dl.path, lavasrDenoiser: den.path },
|
|
1037
|
+
voice: 'F1',
|
|
1038
|
+
config: { language: 'en', useGPU: true },
|
|
1039
|
+
opts: { stats: true }
|
|
1040
|
+
})
|
|
1041
|
+
await model.load()
|
|
1042
|
+
try {
|
|
1043
|
+
const r = await runAndCollect(model, 'The denoiser follows the engine onto the GPU.')
|
|
1044
|
+
t.is(r.sampleRate, 44100, 'denoising preserves the native 44.1 kHz')
|
|
1045
|
+
t.ok(r.samples > 0, 'denoised synthesis produced audio')
|
|
1046
|
+
const { backendDevice, backendId, denoiserBackendDevice, denoiserBackendId } = r.stats
|
|
1047
|
+
console.log(
|
|
1048
|
+
`[denoiser/GPU] backendDevice=${backendDevice} denoiserBackendDevice=` +
|
|
1049
|
+
`${denoiserBackendDevice} denoiserBackendId=${denoiserBackendId} ` +
|
|
1050
|
+
`(${backendIdToName(denoiserBackendId)})`
|
|
1051
|
+
)
|
|
1052
|
+
t.not(denoiserBackendDevice, -1, 'denoiser was loaded (denoiserBackendDevice != -1)')
|
|
1053
|
+
t.is(denoiserBackendDevice, backendDevice, 'denoiser runs on the engine device')
|
|
1054
|
+
if (backendDevice === 1) {
|
|
1055
|
+
t.is(denoiserBackendId, backendId, 'GPU denoiser uses the engine backend')
|
|
1056
|
+
} else {
|
|
1057
|
+
t.is(denoiserBackendId, 0, 'a CPU-fallback engine keeps the denoiser on CPU')
|
|
1058
|
+
}
|
|
1059
|
+
} finally {
|
|
1060
|
+
try {
|
|
1061
|
+
await model.unload()
|
|
1062
|
+
} catch (_e) {}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
)
|
|
1066
|
+
|
|
967
1067
|
test(
|
|
968
1068
|
'Chatterbox + LavaSR enhancer on GPU (useGPU:true, batch) runs the enhancer on the GPU and reports 48 kHz',
|
|
969
1069
|
{ timeout: 900000, skip: NO_GPU },
|
|
@@ -986,6 +1086,7 @@ test(
|
|
|
986
1086
|
const dir = dl.targetDir || modelsDir
|
|
987
1087
|
|
|
988
1088
|
const model = new TTSGgml({
|
|
1089
|
+
threads: TTS_TEST_THREADS,
|
|
989
1090
|
engine: TTSGgml.ENGINE_CHATTERBOX,
|
|
990
1091
|
files: {
|
|
991
1092
|
modelDir: dir,
|