@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.
Files changed (42) hide show
  1. package/README.md +317 -33
  2. package/docs/pocket-tts.md +201 -0
  3. package/examples/moss-dialogue-tts.js +113 -0
  4. package/examples/moss-tts.js +154 -0
  5. package/examples/pocket-tts.js +51 -0
  6. package/examples/supertonic-chunk-stream-tts.js +156 -0
  7. package/examples/supertonic-sentence-stream-tts.js +4 -4
  8. package/index.d.ts +352 -29
  9. package/index.js +698 -52
  10. package/lib/fit.d.ts +127 -0
  11. package/lib/fit.js +29 -0
  12. package/lib/pocketConfig.d.ts +10 -0
  13. package/lib/pocketConfig.js +145 -0
  14. package/package.json +14 -9
  15. package/test/integration/addon.test.js +5 -0
  16. package/test/integration/audio8.test.js +7 -0
  17. package/test/integration/chatterbox-mtl.test.js +6 -0
  18. package/test/integration/chatterbox-speed.test.js +13 -2
  19. package/test/integration/cosyvoice3-clone.test.js +16 -1
  20. package/test/integration/cosyvoice3-lavasr.test.js +6 -0
  21. package/test/integration/cosyvoice3.test.js +19 -0
  22. package/test/integration/fit.test.js +174 -0
  23. package/test/integration/gpu-smoke.test.js +13 -0
  24. package/test/integration/lavasr-enhancer.test.js +104 -3
  25. package/test/integration/moss.test.js +282 -0
  26. package/test/integration/multiple-runs.test.js +18 -2
  27. package/test/integration/output-sample-rate.test.js +6 -0
  28. package/test/integration/parler-wer.test.js +7 -1
  29. package/test/integration/parler.test.js +27 -3
  30. package/test/integration/pocket.integration.test.js +156 -0
  31. package/test/integration/supertonic-mtl.test.js +2 -0
  32. package/test/integration/supertonic.test.js +70 -0
  33. package/test/integration/supertonic3-quant.test.js +2 -0
  34. package/test/integration/voice-controls.test.js +56 -0
  35. package/test/mobile/integration.auto.cjs +16 -1
  36. package/test/mobile/pocket-worklet-host.cpp +92 -0
  37. package/test/mobile/pocket-worklet.cjs +47 -0
  38. package/test/mobile/test-groups.json +8 -4
  39. package/test/utils/runCosyvoiceTTS.js +1 -0
  40. package/test/utils/runTTS.js +6 -11
  41. package/tts.d.ts +13 -2
  42. package/tts.js +3 -2
package/index.js CHANGED
@@ -36,12 +36,18 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  const bareOs = require("bare-os");
37
37
  const path = require("bare-path");
38
38
  const fs = require("bare-fs");
39
- const QvacLogger = require("@qvac/logging");
39
+ const loggingModule = require("@qvac/logging");
40
+ // Published logging releases expose a CJS constructor; the workspace exposes
41
+ // an ESM default. Bare require(ESM) returns a namespace without __esModule.
42
+ const loggingExport = loggingModule;
43
+ const QvacLogger = typeof loggingExport === "function" ? loggingExport : loggingExport.default;
40
44
  /* eslint-enable @typescript-eslint/no-require-imports */
41
45
  const infer_base_1 = require("@qvac/infer-base");
42
46
  const tts_1 = require("./tts");
43
47
  const errorModule = __importStar(require("./lib/error"));
48
+ const pocketConfig_1 = require("./lib/pocketConfig");
44
49
  const backends_1 = require("./lib/backends");
50
+ const fit_1 = require("./lib/fit");
45
51
  const textChunker_1 = require("./lib/textChunker");
46
52
  const textStreamAccumulator_1 = require("./lib/textStreamAccumulator");
47
53
  const { platform } = bareOs;
@@ -55,6 +61,8 @@ const ENGINE_SUPERTONIC = "supertonic";
55
61
  const ENGINE_COSYVOICE3 = "cosyvoice3";
56
62
  const ENGINE_PARLER = "parler";
57
63
  const ENGINE_AUDIO8 = "audio8";
64
+ const ENGINE_MOSS = "moss";
65
+ const ENGINE_POCKET = "pocket";
58
66
  const MIN_OUTPUT_SAMPLE_RATE = 8000;
59
67
  const MAX_OUTPUT_SAMPLE_RATE = 192000;
60
68
  const CHATTERBOX_T3_TURBO = "chatterbox-t3-turbo.gguf";
@@ -237,6 +245,17 @@ const AUDIO8_DECODER_RE = /^audio8-codec-decoder(-[a-z0-9_]+)?\.gguf$/i;
237
245
  const AUDIO8_ENCODER_RE = /^audio8-codec-encoder(-[a-z0-9_]+)?\.gguf$/i;
238
246
  const AUDIO8_QUANT_ORDER = ["q8_0", "f16", "q4_0", "f32"];
239
247
  const AUDIO8_VOICE_KEYS = ["referenceAudio", "referenceText"];
248
+ const MOSS_BACKBONE_RE = /^moss-tts-delay(-[a-z0-9_]+)?\.gguf$/i;
249
+ const MOSS_DIALOGUE_BACKBONE_RE = /^moss-ttsd(-[a-z0-9_]+)?\.gguf$/i;
250
+ const MOSS_DECODER_RE = /^moss-codec-decoder(-[a-z0-9_]+)?\.gguf$/i;
251
+ const MOSS_ENCODER_RE = /^moss-codec-encoder(-[a-z0-9_]+)?\.gguf$/i;
252
+ const MOSS_NATIVE_SAMPLE_RATE = 24000;
253
+ const MOSS_FRAMES_PER_SECOND = 12.5;
254
+ const MOSS_MAX_NEW_TOKENS = 2048;
255
+ const MOSS_MAX_CHANNELS = 32;
256
+ const MOSS_TERMINATION_ROWS = 2;
257
+ const MOSS_MAX_DURATION_TOKENS = MOSS_MAX_NEW_TOKENS - (MOSS_MAX_CHANNELS - 1) - MOSS_TERMINATION_ROWS;
258
+ const MOSS_INSTANCE_VOICE_KEYS = ["referenceAudio", "dialogueReferences"];
240
259
  /** Engines that draw tokens, and so accept temperature / topK / topP / maxFrames. */
241
260
  const SAMPLING_ENGINES = [ENGINE_PARLER, ENGINE_AUDIO8];
242
261
  // Per-engine supported subsets, mirroring controls::supported_emotions() /
@@ -248,24 +267,35 @@ const ENGINE_EMOTIONS = {
248
267
  [ENGINE_COSYVOICE3]: ["anger", "happy", "neutral", "sad"],
249
268
  [ENGINE_SUPERTONIC]: [],
250
269
  [ENGINE_CHATTERBOX]: [],
270
+ [ENGINE_POCKET]: [],
251
271
  [ENGINE_AUDIO8]: [],
272
+ [ENGINE_MOSS]: [],
252
273
  };
253
274
  const ENGINE_PACES = {
254
275
  [ENGINE_PARLER]: PACES, // rendered into the training caption
255
276
  [ENGINE_COSYVOICE3]: PACES, // slow/fast -> instruct; moderate -> none
256
277
  [ENGINE_SUPERTONIC]: PACES, // mapped onto the duration multiplier
257
278
  [ENGINE_CHATTERBOX]: [], // time-stretch only; use `speed`
279
+ [ENGINE_POCKET]: [],
258
280
  [ENGINE_AUDIO8]: [], // no rate control at all
281
+ [ENGINE_MOSS]: [],
259
282
  };
260
283
  // Which channels an engine can change per call. Supertonic takes its pace
261
284
  // through EngineOptions when the engine is built, and tts-cpp exposes no
262
285
  // per-call surface for it, so it only moves at construction or reload().
286
+ // tts-cpp engine names (tts-cpp/voice_controls.h engine_name) that differ from
287
+ // this package's engine types.
288
+ const FABRIC_ENGINE_TYPES = {
289
+ cosyvoice: ENGINE_COSYVOICE3,
290
+ };
263
291
  const ENGINE_PER_CALL_CONDITIONING = {
264
292
  [ENGINE_PARLER]: CONDITIONING_KEYS,
265
293
  [ENGINE_COSYVOICE3]: CONDITIONING_KEYS,
266
294
  [ENGINE_SUPERTONIC]: [],
267
295
  [ENGINE_CHATTERBOX]: [],
296
+ [ENGINE_POCKET]: [],
268
297
  [ENGINE_AUDIO8]: [],
298
+ [ENGINE_MOSS]: [],
269
299
  };
270
300
  function normalizeError(error) {
271
301
  return typeof error === "string"
@@ -384,7 +414,7 @@ function readDirSafe(modelDir) {
384
414
  return [];
385
415
  }
386
416
  }
387
- function audio8QuantRank(name, pattern) {
417
+ function ggufQuantRank(name, pattern) {
388
418
  const match = name.match(pattern);
389
419
  if (!match)
390
420
  return Number.MAX_SAFE_INTEGER;
@@ -394,16 +424,16 @@ function audio8QuantRank(name, pattern) {
394
424
  return index === -1 ? AUDIO8_QUANT_ORDER.length + 1 : index + 1;
395
425
  }
396
426
  /**
397
- * Find one Audio8 GGUF in `modelDir`, preferring the quant tier that reads
398
- * best for its size (q8_0 > f16 > q4_0 > f32).
427
+ * Find one Audio8 or MOSS GGUF in `modelDir`, preferring the quant tier that
428
+ * reads best for its size (q8_0 > f16 > q4_0 > f32).
399
429
  */
400
- function findAudio8InDir(modelDir, pattern) {
430
+ function findQuantRankedGguf(modelDir, pattern) {
401
431
  if (!modelDir)
402
432
  return undefined;
403
433
  const matches = readDirSafe(modelDir).filter((name) => pattern.test(name));
404
434
  if (matches.length === 0)
405
435
  return undefined;
406
- matches.sort((left, right) => audio8QuantRank(left, pattern) - audio8QuantRank(right, pattern));
436
+ matches.sort((left, right) => ggufQuantRank(left, pattern) - ggufQuantRank(right, pattern));
407
437
  return path.join(modelDir, matches[0]);
408
438
  }
409
439
  /**
@@ -566,6 +596,10 @@ function normalizeGgmlFiles(files) {
566
596
  return {};
567
597
  return {
568
598
  modelDir: firstNonEmpty(files.modelDir),
599
+ pocketFlowModel: firstNonEmpty(files.pocketFlowModel),
600
+ pocketMimiModel: firstNonEmpty(files.pocketMimiModel),
601
+ pocketFrontend: firstNonEmpty(files.pocketFrontend),
602
+ pocketVoice: firstNonEmpty(files.pocketVoice),
569
603
  t3Model: firstNonEmpty(files.t3Model, files.t3ModelPath, files.t3),
570
604
  s3genModel: firstNonEmpty(files.s3genModel, files.s3genModelPath, files.s3gen),
571
605
  supertonicModel: firstNonEmpty(files.supertonicModel, files.supertonicModelPath, files.supertonic),
@@ -577,10 +611,16 @@ function normalizeGgmlFiles(files) {
577
611
  cosyvoiceHiftModel: firstNonEmpty(files.cosyvoiceHiftModel, files.cosyvoiceHiftModelPath),
578
612
  cosyvoiceS3tokModel: firstNonEmpty(files.cosyvoiceS3tokModel, files.cosyvoiceS3tokModelPath),
579
613
  cosyvoiceCampplusModel: firstNonEmpty(files.cosyvoiceCampplusModel, files.cosyvoiceCampplusModelPath),
614
+ cosyvoiceVocab: firstNonEmpty(files.cosyvoiceVocab, files.cosyvoiceVocabPath),
615
+ cosyvoiceMerges: firstNonEmpty(files.cosyvoiceMerges, files.cosyvoiceMergesPath),
616
+ cosyvoiceVoiceModel: firstNonEmpty(files.cosyvoiceVoiceModel, files.cosyvoiceVoiceModelPath),
580
617
  parlerModel: firstNonEmpty(files.parlerModel, files.parlerModelPath, files.parler),
581
618
  audio8Lm: firstNonEmpty(files.audio8Lm, files.audio8LmPath),
582
619
  audio8CodecDecoder: firstNonEmpty(files.audio8CodecDecoder, files.audio8CodecDecoderPath),
583
620
  audio8CodecEncoder: firstNonEmpty(files.audio8CodecEncoder, files.audio8CodecEncoderPath),
621
+ mossBackbone: firstNonEmpty(files.mossBackbone, files.mossBackbonePath),
622
+ mossCodecDecoder: firstNonEmpty(files.mossCodecDecoder, files.mossCodecDecoderPath),
623
+ mossCodecEncoder: firstNonEmpty(files.mossCodecEncoder, files.mossCodecEncoderPath),
584
624
  voicesDir: firstNonEmpty(files.voicesDir),
585
625
  lavasrEnhancer: firstNonEmpty(files.lavasrEnhancer),
586
626
  lavasrDenoiser: firstNonEmpty(files.lavasrDenoiser),
@@ -593,13 +633,17 @@ function detectEngineType(engine, files) {
593
633
  engine === ENGINE_SUPERTONIC ||
594
634
  engine === ENGINE_COSYVOICE3 ||
595
635
  engine === ENGINE_PARLER ||
596
- engine === ENGINE_AUDIO8) {
636
+ engine === ENGINE_AUDIO8 ||
637
+ engine === ENGINE_MOSS ||
638
+ engine === ENGINE_POCKET) {
597
639
  return engine;
598
640
  }
599
641
  if (engine != null && engine !== "") {
600
642
  throw new Error("tts-ggml: 'engine' option must be 'chatterbox', 'supertonic', " +
601
- `'cosyvoice3', 'parler' or 'audio8' (got '${String(engine)}')`);
643
+ `'cosyvoice3', 'parler', 'audio8', 'moss' or 'pocket' (got '${String(engine)}')`);
602
644
  }
645
+ if (files.pocketFlowModel || files.pocketMimiModel)
646
+ return ENGINE_POCKET;
603
647
  // Explicit CosyVoice3 files/dir take precedence over shared-modelDir sniffing.
604
648
  if (files.cosyvoiceModelDir || files.cosyvoiceLlmModel) {
605
649
  return ENGINE_COSYVOICE3;
@@ -612,6 +656,8 @@ function detectEngineType(engine, files) {
612
656
  return ENGINE_PARLER;
613
657
  if (files.audio8Lm || files.audio8CodecDecoder)
614
658
  return ENGINE_AUDIO8;
659
+ if (files.mossBackbone || files.mossCodecDecoder)
660
+ return ENGINE_MOSS;
615
661
  if (files.modelDir) {
616
662
  if (dirHasCosyvoice3(files.modelDir))
617
663
  return ENGINE_COSYVOICE3;
@@ -626,8 +672,15 @@ function detectEngineType(engine, files) {
626
672
  return ENGINE_SUPERTONIC;
627
673
  if (findParlerInDir(files.modelDir))
628
674
  return ENGINE_PARLER;
629
- if (findAudio8InDir(files.modelDir, AUDIO8_LM_RE))
675
+ if (findQuantRankedGguf(files.modelDir, AUDIO8_LM_RE))
630
676
  return ENGINE_AUDIO8;
677
+ if (findQuantRankedGguf(files.modelDir, MOSS_BACKBONE_RE))
678
+ return ENGINE_MOSS;
679
+ if (findQuantRankedGguf(files.modelDir, MOSS_DIALOGUE_BACKBONE_RE)) {
680
+ return ENGINE_MOSS;
681
+ }
682
+ if (fileExistsSafe(path.join(files.modelDir, "flow-lm.gguf")))
683
+ return ENGINE_POCKET;
631
684
  }
632
685
  return ENGINE_CHATTERBOX;
633
686
  }
@@ -751,6 +804,34 @@ function assertAudio8SamplingFinite(sampling, where) {
751
804
  throw new Error(`tts-ggml: ${where}: ${field} must be a finite number`);
752
805
  }
753
806
  }
807
+ function isValidDurationTokens(value) {
808
+ return Number.isInteger(value) && value >= 0 && value <= MOSS_MAX_DURATION_TOKENS;
809
+ }
810
+ function assertMossDurationTokens(value, where) {
811
+ if (value == null || isValidDurationTokens(value))
812
+ return;
813
+ throw new Error(`tts-ggml: ${where}: durationTokens must be an integer from 0 to ` +
814
+ `${MOSS_MAX_DURATION_TOKENS} (0 = free length)`);
815
+ }
816
+ function copyDialogueReferences(references) {
817
+ if (references == null)
818
+ return undefined;
819
+ return Array.isArray(references)
820
+ ? [...references]
821
+ : references;
822
+ }
823
+ function isNonEmptyString(value) {
824
+ return typeof value === "string" && value !== "";
825
+ }
826
+ function assertDialogueReferenceList(references) {
827
+ if (Array.isArray(references) &&
828
+ references.length > 0 &&
829
+ references.every(isNonEmptyString)) {
830
+ return;
831
+ }
832
+ throw new Error("tts-ggml: dialogueReferences must be a non-empty array of WAV paths, " +
833
+ "one per speaker");
834
+ }
754
835
  function assertNotNegative(value, name, zeroMeans, where) {
755
836
  if (value === undefined || value >= 0)
756
837
  return;
@@ -823,7 +904,7 @@ function computeSentenceStreamStats(chunks, accumulator, countsFrames) {
823
904
  }
824
905
  /**
825
906
  * GGML-backed TTS via the `tts-cpp` library. Wraps the chatterbox,
826
- * supertonic, parler, cosyvoice3 and audio8 engines behind a single
907
+ * supertonic, parler, cosyvoice3, audio8 and moss engines behind a single
827
908
  * engine-agnostic JavaScript surface. Engine type is auto-detected from
828
909
  * `files` or selected explicitly with `engine`.
829
910
  *
@@ -840,6 +921,8 @@ class TTSGgml {
840
921
  static ENGINE_COSYVOICE3 = ENGINE_COSYVOICE3;
841
922
  static ENGINE_PARLER = ENGINE_PARLER;
842
923
  static ENGINE_AUDIO8 = ENGINE_AUDIO8;
924
+ static ENGINE_MOSS = ENGINE_MOSS;
925
+ static ENGINE_POCKET = ENGINE_POCKET;
843
926
  opts;
844
927
  exclusiveRun;
845
928
  logger;
@@ -850,6 +933,12 @@ class TTSGgml {
850
933
  _ttsInferenceQueueWaiter;
851
934
  _sentenceStreamCtx;
852
935
  _config;
936
+ _pocketJobPending = null;
937
+ _pocketCancelPromise = null;
938
+ _pocketLifecycleInProgress = false;
939
+ _pocketParams = null;
940
+ _pocketOptions = {};
941
+ _pocketFiles = {};
853
942
  _lazySessionLoading;
854
943
  _outputSampleRate;
855
944
  _engineType;
@@ -877,6 +966,23 @@ class TTSGgml {
877
966
  _streamLeftContextTokens;
878
967
  _cfmSteps;
879
968
  _cfgRate;
969
+ _batchCfmSteps;
970
+ _nPredict;
971
+ _maxSentenceChars;
972
+ _crossfadeMs;
973
+ _repeatPenalty;
974
+ _exaggeration;
975
+ _cfgWeight;
976
+ _minP;
977
+ _streamChunkTolerancePct;
978
+ _streamMinChunkTokens;
979
+ _voiceJsonPath;
980
+ _prewarmText;
981
+ _vulkanDevice;
982
+ _flowCutPrompt;
983
+ _cosyvoiceVocabPath;
984
+ _cosyvoiceMergesPath;
985
+ _cosyvoiceVoiceModelPath;
880
986
  _promptText;
881
987
  _instruct;
882
988
  _voice;
@@ -892,6 +998,11 @@ class TTSGgml {
892
998
  _audio8LmPath;
893
999
  _audio8CodecDecoderPath;
894
1000
  _audio8CodecEncoderPath;
1001
+ _mossBackbonePath;
1002
+ _mossCodecDecoderPath;
1003
+ _mossCodecEncoderPath;
1004
+ _dialogueReferences;
1005
+ _durationTokens;
895
1006
  _referenceText;
896
1007
  _greedy;
897
1008
  _description;
@@ -921,7 +1032,7 @@ class TTSGgml {
921
1032
  this._sentenceStreamCtx = null;
922
1033
  this._ttsInferenceQueueWaiter = Promise.resolve();
923
1034
  this._job = (0, infer_base_1.createJobHandler)({
924
- cancel: () => this._optionalAddon()?.cancel(),
1035
+ cancel: () => this._engineType === ENGINE_POCKET ? this.cancel() : this._optionalAddon()?.cancel(),
925
1036
  });
926
1037
  this._runExclusive = this.exclusiveRun
927
1038
  ? (0, infer_base_1.exclusiveRunQueue)()
@@ -933,6 +1044,15 @@ class TTSGgml {
933
1044
  this._lazySessionLoading = resolveDefaultLazySessionLoading(options.lazySessionLoading);
934
1045
  this._outputSampleRate = validateOutputSampleRate(this._config.outputSampleRate);
935
1046
  this._engineType = detectEngineType(options.engine, normalizedFiles);
1047
+ this._assignMossVoiceOptions(options);
1048
+ if (this._engineType === ENGINE_POCKET) {
1049
+ if (normalizedFiles.lavasrEnhancer || normalizedFiles.lavasrDenoiser) {
1050
+ throw new Error("Pocket does not support LavaSR enhancement or denoising");
1051
+ }
1052
+ this._pocketFiles = normalizedFiles;
1053
+ this._pocketOptions = { ...options };
1054
+ this._pocketParams = (0, pocketConfig_1.buildPocketParams)(normalizedFiles, options, this._config);
1055
+ }
936
1056
  this._resolveEngineAndModelPaths(normalizedFiles);
937
1057
  this._mecabDictPath = firstNonEmpty(options.mecabDictPath, options.mecabDictDir, normalizedFiles.mecabDictDir);
938
1058
  this._cangjieTsvPath = firstNonEmpty(options.cangjieTsvPath, normalizedFiles.cangjieTsvPath);
@@ -951,6 +1071,8 @@ class TTSGgml {
951
1071
  }
952
1072
  _resolveEngineAndModelPaths(files) {
953
1073
  this._voicesDir = files.voicesDir;
1074
+ if (this._engineType === ENGINE_POCKET)
1075
+ return;
954
1076
  if (this._engineType === ENGINE_COSYVOICE3) {
955
1077
  // CosyVoice3 discovers its sub-model GGUFs from a model directory; the
956
1078
  // native engine resolves the individual components. Explicit
@@ -961,6 +1083,9 @@ class TTSGgml {
961
1083
  this._cosyvoiceHiftModelPath = files.cosyvoiceHiftModel;
962
1084
  this._cosyvoiceS3tokModelPath = files.cosyvoiceS3tokModel;
963
1085
  this._cosyvoiceCampplusModelPath = files.cosyvoiceCampplusModel;
1086
+ this._cosyvoiceVocabPath = files.cosyvoiceVocab;
1087
+ this._cosyvoiceMergesPath = files.cosyvoiceMerges;
1088
+ this._cosyvoiceVoiceModelPath = files.cosyvoiceVoiceModel;
964
1089
  return;
965
1090
  }
966
1091
  if (this._engineType === ENGINE_SUPERTONIC) {
@@ -977,6 +1102,10 @@ class TTSGgml {
977
1102
  this._resolveAudio8ModelPaths(files);
978
1103
  return;
979
1104
  }
1105
+ if (this._engineType === ENGINE_MOSS) {
1106
+ this._resolveMossModelPaths(files);
1107
+ return;
1108
+ }
980
1109
  if (files.modelDir) {
981
1110
  const resolved = resolveChatterboxModelDirPaths(files.modelDir);
982
1111
  this._t3ModelPath = firstNonEmpty(files.t3Model, resolved.t3);
@@ -988,9 +1117,26 @@ class TTSGgml {
988
1117
  }
989
1118
  }
990
1119
  _resolveAudio8ModelPaths(files) {
991
- this._audio8LmPath = firstNonEmpty(files.audio8Lm, findAudio8InDir(files.modelDir, AUDIO8_LM_RE));
992
- this._audio8CodecDecoderPath = firstNonEmpty(files.audio8CodecDecoder, findAudio8InDir(files.modelDir, AUDIO8_DECODER_RE));
993
- this._audio8CodecEncoderPath = firstNonEmpty(files.audio8CodecEncoder, findAudio8InDir(files.modelDir, AUDIO8_ENCODER_RE));
1120
+ this._audio8LmPath = firstNonEmpty(files.audio8Lm, findQuantRankedGguf(files.modelDir, AUDIO8_LM_RE));
1121
+ this._audio8CodecDecoderPath = firstNonEmpty(files.audio8CodecDecoder, findQuantRankedGguf(files.modelDir, AUDIO8_DECODER_RE));
1122
+ this._audio8CodecEncoderPath = firstNonEmpty(files.audio8CodecEncoder, findQuantRankedGguf(files.modelDir, AUDIO8_ENCODER_RE));
1123
+ }
1124
+ _mossBackbonePatterns() {
1125
+ return this._dialogueReferences
1126
+ ? [MOSS_DIALOGUE_BACKBONE_RE]
1127
+ : [MOSS_BACKBONE_RE, MOSS_DIALOGUE_BACKBONE_RE];
1128
+ }
1129
+ _findMossBackbone(modelDir) {
1130
+ return firstNonEmpty(...this._mossBackbonePatterns().map((pattern) => findQuantRankedGguf(modelDir, pattern)));
1131
+ }
1132
+ _resolveMossModelPaths(files) {
1133
+ this._mossBackbonePath = firstNonEmpty(files.mossBackbone, this._findMossBackbone(files.modelDir));
1134
+ this._mossCodecDecoderPath = firstNonEmpty(files.mossCodecDecoder, findQuantRankedGguf(files.modelDir, MOSS_DECODER_RE));
1135
+ this._mossCodecEncoderPath = firstNonEmpty(files.mossCodecEncoder, findQuantRankedGguf(files.modelDir, MOSS_ENCODER_RE));
1136
+ }
1137
+ _assignMossVoiceOptions(options) {
1138
+ this._dialogueReferences = copyDialogueReferences(options.dialogueReferences);
1139
+ this._durationTokens = options.durationTokens ?? undefined;
994
1140
  }
995
1141
  _assignSynthesisOptions(options) {
996
1142
  this._referenceAudio = options.referenceAudio;
@@ -1004,10 +1150,23 @@ class TTSGgml {
1004
1150
  this._threads = options.threads;
1005
1151
  this._streamChunkTokens = options.streamChunkTokens;
1006
1152
  this._streamFirstChunkTokens = options.streamFirstChunkTokens;
1007
- // CosyVoice3-only: left-context speech tokens carried into each streaming chunk.
1153
+ this._streamChunkTolerancePct = options.streamChunkTolerancePct;
1154
+ this._streamMinChunkTokens = options.streamMinChunkTokens;
1008
1155
  this._streamLeftContextTokens = options.streamLeftContextTokens;
1009
1156
  this._cfmSteps = options.cfmSteps;
1010
1157
  this._cfgRate = options.cfgRate;
1158
+ this._batchCfmSteps = options.batchCfmSteps;
1159
+ this._nPredict = options.nPredict;
1160
+ this._maxSentenceChars = options.maxSentenceChars;
1161
+ this._crossfadeMs = options.crossfadeMs;
1162
+ this._repeatPenalty = options.repeatPenalty;
1163
+ this._exaggeration = options.exaggeration;
1164
+ this._cfgWeight = options.cfgWeight;
1165
+ this._minP = options.minP;
1166
+ this._voiceJsonPath = options.voiceJsonPath;
1167
+ this._prewarmText = options.prewarmText;
1168
+ this._vulkanDevice = options.vulkanDevice;
1169
+ this._flowCutPrompt = options.flowCutPrompt;
1011
1170
  // CosyVoice3-only: transcript of the reference audio for zero-shot cloning.
1012
1171
  this._promptText = options.promptText;
1013
1172
  // CosyVoice3-only: render the structured/raw instruct2 control to its string.
@@ -1034,15 +1193,17 @@ class TTSGgml {
1034
1193
  this._normalizeNumbers = options.normalizeNumbers;
1035
1194
  }
1036
1195
  _assertEngineStreamingSupport() {
1196
+ // Checked ahead of the shared denoiser guard below: Supertonic rejects both
1197
+ // LavaSR stages for one reason (tts-cpp has only one-shot enhance /
1198
+ // denoise, and the engine reports no native rate before synthesis).
1037
1199
  if (this._engineType === ENGINE_SUPERTONIC &&
1038
- (this._streamChunkTokens != null ||
1039
- this._streamFirstChunkTokens != null)) {
1040
- throw new Error("tts-ggml: streamChunkTokens / streamFirstChunkTokens are " +
1041
- "Chatterbox-only options (sub-sentence native streaming via " +
1042
- "the chatterbox::Engine streaming chunked S3Gen+HiFT loop). " +
1043
- "Supertonic does not support sub-sentence native streaming; " +
1044
- "use sentence-level streaming via the engine-agnostic " +
1045
- "runStream() / runStreaming() / run({ streamOutput: true }) APIs.");
1200
+ (this._enhancerGgufPath || this._denoiserGgufPath) &&
1201
+ this._requestsChunkStreaming()) {
1202
+ throw new Error("tts-ggml: the LavaSR enhancer/denoiser are not supported with " +
1203
+ "supertonic native chunk streaming (streamChunkTokens > 0). Use " +
1204
+ "batch synthesis or sentence-level streaming via runStream() / " +
1205
+ "runStreaming() / run({ streamOutput: true }) for enhanced or " +
1206
+ "denoised output.");
1046
1207
  }
1047
1208
  if (this._engineType === ENGINE_AUDIO8 &&
1048
1209
  (this._streamChunkTokens != null ||
@@ -1052,12 +1213,20 @@ class TTSGgml {
1052
1213
  "utterance at a time. Use sentence-level streaming via " +
1053
1214
  "runStream() / runStreaming() / run({ streamOutput: true }).");
1054
1215
  }
1216
+ if (this._engineType === ENGINE_MOSS &&
1217
+ this._streamFirstChunkTokens != null) {
1218
+ throw new Error("tts-ggml: streamFirstChunkTokens is not supported by the moss " +
1219
+ "engine, which streams fixed-size chunks; set streamChunkTokens " +
1220
+ `(codec frames per chunk, ${MOSS_FRAMES_PER_SECOND} per second) only.`);
1221
+ }
1055
1222
  // Runs before the denoiser guard so a Parler description/template conflict
1056
1223
  // is reported ahead of the engine-agnostic streaming constraints.
1057
1224
  this._assertParlerOptionConsistency();
1225
+ this._assertEngineScopedOptions();
1058
1226
  this._assertCosyvoiceOptionConsistency();
1059
1227
  this._assertCosyvoiceCloneConsistent();
1060
1228
  this._assertAudio8OptionConsistency();
1229
+ this._assertMossOptionConsistency();
1061
1230
  this._assertConditioningConsistency("constructor");
1062
1231
  if (this._denoiserGgufPath && this._requestsChunkStreaming()) {
1063
1232
  throw new Error("tts-ggml: the LavaSR denoiser is not yet supported with " +
@@ -1111,12 +1280,18 @@ class TTSGgml {
1111
1280
  this._assertSamplerOptionSupport();
1112
1281
  }
1113
1282
  _assertSamplerOptionSupport() {
1114
- if (SAMPLING_ENGINES.includes(this._engineType))
1283
+ if (this._engineType === ENGINE_POCKET || SAMPLING_ENGINES.includes(this._engineType))
1115
1284
  return;
1285
+ // Chatterbox's T3 samples too, but its length cap is nPredict, not maxFrames.
1286
+ const tokenSampling = this._engineType === ENGINE_CHATTERBOX
1287
+ ? {}
1288
+ : {
1289
+ temperature: this._temperature,
1290
+ topK: this._topK,
1291
+ topP: this._topP,
1292
+ };
1116
1293
  const sampling = setOptionNames({
1117
- temperature: this._temperature,
1118
- topK: this._topK,
1119
- topP: this._topP,
1294
+ ...tokenSampling,
1120
1295
  maxFrames: this._maxFrames,
1121
1296
  });
1122
1297
  if (sampling.length === 0)
@@ -1124,6 +1299,54 @@ class TTSGgml {
1124
1299
  throw new Error(`tts-ggml: ${sampling.join(", ")} are parler/audio8-only options ` +
1125
1300
  `(engine is ${this._engineType})`);
1126
1301
  }
1302
+ /**
1303
+ * Options only some engines read. Set on any other engine they would be
1304
+ * dropped without effect, so they are rejected by name instead.
1305
+ */
1306
+ _assertEngineScopedOptions() {
1307
+ const scoped = [
1308
+ [
1309
+ [ENGINE_CHATTERBOX],
1310
+ {
1311
+ batchCfmSteps: this._batchCfmSteps,
1312
+ nPredict: this._nPredict,
1313
+ maxSentenceChars: this._maxSentenceChars,
1314
+ crossfadeMs: this._crossfadeMs,
1315
+ repeatPenalty: this._repeatPenalty,
1316
+ exaggeration: this._exaggeration,
1317
+ cfgWeight: this._cfgWeight,
1318
+ minP: this._minP,
1319
+ },
1320
+ ],
1321
+ [
1322
+ [ENGINE_CHATTERBOX, ENGINE_COSYVOICE3],
1323
+ { streamLeftContextTokens: this._streamLeftContextTokens },
1324
+ ],
1325
+ [
1326
+ [ENGINE_SUPERTONIC],
1327
+ {
1328
+ voiceJsonPath: this._voiceJsonPath,
1329
+ prewarmText: this._prewarmText,
1330
+ streamChunkTolerancePct: this._streamChunkTolerancePct,
1331
+ streamMinChunkTokens: this._streamMinChunkTokens,
1332
+ },
1333
+ ],
1334
+ [
1335
+ [ENGINE_SUPERTONIC, ENGINE_COSYVOICE3],
1336
+ { vulkanDevice: this._vulkanDevice },
1337
+ ],
1338
+ [[ENGINE_COSYVOICE3], { flowCutPrompt: this._flowCutPrompt }],
1339
+ ];
1340
+ for (const [engines, fields] of scoped) {
1341
+ if (engines.includes(this._engineType))
1342
+ continue;
1343
+ const names = setOptionNames(fields);
1344
+ if (names.length === 0)
1345
+ continue;
1346
+ throw new Error(`tts-ggml: ${names.join(", ")} are ${engines.join("/")}-only ` +
1347
+ `options (engine is ${this._engineType})`);
1348
+ }
1349
+ }
1127
1350
  _assertAudio8OptionConsistency() {
1128
1351
  if (this._engineType === ENGINE_AUDIO8) {
1129
1352
  if (this._enhancerGgufPath || this._denoiserGgufPath) {
@@ -1146,6 +1369,68 @@ class TTSGgml {
1146
1369
  `(engine is ${this._engineType})`);
1147
1370
  }
1148
1371
  }
1372
+ _assertMossOptionConsistency() {
1373
+ if (this._engineType !== ENGINE_MOSS) {
1374
+ this._assertNoMossOnlyOptions();
1375
+ return;
1376
+ }
1377
+ if (this._enhancerGgufPath || this._denoiserGgufPath) {
1378
+ throw new Error("tts-ggml: the LavaSR enhancer/denoiser are not supported with " +
1379
+ "the moss engine. Drop lavasrEnhancer / lavasrDenoiser.");
1380
+ }
1381
+ this._assertMossOutputRate();
1382
+ this._assertMossVoiceConsistent();
1383
+ assertMossDurationTokens(this._durationTokens, "constructor");
1384
+ this._assertMossDialogueReferences();
1385
+ }
1386
+ _assertNoMossOnlyOptions() {
1387
+ const mossOnly = setOptionNames({
1388
+ durationTokens: this._durationTokens,
1389
+ dialogueReferences: this._dialogueReferences !== undefined || undefined,
1390
+ });
1391
+ if (mossOnly.length === 0)
1392
+ return;
1393
+ throw new Error(`tts-ggml: ${mossOnly.join(", ")} are moss-only options ` +
1394
+ `(engine is ${this._engineType})`);
1395
+ }
1396
+ _assertMossDialogueReferences() {
1397
+ if (this._dialogueReferences === undefined)
1398
+ return;
1399
+ assertDialogueReferenceList(this._dialogueReferences);
1400
+ if (this._referenceAudio) {
1401
+ throw new Error("tts-ggml: referenceAudio and dialogueReferences are exclusive; " +
1402
+ "pass one recording per speaker in dialogueReferences");
1403
+ }
1404
+ if (!this._mossCodecEncoderPath) {
1405
+ throw new Error("tts-ggml: dialogue synthesis with the moss engine needs the codec " +
1406
+ "encoder GGUF (files.mossCodecEncoder)");
1407
+ }
1408
+ if (!this._mossBackbonePath) {
1409
+ throw new Error("tts-ggml: dialogue synthesis with the moss engine needs the MOSS-TTSD " +
1410
+ "backbone: stage moss-ttsd-*.gguf in modelDir or set files.mossBackbone");
1411
+ }
1412
+ }
1413
+ _assertSentenceStreamingAllowed(where) {
1414
+ if (this._dialogueReferences === undefined)
1415
+ return;
1416
+ throw new Error(`tts-ggml: ${where}: MOSS dialogue cannot be split into sentences, ` +
1417
+ "because every job must open with the reference transcripts; use " +
1418
+ "run() or native streaming (streamChunkTokens)");
1419
+ }
1420
+ _assertMossOutputRate() {
1421
+ if (this._outputSampleRate == null ||
1422
+ this._outputSampleRate === MOSS_NATIVE_SAMPLE_RATE) {
1423
+ return;
1424
+ }
1425
+ throw new Error(`tts-ggml: the moss engine emits ${MOSS_NATIVE_SAMPLE_RATE} Hz only; ` +
1426
+ `drop outputSampleRate (got ${this._outputSampleRate}).`);
1427
+ }
1428
+ _assertMossVoiceConsistent() {
1429
+ if (!this._referenceAudio || this._mossCodecEncoderPath)
1430
+ return;
1431
+ throw new Error("tts-ggml: voice cloning with the moss engine needs the codec " +
1432
+ "encoder GGUF (files.mossCodecEncoder)");
1433
+ }
1149
1434
  /**
1150
1435
  * A recording without its transcript is accepted by the model but degrades
1151
1436
  * the clone silently, and a transcript alone has nothing to attach to, so
@@ -1193,7 +1478,6 @@ class TTSGgml {
1193
1478
  const cosyvoiceOnlyFields = {
1194
1479
  instruct: this._instruct,
1195
1480
  promptText: this._promptText,
1196
- streamLeftContextTokens: this._streamLeftContextTokens,
1197
1481
  };
1198
1482
  for (const [key, value] of Object.entries(cosyvoiceOnlyFields)) {
1199
1483
  if (value != null)
@@ -1222,7 +1506,7 @@ class TTSGgml {
1222
1506
  * The per-call surface of a non-Parler engine: only the cross-engine
1223
1507
  * emotion/pace it declares support for, never Parler's template fields.
1224
1508
  */
1225
- _resolveConditioningJobFields(source, where) {
1509
+ _resolveConditioningJobFields(source, where, instruct) {
1226
1510
  const parlerOnly = keysPresent(source, PARLER_ONLY_KEYS);
1227
1511
  if (parlerOnly.length > 0) {
1228
1512
  throw new Error(`tts-ggml: ${where}: per-call description/voice-template options ` +
@@ -1237,10 +1521,24 @@ class TTSGgml {
1237
1521
  if (this._engineType === ENGINE_COSYVOICE3) {
1238
1522
  // A per-call control replaces the constructor's, so only the per-call
1239
1523
  // fields participate in the one-instruction count.
1240
- assertSingleCosyvoiceControl(fields, undefined, where);
1524
+ assertSingleCosyvoiceControl(fields, instruct, where);
1241
1525
  }
1242
1526
  return fields;
1243
1527
  }
1528
+ /**
1529
+ * CosyVoice3's per-call instruction, rendered like the constructor's. The
1530
+ * native layer reads it next to emotion/pace, and any of the three replaces
1531
+ * the configured conditioning for that synthesis.
1532
+ */
1533
+ _resolveJobInstruct(source, where) {
1534
+ if (source?.instruct === undefined)
1535
+ return undefined;
1536
+ if (this._engineType !== ENGINE_COSYVOICE3) {
1537
+ throw new Error(`tts-ggml: ${where}: per-call instruct is cosyvoice3-only ` +
1538
+ `(engine is ${this._engineType})`);
1539
+ }
1540
+ return renderCosyvoiceInstruct(source.instruct) || undefined;
1541
+ }
1244
1542
  /**
1245
1543
  * Parler's per-call surface: description/template fields, where a per-call
1246
1544
  * template cannot be merged with a constructor-level free-text description.
@@ -1300,13 +1598,18 @@ class TTSGgml {
1300
1598
  * and every engine the cross-engine conditioning it supports.
1301
1599
  */
1302
1600
  _resolveJobFields(source, where) {
1601
+ const instruct = this._resolveJobInstruct(source, where);
1303
1602
  const engineFields = this._engineType === ENGINE_PARLER
1304
1603
  ? this._resolveParlerJobFields(source, where)
1305
- : this._resolveConditioningJobFields(source, where);
1604
+ : this._resolveConditioningJobFields(source, where, instruct);
1306
1605
  const audio8 = this._resolveAudio8JobFields(source, where);
1307
- if (!engineFields && !audio8)
1606
+ if (!engineFields && !audio8 && instruct === undefined)
1308
1607
  return undefined;
1309
- return { ...(engineFields ?? {}), ...(audio8 ?? {}) };
1608
+ return {
1609
+ ...(engineFields ?? {}),
1610
+ ...(audio8 ?? {}),
1611
+ ...(instruct === undefined ? {} : { instruct }),
1612
+ };
1310
1613
  }
1311
1614
  getEngineType() {
1312
1615
  return this._engineType;
@@ -1321,6 +1624,9 @@ class TTSGgml {
1321
1624
  }
1322
1625
  async load(..._args) {
1323
1626
  void _args;
1627
+ return this._withPocketLifecycle(() => this._loadModel());
1628
+ }
1629
+ async _loadModel() {
1324
1630
  if (this.state.destroyed) {
1325
1631
  throw new QvacErrorAddonTTSGgml({
1326
1632
  code: ERR_CODES.FAILED_TO_LOAD,
@@ -1329,14 +1635,17 @@ class TTSGgml {
1329
1635
  }
1330
1636
  if (this.state.configLoaded || this.state.weightsLoaded) {
1331
1637
  this._getLogger().info("Reload requested - unloading existing model first");
1332
- await this.unload();
1638
+ await this._unloadModel();
1333
1639
  }
1334
1640
  await this._load();
1335
1641
  this.state.configLoaded = true;
1336
1642
  this.state.weightsLoaded = true;
1337
1643
  }
1338
1644
  async run(input) {
1339
- if (input?.streamOutput === true) {
1645
+ await this._waitPocketCancel();
1646
+ this._checkPocketReload();
1647
+ if (input?.streamOutput === true && this._engineType !== ENGINE_POCKET) {
1648
+ this._assertSentenceStreamingAllowed("run with streamOutput");
1340
1649
  if (typeof input.input !== "string" ||
1341
1650
  input.input.trim().length === 0) {
1342
1651
  throw new QvacErrorAddonTTSGgml({
@@ -1360,7 +1669,13 @@ class TTSGgml {
1360
1669
  * `run({ input: text, streamOutput: true, ...options })`.
1361
1670
  */
1362
1671
  async runStream(text, options = {}) {
1672
+ await this._waitPocketCancel();
1673
+ this._checkPocketReload();
1363
1674
  const normalized = options == null || typeof options !== "object" ? {} : options;
1675
+ if (this._engineType === ENGINE_POCKET) {
1676
+ const run = () => this._runStreamOrchestrator(text, normalized, this._resolveJobFields(normalized, "runStream"));
1677
+ return this.exclusiveRun ? this._enqueueExclusiveTtsResponse(run) : run();
1678
+ }
1364
1679
  return this.run({
1365
1680
  input: text,
1366
1681
  streamOutput: true,
@@ -1368,6 +1683,9 @@ class TTSGgml {
1368
1683
  maxChunkScalars: normalized.maxChunkScalars,
1369
1684
  ...(pickParlerDescFields(normalized) ?? {}),
1370
1685
  ...(pickAudio8VoiceFields(normalized) ?? {}),
1686
+ ...(normalized.instruct === undefined
1687
+ ? {}
1688
+ : { instruct: normalized.instruct }),
1371
1689
  });
1372
1690
  }
1373
1691
  /**
@@ -1378,6 +1696,9 @@ class TTSGgml {
1378
1696
  * small streamed fragments are coalesced.
1379
1697
  */
1380
1698
  async runStreaming(textStream, options = {}) {
1699
+ await this._waitPocketCancel();
1700
+ this._checkPocketReload();
1701
+ this._assertSentenceStreamingAllowed("runStreaming");
1381
1702
  const jobFields = this._resolveJobFields(options, "runStreaming");
1382
1703
  const streamOptions = this._resolveRunStreamingOptions(textStream, options);
1383
1704
  let normalized = this._normalizeTextStream(textStream);
@@ -1468,6 +1789,7 @@ class TTSGgml {
1468
1789
  });
1469
1790
  }
1470
1791
  _runTextStreamOrchestrator(source, jobFields) {
1792
+ this._checkPocketRequest();
1471
1793
  const response = this._job.start();
1472
1794
  this._sentenceStreamCtx = {
1473
1795
  textStreamMode: true,
@@ -1478,7 +1800,10 @@ class TTSGgml {
1478
1800
  chunkResolver: null,
1479
1801
  jobFields,
1480
1802
  };
1803
+ const context = this._sentenceStreamCtx;
1481
1804
  void this._sentenceStreamTextIterableDrive().catch((error) => {
1805
+ if (context !== this._sentenceStreamCtx)
1806
+ return;
1482
1807
  this._rejectActiveChunk(error);
1483
1808
  this._sentenceStreamCtx = null;
1484
1809
  this._job.fail(normalizeError(error));
@@ -1494,6 +1819,8 @@ class TTSGgml {
1494
1819
  }
1495
1820
  try {
1496
1821
  for await (const piece of context.asyncTextSource) {
1822
+ if (context !== this._sentenceStreamCtx)
1823
+ return;
1497
1824
  const text = String(piece).trim();
1498
1825
  if (text.length === 0)
1499
1826
  continue;
@@ -1502,7 +1829,9 @@ class TTSGgml {
1502
1829
  const done = new Promise((resolve, reject) => {
1503
1830
  context.chunkResolver = { resolve, reject };
1504
1831
  });
1505
- await this._requireAddon().runJob({
1832
+ // Dispatch may fail before the completion promise is awaited.
1833
+ void done.catch(() => { });
1834
+ await this._dispatchJob({
1506
1835
  type: "text",
1507
1836
  input: text,
1508
1837
  ...(context.jobFields ?? {}),
@@ -1511,11 +1840,15 @@ class TTSGgml {
1511
1840
  }
1512
1841
  }
1513
1842
  catch (error) {
1843
+ if (context !== this._sentenceStreamCtx)
1844
+ return;
1514
1845
  this._rejectActiveChunk(error);
1515
1846
  this._sentenceStreamCtx = null;
1516
1847
  this._job.fail(normalizeError(error));
1517
1848
  return;
1518
1849
  }
1850
+ if (context !== this._sentenceStreamCtx)
1851
+ return;
1519
1852
  const current = this._sentenceStreamCtx;
1520
1853
  const chunks = current?.chunks || [];
1521
1854
  const accumulator = current?.acc || emptyStreamAccumulator();
@@ -1531,13 +1864,14 @@ class TTSGgml {
1531
1864
  : computeSentenceStreamStats(chunks, accumulator, this._pacesOnFrames()));
1532
1865
  }
1533
1866
  /**
1534
- * Audio8 reports `tokensPerSecond` as codec frames per second, so the
1535
- * streaming aggregate has to count frames too rather than characters.
1867
+ * Audio8 and MOSS report `tokensPerSecond` as codec frames per second, so
1868
+ * the streaming aggregate has to count frames too rather than characters.
1536
1869
  */
1537
1870
  _pacesOnFrames() {
1538
- return this._engineType === ENGINE_AUDIO8;
1871
+ return (this._engineType === ENGINE_AUDIO8 || this._engineType === ENGINE_MOSS);
1539
1872
  }
1540
1873
  _runStreamOrchestrator(text, options, jobFields) {
1874
+ this._checkPocketRequest();
1541
1875
  const chunks = (0, textChunker_1.splitTtsText)(String(text), {
1542
1876
  language: this._config.language,
1543
1877
  locale: options.locale,
@@ -1549,6 +1883,7 @@ class TTSGgml {
1549
1883
  adds: "chunked synthesis: text produced no chunks after split",
1550
1884
  });
1551
1885
  }
1886
+ this._checkPocketRequest();
1552
1887
  const response = this._job.start();
1553
1888
  this._sentenceStreamCtx = {
1554
1889
  chunks,
@@ -1557,7 +1892,10 @@ class TTSGgml {
1557
1892
  chunkResolver: null,
1558
1893
  jobFields,
1559
1894
  };
1895
+ const context = this._sentenceStreamCtx;
1560
1896
  void this._sentenceStreamDriveBody().catch((error) => {
1897
+ if (context !== this._sentenceStreamCtx)
1898
+ return;
1561
1899
  this._rejectActiveChunk(error);
1562
1900
  this._sentenceStreamCtx = null;
1563
1901
  this._job.fail(normalizeError(error));
@@ -1569,17 +1907,23 @@ class TTSGgml {
1569
1907
  if (!context || context.textStreamMode)
1570
1908
  return;
1571
1909
  for (let index = 0; index < context.chunks.length; index++) {
1910
+ if (context !== this._sentenceStreamCtx)
1911
+ return;
1572
1912
  context.chunkIdx = index;
1573
1913
  const done = new Promise((resolve, reject) => {
1574
1914
  context.chunkResolver = { resolve, reject };
1575
1915
  });
1576
- await this._requireAddon().runJob({
1916
+ // Dispatch may fail before the completion promise is awaited.
1917
+ void done.catch(() => { });
1918
+ await this._dispatchJob({
1577
1919
  type: "text",
1578
1920
  input: context.chunks[index],
1579
1921
  ...(context.jobFields ?? {}),
1580
1922
  });
1581
1923
  await done;
1582
1924
  }
1925
+ if (context !== this._sentenceStreamCtx)
1926
+ return;
1583
1927
  this._sentenceStreamCtx = null;
1584
1928
  }
1585
1929
  async _load() {
@@ -1600,6 +1944,8 @@ class TTSGgml {
1600
1944
  }
1601
1945
  }
1602
1946
  _buildTtsParams() {
1947
+ if (this._pocketParams)
1948
+ return { ...this._pocketParams };
1603
1949
  if (this._engineType === ENGINE_SUPERTONIC) {
1604
1950
  return this._buildSupertonicParams();
1605
1951
  }
@@ -1612,6 +1958,9 @@ class TTSGgml {
1612
1958
  if (this._engineType === ENGINE_AUDIO8) {
1613
1959
  return this._buildAudio8Params();
1614
1960
  }
1961
+ if (this._engineType === ENGINE_MOSS) {
1962
+ return this._buildMossParams();
1963
+ }
1615
1964
  return this._buildChatterboxParams();
1616
1965
  }
1617
1966
  _buildCosyvoiceParams() {
@@ -1635,6 +1984,21 @@ class TTSGgml {
1635
1984
  if (this._cosyvoiceCampplusModelPath) {
1636
1985
  parameters.cosyvoiceCampplusModelPath = this._cosyvoiceCampplusModelPath;
1637
1986
  }
1987
+ if (this._cosyvoiceVocabPath) {
1988
+ parameters.cosyvoiceVocabPath = this._cosyvoiceVocabPath;
1989
+ }
1990
+ if (this._cosyvoiceMergesPath) {
1991
+ parameters.cosyvoiceMergesPath = this._cosyvoiceMergesPath;
1992
+ }
1993
+ if (this._cosyvoiceVoiceModelPath) {
1994
+ parameters.cosyvoiceVoiceModelPath = this._cosyvoiceVoiceModelPath;
1995
+ }
1996
+ if (this._vulkanDevice != null) {
1997
+ parameters.vulkanDevice = this._vulkanDevice | 0;
1998
+ }
1999
+ if (this._flowCutPrompt != null) {
2000
+ parameters.flowCutPrompt = !!this._flowCutPrompt;
2001
+ }
1638
2002
  if (this._referenceAudio != null) {
1639
2003
  parameters.referenceAudio = this._referenceAudio;
1640
2004
  }
@@ -1692,9 +2056,35 @@ class TTSGgml {
1692
2056
  if (this._cfmSteps != null) {
1693
2057
  parameters.cfmSteps = this._cfmSteps | 0;
1694
2058
  }
2059
+ if (this._streamLeftContextTokens != null) {
2060
+ parameters.streamLeftContextTokens = this._streamLeftContextTokens | 0;
2061
+ }
2062
+ if (this._batchCfmSteps != null) {
2063
+ parameters.batchCfmSteps = this._batchCfmSteps | 0;
2064
+ }
1695
2065
  if (this._cfgRate != null) {
1696
2066
  parameters.cfgRate = Number(this._cfgRate);
1697
2067
  }
2068
+ if (this._nPredict != null)
2069
+ parameters.nPredict = this._nPredict | 0;
2070
+ if (this._maxSentenceChars != null) {
2071
+ parameters.maxSentenceChars = this._maxSentenceChars | 0;
2072
+ }
2073
+ if (this._crossfadeMs != null) {
2074
+ parameters.crossfadeMs = this._crossfadeMs | 0;
2075
+ }
2076
+ this._assignSamplingParams(parameters);
2077
+ if (this._repeatPenalty != null) {
2078
+ parameters.repeatPenalty = Number(this._repeatPenalty);
2079
+ }
2080
+ if (this._exaggeration != null) {
2081
+ parameters.exaggeration = Number(this._exaggeration);
2082
+ }
2083
+ if (this._cfgWeight != null) {
2084
+ parameters.cfgWeight = Number(this._cfgWeight);
2085
+ }
2086
+ if (this._minP != null)
2087
+ parameters.minP = Number(this._minP);
1698
2088
  if (this._speed != null)
1699
2089
  parameters.speed = Number(this._speed);
1700
2090
  if (this._mecabDictPath) {
@@ -1728,6 +2118,25 @@ class TTSGgml {
1728
2118
  if (this._vulkanCacheDir) {
1729
2119
  parameters.vulkanCacheDir = this._vulkanCacheDir;
1730
2120
  }
2121
+ if (this._voiceJsonPath)
2122
+ parameters.voiceJsonPath = this._voiceJsonPath;
2123
+ if (this._prewarmText)
2124
+ parameters.prewarmText = this._prewarmText;
2125
+ if (this._vulkanDevice != null) {
2126
+ parameters.vulkanDevice = this._vulkanDevice | 0;
2127
+ }
2128
+ if (this._streamChunkTokens != null) {
2129
+ parameters.streamChunkTokens = this._streamChunkTokens | 0;
2130
+ }
2131
+ if (this._streamFirstChunkTokens != null) {
2132
+ parameters.streamFirstChunkTokens = this._streamFirstChunkTokens | 0;
2133
+ }
2134
+ if (this._streamChunkTolerancePct != null) {
2135
+ parameters.streamChunkTolerancePct = this._streamChunkTolerancePct | 0;
2136
+ }
2137
+ if (this._streamMinChunkTokens != null) {
2138
+ parameters.streamMinChunkTokens = this._streamMinChunkTokens | 0;
2139
+ }
1731
2140
  return parameters;
1732
2141
  }
1733
2142
  _buildParlerParams() {
@@ -1819,6 +2228,32 @@ class TTSGgml {
1819
2228
  this._assignBackendParams(parameters);
1820
2229
  return parameters;
1821
2230
  }
2231
+ _buildMossParams() {
2232
+ this._assertMossOutputRate();
2233
+ const parameters = {
2234
+ engineType: ENGINE_MOSS,
2235
+ mossBackbonePath: this._mossBackbonePath || "",
2236
+ mossCodecDecoderPath: this._mossCodecDecoderPath || "",
2237
+ language: this._config.language || "en",
2238
+ };
2239
+ if (this._mossCodecEncoderPath) {
2240
+ parameters.mossCodecEncoderPath = this._mossCodecEncoderPath;
2241
+ }
2242
+ if (this._referenceAudio != null) {
2243
+ parameters.referenceAudio = this._referenceAudio;
2244
+ }
2245
+ if (this._dialogueReferences) {
2246
+ parameters.dialogueReferences = [...this._dialogueReferences];
2247
+ }
2248
+ if (this._durationTokens != null) {
2249
+ parameters.durationTokens = this._durationTokens | 0;
2250
+ }
2251
+ if (this._streamChunkTokens != null) {
2252
+ parameters.streamChunkTokens = this._streamChunkTokens | 0;
2253
+ }
2254
+ this._assignBackendParams(parameters);
2255
+ return parameters;
2256
+ }
1822
2257
  /** The knobs every engine in SAMPLING_ENGINES reads. */
1823
2258
  _assignSamplingParams(parameters) {
1824
2259
  if (this._temperature != null) {
@@ -1892,27 +2327,62 @@ class TTSGgml {
1892
2327
  return new tts_1.TTSInterface(binding, configuration, outputCallback);
1893
2328
  }
1894
2329
  async unload() {
2330
+ return this._withPocketLifecycle(() => this._unloadModel());
2331
+ }
2332
+ async _unloadModel() {
1895
2333
  await this.cancel();
1896
2334
  this._failAndClearActiveResponse("Model was unloaded");
1897
2335
  const addon = this._optionalAddon();
1898
- if (addon)
1899
- await addon.destroyInstance();
1900
- this.state.configLoaded = false;
1901
- this.state.weightsLoaded = false;
2336
+ try {
2337
+ if (addon)
2338
+ await addon.destroyInstance();
2339
+ }
2340
+ finally {
2341
+ if (this.addon === addon) {
2342
+ this.addon = null;
2343
+ this.state.configLoaded = false;
2344
+ this.state.weightsLoaded = false;
2345
+ }
2346
+ }
1902
2347
  }
1903
2348
  async destroy() {
1904
- await this.unload();
1905
- this.state.destroyed = true;
2349
+ return this._withPocketLifecycle(async () => {
2350
+ await this._unloadModel();
2351
+ this.state.destroyed = true;
2352
+ });
1906
2353
  }
1907
2354
  async _runInternal(input) {
2355
+ await this._waitPocketCancel();
2356
+ this._checkPocketRequest();
1908
2357
  const jobFields = this._resolveJobFields(input, "run");
2358
+ const signal = input?.signal;
2359
+ const pocketSignal = this._engineType === ENGINE_POCKET && signal && !signal.aborted ? signal : undefined;
1909
2360
  const response = this._job.start({
1910
- signal: input?.signal,
2361
+ signal: pocketSignal ? undefined : signal,
1911
2362
  });
1912
- if (input?.signal?.aborted)
2363
+ if (signal?.aborted)
1913
2364
  return response;
2365
+ if (pocketSignal) {
2366
+ const onAbort = () => {
2367
+ if (this._job.active !== response)
2368
+ return;
2369
+ const reason = pocketSignal.reason;
2370
+ const error = reason instanceof Error ? reason : new Error(typeof reason === "string" ? `Aborted: ${reason}` : "Aborted");
2371
+ // Preserve the caller's reason while stopping native work. Cancellation
2372
+ // retains the completion barrier until its terminal callback arrives.
2373
+ try {
2374
+ response.failed(error);
2375
+ }
2376
+ finally {
2377
+ void this.cancel().catch((error) => this._getLogger().error("Pocket abort cancellation failed", error));
2378
+ }
2379
+ };
2380
+ pocketSignal.addEventListener("abort", onAbort, { once: true });
2381
+ const detach = () => pocketSignal.removeEventListener("abort", onAbort);
2382
+ void response.await().then(detach, detach);
2383
+ }
1914
2384
  try {
1915
- await this._requireAddon().runJob({
2385
+ await this._dispatchJob({
1916
2386
  type: input.type || "text",
1917
2387
  input: input.input,
1918
2388
  ...(jobFields ?? {}),
@@ -1925,6 +2395,12 @@ class TTSGgml {
1925
2395
  return response;
1926
2396
  }
1927
2397
  _mergeSentenceStreamStats(accumulator, data) {
2398
+ if (accumulator.firstAudioMs === undefined &&
2399
+ typeof data.firstAudioMs === "number" &&
2400
+ Number.isFinite(data.firstAudioMs) &&
2401
+ data.firstAudioMs >= 0) {
2402
+ accumulator.firstAudioMs = data.firstAudioMs;
2403
+ }
1928
2404
  accumulator.totalTime +=
1929
2405
  typeof data.totalTime === "number" ? data.totalTime : 0;
1930
2406
  accumulator.audioDurationMs +=
@@ -1935,6 +2411,12 @@ class TTSGgml {
1935
2411
  typeof data.totalSamples === "number" ? data.totalSamples : 0;
1936
2412
  accumulator.generatedFrames +=
1937
2413
  typeof data.generatedFrames === "number" ? data.generatedFrames : 0;
2414
+ if (typeof data.codecSidecarLoaded === "number") {
2415
+ accumulator.codecSidecarLoaded = data.codecSidecarLoaded;
2416
+ }
2417
+ if (typeof data.codecOnCoreml === "number") {
2418
+ accumulator.codecOnCoreml = data.codecOnCoreml;
2419
+ }
1938
2420
  }
1939
2421
  _rejectActiveChunk(error) {
1940
2422
  const resolver = this._sentenceStreamCtx?.chunkResolver;
@@ -1950,6 +2432,11 @@ class TTSGgml {
1950
2432
  this._job.end();
1951
2433
  }
1952
2434
  _addonOutputCallback(_addon, event, data, error) {
2435
+ if (this._pocketJobPending && ((typeof error === "string" && error.length > 0) || isStatsEvent(data))) {
2436
+ const pending = this._pocketJobPending;
2437
+ this._pocketJobPending = null;
2438
+ pending.resolve();
2439
+ }
1953
2440
  if (typeof error === "string" && error.length > 0) {
1954
2441
  this._handleAddonError(error);
1955
2442
  }
@@ -1999,7 +2486,7 @@ class TTSGgml {
1999
2486
  enriched.sampleRate = data.sampleRate;
2000
2487
  }
2001
2488
  if (!context.textStreamMode) {
2002
- enriched.isLast = index >= context.chunks.length - 1;
2489
+ enriched.isLast = index >= context.chunks.length - 1 && data.isLast !== false;
2003
2490
  }
2004
2491
  return enriched;
2005
2492
  }
@@ -2020,10 +2507,83 @@ class TTSGgml {
2020
2507
  this._endJobWithStats(computeSentenceStreamStats(context.chunks, context.acc, this._pacesOnFrames()));
2021
2508
  }
2022
2509
  }
2510
+ async _waitPocketCancel() {
2511
+ if (this._engineType === ENGINE_POCKET && this._pocketCancelPromise)
2512
+ await this._pocketCancelPromise;
2513
+ }
2514
+ async _withPocketLifecycle(action) {
2515
+ if (this._engineType !== ENGINE_POCKET)
2516
+ return action();
2517
+ await this._waitPocketCancel();
2518
+ this._checkPocketReload();
2519
+ this._pocketLifecycleInProgress = true;
2520
+ try {
2521
+ await action();
2522
+ }
2523
+ finally {
2524
+ this._pocketLifecycleInProgress = false;
2525
+ }
2526
+ }
2527
+ _checkPocketReload() {
2528
+ if (this._pocketLifecycleInProgress)
2529
+ throw new Error("Pocket lifecycle operation is already in progress");
2530
+ }
2531
+ _checkPocketRequest() {
2532
+ if (this._engineType === ENGINE_POCKET) {
2533
+ if (this.state.destroyed)
2534
+ throw new Error("Pocket instance was destroyed");
2535
+ if (!this.state.weightsLoaded || !this._optionalAddon())
2536
+ throw new Error("Pocket model is not loaded");
2537
+ }
2538
+ if (this._engineType === ENGINE_POCKET && (this._job.active || this._pocketJobPending)) {
2539
+ throw new Error("Pocket synthesis is already in progress");
2540
+ }
2541
+ this._checkPocketReload();
2542
+ }
2543
+ async _dispatchJob(input) {
2544
+ const addon = this._requireAddon();
2545
+ if (this._engineType !== ENGINE_POCKET)
2546
+ return addon.runJob(input);
2547
+ if (this._pocketJobPending)
2548
+ throw new Error("Pocket native job is already in progress");
2549
+ let resolve = () => { };
2550
+ const promise = new Promise((done) => { resolve = done; });
2551
+ const pending = { promise, resolve };
2552
+ this._pocketJobPending = pending;
2553
+ try {
2554
+ await addon.runJob(input);
2555
+ }
2556
+ catch (error) {
2557
+ if (this._pocketJobPending === pending)
2558
+ this._pocketJobPending = null;
2559
+ resolve();
2560
+ throw error;
2561
+ }
2562
+ }
2023
2563
  async cancel() {
2024
2564
  const addon = this._optionalAddon();
2025
- if (addon?.cancel)
2026
- await addon.cancel();
2565
+ if (this._engineType !== ENGINE_POCKET) {
2566
+ if (addon?.cancel)
2567
+ await addon.cancel();
2568
+ return;
2569
+ }
2570
+ if (this._pocketCancelPromise)
2571
+ return this._pocketCancelPromise;
2572
+ const pending = this._pocketJobPending;
2573
+ this._pocketCancelPromise = (async () => {
2574
+ this._failAndClearActiveResponse("Synthesis cancelled");
2575
+ if (addon?.cancel)
2576
+ await addon.cancel();
2577
+ // Joining the worker can precede delivery of its terminal callback.
2578
+ if (pending)
2579
+ await pending.promise;
2580
+ })();
2581
+ try {
2582
+ await this._pocketCancelPromise;
2583
+ }
2584
+ finally {
2585
+ this._pocketCancelPromise = null;
2586
+ }
2027
2587
  }
2028
2588
  _failAndClearActiveResponse(reason) {
2029
2589
  this._rejectActiveChunk(reason instanceof Error ? reason : new Error(reason));
@@ -2052,6 +2612,7 @@ class TTSGgml {
2052
2612
  minNewTokens: this._minNewTokens,
2053
2613
  normalizeNumbers: this._normalizeNumbers,
2054
2614
  seed: this._seed,
2615
+ durationTokens: this._durationTokens,
2055
2616
  };
2056
2617
  }
2057
2618
  _restoreReloadableState(state) {
@@ -2074,8 +2635,10 @@ class TTSGgml {
2074
2635
  this._minNewTokens = state.minNewTokens;
2075
2636
  this._normalizeNumbers = state.normalizeNumbers;
2076
2637
  this._seed = state.seed;
2638
+ this._durationTokens = state.durationTokens;
2077
2639
  }
2078
2640
  _applyReloadableRuntimeConfig(newConfig) {
2641
+ this._assertMossReloadKeepsVoice(newConfig);
2079
2642
  const runtimeConfig = newConfig;
2080
2643
  if (runtimeConfig.language !== undefined) {
2081
2644
  this._config.language = runtimeConfig.language;
@@ -2087,6 +2650,26 @@ class TTSGgml {
2087
2650
  this._outputSampleRate = validateOutputSampleRate(runtimeConfig.outputSampleRate);
2088
2651
  }
2089
2652
  this._applyAudio8Reload(newConfig);
2653
+ this._applyMossReload(newConfig);
2654
+ }
2655
+ _assertMossReloadKeepsVoice(newConfig) {
2656
+ if (this._engineType !== ENGINE_MOSS)
2657
+ return;
2658
+ const voiceKeys = MOSS_INSTANCE_VOICE_KEYS.filter((key) => newConfig[key] !== undefined);
2659
+ if (voiceKeys.length === 0)
2660
+ return;
2661
+ throw new Error(`tts-ggml: reload: the moss engine encodes ${voiceKeys.join(", ")} ` +
2662
+ "once per instance; create a new instance to clone different recordings");
2663
+ }
2664
+ _applyMossReload(newConfig) {
2665
+ if (this._engineType !== ENGINE_MOSS)
2666
+ return;
2667
+ const durationTokens = newConfig
2668
+ .durationTokens;
2669
+ if (durationTokens === undefined)
2670
+ return;
2671
+ assertMossDurationTokens(durationTokens, "reload");
2672
+ this._durationTokens = durationTokens ?? undefined;
2090
2673
  }
2091
2674
  // Cross-engine conditioning is reloadable on every engine that supports it,
2092
2675
  // so both families change emotion the same way.
@@ -2173,8 +2756,47 @@ class TTSGgml {
2173
2756
  throw error;
2174
2757
  }
2175
2758
  }
2759
+ async _reloadPocket(newConfig) {
2760
+ await this._waitPocketCancel();
2761
+ this._checkPocketReload();
2762
+ if (this.state.destroyed)
2763
+ throw new Error("Pocket instance was destroyed");
2764
+ const options = { ...this._pocketOptions, ...newConfig };
2765
+ const config = { ...this._config, ...newConfig };
2766
+ const params = (0, pocketConfig_1.buildPocketParams)(this._pocketFiles, options, config);
2767
+ this._pocketLifecycleInProgress = true;
2768
+ let replacement = null;
2769
+ try {
2770
+ // Drain native work and release its model before allocating another one.
2771
+ // Failed activation leaves the instance unloaded with its last good config.
2772
+ await this._unloadModel();
2773
+ replacement = this._createAddon(params, this._addonOutputCallback.bind(this));
2774
+ await replacement.activate();
2775
+ this.addon = replacement;
2776
+ replacement = null;
2777
+ this._pocketParams = params;
2778
+ this._pocketOptions = options;
2779
+ this._config.language = "en";
2780
+ this._config.useGPU = false;
2781
+ this._outputSampleRate = typeof params.outputSampleRate === "number" ? params.outputSampleRate : null;
2782
+ this._config.outputSampleRate = this._outputSampleRate ?? undefined;
2783
+ this.state.configLoaded = true;
2784
+ this.state.weightsLoaded = true;
2785
+ }
2786
+ finally {
2787
+ try {
2788
+ if (replacement)
2789
+ await replacement.destroyInstance();
2790
+ }
2791
+ finally {
2792
+ this._pocketLifecycleInProgress = false;
2793
+ }
2794
+ }
2795
+ }
2176
2796
  async reload(newConfig = {}) {
2177
2797
  this._getLogger().debug("Reloading addon with new configuration", newConfig);
2798
+ if (this._engineType === ENGINE_POCKET)
2799
+ return this._reloadPocket(newConfig);
2178
2800
  const parameters = this._applyReloadableConfig(newConfig);
2179
2801
  await this.cancel();
2180
2802
  this._failAndClearActiveResponse("Model was reloaded");
@@ -2260,6 +2882,29 @@ class TTSGgml {
2260
2882
  void _params;
2261
2883
  return "tts-ggml";
2262
2884
  }
2885
+ /**
2886
+ * tts-cpp's canonical emotion / pace vocabulary and each engine's supported
2887
+ * subset, read from the native library without loading a model. Engines are
2888
+ * keyed by engine type, so tts-cpp's `cosyvoice` appears as `cosyvoice3`;
2889
+ * an engine this package does not wrap keeps its tts-cpp name.
2890
+ */
2891
+ static getVoiceControls() {
2892
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- native binding is resolved lazily from package prebuilds.
2893
+ const binding = require("./binding");
2894
+ const native = binding.getVoiceControls();
2895
+ const engines = {};
2896
+ for (const [name, controls] of Object.entries(native.engines)) {
2897
+ engines[FABRIC_ENGINE_TYPES[name] ?? name] = {
2898
+ emotions: [...controls.emotions],
2899
+ paces: [...controls.paces],
2900
+ };
2901
+ }
2902
+ return {
2903
+ emotions: [...native.emotions],
2904
+ paces: [...native.paces],
2905
+ engines,
2906
+ };
2907
+ }
2263
2908
  _requireAddon() {
2264
2909
  const addon = this._optionalAddon();
2265
2910
  if (!addon)
@@ -2278,6 +2923,7 @@ class TTSGgml {
2278
2923
  TTSGgml.QvacErrorAddonTTSGgml = errorModule.QvacErrorAddonTTSGgml;
2279
2924
  TTSGgml.ERR_CODES = errorModule.ERR_CODES;
2280
2925
  TTSGgml.resolveBackendsDir = backends_1.resolveBackendsDir;
2926
+ TTSGgml.assessFit = fit_1.assessFit;
2281
2927
  })(TTSGgml || (TTSGgml = {}));
2282
2928
  module.exports.QvacErrorAddonTTSGgml =
2283
2929
  errorModule.QvacErrorAddonTTSGgml;