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