@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.
- package/dist/providers.d.ts +12 -5
- package/dist/providers.js +41 -16
- package/package.json +3 -3
package/dist/providers.d.ts
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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=
|
|
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=
|
|
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
|
|
285
|
-
phonemize: (text) => {
|
|
286
|
-
const
|
|
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
|
-
|
|
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.
|
|
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.
|
|
26
|
-
"@glissade/scene": "0.23.0-pre.
|
|
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"
|