@glissade/narrate 0.15.0-pre.0 → 0.15.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/dist/providers.js +65 -42
- package/package.json +3 -3
package/dist/providers.js
CHANGED
|
@@ -21,9 +21,13 @@ import { pathToFileURL } from "node:url";
|
|
|
21
21
|
* pure-TS Fork A can be checked against it offline.
|
|
22
22
|
*
|
|
23
23
|
* DETERMINISM: g2p runs at PREPARE time (gs narrate), never at render. Its
|
|
24
|
-
* identity
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* identity — a PURE pin-based string `misaki=<MISAKI_PIN> jieba=<JIEBA_PIN>
|
|
25
|
+
* map=<PHONEME_MAP_VERSION>`, with NO Python (no spawn, no wheel introspection) —
|
|
26
|
+
* folds UNCONDITIONALLY into kokoroProvider.version(), which keys the segment
|
|
27
|
+
* cache. The pinned wheel version implies its bundled dict, so a live dict-hash is
|
|
28
|
+
* not needed in the identity. Bumping any pin/map invalidates stale Mandarin audio.
|
|
29
|
+
* The declared pins are ENFORCED against the installed wheel at synth time
|
|
30
|
+
* (phonemize, via importlib.metadata) — a divergent wheel fails LOUDLY there.
|
|
27
31
|
*/
|
|
28
32
|
/** The misaki wheel this seam is pinned to (matches scripts/gen-misaki-parity.py). */
|
|
29
33
|
const MISAKI_PIN = "0.9.4";
|
|
@@ -38,40 +42,56 @@ const PHONEME_MAP_VERSION = "zh-misaki-1";
|
|
|
38
42
|
/** The default Python interpreter; override via opts.python or MISAKI_PYTHON. */
|
|
39
43
|
const DEFAULT_PYTHON = "python3";
|
|
40
44
|
/**
|
|
41
|
-
* The Python program we shell out to. It imports misaki[zh], builds the g2p once,
|
|
42
|
-
* reads the Mandarin text from stdin, prints `<phonemes>\n` to stdout, and — when
|
|
43
|
-
* asked with `--id` — prints a stable identity line
|
|
44
|
-
* `misaki=<ver> jieba=<ver> dict=<sha12>`
|
|
45
|
-
* (jieba dict hash included: its segmentation determines word boundaries, so a
|
|
46
|
-
* dict change can move phonemes). ENOENT (python absent) is the only true
|
|
47
|
-
* "g2p unavailable"; any other failure surfaces the Python traceback tail.
|
|
48
|
-
*/
|
|
49
|
-
/**
|
|
50
45
|
* On a missing misaki/jieba import the program exits with this exact code (not a
|
|
51
46
|
* raw traceback) so the TS side can raise the install hint — the Python-present-
|
|
52
47
|
* but-misaki-absent analogue of ENOENT. Picked to not collide with 0/1/2.
|
|
53
48
|
*/
|
|
54
49
|
const MISAKI_ABSENT_EXIT = 97;
|
|
50
|
+
/**
|
|
51
|
+
* On an installed-vs-PINNED version MISMATCH the phonemize program exits with this
|
|
52
|
+
* exact code so the TS side raises an actionable `installed X != pinned PIN` error
|
|
53
|
+
* (never a silent 'unknown'). Distinct from MISAKI_ABSENT_EXIT (97) and 0/1/2.
|
|
54
|
+
*/
|
|
55
|
+
const MISAKI_PIN_MISMATCH_EXIT = 96;
|
|
56
|
+
/**
|
|
57
|
+
* The phonemize Python program. It imports misaki[zh], ENFORCES the declared pins
|
|
58
|
+
* (resolving the installed versions via `importlib.metadata.version(...)` — the
|
|
59
|
+
* authoritative dist-info, present even when a wheel exposes no `__version__`, as
|
|
60
|
+
* jieba historically does not), reads the Mandarin text from stdin, and writes
|
|
61
|
+
* `<phonemes>` to stdout.
|
|
62
|
+
*
|
|
63
|
+
* Pin enforcement lives at SYNTH time (HERE), NOT in `version()`: `version()` is a
|
|
64
|
+
* PURE pin-based identity string (no Python at all). A divergent wheel is caught
|
|
65
|
+
* loudly here — on a mismatch it writes `<dist>:<installed>:<pinned>` to stderr and
|
|
66
|
+
* exits MISAKI_PIN_MISMATCH_EXIT so the TS side raises an actionable error. ENOENT
|
|
67
|
+
* (python absent) and ImportError (misaki absent → exit 97) remain the two
|
|
68
|
+
* "g2p unavailable" exits; any other failure surfaces the Python traceback tail.
|
|
69
|
+
*/
|
|
55
70
|
const ZH_G2P_PY = String.raw`
|
|
56
|
-
import sys
|
|
71
|
+
import sys
|
|
57
72
|
|
|
58
73
|
try:
|
|
59
74
|
import misaki, jieba
|
|
60
75
|
from misaki import zh
|
|
76
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
61
77
|
except ImportError:
|
|
62
78
|
sys.exit(97)
|
|
63
79
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
# pins arrive as: --pins <misaki_pin> <jieba_pin>; enforce installed==pinned
|
|
81
|
+
def _check_pins():
|
|
82
|
+
args = sys.argv[1:]
|
|
83
|
+
if "--pins" not in args:
|
|
84
|
+
return
|
|
85
|
+
i = args.index("--pins")
|
|
86
|
+
pins = (("misaki", args[i + 1]), ("jieba", args[i + 2]))
|
|
87
|
+
for dist, pin in pins:
|
|
88
|
+
try:
|
|
89
|
+
installed = version(dist)
|
|
90
|
+
except PackageNotFoundError:
|
|
91
|
+
installed = "unknown"
|
|
92
|
+
if installed != pin:
|
|
93
|
+
sys.stderr.write("%s:%s:%s\n" % (dist, installed, pin))
|
|
94
|
+
sys.exit(96)
|
|
75
95
|
|
|
76
96
|
def _phonemize():
|
|
77
97
|
g = zh.ZHG2P()
|
|
@@ -79,16 +99,25 @@ def _phonemize():
|
|
|
79
99
|
phonemes, _ = g(text)
|
|
80
100
|
sys.stdout.buffer.write(phonemes.encode("utf-8"))
|
|
81
101
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
else:
|
|
85
|
-
_phonemize()
|
|
102
|
+
_check_pins()
|
|
103
|
+
_phonemize()
|
|
86
104
|
`;
|
|
87
105
|
/** The shared install hint — raised when Python is absent (ENOENT) OR present
|
|
88
106
|
* but misaki/jieba can't be imported (the MISAKI_ABSENT_EXIT sentinel). */
|
|
89
107
|
function installHint(python, why) {
|
|
90
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.`);
|
|
91
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Raised when the installed misaki/jieba wheel diverges from the declared pins
|
|
112
|
+
* (the MISAKI_PIN_MISMATCH_EXIT sentinel at synth time). The Python side wrote
|
|
113
|
+
* `<dist>:<installed>:<pinned>` to stderr; we surface it as an actionable error so
|
|
114
|
+
* a divergent wheel is caught LOUDLY rather than silently hashing to 'unknown'.
|
|
115
|
+
*/
|
|
116
|
+
function pinMismatchError(python, stderr) {
|
|
117
|
+
const line = (typeof stderr === "string" ? stderr : stderr?.toString("utf8") ?? "").trim().split("\n").pop() ?? "";
|
|
118
|
+
const [dist, installed, pinned] = line.split(":");
|
|
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
|
+
}
|
|
92
121
|
/** Surface a real (non-ENOENT) spawn error as a NarrationError, else null.
|
|
93
122
|
* ENOENT (python absent) becomes the install hint. */
|
|
94
123
|
function spawnFailure(r, python) {
|
|
@@ -106,26 +135,22 @@ function misakiZhG2p(opts = {}) {
|
|
|
106
135
|
const python = opts.python ?? process.env["MISAKI_PYTHON"] ?? DEFAULT_PYTHON;
|
|
107
136
|
return {
|
|
108
137
|
id: "misaki-zh",
|
|
109
|
-
version: () => {
|
|
138
|
+
version: () => `misaki-zh misaki=${MISAKI_PIN} jieba=${JIEBA_PIN} map=${PHONEME_MAP_VERSION}`,
|
|
139
|
+
phonemize: (text) => {
|
|
110
140
|
const r = spawnSync(python, [
|
|
111
141
|
"-c",
|
|
112
142
|
ZH_G2P_PY,
|
|
113
|
-
"--
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (r.status === MISAKI_ABSENT_EXIT) throw installHint(python, "misaki");
|
|
118
|
-
if (r.status !== 0) throw new NarrationError(`misaki[zh] g2p not available via '${python}' (exit ${String(r.status)}): ${stderrTail(r.stderr)} — \`pip install 'misaki[zh]==${MISAKI_PIN}' 'jieba==${JIEBA_PIN}'\` into that interpreter.`);
|
|
119
|
-
return `misaki-zh ${(r.stdout ?? "").trim()} map=${PHONEME_MAP_VERSION}`;
|
|
120
|
-
},
|
|
121
|
-
phonemize: (text) => {
|
|
122
|
-
const r = spawnSync(python, ["-c", ZH_G2P_PY], {
|
|
143
|
+
"--pins",
|
|
144
|
+
MISAKI_PIN,
|
|
145
|
+
JIEBA_PIN
|
|
146
|
+
], {
|
|
123
147
|
input: text,
|
|
124
148
|
maxBuffer: 8 * 1024 * 1024
|
|
125
149
|
});
|
|
126
150
|
const fail = spawnFailure(r, python);
|
|
127
151
|
if (fail) throw fail;
|
|
128
152
|
if (r.status === MISAKI_ABSENT_EXIT) throw installHint(python, "misaki");
|
|
153
|
+
if (r.status === MISAKI_PIN_MISMATCH_EXIT) throw pinMismatchError(python, r.stderr);
|
|
129
154
|
if (r.status !== 0) throw new NarrationError(`misaki[zh] g2p failed: ${stderrTail(r.stderr)}`);
|
|
130
155
|
const out = r.stdout?.toString("utf8") ?? "";
|
|
131
156
|
if (out.length === 0) throw new NarrationError(`misaki[zh] g2p produced no phonemes for text: ${JSON.stringify(text.slice(0, 60))}`);
|
|
@@ -440,7 +465,6 @@ function kokoroProvider(opts = {}) {
|
|
|
440
465
|
const modelId = opts.model ?? KOKORO_MODEL;
|
|
441
466
|
const dtype = opts.dtype ?? "q8";
|
|
442
467
|
const zhG2p = opts.zhG2p ?? misakiZhG2p();
|
|
443
|
-
const usesChinese = isKokoroChineseVoice(opts.voice ?? KOKORO_DEFAULT_VOICE);
|
|
444
468
|
let loaded = null;
|
|
445
469
|
const loadLib = async () => {
|
|
446
470
|
const { entryUrl } = resolveKokoro();
|
|
@@ -463,8 +487,7 @@ function kokoroProvider(opts = {}) {
|
|
|
463
487
|
id: "kokoro",
|
|
464
488
|
version: () => {
|
|
465
489
|
const { version } = resolveKokoro();
|
|
466
|
-
|
|
467
|
-
if (usesChinese) v += ` g2p=[${zhG2p.version()}]`;
|
|
490
|
+
const v = `kokoro-js ${version} ${basename(modelId)} dtype=${dtype} g2p=[${zhG2p.version()}]`;
|
|
468
491
|
return Promise.resolve(v);
|
|
469
492
|
},
|
|
470
493
|
synthesize: async (req) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/narrate",
|
|
3
|
-
"version": "0.15.0
|
|
3
|
+
"version": "0.15.0",
|
|
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.15.0
|
|
26
|
-
"@glissade/scene": "0.15.0
|
|
25
|
+
"@glissade/core": "0.15.0",
|
|
26
|
+
"@glissade/scene": "0.15.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"kokoro-js": "^1.2.0"
|