@qvac/tts-ggml 0.10.1 → 0.11.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/index.js CHANGED
@@ -62,6 +62,8 @@ const ENGINE_COSYVOICE3 = "cosyvoice3";
62
62
  const ENGINE_PARLER = "parler";
63
63
  const ENGINE_AUDIO8 = "audio8";
64
64
  const ENGINE_MOSS = "moss";
65
+ const ENGINE_MOSS_SFX = "moss-sfx";
66
+ const ENGINE_MOSS_SPEECH = "moss-speech";
65
67
  const ENGINE_POCKET = "pocket";
66
68
  const MIN_OUTPUT_SAMPLE_RATE = 8000;
67
69
  const MAX_OUTPUT_SAMPLE_RATE = 192000;
@@ -256,6 +258,49 @@ const MOSS_MAX_CHANNELS = 32;
256
258
  const MOSS_TERMINATION_ROWS = 2;
257
259
  const MOSS_MAX_DURATION_TOKENS = MOSS_MAX_NEW_TOKENS - (MOSS_MAX_CHANNELS - 1) - MOSS_TERMINATION_ROWS;
258
260
  const MOSS_INSTANCE_VOICE_KEYS = ["referenceAudio", "dialogueReferences"];
261
+ const MOSS_SFX_RE = /^moss-sfx(-[a-z0-9_]+)*\.gguf$/i;
262
+ const MOSS_SFX_NATIVE_SAMPLE_RATE = 48000;
263
+ const MOSS_SFX_MAX_SECONDS = 30;
264
+ const MOSS_SFX_MAX_STEPS = 1000;
265
+ const MOSS_SFX_MAX_GUIDANCE = 50;
266
+ const MOSS_SFX_MAX_SHIFT = 100;
267
+ const MOSS_SFX_CALL_KEYS = [
268
+ "seconds",
269
+ "negativePrompt",
270
+ "steps",
271
+ "guidance",
272
+ "shift",
273
+ ];
274
+ const MOSS_SPEECH_RE = /^moss-speech(-(?!codec)[a-z0-9_]+)?\.gguf$/i;
275
+ const MOSS_SPEECH_CODEC_RE = /^moss-speech-codec(-[a-z0-9_]+)?\.gguf$/i;
276
+ const MOSS_SPEECH_NATIVE_SAMPLE_RATE = 24000;
277
+ const MOSS_SPEECH_MIN_SAMPLE_RATE = 8000;
278
+ const MOSS_SPEECH_MAX_SAMPLE_RATE = 192000;
279
+ const MOSS_SPEECH_MAX_NEW_TOKENS = 4096;
280
+ const MOSS_SPEECH_MAX_REPLY_SECONDS = 3600;
281
+ const MOSS_SPEECH_MAX_TEMPERATURE = 10;
282
+ const MOSS_SPEECH_MAX_TOP_P = 1;
283
+ const MOSS_SPEECH_ROLES = ["system", "user", "assistant"];
284
+ const MOSS_SPEECH_ONLY_KEYS = [
285
+ "audio",
286
+ "sampleRate",
287
+ "messages",
288
+ "systemPrompt",
289
+ "replyVoice",
290
+ "replyVoiceSampleRate",
291
+ "textReply",
292
+ "maxReplySeconds",
293
+ "maxNewTokens",
294
+ ];
295
+ const MOSS_SPEECH_CONTROL_KEYS = [
296
+ "textReply",
297
+ "maxReplySeconds",
298
+ "maxNewTokens",
299
+ "greedy",
300
+ "temperature",
301
+ "topP",
302
+ "topK",
303
+ ];
259
304
  /** Engines that draw tokens, and so accept temperature / topK / topP / maxFrames. */
260
305
  const SAMPLING_ENGINES = [ENGINE_PARLER, ENGINE_AUDIO8];
261
306
  // Per-engine supported subsets, mirroring controls::supported_emotions() /
@@ -270,6 +315,8 @@ const ENGINE_EMOTIONS = {
270
315
  [ENGINE_POCKET]: [],
271
316
  [ENGINE_AUDIO8]: [],
272
317
  [ENGINE_MOSS]: [],
318
+ [ENGINE_MOSS_SFX]: [],
319
+ [ENGINE_MOSS_SPEECH]: [],
273
320
  };
274
321
  const ENGINE_PACES = {
275
322
  [ENGINE_PARLER]: PACES, // rendered into the training caption
@@ -279,6 +326,8 @@ const ENGINE_PACES = {
279
326
  [ENGINE_POCKET]: [],
280
327
  [ENGINE_AUDIO8]: [], // no rate control at all
281
328
  [ENGINE_MOSS]: [],
329
+ [ENGINE_MOSS_SFX]: [],
330
+ [ENGINE_MOSS_SPEECH]: [],
282
331
  };
283
332
  // Which channels an engine can change per call. Supertonic takes its pace
284
333
  // through EngineOptions when the engine is built, and tts-cpp exposes no
@@ -296,6 +345,8 @@ const ENGINE_PER_CALL_CONDITIONING = {
296
345
  [ENGINE_POCKET]: [],
297
346
  [ENGINE_AUDIO8]: [],
298
347
  [ENGINE_MOSS]: [],
348
+ [ENGINE_MOSS_SFX]: [],
349
+ [ENGINE_MOSS_SPEECH]: [],
299
350
  };
300
351
  function normalizeError(error) {
301
352
  return typeof error === "string"
@@ -436,6 +487,171 @@ function findQuantRankedGguf(modelDir, pattern) {
436
487
  matches.sort((left, right) => ggufQuantRank(left, pattern) - ggufQuantRank(right, pattern));
437
488
  return path.join(modelDir, matches[0]);
438
489
  }
490
+ function assertSoundEffectRange(name, value, accepts, range) {
491
+ if (typeof value === "number" && Number.isFinite(value) && accepts(value))
492
+ return;
493
+ throw new Error(`tts-ggml: moss-sfx ${name} must be ${range} (got ${String(value)})`);
494
+ }
495
+ function assertSoundEffectFields(fields) {
496
+ if (fields.seconds !== undefined) {
497
+ assertSoundEffectRange("seconds", fields.seconds, (n) => n > 0 && n <= MOSS_SFX_MAX_SECONDS, `in (0, ${MOSS_SFX_MAX_SECONDS}]`);
498
+ }
499
+ if (fields.steps !== undefined) {
500
+ assertSoundEffectRange("steps", fields.steps, (n) => Number.isInteger(n) && n >= 1 && n <= MOSS_SFX_MAX_STEPS, `an integer in [1, ${MOSS_SFX_MAX_STEPS}]`);
501
+ }
502
+ if (fields.guidance !== undefined) {
503
+ assertSoundEffectRange("guidance", fields.guidance, (n) => n >= 1 && n <= MOSS_SFX_MAX_GUIDANCE, `in [1, ${MOSS_SFX_MAX_GUIDANCE}]`);
504
+ }
505
+ if (fields.shift !== undefined) {
506
+ assertSoundEffectRange("shift", fields.shift, (n) => n > 0 && n <= MOSS_SFX_MAX_SHIFT, `in (0, ${MOSS_SFX_MAX_SHIFT}]`);
507
+ }
508
+ if (fields.negativePrompt !== undefined && typeof fields.negativePrompt !== "string") {
509
+ throw new Error("tts-ggml: moss-sfx negativePrompt must be a string");
510
+ }
511
+ }
512
+ function isSpeechPcm(value) {
513
+ return value instanceof Int16Array || value instanceof Float32Array;
514
+ }
515
+ function assertSpeechSampleRate(value, name, where) {
516
+ if (typeof value === "number" &&
517
+ Number.isInteger(value) &&
518
+ value >= MOSS_SPEECH_MIN_SAMPLE_RATE &&
519
+ value <= MOSS_SPEECH_MAX_SAMPLE_RATE) {
520
+ return;
521
+ }
522
+ throw new Error(`tts-ggml: ${where}: moss-speech ${name} must be an integer in ` +
523
+ `[${MOSS_SPEECH_MIN_SAMPLE_RATE}, ${MOSS_SPEECH_MAX_SAMPLE_RATE}] (got ${String(value)})`);
524
+ }
525
+ function assertSpeechAudio(audio, rate, names, where) {
526
+ if (!isSpeechPcm(audio) || audio.length === 0) {
527
+ throw new Error(`tts-ggml: ${where}: moss-speech ${names[0]} must be a non-empty Int16Array or Float32Array`);
528
+ }
529
+ assertSpeechSampleRate(rate, names[1], where);
530
+ }
531
+ function assertSpeechMessage(message, index, where) {
532
+ const label = `messages[${index}]`;
533
+ if (message == null || typeof message !== "object") {
534
+ throw new Error(`tts-ggml: ${where}: moss-speech ${label} must be an object`);
535
+ }
536
+ const turn = message;
537
+ if (!MOSS_SPEECH_ROLES.includes(turn.role)) {
538
+ throw new Error(`tts-ggml: ${where}: moss-speech ${label}.role must be one of ${MOSS_SPEECH_ROLES.join(", ")}`);
539
+ }
540
+ const hasText = typeof turn.text === "string" && turn.text.length > 0;
541
+ const hasAudio = turn.audio !== undefined;
542
+ if (hasText === hasAudio) {
543
+ throw new Error(`tts-ggml: ${where}: moss-speech ${label} carries exactly one of text or audio`);
544
+ }
545
+ if (hasAudio) {
546
+ assertSpeechAudio(turn.audio, turn.sampleRate, [`${label}.audio`, `${label}.sampleRate`], where);
547
+ }
548
+ }
549
+ function assertSpeechMessages(messages, where) {
550
+ if (messages === undefined)
551
+ return;
552
+ if (!Array.isArray(messages)) {
553
+ throw new Error(`tts-ggml: ${where}: moss-speech messages must be an array`);
554
+ }
555
+ assertEachSpeechMessage(messages, where);
556
+ }
557
+ function assertEachSpeechMessage(messages, where) {
558
+ messages.forEach((message, index) => assertSpeechMessage(message, index, where));
559
+ }
560
+ function assertSpeechUserTurn(fields, where) {
561
+ const hasText = typeof fields.input === "string" && fields.input.trim().length > 0;
562
+ const hasAudio = fields.audio !== undefined;
563
+ if (hasText && hasAudio) {
564
+ throw new Error(`tts-ggml: ${where}: moss-speech takes the user turn as audio or input text, not both`);
565
+ }
566
+ if (hasAudio) {
567
+ assertSpeechAudio(fields.audio, fields.sampleRate, ["audio", "sampleRate"], where);
568
+ return;
569
+ }
570
+ if (!hasText) {
571
+ throw new Error(`tts-ggml: ${where}: moss-speech needs the user turn as audio (with sampleRate) or input text`);
572
+ }
573
+ }
574
+ function assertSpeechRange(name, value, accepts, range, where) {
575
+ if (value === undefined)
576
+ return;
577
+ if (typeof value === "number" && Number.isFinite(value) && accepts(value))
578
+ return;
579
+ const got = typeof value === "number" ? String(value) : typeof value;
580
+ throw new Error(`tts-ggml: ${where}: moss-speech ${name} must be ${range} (got ${got})`);
581
+ }
582
+ function assertSpeechControls(fields, where) {
583
+ assertSpeechRange("maxReplySeconds", fields.maxReplySeconds, (n) => n >= 0 && n <= MOSS_SPEECH_MAX_REPLY_SECONDS, `in [0, ${MOSS_SPEECH_MAX_REPLY_SECONDS}]`, where);
584
+ assertSpeechRange("maxNewTokens", fields.maxNewTokens, (n) => Number.isInteger(n) && n >= 1 && n <= MOSS_SPEECH_MAX_NEW_TOKENS, `an integer in [1, ${MOSS_SPEECH_MAX_NEW_TOKENS}]`, where);
585
+ assertSpeechRange("temperature", fields.temperature, (n) => n > 0 && n <= MOSS_SPEECH_MAX_TEMPERATURE, `in (0, ${MOSS_SPEECH_MAX_TEMPERATURE}]`, where);
586
+ assertSpeechRange("topP", fields.topP, (n) => n > 0 && n <= MOSS_SPEECH_MAX_TOP_P, `in (0, ${MOSS_SPEECH_MAX_TOP_P}]`, where);
587
+ assertSpeechRange("topK", fields.topK, (n) => Number.isInteger(n) && n >= 0, "an integer >= 0", where);
588
+ assertSpeechBooleans(fields, where);
589
+ if (fields.systemPrompt !== undefined && typeof fields.systemPrompt !== "string") {
590
+ throw new Error(`tts-ggml: ${where}: moss-speech systemPrompt must be a string`);
591
+ }
592
+ }
593
+ function assertSpeechBooleans(fields, where) {
594
+ for (const flag of ["textReply", "greedy"]) {
595
+ if (fields[flag] !== undefined && typeof fields[flag] !== "boolean") {
596
+ throw new Error(`tts-ggml: ${where}: moss-speech ${flag} must be a boolean`);
597
+ }
598
+ }
599
+ }
600
+ function assertSpeechRateHasAudio(audio, rate, names, where) {
601
+ if (audio !== undefined || rate === undefined)
602
+ return;
603
+ throw new Error(`tts-ggml: ${where}: moss-speech ${names[1]} needs ${names[0]}`);
604
+ }
605
+ function assertSpeechCall(fields, where) {
606
+ assertSpeechUserTurn(fields, where);
607
+ assertSpeechRateHasAudio(fields.audio, fields.sampleRate, ["audio", "sampleRate"], where);
608
+ assertSpeechRateHasAudio(fields.replyVoice, fields.replyVoiceSampleRate, ["replyVoice", "replyVoiceSampleRate"], where);
609
+ assertSpeechMessages(fields.messages, where);
610
+ if (fields.replyVoice !== undefined) {
611
+ assertSpeechAudio(fields.replyVoice, fields.replyVoiceSampleRate, ["replyVoice", "replyVoiceSampleRate"], where);
612
+ }
613
+ assertSpeechControls(fields, where);
614
+ }
615
+ function speechUserTurn(fields) {
616
+ if (fields.audio !== undefined) {
617
+ return { role: "user", audio: fields.audio, sampleRate: fields.sampleRate };
618
+ }
619
+ return { role: "user", text: fields.input };
620
+ }
621
+ function speechMessages(fields) {
622
+ const system = fields.systemPrompt ? [{ role: "system", text: fields.systemPrompt }] : [];
623
+ return [...system, ...(fields.messages ?? []), speechUserTurn(fields)];
624
+ }
625
+ function speechReplyVoice(fields) {
626
+ if (fields.replyVoice === undefined)
627
+ return {};
628
+ return { replyVoice: fields.replyVoice, replyVoiceSampleRate: fields.replyVoiceSampleRate };
629
+ }
630
+ function speechControls(fields) {
631
+ const present = MOSS_SPEECH_CONTROL_KEYS.filter((key) => fields[key] !== undefined);
632
+ return Object.fromEntries(present.map((key) => [key, fields[key]]));
633
+ }
634
+ /** Collect the fields only MOSS-Speech reads that are present on `source`; undefined when none are set. */
635
+ function pickSpeechFields(source) {
636
+ if (source == null || typeof source !== "object")
637
+ return undefined;
638
+ const present = MOSS_SPEECH_ONLY_KEYS.filter((key) => source[key] !== undefined);
639
+ if (present.length === 0)
640
+ return undefined;
641
+ return Object.fromEntries(present.map((key) => [key, source[key]]));
642
+ }
643
+ /**
644
+ * Collect the MOSS-SoundEffect generation controls present on `source`.
645
+ * Returns undefined when none are set.
646
+ */
647
+ function pickSoundEffectFields(source) {
648
+ if (source == null || typeof source !== "object")
649
+ return undefined;
650
+ const present = MOSS_SFX_CALL_KEYS.filter((key) => source[key] !== undefined);
651
+ if (present.length === 0)
652
+ return undefined;
653
+ return Object.fromEntries(present.map((key) => [key, source[key]]));
654
+ }
439
655
  /**
440
656
  * Collect the Audio8 voice-cloning properties present on `source` (a run
441
657
  * input, streaming options, or constructor options). Returns undefined when
@@ -621,6 +837,9 @@ function normalizeGgmlFiles(files) {
621
837
  mossBackbone: firstNonEmpty(files.mossBackbone, files.mossBackbonePath),
622
838
  mossCodecDecoder: firstNonEmpty(files.mossCodecDecoder, files.mossCodecDecoderPath),
623
839
  mossCodecEncoder: firstNonEmpty(files.mossCodecEncoder, files.mossCodecEncoderPath),
840
+ mossSoundEffect: firstNonEmpty(files.mossSoundEffect, files.mossSoundEffectPath),
841
+ mossSpeechModel: firstNonEmpty(files.mossSpeechModel, files.mossSpeechModelPath),
842
+ mossSpeechCodec: firstNonEmpty(files.mossSpeechCodec, files.mossSpeechCodecPath),
624
843
  voicesDir: firstNonEmpty(files.voicesDir),
625
844
  lavasrEnhancer: firstNonEmpty(files.lavasrEnhancer),
626
845
  lavasrDenoiser: firstNonEmpty(files.lavasrDenoiser),
@@ -635,12 +854,14 @@ function detectEngineType(engine, files) {
635
854
  engine === ENGINE_PARLER ||
636
855
  engine === ENGINE_AUDIO8 ||
637
856
  engine === ENGINE_MOSS ||
857
+ engine === ENGINE_MOSS_SFX ||
858
+ engine === ENGINE_MOSS_SPEECH ||
638
859
  engine === ENGINE_POCKET) {
639
860
  return engine;
640
861
  }
641
862
  if (engine != null && engine !== "") {
642
863
  throw new Error("tts-ggml: 'engine' option must be 'chatterbox', 'supertonic', " +
643
- `'cosyvoice3', 'parler', 'audio8', 'moss' or 'pocket' (got '${String(engine)}')`);
864
+ `'cosyvoice3', 'parler', 'audio8', 'moss', 'moss-sfx', 'moss-speech' or 'pocket' (got '${String(engine)}')`);
644
865
  }
645
866
  if (files.pocketFlowModel || files.pocketMimiModel)
646
867
  return ENGINE_POCKET;
@@ -658,6 +879,10 @@ function detectEngineType(engine, files) {
658
879
  return ENGINE_AUDIO8;
659
880
  if (files.mossBackbone || files.mossCodecDecoder)
660
881
  return ENGINE_MOSS;
882
+ if (files.mossSoundEffect)
883
+ return ENGINE_MOSS_SFX;
884
+ if (files.mossSpeechModel || files.mossSpeechCodec)
885
+ return ENGINE_MOSS_SPEECH;
661
886
  if (files.modelDir) {
662
887
  if (dirHasCosyvoice3(files.modelDir))
663
888
  return ENGINE_COSYVOICE3;
@@ -679,6 +904,10 @@ function detectEngineType(engine, files) {
679
904
  if (findQuantRankedGguf(files.modelDir, MOSS_DIALOGUE_BACKBONE_RE)) {
680
905
  return ENGINE_MOSS;
681
906
  }
907
+ if (findQuantRankedGguf(files.modelDir, MOSS_SFX_RE))
908
+ return ENGINE_MOSS_SFX;
909
+ if (findQuantRankedGguf(files.modelDir, MOSS_SPEECH_RE))
910
+ return ENGINE_MOSS_SPEECH;
682
911
  if (fileExistsSafe(path.join(files.modelDir, "flow-lm.gguf")))
683
912
  return ENGINE_POCKET;
684
913
  }
@@ -922,6 +1151,8 @@ class TTSGgml {
922
1151
  static ENGINE_PARLER = ENGINE_PARLER;
923
1152
  static ENGINE_AUDIO8 = ENGINE_AUDIO8;
924
1153
  static ENGINE_MOSS = ENGINE_MOSS;
1154
+ static ENGINE_MOSS_SFX = ENGINE_MOSS_SFX;
1155
+ static ENGINE_MOSS_SPEECH = ENGINE_MOSS_SPEECH;
925
1156
  static ENGINE_POCKET = ENGINE_POCKET;
926
1157
  opts;
927
1158
  exclusiveRun;
@@ -1001,6 +1232,9 @@ class TTSGgml {
1001
1232
  _mossBackbonePath;
1002
1233
  _mossCodecDecoderPath;
1003
1234
  _mossCodecEncoderPath;
1235
+ _mossSoundEffectPath;
1236
+ _mossSpeechModelPath;
1237
+ _mossSpeechCodecPath;
1004
1238
  _dialogueReferences;
1005
1239
  _durationTokens;
1006
1240
  _referenceText;
@@ -1106,6 +1340,15 @@ class TTSGgml {
1106
1340
  this._resolveMossModelPaths(files);
1107
1341
  return;
1108
1342
  }
1343
+ if (this._engineType === ENGINE_MOSS_SFX) {
1344
+ this._mossSoundEffectPath = firstNonEmpty(files.mossSoundEffect, findQuantRankedGguf(files.modelDir, MOSS_SFX_RE));
1345
+ return;
1346
+ }
1347
+ if (this._engineType === ENGINE_MOSS_SPEECH) {
1348
+ this._mossSpeechModelPath = firstNonEmpty(files.mossSpeechModel, findQuantRankedGguf(files.modelDir, MOSS_SPEECH_RE));
1349
+ this._mossSpeechCodecPath = firstNonEmpty(files.mossSpeechCodec, findQuantRankedGguf(files.modelDir, MOSS_SPEECH_CODEC_RE));
1350
+ return;
1351
+ }
1109
1352
  if (files.modelDir) {
1110
1353
  const resolved = resolveChatterboxModelDirPaths(files.modelDir);
1111
1354
  this._t3ModelPath = firstNonEmpty(files.t3Model, resolved.t3);
@@ -1213,6 +1456,14 @@ class TTSGgml {
1213
1456
  "utterance at a time. Use sentence-level streaming via " +
1214
1457
  "runStream() / runStreaming() / run({ streamOutput: true }).");
1215
1458
  }
1459
+ if ((this._engineType === ENGINE_MOSS_SFX ||
1460
+ this._engineType === ENGINE_MOSS_SPEECH) &&
1461
+ (this._streamChunkTokens != null ||
1462
+ this._streamFirstChunkTokens != null)) {
1463
+ throw new Error("tts-ggml: streamChunkTokens / streamFirstChunkTokens are not " +
1464
+ `supported by the ${this._engineType} engine, which returns a ` +
1465
+ "whole result at a time. Use run().");
1466
+ }
1216
1467
  if (this._engineType === ENGINE_MOSS &&
1217
1468
  this._streamFirstChunkTokens != null) {
1218
1469
  throw new Error("tts-ggml: streamFirstChunkTokens is not supported by the moss " +
@@ -1227,6 +1478,8 @@ class TTSGgml {
1227
1478
  this._assertCosyvoiceCloneConsistent();
1228
1479
  this._assertAudio8OptionConsistency();
1229
1480
  this._assertMossOptionConsistency();
1481
+ this._assertMossSoundEffectOptionConsistency();
1482
+ this._assertMossSpeechOptionConsistency();
1230
1483
  this._assertConditioningConsistency("constructor");
1231
1484
  if (this._denoiserGgufPath && this._requestsChunkStreaming()) {
1232
1485
  throw new Error("tts-ggml: the LavaSR denoiser is not yet supported with " +
@@ -1369,6 +1622,61 @@ class TTSGgml {
1369
1622
  `(engine is ${this._engineType})`);
1370
1623
  }
1371
1624
  }
1625
+ _assertMossSoundEffectOptionConsistency() {
1626
+ if (this._engineType !== ENGINE_MOSS_SFX)
1627
+ return;
1628
+ if (this._enhancerGgufPath || this._denoiserGgufPath) {
1629
+ throw new Error("tts-ggml: the LavaSR enhancer/denoiser are not supported with " +
1630
+ "the moss-sfx engine. Drop lavasrEnhancer / lavasrDenoiser.");
1631
+ }
1632
+ if (this._referenceAudio) {
1633
+ throw new Error("tts-ggml: referenceAudio is not supported by the moss-sfx engine, " +
1634
+ "which generates sound effects from a text prompt");
1635
+ }
1636
+ if (this._steps != null) {
1637
+ throw new Error("tts-ggml: the moss-sfx engine takes steps per call " +
1638
+ "(run({ input, steps })), not in the constructor");
1639
+ }
1640
+ this._assertMossSoundEffectOutputRate();
1641
+ if (!this._mossSoundEffectPath) {
1642
+ throw new Error("tts-ggml: the moss-sfx engine needs its GGUF: stage " +
1643
+ "moss-sfx-*.gguf in modelDir or set files.mossSoundEffect");
1644
+ }
1645
+ }
1646
+ _assertMossSpeechOptionConsistency() {
1647
+ if (this._engineType !== ENGINE_MOSS_SPEECH)
1648
+ return;
1649
+ if (this._enhancerGgufPath || this._denoiserGgufPath) {
1650
+ throw new Error("tts-ggml: the LavaSR enhancer/denoiser are not supported with " +
1651
+ "the moss-speech engine. Drop lavasrEnhancer / lavasrDenoiser.");
1652
+ }
1653
+ if (this._referenceAudio) {
1654
+ throw new Error("tts-ggml: the moss-speech engine takes its reply voice per call " +
1655
+ "(run({ replyVoice, replyVoiceSampleRate })), not referenceAudio");
1656
+ }
1657
+ this._assertMossSpeechOutputRate();
1658
+ if (!this._mossSpeechModelPath || !this._mossSpeechCodecPath) {
1659
+ throw new Error("tts-ggml: the moss-speech engine needs its model and codec GGUFs: " +
1660
+ "stage moss-speech-*.gguf and moss-speech-codec-*.gguf in modelDir " +
1661
+ "or set files.mossSpeechModel and files.mossSpeechCodec");
1662
+ }
1663
+ }
1664
+ _assertMossSpeechOutputRate() {
1665
+ if (this._outputSampleRate == null ||
1666
+ this._outputSampleRate === MOSS_SPEECH_NATIVE_SAMPLE_RATE) {
1667
+ return;
1668
+ }
1669
+ throw new Error(`tts-ggml: the moss-speech engine outputs ${MOSS_SPEECH_NATIVE_SAMPLE_RATE} Hz ` +
1670
+ `audio and cannot resample (outputSampleRate=${this._outputSampleRate})`);
1671
+ }
1672
+ _assertMossSoundEffectOutputRate() {
1673
+ if (this._outputSampleRate == null ||
1674
+ this._outputSampleRate === MOSS_SFX_NATIVE_SAMPLE_RATE) {
1675
+ return;
1676
+ }
1677
+ throw new Error(`tts-ggml: the moss-sfx engine outputs ${MOSS_SFX_NATIVE_SAMPLE_RATE} Hz ` +
1678
+ `audio and cannot resample (outputSampleRate=${this._outputSampleRate})`);
1679
+ }
1372
1680
  _assertMossOptionConsistency() {
1373
1681
  if (this._engineType !== ENGINE_MOSS) {
1374
1682
  this._assertNoMossOnlyOptions();
@@ -1411,6 +1719,14 @@ class TTSGgml {
1411
1719
  }
1412
1720
  }
1413
1721
  _assertSentenceStreamingAllowed(where) {
1722
+ if (this._engineType === ENGINE_MOSS_SPEECH) {
1723
+ throw new Error(`tts-ggml: ${where}: the moss-speech engine answers one turn at a ` +
1724
+ "time and does not stream; use run()");
1725
+ }
1726
+ if (this._engineType === ENGINE_MOSS_SFX) {
1727
+ throw new Error(`tts-ggml: ${where}: the moss-sfx engine generates a whole sound ` +
1728
+ "effect from one prompt and does not stream; use run()");
1729
+ }
1414
1730
  if (this._dialogueReferences === undefined)
1415
1731
  return;
1416
1732
  throw new Error(`tts-ggml: ${where}: MOSS dialogue cannot be split into sentences, ` +
@@ -1592,22 +1908,70 @@ class TTSGgml {
1592
1908
  this._assertAudio8VoiceConsistent(this._mergeAudio8Voice(fields), where);
1593
1909
  return fields;
1594
1910
  }
1911
+ _assertNoSpeechFields(source, where) {
1912
+ const fields = pickSpeechFields(source);
1913
+ if (!fields)
1914
+ return;
1915
+ throw new Error(`tts-ggml: ${where}: ${Object.keys(fields).join(", ")} are ` +
1916
+ `moss-speech-only options (engine is ${this._engineType})`);
1917
+ }
1918
+ _assertNoOtherEngineFields(source, where) {
1919
+ this._resolveConditioningJobFields(source, where, this._resolveJobInstruct(source, where));
1920
+ this._resolveAudio8JobFields(source, where);
1921
+ this._resolveSoundEffectJobFields(source, where);
1922
+ }
1923
+ /**
1924
+ * Validate the MOSS-Speech call and fold the new user turn (spoken `audio`
1925
+ * or text `input`) and `systemPrompt` into the native `messages` list.
1926
+ */
1927
+ _resolveSpeechJobFields(source, where) {
1928
+ const fields = source ?? {};
1929
+ assertSpeechCall(fields, where);
1930
+ return {
1931
+ messages: speechMessages(fields),
1932
+ ...speechReplyVoice(fields),
1933
+ ...speechControls(fields),
1934
+ };
1935
+ }
1936
+ /**
1937
+ * Extract + validate the per-call MOSS-SoundEffect controls from a run
1938
+ * input. Returns undefined when none are present.
1939
+ */
1940
+ _resolveSoundEffectJobFields(source, where) {
1941
+ const fields = pickSoundEffectFields(source);
1942
+ if (!fields)
1943
+ return undefined;
1944
+ if (this._engineType !== ENGINE_MOSS_SFX) {
1945
+ throw new Error(`tts-ggml: ${where}: ${Object.keys(fields).join(", ")} are ` +
1946
+ `moss-sfx-only options (engine is ${this._engineType})`);
1947
+ }
1948
+ assertSoundEffectFields(fields);
1949
+ return fields;
1950
+ }
1595
1951
  /**
1596
1952
  * The per-call fields of whichever engine is loaded, if any are set. Parler
1597
1953
  * takes the full description/template surface, Audio8 its voice override,
1598
1954
  * and every engine the cross-engine conditioning it supports.
1599
1955
  */
1600
1956
  _resolveJobFields(source, where) {
1957
+ if (this._engineType === ENGINE_MOSS_SPEECH) {
1958
+ this._assertNoOtherEngineFields(source, where);
1959
+ return this._resolveSpeechJobFields(source, where);
1960
+ }
1601
1961
  const instruct = this._resolveJobInstruct(source, where);
1602
1962
  const engineFields = this._engineType === ENGINE_PARLER
1603
1963
  ? this._resolveParlerJobFields(source, where)
1604
1964
  : this._resolveConditioningJobFields(source, where, instruct);
1605
1965
  const audio8 = this._resolveAudio8JobFields(source, where);
1606
- if (!engineFields && !audio8 && instruct === undefined)
1966
+ const soundEffect = this._resolveSoundEffectJobFields(source, where);
1967
+ this._assertNoSpeechFields(source, where);
1968
+ if (!engineFields && !audio8 && !soundEffect && instruct === undefined) {
1607
1969
  return undefined;
1970
+ }
1608
1971
  return {
1609
1972
  ...(engineFields ?? {}),
1610
1973
  ...(audio8 ?? {}),
1974
+ ...(soundEffect ?? {}),
1611
1975
  ...(instruct === undefined ? {} : { instruct }),
1612
1976
  };
1613
1977
  }
@@ -1961,6 +2325,12 @@ class TTSGgml {
1961
2325
  if (this._engineType === ENGINE_MOSS) {
1962
2326
  return this._buildMossParams();
1963
2327
  }
2328
+ if (this._engineType === ENGINE_MOSS_SFX) {
2329
+ return this._buildMossSoundEffectParams();
2330
+ }
2331
+ if (this._engineType === ENGINE_MOSS_SPEECH) {
2332
+ return this._buildMossSpeechParams();
2333
+ }
1964
2334
  return this._buildChatterboxParams();
1965
2335
  }
1966
2336
  _buildCosyvoiceParams() {
@@ -2254,6 +2624,25 @@ class TTSGgml {
2254
2624
  this._assignBackendParams(parameters);
2255
2625
  return parameters;
2256
2626
  }
2627
+ _buildMossSoundEffectParams() {
2628
+ this._assertMossSoundEffectOutputRate();
2629
+ const parameters = {
2630
+ engineType: ENGINE_MOSS_SFX,
2631
+ mossSoundEffectPath: this._mossSoundEffectPath || "",
2632
+ };
2633
+ this._assignBackendParams(parameters);
2634
+ return parameters;
2635
+ }
2636
+ _buildMossSpeechParams() {
2637
+ this._assertMossSpeechOutputRate();
2638
+ const parameters = {
2639
+ engineType: ENGINE_MOSS_SPEECH,
2640
+ mossSpeechModelPath: this._mossSpeechModelPath || "",
2641
+ mossSpeechCodecPath: this._mossSpeechCodecPath || "",
2642
+ };
2643
+ this._assignBackendParams(parameters);
2644
+ return parameters;
2645
+ }
2257
2646
  /** The knobs every engine in SAMPLING_ENGINES reads. */
2258
2647
  _assignSamplingParams(parameters) {
2259
2648
  if (this._temperature != null) {
package/lib/fit.d.ts CHANGED
@@ -69,10 +69,30 @@ export interface CosyvoiceFitRequest extends TtsFitCommon {
69
69
  export interface MossFitRequest extends TtsFitCommon {
70
70
  engineType: 'moss';
71
71
  mossBackbonePath: string;
72
- mossCodecDecoderPath?: string;
72
+ mossCodecDecoderPath: string;
73
73
  mossCodecEncoderPath?: string;
74
+ /** Rows in the complete native prompt, including reference and dialogue rows. */
75
+ promptRows: number;
76
+ /** Total mono reference samples; use 0 for a voice without a reference. */
77
+ referenceSamples: number;
78
+ /** Project native chunk streaming when true, batch synthesis when false. */
79
+ streaming: boolean;
80
+ threads?: number;
81
+ streamChunkTokens?: number;
82
+ durationTokens?: number;
74
83
  }
75
- export type TtsFitRequest = SupertonicFitRequest | ParlerFitRequest | ChatterboxFitRequest | Audio8FitRequest | CosyvoiceFitRequest | MossFitRequest;
84
+ export interface MossSoundEffectFitRequest extends TtsFitCommon {
85
+ engineType: 'moss-sfx';
86
+ mossSoundEffectPath: string;
87
+ prompt: string;
88
+ seconds: number;
89
+ negativePrompt?: string;
90
+ steps?: number;
91
+ guidance?: number;
92
+ shift?: number;
93
+ threads?: number;
94
+ }
95
+ export type TtsFitRequest = SupertonicFitRequest | ParlerFitRequest | ChatterboxFitRequest | Audio8FitRequest | CosyvoiceFitRequest | MossFitRequest | MossSoundEffectFitRequest;
76
96
  /** The voice engines a fit can be asked for, read off the request union. */
77
97
  export type TtsFitEngine = TtsFitRequest['engineType'];
78
98
  export type TtsFitStatus = 'fits' | 'does-not-fit' | 'error';