@glissade/narrate 0.23.0-pre.2 → 0.23.0-pre.4

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.
@@ -20,10 +20,12 @@ interface ZhG2p {
20
20
 
21
21
  interface EnG2p {
22
22
  readonly id: string;
23
- /** the g2p identity, folded into kokoroProvider.version() → the cache key */
24
- version(): string;
25
- /** English text → a misaki[en] phoneme string (US English) */
26
- phonemize(text: string): string;
23
+ /** the g2p identity, folded into kokoroProvider.version() → the cache key.
24
+ * `british` ⇒ GB dialect, a DISTINCT cache entry from US (default false). */
25
+ version(british?: boolean): string;
26
+ /** English text → a misaki[en] phoneme string. `british` ⇒ GB (`bf_`/`bm_`
27
+ * voices); default US (`af_`/`am_`). */
28
+ phonemize(text: string, british?: boolean): string;
27
29
  }
28
30
  /**
29
31
  * Fork B: shell out to a pinned Python misaki[en]. Mirrors {@link misakiZhG2p} —
@@ -164,6 +166,9 @@ interface ResolvedBlend {
164
166
  entries: readonly (readonly [string, number])[];
165
167
  /** 'zh' (all zf_/zm_) or 'en' (all other / English) — the g2p front-end. */
166
168
  language: 'zh' | 'en';
169
+ /** For English: 'gb' (all bf_/bm_) or 'us' (all af_/am_ / other) — the misaki[en]
170
+ * dialect (`british` flag). Undefined for 'zh'. A mixed US+GB blend throws. */
171
+ dialect?: 'us' | 'gb';
167
172
  }
168
173
  /**
169
174
  * Validate + normalize a `VoiceBlend`: ≥2 entries, finite positive weights,
@@ -212,6 +217,8 @@ declare function loadKokoroVoiceData(entry: string, name: string): Float32Array;
212
217
  declare function kokoroGenerateFromBlend(tts: KokoroModel, Tensor: TensorCtor, inputIds: KokoroInputIds, blended: Float32Array, speed: number): Promise<KokoroAudio>;
213
218
  /** A z* (zf_/zm_) kokoro voice routes through misaki[zh] g2p (not espeak cmn). */
214
219
  declare function isKokoroChineseVoice(voice: string): boolean;
220
+ /** A b* (bf_/bm_) kokoro voice is BRITISH English → misaki[en] british=True. */
221
+ declare function isKokoroBritishVoice(voice: string): boolean;
215
222
  declare function kokoroProvider(opts?: {
216
223
  model?: string;
217
224
  voice?: VoiceSpec;
@@ -350,4 +357,4 @@ declare function synthesizeScript(scriptPath: string, opts?: SynthesizeOptions):
350
357
  /** Resolve `<scene>.narration.json` for a scene-module path (or accept the script itself). */
351
358
  declare function scriptPathFor(input: string): string;
352
359
  //#endregion
353
- export { AlignRequest, Aligner, BLEND_SPEC_VERSION, KokoroDtype, ResolvedBlend, SynthesizeOptions, SynthesizeResult, TtsProvider, TtsRequest, TtsResult, VoskAlignWord, alignerById, blendIdentity, blendStyleVectors, cacheKey, espeakProvider, fakeProvider, floatToWav, heuristicAligner, heuristicWords, interpolateMissing, isKokoroChineseVoice, kokoroGenerateFromBlend, kokoroProvider, loadKokoroVoiceData, mapAsrToScript, openaiProvider, piperProvider, providerById, requireStringVoice, resolveBlend, resolvePiperVoice, scriptPathFor, stderrTail, synthesizeScript, voskAligner, wavDuration };
360
+ export { AlignRequest, Aligner, BLEND_SPEC_VERSION, KokoroDtype, ResolvedBlend, SynthesizeOptions, SynthesizeResult, TtsProvider, TtsRequest, TtsResult, VoskAlignWord, alignerById, blendIdentity, blendStyleVectors, cacheKey, espeakProvider, fakeProvider, floatToWav, heuristicAligner, heuristicWords, interpolateMissing, isKokoroBritishVoice, isKokoroChineseVoice, kokoroGenerateFromBlend, kokoroProvider, loadKokoroVoiceData, mapAsrToScript, openaiProvider, piperProvider, providerById, requireStringVoice, resolveBlend, resolvePiperVoice, scriptPathFor, stderrTail, synthesizeScript, voskAligner, wavDuration };
package/dist/providers.js CHANGED
@@ -176,9 +176,12 @@ function misakiZhG2p(opts = {}) {
176
176
  * model + espeak-ng); each missing piece exits with a distinct code → an
177
177
  * actionable hint.
178
178
  *
179
- * DIALECT: US English (`british=False`) the `af_`/`am_` (American) voices.
180
- * GB-voice (`bf_`/`bm_`) blends are a documented follow-up; the dialect folds
181
- * into `version()` so US and GB never share a cache entry.
179
+ * DIALECT: US (`british=False`, the `af_`/`am_` voices) OR GB (`british=True`,
180
+ * `bf_`/`bm_`) `phonemize(text, british)` and `version(british)` take the
181
+ * dialect per call (the caller derives it from the blend's voice prefixes via
182
+ * `resolveBlend`). The dialect folds into `version()` (`dialect=us`/`gb`) so US
183
+ * and GB never share a cache entry. A mixed US+GB blend is rejected upstream
184
+ * (different espeak phoneme front-ends, like mixed languages).
182
185
  *
183
186
  * DETERMINISM: g2p runs at PREPARE time, never at render. Its identity — a PURE
184
187
  * pin string `misaki=<PIN> map=<MAP> dialect=us` (NO Python, no spawn) — folds
@@ -238,13 +241,15 @@ def _check_pins():
238
241
  sys.exit(96)
239
242
 
240
243
  def _g2p():
244
+ # dialect: --british ⇒ GB English (bf_/bm_ voices); default US (af_/am_)
245
+ british = "--british" in sys.argv[1:]
241
246
  try:
242
247
  from misaki import espeak
243
- fallback = espeak.EspeakFallback(british=False)
248
+ fallback = espeak.EspeakFallback(british=british)
244
249
  except Exception:
245
250
  sys.exit(95)
246
251
  try:
247
- g = en.G2P(trf=False, british=False, fallback=fallback)
252
+ g = en.G2P(trf=False, british=british, fallback=fallback)
248
253
  except OSError:
249
254
  # spaCy en_core_web_sm model not downloaded
250
255
  sys.exit(94)
@@ -281,14 +286,16 @@ function misakiEnG2p(opts = {}) {
281
286
  const python = opts.python ?? process.env["MISAKI_PYTHON"] ?? DEFAULT_PYTHON;
282
287
  return {
283
288
  id: "misaki-en",
284
- version: () => `misaki-en misaki=${MISAKI_PIN} map=${EN_PHONEME_MAP_VERSION} dialect=us`,
285
- phonemize: (text) => {
286
- const r = spawnSync(python, [
289
+ version: (british = false) => `misaki-en misaki=${MISAKI_PIN} map=${EN_PHONEME_MAP_VERSION} dialect=${british ? "gb" : "us"}`,
290
+ phonemize: (text, british = false) => {
291
+ const args = [
287
292
  "-c",
288
293
  EN_G2P_PY,
289
294
  "--pins",
290
295
  MISAKI_PIN
291
- ], {
296
+ ];
297
+ if (british) args.push("--british");
298
+ const r = spawnSync(python, args, {
292
299
  input: text,
293
300
  maxBuffer: 8 * 1024 * 1024
294
301
  });
@@ -611,9 +618,22 @@ function resolveBlend(spec) {
611
618
  const en = raw.filter((_, i) => !chinese[i]).map(([n]) => n);
612
619
  throw new NarrationError(`voice blend mixes languages — Chinese [${zh.join(", ")}] with non-Chinese [${en.join(", ")}]; a blend must share one g2p front-end (all zf_/zm_ OR all English)`);
613
620
  }
621
+ let dialect;
622
+ if (allEn) {
623
+ const british = raw.map(([name]) => isKokoroBritishVoice(name));
624
+ const allGb = british.every(Boolean);
625
+ const allUs = british.every((b) => !b);
626
+ if (!allGb && !allUs) {
627
+ const gb = raw.filter((_, i) => british[i]).map(([n]) => n);
628
+ const us = raw.filter((_, i) => !british[i]).map(([n]) => n);
629
+ throw new NarrationError(`voice blend mixes English dialects — British [${gb.join(", ")}] with American [${us.join(", ")}]; a blend must share one espeak front-end (all bf_/bm_ OR all af_/am_)`);
630
+ }
631
+ dialect = allGb ? "gb" : "us";
632
+ }
614
633
  return {
615
634
  entries: raw.map(([name, weight]) => [name, weight / sum]),
616
- language: allZh ? "zh" : "en"
635
+ language: allZh ? "zh" : "en",
636
+ ...dialect !== void 0 ? { dialect } : {}
617
637
  };
618
638
  }
619
639
  /**
@@ -624,8 +644,8 @@ function resolveBlend(spec) {
624
644
  * segment cache, EXACTLY the 0.15 g2p-identity pattern.
625
645
  */
626
646
  function blendIdentity(spec) {
627
- const { entries, language } = resolveBlend(spec);
628
- return `blend=[${entries.map(([name, w]) => `${name}:${w.toFixed(6)}`).join(",")} lang=${language} v${BLEND_SPEC_VERSION}]`;
647
+ const { entries, language, dialect } = resolveBlend(spec);
648
+ return `blend=[${entries.map(([name, w]) => `${name}:${w.toFixed(6)}`).join(",")} lang=${language}${dialect !== void 0 ? ` dialect=${dialect}` : ""} v${BLEND_SPEC_VERSION}]`;
629
649
  }
630
650
  /**
631
651
  * Compute the normalized weighted sum of base-voice style tensors into one
@@ -756,6 +776,10 @@ async function kokoroGenerateFromBlend(tts, Tensor, inputIds, blended, speed) {
756
776
  function isKokoroChineseVoice(voice) {
757
777
  return voice.startsWith("zf_") || voice.startsWith("zm_");
758
778
  }
779
+ /** A b* (bf_/bm_) kokoro voice is BRITISH English → misaki[en] british=True. */
780
+ function isKokoroBritishVoice(voice) {
781
+ return voice.startsWith("bf_") || voice.startsWith("bm_");
782
+ }
759
783
  function kokoroProvider(opts = {}) {
760
784
  const modelId = opts.model ?? KOKORO_MODEL;
761
785
  const dtype = opts.dtype ?? "q8";
@@ -792,7 +816,8 @@ function kokoroProvider(opts = {}) {
792
816
  let blendSuffix = "";
793
817
  if (isVoiceBlend(opts.voice)) {
794
818
  blendSuffix = ` ${blendIdentity(opts.voice)}`;
795
- if (resolveBlend(opts.voice).language === "en") blendSuffix += ` en-g2p=[${enG2p.version()}]`;
819
+ const resolvedV = resolveBlend(opts.voice);
820
+ if (resolvedV.language === "en") blendSuffix += ` en-g2p=[${enG2p.version(resolvedV.dialect === "gb")}]`;
796
821
  }
797
822
  const v = `kokoro-js ${version} ${basename(modelId)} dtype=${dtype} g2p=[${zhG2p.version()}]${blendSuffix}`;
798
823
  return Promise.resolve(v);
@@ -806,14 +831,14 @@ function kokoroProvider(opts = {}) {
806
831
  const blended = blendStyleVectors(resolved.entries, (name) => loadKokoroVoiceData(entry, name));
807
832
  const Tensor = await loadTensorCtor(entry);
808
833
  const recipe = resolved.entries.map(([n, w]) => `${n}*${w.toFixed(4)}`).join(" + ");
809
- console.log(`[narrate] kokoro blended voice (Apache-2.0, derived) lang=${resolved.language}: ${recipe}`);
834
+ console.log(`[narrate] kokoro blended voice (Apache-2.0, derived) lang=${resolved.language}${resolved.dialect !== void 0 ? `/${resolved.dialect}` : ""}: ${recipe}`);
810
835
  let inputIds;
811
836
  try {
812
837
  if (resolved.language === "zh") {
813
838
  const phonemes = zhG2p.phonemize(req.text);
814
839
  inputIds = tts.tokenizer(phonemes, { truncation: true }).input_ids;
815
840
  } else {
816
- const phonemes = enG2p.phonemize(req.text);
841
+ const phonemes = enG2p.phonemize(req.text, resolved.dialect === "gb");
817
842
  inputIds = tts.tokenizer(phonemes, { truncation: true }).input_ids;
818
843
  }
819
844
  const audio = await kokoroGenerateFromBlend(tts, Tensor, inputIds, blended, speed ?? 1);
@@ -1209,4 +1234,4 @@ function scriptPathFor(input) {
1209
1234
  return candidate;
1210
1235
  }
1211
1236
  //#endregion
1212
- export { BLEND_SPEC_VERSION, alignerById, blendIdentity, blendStyleVectors, cacheKey, espeakProvider, fakeProvider, floatToWav, heuristicAligner, heuristicWords, interpolateMissing, isKokoroChineseVoice, kokoroGenerateFromBlend, kokoroProvider, loadKokoroVoiceData, mapAsrToScript, openaiProvider, piperProvider, providerById, requireStringVoice, resolveBlend, resolvePiperVoice, scriptPathFor, stderrTail, synthesizeScript, voskAligner, wavDuration };
1237
+ export { BLEND_SPEC_VERSION, alignerById, blendIdentity, blendStyleVectors, cacheKey, espeakProvider, fakeProvider, floatToWav, heuristicAligner, heuristicWords, interpolateMissing, isKokoroBritishVoice, isKokoroChineseVoice, kokoroGenerateFromBlend, kokoroProvider, loadKokoroVoiceData, mapAsrToScript, openaiProvider, piperProvider, providerById, requireStringVoice, resolveBlend, resolvePiperVoice, scriptPathFor, stderrTail, synthesizeScript, voskAligner, wavDuration };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/narrate",
3
- "version": "0.23.0-pre.2",
3
+ "version": "0.23.0-pre.4",
4
4
  "description": "glissade narration + captions: TTS at prepare time (gs narrate), deterministic caching, narration-anchored timeline beats, and captions as plain tracks. Render stays offline.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -22,8 +22,8 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "@glissade/core": "0.23.0-pre.2",
26
- "@glissade/scene": "0.23.0-pre.2"
25
+ "@glissade/core": "0.23.0-pre.4",
26
+ "@glissade/scene": "0.23.0-pre.4"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "kokoro-js": "^1.2.0"