@qvac/tts-ggml 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -33
- 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 +14 -9
- package/test/integration/addon.test.js +5 -0
- package/test/integration/audio8.test.js +7 -0
- package/test/integration/chatterbox-mtl.test.js +6 -0
- package/test/integration/chatterbox-speed.test.js +13 -2
- package/test/integration/cosyvoice3-clone.test.js +16 -1
- package/test/integration/cosyvoice3-lavasr.test.js +6 -0
- package/test/integration/cosyvoice3.test.js +19 -0
- package/test/integration/fit.test.js +174 -0
- package/test/integration/gpu-smoke.test.js +13 -0
- package/test/integration/lavasr-enhancer.test.js +104 -3
- package/test/integration/moss.test.js +282 -0
- package/test/integration/multiple-runs.test.js +18 -2
- package/test/integration/output-sample-rate.test.js +6 -0
- package/test/integration/parler-wer.test.js +7 -1
- package/test/integration/parler.test.js +27 -3
- 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.0",
|
|
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",
|
|
@@ -104,7 +106,9 @@
|
|
|
104
106
|
"setup:venv": "bash scripts/setup-venv.sh",
|
|
105
107
|
"convert-models": "bash scripts/convert-models.sh",
|
|
106
108
|
"setup-models": "bash scripts/setup-venv.sh && bash scripts/convert-models.sh",
|
|
107
|
-
"download-models:registry": "node scripts/download-tts-ggml-models.js"
|
|
109
|
+
"download-models:registry": "node scripts/download-tts-ggml-models.js",
|
|
110
|
+
"example:pocket": "bare examples/pocket-tts.js",
|
|
111
|
+
"test:pocket": "npm run build:ts && brittle-bare test/integration/pocket.integration.test.js"
|
|
108
112
|
},
|
|
109
113
|
"files": [
|
|
110
114
|
"addonLogging.js",
|
|
@@ -134,7 +138,8 @@
|
|
|
134
138
|
"test/utils/textMetrics.js",
|
|
135
139
|
"test/data/sentences-medium.js",
|
|
136
140
|
"test/data/sentences-long.js",
|
|
137
|
-
"LICENSE"
|
|
141
|
+
"LICENSE",
|
|
142
|
+
"docs"
|
|
138
143
|
],
|
|
139
144
|
"exports": {
|
|
140
145
|
"./package": "./package.json",
|
|
@@ -209,10 +214,10 @@
|
|
|
209
214
|
},
|
|
210
215
|
"types": "index.d.ts",
|
|
211
216
|
"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.
|
|
217
|
+
"@qvac/tts-ggml-linux-x64": "0.10.0",
|
|
218
|
+
"@qvac/tts-ggml-linux-arm64": "0.10.0",
|
|
219
|
+
"@qvac/tts-ggml-darwin-arm64": "0.10.0",
|
|
220
|
+
"@qvac/tts-ggml-darwin-x64": "0.10.0",
|
|
221
|
+
"@qvac/tts-ggml-win32-x64": "0.10.0"
|
|
217
222
|
}
|
|
218
223
|
}
|
|
@@ -15,6 +15,7 @@ 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')
|
|
18
19
|
|
|
19
20
|
const platform = os.platform()
|
|
20
21
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
@@ -83,6 +84,7 @@ 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
89
|
language: 'en'
|
|
88
90
|
})
|
|
@@ -250,6 +252,7 @@ test(
|
|
|
250
252
|
// empty/undefined value flows through to the engine cleanly.
|
|
251
253
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
252
254
|
const model = new TTSGgml({
|
|
255
|
+
threads: TTS_TEST_THREADS,
|
|
253
256
|
files: { modelDir: download.targetDir },
|
|
254
257
|
config: { language: 'en', ...(forceNoGpu ? { useGPU: false } : {}) },
|
|
255
258
|
opts: { stats: true }
|
|
@@ -309,6 +312,7 @@ test(
|
|
|
309
312
|
// resolve to (the latter is not readable from native code on iOS).
|
|
310
313
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
311
314
|
const model = new TTSGgml({
|
|
315
|
+
threads: TTS_TEST_THREADS,
|
|
312
316
|
files: { modelDir: download.targetDir },
|
|
313
317
|
referenceAudio: resolveRefWavPath({}),
|
|
314
318
|
config: { language: 'en', outputSampleRate: 16000, ...(forceNoGpu ? { useGPU: false } : {}) },
|
|
@@ -361,6 +365,7 @@ test(
|
|
|
361
365
|
// rationale).
|
|
362
366
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
363
367
|
const model = new TTSGgml({
|
|
368
|
+
threads: TTS_TEST_THREADS,
|
|
364
369
|
files: { modelDir: download.targetDir },
|
|
365
370
|
referenceAudio: resolveRefWavPath({}),
|
|
366
371
|
streamChunkTokens: 25,
|
|
@@ -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,6 +25,7 @@ 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')
|
|
28
29
|
|
|
29
30
|
const platform = os.platform()
|
|
30
31
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
@@ -61,6 +62,7 @@ async function loadChatterboxMtlTTS(params) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const model = new TTSGgml({
|
|
65
|
+
threads: TTS_TEST_THREADS,
|
|
64
66
|
files: {
|
|
65
67
|
modelDir: params.modelDir,
|
|
66
68
|
t3Model: params.t3ModelPath,
|
|
@@ -145,6 +147,10 @@ test(
|
|
|
145
147
|
'backendDevice surfaced in stats'
|
|
146
148
|
)
|
|
147
149
|
t.ok(typeof result.data.stats.backendId === 'number', 'backendId surfaced in stats')
|
|
150
|
+
const { t3Tokens, t3Ms, s3genMs, denoiserBackendDevice } = result.data.stats
|
|
151
|
+
t.ok(t3Tokens > 0, 'stats report the speech tokens T3 emitted')
|
|
152
|
+
t.ok(t3Ms > 0 && s3genMs > 0, 'stats report the T3 and S3Gen stage times')
|
|
153
|
+
t.is(denoiserBackendDevice, -1, 'no denoiser loaded -> denoiserBackendDevice=-1')
|
|
148
154
|
} else {
|
|
149
155
|
t.fail('expected stats from MTL run')
|
|
150
156
|
}
|
|
@@ -11,6 +11,7 @@ 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')
|
|
14
15
|
|
|
15
16
|
function getBaseDir() {
|
|
16
17
|
const platform = os.platform()
|
|
@@ -47,7 +48,12 @@ test(
|
|
|
47
48
|
// across loads, so the only difference between runs is the speed knob.
|
|
48
49
|
async function sampleCountAt(speed) {
|
|
49
50
|
const label = speed === undefined ? 'default' : String(speed)
|
|
50
|
-
const model = await loadChatterboxTTS({
|
|
51
|
+
const model = await loadChatterboxTTS({
|
|
52
|
+
threads: TTS_TEST_THREADS,
|
|
53
|
+
modelDir,
|
|
54
|
+
language: 'en',
|
|
55
|
+
speed
|
|
56
|
+
})
|
|
51
57
|
try {
|
|
52
58
|
const r = await runChatterboxTTS(model, { text: TEXT }, EXPECTATION)
|
|
53
59
|
t.ok(r.passed, `speed=${label}: passes synthesis expectations`)
|
|
@@ -94,7 +100,12 @@ test('Chatterbox TTS (ggml): out-of-range speed is rejected', { timeout: 600000
|
|
|
94
100
|
// `speed` is bounded to [0.25, 4.0] by ChatterboxModel::validateConfig,
|
|
95
101
|
// which runs at load time and surfaces as a rejected load.
|
|
96
102
|
await t.exception(
|
|
97
|
-
loadChatterboxTTS({
|
|
103
|
+
loadChatterboxTTS({
|
|
104
|
+
threads: TTS_TEST_THREADS,
|
|
105
|
+
modelDir: download.targetDir,
|
|
106
|
+
language: 'en',
|
|
107
|
+
speed: 5
|
|
108
|
+
}),
|
|
98
109
|
'speed=5 (> 4.0) is rejected at load'
|
|
99
110
|
)
|
|
100
111
|
})
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const fs = require('bare-fs')
|
|
9
9
|
const os = require('bare-os')
|
|
10
10
|
const path = require('bare-path')
|
|
11
|
+
const proc = require('bare-process')
|
|
11
12
|
const test = require('brittle')
|
|
12
13
|
const TTSGgml = require('@qvac/tts-ggml')
|
|
13
14
|
|
|
@@ -18,9 +19,11 @@ const {
|
|
|
18
19
|
cosyvoiceBaseFileNames
|
|
19
20
|
} = require('../utils/downloadModel')
|
|
20
21
|
const { resolveRefWavPath } = require('../utils/runChatterboxTTS')
|
|
22
|
+
const { TTS_TEST_THREADS } = require('../utils/testThreads')
|
|
21
23
|
|
|
22
24
|
const platform = os.platform()
|
|
23
25
|
const isMobile = platform === 'ios' || platform === 'android'
|
|
26
|
+
const useGPU = !isMobile && proc.env.NO_GPU === 'false'
|
|
24
27
|
|
|
25
28
|
function getBaseDir() {
|
|
26
29
|
return isMobile && global.testDir ? global.testDir : '.'
|
|
@@ -66,7 +69,12 @@ function samplesDiffer(a, b) {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
async function synthOnce(t, loadParams, label) {
|
|
69
|
-
const model = await loadCosyvoiceTTS({
|
|
72
|
+
const model = await loadCosyvoiceTTS({
|
|
73
|
+
threads: TTS_TEST_THREADS,
|
|
74
|
+
...loadParams,
|
|
75
|
+
seed: CLONE_SEED,
|
|
76
|
+
useGPU
|
|
77
|
+
})
|
|
70
78
|
try {
|
|
71
79
|
const result = await runCosyvoiceTTS(
|
|
72
80
|
model,
|
|
@@ -76,6 +84,11 @@ async function synthOnce(t, loadParams, label) {
|
|
|
76
84
|
console.log(result.output)
|
|
77
85
|
t.ok(result.passed, `${label} synth passes expectations`)
|
|
78
86
|
t.ok(result.data.sampleCount > 0, `${label} produced audio`)
|
|
87
|
+
t.is(
|
|
88
|
+
result.data.stats?.backendDevice,
|
|
89
|
+
useGPU ? 1 : 0,
|
|
90
|
+
`${label} runs on the ${useGPU ? 'GPU' : 'CPU'} selected by the CI lane`
|
|
91
|
+
)
|
|
79
92
|
return result.data
|
|
80
93
|
} finally {
|
|
81
94
|
try {
|
|
@@ -127,6 +140,7 @@ test(
|
|
|
127
140
|
t.exception(
|
|
128
141
|
() =>
|
|
129
142
|
new TTSGgml({
|
|
143
|
+
threads: TTS_TEST_THREADS,
|
|
130
144
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
131
145
|
referenceAudio: resolveRefWavPath({}),
|
|
132
146
|
files: {
|
|
@@ -167,6 +181,7 @@ test(
|
|
|
167
181
|
}
|
|
168
182
|
|
|
169
183
|
const model = new TTSGgml({
|
|
184
|
+
threads: TTS_TEST_THREADS,
|
|
170
185
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
171
186
|
files: { cosyvoiceModelDir: baseOnlyDir },
|
|
172
187
|
referenceAudio: resolveRefWavPath({})
|
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
ensureLavaSRDenoiserGguf,
|
|
25
25
|
ensureCosyvoiceModel
|
|
26
26
|
} = 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'
|
|
@@ -126,6 +127,7 @@ test(
|
|
|
126
127
|
if (!dl) return
|
|
127
128
|
|
|
128
129
|
const model = new TTSGgml({
|
|
130
|
+
threads: TTS_TEST_THREADS,
|
|
129
131
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
130
132
|
files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
|
|
131
133
|
config: { language: 'en', useGPU: false },
|
|
@@ -161,6 +163,7 @@ test(
|
|
|
161
163
|
if (!dl) return
|
|
162
164
|
|
|
163
165
|
const model = new TTSGgml({
|
|
166
|
+
threads: TTS_TEST_THREADS,
|
|
164
167
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
165
168
|
files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
|
|
166
169
|
streamChunkTokens: 25,
|
|
@@ -201,6 +204,7 @@ test(
|
|
|
201
204
|
if (!dl) return
|
|
202
205
|
|
|
203
206
|
const model = new TTSGgml({
|
|
207
|
+
threads: TTS_TEST_THREADS,
|
|
204
208
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
205
209
|
files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: enh.path },
|
|
206
210
|
streamChunkTokens: 25,
|
|
@@ -240,6 +244,7 @@ test(
|
|
|
240
244
|
|
|
241
245
|
const synth = async (files) => {
|
|
242
246
|
const model = new TTSGgml({
|
|
247
|
+
threads: TTS_TEST_THREADS,
|
|
243
248
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
244
249
|
files: Object.assign({ cosyvoiceModelDir: dl.modelDir }, files),
|
|
245
250
|
config: { language: 'en', useGPU: false, seed: SEED },
|
|
@@ -292,6 +297,7 @@ test(
|
|
|
292
297
|
fs.writeFileSync(badEnhancer, 'not a gguf')
|
|
293
298
|
|
|
294
299
|
const model = new TTSGgml({
|
|
300
|
+
threads: TTS_TEST_THREADS,
|
|
295
301
|
engine: TTSGgml.ENGINE_COSYVOICE3,
|
|
296
302
|
files: { cosyvoiceModelDir: dl.modelDir, lavasrEnhancer: badEnhancer },
|
|
297
303
|
config: { language: 'en', useGPU: false },
|