@reelscript/cli 0.1.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/LICENSE +21 -0
- package/README.md +179 -0
- package/assets/fonts/InterVariable.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Variable.ttf +0 -0
- package/assets/fonts/LICENSE-Inter.txt +92 -0
- package/assets/fonts/LICENSE-JetBrainsMono.txt +93 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +114 -0
- package/dist/clock.d.ts +11 -0
- package/dist/clock.js +88 -0
- package/dist/cursor.d.ts +14 -0
- package/dist/cursor.js +55 -0
- package/dist/easing.d.ts +6 -0
- package/dist/easing.js +25 -0
- package/dist/encoder.d.ts +38 -0
- package/dist/encoder.js +122 -0
- package/dist/index.d.ts +187 -0
- package/dist/index.js +197 -0
- package/dist/renderer.d.ts +48 -0
- package/dist/renderer.js +687 -0
- package/dist/resolve-self.d.ts +12 -0
- package/dist/resolve-self.js +12 -0
- package/dist/terminal.d.ts +38 -0
- package/dist/terminal.js +164 -0
- package/dist/theme.d.ts +51 -0
- package/dist/theme.js +181 -0
- package/dist/timeline.d.ts +103 -0
- package/dist/timeline.js +16 -0
- package/dist/tts.d.ts +34 -0
- package/dist/tts.js +154 -0
- package/package.json +72 -0
package/dist/tts.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narration: text-to-speech engines and a cache of synthesized clips.
|
|
3
|
+
*
|
|
4
|
+
* The default engine is Kokoro-82M through the optional `kokoro-js`
|
|
5
|
+
* dependency: open weights, runs on CPU faster than real time, no account.
|
|
6
|
+
* Any object implementing TtsEngine can be passed instead (ElevenLabs,
|
|
7
|
+
* OpenAI, a cloud service).
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
export const DEFAULT_VOICE = "af_heart";
|
|
14
|
+
export const KOKORO_MODEL = "onnx-community/Kokoro-82M-v1.0-ONNX";
|
|
15
|
+
/** Root of reelscript's on-disk cache (models, synthesized clips). */
|
|
16
|
+
export function cacheDir() {
|
|
17
|
+
return process.env.REELSCRIPT_CACHE ?? join(homedir(), ".cache", "reelscript");
|
|
18
|
+
}
|
|
19
|
+
/** Split narration into sentences; TTS models prefer short inputs. */
|
|
20
|
+
export function splitSentences(text) {
|
|
21
|
+
return text
|
|
22
|
+
.replace(/\s+/g, " ")
|
|
23
|
+
.trim()
|
|
24
|
+
.split(/(?<=[.!?])\s+/)
|
|
25
|
+
.map((s) => s.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Point transformers.js (kokoro-js's model loader) at reelscript's cache dir
|
|
30
|
+
* instead of its default inside node_modules, so the model survives
|
|
31
|
+
* reinstalls and can be cached in CI.
|
|
32
|
+
*
|
|
33
|
+
* Node keeps separate module instances for the ESM and CJS builds, each with
|
|
34
|
+
* its own `env`, so this must configure the exact file kokoro-js imports: the
|
|
35
|
+
* ESM build resolved from kokoro-js's own location (nested or hoisted).
|
|
36
|
+
*/
|
|
37
|
+
async function redirectModelCache() {
|
|
38
|
+
const { fileURLToPath, pathToFileURL } = await import("node:url");
|
|
39
|
+
const { dirname, join: pjoin } = await import("node:path");
|
|
40
|
+
let url = "@huggingface/transformers";
|
|
41
|
+
try {
|
|
42
|
+
let dir = dirname(fileURLToPath(import.meta.resolve("kokoro-js")));
|
|
43
|
+
for (;;) {
|
|
44
|
+
const candidate = pjoin(dir, "node_modules", "@huggingface", "transformers", "dist", "transformers.node.mjs");
|
|
45
|
+
if (existsSync(candidate)) {
|
|
46
|
+
url = pathToFileURL(candidate).href;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const parent = dirname(dir);
|
|
50
|
+
if (parent === dir)
|
|
51
|
+
break;
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* fall back to bare specifier */
|
|
57
|
+
}
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
const mod = await import(url);
|
|
60
|
+
if (!mod.env)
|
|
61
|
+
throw new Error("reelscript: could not configure the transformers.js model cache");
|
|
62
|
+
mod.env.cacheDir = join(cacheDir(), "models") + "/";
|
|
63
|
+
}
|
|
64
|
+
/** Kokoro-82M via kokoro-js, loaded lazily on first use. */
|
|
65
|
+
export function kokoro(model = KOKORO_MODEL) {
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
+
let loading = null;
|
|
68
|
+
const load = () => (loading ??= (async () => {
|
|
69
|
+
const spec = "kokoro-js";
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
let mod;
|
|
72
|
+
try {
|
|
73
|
+
mod = await import(spec);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new Error("reelscript: narration needs the optional dependency kokoro-js.\n" +
|
|
77
|
+
" npm install kokoro-js\n" +
|
|
78
|
+
"or pass your own engine via createDemo({ tts })");
|
|
79
|
+
}
|
|
80
|
+
await redirectModelCache();
|
|
81
|
+
return mod.KokoroTTS.from_pretrained(model, { dtype: "q8", device: "cpu" });
|
|
82
|
+
})());
|
|
83
|
+
return {
|
|
84
|
+
id: `kokoro:${model}:q8`,
|
|
85
|
+
async synthesize(text, { voice = DEFAULT_VOICE, speed = 1 } = {}) {
|
|
86
|
+
const tts = await load();
|
|
87
|
+
const parts = [];
|
|
88
|
+
let sampleRate = 24000;
|
|
89
|
+
for (const sentence of splitSentences(text)) {
|
|
90
|
+
const out = await tts.generate(sentence, { voice, speed });
|
|
91
|
+
parts.push(out.audio);
|
|
92
|
+
sampleRate = out.sampling_rate;
|
|
93
|
+
}
|
|
94
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
95
|
+
const audio = new Float32Array(total);
|
|
96
|
+
let offset = 0;
|
|
97
|
+
for (const p of parts) {
|
|
98
|
+
audio.set(p, offset);
|
|
99
|
+
offset += p.length;
|
|
100
|
+
}
|
|
101
|
+
return { audio, sampleRate };
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Encode mono float samples as a 16-bit PCM WAV file. */
|
|
106
|
+
export function toWav({ audio, sampleRate }) {
|
|
107
|
+
const n = audio.length;
|
|
108
|
+
const buf = Buffer.alloc(44 + n * 2);
|
|
109
|
+
buf.write("RIFF", 0);
|
|
110
|
+
buf.writeUInt32LE(36 + n * 2, 4);
|
|
111
|
+
buf.write("WAVE", 8);
|
|
112
|
+
buf.write("fmt ", 12);
|
|
113
|
+
buf.writeUInt32LE(16, 16); // chunk size
|
|
114
|
+
buf.writeUInt16LE(1, 20); // PCM
|
|
115
|
+
buf.writeUInt16LE(1, 22); // mono
|
|
116
|
+
buf.writeUInt32LE(sampleRate, 24);
|
|
117
|
+
buf.writeUInt32LE(sampleRate * 2, 28); // byte rate
|
|
118
|
+
buf.writeUInt16LE(2, 32); // block align
|
|
119
|
+
buf.writeUInt16LE(16, 34); // bits per sample
|
|
120
|
+
buf.write("data", 36);
|
|
121
|
+
buf.writeUInt32LE(n * 2, 40);
|
|
122
|
+
for (let i = 0; i < n; i++) {
|
|
123
|
+
const s = Math.max(-1, Math.min(1, audio[i]));
|
|
124
|
+
buf.writeInt16LE(Math.round(s < 0 ? s * 0x8000 : s * 0x7fff), 44 + i * 2);
|
|
125
|
+
}
|
|
126
|
+
return buf;
|
|
127
|
+
}
|
|
128
|
+
/** Synthesize (or fetch from cache) one narration clip. */
|
|
129
|
+
export async function synthesizeClip(engine, text, opts) {
|
|
130
|
+
const voice = opts.voice ?? DEFAULT_VOICE;
|
|
131
|
+
const speed = opts.speed ?? 1;
|
|
132
|
+
const key = createHash("sha1").update([engine.id, voice, String(speed), text].join("\0")).digest("hex");
|
|
133
|
+
const dir = join(cacheDir(), "tts");
|
|
134
|
+
mkdirSync(dir, { recursive: true });
|
|
135
|
+
const file = join(dir, `${key}.wav`);
|
|
136
|
+
const meta = `${file}.json`;
|
|
137
|
+
if (existsSync(file) && existsSync(meta)) {
|
|
138
|
+
return { file, seconds: JSON.parse(readFileSync(meta, "utf8")).seconds };
|
|
139
|
+
}
|
|
140
|
+
const audio = await engine.synthesize(text, { voice, speed });
|
|
141
|
+
const seconds = audio.audio.length / audio.sampleRate;
|
|
142
|
+
writeFileSync(file, toWav(audio));
|
|
143
|
+
writeFileSync(meta, JSON.stringify({ seconds, text, voice, speed, engine: engine.id }));
|
|
144
|
+
return { file, seconds };
|
|
145
|
+
}
|
|
146
|
+
/** Replace words the TTS mispronounces, e.g. { Reelscript: "Reel script" }. */
|
|
147
|
+
export function applyPronunciations(text, map) {
|
|
148
|
+
if (!map)
|
|
149
|
+
return text;
|
|
150
|
+
let out = text;
|
|
151
|
+
for (const [word, spoken] of Object.entries(map))
|
|
152
|
+
out = out.split(word).join(spoken);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reelscript/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Product demos as code. Write a script, render a demo, re-run it in CI when your UI changes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Trevin Lee",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"demo",
|
|
10
|
+
"screencast",
|
|
11
|
+
"screen-recording",
|
|
12
|
+
"video",
|
|
13
|
+
"automation",
|
|
14
|
+
"playwright",
|
|
15
|
+
"demo-as-code"
|
|
16
|
+
],
|
|
17
|
+
"bin": {
|
|
18
|
+
"reelscript": "./dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"assets"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"dev": "tsc --watch",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"example": "npm run build && tsx examples/basic.ts",
|
|
37
|
+
"preview": "tsx src/cli.ts preview examples/basic.ts",
|
|
38
|
+
"test": "node --import tsx --test test/*.test.ts",
|
|
39
|
+
"prepublishOnly": "npm run build && npm test",
|
|
40
|
+
"example:terminal": "npm run build && tsx examples/terminal.ts",
|
|
41
|
+
"example:desktop": "npm run build && tsx examples/desktop.ts"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
48
|
+
"@xterm/xterm": "^6.0.0",
|
|
49
|
+
"ffmpeg-static": "^5.3.0",
|
|
50
|
+
"playwright": "^1.63.0",
|
|
51
|
+
"sharp": "^0.34.5",
|
|
52
|
+
"tsx": "^4.23.15"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^26.6.2",
|
|
56
|
+
"typescript": "^7.0.2"
|
|
57
|
+
},
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/trevin-lee/reelscript.git"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/trevin-lee/reelscript#readme",
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/trevin-lee/reelscript/issues"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
},
|
|
69
|
+
"optionalDependencies": {
|
|
70
|
+
"kokoro-js": "^1.2.1"
|
|
71
|
+
}
|
|
72
|
+
}
|