@qvac/tts-ggml 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +317 -33
  2. package/docs/pocket-tts.md +201 -0
  3. package/examples/moss-dialogue-tts.js +113 -0
  4. package/examples/moss-tts.js +154 -0
  5. package/examples/pocket-tts.js +51 -0
  6. package/examples/supertonic-chunk-stream-tts.js +156 -0
  7. package/examples/supertonic-sentence-stream-tts.js +4 -4
  8. package/index.d.ts +352 -29
  9. package/index.js +698 -52
  10. package/lib/fit.d.ts +127 -0
  11. package/lib/fit.js +29 -0
  12. package/lib/pocketConfig.d.ts +10 -0
  13. package/lib/pocketConfig.js +145 -0
  14. package/package.json +14 -9
  15. package/test/integration/addon.test.js +5 -0
  16. package/test/integration/audio8.test.js +7 -0
  17. package/test/integration/chatterbox-mtl.test.js +6 -0
  18. package/test/integration/chatterbox-speed.test.js +13 -2
  19. package/test/integration/cosyvoice3-clone.test.js +16 -1
  20. package/test/integration/cosyvoice3-lavasr.test.js +6 -0
  21. package/test/integration/cosyvoice3.test.js +19 -0
  22. package/test/integration/fit.test.js +174 -0
  23. package/test/integration/gpu-smoke.test.js +13 -0
  24. package/test/integration/lavasr-enhancer.test.js +104 -3
  25. package/test/integration/moss.test.js +282 -0
  26. package/test/integration/multiple-runs.test.js +18 -2
  27. package/test/integration/output-sample-rate.test.js +6 -0
  28. package/test/integration/parler-wer.test.js +7 -1
  29. package/test/integration/parler.test.js +27 -3
  30. package/test/integration/pocket.integration.test.js +156 -0
  31. package/test/integration/supertonic-mtl.test.js +2 -0
  32. package/test/integration/supertonic.test.js +70 -0
  33. package/test/integration/supertonic3-quant.test.js +2 -0
  34. package/test/integration/voice-controls.test.js +56 -0
  35. package/test/mobile/integration.auto.cjs +16 -1
  36. package/test/mobile/pocket-worklet-host.cpp +92 -0
  37. package/test/mobile/pocket-worklet.cjs +47 -0
  38. package/test/mobile/test-groups.json +8 -4
  39. package/test/utils/runCosyvoiceTTS.js +1 -0
  40. package/test/utils/runTTS.js +6 -11
  41. package/tts.d.ts +13 -2
  42. package/tts.js +3 -2
@@ -14,6 +14,7 @@ const TTSGgml = require('@qvac/tts-ggml')
14
14
  const { runSupertonicTTS } = require('../utils/runSupertonicTTS')
15
15
  const { ensureSupertonicMtlModel } = require('../utils/downloadModel')
16
16
  const { recordTtsStats } = require('../utils/perf-helper')
17
+ const { TTS_TEST_THREADS } = require('../utils/testThreads')
17
18
 
18
19
  const platform = os.platform()
19
20
  const isMobile = platform === 'ios' || platform === 'android'
@@ -31,6 +32,7 @@ const MTL_SENTENCES = [
31
32
 
32
33
  async function loadSupertonicMtlTTS(params) {
33
34
  const model = new TTSGgml({
35
+ threads: TTS_TEST_THREADS,
34
36
  engine: TTSGgml.ENGINE_SUPERTONIC,
35
37
  files: { supertonicModel: params.supertonicModelPath },
36
38
  voice: params.voice || 'F1',
@@ -12,6 +12,7 @@ const test = require('brittle')
12
12
  const { loadSupertonicTTS, runSupertonicTTS } = require('../utils/runSupertonicTTS')
13
13
  const { ensureSupertonicModel } = require('../utils/downloadModel')
14
14
  const { recordTtsStats } = require('../utils/perf-helper')
15
+ const { TTS_TEST_THREADS } = require('../utils/testThreads')
15
16
 
16
17
  const platform = os.platform()
17
18
  const isMobile = platform === 'ios' || platform === 'android'
@@ -46,6 +47,7 @@ test(
46
47
  }
47
48
 
48
49
  const model = await loadSupertonicTTS({
50
+ threads: TTS_TEST_THREADS,
49
51
  supertonicModelPath: download.path,
50
52
  voice: 'F1',
51
53
  language: 'en',
@@ -140,6 +142,7 @@ test(
140
142
 
141
143
  const TTSGgml = require('@qvac/tts-ggml')
142
144
  const model = new TTSGgml({
145
+ threads: TTS_TEST_THREADS,
143
146
  engine: TTSGgml.ENGINE_SUPERTONIC,
144
147
  files: { supertonicModel: download.path },
145
148
  voice: 'F1',
@@ -196,6 +199,71 @@ test(
196
199
  }
197
200
  )
198
201
 
202
+ test(
203
+ 'Supertonic TTS (ggml): native chunk streaming (streamChunkTokens) emits engine chunks',
204
+ { timeout: 600000 },
205
+ async (t) => {
206
+ const baseDir = getBaseDir()
207
+ const download = await ensureSupertonicModel({ targetDir: path.join(baseDir, 'models') })
208
+ if (!download.success) {
209
+ t.fail(
210
+ 'Supertonic GGUF not available - registry fetch failed. Run `npm run download-models:registry` or stage models locally.'
211
+ )
212
+ return
213
+ }
214
+
215
+ const TTSGgml = require('@qvac/tts-ggml')
216
+ const model = new TTSGgml({
217
+ threads: TTS_TEST_THREADS,
218
+ engine: TTSGgml.ENGINE_SUPERTONIC,
219
+ files: { supertonicModel: download.path },
220
+ voice: 'F1',
221
+ streamChunkTokens: 40,
222
+ streamFirstChunkTokens: 30,
223
+ config: { language: 'en', useGPU: false },
224
+ opts: { stats: true }
225
+ })
226
+ await model.load()
227
+ try {
228
+ const text =
229
+ 'Native streaming splits one request inside the engine. Each chunk is ' +
230
+ 'synthesized and emitted as soon as it is ready, so playback can start ' +
231
+ 'before the whole paragraph has been rendered.'
232
+ const response = await model.run({ type: 'text', input: text })
233
+ const chunkIndices = []
234
+ const isLastFlags = []
235
+ let totalSamples = 0
236
+ let lastSampleRate = null
237
+ await response
238
+ .onUpdate((d) => {
239
+ if (d && d.outputArray) {
240
+ chunkIndices.push(d.chunkIndex)
241
+ isLastFlags.push(!!d.isLast)
242
+ totalSamples += d.outputArray.length
243
+ if (d.sampleRate) lastSampleRate = d.sampleRate
244
+ }
245
+ })
246
+ .await()
247
+
248
+ t.ok(chunkIndices.length >= 2, `engine emitted multiple chunks (got ${chunkIndices.length})`)
249
+ for (let i = 0; i < chunkIndices.length; i++) {
250
+ t.is(chunkIndices[i], i, `chunk ${i} carries chunkIndex=${i}`)
251
+ }
252
+ t.is(isLastFlags.filter(Boolean).length, 1, 'exactly one isLast=true emitted')
253
+ t.is(isLastFlags[isLastFlags.length - 1], true, 'final chunk carries isLast=true')
254
+ t.is(lastSampleRate, 44100, 'native streaming chunks report 44.1 kHz')
255
+ t.ok(totalSamples > 0, 'stream produced audio samples')
256
+ if (response.stats) {
257
+ t.is(response.stats.totalSamples, totalSamples, 'stats count exactly the streamed samples')
258
+ }
259
+ } finally {
260
+ try {
261
+ await model.unload()
262
+ } catch (_e) {}
263
+ }
264
+ }
265
+ )
266
+
199
267
  test(
200
268
  'Supertonic TTS (ggml): runStreaming with async iterator emits one job per yielded sentence',
201
269
  { timeout: 600000 },
@@ -211,6 +279,7 @@ test(
211
279
 
212
280
  const TTSGgml = require('@qvac/tts-ggml')
213
281
  const model = new TTSGgml({
282
+ threads: TTS_TEST_THREADS,
214
283
  engine: TTSGgml.ENGINE_SUPERTONIC,
215
284
  files: { supertonicModel: download.path },
216
285
  voice: 'F1',
@@ -265,6 +334,7 @@ test(
265
334
  }
266
335
 
267
336
  const model = await loadSupertonicTTS({
337
+ threads: TTS_TEST_THREADS,
268
338
  supertonicModelPath: download.path,
269
339
  voice: 'F1',
270
340
  steps: 4,
@@ -24,6 +24,7 @@ const test = require('brittle')
24
24
  const TTSGgml = require('@qvac/tts-ggml')
25
25
  const { runSupertonicTTS } = require('../utils/runSupertonicTTS')
26
26
  const { ensureSupertonic3Model } = require('../utils/downloadModel')
27
+ const { TTS_TEST_THREADS } = require('../utils/testThreads')
27
28
 
28
29
  const platform = os.platform()
29
30
  const isMobile = platform === 'ios' || platform === 'android'
@@ -50,6 +51,7 @@ const SENTENCES = [
50
51
 
51
52
  async function loadSupertonic3TTS(params) {
52
53
  const model = new TTSGgml({
54
+ threads: TTS_TEST_THREADS,
53
55
  engine: TTSGgml.ENGINE_SUPERTONIC,
54
56
  files: { supertonicModel: params.supertonicModelPath },
55
57
  voice: params.voice || 'F1',
@@ -0,0 +1,56 @@
1
+ 'use strict'
2
+
3
+ // Model-free: tts-cpp's conditioning vocabulary, read through the native
4
+ // binding by TTSGgml.getVoiceControls(), must agree with what the JS layer
5
+ // accepts per engine, so the JS validation tables cannot drift from the
6
+ // library they front.
7
+
8
+ const test = require('brittle')
9
+ const TTSGgml = require('../../index.js')
10
+ const { TTS_TEST_THREADS } = require('../utils/testThreads')
11
+
12
+ const ENGINE_FILES = {
13
+ [TTSGgml.ENGINE_CHATTERBOX]: { t3Model: './t3.gguf', s3genModel: './s3gen.gguf' },
14
+ [TTSGgml.ENGINE_SUPERTONIC]: { supertonicModel: './supertonic.gguf' },
15
+ [TTSGgml.ENGINE_COSYVOICE3]: { cosyvoiceModelDir: './cosyvoice3' },
16
+ [TTSGgml.ENGINE_PARLER]: { parlerModel: './parler.gguf' },
17
+ [TTSGgml.ENGINE_AUDIO8]: { audio8Lm: './lm.gguf', audio8CodecDecoder: './decoder.gguf' }
18
+ }
19
+
20
+ function acceptedBy(engine, key, values) {
21
+ return values.filter((value) => {
22
+ try {
23
+ const model = new TTSGgml({
24
+ threads: TTS_TEST_THREADS,
25
+ engine,
26
+ files: ENGINE_FILES[engine],
27
+ [key]: value
28
+ })
29
+ return model.getEngineType() === engine
30
+ } catch (_err) {
31
+ return false
32
+ }
33
+ })
34
+ }
35
+
36
+ test('getVoiceControls: native vocabulary matches the JS validation tables', (t) => {
37
+ const catalog = TTSGgml.getVoiceControls()
38
+ t.ok(catalog.emotions.length > 0, 'canonical emotions are listed')
39
+ t.ok(catalog.paces.includes('moderate'), 'canonical paces include moderate')
40
+
41
+ for (const engine of Object.keys(ENGINE_FILES)) {
42
+ const native = catalog.engines[engine]
43
+ t.ok(native, `${engine} is in the native catalog`)
44
+ if (!native) continue
45
+ t.alike(
46
+ acceptedBy(engine, 'emotion', catalog.emotions),
47
+ native.emotions,
48
+ `${engine}: the emotions the JS layer accepts are the native subset`
49
+ )
50
+ t.alike(
51
+ acceptedBy(engine, 'pace', catalog.paces),
52
+ native.paces,
53
+ `${engine}: the paces the JS layer accepts are the native subset`
54
+ )
55
+ }
56
+ })
@@ -38,6 +38,10 @@ async function runCosyvoice3Test (options = {}) { // eslint-disable-line no-unus
38
38
  return runIntegrationModule('../integration/cosyvoice3.test.js', options)
39
39
  }
40
40
 
41
+ async function runFitTest (options = {}) { // eslint-disable-line no-unused-vars
42
+ return runIntegrationModule('../integration/fit.test.js', options)
43
+ }
44
+
41
45
  async function runGpuSmokeTest (options = {}) { // eslint-disable-line no-unused-vars
42
46
  return runIntegrationModule('../integration/gpu-smoke.test.js', options)
43
47
  }
@@ -46,6 +50,10 @@ async function runLavasrEnhancerTest (options = {}) { // eslint-disable-line no-
46
50
  return runIntegrationModule('../integration/lavasr-enhancer.test.js', options)
47
51
  }
48
52
 
53
+ async function runMossTest (options = {}) { // eslint-disable-line no-unused-vars
54
+ return runIntegrationModule('../integration/moss.test.js', options)
55
+ }
56
+
49
57
  async function runMultipleRunsTest (options = {}) { // eslint-disable-line no-unused-vars
50
58
  return runIntegrationModule('../integration/multiple-runs.test.js', options)
51
59
  }
@@ -82,6 +90,10 @@ async function runSupertonic3QuantTest (options = {}) { // eslint-disable-line n
82
90
  return runIntegrationModule('../integration/supertonic3-quant.test.js', options)
83
91
  }
84
92
 
93
+ async function runVoiceControlsTest (options = {}) { // eslint-disable-line no-unused-vars
94
+ return runIntegrationModule('../integration/voice-controls.test.js', options)
95
+ }
96
+
85
97
  module.exports = {
86
98
  runAddonTest,
87
99
  runAudio8Test,
@@ -91,8 +103,10 @@ module.exports = {
91
103
  runCosyvoice3CloneTest,
92
104
  runCosyvoice3LavasrTest,
93
105
  runCosyvoice3Test,
106
+ runFitTest,
94
107
  runGpuSmokeTest,
95
108
  runLavasrEnhancerTest,
109
+ runMossTest,
96
110
  runMultipleRunsTest,
97
111
  runOutputSampleRateTest,
98
112
  runParlerWerTest,
@@ -101,5 +115,6 @@ module.exports = {
101
115
  runStreamingBenchmarkTest,
102
116
  runSupertonicMtlTest,
103
117
  runSupertonicTest,
104
- runSupertonic3QuantTest
118
+ runSupertonic3QuantTest,
119
+ runVoiceControlsTest
105
120
  }
@@ -0,0 +1,92 @@
1
+ // Standalone iOS Simulator host for the packaged Pocket Bare worklet test.
2
+ // Uses the same exported Bare Kit C API as react-native-bare-kit/shared/
3
+ // BareKitModule.cc. Keep IPC initialization after bare_worklet_start().
4
+ // This tests a worklet process, not a signed app or a physical device.
5
+ #include <chrono>
6
+ #include <cstdio>
7
+ #include <cstdlib>
8
+ #include <string>
9
+ #include <thread>
10
+
11
+ #include <dlfcn.h>
12
+ extern "C" {
13
+ struct bare_worklet_s;
14
+ using bare_worklet_t = bare_worklet_s;
15
+ struct bare_ipc_s;
16
+ using bare_ipc_t = bare_ipc_s;
17
+ struct uv_buf_t {
18
+ char* base;
19
+ size_t len;
20
+ };
21
+ struct bare_worklet_options_t {
22
+ size_t memory_limit;
23
+ const char* assets;
24
+ };
25
+ int bare_worklet_alloc(bare_worklet_t**);
26
+ int bare_worklet_init(bare_worklet_t*, const bare_worklet_options_t*);
27
+ int bare_worklet_start(
28
+ bare_worklet_t*, const char*, const uv_buf_t*, int, const char*[]);
29
+ int bare_worklet_terminate(bare_worklet_t*);
30
+ void bare_worklet_destroy(bare_worklet_t*);
31
+ int bare_ipc_alloc(bare_ipc_t**);
32
+ int bare_ipc_init(bare_ipc_t*, bare_worklet_t*);
33
+ int bare_ipc_read(bare_ipc_t*, void**, size_t*);
34
+ void bare_ipc_destroy(bare_ipc_t*);
35
+ }
36
+ void check(int code, const char* stage) {
37
+ if (code) {
38
+ std::fprintf(stderr, "%s: %d\n", stage, code);
39
+ std::exit(2);
40
+ }
41
+ }
42
+ int main(int argc, char** argv) {
43
+ if (argc < 2)
44
+ return 2;
45
+ // A broken native job must not leave an orphaned simulator process if its
46
+ // termination path hangs. Ordinary completion exits before this deadline.
47
+ std::thread([] {
48
+ std::this_thread::sleep_for(std::chrono::seconds(120));
49
+ std::fprintf(stderr, "Worklet host exceeded its hard deadline\n");
50
+ std::_Exit(124);
51
+ }).detach();
52
+ for (int i = 2; i < argc; ++i)
53
+ if (!dlopen(argv[i], RTLD_NOW | RTLD_GLOBAL)) {
54
+ std::fprintf(stderr, "%s\n", dlerror());
55
+ return 3;
56
+ }
57
+ bare_worklet_t* worklet;
58
+ bare_ipc_t* ipc;
59
+ check(bare_worklet_alloc(&worklet), "worklet alloc");
60
+ bare_worklet_options_t options{0, nullptr};
61
+ check(bare_worklet_init(worklet, &options), "worklet init");
62
+ check(
63
+ bare_worklet_start(worklet, argv[1], nullptr, 0, nullptr),
64
+ "worklet start");
65
+ check(bare_ipc_alloc(&ipc), "ipc alloc");
66
+ check(bare_ipc_init(ipc, worklet), "ipc init");
67
+ std::string received;
68
+ const auto deadline =
69
+ std::chrono::steady_clock::now() + std::chrono::seconds(90);
70
+ while (std::chrono::steady_clock::now() < deadline) {
71
+ void* data = nullptr;
72
+ size_t len = 0;
73
+ int result = bare_ipc_read(ipc, &data, &len);
74
+ if (result == 0)
75
+ received.append(static_cast<const char*>(data), len);
76
+ else if (result != -1) {
77
+ std::fprintf(stderr, "ipc read %d\n", result);
78
+ break;
79
+ }
80
+ if (received.find('\n') != std::string::npos)
81
+ break;
82
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
83
+ }
84
+ std::printf("WORKLET_RESULT %s\n", received.c_str());
85
+ std::fflush(stdout);
86
+ check(bare_worklet_terminate(worklet), "worklet terminate");
87
+ bare_ipc_destroy(ipc);
88
+ std::free(ipc);
89
+ bare_worklet_destroy(worklet);
90
+ std::free(worklet);
91
+ return received.find("\"passed\":true") != std::string::npos ? 0 : 1;
92
+ }
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ // Bundle with bare-pack --linked for the mobile target. The host provides
4
+ // QVAC_POCKET_MODEL_DIR and reads one JSON result from BareKit.IPC. Exporting
5
+ // the integration-test promise lets this worklet report completion without
6
+ // relying on beforeExit, since the host's IPC connection keeps it alive.
7
+ /* global BareKit, Bare */
8
+ let sent = false
9
+ let assertions = 0
10
+ let failed = false
11
+ const tap = []
12
+ const send = (result) => {
13
+ if (sent) return
14
+ sent = true
15
+ BareKit.IPC.write(
16
+ Buffer.from(JSON.stringify({ ...result, assertions, tap, versions: Bare.versions }) + '\n')
17
+ )
18
+ }
19
+ Bare.on('uncaughtException', (error) =>
20
+ send({ passed: false, error: String(error.stack || error) })
21
+ )
22
+ Bare.on('unhandledRejection', (error) =>
23
+ send({ passed: false, error: String(error.stack || error) })
24
+ )
25
+
26
+ const originalLog = console.log.bind(console)
27
+ console.log = (...args) => {
28
+ originalLog(...args)
29
+ tap.push(args.map(String).join(' '))
30
+ const line = String(args[0] || '')
31
+ if (/^\s+(?:not )?ok \d/.test(line)) assertions++
32
+ if (/^\s*not ok \d/.test(line)) failed = true
33
+ }
34
+
35
+ const process = require('bare-process')
36
+ global.process = process
37
+ if (!process.env.QVAC_POCKET_MODEL_DIR) {
38
+ send({
39
+ passed: false,
40
+ error: 'QVAC_POCKET_MODEL_DIR is required; mobile validation must not skip inference'
41
+ })
42
+ } else {
43
+ Promise.resolve(require('../integration/pocket.integration.test.js')).then(
44
+ () => send({ passed: !failed && assertions > 0 && !Bare.exitCode }),
45
+ (error) => send({ passed: false, error: String(error.stack || error) })
46
+ )
47
+ }
@@ -10,7 +10,8 @@
10
10
  "runChatterboxMtlTest"
11
11
  ],
12
12
  "supertonic": [
13
- "runSupertonicTest"
13
+ "runSupertonicTest",
14
+ "runFitTest"
14
15
  ],
15
16
  "supertonic-mtl": [
16
17
  "runSupertonicMtlTest"
@@ -31,7 +32,8 @@
31
32
  "cross-model-compatibility": [
32
33
  "runMultipleRunsTest",
33
34
  "runGpuSmokeTest",
34
- "runOutputSampleRateTest"
35
+ "runOutputSampleRateTest",
36
+ "runVoiceControlsTest"
35
37
  ]
36
38
  },
37
39
  "android": {
@@ -44,7 +46,8 @@
44
46
  "runChatterboxMtlTest"
45
47
  ],
46
48
  "supertonic": [
47
- "runSupertonicTest"
49
+ "runSupertonicTest",
50
+ "runFitTest"
48
51
  ],
49
52
  "supertonic-mtl": [
50
53
  "runSupertonicMtlTest"
@@ -65,7 +68,8 @@
65
68
  "cross-model-compatibility": [
66
69
  "runMultipleRunsTest",
67
70
  "runGpuSmokeTest",
68
- "runOutputSampleRateTest"
71
+ "runOutputSampleRateTest",
72
+ "runVoiceControlsTest"
69
73
  ]
70
74
  }
71
75
  }
@@ -54,6 +54,7 @@ function buildCosyvoiceLoadOptions(params = {}) {
54
54
  // the logits, so without it a before/after diff measures the sampler rather
55
55
  // than the conditioning.
56
56
  if (params.seed !== undefined) options.seed = params.seed
57
+ if (params.threads !== undefined) options.threads = params.threads
57
58
  return options
58
59
  }
59
60
 
@@ -235,25 +235,20 @@ async function runTTS(model, params, expectation = {}, options = {}) {
235
235
 
236
236
  const stats = response.stats || jobStats
237
237
 
238
+ // Every engine stat passes through, with only the three rates rounded for
239
+ // logging. An allowlist here used to drop fields the engine reported
240
+ // (gpuUnsupported, then the per-engine stage stats), so tests read what
241
+ // the addon emitted rather than a curated subset.
238
242
  const roundedStats = stats
239
243
  ? {
244
+ ...stats,
240
245
  totalTime: stats.totalTime ? Number(stats.totalTime.toFixed(4)) : stats.totalTime,
241
246
  tokensPerSecond: stats.tokensPerSecond
242
247
  ? Number(stats.tokensPerSecond.toFixed(2))
243
248
  : stats.tokensPerSecond,
244
249
  realTimeFactor: stats.realTimeFactor
245
250
  ? Number(stats.realTimeFactor.toFixed(5))
246
- : stats.realTimeFactor,
247
- audioDurationMs: stats.audioDurationMs,
248
- totalSamples: stats.totalSamples,
249
- backendDevice: stats.backendDevice,
250
- backendId: stats.backendId,
251
- // The allowlist above dropped gpuUnsupported, so gpu-smoke's
252
- // policy-CPU escape hatch (allowPolicyCpu && dev===0 &&
253
- // stats.gpuUnsupported) never saw the flag the engine set and the
254
- // Chatterbox/Mali fallback failed the strict assertion. Pass it
255
- // through (runSupertonicTTS already returns the raw stats object).
256
- gpuUnsupported: stats.gpuUnsupported
251
+ : stats.realTimeFactor
257
252
  }
258
253
  : null
259
254
 
package/tts.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export interface TTSConfigurationParams {
2
- [key: string]: string | number | boolean | undefined;
2
+ [key: string]: string | number | boolean | string[] | undefined;
3
3
  }
4
4
  export interface TTSJobData {
5
5
  type: string;
@@ -14,6 +14,7 @@ export interface TTSJobData {
14
14
  noise?: string;
15
15
  reverb?: string;
16
16
  quality?: string;
17
+ instruct?: string;
17
18
  referenceAudio?: string;
18
19
  referenceText?: string;
19
20
  }
@@ -23,10 +24,20 @@ export interface TTSWeightData {
23
24
  completed: boolean;
24
25
  }
25
26
  export type TTSOutputCallback = (addon: unknown, event: unknown, data: unknown, error: unknown) => void;
27
+ /** getVoiceControls() payload, keyed by tts-cpp's engine names. */
28
+ export interface NativeVoiceControls {
29
+ emotions: string[];
30
+ paces: string[];
31
+ engines: Record<string, {
32
+ emotions: string[];
33
+ paces: string[];
34
+ }>;
35
+ }
26
36
  export interface TTSBinding {
37
+ getVoiceControls(): NativeVoiceControls;
27
38
  createInstance(owner: TTSInterface, configuration: TTSConfigurationParams, outputCallback: TTSOutputCallback | null): object;
28
39
  activate(handle: object | null): Promise<void>;
29
- runJob(handle: object | null, data: TTSJobData): void;
40
+ runJob(handle: object | null, data: TTSJobData): boolean | void | Promise<boolean | void>;
30
41
  loadWeights(handle: object | null, weightsData: TTSWeightData): void;
31
42
  cancel(handle: object | null): Promise<void>;
32
43
  destroyInstance(handle: object): Promise<void> | void;
package/tts.js CHANGED
@@ -35,10 +35,11 @@ class TTSInterface {
35
35
  });
36
36
  }
37
37
  }
38
- // eslint-disable-next-line @typescript-eslint/require-await -- preserves the established promise-returning wrapper API.
39
38
  async runJob(data) {
40
39
  try {
41
- this._binding.runJob(this._handle, data);
40
+ if (await this._binding.runJob(this._handle, data) === false) {
41
+ throw new Error("Native addon rejected the job");
42
+ }
42
43
  }
43
44
  catch (error) {
44
45
  throw new error_1.QvacErrorAddonTTSGgml({