@glissade/narrate 0.22.0-pre.3 → 0.22.0-pre.5
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 +16 -0
- package/dist/providers.js +167 -12
- package/package.json +3 -3
package/dist/providers.d.ts
CHANGED
|
@@ -16,6 +16,21 @@ interface ZhG2p {
|
|
|
16
16
|
* its Python tool, so the precedent is exact).
|
|
17
17
|
*/
|
|
18
18
|
//#endregion
|
|
19
|
+
//#region src/en-g2p.d.ts
|
|
20
|
+
|
|
21
|
+
interface EnG2p {
|
|
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;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fork B: shell out to a pinned Python misaki[en]. Mirrors {@link misakiZhG2p} —
|
|
30
|
+
* same spawnSync, same ENOENT-is-the-only-true-absent feature detection, same
|
|
31
|
+
* PURE pin-based version()-fold-into-the-cache-key contract.
|
|
32
|
+
*/
|
|
33
|
+
//#endregion
|
|
19
34
|
//#region src/providers.d.ts
|
|
20
35
|
|
|
21
36
|
interface TtsRequest {
|
|
@@ -202,6 +217,7 @@ declare function kokoroProvider(opts?: {
|
|
|
202
217
|
voice?: VoiceSpec;
|
|
203
218
|
dtype?: KokoroDtype;
|
|
204
219
|
zhG2p?: ZhG2p;
|
|
220
|
+
enG2p?: EnG2p;
|
|
205
221
|
}): TtsProvider;
|
|
206
222
|
declare function providerById(id: string): TtsProvider;
|
|
207
223
|
interface AlignRequest {
|
package/dist/providers.js
CHANGED
|
@@ -40,19 +40,19 @@ const JIEBA_PIN = "0.42.1";
|
|
|
40
40
|
*/
|
|
41
41
|
const PHONEME_MAP_VERSION = "zh-misaki-1";
|
|
42
42
|
/** The default Python interpreter; override via opts.python or MISAKI_PYTHON. */
|
|
43
|
-
const DEFAULT_PYTHON = "python3";
|
|
43
|
+
const DEFAULT_PYTHON$1 = "python3";
|
|
44
44
|
/**
|
|
45
45
|
* On a missing misaki/jieba import the program exits with this exact code (not a
|
|
46
46
|
* raw traceback) so the TS side can raise the install hint — the Python-present-
|
|
47
47
|
* but-misaki-absent analogue of ENOENT. Picked to not collide with 0/1/2.
|
|
48
48
|
*/
|
|
49
|
-
const MISAKI_ABSENT_EXIT = 97;
|
|
49
|
+
const MISAKI_ABSENT_EXIT$1 = 97;
|
|
50
50
|
/**
|
|
51
51
|
* On an installed-vs-PINNED version MISMATCH the phonemize program exits with this
|
|
52
52
|
* exact code so the TS side raises an actionable `installed X != pinned PIN` error
|
|
53
53
|
* (never a silent 'unknown'). Distinct from MISAKI_ABSENT_EXIT (97) and 0/1/2.
|
|
54
54
|
*/
|
|
55
|
-
const MISAKI_PIN_MISMATCH_EXIT = 96;
|
|
55
|
+
const MISAKI_PIN_MISMATCH_EXIT$1 = 96;
|
|
56
56
|
/**
|
|
57
57
|
* The phonemize Python program. It imports misaki[zh], ENFORCES the declared pins
|
|
58
58
|
* (resolving the installed versions via `importlib.metadata.version(...)` — the
|
|
@@ -104,7 +104,7 @@ _phonemize()
|
|
|
104
104
|
`;
|
|
105
105
|
/** The shared install hint — raised when Python is absent (ENOENT) OR present
|
|
106
106
|
* but misaki/jieba can't be imported (the MISAKI_ABSENT_EXIT sentinel). */
|
|
107
|
-
function installHint(python, why) {
|
|
107
|
+
function installHint$1(python, why) {
|
|
108
108
|
return new NarrationError(`${why === "python" ? `Python ('${python}') not found on PATH for misaki[zh] g2p` : `misaki[zh] is not importable from '${python}'`} — the kokoro Chinese (z*) route needs it. Install: \`pip install 'misaki[zh]==${MISAKI_PIN}' 'jieba==${JIEBA_PIN}'\` into that interpreter (or set MISAKI_PYTHON to one that has it), or use --provider piper for Chinese.`);
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
@@ -113,16 +113,16 @@ function installHint(python, why) {
|
|
|
113
113
|
* `<dist>:<installed>:<pinned>` to stderr; we surface it as an actionable error so
|
|
114
114
|
* a divergent wheel is caught LOUDLY rather than silently hashing to 'unknown'.
|
|
115
115
|
*/
|
|
116
|
-
function pinMismatchError(python, stderr) {
|
|
116
|
+
function pinMismatchError$1(python, stderr) {
|
|
117
117
|
const line = (typeof stderr === "string" ? stderr : stderr?.toString("utf8") ?? "").trim().split("\n").pop() ?? "";
|
|
118
118
|
const [dist, installed, pinned] = line.split(":");
|
|
119
119
|
return new NarrationError(`misaki[zh] g2p pin mismatch via '${python}': ${dist && installed && pinned ? `installed ${dist} ${installed} != pinned ${dist === "misaki" ? "MISAKI_PIN" : "JIEBA_PIN"} ${pinned}` : `installed misaki/jieba wheel diverges from the declared pins (${line || "no detail"})`} — the segment cache identity is keyed on the pins, so a divergent wheel would produce uncacheable/cross-machine-divergent audio. Install the pinned wheels: \`pip install 'misaki[zh]==${MISAKI_PIN}' 'jieba==${JIEBA_PIN}'\` into that interpreter (or set MISAKI_PYTHON to one that has them).`);
|
|
120
120
|
}
|
|
121
121
|
/** Surface a real (non-ENOENT) spawn error as a NarrationError, else null.
|
|
122
122
|
* ENOENT (python absent) becomes the install hint. */
|
|
123
|
-
function spawnFailure(r, python) {
|
|
123
|
+
function spawnFailure$1(r, python) {
|
|
124
124
|
if (!r.error) return null;
|
|
125
|
-
if (r.error.code === "ENOENT") return installHint(python, "python");
|
|
125
|
+
if (r.error.code === "ENOENT") return installHint$1(python, "python");
|
|
126
126
|
return new NarrationError(`could not run misaki[zh] g2p via '${python}': ${r.error.message}`);
|
|
127
127
|
}
|
|
128
128
|
/**
|
|
@@ -132,7 +132,7 @@ function spawnFailure(r, python) {
|
|
|
132
132
|
* its Python tool, so the precedent is exact).
|
|
133
133
|
*/
|
|
134
134
|
function misakiZhG2p(opts = {}) {
|
|
135
|
-
const python = opts.python ?? process.env["MISAKI_PYTHON"] ?? DEFAULT_PYTHON;
|
|
135
|
+
const python = opts.python ?? process.env["MISAKI_PYTHON"] ?? DEFAULT_PYTHON$1;
|
|
136
136
|
return {
|
|
137
137
|
id: "misaki-zh",
|
|
138
138
|
version: () => `misaki-zh misaki=${MISAKI_PIN} jieba=${JIEBA_PIN} map=${PHONEME_MAP_VERSION}`,
|
|
@@ -147,13 +147,160 @@ function misakiZhG2p(opts = {}) {
|
|
|
147
147
|
input: text,
|
|
148
148
|
maxBuffer: 8 * 1024 * 1024
|
|
149
149
|
});
|
|
150
|
+
const fail = spawnFailure$1(r, python);
|
|
151
|
+
if (fail) throw fail;
|
|
152
|
+
if (r.status === MISAKI_ABSENT_EXIT$1) throw installHint$1(python, "misaki");
|
|
153
|
+
if (r.status === MISAKI_PIN_MISMATCH_EXIT$1) throw pinMismatchError$1(python, r.stderr);
|
|
154
|
+
if (r.status !== 0) throw new NarrationError(`misaki[zh] g2p failed: ${stderrTail(r.stderr)}`);
|
|
155
|
+
const out = r.stdout?.toString("utf8") ?? "";
|
|
156
|
+
if (out.length === 0) throw new NarrationError(`misaki[zh] g2p produced no phonemes for text: ${JSON.stringify(text.slice(0, 60))}`);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/en-g2p.ts
|
|
163
|
+
/**
|
|
164
|
+
* '@glissade/narrate/providers' English g2p seam — English text → a misaki[en]
|
|
165
|
+
* phoneme string, the alphabet the kokoro English (`af_`/`am_`/`bf_`/`bm_`)
|
|
166
|
+
* voices were trained on. The gh#2 ENGLISH BLEND follow-up: a blended voice has
|
|
167
|
+
* no registered name, so it must drive `generate_from_ids` with a SUMMED style
|
|
168
|
+
* tensor — which bypasses kokoro-js's internal English phonemizer. So, exactly as
|
|
169
|
+
* the shipped Chinese blend runs misaki[zh] itself, this runs misaki[en] itself.
|
|
170
|
+
*
|
|
171
|
+
* Fork B = shell out to a PINNED Python misaki[en], the SAME pattern as
|
|
172
|
+
* {@link './zh-g2p'} (spawnSync + ENOENT-is-the-only-true-absent + a pure
|
|
173
|
+
* pin-based `version()` that folds into the cache key). misaki[en]'s G2P uses a
|
|
174
|
+
* spaCy tagger (`en_core_web_sm`) + an espeak fallback for out-of-dictionary
|
|
175
|
+
* words — so the install surface is heavier than zh's (misaki[en] + the spaCy
|
|
176
|
+
* model + espeak-ng); each missing piece exits with a distinct code → an
|
|
177
|
+
* actionable hint.
|
|
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.
|
|
182
|
+
*
|
|
183
|
+
* DETERMINISM: g2p runs at PREPARE time, never at render. Its identity — a PURE
|
|
184
|
+
* pin string `misaki=<PIN> map=<MAP> dialect=us` (NO Python, no spawn) — folds
|
|
185
|
+
* UNCONDITIONALLY into kokoroProvider.version() → the segment cache key. The
|
|
186
|
+
* misaki dictionary (the vast majority of words) is fully pinned/deterministic;
|
|
187
|
+
* OOV words route through the espeak fallback, whose phonemes depend on the local
|
|
188
|
+
* espeak-ng (the one OOV-determinism caveat, shared with Kokoro's own English
|
|
189
|
+
* g2p — noted, not silently swept). The pin is ENFORCED against the installed
|
|
190
|
+
* wheel at synth time.
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* The phoneme-map version: bump when our text→misaki[en] invocation changes in a
|
|
194
|
+
* way that can move phoneme bytes (normalization, dialect default, fallback
|
|
195
|
+
* wiring), independent of the misaki wheel. Folds into the g2p identity.
|
|
196
|
+
*/
|
|
197
|
+
const EN_PHONEME_MAP_VERSION = "en-misaki-1";
|
|
198
|
+
/** The default Python interpreter; override via opts.python or MISAKI_PYTHON
|
|
199
|
+
* (shared with the zh seam — one interpreter has both extras). */
|
|
200
|
+
const DEFAULT_PYTHON = "python3";
|
|
201
|
+
/** misaki[en] not importable (the Python-present-but-misaki-absent analogue of ENOENT). */
|
|
202
|
+
const MISAKI_ABSENT_EXIT = 97;
|
|
203
|
+
/** installed misaki wheel != the declared pin (caught loudly at synth). */
|
|
204
|
+
const MISAKI_PIN_MISMATCH_EXIT = 96;
|
|
205
|
+
/** the espeak fallback (espeakng) can't load — OOV words would be dropped. */
|
|
206
|
+
const ESPEAK_ABSENT_EXIT = 95;
|
|
207
|
+
/** the spaCy English tagger model (`en_core_web_sm`) isn't installed. */
|
|
208
|
+
const SPACY_MODEL_ABSENT_EXIT = 94;
|
|
209
|
+
/**
|
|
210
|
+
* The phonemize Python program: import misaki[en], ENFORCE the misaki pin, build
|
|
211
|
+
* the US-English G2P (spaCy `en_core_web_sm` tagger + espeak fallback for OOV),
|
|
212
|
+
* read text from stdin, write `<phonemes>` to stdout. Each missing dependency
|
|
213
|
+
* gets its OWN exit code so the TS side raises a specific install hint.
|
|
214
|
+
*/
|
|
215
|
+
const EN_G2P_PY = String.raw`
|
|
216
|
+
import sys
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
import misaki
|
|
220
|
+
from misaki import en
|
|
221
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
222
|
+
except ImportError:
|
|
223
|
+
sys.exit(97)
|
|
224
|
+
|
|
225
|
+
# pins arrive as: --pins <misaki_pin>; enforce installed==pinned
|
|
226
|
+
def _check_pins():
|
|
227
|
+
args = sys.argv[1:]
|
|
228
|
+
if "--pins" not in args:
|
|
229
|
+
return
|
|
230
|
+
i = args.index("--pins")
|
|
231
|
+
pin = args[i + 1]
|
|
232
|
+
try:
|
|
233
|
+
installed = version("misaki")
|
|
234
|
+
except PackageNotFoundError:
|
|
235
|
+
installed = "unknown"
|
|
236
|
+
if installed != pin:
|
|
237
|
+
sys.stderr.write("misaki:%s:%s\n" % (installed, pin))
|
|
238
|
+
sys.exit(96)
|
|
239
|
+
|
|
240
|
+
def _g2p():
|
|
241
|
+
try:
|
|
242
|
+
from misaki import espeak
|
|
243
|
+
fallback = espeak.EspeakFallback(british=False)
|
|
244
|
+
except Exception:
|
|
245
|
+
sys.exit(95)
|
|
246
|
+
try:
|
|
247
|
+
g = en.G2P(trf=False, british=False, fallback=fallback)
|
|
248
|
+
except OSError:
|
|
249
|
+
# spaCy en_core_web_sm model not downloaded
|
|
250
|
+
sys.exit(94)
|
|
251
|
+
text = sys.stdin.buffer.read().decode("utf-8")
|
|
252
|
+
phonemes, _ = g(text)
|
|
253
|
+
sys.stdout.buffer.write((phonemes or "").encode("utf-8"))
|
|
254
|
+
|
|
255
|
+
_check_pins()
|
|
256
|
+
_g2p()
|
|
257
|
+
`;
|
|
258
|
+
/** Install hint — Python absent (ENOENT), misaki[en] absent, or a missing
|
|
259
|
+
* sub-dependency (espeak fallback / spaCy model). */
|
|
260
|
+
function installHint(python, why) {
|
|
261
|
+
return new NarrationError(`${why === "python" ? `Python ('${python}') not found on PATH for misaki[en] g2p` : why === "misaki" ? `misaki[en] is not importable from '${python}'` : why === "espeak" ? `misaki[en]'s espeak fallback is unavailable from '${python}' (needed for out-of-dictionary words)` : `misaki[en]'s spaCy tagger model 'en_core_web_sm' is not installed for '${python}'`} — the kokoro ENGLISH voice-blend route (gh#2) needs it. Install: ${why === "spacy" ? `\`${python} -m spacy download en_core_web_sm\`` : why === "espeak" ? `\`pip install misaki[en]\` AND install the espeak-ng system library (e.g. \`apt-get install espeak-ng\`)` : `\`pip install 'misaki[en]==${MISAKI_PIN}'\` AND \`${python} -m spacy download en_core_web_sm\` AND the espeak-ng system library`} into that interpreter (or set MISAKI_PYTHON to one that has it). A single English voice (the named path) does NOT need this; only English BLENDS do.`);
|
|
262
|
+
}
|
|
263
|
+
/** The installed misaki wheel diverges from the declared pin (caught at synth). */
|
|
264
|
+
function pinMismatchError(python, stderr) {
|
|
265
|
+
const line = (typeof stderr === "string" ? stderr : stderr?.toString("utf8") ?? "").trim().split("\n").pop() ?? "";
|
|
266
|
+
const [, installed, pinned] = line.split(":");
|
|
267
|
+
return new NarrationError(`misaki[en] g2p pin mismatch via '${python}': ${installed && pinned ? `installed misaki ${installed} != pinned ${pinned}` : `installed misaki wheel diverges from the declared pin (${line || "no detail"})`} — the segment cache identity is keyed on the pin, so a divergent wheel would produce uncacheable/cross-machine-divergent audio. Install the pinned wheel: \`pip install 'misaki[en]==${MISAKI_PIN}'\` into that interpreter.`);
|
|
268
|
+
}
|
|
269
|
+
/** Surface a real (non-ENOENT) spawn error, else null. */
|
|
270
|
+
function spawnFailure(r, python) {
|
|
271
|
+
if (!r.error) return null;
|
|
272
|
+
if (r.error.code === "ENOENT") return installHint(python, "python");
|
|
273
|
+
return new NarrationError(`could not run misaki[en] g2p via '${python}': ${r.error.message}`);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Fork B: shell out to a pinned Python misaki[en]. Mirrors {@link misakiZhG2p} —
|
|
277
|
+
* same spawnSync, same ENOENT-is-the-only-true-absent feature detection, same
|
|
278
|
+
* PURE pin-based version()-fold-into-the-cache-key contract.
|
|
279
|
+
*/
|
|
280
|
+
function misakiEnG2p(opts = {}) {
|
|
281
|
+
const python = opts.python ?? process.env["MISAKI_PYTHON"] ?? DEFAULT_PYTHON;
|
|
282
|
+
return {
|
|
283
|
+
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, [
|
|
287
|
+
"-c",
|
|
288
|
+
EN_G2P_PY,
|
|
289
|
+
"--pins",
|
|
290
|
+
MISAKI_PIN
|
|
291
|
+
], {
|
|
292
|
+
input: text,
|
|
293
|
+
maxBuffer: 8 * 1024 * 1024
|
|
294
|
+
});
|
|
150
295
|
const fail = spawnFailure(r, python);
|
|
151
296
|
if (fail) throw fail;
|
|
152
297
|
if (r.status === MISAKI_ABSENT_EXIT) throw installHint(python, "misaki");
|
|
298
|
+
if (r.status === ESPEAK_ABSENT_EXIT) throw installHint(python, "espeak");
|
|
299
|
+
if (r.status === SPACY_MODEL_ABSENT_EXIT) throw installHint(python, "spacy");
|
|
153
300
|
if (r.status === MISAKI_PIN_MISMATCH_EXIT) throw pinMismatchError(python, r.stderr);
|
|
154
|
-
if (r.status !== 0) throw new NarrationError(`misaki[
|
|
301
|
+
if (r.status !== 0) throw new NarrationError(`misaki[en] g2p failed: ${stderrTail(r.stderr)}`);
|
|
155
302
|
const out = r.stdout?.toString("utf8") ?? "";
|
|
156
|
-
if (out.length === 0) throw new NarrationError(`misaki[
|
|
303
|
+
if (out.length === 0) throw new NarrationError(`misaki[en] g2p produced no phonemes for text: ${JSON.stringify(text.slice(0, 60))}`);
|
|
157
304
|
return out;
|
|
158
305
|
}
|
|
159
306
|
};
|
|
@@ -613,6 +760,7 @@ function kokoroProvider(opts = {}) {
|
|
|
613
760
|
const modelId = opts.model ?? KOKORO_MODEL;
|
|
614
761
|
const dtype = opts.dtype ?? "q8";
|
|
615
762
|
const zhG2p = opts.zhG2p ?? misakiZhG2p();
|
|
763
|
+
const enG2p = opts.enG2p ?? misakiEnG2p();
|
|
616
764
|
let loaded = null;
|
|
617
765
|
const loadLib = async () => {
|
|
618
766
|
const { entry, entryUrl } = resolveKokoro();
|
|
@@ -641,7 +789,11 @@ function kokoroProvider(opts = {}) {
|
|
|
641
789
|
id: "kokoro",
|
|
642
790
|
version: () => {
|
|
643
791
|
const { version } = resolveKokoro();
|
|
644
|
-
|
|
792
|
+
let blendSuffix = "";
|
|
793
|
+
if (isVoiceBlend(opts.voice)) {
|
|
794
|
+
blendSuffix = ` ${blendIdentity(opts.voice)}`;
|
|
795
|
+
if (resolveBlend(opts.voice).language === "en") blendSuffix += ` en-g2p=[${enG2p.version()}]`;
|
|
796
|
+
}
|
|
645
797
|
const v = `kokoro-js ${version} ${basename(modelId)} dtype=${dtype} g2p=[${zhG2p.version()}]${blendSuffix}`;
|
|
646
798
|
return Promise.resolve(v);
|
|
647
799
|
},
|
|
@@ -660,7 +812,10 @@ function kokoroProvider(opts = {}) {
|
|
|
660
812
|
if (resolved.language === "zh") {
|
|
661
813
|
const phonemes = zhG2p.phonemize(req.text);
|
|
662
814
|
inputIds = tts.tokenizer(phonemes, { truncation: true }).input_ids;
|
|
663
|
-
} else
|
|
815
|
+
} else {
|
|
816
|
+
const phonemes = enG2p.phonemize(req.text);
|
|
817
|
+
inputIds = tts.tokenizer(phonemes, { truncation: true }).input_ids;
|
|
818
|
+
}
|
|
664
819
|
const audio = await kokoroGenerateFromBlend(tts, Tensor, inputIds, blended, speed ?? 1);
|
|
665
820
|
const wav = floatToWav(audio.audio, audio.sampling_rate);
|
|
666
821
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/narrate",
|
|
3
|
-
"version": "0.22.0-pre.
|
|
3
|
+
"version": "0.22.0-pre.5",
|
|
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.22.0-pre.
|
|
26
|
-
"@glissade/scene": "0.22.0-pre.
|
|
25
|
+
"@glissade/core": "0.22.0-pre.5",
|
|
26
|
+
"@glissade/scene": "0.22.0-pre.5"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"kokoro-js": "^1.2.0"
|