@qvac/tts-ggml 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -10
- package/docs/pocket-tts.md +12 -10
- package/index.d.ts +43 -1130
- package/index.js +127 -3042
- package/lib/addonEvents.d.ts +26 -0
- package/lib/addonEvents.js +51 -0
- package/lib/conditioning.d.ts +50 -0
- package/lib/conditioning.js +184 -0
- package/lib/engineDetection.d.ts +14 -0
- package/lib/engineDetection.js +110 -0
- package/lib/engineIds.d.ts +15 -0
- package/lib/engineIds.js +39 -0
- package/lib/engineParams.d.ts +3 -0
- package/lib/engineParams.js +34 -0
- package/lib/engineSetup.d.ts +153 -0
- package/lib/engineSetup.js +2 -0
- package/lib/engines/audio8.d.ts +83 -0
- package/lib/engines/audio8.js +262 -0
- package/lib/engines/chatterbox.d.ts +10 -0
- package/lib/engines/chatterbox.js +112 -0
- package/lib/engines/cosyvoice.d.ts +87 -0
- package/lib/engines/cosyvoice.js +267 -0
- package/lib/engines/moss.d.ts +26 -0
- package/lib/engines/moss.js +177 -0
- package/lib/engines/mossSoundEffect.d.ts +41 -0
- package/lib/engines/mossSoundEffect.js +117 -0
- package/lib/engines/mossSpeech.d.ts +86 -0
- package/lib/engines/mossSpeech.js +228 -0
- package/lib/engines/parler.d.ts +71 -0
- package/lib/engines/parler.js +287 -0
- package/lib/engines/supertonic.d.ts +11 -0
- package/lib/engines/supertonic.js +115 -0
- package/lib/exclusiveQueue.d.ts +16 -0
- package/lib/exclusiveQueue.js +45 -0
- package/lib/jobFields.d.ts +20 -0
- package/lib/jobFields.js +73 -0
- package/lib/modelFiles.d.ts +207 -0
- package/lib/modelFiles.js +117 -0
- package/lib/nativeParams.d.ts +12 -0
- package/lib/nativeParams.js +73 -0
- package/lib/optionValidation.d.ts +2 -0
- package/lib/optionValidation.js +103 -0
- package/lib/options.d.ts +4 -0
- package/lib/options.js +17 -0
- package/lib/pocketLifecycle.d.ts +67 -0
- package/lib/pocketLifecycle.js +170 -0
- package/lib/reload.d.ts +12 -0
- package/lib/reload.js +76 -0
- package/lib/sentenceStream.d.ts +85 -0
- package/lib/sentenceStream.js +367 -0
- package/lib/setup.d.ts +7 -0
- package/lib/setup.js +169 -0
- package/lib/streamingSupport.d.ts +8 -0
- package/lib/streamingSupport.js +77 -0
- package/lib/types.d.ts +465 -0
- package/lib/types.js +2 -0
- package/lib/voiceControls.d.ts +12 -0
- package/lib/voiceControls.js +23 -0
- package/package.json +11 -10
- package/test/integration/audio8.test.js +6 -12
- package/test/integration/gpu-smoke.test.js +2 -11
- package/test/integration/lavasr-enhancer.test.js +2 -15
- package/test/integration/moss-sfx.test.js +29 -4
- package/test/integration/moss.test.js +1 -1
- package/test/utils/desktopGpuBackend.js +21 -0
- package/test/utils/runTTS.js +1 -1
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SentenceStreamer = void 0;
|
|
4
|
+
exports.normalizeError = normalizeError;
|
|
5
|
+
exports.ttsOutputDebugString = ttsOutputDebugString;
|
|
6
|
+
exports.isAudioOutputEvent = isAudioOutputEvent;
|
|
7
|
+
exports.isStatsEvent = isStatsEvent;
|
|
8
|
+
exports.emptyStreamAccumulator = emptyStreamAccumulator;
|
|
9
|
+
exports.computeSentenceStreamStats = computeSentenceStreamStats;
|
|
10
|
+
exports.resolveRunStreamingOptions = resolveRunStreamingOptions;
|
|
11
|
+
exports.normalizeTextStream = normalizeTextStream;
|
|
12
|
+
exports.requireStreamOutputText = requireStreamOutputText;
|
|
13
|
+
exports.splitSentenceChunks = splitSentenceChunks;
|
|
14
|
+
const error_1 = require("./error");
|
|
15
|
+
const textChunker_1 = require("./textChunker");
|
|
16
|
+
const textStreamAccumulator_1 = require("./textStreamAccumulator");
|
|
17
|
+
function normalizeError(error) {
|
|
18
|
+
return typeof error === "string"
|
|
19
|
+
? error
|
|
20
|
+
: error instanceof Error
|
|
21
|
+
? error
|
|
22
|
+
: new Error("Unknown TTS error");
|
|
23
|
+
}
|
|
24
|
+
function defaultAccumulateSentencesForStreamInput(textStream) {
|
|
25
|
+
if (textStream == null ||
|
|
26
|
+
typeof textStream === "string" ||
|
|
27
|
+
Array.isArray(textStream)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return (typeof textStream[Symbol.asyncIterator] === "function");
|
|
31
|
+
}
|
|
32
|
+
function ttsOutputDebugString(data) {
|
|
33
|
+
if (!data)
|
|
34
|
+
return "";
|
|
35
|
+
if (typeof data === "string")
|
|
36
|
+
return data;
|
|
37
|
+
if (typeof data === "number" ||
|
|
38
|
+
typeof data === "boolean" ||
|
|
39
|
+
typeof data === "bigint") {
|
|
40
|
+
return data.toString();
|
|
41
|
+
}
|
|
42
|
+
if (typeof data === "symbol")
|
|
43
|
+
return data.description || "";
|
|
44
|
+
if (typeof data === "function")
|
|
45
|
+
return data.name;
|
|
46
|
+
const value = data;
|
|
47
|
+
const summary = {};
|
|
48
|
+
if (value.sampleRate != null)
|
|
49
|
+
summary.sampleRate = value.sampleRate;
|
|
50
|
+
if (value.chunkIndex != null)
|
|
51
|
+
summary.chunkIndex = value.chunkIndex;
|
|
52
|
+
if (value.isLast != null)
|
|
53
|
+
summary.isLast = value.isLast;
|
|
54
|
+
if (value.sentenceChunk != null) {
|
|
55
|
+
summary.sentenceChunk = value.sentenceChunk;
|
|
56
|
+
}
|
|
57
|
+
if (value.outputArray &&
|
|
58
|
+
typeof value.outputArray.length === "number") {
|
|
59
|
+
summary.outputArrayLen = value.outputArray.length;
|
|
60
|
+
}
|
|
61
|
+
return JSON.stringify(summary);
|
|
62
|
+
}
|
|
63
|
+
function isAudioOutputEvent(data) {
|
|
64
|
+
return (data != null &&
|
|
65
|
+
typeof data === "object" &&
|
|
66
|
+
"outputArray" in data &&
|
|
67
|
+
data.outputArray != null);
|
|
68
|
+
}
|
|
69
|
+
function isStatsEvent(data) {
|
|
70
|
+
return (data != null &&
|
|
71
|
+
typeof data === "object" &&
|
|
72
|
+
("totalTime" in data ||
|
|
73
|
+
"audioDurationMs" in data ||
|
|
74
|
+
"totalSamples" in data));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* What `tokensPerSecond` counts. Audio8 paces on the codec frame grid and
|
|
78
|
+
* reports frames per second in batch synthesis; the text-paced engines report
|
|
79
|
+
* characters per second. Streaming has to keep the same unit as `run()` or the
|
|
80
|
+
* number silently changes meaning between the two entry points.
|
|
81
|
+
*/
|
|
82
|
+
function emptyStreamAccumulator() {
|
|
83
|
+
return {
|
|
84
|
+
totalTime: 0,
|
|
85
|
+
audioDurationMs: 0,
|
|
86
|
+
totalSamples: 0,
|
|
87
|
+
generatedFrames: 0,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function streamedTokenCount(chunks, accumulator, countsFrames) {
|
|
91
|
+
return countsFrames
|
|
92
|
+
? accumulator.generatedFrames
|
|
93
|
+
: chunks.join("").length;
|
|
94
|
+
}
|
|
95
|
+
function computeSentenceStreamStats(chunks, accumulator, countsFrames) {
|
|
96
|
+
const { generatedFrames, ...totals } = accumulator;
|
|
97
|
+
const produced = streamedTokenCount(chunks, accumulator, countsFrames);
|
|
98
|
+
const stats = {
|
|
99
|
+
...totals,
|
|
100
|
+
tokensPerSecond: accumulator.totalTime > 0 ? produced / accumulator.totalTime : 0,
|
|
101
|
+
realTimeFactor: accumulator.audioDurationMs > 0
|
|
102
|
+
? (accumulator.totalTime * 1000) /
|
|
103
|
+
accumulator.audioDurationMs
|
|
104
|
+
: 0,
|
|
105
|
+
};
|
|
106
|
+
if (countsFrames)
|
|
107
|
+
stats.generatedFrames = generatedFrames;
|
|
108
|
+
return stats;
|
|
109
|
+
}
|
|
110
|
+
function emptyRuntimeStats() {
|
|
111
|
+
return {
|
|
112
|
+
totalTime: 0,
|
|
113
|
+
tokensPerSecond: 0,
|
|
114
|
+
realTimeFactor: 0,
|
|
115
|
+
audioDurationMs: 0,
|
|
116
|
+
totalSamples: 0,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function resolveRunStreamingOptions(textStream, options) {
|
|
120
|
+
const normalized = options == null || typeof options !== "object" ? {} : options;
|
|
121
|
+
let accumulateSentences = normalized.accumulateSentences;
|
|
122
|
+
if (accumulateSentences === undefined) {
|
|
123
|
+
accumulateSentences =
|
|
124
|
+
defaultAccumulateSentencesForStreamInput(textStream);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
accumulateSentences: !!accumulateSentences,
|
|
128
|
+
sentenceDelimiterPreset: normalized.sentenceDelimiterPreset === "latin" ||
|
|
129
|
+
normalized.sentenceDelimiterPreset === "cjk" ||
|
|
130
|
+
normalized.sentenceDelimiterPreset === "multilingual"
|
|
131
|
+
? normalized.sentenceDelimiterPreset
|
|
132
|
+
: "multilingual",
|
|
133
|
+
maxBufferScalars: normalized.maxBufferScalars,
|
|
134
|
+
flushAfterMs: normalized.flushAfterMs ?? textStreamAccumulator_1.DEFAULT_FLUSH_AFTER_MS,
|
|
135
|
+
sentenceDelimiter: normalized.sentenceDelimiter instanceof RegExp
|
|
136
|
+
? normalized.sentenceDelimiter
|
|
137
|
+
: undefined,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function normalizeTextStream(textStream) {
|
|
141
|
+
if (textStream == null) {
|
|
142
|
+
throw new error_1.QvacErrorAddonTTSGgml({
|
|
143
|
+
code: error_1.ERR_CODES.FAILED_TO_APPEND,
|
|
144
|
+
adds: "runStreaming: text stream is required",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (typeof textStream === "string") {
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- async iterable shape is required by the public API.
|
|
149
|
+
return (async function* oneString() {
|
|
150
|
+
yield textStream;
|
|
151
|
+
})();
|
|
152
|
+
}
|
|
153
|
+
if (typeof textStream[Symbol.asyncIterator] === "function") {
|
|
154
|
+
return textStream;
|
|
155
|
+
}
|
|
156
|
+
if (Array.isArray(textStream) ||
|
|
157
|
+
typeof textStream[Symbol.iterator] === "function") {
|
|
158
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- adapts synchronous iterables to the async iterable contract.
|
|
159
|
+
return (async function* fromIterable() {
|
|
160
|
+
for (const value of textStream) {
|
|
161
|
+
yield value;
|
|
162
|
+
}
|
|
163
|
+
})();
|
|
164
|
+
}
|
|
165
|
+
throw new error_1.QvacErrorAddonTTSGgml({
|
|
166
|
+
code: error_1.ERR_CODES.FAILED_TO_APPEND,
|
|
167
|
+
adds: "runStreaming: expected string, array of strings, Iterable, or AsyncIterable",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/** `run({ streamOutput: true })` splits its text, so it needs a non-empty string. */
|
|
171
|
+
function requireStreamOutputText(input) {
|
|
172
|
+
if (typeof input !== "string" || input.trim().length === 0) {
|
|
173
|
+
throw new error_1.QvacErrorAddonTTSGgml({
|
|
174
|
+
code: error_1.ERR_CODES.FAILED_TO_APPEND,
|
|
175
|
+
adds: "run with streamOutput: non-empty string `input` is required",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return input;
|
|
179
|
+
}
|
|
180
|
+
/** The sentence chunks of `text`; an input that yields none is an error. */
|
|
181
|
+
function splitSentenceChunks(text, options, language) {
|
|
182
|
+
const chunks = (0, textChunker_1.splitTtsText)(String(text), {
|
|
183
|
+
language,
|
|
184
|
+
locale: options.locale,
|
|
185
|
+
maxScalars: options.maxChunkScalars,
|
|
186
|
+
});
|
|
187
|
+
if (chunks.length === 0) {
|
|
188
|
+
throw new error_1.QvacErrorAddonTTSGgml({
|
|
189
|
+
code: error_1.ERR_CODES.FAILED_TO_APPEND,
|
|
190
|
+
adds: "chunked synthesis: text produced no chunks after split",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return chunks;
|
|
194
|
+
}
|
|
195
|
+
function mergeSentenceStreamStats(accumulator, data) {
|
|
196
|
+
if (accumulator.firstAudioMs === undefined &&
|
|
197
|
+
typeof data.firstAudioMs === "number" &&
|
|
198
|
+
Number.isFinite(data.firstAudioMs) &&
|
|
199
|
+
data.firstAudioMs >= 0) {
|
|
200
|
+
accumulator.firstAudioMs = data.firstAudioMs;
|
|
201
|
+
}
|
|
202
|
+
accumulator.totalTime +=
|
|
203
|
+
typeof data.totalTime === "number" ? data.totalTime : 0;
|
|
204
|
+
accumulator.audioDurationMs +=
|
|
205
|
+
typeof data.audioDurationMs === "number"
|
|
206
|
+
? data.audioDurationMs
|
|
207
|
+
: 0;
|
|
208
|
+
accumulator.totalSamples +=
|
|
209
|
+
typeof data.totalSamples === "number" ? data.totalSamples : 0;
|
|
210
|
+
accumulator.generatedFrames +=
|
|
211
|
+
typeof data.generatedFrames === "number" ? data.generatedFrames : 0;
|
|
212
|
+
if (typeof data.codecSidecarLoaded === "number") {
|
|
213
|
+
accumulator.codecSidecarLoaded = data.codecSidecarLoaded;
|
|
214
|
+
}
|
|
215
|
+
if (typeof data.codecOnCoreml === "number") {
|
|
216
|
+
accumulator.codecOnCoreml = data.codecOnCoreml;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Drives one sentence stream at a time. The wrapper routes native output and
|
|
221
|
+
* stats events here while a context is active; a newer stream, a cancel or a
|
|
222
|
+
* reload replaces or clears the context, and the driver of the old stream
|
|
223
|
+
* notices and stops.
|
|
224
|
+
*/
|
|
225
|
+
class SentenceStreamer {
|
|
226
|
+
host;
|
|
227
|
+
/** The active stream, or null between streams. */
|
|
228
|
+
ctx = null;
|
|
229
|
+
constructor(host) {
|
|
230
|
+
this.host = host;
|
|
231
|
+
}
|
|
232
|
+
streamChunks(chunks, jobFields) {
|
|
233
|
+
const response = this.host.startJob();
|
|
234
|
+
const context = {
|
|
235
|
+
chunks,
|
|
236
|
+
chunkIdx: 0,
|
|
237
|
+
acc: emptyStreamAccumulator(),
|
|
238
|
+
chunkResolver: null,
|
|
239
|
+
jobFields,
|
|
240
|
+
};
|
|
241
|
+
this.ctx = context;
|
|
242
|
+
void this.driveChunks(context).catch((error) => this.abort(context, error));
|
|
243
|
+
return response;
|
|
244
|
+
}
|
|
245
|
+
streamText(source, jobFields) {
|
|
246
|
+
const response = this.host.startJob();
|
|
247
|
+
const context = {
|
|
248
|
+
textStreamMode: true,
|
|
249
|
+
asyncTextSource: source,
|
|
250
|
+
chunks: [],
|
|
251
|
+
chunkIdx: 0,
|
|
252
|
+
acc: emptyStreamAccumulator(),
|
|
253
|
+
chunkResolver: null,
|
|
254
|
+
jobFields,
|
|
255
|
+
};
|
|
256
|
+
this.ctx = context;
|
|
257
|
+
void this.driveText(context, source).catch((error) => this.abort(context, error));
|
|
258
|
+
return response;
|
|
259
|
+
}
|
|
260
|
+
rejectActiveChunk(error) {
|
|
261
|
+
const resolver = this.ctx?.chunkResolver;
|
|
262
|
+
if (!resolver)
|
|
263
|
+
return;
|
|
264
|
+
this.ctx.chunkResolver = null;
|
|
265
|
+
resolver.reject(error);
|
|
266
|
+
}
|
|
267
|
+
fail(reason) {
|
|
268
|
+
this.rejectActiveChunk(reason instanceof Error ? reason : new Error(reason));
|
|
269
|
+
this.ctx = null;
|
|
270
|
+
this.host.failJob(reason);
|
|
271
|
+
}
|
|
272
|
+
enrich(data) {
|
|
273
|
+
const context = this.ctx;
|
|
274
|
+
if (!context) {
|
|
275
|
+
return data;
|
|
276
|
+
}
|
|
277
|
+
const index = context.chunkIdx;
|
|
278
|
+
const enriched = {
|
|
279
|
+
outputArray: data.outputArray,
|
|
280
|
+
chunkIndex: index,
|
|
281
|
+
sentenceChunk: context.chunks[index] || "",
|
|
282
|
+
};
|
|
283
|
+
if (data.sampleRate != null) {
|
|
284
|
+
enriched.sampleRate = data.sampleRate;
|
|
285
|
+
}
|
|
286
|
+
if (!context.textStreamMode) {
|
|
287
|
+
enriched.isLast = index >= context.chunks.length - 1 && data.isLast !== false;
|
|
288
|
+
}
|
|
289
|
+
return enriched;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Folds a chunk's stats into the aggregate and releases the chunk; after the
|
|
293
|
+
* last pre-chunked sentence, ends the job. Returns false when no stream is
|
|
294
|
+
* active, so the caller can end a plain job with the stats as they are.
|
|
295
|
+
*/
|
|
296
|
+
onStats(data) {
|
|
297
|
+
const context = this.ctx;
|
|
298
|
+
if (!context)
|
|
299
|
+
return false;
|
|
300
|
+
mergeSentenceStreamStats(context.acc, data);
|
|
301
|
+
if (context.chunkResolver) {
|
|
302
|
+
context.chunkResolver.resolve();
|
|
303
|
+
context.chunkResolver = null;
|
|
304
|
+
}
|
|
305
|
+
if (!context.textStreamMode &&
|
|
306
|
+
context.chunkIdx >= context.chunks.length - 1) {
|
|
307
|
+
this.host.endJob(computeSentenceStreamStats(context.chunks, context.acc, this.host.countsFrames()));
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
abort(context, error) {
|
|
312
|
+
if (context !== this.ctx)
|
|
313
|
+
return;
|
|
314
|
+
this.rejectActiveChunk(error);
|
|
315
|
+
this.ctx = null;
|
|
316
|
+
this.host.failJob(normalizeError(error));
|
|
317
|
+
}
|
|
318
|
+
async driveChunks(context) {
|
|
319
|
+
for (let index = 0; index < context.chunks.length; index++) {
|
|
320
|
+
if (context !== this.ctx)
|
|
321
|
+
return;
|
|
322
|
+
context.chunkIdx = index;
|
|
323
|
+
await this.synthesizeChunk(context, context.chunks[index]);
|
|
324
|
+
}
|
|
325
|
+
if (context !== this.ctx)
|
|
326
|
+
return;
|
|
327
|
+
this.ctx = null;
|
|
328
|
+
}
|
|
329
|
+
async driveText(context, source) {
|
|
330
|
+
try {
|
|
331
|
+
for await (const piece of source) {
|
|
332
|
+
if (context !== this.ctx)
|
|
333
|
+
return;
|
|
334
|
+
const text = String(piece).trim();
|
|
335
|
+
if (text.length === 0)
|
|
336
|
+
continue;
|
|
337
|
+
context.chunks.push(text);
|
|
338
|
+
context.chunkIdx = context.chunks.length - 1;
|
|
339
|
+
await this.synthesizeChunk(context, text);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
this.abort(context, error);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (context !== this.ctx)
|
|
347
|
+
return;
|
|
348
|
+
this.ctx = null;
|
|
349
|
+
this.host.endJob(context.chunks.length === 0
|
|
350
|
+
? emptyRuntimeStats()
|
|
351
|
+
: computeSentenceStreamStats(context.chunks, context.acc, this.host.countsFrames()));
|
|
352
|
+
}
|
|
353
|
+
async synthesizeChunk(context, text) {
|
|
354
|
+
const done = new Promise((resolve, reject) => {
|
|
355
|
+
context.chunkResolver = { resolve, reject };
|
|
356
|
+
});
|
|
357
|
+
// Dispatch may fail before the completion promise is awaited.
|
|
358
|
+
void done.catch(() => { });
|
|
359
|
+
await this.host.dispatchJob({
|
|
360
|
+
type: "text",
|
|
361
|
+
input: text,
|
|
362
|
+
...(context.jobFields ?? {}),
|
|
363
|
+
});
|
|
364
|
+
await done;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
exports.SentenceStreamer = SentenceStreamer;
|
package/lib/setup.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EngineSetup } from "./engineSetup";
|
|
2
|
+
import type { TTSGgmlOptions } from "./types";
|
|
3
|
+
export declare function resolveDefaultLazySessionLoading(lazySessionLoading?: boolean): boolean;
|
|
4
|
+
export declare function validateOutputSampleRate(outputSampleRate?: number): number | null;
|
|
5
|
+
export declare function assertGpuIntentConsistent(useGPU?: boolean, nGpuLayers?: number): void;
|
|
6
|
+
/** Resolve and validate everything the constructor derives from `options`. */
|
|
7
|
+
export declare function resolveEngineSetup(options: TTSGgmlOptions): EngineSetup;
|
package/lib/setup.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveDefaultLazySessionLoading = resolveDefaultLazySessionLoading;
|
|
4
|
+
exports.validateOutputSampleRate = validateOutputSampleRate;
|
|
5
|
+
exports.assertGpuIntentConsistent = assertGpuIntentConsistent;
|
|
6
|
+
exports.resolveEngineSetup = resolveEngineSetup;
|
|
7
|
+
/**
|
|
8
|
+
* Turns constructor options into an `EngineSetup`: normalized files, the
|
|
9
|
+
* detected engine and its model paths, synthesis settings, backend paths, and
|
|
10
|
+
* (for Pocket) the native parameters, validated in the order the wrapper has
|
|
11
|
+
* always reported them.
|
|
12
|
+
*/
|
|
13
|
+
/* eslint-disable @typescript-eslint/no-require-imports -- Bare modules expose CommonJS export shapes. */
|
|
14
|
+
const bareOs = require("bare-os");
|
|
15
|
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
16
|
+
const engineDetection_1 = require("./engineDetection");
|
|
17
|
+
const engineIds_1 = require("./engineIds");
|
|
18
|
+
const cosyvoice_1 = require("./engines/cosyvoice");
|
|
19
|
+
const moss_1 = require("./engines/moss");
|
|
20
|
+
const modelFiles_1 = require("./modelFiles");
|
|
21
|
+
const optionValidation_1 = require("./optionValidation");
|
|
22
|
+
const pocketConfig_1 = require("./pocketConfig");
|
|
23
|
+
const { platform } = bareOs;
|
|
24
|
+
const MIN_OUTPUT_SAMPLE_RATE = 8000;
|
|
25
|
+
const MAX_OUTPUT_SAMPLE_RATE = 192000;
|
|
26
|
+
function resolveDefaultLazySessionLoading(lazySessionLoading) {
|
|
27
|
+
if (lazySessionLoading != null)
|
|
28
|
+
return lazySessionLoading;
|
|
29
|
+
return platform() === "ios" || platform() === "android";
|
|
30
|
+
}
|
|
31
|
+
function validateOutputSampleRate(outputSampleRate) {
|
|
32
|
+
if (outputSampleRate == null)
|
|
33
|
+
return null;
|
|
34
|
+
if (outputSampleRate < MIN_OUTPUT_SAMPLE_RATE ||
|
|
35
|
+
outputSampleRate > MAX_OUTPUT_SAMPLE_RATE) {
|
|
36
|
+
throw new Error(`outputSampleRate must be between ${MIN_OUTPUT_SAMPLE_RATE} and ` +
|
|
37
|
+
`${MAX_OUTPUT_SAMPLE_RATE}, got ${outputSampleRate}`);
|
|
38
|
+
}
|
|
39
|
+
return outputSampleRate;
|
|
40
|
+
}
|
|
41
|
+
function resolveEnhancerGgufPath(files, enhancer) {
|
|
42
|
+
if (enhancer != null && enhancer.type !== "lavasr") {
|
|
43
|
+
throw new Error(`tts-ggml: unknown enhancer.type '${String(enhancer.type)}', expected 'lavasr'.`);
|
|
44
|
+
}
|
|
45
|
+
return (0, modelFiles_1.firstNonEmpty)(files.lavasrEnhancer, enhancer?.enhancerPath);
|
|
46
|
+
}
|
|
47
|
+
function resolveDenoiserGgufPath(files, denoiser) {
|
|
48
|
+
if (denoiser != null && denoiser.type !== "lavasr") {
|
|
49
|
+
throw new Error(`tts-ggml: unknown denoiser.type '${String(denoiser.type)}', expected 'lavasr'.`);
|
|
50
|
+
}
|
|
51
|
+
return (0, modelFiles_1.firstNonEmpty)(files.lavasrDenoiser, denoiser?.denoiserPath);
|
|
52
|
+
}
|
|
53
|
+
function assertGpuIntentConsistent(useGPU, nGpuLayers) {
|
|
54
|
+
if (typeof useGPU !== "boolean" || nGpuLayers == null)
|
|
55
|
+
return;
|
|
56
|
+
if (useGPU === (nGpuLayers !== 0))
|
|
57
|
+
return;
|
|
58
|
+
throw new Error(`tts-ggml: useGPU=${String(useGPU)} conflicts with ` +
|
|
59
|
+
`nGpuLayers=${nGpuLayers}. Either drop one of the two, or make ` +
|
|
60
|
+
"them agree (useGPU:true + nGpuLayers!=0, or " +
|
|
61
|
+
"useGPU:false + nGpuLayers=0).");
|
|
62
|
+
}
|
|
63
|
+
/** Synthesis options with their aliases collapsed; may throw on a malformed `instruct`. */
|
|
64
|
+
function settingsFromOptions(options) {
|
|
65
|
+
return {
|
|
66
|
+
referenceAudio: options.referenceAudio,
|
|
67
|
+
referenceText: options.referenceText,
|
|
68
|
+
greedy: options.greedy,
|
|
69
|
+
voiceDir: options.voiceDir,
|
|
70
|
+
seed: options.seed,
|
|
71
|
+
nGpuLayers: options.nGpuLayers,
|
|
72
|
+
nCtx: options.nCtx,
|
|
73
|
+
kvCacheType: options.kvCacheType,
|
|
74
|
+
threads: options.threads,
|
|
75
|
+
streamChunkTokens: options.streamChunkTokens,
|
|
76
|
+
streamFirstChunkTokens: options.streamFirstChunkTokens,
|
|
77
|
+
streamChunkTolerancePct: options.streamChunkTolerancePct,
|
|
78
|
+
streamMinChunkTokens: options.streamMinChunkTokens,
|
|
79
|
+
streamLeftContextTokens: options.streamLeftContextTokens,
|
|
80
|
+
cfmSteps: options.cfmSteps,
|
|
81
|
+
cfgRate: options.cfgRate,
|
|
82
|
+
batchCfmSteps: options.batchCfmSteps,
|
|
83
|
+
nPredict: options.nPredict,
|
|
84
|
+
maxSentenceChars: options.maxSentenceChars,
|
|
85
|
+
crossfadeMs: options.crossfadeMs,
|
|
86
|
+
repeatPenalty: options.repeatPenalty,
|
|
87
|
+
exaggeration: options.exaggeration,
|
|
88
|
+
cfgWeight: options.cfgWeight,
|
|
89
|
+
minP: options.minP,
|
|
90
|
+
voiceJsonPath: options.voiceJsonPath,
|
|
91
|
+
prewarmText: options.prewarmText,
|
|
92
|
+
vulkanDevice: options.vulkanDevice,
|
|
93
|
+
flowCutPrompt: options.flowCutPrompt,
|
|
94
|
+
promptText: options.promptText,
|
|
95
|
+
instruct: (0, cosyvoice_1.renderCosyvoiceInstruct)(options.instruct) || undefined,
|
|
96
|
+
voice: (0, modelFiles_1.firstNonEmpty)(options.voice, options.voiceName),
|
|
97
|
+
steps: (0, modelFiles_1.firstNonEmpty)(options.steps, options.numInferenceSteps),
|
|
98
|
+
speed: options.speed,
|
|
99
|
+
noiseNpyPath: options.noiseNpyPath,
|
|
100
|
+
description: (0, modelFiles_1.firstNonEmpty)(options.description, options.voiceDescription),
|
|
101
|
+
emotion: options.emotion,
|
|
102
|
+
pitch: options.pitch,
|
|
103
|
+
pace: options.pace,
|
|
104
|
+
expressivity: options.expressivity,
|
|
105
|
+
noise: options.noise,
|
|
106
|
+
reverb: options.reverb,
|
|
107
|
+
quality: options.quality,
|
|
108
|
+
temperature: options.temperature,
|
|
109
|
+
topK: options.topK,
|
|
110
|
+
topP: options.topP,
|
|
111
|
+
maxFrames: options.maxFrames,
|
|
112
|
+
minNewTokens: options.minNewTokens,
|
|
113
|
+
normalizeNumbers: options.normalizeNumbers,
|
|
114
|
+
...(0, moss_1.mossVoiceSettings)(options),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function resolveBackendPaths(files, options, config) {
|
|
118
|
+
return {
|
|
119
|
+
enhancerGgufPath: resolveEnhancerGgufPath(files, options.enhancer),
|
|
120
|
+
denoiserGgufPath: resolveDenoiserGgufPath(files, options.denoiser),
|
|
121
|
+
backendsDir: (0, modelFiles_1.firstNonEmpty)(options.backendsDir, config.backendsDir),
|
|
122
|
+
openclCacheDir: (0, modelFiles_1.firstNonEmpty)(options.openclCacheDir, config.openclCacheDir),
|
|
123
|
+
vulkanCacheDir: (0, modelFiles_1.firstNonEmpty)(options.vulkanCacheDir, config.vulkanCacheDir),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function resolvePocketSetup(files, options, config) {
|
|
127
|
+
if (files.lavasrEnhancer || files.lavasrDenoiser) {
|
|
128
|
+
throw new Error("Pocket does not support LavaSR enhancement or denoising");
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
files,
|
|
132
|
+
options: { ...options },
|
|
133
|
+
params: (0, pocketConfig_1.buildPocketParams)(files, options, config),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Resolve and validate everything the constructor derives from `options`. */
|
|
137
|
+
function resolveEngineSetup(options) {
|
|
138
|
+
const files = (0, modelFiles_1.normalizeGgmlFiles)(options.files || {});
|
|
139
|
+
const config = { ...(options.config || {}) };
|
|
140
|
+
const outputSampleRate = validateOutputSampleRate(config.outputSampleRate);
|
|
141
|
+
const engineType = (0, engineDetection_1.detectEngineType)(options.engine, files);
|
|
142
|
+
const mossVoice = (0, moss_1.mossVoiceSettings)(options);
|
|
143
|
+
const pocket = engineType === engineIds_1.ENGINE_POCKET
|
|
144
|
+
? resolvePocketSetup(files, options, config)
|
|
145
|
+
: null;
|
|
146
|
+
const paths = (0, engineDetection_1.resolveModelPaths)(engineType, files, {
|
|
147
|
+
mecabDictPath: options.mecabDictPath,
|
|
148
|
+
mecabDictDir: options.mecabDictDir,
|
|
149
|
+
cangjieTsvPath: options.cangjieTsvPath,
|
|
150
|
+
dialogue: mossVoice.dialogueReferences !== undefined,
|
|
151
|
+
});
|
|
152
|
+
const settings = settingsFromOptions(options);
|
|
153
|
+
const backend = resolveBackendPaths(files, options, config);
|
|
154
|
+
const setup = {
|
|
155
|
+
engineType,
|
|
156
|
+
config,
|
|
157
|
+
outputSampleRate,
|
|
158
|
+
paths,
|
|
159
|
+
settings,
|
|
160
|
+
backend,
|
|
161
|
+
pocket,
|
|
162
|
+
};
|
|
163
|
+
assertGpuIntentConsistent(config.useGPU, settings.nGpuLayers);
|
|
164
|
+
(0, optionValidation_1.validateEngineOptions)(setup);
|
|
165
|
+
if (config.useGPU === undefined && settings.nGpuLayers == null) {
|
|
166
|
+
config.useGPU = false;
|
|
167
|
+
}
|
|
168
|
+
return setup;
|
|
169
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EngineSetup, SynthesisSettings } from "./engineSetup";
|
|
2
|
+
export declare function requestsChunkStreaming(settings: SynthesisSettings): boolean;
|
|
3
|
+
/** Native chunk streaming against what the engine can actually do. */
|
|
4
|
+
export declare function assertChunkStreamingSupport(setup: EngineSetup): void;
|
|
5
|
+
/** The LavaSR denoiser has no streaming form yet. */
|
|
6
|
+
export declare function assertDenoiserBatchOnly(setup: EngineSetup): void;
|
|
7
|
+
/** Sentence-level streaming (runStream / runStreaming / streamOutput). */
|
|
8
|
+
export declare function assertSentenceStreamingAllowed(setup: EngineSetup, where: string): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestsChunkStreaming = requestsChunkStreaming;
|
|
4
|
+
exports.assertChunkStreamingSupport = assertChunkStreamingSupport;
|
|
5
|
+
exports.assertDenoiserBatchOnly = assertDenoiserBatchOnly;
|
|
6
|
+
exports.assertSentenceStreamingAllowed = assertSentenceStreamingAllowed;
|
|
7
|
+
const engineIds_1 = require("./engineIds");
|
|
8
|
+
const moss_1 = require("./engines/moss");
|
|
9
|
+
// Every streaming engine starts native chunk streaming on
|
|
10
|
+
// streamChunkTokens > 0 alone: a count of 0 means batch, and
|
|
11
|
+
// streamFirstChunkTokens only sizes the first chunk once streaming is on.
|
|
12
|
+
function requestsChunkStreaming(settings) {
|
|
13
|
+
return (settings.streamChunkTokens ?? 0) > 0;
|
|
14
|
+
}
|
|
15
|
+
/** Native chunk streaming against what the engine can actually do. */
|
|
16
|
+
function assertChunkStreamingSupport(setup) {
|
|
17
|
+
const { engineType, settings, backend } = setup;
|
|
18
|
+
// Checked ahead of the shared denoiser guard: Supertonic rejects both
|
|
19
|
+
// LavaSR stages for one reason (tts-cpp has only one-shot enhance /
|
|
20
|
+
// denoise, and the engine reports no native rate before synthesis).
|
|
21
|
+
if (engineType === engineIds_1.ENGINE_SUPERTONIC &&
|
|
22
|
+
(backend.enhancerGgufPath || backend.denoiserGgufPath) &&
|
|
23
|
+
requestsChunkStreaming(settings)) {
|
|
24
|
+
throw new Error("tts-ggml: the LavaSR enhancer/denoiser are not supported with " +
|
|
25
|
+
"supertonic native chunk streaming (streamChunkTokens > 0). Use " +
|
|
26
|
+
"batch synthesis or sentence-level streaming via runStream() / " +
|
|
27
|
+
"runStreaming() / run({ streamOutput: true }) for enhanced or " +
|
|
28
|
+
"denoised output.");
|
|
29
|
+
}
|
|
30
|
+
if (engineType === engineIds_1.ENGINE_AUDIO8 &&
|
|
31
|
+
(settings.streamChunkTokens != null ||
|
|
32
|
+
settings.streamFirstChunkTokens != null)) {
|
|
33
|
+
throw new Error("tts-ggml: streamChunkTokens / streamFirstChunkTokens are not " +
|
|
34
|
+
"supported by the audio8 engine, which synthesizes a whole " +
|
|
35
|
+
"utterance at a time. Use sentence-level streaming via " +
|
|
36
|
+
"runStream() / runStreaming() / run({ streamOutput: true }).");
|
|
37
|
+
}
|
|
38
|
+
if ((engineType === engineIds_1.ENGINE_MOSS_SFX || engineType === engineIds_1.ENGINE_MOSS_SPEECH) &&
|
|
39
|
+
(settings.streamChunkTokens != null ||
|
|
40
|
+
settings.streamFirstChunkTokens != null)) {
|
|
41
|
+
throw new Error("tts-ggml: streamChunkTokens / streamFirstChunkTokens are not " +
|
|
42
|
+
`supported by the ${engineType} engine, which returns a ` +
|
|
43
|
+
"whole result at a time. Use run().");
|
|
44
|
+
}
|
|
45
|
+
if (engineType === engineIds_1.ENGINE_MOSS && settings.streamFirstChunkTokens != null) {
|
|
46
|
+
throw new Error("tts-ggml: streamFirstChunkTokens is not supported by the moss " +
|
|
47
|
+
"engine, which streams fixed-size chunks; set streamChunkTokens " +
|
|
48
|
+
`(codec frames per chunk, ${moss_1.MOSS_FRAMES_PER_SECOND} per second) only.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** The LavaSR denoiser has no streaming form yet. */
|
|
52
|
+
function assertDenoiserBatchOnly(setup) {
|
|
53
|
+
if (!setup.backend.denoiserGgufPath || !requestsChunkStreaming(setup.settings)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw new Error("tts-ggml: the LavaSR denoiser is not yet supported with " +
|
|
57
|
+
"native chunk streaming (streamChunkTokens > 0). Use batch " +
|
|
58
|
+
"synthesis, or drop the denoiser for streaming. Streaming " +
|
|
59
|
+
"denoise is a planned follow-up (needs a stateful streaming " +
|
|
60
|
+
"denoiser).");
|
|
61
|
+
}
|
|
62
|
+
/** Sentence-level streaming (runStream / runStreaming / streamOutput). */
|
|
63
|
+
function assertSentenceStreamingAllowed(setup, where) {
|
|
64
|
+
if (setup.engineType === engineIds_1.ENGINE_MOSS_SPEECH) {
|
|
65
|
+
throw new Error(`tts-ggml: ${where}: the moss-speech engine answers one turn at a ` +
|
|
66
|
+
"time and does not stream; use run()");
|
|
67
|
+
}
|
|
68
|
+
if (setup.engineType === engineIds_1.ENGINE_MOSS_SFX) {
|
|
69
|
+
throw new Error(`tts-ggml: ${where}: the moss-sfx engine generates a whole sound ` +
|
|
70
|
+
"effect from one prompt and does not stream; use run()");
|
|
71
|
+
}
|
|
72
|
+
if (setup.settings.dialogueReferences === undefined)
|
|
73
|
+
return;
|
|
74
|
+
throw new Error(`tts-ggml: ${where}: MOSS dialogue cannot be split into sentences, ` +
|
|
75
|
+
"because every job must open with the reference transcripts; use " +
|
|
76
|
+
"run() or native streaming (streamChunkTokens)");
|
|
77
|
+
}
|