@qvac/tts-ggml 0.14.1 → 0.15.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/lib/fit.d.ts CHANGED
@@ -4,16 +4,13 @@
4
4
  * the same keys and applies the same defaults as the load it describes.
5
5
  */
6
6
  interface TtsFitCommon {
7
+ backend?: string;
7
8
  /** Layers to offload. Unset defers to `useGPU`. */
8
9
  nGpuLayers?: number;
9
10
  /** All layers or none, when `nGpuLayers` is unset. */
10
11
  useGPU?: boolean;
11
12
  vulkanDevice?: number;
12
13
  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
14
  /** Free memory that must remain for the projection to count as fitting. Defaults to 256 MiB. */
18
15
  marginBytes?: number;
19
16
  }
@@ -32,6 +29,8 @@ export interface SupertonicFitRequest extends TtsFitCommon {
32
29
  export interface ParlerFitRequest extends TtsFitCommon {
33
30
  engineType: 'parler';
34
31
  parlerModelPath: string;
32
+ /** Match the Parler decoder-sidecar load policy. */
33
+ coremlDecoder?: boolean;
35
34
  descriptionTokens?: number;
36
35
  promptTokens?: number;
37
36
  /** 0 takes the checkpoint's default generation length. */
@@ -67,6 +66,8 @@ export interface CosyvoiceFitRequest extends TtsFitCommon {
67
66
  speechTokens?: number;
68
67
  }
69
68
  export interface MossFitRequest extends TtsFitCommon {
69
+ nCtx?: number;
70
+ maxNewTokens?: number;
70
71
  engineType: 'moss';
71
72
  mossBackbonePath: string;
72
73
  mossCodecDecoderPath: string;
@@ -92,41 +93,50 @@ export interface MossSoundEffectFitRequest extends TtsFitCommon {
92
93
  shift?: number;
93
94
  threads?: number;
94
95
  }
95
- export type TtsFitRequest = SupertonicFitRequest | ParlerFitRequest | ChatterboxFitRequest | Audio8FitRequest | CosyvoiceFitRequest | MossFitRequest | MossSoundEffectFitRequest;
96
+ export interface PocketFitRequest extends TtsFitCommon {
97
+ engineType: 'pocket';
98
+ pocketFlowModelPath: string;
99
+ pocketMimiModelPath: string;
100
+ pocketFrontendPath: string;
101
+ pocketVoicePath?: string;
102
+ referenceAudio?: string;
103
+ text: string;
104
+ threads?: number;
105
+ nCtx?: number;
106
+ maxTokens?: number;
107
+ steps?: number;
108
+ framesAfterEos?: number;
109
+ memoryBudgetBytes?: number;
110
+ }
111
+ export type TtsFitRequest = PocketFitRequest | SupertonicFitRequest | ParlerFitRequest | ChatterboxFitRequest | Audio8FitRequest | CosyvoiceFitRequest | MossFitRequest | MossSoundEffectFitRequest;
96
112
  /** The voice engines a fit can be asked for, read off the request union. */
97
113
  export type TtsFitEngine = TtsFitRequest['engineType'];
98
- export type TtsFitStatus = 'fits' | 'does-not-fit' | 'error';
114
+ /**
115
+ * `unknown` is a load the fitter cannot size: supertonic outside the fused
116
+ * graph path it models, a CosyVoice3 or Parler load with Core ML sidecars, or a failed
117
+ * measurement.
118
+ */
119
+ export type TtsFitStatus = 'fits' | 'does-not-fit' | 'unknown';
120
+ export interface TtsFitDevice {
121
+ /** The PCI bus ID when the backend reports one, the device name otherwise. */
122
+ id: string;
123
+ name: string;
124
+ /** Memory the host owns: the host CPU, and a GPU that shares the host's memory. */
125
+ unified: boolean;
126
+ /** Memory the load takes on this device. */
127
+ usedBytes: number;
128
+ freeBytes: number;
129
+ totalBytes: number;
130
+ }
131
+ export type TtsFitStats = Record<string, never>;
99
132
  export interface TtsFitResult {
100
133
  status: TtsFitStatus;
101
134
  /**
102
- * The engine's own wording, e.g. `model-unreadable`, `workload-too-large`.
103
- * `unsupported-engine` names a voice the addon loads and the fitter does
104
- * not cover.
105
- */
106
- reason: string;
107
- /** Which pipeline was projected, e.g. `chatterbox-t3-turbo`. */
108
- modelVariant: string;
109
- deviceName: string;
110
- deviceIsCpu: boolean;
111
- /** The device pool is system RAM, so host bytes compete with device bytes. */
112
- deviceSharesHostMemory: boolean;
113
- deviceFreeBytes: number;
114
- deviceTotalBytes: number;
115
- deviceBytes: number;
116
- weightsBytes: number;
117
- stateBytes: number;
118
- /** 0 for a pipeline with no language-model stage, such as supertonic. */
119
- lmComputeBytes: number;
120
- /** 0 for a pipeline with no separate codec stage. */
121
- codecComputeBytes: number;
122
- hostBytes: number;
123
- /**
124
- * The LavaSR stages' size on disk, 0 when none was named. They have no
125
- * fitter, so this is a size on disk. It is counted in neither `deviceBytes`
126
- * nor `hostBytes`.
135
+ * The device the load resolves to, then the host CPU. One CPU row for a load
136
+ * on the CPU, and none for `unknown`.
127
137
  */
128
- lavasrFileBytes: number;
129
- report: string;
138
+ devices: TtsFitDevice[];
139
+ stats: TtsFitStats;
130
140
  }
131
141
  /**
132
142
  * Projects one voice against the memory free right now, reading GGUF metadata
@@ -139,9 +149,9 @@ export interface TtsFitResult {
139
149
  *
140
150
  * `engineType` picks the fitter, the same key a load uses, and is required
141
151
  * here: a fit request carries none of the file keys a load is inferred from.
142
- * A model the engine cannot read resolves to `status: "error"`, as does a
143
- * voice with no fitter; a broken request, or a host with no native binding,
144
- * rejects. Fits run one at a time within this addon.
152
+ * A model the engine cannot read, a voice with no fitter, an invalid request,
153
+ * or a host with no native binding rejects. Fits run one at a time within this
154
+ * addon.
145
155
  */
146
156
  export declare function assessFit(request: TtsFitRequest): Promise<TtsFitResult>;
147
157
  export {};
package/lib/fit.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assessFit = assessFit;
4
+ const fabricOptions_1 = require("./fabricOptions");
4
5
  /**
5
6
  * Projects one voice against the memory free right now, reading GGUF metadata
6
7
  * and never weight data. The registry's weightless copy of each file answers
@@ -12,11 +13,12 @@ exports.assessFit = assessFit;
12
13
  *
13
14
  * `engineType` picks the fitter, the same key a load uses, and is required
14
15
  * here: a fit request carries none of the file keys a load is inferred from.
15
- * A model the engine cannot read resolves to `status: "error"`, as does a
16
- * voice with no fitter; a broken request, or a host with no native binding,
17
- * rejects. Fits run one at a time within this addon.
16
+ * A model the engine cannot read, a voice with no fitter, an invalid request,
17
+ * or a host with no native binding rejects. Fits run one at a time within this
18
+ * addon.
18
19
  */
19
20
  async function assessFit(request) {
21
+ (0, fabricOptions_1.validateBackendOption)(request.engineType, request.backend);
20
22
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- the native binding loads on first use.
21
23
  const binding = require('../binding.js');
22
24
  return await binding.assessFit(request);
package/lib/jobFields.js CHANGED
@@ -50,6 +50,7 @@ function assertNoOtherEngineFields(setup, source, where) {
50
50
  * and every engine the cross-engine conditioning it supports.
51
51
  */
52
52
  function resolveJobFields(setup, source, where) {
53
+ (0, parler_1.assertNoPerCallParlerCoreml)(source, where);
53
54
  if (setup.engineType === engineIds_1.ENGINE_MOSS_SPEECH) {
54
55
  assertNoOtherEngineFields(setup, source, where);
55
56
  return (0, mossSpeech_1.resolveSpeechJobFields)(source, where);
@@ -70,4 +70,16 @@ function assignLavasrParams(setup, parameters) {
70
70
  if (setup.backend.denoiserGgufPath) {
71
71
  parameters.lavasrDenoiserPath = setup.backend.denoiserGgufPath;
72
72
  }
73
+ if (setup.backend.enhancerCoremlModelPath !== undefined) {
74
+ parameters.lavasrEnhancerCoremlModelPath =
75
+ setup.backend.enhancerCoremlModelPath;
76
+ parameters.lavasrEnhancerCoremlComputeUnits =
77
+ setup.backend.enhancerCoremlComputeUnits;
78
+ }
79
+ if (setup.backend.denoiserCoremlModelPath !== undefined) {
80
+ parameters.lavasrDenoiserCoremlModelPath =
81
+ setup.backend.denoiserCoremlModelPath;
82
+ parameters.lavasrDenoiserCoremlComputeUnits =
83
+ setup.backend.denoiserCoremlComputeUnits;
84
+ }
73
85
  }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.buildPocketParams = buildPocketParams;
7
7
  const bare_path_1 = __importDefault(require("bare-path"));
8
+ const fabricOptions_1 = require("./fabricOptions");
8
9
  const DEFAULT_SAMPLING_STEPS = 1;
9
10
  const UNSUPPORTED_OPTIONS = [
10
11
  'voice',
@@ -36,6 +37,9 @@ const UNSUPPORTED_OPTIONS = [
36
37
  'maxFrames',
37
38
  'minNewTokens',
38
39
  'normalizeNumbers',
40
+ 'coremlDecoder',
41
+ 'coremlComputeUnits',
42
+ 'coremlStrict',
39
43
  'enhancer',
40
44
  'denoiser'
41
45
  ];
@@ -69,14 +73,21 @@ function resolveModelFiles(files, options) {
69
73
  };
70
74
  }
71
75
  function validateEngineOptions(options, config) {
76
+ (0, fabricOptions_1.resolveFabricOptions)('pocket', options);
77
+ (0, fabricOptions_1.validateBackendOption)('pocket', config.backend);
78
+ if (config.useGPU !== undefined && typeof config.useGPU !== 'boolean')
79
+ throw new Error('Pocket useGPU must be a boolean');
80
+ if (options.nGpuLayers !== undefined &&
81
+ (typeof options.nGpuLayers !== 'number' || !Number.isInteger(options.nGpuLayers) ||
82
+ options.nGpuLayers < -2147483648 || options.nGpuLayers > 2147483647))
83
+ throw new Error('Pocket nGpuLayers must be a 32-bit integer');
84
+ if (typeof config.useGPU === 'boolean' && typeof options.nGpuLayers === 'number' &&
85
+ config.useGPU !== (options.nGpuLayers !== 0))
86
+ throw new Error('Pocket useGPU conflicts with nGpuLayers');
72
87
  if (options.referenceAudio !== undefined && typeof options.referenceAudio !== 'string')
73
88
  throw new Error('Pocket referenceAudio must be a path string');
74
89
  if (config.language !== undefined && config.language !== 'en')
75
90
  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
91
  if (options.steps !== undefined &&
81
92
  options.numInferenceSteps !== undefined &&
82
93
  options.steps !== options.numInferenceSteps)
@@ -138,6 +149,18 @@ function buildPocketParams(files, optionInput, configInput) {
138
149
  validateEngineOptions(options, config);
139
150
  rejectUnsupportedOptions(options);
140
151
  const params = resolveModelFiles(files, options);
152
+ if (config.useGPU !== undefined)
153
+ params.useGPU = config.useGPU;
154
+ if (options.nGpuLayers !== undefined)
155
+ params.nGpuLayers = options.nGpuLayers;
156
+ if (options.nGpuLayers !== undefined && config.useGPU === undefined)
157
+ params.useGPU = options.nGpuLayers !== 0;
158
+ if (config.backendsDir !== undefined)
159
+ params.backendsDir = config.backendsDir;
160
+ if (options.backendsDir !== undefined)
161
+ params.backendsDir = options.backendsDir;
162
+ if (config.backend !== undefined)
163
+ params.backend = config.backend;
141
164
  applyNumericOptions(params, options, config);
142
165
  validateRequiredFiles(params);
143
166
  validateVoiceConditioning(params);
@@ -138,6 +138,12 @@ class PocketLifecycle {
138
138
  throw new Error("Pocket engine is not configured");
139
139
  const options = { ...pocket.options, ...newConfig };
140
140
  const config = { ...setup.config, ...newConfig };
141
+ if (newConfig.nGpuLayers !== undefined && newConfig.useGPU === undefined) {
142
+ config.useGPU = undefined;
143
+ }
144
+ if (newConfig.useGPU !== undefined && newConfig.nGpuLayers === undefined) {
145
+ options.nGpuLayers = undefined;
146
+ }
141
147
  const params = (0, pocketConfig_1.buildPocketParams)(pocket.files, options, config);
142
148
  this.inProgress = true;
143
149
  let replacement = null;
@@ -149,7 +155,9 @@ class PocketLifecycle {
149
155
  replacement = null;
150
156
  setup.pocket = { files: pocket.files, options, params };
151
157
  setup.config.language = "en";
152
- setup.config.useGPU = false;
158
+ setup.config.useGPU = params.useGPU;
159
+ setup.config.backend = params.backend;
160
+ setup.settings.nGpuLayers = params.nGpuLayers;
153
161
  setup.outputSampleRate =
154
162
  typeof params.outputSampleRate === "number" ? params.outputSampleRate : null;
155
163
  setup.config.outputSampleRate = setup.outputSampleRate ?? undefined;
package/lib/reload.js CHANGED
@@ -5,12 +5,15 @@ const conditioning_1 = require("./conditioning");
5
5
  const engineParams_1 = require("./engineParams");
6
6
  const audio8_1 = require("./engines/audio8");
7
7
  const moss_1 = require("./engines/moss");
8
+ const fabricOptions_1 = require("./fabricOptions");
9
+ const engineIds_1 = require("./engineIds");
8
10
  const parler_1 = require("./engines/parler");
9
11
  const setup_1 = require("./setup");
10
12
  function captureReloadableState(setup) {
11
13
  return {
12
14
  language: setup.config.language,
13
15
  useGPU: setup.config.useGPU,
16
+ backend: setup.config.backend,
14
17
  outputSampleRate: setup.outputSampleRate,
15
18
  settings: { ...setup.settings },
16
19
  };
@@ -18,6 +21,7 @@ function captureReloadableState(setup) {
18
21
  function restoreReloadableState(setup, state) {
19
22
  setup.config.language = state.language;
20
23
  setup.config.useGPU = state.useGPU;
24
+ setup.config.backend = state.backend;
21
25
  setup.outputSampleRate = state.outputSampleRate;
22
26
  const live = setup.settings;
23
27
  const snapshot = state.settings;
@@ -28,6 +32,18 @@ function restoreReloadableState(setup, state) {
28
32
  function applyReloadableRuntimeConfig(setup, newConfig) {
29
33
  (0, moss_1.assertMossReloadKeepsVoice)(setup, newConfig);
30
34
  const runtimeConfig = newConfig;
35
+ (0, fabricOptions_1.validateBackendOption)(setup.engineType, runtimeConfig.backend);
36
+ if (runtimeConfig.backend !== undefined)
37
+ setup.config.backend = runtimeConfig.backend;
38
+ Object.assign(setup.settings, (0, fabricOptions_1.resolveFabricOptions)(setup.engineType, { ...setup.settings, ...newConfig }));
39
+ if (setup.engineType === engineIds_1.ENGINE_MOSS && newConfig.nCtx !== undefined) {
40
+ setup.settings.nCtx = newConfig.nCtx;
41
+ }
42
+ if (setup.engineType === engineIds_1.ENGINE_COSYVOICE3 && newConfig.greedy !== undefined) {
43
+ if (typeof newConfig.greedy !== "boolean")
44
+ throw new Error("greedy must be a boolean");
45
+ setup.settings.greedy = newConfig.greedy;
46
+ }
31
47
  if (runtimeConfig.language !== undefined) {
32
48
  setup.config.language = runtimeConfig.language;
33
49
  }
@@ -39,6 +55,9 @@ function applyReloadableRuntimeConfig(setup, newConfig) {
39
55
  }
40
56
  (0, audio8_1.applyAudio8Reload)(setup, newConfig);
41
57
  (0, moss_1.applyMossReload)(setup, newConfig);
58
+ if (setup.engineType === engineIds_1.ENGINE_MOSS) {
59
+ (0, moss_1.assertMossDurationTokens)(setup.settings.durationTokens, "reload", setup.settings.maxNewTokens);
60
+ }
42
61
  }
43
62
  // Cross-engine conditioning is reloadable on every engine that supports it,
44
63
  // so both families change emotion the same way.
@@ -7,14 +7,26 @@ export interface ChunkResolver {
7
7
  resolve(): void;
8
8
  reject(error: unknown): void;
9
9
  }
10
- export interface StreamAccumulator {
10
+ /**
11
+ * Stats a stream keeps from the last chunk that reported them instead of
12
+ * summing: where the synthesis ran, whether the Audio8 codec used its Core ML
13
+ * sidecar, and which Parler sidecars were still loaded.
14
+ */
15
+ export declare const LAST_REPORTED_STREAM_STATS: readonly ["backendDevice", "backendId", "gpuUnsupported", "enhancerBackendDevice", "enhancerBackendId", "denoiserBackendDevice", "denoiserBackendId", "codecSidecarLoaded", "codecOnCoreml", "decoderSidecarLoaded", "dacSidecarLoaded"];
16
+ export type LastReportedStreamStat = (typeof LAST_REPORTED_STREAM_STATS)[number];
17
+ /**
18
+ * Stats a stream keeps at the lowest value any chunk reported. Complete
19
+ * Core ML execution requires every sentence to have used it: a capacity
20
+ * fallback can leave the sidecar attached for the next sentence.
21
+ */
22
+ export declare const EVERY_CHUNK_STREAM_STATS: readonly ["decoderOnCoreml", "dacOnCoreml"];
23
+ export type EveryChunkStreamStat = (typeof EVERY_CHUNK_STREAM_STATS)[number];
24
+ export interface StreamAccumulator extends Partial<Pick<RuntimeStats, LastReportedStreamStat | EveryChunkStreamStat>> {
11
25
  firstAudioMs?: number;
12
26
  totalTime: number;
13
27
  audioDurationMs: number;
14
28
  totalSamples: number;
15
29
  generatedFrames: number;
16
- codecSidecarLoaded?: number;
17
- codecOnCoreml?: number;
18
30
  }
19
31
  export interface SentenceStreamContext {
20
32
  textStreamMode?: boolean;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SentenceStreamer = void 0;
3
+ exports.SentenceStreamer = exports.EVERY_CHUNK_STREAM_STATS = exports.LAST_REPORTED_STREAM_STATS = void 0;
4
4
  exports.normalizeError = normalizeError;
5
5
  exports.ttsOutputDebugString = ttsOutputDebugString;
6
6
  exports.isAudioOutputEvent = isAudioOutputEvent;
@@ -14,6 +14,33 @@ exports.splitSentenceChunks = splitSentenceChunks;
14
14
  const error_1 = require("./error");
15
15
  const textChunker_1 = require("./textChunker");
16
16
  const textStreamAccumulator_1 = require("./textStreamAccumulator");
17
+ /**
18
+ * Stats a stream keeps from the last chunk that reported them instead of
19
+ * summing: where the synthesis ran, whether the Audio8 codec used its Core ML
20
+ * sidecar, and which Parler sidecars were still loaded.
21
+ */
22
+ exports.LAST_REPORTED_STREAM_STATS = [
23
+ "backendDevice",
24
+ "backendId",
25
+ "gpuUnsupported",
26
+ "enhancerBackendDevice",
27
+ "enhancerBackendId",
28
+ "denoiserBackendDevice",
29
+ "denoiserBackendId",
30
+ "codecSidecarLoaded",
31
+ "codecOnCoreml",
32
+ "decoderSidecarLoaded",
33
+ "dacSidecarLoaded",
34
+ ];
35
+ /**
36
+ * Stats a stream keeps at the lowest value any chunk reported. Complete
37
+ * Core ML execution requires every sentence to have used it: a capacity
38
+ * fallback can leave the sidecar attached for the next sentence.
39
+ */
40
+ exports.EVERY_CHUNK_STREAM_STATS = [
41
+ "decoderOnCoreml",
42
+ "dacOnCoreml",
43
+ ];
17
44
  function normalizeError(error) {
18
45
  return typeof error === "string"
19
46
  ? error
@@ -209,13 +236,27 @@ function mergeSentenceStreamStats(accumulator, data) {
209
236
  typeof data.totalSamples === "number" ? data.totalSamples : 0;
210
237
  accumulator.generatedFrames +=
211
238
  typeof data.generatedFrames === "number" ? data.generatedFrames : 0;
212
- if (typeof data.codecSidecarLoaded === "number") {
213
- accumulator.codecSidecarLoaded = data.codecSidecarLoaded;
239
+ retainLastReportedStats(accumulator, data);
240
+ retainLowestReportedStats(accumulator, data);
241
+ }
242
+ function retainLastReportedStats(accumulator, data) {
243
+ for (const field of exports.LAST_REPORTED_STREAM_STATS) {
244
+ const value = data[field];
245
+ if (typeof value === "number")
246
+ accumulator[field] = value;
214
247
  }
215
- if (typeof data.codecOnCoreml === "number") {
216
- accumulator.codecOnCoreml = data.codecOnCoreml;
248
+ }
249
+ function retainLowestReportedStats(accumulator, data) {
250
+ for (const field of exports.EVERY_CHUNK_STREAM_STATS) {
251
+ const value = data[field];
252
+ if (typeof value === "number") {
253
+ accumulator[field] = lowestReported(accumulator[field], value);
254
+ }
217
255
  }
218
256
  }
257
+ function lowestReported(previous, value) {
258
+ return previous === undefined ? value : Math.min(previous, value);
259
+ }
219
260
  /**
220
261
  * Drives one sentence stream at a time. The wrapper routes native output and
221
262
  * stats events here while a context is active; a newer stream, a cancel or a
@@ -283,6 +324,9 @@ class SentenceStreamer {
283
324
  if (data.sampleRate != null) {
284
325
  enriched.sampleRate = data.sampleRate;
285
326
  }
327
+ if (data.speechTokenIds !== undefined) {
328
+ enriched.speechTokenIds = data.speechTokenIds;
329
+ }
286
330
  if (!context.textStreamMode) {
287
331
  enriched.isLast = index >= context.chunks.length - 1 && data.isLast !== false;
288
332
  }
package/lib/setup.js CHANGED
@@ -17,12 +17,35 @@ const engineDetection_1 = require("./engineDetection");
17
17
  const engineIds_1 = require("./engineIds");
18
18
  const cosyvoice_1 = require("./engines/cosyvoice");
19
19
  const moss_1 = require("./engines/moss");
20
+ const fabricOptions_1 = require("./fabricOptions");
20
21
  const modelFiles_1 = require("./modelFiles");
21
22
  const optionValidation_1 = require("./optionValidation");
22
23
  const pocketConfig_1 = require("./pocketConfig");
23
24
  const { platform } = bareOs;
24
25
  const MIN_OUTPUT_SAMPLE_RATE = 8000;
25
26
  const MAX_OUTPUT_SAMPLE_RATE = 192000;
27
+ const LAVASR_COREML_COMPUTE_UNITS = [
28
+ "CPU_ONLY",
29
+ "CPU_AND_GPU",
30
+ "CPU_AND_NE",
31
+ "ALL",
32
+ ];
33
+ function validateLavaSRCoreML(options, ggufPath, stage) {
34
+ const modelPath = options?.coremlModelPath;
35
+ const computeUnits = options?.coremlComputeUnits;
36
+ if (modelPath === undefined && computeUnits === undefined)
37
+ return;
38
+ if (typeof modelPath !== "string" || modelPath.trim().length === 0) {
39
+ throw new Error(`${stage}.coremlModelPath must be a non-empty string`);
40
+ }
41
+ if (typeof computeUnits !== "string" ||
42
+ !LAVASR_COREML_COMPUTE_UNITS.includes(computeUnits)) {
43
+ throw new Error(`${stage}.coremlComputeUnits must be CPU_ONLY, CPU_AND_GPU, CPU_AND_NE or ALL`);
44
+ }
45
+ if (!ggufPath) {
46
+ throw new Error(`${stage} Core ML requires the corresponding LavaSR GGUF path`);
47
+ }
48
+ }
26
49
  function resolveDefaultLazySessionLoading(lazySessionLoading) {
27
50
  if (lazySessionLoading != null)
28
51
  return lazySessionLoading;
@@ -111,13 +134,24 @@ function settingsFromOptions(options) {
111
134
  maxFrames: options.maxFrames,
112
135
  minNewTokens: options.minNewTokens,
113
136
  normalizeNumbers: options.normalizeNumbers,
137
+ coremlDecoder: options.coremlDecoder,
138
+ coremlComputeUnits: options.coremlComputeUnits,
139
+ coremlStrict: options.coremlStrict,
114
140
  ...(0, moss_1.mossVoiceSettings)(options),
115
141
  };
116
142
  }
117
143
  function resolveBackendPaths(files, options, config) {
144
+ const enhancerGgufPath = resolveEnhancerGgufPath(files, options.enhancer);
145
+ const denoiserGgufPath = resolveDenoiserGgufPath(files, options.denoiser);
146
+ validateLavaSRCoreML(options.enhancer, enhancerGgufPath, "enhancer");
147
+ validateLavaSRCoreML(options.denoiser, denoiserGgufPath, "denoiser");
118
148
  return {
119
- enhancerGgufPath: resolveEnhancerGgufPath(files, options.enhancer),
120
- denoiserGgufPath: resolveDenoiserGgufPath(files, options.denoiser),
149
+ enhancerGgufPath,
150
+ denoiserGgufPath,
151
+ enhancerCoremlModelPath: options.enhancer?.coremlModelPath,
152
+ enhancerCoremlComputeUnits: options.enhancer?.coremlComputeUnits,
153
+ denoiserCoremlModelPath: options.denoiser?.coremlModelPath,
154
+ denoiserCoremlComputeUnits: options.denoiser?.coremlComputeUnits,
121
155
  backendsDir: (0, modelFiles_1.firstNonEmpty)(options.backendsDir, config.backendsDir),
122
156
  openclCacheDir: (0, modelFiles_1.firstNonEmpty)(options.openclCacheDir, config.openclCacheDir),
123
157
  vulkanCacheDir: (0, modelFiles_1.firstNonEmpty)(options.vulkanCacheDir, config.vulkanCacheDir),
@@ -139,6 +173,7 @@ function resolveEngineSetup(options) {
139
173
  const config = { ...(options.config || {}) };
140
174
  const outputSampleRate = validateOutputSampleRate(config.outputSampleRate);
141
175
  const engineType = (0, engineDetection_1.detectEngineType)(options.engine, files);
176
+ (0, fabricOptions_1.validateBackendOption)(engineType, config.backend);
142
177
  const mossVoice = (0, moss_1.mossVoiceSettings)(options);
143
178
  const pocket = engineType === engineIds_1.ENGINE_POCKET
144
179
  ? resolvePocketSetup(files, options, config)
@@ -150,6 +185,7 @@ function resolveEngineSetup(options) {
150
185
  dialogue: mossVoice.dialogueReferences !== undefined,
151
186
  });
152
187
  const settings = settingsFromOptions(options);
188
+ Object.assign(settings, (0, fabricOptions_1.resolveFabricOptions)(engineType, options));
153
189
  const backend = resolveBackendPaths(files, options, config);
154
190
  const setup = {
155
191
  engineType,
package/lib/types.d.ts CHANGED
@@ -1,22 +1,28 @@
1
1
  import type { TTSConditioningFields } from "./conditioning";
2
2
  import type { EngineType } from "./engineIds";
3
+ import type { FabricOptions } from "./fabricOptions";
3
4
  import type { TTSGgmlRuntimeConfig } from "./engineSetup";
4
5
  import type { Audio8VoiceFields } from "./engines/audio8";
5
6
  import type { CosyvoiceInstruct, CosyvoiceJobFields } from "./engines/cosyvoice";
6
7
  import type { MossSoundEffectFields } from "./engines/mossSoundEffect";
7
8
  import type { MossSpeechFields } from "./engines/mossSpeech";
8
- import type { ParlerDescriptionFields } from "./engines/parler";
9
+ import type { ParlerCoremlFields, ParlerDescriptionFields } from "./engines/parler";
9
10
  import type { TTSGgmlFiles } from "./modelFiles";
10
11
  import type { SentenceDelimiterPreset } from "./textStreamAccumulator";
11
- export type { Audio8VoiceFields, CosyvoiceInstruct, CosyvoiceJobFields, MossSoundEffectFields, MossSpeechFields, ParlerDescriptionFields, TTSConditioningFields, TTSGgmlFiles, TTSGgmlRuntimeConfig, };
12
+ export type { Audio8VoiceFields, CosyvoiceInstruct, CosyvoiceJobFields, MossSoundEffectFields, MossSpeechFields, ParlerDescriptionFields, ParlerCoremlFields, TTSConditioningFields, TTSGgmlFiles, TTSGgmlRuntimeConfig, };
12
13
  export type { MossSpeechJobFields, MossSpeechMessage, MossSpeechPcm, } from "./engines/mossSpeech";
13
14
  export type { Audio8SamplingFields } from "./engines/audio8";
15
+ export type LavaSRCoreMLComputeUnits = "CPU_ONLY" | "CPU_AND_GPU" | "CPU_AND_NE" | "ALL";
16
+ export interface LavaSRCoreMLOptions {
17
+ coremlModelPath?: string;
18
+ coremlComputeUnits?: LavaSRCoreMLComputeUnits;
19
+ }
14
20
  /**
15
21
  * LavaSR enhancer config. The discriminated `type` leaves room for future
16
22
  * enhancer kinds; v1 ships `lavasr`. Enhancement is enabled by providing a
17
23
  * GGUF path here or via `files.lavasrEnhancer`.
18
24
  */
19
- export interface LavaSREnhancerOptions {
25
+ export interface LavaSREnhancerOptions extends LavaSRCoreMLOptions {
20
26
  type: "lavasr";
21
27
  /** Enhancer GGUF path (alternative to `files.lavasrEnhancer`). */
22
28
  enhancerPath?: string;
@@ -26,12 +32,12 @@ export interface LavaSREnhancerOptions {
26
32
  * is enabled by providing a GGUF path here or via `files.lavasrDenoiser`.
27
33
  * It runs before the enhancer and preserves the sample rate.
28
34
  */
29
- export interface LavaSRDenoiserOptions {
35
+ export interface LavaSRDenoiserOptions extends LavaSRCoreMLOptions {
30
36
  type: "lavasr";
31
37
  /** Denoiser GGUF path (alternative to `files.lavasrDenoiser`). */
32
38
  denoiserPath?: string;
33
39
  }
34
- export interface TTSGgmlOptions extends ParlerDescriptionFields, Audio8VoiceFields, TTSConditioningFields {
40
+ export interface TTSGgmlOptions extends FabricOptions, ParlerDescriptionFields, ParlerCoremlFields, Audio8VoiceFields, TTSConditioningFields {
35
41
  /** Pocket: generation/context controls; native sampling uses a portable RNG. */
36
42
  maxTokens?: number;
37
43
  noiseClamp?: number;
@@ -68,6 +74,7 @@ export interface TTSGgmlOptions extends ParlerDescriptionFields, Audio8VoiceFiel
68
74
  * front at this length, so the cap directly bounds memory. Pass 0 to use
69
75
  * the GGUF's full context; negative values are rejected.
70
76
  * Pocket: FlowLM context capacity; accepts integers from 1 to 8192.
77
+ * MOSS / TTSD: context capacity; omission retains the native value.
71
78
  */
72
79
  nCtx?: number;
73
80
  /**
@@ -274,12 +281,13 @@ export interface TTSGgmlOptions extends ParlerDescriptionFields, Audio8VoiceFiel
274
281
  * ~86 frames/s, Audio8 ~21.5.
275
282
  */
276
283
  maxFrames?: number;
277
- /** Audio8: take the argmax instead of sampling. */
284
+ /** Audio8 / CosyVoice3: take the argmax instead of sampling. */
278
285
  greedy?: boolean;
279
286
  /**
280
- * MOSS: target length in codec frames (12.5 per second), from 0 to 2015
281
- * (about 161 s); 0 or unset keeps the length free. Set at construction or
282
- * with `reload()`, not per call.
287
+ * MOSS: target length in codec frames (12.5 per second), up to
288
+ * maxNewTokens minus 33 native delay/EOS rows. The native token budget is
289
+ * 2048 when omitted. Zero keeps the length free. Set at construction or
290
+ * with reload(), not per call.
283
291
  */
284
292
  durationTokens?: number;
285
293
  /**
@@ -304,6 +312,7 @@ export interface InferenceState {
304
312
  destroyed: boolean;
305
313
  }
306
314
  export interface TTSOutputChunk {
315
+ speechTokenIds?: number[];
307
316
  chunkIndex?: number;
308
317
  isLast?: boolean;
309
318
  /** Signed 16-bit mono PCM audio payload. */
@@ -318,6 +327,7 @@ export interface TTSOutputChunk {
318
327
  sampleRate?: number;
319
328
  }
320
329
  export interface NativeOutputChunk {
330
+ speechTokenIds?: number[];
321
331
  outputArray: Int16Array;
322
332
  text?: string;
323
333
  isLast?: boolean;
@@ -330,15 +340,15 @@ export interface RuntimeStats {
330
340
  realTimeFactor: number;
331
341
  audioDurationMs: number;
332
342
  totalSamples: number;
333
- /** Active compute device after load-time backend selection. 0 = CPU, 1 = GPU. */
343
+ /** Active compute device: 0 = CPU, 1 = GPU, 3 = NPU, 99 = other. */
334
344
  backendDevice?: number;
335
- /** Stable backend code: 0=CPU, 1=Metal, 2=CUDA, 3=Vulkan, 4=OpenCL, 99=other GPU. */
345
+ /** Backend: 0=CPU, 1=Metal, 2=CUDA, 3=Vulkan, 4=OpenCL, 5=WebGPU, 99=other. */
336
346
  backendId?: number;
337
- /** LavaSR enhancer compute device. -1 = not loaded, 0 = CPU, 1 = GPU. */
347
+ /** LavaSR enhancer device. -1 = off, 0 = CPU, 1 = GPU, 2 = Core ML-managed. */
338
348
  enhancerBackendDevice?: number;
339
349
  /** LavaSR enhancer backend code, using the same values as `backendId`. */
340
350
  enhancerBackendId?: number;
341
- /** LavaSR denoiser compute device. -1 = not loaded, 0 = CPU, 1 = GPU. */
351
+ /** LavaSR denoiser device. -1 = off, 0 = CPU, 1 = GPU, 2 = Core ML-managed. */
342
352
  denoiserBackendDevice?: number;
343
353
  /** LavaSR denoiser backend code, using the same values as `backendId`. */
344
354
  denoiserBackendId?: number;
@@ -366,6 +376,17 @@ export interface RuntimeStats {
366
376
  * Streams report the last chunk that supplied this field, not a sum.
367
377
  */
368
378
  codecOnCoreml?: number;
379
+ /** Parler only: 1 while the decoder sidecar is attached; 0 after fallback retires it. */
380
+ decoderSidecarLoaded?: number;
381
+ /** Parler only: 1 while the DAC sidecar is attached; 0 after fallback retires it. */
382
+ dacSidecarLoaded?: number;
383
+ /** Parler only: 1 when this synthesis used the Core ML decoder; 0 for ggml or mixed fallback. */
384
+ decoderOnCoreml?: number;
385
+ /** Parler only: 1 when this synthesis used the Core ML DAC; 0 for ggml or mixed fallback.
386
+ * Sentence streams retain the latest availability flags and require all reported sentences to use Core ML for a use flag of 1.
387
+ * These flags do not identify the hardware Core ML scheduled work on.
388
+ */
389
+ dacOnCoreml?: number;
369
390
  /** Chatterbox only: T3 decode wall time of the last synthesis, in ms. */
370
391
  t3Ms?: number;
371
392
  /** Chatterbox only: S3Gen + HiFT wall time of the last synthesis, in ms. */