@qvac/tts-ggml 0.11.1 → 0.13.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.
@@ -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('MOSS routes to its fitter when a model cannot be read', (t) => {
145
- const fit = TTSGgml.assessFit({
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 throw', (t) => {
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
- TTSGgml.assessFit({
172
- engineType: 'supertonic',
173
- supertonicModelPath: '/models/supertonic.gguf',
174
- textTokens: -1
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
+ })
@@ -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(() => TTSGgml.assessFit({ ...request, promptRows: invalid }))
19
- t.exception(() => TTSGgml.assessFit({ ...request, referenceSamples: invalid }))
18
+ await t.exception(TTSGgml.assessFit({ ...request, promptRows: invalid }))
19
+ await t.exception(TTSGgml.assessFit({ ...request, referenceSamples: invalid }))
20
20
  }
21
- t.exception(() => TTSGgml.assessFit({ ...request, promptRows: undefined }))
22
- t.exception(() => TTSGgml.assessFit({ ...request, streaming: undefined }))
23
- t.exception(() => TTSGgml.assessFit({ ...request, threads: -1 }))
24
- t.exception(() => TTSGgml.assessFit({ ...request, durationTokens: -1 }))
25
- t.exception(() => TTSGgml.assessFit({ ...request, streamChunkTokens: -1 }))
26
- t.exception(() => TTSGgml.assessFit({ ...request, useGPU: true, nGpuLayers: 0 }))
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(() => TTSGgml.assessFit(request({ prompt: undefined })))
20
- t.exception(() => TTSGgml.assessFit(request({ seconds: undefined })))
21
- t.exception(() => TTSGgml.assessFit(request({ seconds: NaN })))
22
- t.exception(() => TTSGgml.assessFit(request({ threads: -1 })))
23
- t.exception(() => TTSGgml.assessFit(request({ useGPU: true, nGpuLayers: 0 })))
24
- t.exception(() => TTSGgml.assessFit(request({ marginBytes: -1 })))
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
  }
@@ -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 CPU_DEVICE = 0
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(resolveRefWavPath({}))
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.is(result.stats.backendDevice, CPU_DEVICE, 'the reply ran on the CPU')
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.is(cut.stats.backendDevice, GPU_DEVICE, 'the replies ran on the GPU')
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
+ }
@@ -12,6 +12,12 @@ 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
14
  const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
15
+ const { assertMossBackend } = require('../utils/mossBackend')
16
+ const {
17
+ createMossGpuValidation,
18
+ ENCODER_RE,
19
+ DIALOGUE_BACKBONE_RE
20
+ } = require('../utils/mossGpuValidation')
15
21
 
16
22
  const MOSS_SAMPLE_RATE = 24000
17
23
  const MOSS_STREAM_FRAMES = 10
@@ -19,8 +25,6 @@ const MOSS_SAMPLES_PER_FRAME = 1920
19
25
  const TEST_TIMEOUT_MS = 1800000
20
26
  const SYNTHESIS_TEXT = 'The MOSS integration test checks this generated voice.'
21
27
  const MODEL_DIR_ENV = 'QVAC_TEST_MOSS_MODEL_DIR'
22
- const ENCODER_RE = /^moss-codec-encoder(-[a-z0-9_]+)?\.gguf$/i
23
- const DIALOGUE_BACKBONE_RE = /^moss-ttsd(-[a-z0-9_]+)?\.gguf$/i
24
28
  const DIALOGUE_TEXT =
25
29
  '[S1] And so, my fellow Americans, [S2] ask what you can do for your country. ' +
26
30
  '[S1] Did the build finish this morning? [S2] Yes, every single test passed.'
@@ -29,12 +33,12 @@ const SPEAKER_TWO_SECONDS = [7.6, 10.6]
29
33
  const SHORT_DURATION_TOKENS = 25
30
34
  const LONG_DURATION_TOKENS = 100
31
35
  const INT16_FULL_SCALE = 32767
32
- const CPU_DEVICE = 0
33
- const CPU_BACKEND = 0
34
- const GPU_DEVICE = 1
36
+ const PINNED_GPU_BACKEND = (proc.env && proc.env.TTS_CPP_GPU_BACKEND) || ''
35
37
 
36
38
  const modelDir = (proc.env && proc.env[MODEL_DIR_ENV]) || ''
37
39
  const skipWithoutModels = modelDir === ''
40
+ const sourceAudio = resolveRefWavPath({})
41
+ const validation = createMossGpuValidation(proc.env || {}, sourceAudio, GPU_ONLY_USE_GPU)
38
42
 
39
43
  function createMossModel(extra = {}, useGPU = false) {
40
44
  return new TTSGgml({
@@ -133,9 +137,8 @@ function recordMoss(t, label, result, wallMs) {
133
137
  )
134
138
  }
135
139
 
136
- function assertCpuBackend(t, label, stats) {
137
- t.is(stats.backendDevice, CPU_DEVICE, `${label} reports a CPU device`)
138
- t.is(stats.backendId, CPU_BACKEND, `${label} reports the CPU backend`)
140
+ function assertBackend(t, label, stats, useGPU = false) {
141
+ assertMossBackend(t, label, stats, useGPU, PINNED_GPU_BACKEND)
139
142
  }
140
143
 
141
144
  async function withLoadedModel(extra, body, useGPU = false) {
@@ -156,31 +159,40 @@ test(
156
159
  const started = Date.now()
157
160
  const result = await synthesize(model, SYNTHESIS_TEXT)
158
161
  assertAudio(t, 'MOSS batch', result)
159
- assertCpuBackend(t, 'MOSS batch', result.stats)
162
+ assertBackend(t, 'MOSS batch', result.stats)
160
163
  t.is(nonEmptyChunks(result).length, 1, 'batch synthesis emits one buffer')
161
164
  recordMoss(t, 'moss batch', result, Date.now() - started)
162
165
  })
166
+ validation.completed('CPU batch')
163
167
  }
164
168
  )
165
169
 
166
- test(
167
- 'MOSS TTS: native chunk streaming emits frame-sized chunks',
168
- { timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels },
169
- async (t) => {
170
- await withLoadedModel({ streamChunkTokens: MOSS_STREAM_FRAMES }, async (model) => {
171
- const started = Date.now()
172
- const result = await synthesize(model, SYNTHESIS_TEXT)
173
- assertAudio(t, 'MOSS stream', result)
174
- const chunks = nonEmptyChunks(result)
175
- t.ok(chunks.length > 1, `streaming emits several chunks (got ${chunks.length})`)
176
- t.ok(
177
- chunks.every((length) => length <= MOSS_STREAM_FRAMES * MOSS_SAMPLES_PER_FRAME),
178
- 'no chunk exceeds the configured frame count'
170
+ for (const useGPU of [false, true]) {
171
+ test(
172
+ `MOSS TTS: native chunk streaming emits frame-sized chunks (${useGPU ? 'GPU' : 'CPU'})`,
173
+ { timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels || (useGPU && NO_GPU) },
174
+ async (t) => {
175
+ await withLoadedModel(
176
+ { streamChunkTokens: MOSS_STREAM_FRAMES },
177
+ async (model) => {
178
+ const started = Date.now()
179
+ const result = await synthesize(model, SYNTHESIS_TEXT)
180
+ assertAudio(t, 'MOSS stream', result)
181
+ assertBackend(t, 'MOSS stream', result.stats, useGPU)
182
+ const chunks = nonEmptyChunks(result)
183
+ t.ok(chunks.length > 1, `streaming emits several chunks (got ${chunks.length})`)
184
+ t.ok(
185
+ chunks.every((length) => length <= MOSS_STREAM_FRAMES * MOSS_SAMPLES_PER_FRAME),
186
+ 'no chunk exceeds the configured frame count'
187
+ )
188
+ recordMoss(t, 'moss stream', result, Date.now() - started)
189
+ },
190
+ useGPU
179
191
  )
180
- recordMoss(t, 'moss stream', result, Date.now() - started)
181
- })
182
- }
183
- )
192
+ validation.completed(`${useGPU ? 'GPU' : 'CPU'} streaming`)
193
+ }
194
+ )
195
+ }
184
196
 
185
197
  test(
186
198
  'MOSS TTS: useGPU=true synthesizes on a GPU backend',
@@ -192,42 +204,48 @@ test(
192
204
  const started = Date.now()
193
205
  const result = await synthesize(model, SYNTHESIS_TEXT)
194
206
  assertAudio(t, 'MOSS GPU', result)
195
- t.is(result.stats.backendDevice, GPU_DEVICE, 'MOSS GPU reports a GPU device')
196
- t.not(result.stats.backendId, CPU_BACKEND, 'MOSS GPU does not report the CPU backend')
197
- t.is(result.stats.gpuUnsupported, 0, 'MOSS GPU does not report a GPU fallback')
207
+ assertBackend(t, 'MOSS GPU', result.stats, true)
198
208
  recordMoss(t, 'moss gpu', result, Date.now() - started)
199
209
  },
200
210
  true
201
211
  )
212
+ validation.completed('GPU batch')
202
213
  }
203
214
  )
204
215
 
205
- test(
206
- 'MOSS TTS: voice cloning synthesizes from a reference recording',
207
- { timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels },
208
- async (t) => {
209
- const sourceAudio = resolveRefWavPath({})
210
- if (!hasEncoder()) {
211
- t.comment(`skipping: no moss-codec-encoder GGUF in ${modelDir}`)
212
- return
213
- }
214
- if (!fs.existsSync(sourceAudio)) {
215
- t.fail(`MOSS reference audio is missing: ${sourceAudio}`)
216
- return
217
- }
218
- const referenceAudio = writeReferenceAtMossRate(sourceAudio)
219
- try {
220
- await withLoadedModel({ referenceAudio, seed: 7 }, async (model) => {
221
- const started = Date.now()
222
- const result = await synthesize(model, SYNTHESIS_TEXT)
223
- assertAudio(t, 'MOSS clone', result)
224
- recordMoss(t, 'moss clone', result, Date.now() - started)
225
- })
226
- } finally {
227
- fs.unlinkSync(referenceAudio)
216
+ for (const useGPU of [false, true]) {
217
+ test(
218
+ `MOSS TTS: voice cloning synthesizes from a reference recording (${useGPU ? 'GPU' : 'CPU'})`,
219
+ { timeout: TEST_TIMEOUT_MS, skip: skipWithoutModels || (useGPU && NO_GPU) },
220
+ async (t) => {
221
+ if (!hasEncoder()) {
222
+ t.comment(`skipping: no moss-codec-encoder GGUF in ${modelDir}`)
223
+ return
224
+ }
225
+ if (!fs.existsSync(sourceAudio)) {
226
+ t.fail(`MOSS reference audio is missing: ${sourceAudio}`)
227
+ return
228
+ }
229
+ const referenceAudio = writeReferenceAtMossRate(sourceAudio)
230
+ try {
231
+ await withLoadedModel(
232
+ { referenceAudio, seed: 7 },
233
+ async (model) => {
234
+ const started = Date.now()
235
+ const result = await synthesize(model, SYNTHESIS_TEXT)
236
+ assertAudio(t, 'MOSS clone', result)
237
+ assertBackend(t, 'MOSS clone', result.stats, useGPU)
238
+ recordMoss(t, 'moss clone', result, Date.now() - started)
239
+ },
240
+ useGPU
241
+ )
242
+ } finally {
243
+ fs.unlinkSync(referenceAudio)
244
+ }
245
+ validation.completed(`${useGPU ? 'GPU' : 'CPU'} cloning`)
228
246
  }
229
- }
230
- )
247
+ )
248
+ }
231
249
 
232
250
  test(
233
251
  'MOSS TTS: durationTokens steers the length of the synthesized speech',
@@ -238,9 +256,11 @@ test(
238
256
  async (model) => {
239
257
  const short = await synthesize(model, SYNTHESIS_TEXT)
240
258
  assertAudio(t, 'MOSS short target', short)
259
+ assertBackend(t, 'MOSS short target', short.stats, GPU_ONLY_USE_GPU)
241
260
  await model.reload({ durationTokens: LONG_DURATION_TOKENS })
242
261
  const long = await synthesize(model, SYNTHESIS_TEXT)
243
262
  assertAudio(t, 'MOSS long target', long)
263
+ assertBackend(t, 'MOSS long target', long.stats, GPU_ONLY_USE_GPU)
244
264
  t.ok(
245
265
  long.samples.length > short.samples.length,
246
266
  `a longer target yields longer speech (${short.samples.length} -> ${long.samples.length} samples)`
@@ -248,6 +268,7 @@ test(
248
268
  },
249
269
  GPU_ONLY_USE_GPU
250
270
  )
271
+ validation.completed('GPU duration/reload')
251
272
  }
252
273
  )
253
274
 
@@ -259,7 +280,6 @@ test(
259
280
  t.comment(`skipping: the TTSD backbone or the codec encoder is not in ${modelDir}`)
260
281
  return
261
282
  }
262
- const sourceAudio = resolveRefWavPath({})
263
283
  if (!fs.existsSync(sourceAudio)) {
264
284
  t.fail(`MOSS reference audio is missing: ${sourceAudio}`)
265
285
  return
@@ -276,6 +296,7 @@ test(
276
296
  const started = Date.now()
277
297
  const result = await synthesize(model, DIALOGUE_TEXT)
278
298
  assertAudio(t, 'MOSS dialogue', result)
299
+ assertBackend(t, 'MOSS dialogue', result.stats, GPU_ONLY_USE_GPU)
279
300
  recordMoss(t, 'moss dialogue', result, Date.now() - started)
280
301
  },
281
302
  GPU_ONLY_USE_GPU
@@ -283,5 +304,21 @@ test(
283
304
  } finally {
284
305
  removeFiles(references)
285
306
  }
307
+ validation.completed('GPU TTSD dialogue')
286
308
  }
287
309
  )
310
+
311
+ if (validation.enabled) {
312
+ test('MOSS TTS: full GPU validation completed every required case', (t) => {
313
+ validation.assertComplete(t, [
314
+ 'CPU batch',
315
+ 'CPU streaming',
316
+ 'GPU streaming',
317
+ 'GPU batch',
318
+ 'CPU cloning',
319
+ 'GPU cloning',
320
+ 'GPU duration/reload',
321
+ 'GPU TTSD dialogue'
322
+ ])
323
+ })
324
+ }
@@ -50,6 +50,14 @@ async function runLavasrEnhancerTest (options = {}) { // eslint-disable-line no-
50
50
  return runIntegrationModule('../integration/lavasr-enhancer.test.js', options)
51
51
  }
52
52
 
53
+ async function runMossFitTest (options = {}) { // eslint-disable-line no-unused-vars
54
+ return runIntegrationModule('../integration/moss-fit.test.js', options)
55
+ }
56
+
57
+ async function runMossSfxFitTest (options = {}) { // eslint-disable-line no-unused-vars
58
+ return runIntegrationModule('../integration/moss-sfx-fit.test.js', options)
59
+ }
60
+
53
61
  async function runMossSfxTest (options = {}) { // eslint-disable-line no-unused-vars
54
62
  return runIntegrationModule('../integration/moss-sfx.test.js', options)
55
63
  }
@@ -118,6 +126,8 @@ module.exports = {
118
126
  runFitTest,
119
127
  runGpuSmokeTest,
120
128
  runLavasrEnhancerTest,
129
+ runMossFitTest,
130
+ runMossSfxFitTest,
121
131
  runMossSfxTest,
122
132
  runMossSpeechTest,
123
133
  runMossTest,
@@ -33,7 +33,9 @@
33
33
  "runMultipleRunsTest",
34
34
  "runGpuSmokeTest",
35
35
  "runOutputSampleRateTest",
36
- "runVoiceControlsTest"
36
+ "runVoiceControlsTest",
37
+ "runMossFitTest",
38
+ "runMossSfxFitTest"
37
39
  ]
38
40
  },
39
41
  "android": {
@@ -69,7 +71,9 @@
69
71
  "runMultipleRunsTest",
70
72
  "runGpuSmokeTest",
71
73
  "runOutputSampleRateTest",
72
- "runVoiceControlsTest"
74
+ "runVoiceControlsTest",
75
+ "runMossFitTest",
76
+ "runMossSfxFitTest"
73
77
  ]
74
78
  }
75
79
  }