@qvac/tts-ggml 0.9.2 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -36
- package/docs/pocket-tts.md +201 -0
- package/examples/moss-dialogue-tts.js +113 -0
- package/examples/moss-tts.js +154 -0
- package/examples/pocket-tts.js +51 -0
- package/examples/supertonic-chunk-stream-tts.js +156 -0
- package/examples/supertonic-sentence-stream-tts.js +4 -4
- package/index.d.ts +352 -29
- package/index.js +698 -52
- package/lib/fit.d.ts +127 -0
- package/lib/fit.js +29 -0
- package/lib/pocketConfig.d.ts +10 -0
- package/lib/pocketConfig.js +145 -0
- package/package.json +16 -9
- package/test/integration/addon.test.js +13 -7
- package/test/integration/audio8.test.js +7 -0
- package/test/integration/chatterbox-mtl.test.js +20 -9
- package/test/integration/chatterbox-speed.test.js +16 -3
- package/test/integration/cosyvoice3-clone.test.js +17 -2
- package/test/integration/cosyvoice3-lavasr.test.js +21 -11
- package/test/integration/cosyvoice3.test.js +33 -8
- package/test/integration/fit.test.js +174 -0
- package/test/integration/gpu-smoke.test.js +13 -0
- package/test/integration/lavasr-enhancer.test.js +107 -5
- package/test/integration/moss.test.js +287 -0
- package/test/integration/multiple-runs.test.js +84 -57
- package/test/integration/output-sample-rate.test.js +10 -2
- package/test/integration/parler-wer.test.js +14 -9
- package/test/integration/parler.test.js +54 -24
- package/test/integration/pocket.integration.test.js +156 -0
- package/test/integration/supertonic-mtl.test.js +2 -0
- package/test/integration/supertonic.test.js +70 -0
- package/test/integration/supertonic3-quant.test.js +2 -0
- package/test/integration/voice-controls.test.js +56 -0
- package/test/mobile/integration.auto.cjs +16 -1
- package/test/mobile/pocket-worklet-host.cpp +92 -0
- package/test/mobile/pocket-worklet.cjs +47 -0
- package/test/mobile/test-groups.json +8 -4
- package/test/utils/runCosyvoiceTTS.js +1 -0
- package/test/utils/runTTS.js +6 -11
- package/tts.d.ts +13 -2
- package/tts.js +3 -2
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const test = require('brittle')
|
|
3
|
+
const TTSGgml = require('../../index')
|
|
4
|
+
const process = require('bare-process')
|
|
5
|
+
global.process = process
|
|
6
|
+
const bundle = process.env.QVAC_POCKET_MODEL_DIR
|
|
7
|
+
const text = 'Hello! We can generate speech with Fabric.'
|
|
8
|
+
|
|
9
|
+
// Opt in with QVAC_POCKET_MODEL_DIR pointing at the converted four-file bundle.
|
|
10
|
+
module.exports = test(
|
|
11
|
+
'Pocket native addon streams, cancels, recovers and resamples on reload',
|
|
12
|
+
{ skip: !bundle },
|
|
13
|
+
async (t) => {
|
|
14
|
+
const model = new TTSGgml({
|
|
15
|
+
engine: 'pocket',
|
|
16
|
+
files: { modelDir: bundle },
|
|
17
|
+
opts: { stats: true }
|
|
18
|
+
})
|
|
19
|
+
try {
|
|
20
|
+
await model.load()
|
|
21
|
+
const run = async (rate) => {
|
|
22
|
+
const response = await model.run({ input: text })
|
|
23
|
+
const pcm = []
|
|
24
|
+
const indices = []
|
|
25
|
+
let last = 0
|
|
26
|
+
for await (const chunk of response.iterate()) {
|
|
27
|
+
t.is(chunk.sampleRate, rate)
|
|
28
|
+
indices.push(chunk.chunkIndex)
|
|
29
|
+
pcm.push(...chunk.outputArray)
|
|
30
|
+
if (chunk.isLast) {
|
|
31
|
+
last++
|
|
32
|
+
t.is(chunk.outputArray.length, 0)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
t.is(last, 1)
|
|
36
|
+
t.alike(
|
|
37
|
+
indices,
|
|
38
|
+
indices.map((_, i) => i)
|
|
39
|
+
)
|
|
40
|
+
t.is(pcm.length, response.stats.totalSamples)
|
|
41
|
+
t.ok(pcm.some((x) => x !== 0))
|
|
42
|
+
t.ok(response.stats.firstAudioMs > 0)
|
|
43
|
+
return pcm
|
|
44
|
+
}
|
|
45
|
+
const original = await run(24000)
|
|
46
|
+
const sentenceStream = await model.runStream('Hello. Speech is ready.', {
|
|
47
|
+
maxChunkScalars: 16
|
|
48
|
+
})
|
|
49
|
+
await sentenceStream.onUpdate(() => {}).await()
|
|
50
|
+
t.ok(sentenceStream.stats.firstAudioMs > 0, 'runStream preserves native first-audio latency')
|
|
51
|
+
const textStream = await model.runStreaming(
|
|
52
|
+
(async function* () {
|
|
53
|
+
yield 'Hello.'
|
|
54
|
+
yield 'Speech is ready.'
|
|
55
|
+
})(),
|
|
56
|
+
{ accumulateSentences: false }
|
|
57
|
+
)
|
|
58
|
+
await textStream.onUpdate(() => {}).await()
|
|
59
|
+
t.ok(textStream.stats.firstAudioMs > 0, 'runStreaming preserves native first-audio latency')
|
|
60
|
+
|
|
61
|
+
// Check that omitted steps match the explicit upstream/native default
|
|
62
|
+
// through the complete addon path. PCM parity validates configuration,
|
|
63
|
+
// not subjective quality or the absence of model-generated artifacts.
|
|
64
|
+
const explicit = new TTSGgml({
|
|
65
|
+
engine: 'pocket',
|
|
66
|
+
files: { modelDir: bundle },
|
|
67
|
+
steps: 1
|
|
68
|
+
})
|
|
69
|
+
try {
|
|
70
|
+
await explicit.load()
|
|
71
|
+
const reference = await explicit.run({ input: text })
|
|
72
|
+
const pcm = []
|
|
73
|
+
for await (const chunk of reference.iterate()) pcm.push(...chunk.outputArray)
|
|
74
|
+
t.is(pcm.length, original.length, 'default and explicit one-step lengths match')
|
|
75
|
+
t.ok(
|
|
76
|
+
pcm.every((v, i) => Math.abs(v - original[i]) <= 4),
|
|
77
|
+
'default native PCM matches explicit one-step synthesis'
|
|
78
|
+
)
|
|
79
|
+
} finally {
|
|
80
|
+
await explicit.destroy()
|
|
81
|
+
}
|
|
82
|
+
if (process.env.QVAC_POCKET_AUDIO_OUTPUT) {
|
|
83
|
+
require('../utils/wav-helper').createWav(
|
|
84
|
+
new Int16Array(original),
|
|
85
|
+
24000,
|
|
86
|
+
process.env.QVAC_POCKET_AUDIO_OUTPUT
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
const response = await model.run({ input: text.repeat(5) })
|
|
90
|
+
let cancelPromise
|
|
91
|
+
const outcomePromise = response.await().then(
|
|
92
|
+
() => ({ completed: true }),
|
|
93
|
+
(error) => ({ error })
|
|
94
|
+
)
|
|
95
|
+
response.onUpdate((chunk) => {
|
|
96
|
+
if (!cancelPromise && chunk.outputArray.length) {
|
|
97
|
+
cancelPromise = response.cancel()
|
|
98
|
+
// Attach a rejection handler immediately; await the original promise
|
|
99
|
+
// below so cancellation failures still fail the integration test.
|
|
100
|
+
cancelPromise.catch(() => {})
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
const outcome = await outcomePromise
|
|
104
|
+
t.ok(cancelPromise, 'requested cancellation after first PCM')
|
|
105
|
+
if (cancelPromise) await cancelPromise
|
|
106
|
+
t.ok(
|
|
107
|
+
outcome.error && /cancel|abort/i.test(String(outcome.error)),
|
|
108
|
+
'cancelled synthesis rejects instead of completing'
|
|
109
|
+
)
|
|
110
|
+
const recovered = await run(24000)
|
|
111
|
+
t.is(recovered.length, original.length)
|
|
112
|
+
t.ok(
|
|
113
|
+
recovered.every((v, i) => Math.abs(v - original[i]) <= 4),
|
|
114
|
+
'deterministic PCM after cancellation'
|
|
115
|
+
)
|
|
116
|
+
await model.reload({ outputSampleRate: 44100 })
|
|
117
|
+
const resampled = await run(44100)
|
|
118
|
+
t.ok(Math.abs(resampled.length / 44100 - original.length / 24000) < 1 / 44100)
|
|
119
|
+
await t.exception(model.reload({ referenceAudio: `${bundle}/missing-reload-test.wav` }))
|
|
120
|
+
t.is(
|
|
121
|
+
model.getState().weightsLoaded,
|
|
122
|
+
false,
|
|
123
|
+
'failed native activation leaves the model unloaded'
|
|
124
|
+
)
|
|
125
|
+
await t.exception(model.run({ input: text }), /not loaded/)
|
|
126
|
+
await model.load()
|
|
127
|
+
const restored = await run(44100)
|
|
128
|
+
t.is(
|
|
129
|
+
restored.length,
|
|
130
|
+
resampled.length,
|
|
131
|
+
'load restores the last successful sample rate and voice'
|
|
132
|
+
)
|
|
133
|
+
t.ok(
|
|
134
|
+
restored.every((v, i) => Math.abs(v - resampled[i]) <= 4),
|
|
135
|
+
'PCM recovers after failed reload'
|
|
136
|
+
)
|
|
137
|
+
const empty = await model.run({ input: ' ' })
|
|
138
|
+
await t.exception(empty.await())
|
|
139
|
+
await run(44100)
|
|
140
|
+
// Verify the packaged native dependency includes the merged EOS-tail
|
|
141
|
+
// fix: the old token-only budget threw after emitting partial audio.
|
|
142
|
+
await model.reload({ outputSampleRate: 24000, framesAfterEos: 100, eosThreshold: -1e30 })
|
|
143
|
+
const tail = await model.run({ input: 'Hi.' })
|
|
144
|
+
let tailSamples = 0
|
|
145
|
+
let tailCompleted = 0
|
|
146
|
+
for await (const chunk of tail.iterate()) {
|
|
147
|
+
tailSamples += chunk.outputArray.length
|
|
148
|
+
if (chunk.isLast) tailCompleted++
|
|
149
|
+
}
|
|
150
|
+
t.is(tailCompleted, 1, 'explicit EOS tail completes normally')
|
|
151
|
+
t.is(tailSamples, 100 * 1920, 'all 100 EOS-tail frames are delivered')
|
|
152
|
+
} finally {
|
|
153
|
+
await model.destroy()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
)
|
|
@@ -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
|
|
package/test/utils/runTTS.js
CHANGED
|
@@ -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({
|