@qvac/tts-ggml 0.12.0 → 0.14.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 +127 -6
- package/docs/pocket-tts.md +12 -10
- package/examples/moss-sfx-fit.js +14 -7
- package/index.d.ts +43 -1130
- package/index.js +127 -3042
- package/lib/addonEvents.d.ts +26 -0
- package/lib/addonEvents.js +51 -0
- package/lib/conditioning.d.ts +50 -0
- package/lib/conditioning.js +184 -0
- package/lib/engineDetection.d.ts +14 -0
- package/lib/engineDetection.js +110 -0
- package/lib/engineIds.d.ts +15 -0
- package/lib/engineIds.js +39 -0
- package/lib/engineParams.d.ts +3 -0
- package/lib/engineParams.js +34 -0
- package/lib/engineSetup.d.ts +153 -0
- package/lib/engineSetup.js +2 -0
- package/lib/engines/audio8.d.ts +83 -0
- package/lib/engines/audio8.js +262 -0
- package/lib/engines/chatterbox.d.ts +10 -0
- package/lib/engines/chatterbox.js +112 -0
- package/lib/engines/cosyvoice.d.ts +87 -0
- package/lib/engines/cosyvoice.js +267 -0
- package/lib/engines/moss.d.ts +26 -0
- package/lib/engines/moss.js +177 -0
- package/lib/engines/mossSoundEffect.d.ts +41 -0
- package/lib/engines/mossSoundEffect.js +117 -0
- package/lib/engines/mossSpeech.d.ts +86 -0
- package/lib/engines/mossSpeech.js +228 -0
- package/lib/engines/parler.d.ts +71 -0
- package/lib/engines/parler.js +287 -0
- package/lib/engines/supertonic.d.ts +11 -0
- package/lib/engines/supertonic.js +115 -0
- package/lib/exclusiveQueue.d.ts +16 -0
- package/lib/exclusiveQueue.js +45 -0
- package/lib/fit.d.ts +3 -3
- package/lib/fit.js +4 -4
- package/lib/jobFields.d.ts +20 -0
- package/lib/jobFields.js +73 -0
- package/lib/modelFiles.d.ts +207 -0
- package/lib/modelFiles.js +117 -0
- package/lib/nativeParams.d.ts +12 -0
- package/lib/nativeParams.js +73 -0
- package/lib/optionValidation.d.ts +2 -0
- package/lib/optionValidation.js +103 -0
- package/lib/options.d.ts +4 -0
- package/lib/options.js +17 -0
- package/lib/pocketLifecycle.d.ts +67 -0
- package/lib/pocketLifecycle.js +170 -0
- package/lib/reload.d.ts +12 -0
- package/lib/reload.js +76 -0
- package/lib/sentenceStream.d.ts +85 -0
- package/lib/sentenceStream.js +367 -0
- package/lib/setup.d.ts +7 -0
- package/lib/setup.js +169 -0
- package/lib/streamingSupport.d.ts +8 -0
- package/lib/streamingSupport.js +77 -0
- package/lib/types.d.ts +465 -0
- package/lib/types.js +2 -0
- package/lib/voiceControls.d.ts +12 -0
- package/lib/voiceControls.js +23 -0
- package/package.json +17 -11
- package/test/integration/audio8.test.js +6 -12
- package/test/integration/fit.test.js +115 -18
- package/test/integration/gpu-smoke.test.js +2 -11
- package/test/integration/lavasr-enhancer.test.js +2 -15
- package/test/integration/moss-fit.test.js +13 -13
- package/test/integration/moss-sfx-fit.test.js +14 -14
- package/test/integration/moss-sfx.test.js +29 -4
- package/test/integration/moss-speech.test.js +19 -5
- package/test/integration/moss.test.js +93 -56
- package/test/mobile/integration.auto.cjs +10 -0
- package/test/mobile/test-groups.json +6 -2
- package/test/utils/desktopGpuBackend.js +21 -0
- package/test/utils/gpuOnly.js +15 -0
- package/test/utils/mossBackend.js +34 -0
- package/test/utils/mossGpuValidation.js +70 -0
- package/test/utils/perf-helper.js +275 -0
- package/test/utils/runTTS.js +1 -1
- package/test/utils/testThreads.js +5 -0
|
@@ -64,7 +64,7 @@ test('a projection is internally consistent', { timeout: TEST_TIMEOUT_MS }, asyn
|
|
|
64
64
|
const model = await supertonicPath(t)
|
|
65
65
|
if (model === null) return
|
|
66
66
|
|
|
67
|
-
const fit = TTSGgml.assessFit({
|
|
67
|
+
const fit = await TTSGgml.assessFit({
|
|
68
68
|
engineType: 'supertonic',
|
|
69
69
|
supertonicModelPath: model,
|
|
70
70
|
useGPU: true
|
|
@@ -84,13 +84,13 @@ test('a weightless copy projects the same', { timeout: TEST_TIMEOUT_MS }, async
|
|
|
84
84
|
const model = await supertonicPath(t)
|
|
85
85
|
if (model === null) return
|
|
86
86
|
|
|
87
|
-
const full = TTSGgml.assessFit({
|
|
87
|
+
const full = await TTSGgml.assessFit({
|
|
88
88
|
engineType: 'supertonic',
|
|
89
89
|
supertonicModelPath: model,
|
|
90
90
|
useGPU: true
|
|
91
91
|
})
|
|
92
92
|
if (!projected(t, full)) return
|
|
93
|
-
const stub = TTSGgml.assessFit({
|
|
93
|
+
const stub = await TTSGgml.assessFit({
|
|
94
94
|
engineType: 'supertonic',
|
|
95
95
|
supertonicModelPath: weightlessCopy(tempDir(t), model, 1024 * 1024),
|
|
96
96
|
useGPU: true
|
|
@@ -106,12 +106,12 @@ test('the load’s offload keys reach the projection', { timeout: TEST_TIMEOUT_M
|
|
|
106
106
|
const model = await supertonicPath(t)
|
|
107
107
|
if (model === null) return
|
|
108
108
|
|
|
109
|
-
const cpu = TTSGgml.assessFit({
|
|
109
|
+
const cpu = await TTSGgml.assessFit({
|
|
110
110
|
engineType: 'supertonic',
|
|
111
111
|
supertonicModelPath: model,
|
|
112
112
|
useGPU: false
|
|
113
113
|
})
|
|
114
|
-
const pinned = TTSGgml.assessFit({
|
|
114
|
+
const pinned = await TTSGgml.assessFit({
|
|
115
115
|
engineType: 'supertonic',
|
|
116
116
|
supertonicModelPath: model,
|
|
117
117
|
nGpuLayers: 0
|
|
@@ -129,7 +129,7 @@ test('a LavaSR stage is sized beside the projection', { timeout: TEST_TIMEOUT_MS
|
|
|
129
129
|
const enhancer = path.join(dir, 'enhancer.gguf')
|
|
130
130
|
fs.writeFileSync(enhancer, Buffer.alloc(4096, 1))
|
|
131
131
|
|
|
132
|
-
const fit = TTSGgml.assessFit({
|
|
132
|
+
const fit = await TTSGgml.assessFit({
|
|
133
133
|
engineType: 'supertonic',
|
|
134
134
|
supertonicModelPath: model,
|
|
135
135
|
useGPU: true,
|
|
@@ -141,8 +141,97 @@ test('a LavaSR stage is sized beside the projection', { timeout: TEST_TIMEOUT_MS
|
|
|
141
141
|
t.ok(fit.lavasrFileBytes < fit.deviceBytes, 'reported beside the projection')
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
-
test('
|
|
145
|
-
const
|
|
144
|
+
test('fitting keeps the event loop responsive', { timeout: TEST_TIMEOUT_MS }, async (t) => {
|
|
145
|
+
const model = await supertonicPath(t)
|
|
146
|
+
if (model === null) return
|
|
147
|
+
|
|
148
|
+
const request = { engineType: 'supertonic', supertonicModelPath: model, useGPU: true }
|
|
149
|
+
|
|
150
|
+
let pending = true
|
|
151
|
+
let turns = 0
|
|
152
|
+
const spin = () => {
|
|
153
|
+
if (!pending) return
|
|
154
|
+
turns++
|
|
155
|
+
setImmediate(spin)
|
|
156
|
+
}
|
|
157
|
+
setImmediate(spin)
|
|
158
|
+
|
|
159
|
+
const first = TTSGgml.assessFit(request)
|
|
160
|
+
t.ok(first instanceof Promise, 'the call returns before the projection')
|
|
161
|
+
const fits = await Promise.all([
|
|
162
|
+
first,
|
|
163
|
+
...Array.from({ length: 15 }, () => TTSGgml.assessFit(request))
|
|
164
|
+
])
|
|
165
|
+
pending = false
|
|
166
|
+
|
|
167
|
+
t.comment(`${turns} loop turns before the fits settled`)
|
|
168
|
+
t.ok(turns > 0, 'the loop turned while the fitter ran')
|
|
169
|
+
for (const fit of fits) {
|
|
170
|
+
t.not(fit.reason, 'model-unreadable', 'every queued projection ran')
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test(
|
|
175
|
+
'concurrent fits keep their own requests and all settle',
|
|
176
|
+
{ timeout: TEST_TIMEOUT_MS },
|
|
177
|
+
async (t) => {
|
|
178
|
+
const model = await supertonicPath(t)
|
|
179
|
+
if (model === null) return
|
|
180
|
+
|
|
181
|
+
const dir = tempDir(t)
|
|
182
|
+
const enhancer = path.join(dir, 'enhancer.gguf')
|
|
183
|
+
fs.writeFileSync(enhancer, Buffer.alloc(4096, 1))
|
|
184
|
+
|
|
185
|
+
const request = {
|
|
186
|
+
engineType: 'supertonic',
|
|
187
|
+
supertonicModelPath: model,
|
|
188
|
+
useGPU: false,
|
|
189
|
+
lavasrEnhancerPath: enhancer
|
|
190
|
+
}
|
|
191
|
+
const first = TTSGgml.assessFit(request)
|
|
192
|
+
request.lavasrEnhancerPath = ''
|
|
193
|
+
const second = TTSGgml.assessFit(request)
|
|
194
|
+
const missing = TTSGgml.assessFit({
|
|
195
|
+
engineType: 'supertonic',
|
|
196
|
+
supertonicModelPath: '/nonexistent/model.gguf'
|
|
197
|
+
})
|
|
198
|
+
const [a, b, c] = await Promise.all([first, second, missing])
|
|
199
|
+
|
|
200
|
+
t.is(a.lavasrFileBytes, 4096, 'the first call owns its original request')
|
|
201
|
+
t.is(b.lavasrFileBytes, 0, 'the second call owns its changed request')
|
|
202
|
+
t.is(c.status, 'error', 'an error outcome does not strand queued callers')
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
test('a fit settles while a loaded model unloads', { timeout: TEST_TIMEOUT_MS }, async (t) => {
|
|
207
|
+
const model = await supertonicPath(t)
|
|
208
|
+
if (model === null) return
|
|
209
|
+
|
|
210
|
+
const voice = new TTSGgml({
|
|
211
|
+
engine: TTSGgml.ENGINE_SUPERTONIC,
|
|
212
|
+
files: { supertonicModel: model },
|
|
213
|
+
voice: 'F1',
|
|
214
|
+
config: { language: 'en', useGPU: false }
|
|
215
|
+
})
|
|
216
|
+
await voice.load()
|
|
217
|
+
try {
|
|
218
|
+
const fit = TTSGgml.assessFit({
|
|
219
|
+
engineType: 'supertonic',
|
|
220
|
+
supertonicModelPath: model,
|
|
221
|
+
useGPU: false
|
|
222
|
+
})
|
|
223
|
+
await voice.unload()
|
|
224
|
+
const result = await fit
|
|
225
|
+
t.ok(result.deviceTotalBytes > 0, 'the projection measured a device')
|
|
226
|
+
} finally {
|
|
227
|
+
try {
|
|
228
|
+
await voice.unload()
|
|
229
|
+
} catch (_e) {}
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('MOSS routes to its fitter when a model cannot be read', async (t) => {
|
|
234
|
+
const fit = await TTSGgml.assessFit({
|
|
146
235
|
engineType: 'moss',
|
|
147
236
|
mossBackbonePath: '/models/moss-tts-delay.gguf',
|
|
148
237
|
mossCodecDecoderPath: '/models/moss-codec-decoder.gguf',
|
|
@@ -156,8 +245,8 @@ test('MOSS routes to its fitter when a model cannot be read', (t) => {
|
|
|
156
245
|
t.is(fit.modelVariant, 'moss')
|
|
157
246
|
})
|
|
158
247
|
|
|
159
|
-
test('a model that cannot be read is an outcome, not a
|
|
160
|
-
const fit = TTSGgml.assessFit({
|
|
248
|
+
test('a model that cannot be read is an outcome, not a rejection', async (t) => {
|
|
249
|
+
const fit = await TTSGgml.assessFit({
|
|
161
250
|
engineType: 'supertonic',
|
|
162
251
|
supertonicModelPath: '/nonexistent/model.gguf'
|
|
163
252
|
})
|
|
@@ -165,14 +254,22 @@ test('a model that cannot be read is an outcome, not a throw', (t) => {
|
|
|
165
254
|
t.is(fit.status, 'error')
|
|
166
255
|
})
|
|
167
256
|
|
|
168
|
-
test('a count that is not a count is refused', (t) => {
|
|
169
|
-
t.exception(
|
|
170
|
-
(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}),
|
|
257
|
+
test('a count that is not a count is refused', async (t) => {
|
|
258
|
+
await t.exception(
|
|
259
|
+
TTSGgml.assessFit({
|
|
260
|
+
engineType: 'supertonic',
|
|
261
|
+
supertonicModelPath: '/models/supertonic.gguf',
|
|
262
|
+
textTokens: -1
|
|
263
|
+
}),
|
|
176
264
|
/non-negative count/
|
|
177
265
|
)
|
|
178
266
|
})
|
|
267
|
+
|
|
268
|
+
test('a broken request rejects', async (t) => {
|
|
269
|
+
let promise
|
|
270
|
+
t.execution(() => {
|
|
271
|
+
promise = TTSGgml.assessFit({})
|
|
272
|
+
}, 'the call itself does not throw')
|
|
273
|
+
t.ok(promise instanceof Promise)
|
|
274
|
+
await t.exception(promise, /engineType is required/)
|
|
275
|
+
})
|
|
@@ -46,15 +46,11 @@ const {
|
|
|
46
46
|
} = require('../utils/downloadModel')
|
|
47
47
|
const { recordTtsStats } = require('../utils/perf-helper')
|
|
48
48
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
49
|
+
const { assertDesktopGpuBackend } = require('../utils/desktopGpuBackend')
|
|
49
50
|
|
|
50
51
|
const platform = os.platform()
|
|
51
52
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
52
53
|
const isApple = platform === 'darwin' || platform === 'ios'
|
|
53
|
-
const GPU_BACKEND_IDS = { metal: 1, cuda: 2, vulkan: 3, opencl: 4 }
|
|
54
|
-
// CI rows whose prebuild bundles more than one usable GPU backend (the linux
|
|
55
|
-
// runners carry CUDA and Vulkan) pin the engine cascade through
|
|
56
|
-
// TTS_CPP_GPU_BACKEND; the assertions expect whatever the row pinned and fall
|
|
57
|
-
// back to the platform's cascade default when unset.
|
|
58
54
|
const PINNED_GPU_BACKEND = (proc.env && proc.env.TTS_CPP_GPU_BACKEND) || ''
|
|
59
55
|
// Parler GPU coverage is validated on Apple, the Android Device Farm, and the
|
|
60
56
|
// linux CUDA lane. Keep desktop Vulkan out until dedicated runs prove it there.
|
|
@@ -153,12 +149,7 @@ function assertGpuBackend(t, engineTag, stats, allowPolicyCpu = false) {
|
|
|
153
149
|
if (platform === 'darwin' || platform === 'ios') {
|
|
154
150
|
t.is(id, 1, `${engineTag}/${platform}: expected Metal backendId=1, got ${name}`)
|
|
155
151
|
} else if (platform === 'linux' || platform === 'win32') {
|
|
156
|
-
|
|
157
|
-
t.is(
|
|
158
|
-
id,
|
|
159
|
-
expectedId,
|
|
160
|
-
`${engineTag}/${platform}: expected ${backendIdToName(expectedId)} backendId=${expectedId}, got ${name}`
|
|
161
|
-
)
|
|
152
|
+
assertDesktopGpuBackend(t, `${engineTag}/${platform}`, dev, id, PINNED_GPU_BACKEND)
|
|
162
153
|
} else if (platform === 'android') {
|
|
163
154
|
t.ok(
|
|
164
155
|
id === 3 || id === 4,
|
|
@@ -34,6 +34,7 @@ const {
|
|
|
34
34
|
const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
|
|
35
35
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
36
36
|
const { GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
37
|
+
const { assertDesktopGpuBackend } = require('../utils/desktopGpuBackend')
|
|
37
38
|
|
|
38
39
|
const platform = os.platform()
|
|
39
40
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
@@ -42,11 +43,6 @@ const isMobile = platform === 'ios' || platform === 'android'
|
|
|
42
43
|
// fallback from a failure to a warning (e.g. a Linux host with no Vulkan SDK).
|
|
43
44
|
const NO_GPU = proc.env && proc.env.NO_GPU === 'true'
|
|
44
45
|
const RELAX = proc.env && proc.env.QVAC_TTS_GPU_SMOKE_RELAX === '1'
|
|
45
|
-
const GPU_BACKEND_IDS = { metal: 1, cuda: 2, vulkan: 3, opencl: 4 }
|
|
46
|
-
// CI rows whose prebuild bundles more than one usable GPU backend (the linux
|
|
47
|
-
// runners carry CUDA and Vulkan) pin the engine cascade through
|
|
48
|
-
// TTS_CPP_GPU_BACKEND; the enhancer assertion expects whatever the row pinned
|
|
49
|
-
// and falls back to the platform's cascade default when unset.
|
|
50
46
|
const PINNED_GPU_BACKEND = (proc.env && proc.env.TTS_CPP_GPU_BACKEND) || ''
|
|
51
47
|
|
|
52
48
|
const ENHANCED_RATE = 48000
|
|
@@ -92,10 +88,6 @@ function expectsGpu() {
|
|
|
92
88
|
)
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
function expectedDesktopEnhancerBackendId() {
|
|
96
|
-
return GPU_BACKEND_IDS[PINNED_GPU_BACKEND] || GPU_BACKEND_IDS.vulkan
|
|
97
|
-
}
|
|
98
|
-
|
|
99
91
|
// Strict check that the LavaSR *enhancer* network engaged the GPU. Reads the
|
|
100
92
|
// enhancer-specific backend surfaced by ChatterboxModel/SupertonicModel
|
|
101
93
|
// runtimeStats (enhancerBackendDevice: -1 none / 0 CPU / 1 GPU;
|
|
@@ -134,12 +126,7 @@ function assertEnhancerGpuBackend(t, stats) {
|
|
|
134
126
|
if (platform === 'darwin' || platform === 'ios') {
|
|
135
127
|
t.is(id, 1, `enhancer/${platform}: expected Metal backendId=1, got ${backendIdToName(id)}`)
|
|
136
128
|
} else if (platform === 'linux' || platform === 'win32') {
|
|
137
|
-
|
|
138
|
-
t.is(
|
|
139
|
-
id,
|
|
140
|
-
expectedId,
|
|
141
|
-
`enhancer/${platform}: expected ${backendIdToName(expectedId)} backendId=${expectedId}, got ${backendIdToName(id)}`
|
|
142
|
-
)
|
|
129
|
+
assertDesktopGpuBackend(t, `enhancer/${platform}`, dev, id, PINNED_GPU_BACKEND)
|
|
143
130
|
} else if (platform === 'android') {
|
|
144
131
|
t.ok(
|
|
145
132
|
id === 3 || id === 4,
|
|
@@ -5,7 +5,7 @@ const process = require('bare-process')
|
|
|
5
5
|
const test = require('brittle')
|
|
6
6
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
7
7
|
|
|
8
|
-
test('MOSS fit requires explicit, representable workload counts', (t) => {
|
|
8
|
+
test('MOSS fit requires explicit, representable workload counts', async (t) => {
|
|
9
9
|
const request = {
|
|
10
10
|
engineType: 'moss',
|
|
11
11
|
mossBackbonePath: '/models/moss-tts-delay.gguf',
|
|
@@ -15,18 +15,18 @@ test('MOSS fit requires explicit, representable workload counts', (t) => {
|
|
|
15
15
|
streaming: false
|
|
16
16
|
}
|
|
17
17
|
for (const invalid of [NaN, Infinity, -1, 0.5, 2147483648]) {
|
|
18
|
-
t.exception(
|
|
19
|
-
t.exception(
|
|
18
|
+
await t.exception(TTSGgml.assessFit({ ...request, promptRows: invalid }))
|
|
19
|
+
await t.exception(TTSGgml.assessFit({ ...request, referenceSamples: invalid }))
|
|
20
20
|
}
|
|
21
|
-
t.exception(
|
|
22
|
-
t.exception(
|
|
23
|
-
t.exception(
|
|
24
|
-
t.exception(
|
|
25
|
-
t.exception(
|
|
26
|
-
t.exception(
|
|
21
|
+
await t.exception(TTSGgml.assessFit({ ...request, promptRows: undefined }))
|
|
22
|
+
await t.exception(TTSGgml.assessFit({ ...request, streaming: undefined }))
|
|
23
|
+
await t.exception(TTSGgml.assessFit({ ...request, threads: -1 }))
|
|
24
|
+
await t.exception(TTSGgml.assessFit({ ...request, durationTokens: -1 }))
|
|
25
|
+
await t.exception(TTSGgml.assessFit({ ...request, streamChunkTokens: -1 }))
|
|
26
|
+
await t.exception(TTSGgml.assessFit({ ...request, useGPU: true, nGpuLayers: 0 }))
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
test('MOSS metadata-only fit covers batch, streaming and cloning', (t) => {
|
|
29
|
+
test('MOSS metadata-only fit covers batch, streaming and cloning', async (t) => {
|
|
30
30
|
const modelDir = process.env.QVAC_TEST_MOSS_MODEL_DIR
|
|
31
31
|
if (!modelDir) {
|
|
32
32
|
t.comment('Set QVAC_TEST_MOSS_MODEL_DIR to exercise real MOSS GGUFs')
|
|
@@ -40,14 +40,14 @@ test('MOSS metadata-only fit covers batch, streaming and cloning', (t) => {
|
|
|
40
40
|
referenceSamples: 0,
|
|
41
41
|
streaming: false
|
|
42
42
|
}
|
|
43
|
-
const batch = TTSGgml.assessFit(request)
|
|
43
|
+
const batch = await TTSGgml.assessFit(request)
|
|
44
44
|
t.not(batch.status, 'error', batch.report)
|
|
45
45
|
t.ok(batch.weightsBytes > 0)
|
|
46
46
|
t.ok(batch.lmComputeBytes > 0)
|
|
47
47
|
t.ok(batch.codecComputeBytes > 0)
|
|
48
|
-
const stream = TTSGgml.assessFit({ ...request, streaming: true })
|
|
48
|
+
const stream = await TTSGgml.assessFit({ ...request, streaming: true })
|
|
49
49
|
t.not(stream.status, 'error', stream.report)
|
|
50
|
-
const cloning = TTSGgml.assessFit({
|
|
50
|
+
const cloning = await TTSGgml.assessFit({
|
|
51
51
|
...request,
|
|
52
52
|
mossCodecEncoderPath: path.join(modelDir, 'moss-codec-encoder-f16.gguf'),
|
|
53
53
|
referenceSamples: 24000
|
|
@@ -15,17 +15,17 @@ function request(overrides = {}) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
test('MOSS sound-effect fit rejects missing workload and invalid load controls', (t) => {
|
|
19
|
-
t.exception(
|
|
20
|
-
t.exception(
|
|
21
|
-
t.exception(
|
|
22
|
-
t.exception(
|
|
23
|
-
t.exception(
|
|
24
|
-
t.exception(
|
|
18
|
+
test('MOSS sound-effect fit rejects missing workload and invalid load controls', async (t) => {
|
|
19
|
+
await t.exception(TTSGgml.assessFit(request({ prompt: undefined })))
|
|
20
|
+
await t.exception(TTSGgml.assessFit(request({ seconds: undefined })))
|
|
21
|
+
await t.exception(TTSGgml.assessFit(request({ seconds: NaN })))
|
|
22
|
+
await t.exception(TTSGgml.assessFit(request({ threads: -1 })))
|
|
23
|
+
await t.exception(TTSGgml.assessFit(request({ useGPU: true, nGpuLayers: 0 })))
|
|
24
|
+
await t.exception(TTSGgml.assessFit(request({ marginBytes: -1 })))
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
test('MOSS sound-effect fit reports unreadable models', (t) => {
|
|
28
|
-
const fit = TTSGgml.assessFit(request())
|
|
27
|
+
test('MOSS sound-effect fit reports unreadable models', async (t) => {
|
|
28
|
+
const fit = await TTSGgml.assessFit(request())
|
|
29
29
|
t.is(fit.status, 'error')
|
|
30
30
|
t.is(fit.reason, 'model-unreadable')
|
|
31
31
|
t.is(fit.modelVariant, 'moss-sfx')
|
|
@@ -36,10 +36,10 @@ test(
|
|
|
36
36
|
{
|
|
37
37
|
skip: !process.env.QVAC_TEST_MOSS_SFX_GGUF
|
|
38
38
|
},
|
|
39
|
-
(t) => {
|
|
39
|
+
async (t) => {
|
|
40
40
|
const model = process.env.QVAC_TEST_MOSS_SFX_GGUF
|
|
41
41
|
const cfg = { mossSoundEffectPath: model }
|
|
42
|
-
const fit = TTSGgml.assessFit(request(cfg))
|
|
42
|
+
const fit = await TTSGgml.assessFit(request(cfg))
|
|
43
43
|
t.ok(fit.status === 'fits' || fit.status === 'does-not-fit')
|
|
44
44
|
t.is(fit.modelVariant, 'moss-sfx')
|
|
45
45
|
t.ok(fit.deviceIsCpu)
|
|
@@ -51,12 +51,12 @@ test(
|
|
|
51
51
|
fit.deviceBytes,
|
|
52
52
|
fit.weightsBytes + fit.stateBytes + fit.lmComputeBytes + fit.codecComputeBytes
|
|
53
53
|
)
|
|
54
|
-
const shorter = TTSGgml.assessFit(request({ ...cfg, seconds: 1 }))
|
|
54
|
+
const shorter = await TTSGgml.assessFit(request({ ...cfg, seconds: 1 }))
|
|
55
55
|
t.is(shorter.weightsBytes, fit.weightsBytes)
|
|
56
56
|
t.is(shorter.lmComputeBytes, fit.lmComputeBytes)
|
|
57
|
-
const noHeadroom = TTSGgml.assessFit(request({ ...cfg, marginBytes: Number.MAX_VALUE }))
|
|
57
|
+
const noHeadroom = await TTSGgml.assessFit(request({ ...cfg, marginBytes: Number.MAX_VALUE }))
|
|
58
58
|
t.is(noHeadroom.status, 'does-not-fit')
|
|
59
|
-
const invalid = TTSGgml.assessFit(request({ ...cfg, seconds: 0 }))
|
|
59
|
+
const invalid = await TTSGgml.assessFit(request({ ...cfg, seconds: 0 }))
|
|
60
60
|
t.is(invalid.status, 'error')
|
|
61
61
|
t.is(invalid.reason, 'invalid-arguments')
|
|
62
62
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const path = require('bare-path')
|
|
4
|
+
const fs = require('bare-fs')
|
|
4
5
|
const proc = require('bare-process')
|
|
5
6
|
const test = require('brittle')
|
|
6
7
|
|
|
7
8
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
9
|
+
const { assertMossBackend } = require('../utils/mossBackend')
|
|
8
10
|
|
|
9
11
|
const SFX_SAMPLE_RATE = 48000
|
|
10
12
|
const TENTHS_PER_SECOND = 10
|
|
@@ -14,13 +16,35 @@ const MODEL_DIR_ENV = 'QVAC_TEST_MOSS_SFX_MODEL_DIR'
|
|
|
14
16
|
const QUICK_STEPS = 2
|
|
15
17
|
const SHORT_SECONDS = 1.5
|
|
16
18
|
const LONGER_SECONDS = 2.5
|
|
17
|
-
const
|
|
18
|
-
const GPU_DEVICE = 1
|
|
19
|
+
const PINNED_GPU_BACKEND = (proc.env && proc.env.TTS_CPP_GPU_BACKEND) || ''
|
|
19
20
|
|
|
20
21
|
const modelDir = (proc.env && proc.env[MODEL_DIR_ENV]) || ''
|
|
21
22
|
const skipWithoutModels = modelDir === ''
|
|
22
23
|
const noGpu = proc.env && proc.env.NO_GPU === 'true'
|
|
23
24
|
|
|
25
|
+
if (proc.env && proc.env.QVAC_TEST_MOSS_FULL_GPU === 'true') {
|
|
26
|
+
if (noGpu || !['cuda', 'vulkan', 'metal', 'opencl'].includes(PINNED_GPU_BACKEND.toLowerCase())) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'MOSS-SFX full GPU validation requires an explicit GPU backend and NO_GPU=false'
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
!modelDir ||
|
|
33
|
+
!fs.existsSync(modelDir) ||
|
|
34
|
+
!fs
|
|
35
|
+
.readdirSync(modelDir)
|
|
36
|
+
.some(
|
|
37
|
+
(name) =>
|
|
38
|
+
/^moss-sfx(-[a-z0-9_]+)*\.gguf$/i.test(name) &&
|
|
39
|
+
fs.statSync(path.join(modelDir, name)).isFile()
|
|
40
|
+
)
|
|
41
|
+
) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'MOSS-SFX full GPU validation requires QVAC_TEST_MOSS_SFX_MODEL_DIR with a GGUF'
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
function createSfxModel(useGPU = false) {
|
|
25
49
|
return new TTSGgml({
|
|
26
50
|
engine: TTSGgml.ENGINE_MOSS_SFX,
|
|
@@ -70,7 +94,7 @@ test(
|
|
|
70
94
|
try {
|
|
71
95
|
const result = await generate(model, { seconds: SHORT_SECONDS })
|
|
72
96
|
assertClip(t, 'cpu clip', result, SHORT_SECONDS)
|
|
73
|
-
t
|
|
97
|
+
assertMossBackend(t, 'cpu clip', result.stats, false, PINNED_GPU_BACKEND)
|
|
74
98
|
} finally {
|
|
75
99
|
await model.unload()
|
|
76
100
|
}
|
|
@@ -88,7 +112,8 @@ test(
|
|
|
88
112
|
const longer = await generate(model, { seconds: LONGER_SECONDS, negativePrompt: 'music' })
|
|
89
113
|
assertClip(t, 'short clip', short, SHORT_SECONDS)
|
|
90
114
|
assertClip(t, 'longer clip', longer, LONGER_SECONDS)
|
|
91
|
-
t
|
|
115
|
+
assertMossBackend(t, 'short clip', short.stats, true, PINNED_GPU_BACKEND)
|
|
116
|
+
assertMossBackend(t, 'longer clip', longer.stats, true, PINNED_GPU_BACKEND)
|
|
92
117
|
} finally {
|
|
93
118
|
await model.unload()
|
|
94
119
|
}
|
|
@@ -8,6 +8,9 @@ const TTSGgml = require('@qvac/tts-ggml')
|
|
|
8
8
|
const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
|
|
9
9
|
const { readWavAsFloat32 } = require('../utils/wav-helper')
|
|
10
10
|
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
11
|
+
const { assertMossBackend } = require('../utils/mossBackend')
|
|
12
|
+
const { createMossGpuValidation } = require('../utils/mossGpuValidation')
|
|
13
|
+
const { GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
11
14
|
|
|
12
15
|
const SPEECH_SAMPLE_RATE = 24000
|
|
13
16
|
const TEST_TIMEOUT_MS = 1800000
|
|
@@ -15,12 +18,13 @@ const MODEL_DIR_ENV = 'QVAC_TEST_MOSS_SPEECH_MODEL_DIR'
|
|
|
15
18
|
const MAX_NEW_TOKENS = 120
|
|
16
19
|
const CUT_SECONDS = 2
|
|
17
20
|
const CUT_MARGIN_SECONDS = 0.5
|
|
18
|
-
const
|
|
19
|
-
const GPU_DEVICE = 1
|
|
21
|
+
const PINNED_GPU_BACKEND = (proc.env && proc.env.TTS_CPP_GPU_BACKEND) || ''
|
|
20
22
|
|
|
21
23
|
const modelDir = (proc.env && proc.env[MODEL_DIR_ENV]) || ''
|
|
22
24
|
const skipWithoutModels = modelDir === ''
|
|
23
25
|
const noGpu = proc.env && proc.env.NO_GPU === 'true'
|
|
26
|
+
const sourceAudio = resolveRefWavPath({})
|
|
27
|
+
const validation = createMossGpuValidation(proc.env || {}, sourceAudio, GPU_ONLY_USE_GPU)
|
|
24
28
|
|
|
25
29
|
function createSpeechModel(useGPU = false) {
|
|
26
30
|
return new TTSGgml({
|
|
@@ -34,7 +38,7 @@ function createSpeechModel(useGPU = false) {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
function spokenQuestion() {
|
|
37
|
-
const wav = readWavAsFloat32(
|
|
41
|
+
const wav = readWavAsFloat32(sourceAudio)
|
|
38
42
|
return { audio: wav.samples, sampleRate: wav.sampleRate }
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -75,10 +79,11 @@ test(
|
|
|
75
79
|
try {
|
|
76
80
|
const result = await respond(model, spokenQuestion())
|
|
77
81
|
assertSpokenReply(t, 'cpu reply', result)
|
|
78
|
-
t
|
|
82
|
+
assertMossBackend(t, 'cpu reply', result.stats, false, PINNED_GPU_BACKEND)
|
|
79
83
|
} finally {
|
|
80
84
|
await model.unload()
|
|
81
85
|
}
|
|
86
|
+
validation.completed('CPU spoken reply')
|
|
82
87
|
}
|
|
83
88
|
)
|
|
84
89
|
|
|
@@ -92,6 +97,7 @@ test(
|
|
|
92
97
|
const textReply = await respond(model, { ...spokenQuestion(), textReply: true })
|
|
93
98
|
t.is(textReply.samples, 0, 'a text reply carries no audio')
|
|
94
99
|
t.ok(textReply.text.trim().length > 0, 'a text reply carries the answer')
|
|
100
|
+
assertMossBackend(t, 'text reply', textReply.stats, true, PINNED_GPU_BACKEND)
|
|
95
101
|
|
|
96
102
|
const cut = await respond(model, { ...spokenQuestion(), maxReplySeconds: CUT_SECONDS })
|
|
97
103
|
assertSpokenReply(t, 'cut reply', cut)
|
|
@@ -99,12 +105,20 @@ test(
|
|
|
99
105
|
cut.samples <= (CUT_SECONDS + CUT_MARGIN_SECONDS) * SPEECH_SAMPLE_RATE,
|
|
100
106
|
'maxReplySeconds bounds the spoken reply'
|
|
101
107
|
)
|
|
102
|
-
t
|
|
108
|
+
assertMossBackend(t, 'cut reply', cut.stats, true, PINNED_GPU_BACKEND)
|
|
103
109
|
|
|
104
110
|
const typed = await respond(model, { input: 'What is the capital of France?' })
|
|
105
111
|
assertSpokenReply(t, 'reply to a typed question', typed)
|
|
112
|
+
assertMossBackend(t, 'typed reply', typed.stats, true, PINNED_GPU_BACKEND)
|
|
106
113
|
} finally {
|
|
107
114
|
await model.unload()
|
|
108
115
|
}
|
|
116
|
+
validation.completed('GPU text/audio replies and reply cuts')
|
|
109
117
|
}
|
|
110
118
|
)
|
|
119
|
+
|
|
120
|
+
if (validation.enabled) {
|
|
121
|
+
test('MOSS-Speech: full GPU validation completed every required case', (t) => {
|
|
122
|
+
validation.assertComplete(t, ['CPU spoken reply', 'GPU text/audio replies and reply cuts'])
|
|
123
|
+
})
|
|
124
|
+
}
|