@qvac/tts-ggml 0.12.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 +127 -6
- package/docs/pocket-tts.md +12 -10
- package/examples/moss-sfx-fit.js +14 -7
- 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/fit.d.ts +3 -3
- package/lib/fit.js +4 -4
- 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 +17 -11
- package/test/integration/audio8.test.js +6 -12
- package/test/integration/fit.test.js +115 -18
- package/test/integration/gpu-smoke.test.js +2 -11
- package/test/integration/lavasr-enhancer.test.js +2 -15
- package/test/integration/moss-fit.test.js +13 -13
- package/test/integration/moss-sfx-fit.test.js +14 -14
- package/test/integration/moss-sfx.test.js +29 -4
- package/test/integration/moss-speech.test.js +19 -5
- package/test/integration/moss.test.js +93 -56
- package/test/mobile/integration.auto.cjs +10 -0
- package/test/mobile/test-groups.json +6 -2
- package/test/utils/desktopGpuBackend.js +21 -0
- package/test/utils/gpuOnly.js +15 -0
- package/test/utils/mossBackend.js +34 -0
- package/test/utils/mossGpuValidation.js +70 -0
- package/test/utils/perf-helper.js +275 -0
- package/test/utils/runTTS.js +1 -1
- package/test/utils/testThreads.js +5 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PocketLifecycle = void 0;
|
|
4
|
+
const pocketConfig_1 = require("./pocketConfig");
|
|
5
|
+
const sentenceStream_1 = require("./sentenceStream");
|
|
6
|
+
class PocketLifecycle {
|
|
7
|
+
host;
|
|
8
|
+
/** The native job awaiting its terminal callback, or null. */
|
|
9
|
+
jobPending = null;
|
|
10
|
+
cancelPromise = null;
|
|
11
|
+
inProgress = false;
|
|
12
|
+
constructor(host) {
|
|
13
|
+
this.host = host;
|
|
14
|
+
}
|
|
15
|
+
/** A cancel in flight finishes, terminal callback included, before anything else starts. */
|
|
16
|
+
async waitCancel() {
|
|
17
|
+
if (this.cancelPromise)
|
|
18
|
+
await this.cancelPromise;
|
|
19
|
+
}
|
|
20
|
+
/** One lifecycle operation at a time; requests are refused while one runs. */
|
|
21
|
+
checkReload() {
|
|
22
|
+
if (this.inProgress) {
|
|
23
|
+
throw new Error("Pocket lifecycle operation is already in progress");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** A synthesis request needs a loaded instance and no job in flight. */
|
|
27
|
+
checkRequest() {
|
|
28
|
+
const { state } = this.host;
|
|
29
|
+
if (state.destroyed)
|
|
30
|
+
throw new Error("Pocket instance was destroyed");
|
|
31
|
+
if (!state.weightsLoaded || !this.host.optionalAddon()) {
|
|
32
|
+
throw new Error("Pocket model is not loaded");
|
|
33
|
+
}
|
|
34
|
+
if (this.host.activeJob() || this.jobPending) {
|
|
35
|
+
throw new Error("Pocket synthesis is already in progress");
|
|
36
|
+
}
|
|
37
|
+
this.checkReload();
|
|
38
|
+
}
|
|
39
|
+
async withLifecycle(action) {
|
|
40
|
+
await this.waitCancel();
|
|
41
|
+
this.checkReload();
|
|
42
|
+
this.inProgress = true;
|
|
43
|
+
try {
|
|
44
|
+
await action();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
this.inProgress = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Runs the job behind a completion barrier that its terminal callback releases. */
|
|
51
|
+
async runJob(addon, input) {
|
|
52
|
+
if (this.jobPending)
|
|
53
|
+
throw new Error("Pocket native job is already in progress");
|
|
54
|
+
let resolve = () => { };
|
|
55
|
+
const promise = new Promise((done) => {
|
|
56
|
+
resolve = done;
|
|
57
|
+
});
|
|
58
|
+
const pending = { promise, resolve };
|
|
59
|
+
this.jobPending = pending;
|
|
60
|
+
try {
|
|
61
|
+
await addon.runJob(input);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (this.jobPending === pending)
|
|
65
|
+
this.jobPending = null;
|
|
66
|
+
resolve();
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** A terminal native callback (error or stats) releases the completion barrier. */
|
|
71
|
+
onNativeEvent(data, error) {
|
|
72
|
+
const pending = this.jobPending;
|
|
73
|
+
if (!pending)
|
|
74
|
+
return;
|
|
75
|
+
if ((typeof error === "string" && error.length > 0) || (0, sentenceStream_1.isStatsEvent)(data)) {
|
|
76
|
+
this.jobPending = null;
|
|
77
|
+
pending.resolve();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fails `response` with the caller's reason and stops native work. The
|
|
82
|
+
* completion barrier stays until the terminal callback arrives.
|
|
83
|
+
*/
|
|
84
|
+
attachAbort(response, signal) {
|
|
85
|
+
const onAbort = () => {
|
|
86
|
+
if (this.host.activeJob() !== response)
|
|
87
|
+
return;
|
|
88
|
+
const reason = signal.reason;
|
|
89
|
+
const error = reason instanceof Error
|
|
90
|
+
? reason
|
|
91
|
+
: new Error(typeof reason === "string" ? `Aborted: ${reason}` : "Aborted");
|
|
92
|
+
try {
|
|
93
|
+
response.failed(error);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
void this.cancel().catch((error) => this.host.logger().error("Pocket abort cancellation failed", error));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
100
|
+
const detach = () => signal.removeEventListener("abort", onAbort);
|
|
101
|
+
void response.await().then(detach, detach);
|
|
102
|
+
}
|
|
103
|
+
/** Stops native work and waits for the job in flight to report its end. */
|
|
104
|
+
async cancel() {
|
|
105
|
+
if (this.cancelPromise)
|
|
106
|
+
return this.cancelPromise;
|
|
107
|
+
const addon = this.host.optionalAddon();
|
|
108
|
+
const pending = this.jobPending;
|
|
109
|
+
this.cancelPromise = (async () => {
|
|
110
|
+
this.host.failStream("Synthesis cancelled");
|
|
111
|
+
if (addon?.cancel)
|
|
112
|
+
await addon.cancel();
|
|
113
|
+
// Joining the worker can precede delivery of its terminal callback.
|
|
114
|
+
if (pending)
|
|
115
|
+
await pending.promise;
|
|
116
|
+
})();
|
|
117
|
+
try {
|
|
118
|
+
await this.cancelPromise;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
this.cancelPromise = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Replaces the native instance with one built from the merged options.
|
|
126
|
+
* Native work is drained and the old model released before the replacement
|
|
127
|
+
* is allocated; a failed activation leaves the instance unloaded with its
|
|
128
|
+
* last good configuration.
|
|
129
|
+
*/
|
|
130
|
+
async reload(newConfig) {
|
|
131
|
+
await this.waitCancel();
|
|
132
|
+
this.checkReload();
|
|
133
|
+
const { setup, state } = this.host;
|
|
134
|
+
if (state.destroyed)
|
|
135
|
+
throw new Error("Pocket instance was destroyed");
|
|
136
|
+
const pocket = setup.pocket;
|
|
137
|
+
if (!pocket)
|
|
138
|
+
throw new Error("Pocket engine is not configured");
|
|
139
|
+
const options = { ...pocket.options, ...newConfig };
|
|
140
|
+
const config = { ...setup.config, ...newConfig };
|
|
141
|
+
const params = (0, pocketConfig_1.buildPocketParams)(pocket.files, options, config);
|
|
142
|
+
this.inProgress = true;
|
|
143
|
+
let replacement = null;
|
|
144
|
+
try {
|
|
145
|
+
await this.host.unloadModel();
|
|
146
|
+
replacement = this.host.createAddon(params);
|
|
147
|
+
await replacement.activate();
|
|
148
|
+
this.host.setAddon(replacement);
|
|
149
|
+
replacement = null;
|
|
150
|
+
setup.pocket = { files: pocket.files, options, params };
|
|
151
|
+
setup.config.language = "en";
|
|
152
|
+
setup.config.useGPU = false;
|
|
153
|
+
setup.outputSampleRate =
|
|
154
|
+
typeof params.outputSampleRate === "number" ? params.outputSampleRate : null;
|
|
155
|
+
setup.config.outputSampleRate = setup.outputSampleRate ?? undefined;
|
|
156
|
+
state.configLoaded = true;
|
|
157
|
+
state.weightsLoaded = true;
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
try {
|
|
161
|
+
if (replacement)
|
|
162
|
+
await replacement.destroyInstance();
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
this.inProgress = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
exports.PocketLifecycle = PocketLifecycle;
|
package/lib/reload.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reload()` configuration: which settings a reload may rewrite, applied
|
|
3
|
+
* against a snapshot so a rejected reload leaves the instance untouched.
|
|
4
|
+
*/
|
|
5
|
+
import type { TTSConfigurationParams } from "../tts";
|
|
6
|
+
import type { EngineSetup } from "./engineSetup";
|
|
7
|
+
/**
|
|
8
|
+
* Apply the new configuration and build the native parameters from it. A
|
|
9
|
+
* rejected value leaves the instance exactly as it was, so a later partial
|
|
10
|
+
* reload is not validated against state the caller never accepted.
|
|
11
|
+
*/
|
|
12
|
+
export declare function applyReloadableConfig(setup: EngineSetup, newConfig: Record<string, unknown>): TTSConfigurationParams;
|
package/lib/reload.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyReloadableConfig = applyReloadableConfig;
|
|
4
|
+
const conditioning_1 = require("./conditioning");
|
|
5
|
+
const engineParams_1 = require("./engineParams");
|
|
6
|
+
const audio8_1 = require("./engines/audio8");
|
|
7
|
+
const moss_1 = require("./engines/moss");
|
|
8
|
+
const parler_1 = require("./engines/parler");
|
|
9
|
+
const setup_1 = require("./setup");
|
|
10
|
+
function captureReloadableState(setup) {
|
|
11
|
+
return {
|
|
12
|
+
language: setup.config.language,
|
|
13
|
+
useGPU: setup.config.useGPU,
|
|
14
|
+
outputSampleRate: setup.outputSampleRate,
|
|
15
|
+
settings: { ...setup.settings },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function restoreReloadableState(setup, state) {
|
|
19
|
+
setup.config.language = state.language;
|
|
20
|
+
setup.config.useGPU = state.useGPU;
|
|
21
|
+
setup.outputSampleRate = state.outputSampleRate;
|
|
22
|
+
const live = setup.settings;
|
|
23
|
+
const snapshot = state.settings;
|
|
24
|
+
for (const key of new Set([...Object.keys(live), ...Object.keys(snapshot)])) {
|
|
25
|
+
live[key] = snapshot[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function applyReloadableRuntimeConfig(setup, newConfig) {
|
|
29
|
+
(0, moss_1.assertMossReloadKeepsVoice)(setup, newConfig);
|
|
30
|
+
const runtimeConfig = newConfig;
|
|
31
|
+
if (runtimeConfig.language !== undefined) {
|
|
32
|
+
setup.config.language = runtimeConfig.language;
|
|
33
|
+
}
|
|
34
|
+
if (runtimeConfig.useGPU !== undefined) {
|
|
35
|
+
setup.config.useGPU = runtimeConfig.useGPU;
|
|
36
|
+
}
|
|
37
|
+
if (runtimeConfig.outputSampleRate !== undefined) {
|
|
38
|
+
setup.outputSampleRate = (0, setup_1.validateOutputSampleRate)(runtimeConfig.outputSampleRate);
|
|
39
|
+
}
|
|
40
|
+
(0, audio8_1.applyAudio8Reload)(setup, newConfig);
|
|
41
|
+
(0, moss_1.applyMossReload)(setup, newConfig);
|
|
42
|
+
}
|
|
43
|
+
// Cross-engine conditioning is reloadable on every engine that supports it,
|
|
44
|
+
// so both families change emotion the same way.
|
|
45
|
+
function applyReloadableConditioning(setup, newConfig) {
|
|
46
|
+
const conditioning = newConfig;
|
|
47
|
+
if (conditioning.emotion === undefined &&
|
|
48
|
+
conditioning.pace === undefined) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (conditioning.emotion !== undefined) {
|
|
52
|
+
setup.settings.emotion = conditioning.emotion;
|
|
53
|
+
}
|
|
54
|
+
if (conditioning.pace !== undefined) {
|
|
55
|
+
setup.settings.pace = conditioning.pace;
|
|
56
|
+
}
|
|
57
|
+
(0, conditioning_1.assertConditioningConsistency)(setup, "reload");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Apply the new configuration and build the native parameters from it. A
|
|
61
|
+
* rejected value leaves the instance exactly as it was, so a later partial
|
|
62
|
+
* reload is not validated against state the caller never accepted.
|
|
63
|
+
*/
|
|
64
|
+
function applyReloadableConfig(setup, newConfig) {
|
|
65
|
+
const snapshot = captureReloadableState(setup);
|
|
66
|
+
try {
|
|
67
|
+
applyReloadableRuntimeConfig(setup, newConfig);
|
|
68
|
+
applyReloadableConditioning(setup, newConfig);
|
|
69
|
+
(0, parler_1.applyParlerReload)(setup, newConfig);
|
|
70
|
+
return (0, engineParams_1.buildNativeParams)(setup);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
restoreReloadableState(setup, snapshot);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { QvacResponse } from "@qvac/infer-base";
|
|
2
|
+
import type { TTSJobData } from "../tts";
|
|
3
|
+
import type { JobFields } from "./jobFields";
|
|
4
|
+
import type { NativeOutputChunk, NativeStats, RunStreamingOptions, RuntimeStats, SentenceStreamChunkMeta, SentenceStreamOptions, TextStreamInput, TTSOutputChunk } from "./types";
|
|
5
|
+
export type SentenceStreamResponse = QvacResponse<TTSOutputChunk & SentenceStreamChunkMeta>;
|
|
6
|
+
export interface ChunkResolver {
|
|
7
|
+
resolve(): void;
|
|
8
|
+
reject(error: unknown): void;
|
|
9
|
+
}
|
|
10
|
+
export interface StreamAccumulator {
|
|
11
|
+
firstAudioMs?: number;
|
|
12
|
+
totalTime: number;
|
|
13
|
+
audioDurationMs: number;
|
|
14
|
+
totalSamples: number;
|
|
15
|
+
generatedFrames: number;
|
|
16
|
+
codecSidecarLoaded?: number;
|
|
17
|
+
codecOnCoreml?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SentenceStreamContext {
|
|
20
|
+
textStreamMode?: boolean;
|
|
21
|
+
asyncTextSource?: AsyncIterable<string>;
|
|
22
|
+
chunks: string[];
|
|
23
|
+
chunkIdx: number;
|
|
24
|
+
acc: StreamAccumulator;
|
|
25
|
+
chunkResolver: ChunkResolver | null;
|
|
26
|
+
jobFields?: JobFields;
|
|
27
|
+
}
|
|
28
|
+
export declare function normalizeError(error: unknown): string | Error;
|
|
29
|
+
export declare function ttsOutputDebugString(data: unknown): string;
|
|
30
|
+
export declare function isAudioOutputEvent(data: unknown): data is NativeOutputChunk;
|
|
31
|
+
export declare function isStatsEvent(data: unknown): data is NativeStats;
|
|
32
|
+
/**
|
|
33
|
+
* What `tokensPerSecond` counts. Audio8 paces on the codec frame grid and
|
|
34
|
+
* reports frames per second in batch synthesis; the text-paced engines report
|
|
35
|
+
* characters per second. Streaming has to keep the same unit as `run()` or the
|
|
36
|
+
* number silently changes meaning between the two entry points.
|
|
37
|
+
*/
|
|
38
|
+
export declare function emptyStreamAccumulator(): StreamAccumulator;
|
|
39
|
+
export declare function computeSentenceStreamStats(chunks: string[], accumulator: StreamAccumulator, countsFrames: boolean): RuntimeStats;
|
|
40
|
+
export declare function resolveRunStreamingOptions(textStream: TextStreamInput, options: RunStreamingOptions): Required<Pick<RunStreamingOptions, "accumulateSentences" | "sentenceDelimiterPreset" | "flushAfterMs">> & Pick<RunStreamingOptions, "maxBufferScalars" | "sentenceDelimiter">;
|
|
41
|
+
export declare function normalizeTextStream(textStream: TextStreamInput): AsyncIterable<string>;
|
|
42
|
+
/** `run({ streamOutput: true })` splits its text, so it needs a non-empty string. */
|
|
43
|
+
export declare function requireStreamOutputText(input: unknown): string;
|
|
44
|
+
/** The sentence chunks of `text`; an input that yields none is an error. */
|
|
45
|
+
export declare function splitSentenceChunks(text: string, options: SentenceStreamOptions, language: string | undefined): string[];
|
|
46
|
+
/** What the stream driver needs from the wrapper that owns the job handler. */
|
|
47
|
+
export interface SentenceStreamHost {
|
|
48
|
+
/** Opens the response the whole stream reports through. */
|
|
49
|
+
startJob(): SentenceStreamResponse;
|
|
50
|
+
dispatchJob(job: TTSJobData): Promise<void>;
|
|
51
|
+
endJob(stats: RuntimeStats | NativeStats): void;
|
|
52
|
+
failJob(error: string | Error): void;
|
|
53
|
+
/**
|
|
54
|
+
* Audio8 and MOSS report `tokensPerSecond` as codec frames per second, so
|
|
55
|
+
* the streaming aggregate has to count frames too rather than characters.
|
|
56
|
+
*/
|
|
57
|
+
countsFrames(): boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Drives one sentence stream at a time. The wrapper routes native output and
|
|
61
|
+
* stats events here while a context is active; a newer stream, a cancel or a
|
|
62
|
+
* reload replaces or clears the context, and the driver of the old stream
|
|
63
|
+
* notices and stops.
|
|
64
|
+
*/
|
|
65
|
+
export declare class SentenceStreamer {
|
|
66
|
+
private readonly host;
|
|
67
|
+
/** The active stream, or null between streams. */
|
|
68
|
+
ctx: SentenceStreamContext | null;
|
|
69
|
+
constructor(host: SentenceStreamHost);
|
|
70
|
+
streamChunks(chunks: string[], jobFields?: JobFields): SentenceStreamResponse;
|
|
71
|
+
streamText(source: AsyncIterable<string>, jobFields?: JobFields): SentenceStreamResponse;
|
|
72
|
+
rejectActiveChunk(error: unknown): void;
|
|
73
|
+
fail(reason: string | Error): void;
|
|
74
|
+
enrich(data: NativeOutputChunk): TTSOutputChunk & SentenceStreamChunkMeta;
|
|
75
|
+
/**
|
|
76
|
+
* Folds a chunk's stats into the aggregate and releases the chunk; after the
|
|
77
|
+
* last pre-chunked sentence, ends the job. Returns false when no stream is
|
|
78
|
+
* active, so the caller can end a plain job with the stats as they are.
|
|
79
|
+
*/
|
|
80
|
+
onStats(data: NativeStats): boolean;
|
|
81
|
+
private abort;
|
|
82
|
+
private driveChunks;
|
|
83
|
+
private driveText;
|
|
84
|
+
private synthesizeChunk;
|
|
85
|
+
}
|