@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
package/lib/fit.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings every voice takes, spelled as `createInstance` takes them. A fit
|
|
3
|
+
* request is a load config plus the workload below, so the projection reads
|
|
4
|
+
* the same keys and applies the same defaults as the load it describes.
|
|
5
|
+
*/
|
|
6
|
+
interface TtsFitCommon {
|
|
7
|
+
/** Layers to offload. Unset defers to `useGPU`. */
|
|
8
|
+
nGpuLayers?: number;
|
|
9
|
+
/** All layers or none, when `nGpuLayers` is unset. */
|
|
10
|
+
useGPU?: boolean;
|
|
11
|
+
vulkanDevice?: number;
|
|
12
|
+
backendsDir?: string;
|
|
13
|
+
/** LavaSR enhancer; a non-empty path turns enhancement on. */
|
|
14
|
+
lavasrEnhancerPath?: string;
|
|
15
|
+
/** LavaSR denoiser, applied before the enhancer. */
|
|
16
|
+
lavasrDenoiserPath?: string;
|
|
17
|
+
/** Free memory that must remain for the projection to count as fitting. Defaults to 256 MiB. */
|
|
18
|
+
marginBytes?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface SupertonicFitRequest extends TtsFitCommon {
|
|
21
|
+
engineType: 'supertonic';
|
|
22
|
+
supertonicModelPath: string;
|
|
23
|
+
/** `auto` | `f32` | `f16` | `q8_0`; changes the weight footprint. */
|
|
24
|
+
precision?: 'auto' | 'f32' | 'f16' | 'q8_0';
|
|
25
|
+
/** -1 auto, 0 off, 1 on. */
|
|
26
|
+
f16Weights?: number;
|
|
27
|
+
/** 0 takes the GGUF's own default. */
|
|
28
|
+
steps?: number;
|
|
29
|
+
textTokens?: number;
|
|
30
|
+
audioSeconds?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface ParlerFitRequest extends TtsFitCommon {
|
|
33
|
+
engineType: 'parler';
|
|
34
|
+
parlerModelPath: string;
|
|
35
|
+
descriptionTokens?: number;
|
|
36
|
+
promptTokens?: number;
|
|
37
|
+
/** 0 takes the checkpoint's default generation length. */
|
|
38
|
+
maxFrames?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ChatterboxFitRequest extends TtsFitCommon {
|
|
41
|
+
engineType: 'chatterbox';
|
|
42
|
+
t3ModelPath: string;
|
|
43
|
+
s3genModelPath: string;
|
|
44
|
+
nCtx?: number;
|
|
45
|
+
kvCacheType?: 'f32' | 'f16' | 'q8_0';
|
|
46
|
+
textTokens?: number;
|
|
47
|
+
predictTokens?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface Audio8FitRequest extends TtsFitCommon {
|
|
50
|
+
engineType: 'audio8';
|
|
51
|
+
audio8LmPath: string;
|
|
52
|
+
audio8CodecDecoderPath: string;
|
|
53
|
+
/** Supplying it projects voice cloning, which the decoder alone cannot do. */
|
|
54
|
+
audio8CodecEncoderPath?: string;
|
|
55
|
+
promptTokens?: number;
|
|
56
|
+
maxFrames?: number;
|
|
57
|
+
referenceSeconds?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface CosyvoiceFitRequest extends TtsFitCommon {
|
|
60
|
+
engineType: 'cosyvoice3';
|
|
61
|
+
cosyvoiceLlmModelPath: string;
|
|
62
|
+
cosyvoiceFlowModelPath: string;
|
|
63
|
+
cosyvoiceHiftModelPath: string;
|
|
64
|
+
cosyvoiceVoiceModelPath: string;
|
|
65
|
+
textTokens?: number;
|
|
66
|
+
/** 0 derives the runtime cap from the text length. */
|
|
67
|
+
speechTokens?: number;
|
|
68
|
+
}
|
|
69
|
+
export interface MossFitRequest extends TtsFitCommon {
|
|
70
|
+
engineType: 'moss';
|
|
71
|
+
mossBackbonePath: string;
|
|
72
|
+
mossCodecDecoderPath?: string;
|
|
73
|
+
mossCodecEncoderPath?: string;
|
|
74
|
+
}
|
|
75
|
+
export type TtsFitRequest = SupertonicFitRequest | ParlerFitRequest | ChatterboxFitRequest | Audio8FitRequest | CosyvoiceFitRequest | MossFitRequest;
|
|
76
|
+
/** The voice engines a fit can be asked for, read off the request union. */
|
|
77
|
+
export type TtsFitEngine = TtsFitRequest['engineType'];
|
|
78
|
+
export type TtsFitStatus = 'fits' | 'does-not-fit' | 'error';
|
|
79
|
+
export interface TtsFitResult {
|
|
80
|
+
status: TtsFitStatus;
|
|
81
|
+
/**
|
|
82
|
+
* The engine's own wording, e.g. `model-unreadable`, `workload-too-large`.
|
|
83
|
+
* `unsupported-engine` names a voice the addon loads and the fitter does
|
|
84
|
+
* not cover.
|
|
85
|
+
*/
|
|
86
|
+
reason: string;
|
|
87
|
+
/** Which pipeline was projected, e.g. `chatterbox-t3-turbo`. */
|
|
88
|
+
modelVariant: string;
|
|
89
|
+
deviceName: string;
|
|
90
|
+
deviceIsCpu: boolean;
|
|
91
|
+
/** The device pool is system RAM, so host bytes compete with device bytes. */
|
|
92
|
+
deviceSharesHostMemory: boolean;
|
|
93
|
+
deviceFreeBytes: number;
|
|
94
|
+
deviceTotalBytes: number;
|
|
95
|
+
deviceBytes: number;
|
|
96
|
+
weightsBytes: number;
|
|
97
|
+
stateBytes: number;
|
|
98
|
+
/** 0 for a pipeline with no language-model stage, such as supertonic. */
|
|
99
|
+
lmComputeBytes: number;
|
|
100
|
+
/** 0 for a pipeline with no separate codec stage. */
|
|
101
|
+
codecComputeBytes: number;
|
|
102
|
+
hostBytes: number;
|
|
103
|
+
/**
|
|
104
|
+
* The LavaSR stages' size on disk, 0 when none was named. They have no
|
|
105
|
+
* fitter, so this is a size on disk. It is counted in neither `deviceBytes`
|
|
106
|
+
* nor `hostBytes`.
|
|
107
|
+
*/
|
|
108
|
+
lavasrFileBytes: number;
|
|
109
|
+
report: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Projects one voice against the memory free right now, reading GGUF metadata
|
|
113
|
+
* and never weight data. The registry's weightless copy of each file answers
|
|
114
|
+
* the same as the file itself, so this can run before anything is downloaded.
|
|
115
|
+
*
|
|
116
|
+
* The request is a `createInstance` config plus the workload, and the engine's
|
|
117
|
+
* own config builder reads it, so a load that runs and a fit that describes it
|
|
118
|
+
* resolve their settings the same way.
|
|
119
|
+
*
|
|
120
|
+
* `engineType` picks the fitter, the same key a load uses, and is required
|
|
121
|
+
* here: a fit request carries none of the file keys a load is inferred from.
|
|
122
|
+
* A model the engine cannot read comes back as `status: "error"`, as does a
|
|
123
|
+
* voice with no fitter; a broken request, or a host with no native binding,
|
|
124
|
+
* throws.
|
|
125
|
+
*/
|
|
126
|
+
export declare function assessFit(request: TtsFitRequest): TtsFitResult;
|
|
127
|
+
export {};
|
package/lib/fit.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assessFit = assessFit;
|
|
4
|
+
const backends_1 = require("./backends");
|
|
5
|
+
/**
|
|
6
|
+
* Projects one voice against the memory free right now, reading GGUF metadata
|
|
7
|
+
* and never weight data. The registry's weightless copy of each file answers
|
|
8
|
+
* the same as the file itself, so this can run before anything is downloaded.
|
|
9
|
+
*
|
|
10
|
+
* The request is a `createInstance` config plus the workload, and the engine's
|
|
11
|
+
* own config builder reads it, so a load that runs and a fit that describes it
|
|
12
|
+
* resolve their settings the same way.
|
|
13
|
+
*
|
|
14
|
+
* `engineType` picks the fitter, the same key a load uses, and is required
|
|
15
|
+
* here: a fit request carries none of the file keys a load is inferred from.
|
|
16
|
+
* A model the engine cannot read comes back as `status: "error"`, as does a
|
|
17
|
+
* voice with no fitter; a broken request, or a host with no native binding,
|
|
18
|
+
* throws.
|
|
19
|
+
*/
|
|
20
|
+
function assessFit(request) {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- native binding is resolved lazily from package prebuilds.
|
|
22
|
+
const binding = require('../binding.js');
|
|
23
|
+
return binding.assessFit({
|
|
24
|
+
...request,
|
|
25
|
+
backendsDir: typeof request.backendsDir === 'string' && request.backendsDir.length > 0
|
|
26
|
+
? request.backendsDir
|
|
27
|
+
: (0, backends_1.resolveBackendsDir)()
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TTSConfigurationParams } from '../tts';
|
|
2
|
+
interface PocketFiles {
|
|
3
|
+
modelDir?: string;
|
|
4
|
+
pocketFlowModel?: string;
|
|
5
|
+
pocketMimiModel?: string;
|
|
6
|
+
pocketFrontend?: string;
|
|
7
|
+
pocketVoice?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildPocketParams(files: PocketFiles, optionInput: object, configInput: object): TTSConfigurationParams;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildPocketParams = buildPocketParams;
|
|
7
|
+
const bare_path_1 = __importDefault(require("bare-path"));
|
|
8
|
+
const DEFAULT_SAMPLING_STEPS = 1;
|
|
9
|
+
const UNSUPPORTED_OPTIONS = [
|
|
10
|
+
'voice',
|
|
11
|
+
'voiceName',
|
|
12
|
+
'voiceDir',
|
|
13
|
+
'speed',
|
|
14
|
+
'noiseNpyPath',
|
|
15
|
+
'kvCacheType',
|
|
16
|
+
'streamChunkTokens',
|
|
17
|
+
'streamFirstChunkTokens',
|
|
18
|
+
'cfmSteps',
|
|
19
|
+
'cfgRate',
|
|
20
|
+
'streamLeftContextTokens',
|
|
21
|
+
'promptText',
|
|
22
|
+
'referenceText',
|
|
23
|
+
'instruct',
|
|
24
|
+
'description',
|
|
25
|
+
'voiceDescription',
|
|
26
|
+
'emotion',
|
|
27
|
+
'pace',
|
|
28
|
+
'pitch',
|
|
29
|
+
'expressivity',
|
|
30
|
+
'noise',
|
|
31
|
+
'reverb',
|
|
32
|
+
'quality',
|
|
33
|
+
'greedy',
|
|
34
|
+
'topK',
|
|
35
|
+
'topP',
|
|
36
|
+
'maxFrames',
|
|
37
|
+
'minNewTokens',
|
|
38
|
+
'normalizeNumbers',
|
|
39
|
+
'enhancer',
|
|
40
|
+
'denoiser'
|
|
41
|
+
];
|
|
42
|
+
const NUMERIC_OPTIONS = {
|
|
43
|
+
threads: [1, 1024],
|
|
44
|
+
nCtx: [1, 8192],
|
|
45
|
+
maxTokens: [1, 1024],
|
|
46
|
+
steps: [1, 64],
|
|
47
|
+
framesAfterEos: [-1, 100],
|
|
48
|
+
seed: [0, 4294967295],
|
|
49
|
+
temperature: [0, 10, false],
|
|
50
|
+
noiseClamp: [0, 3.402823466e38, false],
|
|
51
|
+
eosThreshold: [-3.402823466e38, 3.402823466e38, false],
|
|
52
|
+
outputSampleRate: [8000, 192000]
|
|
53
|
+
};
|
|
54
|
+
function resolveAsset(root, value, name) {
|
|
55
|
+
return value || (root ? bare_path_1.default.join(root, name) : '');
|
|
56
|
+
}
|
|
57
|
+
function resolveModelFiles(files, options) {
|
|
58
|
+
return {
|
|
59
|
+
engineType: 'pocket',
|
|
60
|
+
pocketFlowModelPath: resolveAsset(files.modelDir, files.pocketFlowModel, 'flow-lm.gguf'),
|
|
61
|
+
pocketMimiModelPath: resolveAsset(files.modelDir, files.pocketMimiModel, 'mimi.gguf'),
|
|
62
|
+
pocketFrontendPath: resolveAsset(files.modelDir, files.pocketFrontend, 'frontend.json'),
|
|
63
|
+
pocketVoicePath: files.pocketVoice ||
|
|
64
|
+
(options.referenceAudio ? '' : resolveAsset(files.modelDir, undefined, 'voice.gguf')),
|
|
65
|
+
referenceAudio: typeof options.referenceAudio === 'string' ? options.referenceAudio : '',
|
|
66
|
+
language: 'en',
|
|
67
|
+
useGPU: false,
|
|
68
|
+
steps: DEFAULT_SAMPLING_STEPS
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function validateEngineOptions(options, config) {
|
|
72
|
+
if (options.referenceAudio !== undefined && typeof options.referenceAudio !== 'string')
|
|
73
|
+
throw new Error('Pocket referenceAudio must be a path string');
|
|
74
|
+
if (config.language !== undefined && config.language !== 'en')
|
|
75
|
+
throw new Error('Pocket currently supports English (en) only');
|
|
76
|
+
if (config.useGPU !== undefined && config.useGPU !== false)
|
|
77
|
+
throw new Error('Pocket currently supports CPU only (useGPU: false)');
|
|
78
|
+
if (options.nGpuLayers !== undefined && options.nGpuLayers !== 0)
|
|
79
|
+
throw new Error('Pocket requires nGpuLayers: 0');
|
|
80
|
+
if (options.steps !== undefined &&
|
|
81
|
+
options.numInferenceSteps !== undefined &&
|
|
82
|
+
options.steps !== options.numInferenceSteps)
|
|
83
|
+
throw new Error('Pocket steps conflicts with numInferenceSteps');
|
|
84
|
+
}
|
|
85
|
+
function rejectUnsupportedOptions(options) {
|
|
86
|
+
for (const key of UNSUPPORTED_OPTIONS) {
|
|
87
|
+
if (options[key] !== undefined)
|
|
88
|
+
throw new Error('Pocket does not support ' + key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function numericOptionValue(key, options, config) {
|
|
92
|
+
if (key === 'outputSampleRate')
|
|
93
|
+
return config.outputSampleRate;
|
|
94
|
+
if (key === 'steps')
|
|
95
|
+
return options.steps === undefined ? options.numInferenceSteps : options.steps;
|
|
96
|
+
return options[key];
|
|
97
|
+
}
|
|
98
|
+
function validateNumericOption(key, value, min, max, integer) {
|
|
99
|
+
if (typeof value !== 'number' ||
|
|
100
|
+
!Number.isFinite(value) ||
|
|
101
|
+
value < min ||
|
|
102
|
+
value > max ||
|
|
103
|
+
(integer && !Number.isInteger(value))) {
|
|
104
|
+
throw new Error('Invalid Pocket ' +
|
|
105
|
+
key +
|
|
106
|
+
': expected ' +
|
|
107
|
+
(integer ? 'an integer' : 'a number') +
|
|
108
|
+
' between ' +
|
|
109
|
+
min +
|
|
110
|
+
' and ' +
|
|
111
|
+
max);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function applyNumericOptions(params, options, config) {
|
|
116
|
+
for (const [key, [min, max, integer = true]] of Object.entries(NUMERIC_OPTIONS)) {
|
|
117
|
+
const value = numericOptionValue(key, options, config);
|
|
118
|
+
if (value !== undefined)
|
|
119
|
+
params[key] = validateNumericOption(key, value, min, max, integer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function validateRequiredFiles(params) {
|
|
123
|
+
for (const key of ['pocketFlowModelPath', 'pocketMimiModelPath', 'pocketFrontendPath']) {
|
|
124
|
+
if (typeof params[key] !== 'string' || !params[key])
|
|
125
|
+
throw new Error('Pocket requires ' + key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function validateVoiceConditioning(params) {
|
|
129
|
+
if (typeof params.pocketVoicePath !== 'string' ||
|
|
130
|
+
typeof params.referenceAudio !== 'string' ||
|
|
131
|
+
!!params.pocketVoicePath === !!params.referenceAudio) {
|
|
132
|
+
throw new Error('Pocket requires exactly one prepared voice file or referenceAudio WAV');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function buildPocketParams(files, optionInput, configInput) {
|
|
136
|
+
const options = optionInput;
|
|
137
|
+
const config = configInput;
|
|
138
|
+
validateEngineOptions(options, config);
|
|
139
|
+
rejectUnsupportedOptions(options);
|
|
140
|
+
const params = resolveModelFiles(files, options);
|
|
141
|
+
applyNumericOptions(params, options, config);
|
|
142
|
+
validateRequiredFiles(params);
|
|
143
|
+
validateVoiceConditioning(params);
|
|
144
|
+
return params;
|
|
145
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qvac/tts-ggml",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Text to Speech (TTS) addon for qvac (ggml backend, wrapping the chatterbox + supertonic + parler + cosyvoice3 + audio8 engines from tts-cpp)",
|
|
3
|
+
"version": "0.10.1",
|
|
4
|
+
"description": "Text to Speech (TTS) addon for qvac (ggml backend, wrapping the chatterbox + supertonic + parler + cosyvoice3 + audio8 + moss + Pocket engines from tts-cpp)",
|
|
5
5
|
"addon": true,
|
|
6
6
|
"engines": {
|
|
7
7
|
"bare": ">=1.19.0"
|
|
@@ -73,11 +73,13 @@
|
|
|
73
73
|
"example:supertonic-mtl": "bare examples/supertonic-mtl-tts.js \"Hello from the multilingual Supertonic engine.\"",
|
|
74
74
|
"example:supertonic-mtl-sweep": "bare examples/supertonic-mtl-sweep-tts.js",
|
|
75
75
|
"example:supertonic-sentence-stream": "bare examples/supertonic-sentence-stream-tts.js",
|
|
76
|
+
"example:supertonic-chunk-stream": "bare examples/supertonic-chunk-stream-tts.js",
|
|
76
77
|
"example:cosyvoice": "bare examples/cosyvoice-tts.js \"Hello from CosyVoice three.\"",
|
|
77
78
|
"example:cosyvoice-enhanced": "bare examples/cosyvoice-enhanced.js \"Hello from CosyVoice three.\"",
|
|
78
79
|
"example:parler": "bare examples/parler-tts.js \"Hey, how are you doing today?\"",
|
|
79
80
|
"example:parler-enhanced": "bare examples/parler-enhanced.js \"Hello from enhanced Parler.\"",
|
|
80
81
|
"example:audio8": "bare examples/audio8-tts.js \"Audio eight speaks on-device.\"",
|
|
82
|
+
"example:moss": "bare examples/moss-tts.js \"MOSS speaks on-device.\"",
|
|
81
83
|
"lint": "npm run lint:js && npm run lint:ts && npm run test:types",
|
|
82
84
|
"lint:js": "prettier --check \"test/**/*.js\" \"scripts/__tests__/package-contract.test.js\" \"scripts/__tests__/binding-loader.test.js\" \"binding.js\" \"addon-unavailable.js\" && lunte \"test/**/*.js\" \"scripts/__tests__/package-contract.test.js\" \"scripts/__tests__/binding-loader.test.js\" \"binding.js\" \"addon-unavailable.js\"",
|
|
83
85
|
"lint:ts": "eslint \"src/**/*.ts\" --max-warnings=0",
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
"test:node": "node --test scripts/__tests__/*.test.js",
|
|
88
90
|
"test": "npm run test:types && npm run test:unit && npm run test:integration",
|
|
89
91
|
"test:integration": "npm run build:ts && brittle-bare test/integration/**/*.test.js",
|
|
92
|
+
"test:integration:smoke": "npm run build:ts && brittle-bare test/integration/supertonic.test.js",
|
|
90
93
|
"test:benchmark:rtf": "npm run build:ts && brittle-bare test/benchmark/rtf-benchmark.test.js",
|
|
91
94
|
"test:benchmark:rtf:matrix": "node scripts/run-rtf-benchmark-matrix.js",
|
|
92
95
|
"test:benchmark:streaming": "npm run build:ts && brittle-bare test/benchmark/streaming-benchmark.test.js",
|
|
@@ -104,7 +107,10 @@
|
|
|
104
107
|
"setup:venv": "bash scripts/setup-venv.sh",
|
|
105
108
|
"convert-models": "bash scripts/convert-models.sh",
|
|
106
109
|
"setup-models": "bash scripts/setup-venv.sh && bash scripts/convert-models.sh",
|
|
107
|
-
"download-models:registry": "node scripts/download-tts-ggml-models.js"
|
|
110
|
+
"download-models:registry": "node scripts/download-tts-ggml-models.js",
|
|
111
|
+
"download-models:smoke": "node scripts/download-tts-ggml-models.js --group supertonic",
|
|
112
|
+
"example:pocket": "bare examples/pocket-tts.js",
|
|
113
|
+
"test:pocket": "npm run build:ts && brittle-bare test/integration/pocket.integration.test.js"
|
|
108
114
|
},
|
|
109
115
|
"files": [
|
|
110
116
|
"addonLogging.js",
|
|
@@ -134,7 +140,8 @@
|
|
|
134
140
|
"test/utils/textMetrics.js",
|
|
135
141
|
"test/data/sentences-medium.js",
|
|
136
142
|
"test/data/sentences-long.js",
|
|
137
|
-
"LICENSE"
|
|
143
|
+
"LICENSE",
|
|
144
|
+
"docs"
|
|
138
145
|
],
|
|
139
146
|
"exports": {
|
|
140
147
|
"./package": "./package.json",
|
|
@@ -209,10 +216,10 @@
|
|
|
209
216
|
},
|
|
210
217
|
"types": "index.d.ts",
|
|
211
218
|
"optionalDependencies": {
|
|
212
|
-
"@qvac/tts-ggml-linux-x64": "0.
|
|
213
|
-
"@qvac/tts-ggml-linux-arm64": "0.
|
|
214
|
-
"@qvac/tts-ggml-darwin-arm64": "0.
|
|
215
|
-
"@qvac/tts-ggml-darwin-x64": "0.
|
|
216
|
-
"@qvac/tts-ggml-win32-x64": "0.
|
|
219
|
+
"@qvac/tts-ggml-linux-x64": "0.10.1",
|
|
220
|
+
"@qvac/tts-ggml-linux-arm64": "0.10.1",
|
|
221
|
+
"@qvac/tts-ggml-darwin-arm64": "0.10.1",
|
|
222
|
+
"@qvac/tts-ggml-darwin-x64": "0.10.1",
|
|
223
|
+
"@qvac/tts-ggml-win32-x64": "0.10.1"
|
|
217
224
|
}
|
|
218
225
|
}
|
|
@@ -15,11 +15,12 @@ const {
|
|
|
15
15
|
const { ensureChatterboxModels, ensureWhisperModel } = require('../utils/downloadModel')
|
|
16
16
|
const { loadWhisper, runWhisper } = require('../utils/runWhisper')
|
|
17
17
|
const { recordTtsStats } = require('../utils/perf-helper')
|
|
18
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
19
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
18
20
|
|
|
19
21
|
const platform = os.platform()
|
|
20
22
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
21
23
|
const isDarwin = platform === 'darwin'
|
|
22
|
-
const forceNoGpu = os.getEnv('NO_GPU') === 'true'
|
|
23
24
|
|
|
24
25
|
const INPUT_SENTENCES = (isMobile ? 'short' : os.getEnv('INPUT_SENTENCES')) || 'short'
|
|
25
26
|
const useSplit = INPUT_SENTENCES !== 'short'
|
|
@@ -41,7 +42,7 @@ function getEnglishSentences() {
|
|
|
41
42
|
|
|
42
43
|
test(
|
|
43
44
|
'Chatterbox TTS (ggml): English synthesis + optional WER verification',
|
|
44
|
-
{ timeout: 1800000 },
|
|
45
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
45
46
|
async (t) => {
|
|
46
47
|
const baseDir = getBaseDir()
|
|
47
48
|
const modelsDir = path.join(baseDir, 'models')
|
|
@@ -83,8 +84,10 @@ test(
|
|
|
83
84
|
`\n=== English synthesis (${englishSentences.length} sentences, tier: ${INPUT_SENTENCES}, modelDir: ${resolvedModelDir}) ===`
|
|
84
85
|
)
|
|
85
86
|
const model = await loadChatterboxTTS({
|
|
87
|
+
threads: TTS_TEST_THREADS,
|
|
86
88
|
modelDir: resolvedModelDir,
|
|
87
|
-
language: 'en'
|
|
89
|
+
language: 'en',
|
|
90
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
88
91
|
})
|
|
89
92
|
t.ok(model, 'Chatterbox (ggml) model should be loaded')
|
|
90
93
|
|
|
@@ -250,8 +253,9 @@ test(
|
|
|
250
253
|
// empty/undefined value flows through to the engine cleanly.
|
|
251
254
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
252
255
|
const model = new TTSGgml({
|
|
256
|
+
threads: TTS_TEST_THREADS,
|
|
253
257
|
files: { modelDir: download.targetDir },
|
|
254
|
-
config: { language: 'en', ...(
|
|
258
|
+
config: { language: 'en', ...(NO_GPU ? { useGPU: false } : {}) },
|
|
255
259
|
opts: { stats: true }
|
|
256
260
|
})
|
|
257
261
|
|
|
@@ -309,9 +313,10 @@ test(
|
|
|
309
313
|
// resolve to (the latter is not readable from native code on iOS).
|
|
310
314
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
311
315
|
const model = new TTSGgml({
|
|
316
|
+
threads: TTS_TEST_THREADS,
|
|
312
317
|
files: { modelDir: download.targetDir },
|
|
313
318
|
referenceAudio: resolveRefWavPath({}),
|
|
314
|
-
config: { language: 'en', outputSampleRate: 16000, ...(
|
|
319
|
+
config: { language: 'en', outputSampleRate: 16000, ...(NO_GPU ? { useGPU: false } : {}) },
|
|
315
320
|
opts: { stats: true }
|
|
316
321
|
})
|
|
317
322
|
await model.load()
|
|
@@ -340,7 +345,7 @@ test(
|
|
|
340
345
|
|
|
341
346
|
test(
|
|
342
347
|
'Chatterbox TTS (ggml): native C++ chunk streaming via streamChunkTokens',
|
|
343
|
-
{ timeout: 600000 },
|
|
348
|
+
{ timeout: 600000, skip: NO_GPU },
|
|
344
349
|
async (t) => {
|
|
345
350
|
const baseDir = getBaseDir()
|
|
346
351
|
const modelsDir = path.join(baseDir, 'models')
|
|
@@ -361,12 +366,13 @@ test(
|
|
|
361
366
|
// rationale).
|
|
362
367
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
363
368
|
const model = new TTSGgml({
|
|
369
|
+
threads: TTS_TEST_THREADS,
|
|
364
370
|
files: { modelDir: download.targetDir },
|
|
365
371
|
referenceAudio: resolveRefWavPath({}),
|
|
366
372
|
streamChunkTokens: 25,
|
|
367
373
|
streamFirstChunkTokens: 10,
|
|
368
374
|
cfmSteps: 1,
|
|
369
|
-
config: { language: 'en',
|
|
375
|
+
config: { language: 'en', useGPU: GPU_ONLY_USE_GPU },
|
|
370
376
|
opts: { stats: true }
|
|
371
377
|
})
|
|
372
378
|
await model.load()
|
|
@@ -10,6 +10,7 @@ const TTSGgml = require('@qvac/tts-ggml')
|
|
|
10
10
|
const { ensureAudio8Models } = require('../utils/downloadModel')
|
|
11
11
|
const { recordTtsStats } = require('../utils/perf-helper')
|
|
12
12
|
const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
|
|
13
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
13
14
|
|
|
14
15
|
const AUDIO8_SAMPLE_RATE = 44100
|
|
15
16
|
const AUDIO8_MAX_FRAMES = 16
|
|
@@ -46,6 +47,7 @@ function resolveAudio8Files(models, useModelDir) {
|
|
|
46
47
|
|
|
47
48
|
function createAudio8Model(models, useGPU, useModelDir) {
|
|
48
49
|
return new TTSGgml({
|
|
50
|
+
threads: TTS_TEST_THREADS,
|
|
49
51
|
engine: TTSGgml.ENGINE_AUDIO8,
|
|
50
52
|
files: resolveAudio8Files(models, useModelDir),
|
|
51
53
|
greedy: true,
|
|
@@ -84,6 +86,9 @@ function assertAudio(t, label, result) {
|
|
|
84
86
|
)
|
|
85
87
|
t.ok(result.stats.tokensPerSecond > 0, `${label} reports codec frames per second`)
|
|
86
88
|
t.ok(result.stats.audioDurationMs > 0, `${label} reports audio duration`)
|
|
89
|
+
for (const key of ['prefillMs', 'fastDecodeMs', 'codecSynthMs', 'stageTotalMs']) {
|
|
90
|
+
t.ok(result.stats[key] > 0, `${label} reports the engine's ${key}`)
|
|
91
|
+
}
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
function sampleBytes(samples) {
|
|
@@ -145,6 +150,7 @@ test(
|
|
|
145
150
|
assertAudio(t, 'Audio8 text-only', textOnly)
|
|
146
151
|
t.is(textOnly.stats.backendDevice, CPU_DEVICE, 'text-only CPU run reports a CPU device')
|
|
147
152
|
t.is(textOnly.stats.backendId, CPU_BACKEND, 'text-only CPU run reports the CPU backend')
|
|
153
|
+
t.is(textOnly.stats.voiceEncodeMs, 0, 'text-only run encodes no reference voice')
|
|
148
154
|
recordAudio8(t, 'audio8 text-only', textOnly, Date.now() - textStarted)
|
|
149
155
|
|
|
150
156
|
const cloneStarted = Date.now()
|
|
@@ -154,6 +160,7 @@ test(
|
|
|
154
160
|
})
|
|
155
161
|
assertAudio(t, 'Audio8 voice clone', cloned)
|
|
156
162
|
t.is(cloned.stats.backendDevice, CPU_DEVICE, 'voice cloning remains on the requested CPU')
|
|
163
|
+
t.ok(cloned.stats.voiceEncodeMs > 0, 'voice cloning reports the reference encode time')
|
|
157
164
|
t.ok(
|
|
158
165
|
samplesDiffer(textOnly.samples, cloned.samples),
|
|
159
166
|
'per-call reference audio changes the synthesized voice'
|
|
@@ -25,13 +25,16 @@ const {
|
|
|
25
25
|
ensureCangjieTsv
|
|
26
26
|
} = require('../utils/downloadModel')
|
|
27
27
|
const { recordTtsStats } = require('../utils/perf-helper')
|
|
28
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
29
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
28
30
|
|
|
29
31
|
const platform = os.platform()
|
|
30
32
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
31
33
|
|
|
32
|
-
// Language coverage
|
|
33
|
-
//
|
|
34
|
-
// Tests that *are* about GPU live
|
|
34
|
+
// Language coverage, not a GPU policy test. A single MTL synthesis already
|
|
35
|
+
// takes close to a minute on the CPU-only runners, so these tests run on the
|
|
36
|
+
// GPU only and are skipped there (NO_GPU=true). Tests that *are* about GPU live
|
|
37
|
+
// in gpu-smoke.test.js.
|
|
35
38
|
|
|
36
39
|
function getBaseDir() {
|
|
37
40
|
return isMobile && global.testDir ? global.testDir : '.'
|
|
@@ -61,6 +64,7 @@ async function loadChatterboxMtlTTS(params) {
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
const model = new TTSGgml({
|
|
67
|
+
threads: TTS_TEST_THREADS,
|
|
64
68
|
files: {
|
|
65
69
|
modelDir: params.modelDir,
|
|
66
70
|
t3Model: params.t3ModelPath,
|
|
@@ -81,7 +85,7 @@ async function loadChatterboxMtlTTS(params) {
|
|
|
81
85
|
|
|
82
86
|
test(
|
|
83
87
|
'Chatterbox MTL TTS (ggml): synthesizes across es/fr/de with shared engine',
|
|
84
|
-
{ timeout: 1800000 },
|
|
88
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
85
89
|
async (t) => {
|
|
86
90
|
const baseDir = getBaseDir()
|
|
87
91
|
const download = await ensureChatterboxMtlModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -96,7 +100,8 @@ test(
|
|
|
96
100
|
modelDir: download.targetDir,
|
|
97
101
|
t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
|
|
98
102
|
s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
|
|
99
|
-
language: MTL_SENTENCES[0].lang
|
|
103
|
+
language: MTL_SENTENCES[0].lang,
|
|
104
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
100
105
|
})
|
|
101
106
|
try {
|
|
102
107
|
for (let i = 0; i < MTL_SENTENCES.length; i++) {
|
|
@@ -145,6 +150,10 @@ test(
|
|
|
145
150
|
'backendDevice surfaced in stats'
|
|
146
151
|
)
|
|
147
152
|
t.ok(typeof result.data.stats.backendId === 'number', 'backendId surfaced in stats')
|
|
153
|
+
const { t3Tokens, t3Ms, s3genMs, denoiserBackendDevice } = result.data.stats
|
|
154
|
+
t.ok(t3Tokens > 0, 'stats report the speech tokens T3 emitted')
|
|
155
|
+
t.ok(t3Ms > 0 && s3genMs > 0, 'stats report the T3 and S3Gen stage times')
|
|
156
|
+
t.is(denoiserBackendDevice, -1, 'no denoiser loaded -> denoiserBackendDevice=-1')
|
|
148
157
|
} else {
|
|
149
158
|
t.fail('expected stats from MTL run')
|
|
150
159
|
}
|
|
@@ -160,7 +169,7 @@ test(
|
|
|
160
169
|
|
|
161
170
|
test(
|
|
162
171
|
'Chatterbox MTL TTS (ggml): synthesizes Japanese with MeCab dictionary',
|
|
163
|
-
{ timeout: 1800000 },
|
|
172
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
164
173
|
async (t) => {
|
|
165
174
|
const baseDir = getBaseDir()
|
|
166
175
|
const download = await ensureChatterboxMtlModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -182,7 +191,8 @@ test(
|
|
|
182
191
|
t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
|
|
183
192
|
s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
|
|
184
193
|
mecabDictDir: mecab.dir,
|
|
185
|
-
language: 'ja'
|
|
194
|
+
language: 'ja',
|
|
195
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
186
196
|
})
|
|
187
197
|
try {
|
|
188
198
|
const t0 = Date.now()
|
|
@@ -227,7 +237,7 @@ test(
|
|
|
227
237
|
|
|
228
238
|
test(
|
|
229
239
|
'Chatterbox MTL TTS (ggml): synthesizes Chinese with Cangjie table',
|
|
230
|
-
{ timeout: 1800000 },
|
|
240
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
231
241
|
async (t) => {
|
|
232
242
|
const baseDir = getBaseDir()
|
|
233
243
|
const download = await ensureChatterboxMtlModels({ targetDir: path.join(baseDir, 'models') })
|
|
@@ -249,7 +259,8 @@ test(
|
|
|
249
259
|
t3ModelPath: path.join(download.targetDir, 'chatterbox-t3-mtl.gguf'),
|
|
250
260
|
s3genModelPath: path.join(download.targetDir, 'chatterbox-s3gen-mtl.gguf'),
|
|
251
261
|
cangjieTsvPath: cangjie.path,
|
|
252
|
-
language: 'zh'
|
|
262
|
+
language: 'zh',
|
|
263
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
253
264
|
})
|
|
254
265
|
try {
|
|
255
266
|
const t0 = Date.now()
|
|
@@ -11,6 +11,8 @@ const path = require('bare-path')
|
|
|
11
11
|
|
|
12
12
|
const { loadChatterboxTTS, runChatterboxTTS } = require('../utils/runChatterboxTTS')
|
|
13
13
|
const { ensureChatterboxModels } = require('../utils/downloadModel')
|
|
14
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
15
|
+
const { NO_GPU, GPU_ONLY_USE_GPU } = require('../utils/gpuOnly')
|
|
14
16
|
|
|
15
17
|
function getBaseDir() {
|
|
16
18
|
const platform = os.platform()
|
|
@@ -29,7 +31,7 @@ const EXPECTATION = {
|
|
|
29
31
|
|
|
30
32
|
test(
|
|
31
33
|
'Chatterbox TTS (ggml): speed config adjusts duration, preserves backward compat',
|
|
32
|
-
{ timeout: 1800000 },
|
|
34
|
+
{ timeout: 1800000, skip: NO_GPU },
|
|
33
35
|
async (t) => {
|
|
34
36
|
const modelsDir = path.join(getBaseDir(), 'models')
|
|
35
37
|
|
|
@@ -47,7 +49,13 @@ test(
|
|
|
47
49
|
// across loads, so the only difference between runs is the speed knob.
|
|
48
50
|
async function sampleCountAt(speed) {
|
|
49
51
|
const label = speed === undefined ? 'default' : String(speed)
|
|
50
|
-
const model = await loadChatterboxTTS({
|
|
52
|
+
const model = await loadChatterboxTTS({
|
|
53
|
+
threads: TTS_TEST_THREADS,
|
|
54
|
+
modelDir,
|
|
55
|
+
language: 'en',
|
|
56
|
+
speed,
|
|
57
|
+
useGPU: GPU_ONLY_USE_GPU
|
|
58
|
+
})
|
|
51
59
|
try {
|
|
52
60
|
const r = await runChatterboxTTS(model, { text: TEXT }, EXPECTATION)
|
|
53
61
|
t.ok(r.passed, `speed=${label}: passes synthesis expectations`)
|
|
@@ -94,7 +102,12 @@ test('Chatterbox TTS (ggml): out-of-range speed is rejected', { timeout: 600000
|
|
|
94
102
|
// `speed` is bounded to [0.25, 4.0] by ChatterboxModel::validateConfig,
|
|
95
103
|
// which runs at load time and surfaces as a rejected load.
|
|
96
104
|
await t.exception(
|
|
97
|
-
loadChatterboxTTS({
|
|
105
|
+
loadChatterboxTTS({
|
|
106
|
+
threads: TTS_TEST_THREADS,
|
|
107
|
+
modelDir: download.targetDir,
|
|
108
|
+
language: 'en',
|
|
109
|
+
speed: 5
|
|
110
|
+
}),
|
|
98
111
|
'speed=5 (> 4.0) is rejected at load'
|
|
99
112
|
)
|
|
100
113
|
})
|