@qvac/tts-ggml 0.10.0 → 0.11.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qvac/tts-ggml",
3
- "version": "0.10.0",
4
- "description": "Text to Speech (TTS) addon for qvac (ggml backend, wrapping the chatterbox + supertonic + parler + cosyvoice3 + audio8 + moss + Pocket engines from tts-cpp)",
3
+ "version": "0.11.0",
4
+ "description": "Text to Speech (TTS) addon for qvac (ggml backend, wrapping the chatterbox + supertonic + parler + cosyvoice3 + audio8 + moss + moss-sfx + moss-speech + Pocket engines from tts-cpp)",
5
5
  "addon": true,
6
6
  "engines": {
7
7
  "bare": ">=1.19.0"
@@ -53,8 +53,8 @@
53
53
  },
54
54
  "scripts": {
55
55
  "build": "npm run build:ts && npm run build:native",
56
- "build:native": "bare-make generate && bare-make build && bare-make install",
57
- "build:native:cuda": "bare-make generate -D ENABLE_CUDA=ON && bare-make build && bare-make install",
56
+ "build:native": "bare-make generate -D BUILD_FUZZING=OFF && bare-make build && bare-make install",
57
+ "build:native:cuda": "bare-make generate -D ENABLE_CUDA=ON -D BUILD_FUZZING=OFF && bare-make build && bare-make install",
58
58
  "build:cuda": "npm run build:ts && npm run build:native:cuda",
59
59
  "build:ts": "tsc -p tsconfig.build.json",
60
60
  "build:pack": "npm run build:ts && mkdir -p dist && npm pack --pack-destination dist",
@@ -80,8 +80,10 @@
80
80
  "example:parler-enhanced": "bare examples/parler-enhanced.js \"Hello from enhanced Parler.\"",
81
81
  "example:audio8": "bare examples/audio8-tts.js \"Audio eight speaks on-device.\"",
82
82
  "example:moss": "bare examples/moss-tts.js \"MOSS speaks on-device.\"",
83
+ "example:moss-sfx": "bare examples/moss-sound-effect.js \"Heavy rain falling on a tin roof with distant thunder.\" 5",
84
+ "example:moss-speech": "bare examples/moss-speech.js test/reference-audio/jfk.wav",
83
85
  "lint": "npm run lint:js && npm run lint:ts && npm run test:types",
84
- "lint:js": "prettier --check \"test/**/*.js\" \"scripts/__tests__/package-contract.test.js\" \"scripts/__tests__/binding-loader.test.js\" \"binding.js\" \"addon-unavailable.js\" && lunte \"test/**/*.js\" \"scripts/__tests__/package-contract.test.js\" \"scripts/__tests__/binding-loader.test.js\" \"binding.js\" \"addon-unavailable.js\"",
86
+ "lint:js": "prettier --check \"test/**/*.js\" \"scripts/run-cpp-fuzz.js\" \"scripts/__tests__/package-contract.test.js\" \"scripts/__tests__/binding-loader.test.js\" \"scripts/__tests__/run-cpp-fuzz.test.js\" \"binding.js\" \"addon-unavailable.js\" && lunte \"test/**/*.js\" \"scripts/run-cpp-fuzz.js\" \"scripts/__tests__/package-contract.test.js\" \"scripts/__tests__/binding-loader.test.js\" \"scripts/__tests__/run-cpp-fuzz.test.js\" \"binding.js\" \"addon-unavailable.js\"",
85
87
  "lint:ts": "eslint \"src/**/*.ts\" --max-warnings=0",
86
88
  "lint:ts:fix": "eslint \"src/**/*.ts\" --fix",
87
89
  "format": "prettier --write \"test/**/*.js\" \"scripts/__tests__/package-contract.test.js\" \"scripts/__tests__/binding-loader.test.js\" \"binding.js\" \"addon-unavailable.js\"",
@@ -89,15 +91,28 @@
89
91
  "test:node": "node --test scripts/__tests__/*.test.js",
90
92
  "test": "npm run test:types && npm run test:unit && npm run test:integration",
91
93
  "test:integration": "npm run build:ts && brittle-bare test/integration/**/*.test.js",
94
+ "test:integration:smoke": "npm run build:ts && brittle-bare test/integration/supertonic.test.js",
92
95
  "test:benchmark:rtf": "npm run build:ts && brittle-bare test/benchmark/rtf-benchmark.test.js",
93
96
  "test:benchmark:rtf:matrix": "node scripts/run-rtf-benchmark-matrix.js",
94
97
  "test:benchmark:streaming": "npm run build:ts && brittle-bare test/benchmark/streaming-benchmark.test.js",
95
98
  "test:mobile:generate": "node scripts/generate-mobile-integration-tests.js",
96
99
  "test:mobile:validate": "node scripts/validate-mobile-tests.js",
97
- "test:cpp:build": "bare-make generate -D BUILD_TESTING=ON && bare-make build --target tts_ggml_tests && bare-make install",
100
+ "test:cpp:build": "bare-make generate -D BUILD_TESTING=ON -D BUILD_FUZZING=OFF && bare-make build --target tts_ggml_tests && bare-make install",
98
101
  "test:cpp:run": "cd build && LLVM_PROFILE_FILE=default.profraw ./tts_ggml_tests --gtest_output=xml:cpp-test-results.xml",
99
102
  "test:cpp": "npm run test:cpp:build && npm run test:cpp:run",
100
- "coverage:cpp:build": "bare-make generate -D BUILD_TESTING=ON -D ENABLE_COVERAGE=ON && bare-make build --target tts_ggml_tests",
103
+ "fuzz:build": "bare-make generate -D BUILD_FUZZING=ON -D BUILD_TESTING=OFF -D FUZZTEST_FUZZING_MODE=OFF && bare-make build --target tts-config-parse-fuzz",
104
+ "fuzz:build:continuous": "bare-make generate -D BUILD_FUZZING=ON -D BUILD_TESTING=OFF -D FUZZTEST_FUZZING_MODE=ON && bare-make build --target tts-config-parse-fuzz",
105
+ "fuzz:run": "node scripts/run-cpp-fuzz.js",
106
+ "fuzz": "npm run fuzz:build && npm run fuzz:run",
107
+ "fuzz:continuous": "npm run fuzz:build:continuous && node scripts/run-cpp-fuzz.js --continuous",
108
+ "test:cpp:fuzz:configure": "bare-make generate -D BUILD_TESTING=ON -D BUILD_FUZZING=ON -D FUZZTEST_FUZZING_MODE=OFF",
109
+ "test:cpp:fuzz:build:tests": "bare-make build --target tts_ggml_tests",
110
+ "test:cpp:fuzz:build:fuzz": "bare-make build --target tts-config-parse-fuzz",
111
+ "test:cpp:fuzz:build": "npm run test:cpp:fuzz:configure && npm run test:cpp:fuzz:build:tests && npm run test:cpp:fuzz:build:fuzz",
112
+ "test:cpp:fuzz:run": "npm run test:scripts && npm run test:cpp:run && node scripts/run-cpp-fuzz.js",
113
+ "test:cpp:fuzz": "npm run test:cpp:fuzz:build && npm run test:cpp:fuzz:run",
114
+ "test:scripts": "node --test scripts/__tests__/run-cpp-fuzz.test.js",
115
+ "coverage:cpp:build": "bare-make generate -D BUILD_TESTING=ON -D ENABLE_COVERAGE=ON -D BUILD_FUZZING=OFF && bare-make build --target tts_ggml_tests",
101
116
  "coverage:cpp:summary": "cd build && llvm-cov report ./tts_ggml_tests --instr-profile=coverage.profdata -ignore-filename-regex='(tests|build|node_modules|gtest|gmock|\\.vcpkg|/usr)/' > coverage-summary.txt",
102
117
  "coverage:cpp:report": "cd build && ls -lha && llvm-profdata merge -sparse default.profraw -o coverage.profdata && llvm-cov show ./tts_ggml_tests -instr-profile=coverage.profdata -format=html -output-dir=coverage-html -ignore-filename-regex='(tests|build|node_modules|gtest|gmock|\\.vcpkg|/usr)/' && llvm-cov export ./tts_ggml_tests -instr-profile=coverage.profdata -format=lcov -ignore-filename-regex='(tests|build|node_modules|gtest|gmock|\\.vcpkg|/usr)/' > lcov.info && npm run coverage:cpp:summary",
103
118
  "coverage:cpp": "npm run coverage:cpp:build && npm run test:cpp:run && npm run coverage:cpp:report",
@@ -107,6 +122,7 @@
107
122
  "convert-models": "bash scripts/convert-models.sh",
108
123
  "setup-models": "bash scripts/setup-venv.sh && bash scripts/convert-models.sh",
109
124
  "download-models:registry": "node scripts/download-tts-ggml-models.js",
125
+ "download-models:smoke": "node scripts/download-tts-ggml-models.js --group supertonic",
110
126
  "example:pocket": "bare examples/pocket-tts.js",
111
127
  "test:pocket": "npm run build:ts && brittle-bare test/integration/pocket.integration.test.js"
112
128
  },
@@ -125,6 +141,7 @@
125
141
  "scripts/download-tts-ggml-models.js",
126
142
  "test/integration",
127
143
  "test/mobile",
144
+ "test/pocket-worklet",
128
145
  "test/reference-audio",
129
146
  "test/utils/runTTS.js",
130
147
  "test/utils/runChatterboxTTS.js",
@@ -195,7 +212,9 @@
195
212
  "cmake-npm": "^1.1.0",
196
213
  "bare-url": "^2.1.6",
197
214
  "bare-https": "^3.0.0",
198
- "bare-stream": "^2.7.0"
215
+ "bare-stream": "^2.7.0",
216
+ "bare-subprocess": "^6.2.1",
217
+ "brittle": "^3.17.0"
199
218
  },
200
219
  "dependencies": {
201
220
  "@qvac/error": "^0.1.0",
@@ -208,16 +227,14 @@
208
227
  "bare-path": "^3.0.0",
209
228
  "bare-process": "^4.2.2",
210
229
  "bare-stream": "^2.7.0",
211
- "bare-subprocess": "^5.2.1",
212
- "bare-url": "^2.4.3",
213
- "brittle": "^3.17.0"
230
+ "bare-url": "^2.4.3"
214
231
  },
215
232
  "types": "index.d.ts",
216
233
  "optionalDependencies": {
217
- "@qvac/tts-ggml-linux-x64": "0.10.0",
218
- "@qvac/tts-ggml-linux-arm64": "0.10.0",
219
- "@qvac/tts-ggml-darwin-arm64": "0.10.0",
220
- "@qvac/tts-ggml-darwin-x64": "0.10.0",
221
- "@qvac/tts-ggml-win32-x64": "0.10.0"
234
+ "@qvac/tts-ggml-linux-x64": "0.11.0",
235
+ "@qvac/tts-ggml-linux-arm64": "0.11.0",
236
+ "@qvac/tts-ggml-darwin-arm64": "0.11.0",
237
+ "@qvac/tts-ggml-darwin-x64": "0.11.0",
238
+ "@qvac/tts-ggml-win32-x64": "0.11.0"
222
239
  }
223
240
  }
@@ -16,11 +16,11 @@ const { ensureChatterboxModels, ensureWhisperModel } = require('../utils/downloa
16
16
  const { loadWhisper, runWhisper } = require('../utils/runWhisper')
17
17
  const { recordTtsStats } = require('../utils/perf-helper')
18
18
  const { TTS_TEST_THREADS } = require('../utils/testThreads')
19
+ const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
19
20
 
20
21
  const platform = os.platform()
21
22
  const isMobile = platform === 'ios' || platform === 'android'
22
23
  const isDarwin = platform === 'darwin'
23
- const forceNoGpu = os.getEnv('NO_GPU') === 'true'
24
24
 
25
25
  const INPUT_SENTENCES = (isMobile ? 'short' : os.getEnv('INPUT_SENTENCES')) || 'short'
26
26
  const useSplit = INPUT_SENTENCES !== 'short'
@@ -42,7 +42,7 @@ function getEnglishSentences() {
42
42
 
43
43
  test(
44
44
  'Chatterbox TTS (ggml): English synthesis + optional WER verification',
45
- { timeout: 1800000 },
45
+ { timeout: 1800000, skip: NO_GPU },
46
46
  async (t) => {
47
47
  const baseDir = getBaseDir()
48
48
  const modelsDir = path.join(baseDir, 'models')
@@ -86,7 +86,8 @@ test(
86
86
  const model = await loadChatterboxTTS({
87
87
  threads: TTS_TEST_THREADS,
88
88
  modelDir: resolvedModelDir,
89
- language: 'en'
89
+ language: 'en',
90
+ useGPU: GPU_ONLY_USE_GPU
90
91
  })
91
92
  t.ok(model, 'Chatterbox (ggml) model should be loaded')
92
93
 
@@ -254,7 +255,7 @@ test(
254
255
  const model = new TTSGgml({
255
256
  threads: TTS_TEST_THREADS,
256
257
  files: { modelDir: download.targetDir },
257
- config: { language: 'en', ...(forceNoGpu ? { useGPU: false } : {}) },
258
+ config: { language: 'en', ...(NO_GPU ? { useGPU: false } : {}) },
258
259
  opts: { stats: true }
259
260
  })
260
261
 
@@ -315,7 +316,7 @@ test(
315
316
  threads: TTS_TEST_THREADS,
316
317
  files: { modelDir: download.targetDir },
317
318
  referenceAudio: resolveRefWavPath({}),
318
- config: { language: 'en', outputSampleRate: 16000, ...(forceNoGpu ? { useGPU: false } : {}) },
319
+ config: { language: 'en', outputSampleRate: 16000, ...(NO_GPU ? { useGPU: false } : {}) },
319
320
  opts: { stats: true }
320
321
  })
321
322
  await model.load()
@@ -344,7 +345,7 @@ test(
344
345
 
345
346
  test(
346
347
  'Chatterbox TTS (ggml): native C++ chunk streaming via streamChunkTokens',
347
- { timeout: 600000 },
348
+ { timeout: 600000, skip: NO_GPU },
348
349
  async (t) => {
349
350
  const baseDir = getBaseDir()
350
351
  const modelsDir = path.join(baseDir, 'models')
@@ -371,7 +372,7 @@ test(
371
372
  streamChunkTokens: 25,
372
373
  streamFirstChunkTokens: 10,
373
374
  cfmSteps: 1,
374
- config: { language: 'en', ...(forceNoGpu ? { useGPU: false } : {}) },
375
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
375
376
  opts: { stats: true }
376
377
  })
377
378
  await model.load()
@@ -26,13 +26,15 @@ const {
26
26
  } = require('../utils/downloadModel')
27
27
  const { recordTtsStats } = require('../utils/perf-helper')
28
28
  const { TTS_TEST_THREADS } = require('../utils/testThreads')
29
+ const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
29
30
 
30
31
  const platform = os.platform()
31
32
  const isMobile = platform === 'ios' || platform === 'android'
32
33
 
33
- // Language coverage test, not a GPU policy test: rely on the package
34
- // default (`useGPU: false`) rather than opting into GPU here.
35
- // Tests that *are* about GPU live in gpu-smoke.test.js.
34
+ // Language coverage, not a GPU policy test. A single MTL synthesis already
35
+ // takes close to a minute on the CPU-only runners, so these tests run on the
36
+ // GPU only and are skipped there (NO_GPU=true). Tests that *are* about GPU live
37
+ // in gpu-smoke.test.js.
36
38
 
37
39
  function getBaseDir() {
38
40
  return isMobile && global.testDir ? global.testDir : '.'
@@ -83,7 +85,7 @@ async function loadChatterboxMtlTTS(params) {
83
85
 
84
86
  test(
85
87
  'Chatterbox MTL TTS (ggml): synthesizes across es/fr/de with shared engine',
86
- { timeout: 1800000 },
88
+ { timeout: 1800000, skip: NO_GPU },
87
89
  async (t) => {
88
90
  const baseDir = getBaseDir()
89
91
  const download = await ensureChatterboxMtlModels({ targetDir: path.join(baseDir, 'models') })
@@ -98,7 +100,8 @@ test(
98
100
  modelDir: download.targetDir,
99
101
  t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
100
102
  s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
101
- language: MTL_SENTENCES[0].lang
103
+ language: MTL_SENTENCES[0].lang,
104
+ useGPU: GPU_ONLY_USE_GPU
102
105
  })
103
106
  try {
104
107
  for (let i = 0; i < MTL_SENTENCES.length; i++) {
@@ -166,7 +169,7 @@ test(
166
169
 
167
170
  test(
168
171
  'Chatterbox MTL TTS (ggml): synthesizes Japanese with MeCab dictionary',
169
- { timeout: 1800000 },
172
+ { timeout: 1800000, skip: NO_GPU },
170
173
  async (t) => {
171
174
  const baseDir = getBaseDir()
172
175
  const download = await ensureChatterboxMtlModels({ targetDir: path.join(baseDir, 'models') })
@@ -188,7 +191,8 @@ test(
188
191
  t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
189
192
  s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
190
193
  mecabDictDir: mecab.dir,
191
- language: 'ja'
194
+ language: 'ja',
195
+ useGPU: GPU_ONLY_USE_GPU
192
196
  })
193
197
  try {
194
198
  const t0 = Date.now()
@@ -233,7 +237,7 @@ test(
233
237
 
234
238
  test(
235
239
  'Chatterbox MTL TTS (ggml): synthesizes Chinese with Cangjie table',
236
- { timeout: 1800000 },
240
+ { timeout: 1800000, skip: NO_GPU },
237
241
  async (t) => {
238
242
  const baseDir = getBaseDir()
239
243
  const download = await ensureChatterboxMtlModels({ targetDir: path.join(baseDir, 'models') })
@@ -255,7 +259,8 @@ test(
255
259
  t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
256
260
  s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
257
261
  cangjieTsvPath: cangjie.path,
258
- language: 'zh'
262
+ language: 'zh',
263
+ useGPU: GPU_ONLY_USE_GPU
259
264
  })
260
265
  try {
261
266
  const t0 = Date.now()
@@ -12,6 +12,7 @@ const path = require('bare-path')
12
12
  const { loadChatterboxTTS, runChatterboxTTS } = require('../utils/runChatterboxTTS')
13
13
  const { ensureChatterboxModels } = require('../utils/downloadModel')
14
14
  const { TTS_TEST_THREADS } = require('../utils/testThreads')
15
+ const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
15
16
 
16
17
  function getBaseDir() {
17
18
  const platform = os.platform()
@@ -30,7 +31,7 @@ const EXPECTATION = {
30
31
 
31
32
  test(
32
33
  'Chatterbox TTS (ggml): speed config adjusts duration, preserves backward compat',
33
- { timeout: 1800000 },
34
+ { timeout: 1800000, skip: NO_GPU },
34
35
  async (t) => {
35
36
  const modelsDir = path.join(getBaseDir(), 'models')
36
37
 
@@ -52,7 +53,8 @@ test(
52
53
  threads: TTS_TEST_THREADS,
53
54
  modelDir,
54
55
  language: 'en',
55
- speed
56
+ speed,
57
+ useGPU: GPU_ONLY_USE_GPU
56
58
  })
57
59
  try {
58
60
  const r = await runChatterboxTTS(model, { text: TEXT }, EXPECTATION)
@@ -8,7 +8,6 @@
8
8
  const fs = require('bare-fs')
9
9
  const os = require('bare-os')
10
10
  const path = require('bare-path')
11
- const proc = require('bare-process')
12
11
  const test = require('brittle')
13
12
  const TTSGgml = require('@qvac/tts-ggml')
14
13
 
@@ -20,10 +19,11 @@ const {
20
19
  } = require('../utils/downloadModel')
21
20
  const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
22
21
  const { TTS_TEST_THREADS } = require('../utils/testThreads')
22
+ const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
23
23
 
24
24
  const platform = os.platform()
25
25
  const isMobile = platform === 'ios' || platform === 'android'
26
- const useGPU = !isMobile && proc.env.NO_GPU === 'false'
26
+ const useGPU = GPU_ONLY_USE_GPU
27
27
 
28
28
  function getBaseDir() {
29
29
  return isMobile && global.testDir ? global.testDir : '.'
@@ -99,7 +99,7 @@ async function synthOnce(t, loadParams, label) {
99
99
 
100
100
  test(
101
101
  'CosyVoice3 cloning (ggml): baked, zero-shot and cross-lingual all differ at one seed',
102
- { timeout: 1800000 },
102
+ { timeout: 1800000, skip: NO_GPU },
103
103
  async (t) => {
104
104
  const modelDir = await ensureCloneReadyModelDir(t)
105
105
  if (!modelDir) return
@@ -7,7 +7,9 @@
7
7
  // CosyVoice3's ~2.4 GB model dir, while this one is listed on the dedicated
8
8
  // 'cosyvoice' row that pre-stages it (see test/mobile/test-groups.json).
9
9
  //
10
- // Text is kept short to bound CPU LM-decode time in CI, as in cosyvoice3.test.js.
10
+ // As in cosyvoice3.test.js, the model-backed tests run on the GPU only and are
11
+ // skipped on CPU-only runners (NO_GPU=true): even one short CosyVoice3 CPU
12
+ // synthesis costs minutes there.
11
13
  //
12
14
  // Stage the enhancer GGUF via scripts/convert-lavasr-enhancer-to-gguf.py (from
13
15
  // the public LavaSRcpp ONNX release) into models/lavasr/lavasr-enhancer.gguf,
@@ -25,6 +27,7 @@ const {
25
27
  ensureCosyvoiceModel
26
28
  } = require('../utils/downloadModel')
27
29
  const { TTS_TEST_THREADS } = require('../utils/testThreads')
30
+ const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
28
31
 
29
32
  const platform = os.platform()
30
33
  const isMobile = platform === 'ios' || platform === 'android'
@@ -118,7 +121,7 @@ async function stageCosyvoice(t, baseDir) {
118
121
 
119
122
  test(
120
123
  'CosyVoice3 + LavaSR enhancer (batch) reports 48 kHz enhanced output',
121
- { timeout: 900000, skip: SKIP_COSYVOICE },
124
+ { timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
122
125
  async (t) => {
123
126
  const baseDir = getBaseDir()
124
127
  const enh = await stageEnhancer(t, baseDir)
@@ -130,7 +133,7 @@ test(
130
133
  threads: TTS_TEST_THREADS,
131
134
  engine: TTSGgml.ENGINE_COSYVOICE3,
132
135
  files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
133
- config: { language: 'en', useGPU: false },
136
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
134
137
  opts: { stats: true }
135
138
  })
136
139
  await model.load()
@@ -141,8 +144,8 @@ test(
141
144
  t.ok(r.stats, 'runtimeStats returned (constructed with stats:true)')
142
145
  t.is(
143
146
  r.stats.enhancerBackendDevice,
144
- 0,
145
- 'enhancer loaded and ran on CPU (enhancerBackendDevice=0, not -1 = never loaded)'
147
+ r.stats.backendDevice,
148
+ 'enhancer loaded and ran on the engine device (not -1 = never loaded)'
146
149
  )
147
150
  } finally {
148
151
  try {
@@ -154,7 +157,7 @@ test(
154
157
 
155
158
  test(
156
159
  'CosyVoice3 + LavaSR enhancer + native chunk streaming emits 48 kHz chunks',
157
- { timeout: 900000, skip: SKIP_COSYVOICE },
160
+ { timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
158
161
  async (t) => {
159
162
  const baseDir = getBaseDir()
160
163
  const enh = await stageEnhancer(t, baseDir)
@@ -167,7 +170,7 @@ test(
167
170
  engine: TTSGgml.ENGINE_COSYVOICE3,
168
171
  files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
169
172
  streamChunkTokens: 25,
170
- config: { language: 'en', useGPU: false },
173
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
171
174
  opts: { stats: true }
172
175
  })
173
176
  await model.load()
@@ -191,7 +194,7 @@ test(
191
194
 
192
195
  test(
193
196
  'CosyVoice3 + enhancer + streaming resamples every chunk to outputSampleRate',
194
- { timeout: 900000, skip: SKIP_COSYVOICE },
197
+ { timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
195
198
  async (t) => {
196
199
  // A non-native rate while streaming is only legal because the enhancer's
197
200
  // overlap-reprocess window folds the resample in seam-free. Without this the
@@ -208,7 +211,7 @@ test(
208
211
  engine: TTSGgml.ENGINE_COSYVOICE3,
209
212
  files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
210
213
  streamChunkTokens: 25,
211
- config: { language: 'en', useGPU: false, outputSampleRate: 16000 },
214
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU, outputSampleRate: 16000 },
212
215
  opts: { stats: true }
213
216
  })
214
217
  await model.load()
@@ -228,7 +231,7 @@ test(
228
231
 
229
232
  test(
230
233
  'CosyVoice3 + LavaSR denoiser changes the emitted PCM',
231
- { timeout: 900000, skip: SKIP_COSYVOICE },
234
+ { timeout: 900000, skip: SKIP_COSYVOICE || NO_GPU },
232
235
  async (t) => {
233
236
  const baseDir = getBaseDir()
234
237
  const dl = await stageCosyvoice(t, baseDir)
@@ -247,7 +250,7 @@ test(
247
250
  threads: TTS_TEST_THREADS,
248
251
  engine: TTSGgml.ENGINE_COSYVOICE3,
249
252
  files: Object.assign({ cosyvoiceModelDir: dl.modelDir }, files),
250
- config: { language: 'en', useGPU: false, seed: SEED },
253
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU, seed: SEED },
251
254
  opts: { stats: true }
252
255
  })
253
256
  await model.load()
@@ -294,6 +297,7 @@ test(
294
297
  if (!dl) return
295
298
 
296
299
  const badEnhancer = path.join(baseDir, 'models', 'lavasr', 'invalid-enhancer.gguf')
300
+ fs.mkdirSync(path.dirname(badEnhancer), { recursive: true })
297
301
  fs.writeFileSync(badEnhancer, 'not a gguf')
298
302
 
299
303
  const model = new TTSGgml({
@@ -3,8 +3,10 @@
3
3
  // CosyVoice3 engine integration smoke: real synthesis in the desktop
4
4
  // `run-integration-tests` lane. Mirrors parler.test.js (download model → build
5
5
  // TTSGgml → run → assert) but for the directory-consuming CosyVoice3 engine
6
- // (Qwen2 speech LM + DiT flow + CausalHiFT vocoder; native 24 kHz, CPU-only).
7
- // Text is kept SHORT to bound CPU LM-decode time in CI.
6
+ // (Qwen2 speech LM + DiT flow + CausalHiFT vocoder; native 24 kHz).
7
+ // Even one short CPU synthesis costs minutes on the CPU-only runners, so these
8
+ // tests run on the GPU only and are skipped there (NO_GPU=true); the CPU crash
9
+ // check is the CosyVoice3 CPU smoke in gpu-smoke.test.js.
8
10
 
9
11
  const os = require('bare-os')
10
12
  const path = require('bare-path')
@@ -13,6 +15,7 @@ const test = require('brittle')
13
15
  const { loadCosyvoiceTTS, runCosyvoiceTTS } = require('../utils/runCosyvoiceTTS')
14
16
  const { ensureCosyvoiceModel } = require('../utils/downloadModel')
15
17
  const { TTS_TEST_THREADS } = require('../utils/testThreads')
18
+ const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
16
19
 
17
20
  const platform = os.platform()
18
21
  const isMobile = platform === 'ios' || platform === 'android'
@@ -27,7 +30,7 @@ const MODEL_MISSING =
27
30
 
28
31
  test(
29
32
  'CosyVoice3 TTS (ggml): outputSampleRate=16000 resamples and reports 16 kHz',
30
- { timeout: 600000 },
33
+ { timeout: 600000, skip: NO_GPU },
31
34
  async (t) => {
32
35
  const baseDir = getBaseDir()
33
36
  const download = await ensureCosyvoiceModel({
@@ -43,7 +46,8 @@ test(
43
46
  const model = await loadCosyvoiceTTS({
44
47
  threads: TTS_TEST_THREADS,
45
48
  cosyvoiceModelDir: download.modelDir,
46
- outputSampleRate: 16000
49
+ outputSampleRate: 16000,
50
+ useGPU: GPU_ONLY_USE_GPU
47
51
  })
48
52
  try {
49
53
  const text = 'Hello from CosyVoice.'
@@ -70,7 +74,7 @@ function samplesEqual(a, b) {
70
74
 
71
75
  test(
72
76
  'CosyVoice3 TTS (ggml): emotion changes the audio; per-call switch needs no reload',
73
- { timeout: 900000 },
77
+ { timeout: 900000, skip: NO_GPU },
74
78
  async (t) => {
75
79
  const baseDir = getBaseDir()
76
80
  const download = await ensureCosyvoiceModel({
@@ -87,7 +91,8 @@ test(
87
91
  const model = await loadCosyvoiceTTS({
88
92
  threads: TTS_TEST_THREADS,
89
93
  cosyvoiceModelDir: download.modelDir,
90
- seed: 42
94
+ seed: 42,
95
+ useGPU: GPU_ONLY_USE_GPU
91
96
  })
92
97
  try {
93
98
  const text = 'Hello from CosyVoice.'
@@ -172,7 +177,7 @@ test(
172
177
 
173
178
  test(
174
179
  'CosyVoice3 TTS (ggml): instruct conditioning produces audio',
175
- { timeout: 600000 },
180
+ { timeout: 600000, skip: NO_GPU },
176
181
  async (t) => {
177
182
  const baseDir = getBaseDir()
178
183
  const download = await ensureCosyvoiceModel({
@@ -186,7 +191,8 @@ test(
186
191
  const model = await loadCosyvoiceTTS({
187
192
  threads: TTS_TEST_THREADS,
188
193
  cosyvoiceModelDir: download.modelDir,
189
- emotion: 'happy'
194
+ emotion: 'happy',
195
+ useGPU: GPU_ONLY_USE_GPU
190
196
  })
191
197
  try {
192
198
  const text = 'Hello from CosyVoice.'
@@ -141,14 +141,18 @@ 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('a voice with no fitter is an outcome, not a throw', (t) => {
144
+ test('MOSS routes to its fitter when a model cannot be read', (t) => {
145
145
  const fit = TTSGgml.assessFit({
146
146
  engineType: 'moss',
147
- mossBackbonePath: '/models/moss-tts-delay.gguf'
147
+ mossBackbonePath: '/models/moss-tts-delay.gguf',
148
+ mossCodecDecoderPath: '/models/moss-codec-decoder.gguf',
149
+ promptRows: 128,
150
+ referenceSamples: 0,
151
+ streaming: false
148
152
  })
149
153
 
150
154
  t.is(fit.status, 'error')
151
- t.is(fit.reason, 'unsupported-engine')
155
+ t.is(fit.reason, 'model-unreadable')
152
156
  t.is(fit.modelVariant, 'moss')
153
157
  })
154
158
 
@@ -33,6 +33,7 @@ const {
33
33
  } = require('../utils/downloadModel')
34
34
  const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
35
35
  const { TTS_TEST_THREADS } = require('../utils/testThreads')
36
+ const { GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
36
37
 
37
38
  const platform = os.platform()
38
39
  const isMobile = platform === 'ios' || platform === 'android'
@@ -632,7 +633,7 @@ test(
632
633
 
633
634
  test(
634
635
  'Chatterbox + LavaSR enhancer + native chunk streaming emits 48 kHz chunks',
635
- { timeout: 900000 },
636
+ { timeout: 900000, skip: NO_GPU },
636
637
  async (t) => {
637
638
  const baseDir = getBaseDir()
638
639
  const enh = await ensureLavaSREnhancerGguf({
@@ -662,7 +663,7 @@ test(
662
663
  },
663
664
  referenceAudio: resolveRefWavPath({}),
664
665
  streamChunkTokens: 25, // native chunk streaming + enhancer (the path)
665
- config: { language: 'en', useGPU: false },
666
+ config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
666
667
  opts: { stats: true }
667
668
  })
668
669
  await model.load()
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const path = require('bare-path')
4
+ const process = require('bare-process')
5
+ const test = require('brittle')
6
+ const TTSGgml = require('@qvac/tts-ggml')
7
+
8
+ test('MOSS fit requires explicit, representable workload counts', (t) => {
9
+ const request = {
10
+ engineType: 'moss',
11
+ mossBackbonePath: '/models/moss-tts-delay.gguf',
12
+ mossCodecDecoderPath: '/models/moss-codec-decoder.gguf',
13
+ promptRows: 128,
14
+ referenceSamples: 0,
15
+ streaming: false
16
+ }
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 }))
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 }))
27
+ })
28
+
29
+ test('MOSS metadata-only fit covers batch, streaming and cloning', (t) => {
30
+ const modelDir = process.env.QVAC_TEST_MOSS_MODEL_DIR
31
+ if (!modelDir) {
32
+ t.comment('Set QVAC_TEST_MOSS_MODEL_DIR to exercise real MOSS GGUFs')
33
+ return
34
+ }
35
+ const request = {
36
+ engineType: 'moss',
37
+ mossBackbonePath: path.join(modelDir, 'moss-tts-delay-f16.gguf'),
38
+ mossCodecDecoderPath: path.join(modelDir, 'moss-codec-decoder-f16.gguf'),
39
+ promptRows: 128,
40
+ referenceSamples: 0,
41
+ streaming: false
42
+ }
43
+ const batch = TTSGgml.assessFit(request)
44
+ t.not(batch.status, 'error', batch.report)
45
+ t.ok(batch.weightsBytes > 0)
46
+ t.ok(batch.lmComputeBytes > 0)
47
+ t.ok(batch.codecComputeBytes > 0)
48
+ const stream = TTSGgml.assessFit({ ...request, streaming: true })
49
+ t.not(stream.status, 'error', stream.report)
50
+ const cloning = TTSGgml.assessFit({
51
+ ...request,
52
+ mossCodecEncoderPath: path.join(modelDir, 'moss-codec-encoder-f16.gguf'),
53
+ referenceSamples: 24000
54
+ })
55
+ t.not(cloning.status, 'error', cloning.report)
56
+ t.ok(cloning.weightsBytes > batch.weightsBytes)
57
+ })