tutorial-forge 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 +19 -0
- package/dist/browser/callout.d.ts +7 -0
- package/dist/browser/callout.js +33 -0
- package/dist/browser/callout.js.map +1 -0
- package/dist/browser/cursor.d.ts +9 -0
- package/dist/browser/cursor.js +61 -0
- package/dist/browser/cursor.js.map +1 -0
- package/dist/browser/instrument.d.ts +16 -0
- package/dist/browser/instrument.js +114 -0
- package/dist/browser/instrument.js.map +1 -0
- package/dist/browser/timing.d.ts +20 -0
- package/dist/browser/timing.js +26 -0
- package/dist/browser/timing.js.map +1 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/post.d.ts +22 -0
- package/dist/pipeline/post.js +63 -0
- package/dist/pipeline/post.js.map +1 -0
- package/dist/pipeline/record.d.ts +23 -0
- package/dist/pipeline/record.js +176 -0
- package/dist/pipeline/record.js.map +1 -0
- package/dist/pipeline/render.d.ts +12 -0
- package/dist/pipeline/render.js +91 -0
- package/dist/pipeline/render.js.map +1 -0
- package/dist/pipeline/tts.d.ts +23 -0
- package/dist/pipeline/tts.js +42 -0
- package/dist/pipeline/tts.js.map +1 -0
- package/dist/post/ffmpeg.d.ts +53 -0
- package/dist/post/ffmpeg.js +162 -0
- package/dist/post/ffmpeg.js.map +1 -0
- package/dist/post/subtitles.d.ts +14 -0
- package/dist/post/subtitles.js +47 -0
- package/dist/post/subtitles.js.map +1 -0
- package/dist/spec.d.ts +7 -0
- package/dist/spec.js +51 -0
- package/dist/spec.js.map +1 -0
- package/dist/tts/cache.d.ts +10 -0
- package/dist/tts/cache.js +43 -0
- package/dist/tts/cache.js.map +1 -0
- package/dist/tts/elevenlabs.d.ts +11 -0
- package/dist/tts/elevenlabs.js +37 -0
- package/dist/tts/elevenlabs.js.map +1 -0
- package/dist/tts/openai.d.ts +9 -0
- package/dist/tts/openai.js +29 -0
- package/dist/tts/openai.js.map +1 -0
- package/dist/tts/piper.d.ts +10 -0
- package/dist/tts/piper.js +20 -0
- package/dist/tts/piper.js.map +1 -0
- package/dist/tts/provider.d.ts +6 -0
- package/dist/tts/provider.js +9 -0
- package/dist/tts/provider.js.map +1 -0
- package/dist/tts/silent.d.ts +8 -0
- package/dist/tts/silent.js +37 -0
- package/dist/tts/silent.js.map +1 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/dist/util/fs.d.ts +5 -0
- package/dist/util/fs.js +30 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/hash.d.ts +1 -0
- package/dist/util/hash.js +7 -0
- package/dist/util/hash.js.map +1 -0
- package/dist/util/logger.d.ts +6 -0
- package/dist/util/logger.js +21 -0
- package/dist/util/logger.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One SRT cue per narrated step: text = narration, range = narration playback
|
|
3
|
+
* window. Cue times are relative to the trimmed video, so trimStartMs (manifest
|
|
4
|
+
* clock) is subtracted.
|
|
5
|
+
*/
|
|
6
|
+
export function generateSrt(manifest, opts) {
|
|
7
|
+
const max = opts.maxLineChars ?? 42;
|
|
8
|
+
const cues = [];
|
|
9
|
+
let n = 0;
|
|
10
|
+
for (const step of manifest.steps) {
|
|
11
|
+
if (!step.narration.trim() || step.audioDurationMs <= 0)
|
|
12
|
+
continue;
|
|
13
|
+
n += 1;
|
|
14
|
+
const start = step.startMs + opts.leadInMs - opts.trimStartMs;
|
|
15
|
+
const end = start + step.audioDurationMs;
|
|
16
|
+
cues.push(`${n}\n${srtTime(start)} --> ${srtTime(end)}\n${wrapText(step.narration, max)}\n`);
|
|
17
|
+
}
|
|
18
|
+
return cues.join('\n');
|
|
19
|
+
}
|
|
20
|
+
export function srtTime(ms) {
|
|
21
|
+
const clamped = Math.max(0, Math.round(ms));
|
|
22
|
+
const h = Math.floor(clamped / 3_600_000);
|
|
23
|
+
const m = Math.floor((clamped % 3_600_000) / 60_000);
|
|
24
|
+
const s = Math.floor((clamped % 60_000) / 1000);
|
|
25
|
+
const f = clamped % 1000;
|
|
26
|
+
const pad = (v, len = 2) => String(v).padStart(len, '0');
|
|
27
|
+
return `${pad(h)}:${pad(m)}:${pad(s)},${pad(f, 3)}`;
|
|
28
|
+
}
|
|
29
|
+
/** Greedy word wrap at ~max chars/line. */
|
|
30
|
+
export function wrapText(text, max) {
|
|
31
|
+
const words = text.trim().split(/\s+/);
|
|
32
|
+
const lines = [];
|
|
33
|
+
let line = '';
|
|
34
|
+
for (const word of words) {
|
|
35
|
+
if (line && line.length + 1 + word.length > max) {
|
|
36
|
+
lines.push(line);
|
|
37
|
+
line = word;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
line = line ? `${line} ${word}` : word;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (line)
|
|
44
|
+
lines.push(line);
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=subtitles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subtitles.js","sourceRoot":"","sources":["../../src/post/subtitles.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,QAAwB,EACxB,IAAsE;IAEtE,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC;IACpC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,eAAe,IAAI,CAAC;YAAE,SAAS;QAClE,CAAC,IAAI,CAAC,CAAC;QACP,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC;QAC9D,MAAM,GAAG,GAAG,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC;QACzC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,KAAK,CAAC,QAAQ,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/F,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,EAAU;IAChC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5C,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC1C,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC;IACrD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IAChD,MAAM,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;IACzB,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,GAAG,GAAG,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACjE,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AACtD,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,GAAW;IAChD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACvC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YAChD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,IAAI,GAAG,IAAI,CAAC;QACd,CAAC;aAAM,CAAC;YACN,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACzC,CAAC;IACH,CAAC;IACD,IAAI,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
package/dist/spec.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Step, Tutorial } from './types.js';
|
|
2
|
+
export declare function step(narration: string, run: Step['run'], opts?: Partial<Step>): Step;
|
|
3
|
+
export declare function tutorial(idOrTitle: string, steps: Step[], meta?: Partial<Tutorial>): Tutorial;
|
|
4
|
+
export declare function validateTutorial(t: Tutorial): void;
|
|
5
|
+
/** Resolve a step's stable id: explicit id, or its zero-padded index. */
|
|
6
|
+
export declare function stepId(s: Step, index: number): string;
|
|
7
|
+
export declare function slugify(s: string): string;
|
package/dist/spec.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { logger } from './util/logger.js';
|
|
2
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
3
|
+
// SSML tags or ASCII control chars in narration usually mean copy-paste from
|
|
4
|
+
// another TTS system; providers read them literally, so warn at load time.
|
|
5
|
+
const SUSPICIOUS_NARRATION_RE = /<[a-z][^>]*>|[\x00-\x08\x0b\x0c\x0e-\x1f]/i;
|
|
6
|
+
export function step(narration, run, opts) {
|
|
7
|
+
return { narration, run, ...opts };
|
|
8
|
+
}
|
|
9
|
+
export function tutorial(idOrTitle, steps, meta) {
|
|
10
|
+
const id = meta?.id ?? slugify(idOrTitle);
|
|
11
|
+
const title = meta?.title ?? idOrTitle;
|
|
12
|
+
const t = { ...meta, id, title, steps };
|
|
13
|
+
validateTutorial(t);
|
|
14
|
+
return t;
|
|
15
|
+
}
|
|
16
|
+
export function validateTutorial(t) {
|
|
17
|
+
if (!t.id || !SLUG_RE.test(t.id)) {
|
|
18
|
+
throw new Error(`Tutorial id "${t.id}" must be a lowercase slug (a-z, 0-9, hyphens)`);
|
|
19
|
+
}
|
|
20
|
+
if (!Array.isArray(t.steps) || t.steps.length === 0) {
|
|
21
|
+
throw new Error(`Tutorial "${t.id}" has no steps`);
|
|
22
|
+
}
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
t.steps.forEach((s, i) => {
|
|
25
|
+
if (typeof s.narration !== 'string') {
|
|
26
|
+
throw new Error(`Tutorial "${t.id}" step ${i}: narration must be a string`);
|
|
27
|
+
}
|
|
28
|
+
if (typeof s.run !== 'function') {
|
|
29
|
+
throw new Error(`Tutorial "${t.id}" step ${i}: run must be a function`);
|
|
30
|
+
}
|
|
31
|
+
const id = stepId(s, i);
|
|
32
|
+
if (seen.has(id)) {
|
|
33
|
+
throw new Error(`Tutorial "${t.id}" step ${i}: duplicate step id "${id}"`);
|
|
34
|
+
}
|
|
35
|
+
seen.add(id);
|
|
36
|
+
if (SUSPICIOUS_NARRATION_RE.test(s.narration)) {
|
|
37
|
+
logger.warn(`Tutorial "${t.id}" step ${i} ("${id}"): narration contains markup or control characters; TTS providers will read these literally`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Resolve a step's stable id: explicit id, or its zero-padded index. */
|
|
42
|
+
export function stepId(s, index) {
|
|
43
|
+
return s.id ?? `step-${String(index + 1).padStart(2, '0')}`;
|
|
44
|
+
}
|
|
45
|
+
export function slugify(s) {
|
|
46
|
+
return s
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
49
|
+
.replace(/^-+|-+$/g, '');
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=spec.js.map
|
package/dist/spec.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../src/spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TTSProvider } from '../types.js';
|
|
2
|
+
export declare function defaultCacheDir(): string;
|
|
3
|
+
export declare function cacheKeyFor(provider: TTSProvider, text: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Synthesize one narration line through the content-hash cache.
|
|
6
|
+
* Cached entries are already normalized to 48kHz mono WAV; on miss we
|
|
7
|
+
* synthesize to a temp file, normalize, and move into the cache atomically.
|
|
8
|
+
* Returns the path of a WAV copy at outPath.
|
|
9
|
+
*/
|
|
10
|
+
export declare function synthesizeCached(provider: TTSProvider, text: string, outPath: string, cacheDir?: string): Promise<void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { copyFile, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import { sha256 } from '../util/hash.js';
|
|
5
|
+
import { ensureDir, exists } from '../util/fs.js';
|
|
6
|
+
import { normalizeToWav } from '../post/ffmpeg.js';
|
|
7
|
+
import { logger } from '../util/logger.js';
|
|
8
|
+
export function defaultCacheDir() {
|
|
9
|
+
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), '.cache'), 'tutorial-forge', 'tts');
|
|
10
|
+
}
|
|
11
|
+
export function cacheKeyFor(provider, text) {
|
|
12
|
+
return sha256(provider.cacheKey, text);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Synthesize one narration line through the content-hash cache.
|
|
16
|
+
* Cached entries are already normalized to 48kHz mono WAV; on miss we
|
|
17
|
+
* synthesize to a temp file, normalize, and move into the cache atomically.
|
|
18
|
+
* Returns the path of a WAV copy at outPath.
|
|
19
|
+
*/
|
|
20
|
+
export async function synthesizeCached(provider, text, outPath, cacheDir = defaultCacheDir()) {
|
|
21
|
+
const cached = join(cacheDir, `${cacheKeyFor(provider, text)}.wav`);
|
|
22
|
+
if (!(await exists(cached))) {
|
|
23
|
+
await ensureDir(cacheDir);
|
|
24
|
+
const raw = cached + '.raw.tmp';
|
|
25
|
+
const normalized = cached + '.tmp';
|
|
26
|
+
try {
|
|
27
|
+
await provider.synthesize(text, raw);
|
|
28
|
+
await normalizeToWav(raw, normalized);
|
|
29
|
+
await rename(normalized, cached);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await rm(raw, { force: true });
|
|
33
|
+
await rm(normalized, { force: true });
|
|
34
|
+
}
|
|
35
|
+
logger.debug(`tts cache miss: "${text.slice(0, 40)}…"`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
logger.debug(`tts cache hit: "${text.slice(0, 40)}…"`);
|
|
39
|
+
}
|
|
40
|
+
await ensureDir(dirname(outPath));
|
|
41
|
+
await copyFile(cached, outPath);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/tts/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,MAAM,UAAU,eAAe;IAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,EAAE,gBAAgB,EAAE,KAAK,CAAC,CAAC;AAChG,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,QAAqB,EAAE,IAAY;IAC7D,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,QAAqB,EACrB,IAAY,EACZ,OAAe,EACf,QAAQ,GAAG,eAAe,EAAE;IAE5B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACpE,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAC5B,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC1B,MAAM,GAAG,GAAG,MAAM,GAAG,UAAU,CAAC;QAChC,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;QACnC,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YACrC,MAAM,cAAc,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;YACtC,MAAM,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACnC,CAAC;gBAAS,CAAC;YACT,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/B,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,oBAAoB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;IAC1D,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,KAAK,CAAC,mBAAmB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAClC,MAAM,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TTSProvider } from '../types.js';
|
|
2
|
+
export interface ElevenLabsOptions {
|
|
3
|
+
voiceId: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
modelId?: string;
|
|
6
|
+
/** 0..1, provider default 0.5 */
|
|
7
|
+
stability?: number;
|
|
8
|
+
/** 0..1, provider default 0.75 */
|
|
9
|
+
similarityBoost?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function ElevenLabs(opts: ElevenLabsOptions): TTSProvider;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
export function ElevenLabs(opts) {
|
|
3
|
+
const apiKey = opts.apiKey ?? process.env.ELEVENLABS_API_KEY;
|
|
4
|
+
const modelId = opts.modelId ?? 'eleven_turbo_v2_5';
|
|
5
|
+
return {
|
|
6
|
+
cacheKey: `elevenlabs:${opts.voiceId}:${modelId}`,
|
|
7
|
+
async synthesize(text, outPath) {
|
|
8
|
+
if (!apiKey)
|
|
9
|
+
throw new Error('ElevenLabs: missing apiKey (set ELEVENLABS_API_KEY)');
|
|
10
|
+
const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(opts.voiceId)}`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'xi-api-key': apiKey, 'content-type': 'application/json' },
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
text,
|
|
15
|
+
model_id: modelId,
|
|
16
|
+
voice_settings: {
|
|
17
|
+
stability: opts.stability ?? 0.5,
|
|
18
|
+
similarity_boost: opts.similarityBoost ?? 0.75,
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
throw new Error(`ElevenLabs: HTTP ${res.status} ${await safeBody(res)}`);
|
|
24
|
+
}
|
|
25
|
+
await writeFile(outPath, Buffer.from(await res.arrayBuffer()));
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function safeBody(res) {
|
|
30
|
+
try {
|
|
31
|
+
return (await res.text()).slice(0, 300);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=elevenlabs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elevenlabs.js","sourceRoot":"","sources":["../../src/tts/elevenlabs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAa7C,MAAM,UAAU,UAAU,CAAC,IAAuB;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,mBAAmB,CAAC;IACpD,OAAO;QACL,QAAQ,EAAE,cAAc,IAAI,CAAC,OAAO,IAAI,OAAO,EAAE;QACjD,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAe;YAC5C,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;YACpF,MAAM,GAAG,GAAG,MAAM,KAAK,CACrB,+CAA+C,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EACjF;gBACE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBACrE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,IAAI;oBACJ,QAAQ,EAAE,OAAO;oBACjB,cAAc,EAAE;wBACd,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,GAAG;wBAChC,gBAAgB,EAAE,IAAI,CAAC,eAAe,IAAI,IAAI;qBAC/C;iBACF,CAAC;aACH,CACF,CAAC;YACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,GAAG,CAAC,MAAM,IAAI,MAAM,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC3E,CAAC;YACD,MAAM,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACjE,CAAC;KACF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,GAAa;IACnC,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TTSProvider } from '../types.js';
|
|
2
|
+
export interface OpenAITTSOptions {
|
|
3
|
+
voice?: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
/** 0.25..4.0, default 1.0 */
|
|
7
|
+
speed?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function OpenAITTS(opts?: OpenAITTSOptions): TTSProvider;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
export function OpenAITTS(opts = {}) {
|
|
3
|
+
const apiKey = opts.apiKey ?? process.env.OPENAI_API_KEY;
|
|
4
|
+
const model = opts.model ?? 'gpt-4o-mini-tts';
|
|
5
|
+
const voice = opts.voice ?? 'alloy';
|
|
6
|
+
return {
|
|
7
|
+
cacheKey: `openai:${voice}:${model}:${opts.speed ?? 1}`,
|
|
8
|
+
async synthesize(text, outPath) {
|
|
9
|
+
if (!apiKey)
|
|
10
|
+
throw new Error('OpenAI TTS: missing apiKey (set OPENAI_API_KEY)');
|
|
11
|
+
const res = await fetch('https://api.openai.com/v1/audio/speech', {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
model,
|
|
16
|
+
voice,
|
|
17
|
+
input: text,
|
|
18
|
+
speed: opts.speed ?? 1,
|
|
19
|
+
response_format: 'wav',
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
throw new Error(`OpenAI TTS: HTTP ${res.status} ${(await res.text()).slice(0, 300)}`);
|
|
24
|
+
}
|
|
25
|
+
await writeFile(outPath, Buffer.from(await res.arrayBuffer()));
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/tts/openai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAW7C,MAAM,UAAU,SAAS,CAAC,OAAyB,EAAE;IACnD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACzD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,iBAAiB,CAAC;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC;IACpC,OAAO;QACL,QAAQ,EAAE,UAAU,KAAK,IAAI,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE;QACvD,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAe;YAC5C,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;YAChF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,wCAAwC,EAAE;gBAChE,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAClF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,KAAK;oBACL,KAAK;oBACL,KAAK,EAAE,IAAI;oBACX,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;oBACtB,eAAe,EAAE,KAAK;iBACvB,CAAC;aACH,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YACxF,CAAC;YACD,MAAM,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACjE,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TTSProvider } from '../types.js';
|
|
2
|
+
export interface PiperOptions {
|
|
3
|
+
/** Path to a piper voice .onnx model. */
|
|
4
|
+
model: string;
|
|
5
|
+
/** Piper binary, default "piper" on PATH. */
|
|
6
|
+
binary?: string;
|
|
7
|
+
speakerId?: number;
|
|
8
|
+
}
|
|
9
|
+
/** Local/offline TTS via the piper CLI (https://github.com/rhasspy/piper). */
|
|
10
|
+
export declare function Piper(opts: PiperOptions): TTSProvider;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
/** Local/offline TTS via the piper CLI (https://github.com/rhasspy/piper). */
|
|
3
|
+
export function Piper(opts) {
|
|
4
|
+
const binary = opts.binary ?? 'piper';
|
|
5
|
+
return {
|
|
6
|
+
cacheKey: `piper:${opts.model}:${opts.speakerId ?? 0}`,
|
|
7
|
+
async synthesize(text, outPath) {
|
|
8
|
+
const args = ['--model', opts.model, '--output_file', outPath];
|
|
9
|
+
if (opts.speakerId !== undefined)
|
|
10
|
+
args.push('--speaker', String(opts.speakerId));
|
|
11
|
+
try {
|
|
12
|
+
await execa(binary, args, { input: text });
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
throw new Error(`Piper synthesis failed (is "${binary}" installed and the model path valid?): ${err.shortMessage ?? err}`);
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=piper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piper.js","sourceRoot":"","sources":["../../src/tts/piper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAW9B,8EAA8E;AAC9E,MAAM,UAAU,KAAK,CAAC,IAAkB;IACtC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC;IACtC,OAAO;QACL,QAAQ,EAAE,SAAS,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,EAAE;QACtD,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAe;YAC5C,MAAM,IAAI,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC;YAC/D,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;gBAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YACjF,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CACb,+BAA+B,MAAM,2CAClC,GAAiC,CAAC,YAAY,IAAI,GACrD,EAAE,CACH,CAAC;YACJ,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { TTSProvider } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fallback duration estimator: average speech runs ~160 wpm; 380ms/word with a
|
|
4
|
+
* 1.2s floor. Also the deterministic duration used by SilentProvider.
|
|
5
|
+
*/
|
|
6
|
+
export declare function estimateDurationMs(text: string): number;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback duration estimator: average speech runs ~160 wpm; 380ms/word with a
|
|
3
|
+
* 1.2s floor. Also the deterministic duration used by SilentProvider.
|
|
4
|
+
*/
|
|
5
|
+
export function estimateDurationMs(text) {
|
|
6
|
+
const words = text.trim().split(/\s+/).filter(Boolean).length;
|
|
7
|
+
return Math.max(1200, words * 380);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/tts/provider.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IAC9D,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,GAAG,CAAC,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TTSProvider } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generates silence whose duration follows the word-count heuristic.
|
|
4
|
+
* Deterministic, offline, no ffmpeg needed — for tests and CI.
|
|
5
|
+
*/
|
|
6
|
+
export declare function SilentProvider(): TTSProvider;
|
|
7
|
+
/** Minimal 16-bit mono PCM WAV of the given duration. */
|
|
8
|
+
export declare function silentWav(durationMs: number, sampleRate?: number): Buffer;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { estimateDurationMs } from './provider.js';
|
|
3
|
+
const SAMPLE_RATE = 48000;
|
|
4
|
+
/**
|
|
5
|
+
* Generates silence whose duration follows the word-count heuristic.
|
|
6
|
+
* Deterministic, offline, no ffmpeg needed — for tests and CI.
|
|
7
|
+
*/
|
|
8
|
+
export function SilentProvider() {
|
|
9
|
+
return {
|
|
10
|
+
cacheKey: 'silent:v1',
|
|
11
|
+
async synthesize(text, outPath) {
|
|
12
|
+
const ms = estimateDurationMs(text);
|
|
13
|
+
await writeFile(outPath, silentWav(ms));
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Minimal 16-bit mono PCM WAV of the given duration. */
|
|
18
|
+
export function silentWav(durationMs, sampleRate = SAMPLE_RATE) {
|
|
19
|
+
const samples = Math.round((durationMs / 1000) * sampleRate);
|
|
20
|
+
const dataSize = samples * 2;
|
|
21
|
+
const buf = Buffer.alloc(44 + dataSize);
|
|
22
|
+
buf.write('RIFF', 0);
|
|
23
|
+
buf.writeUInt32LE(36 + dataSize, 4);
|
|
24
|
+
buf.write('WAVE', 8);
|
|
25
|
+
buf.write('fmt ', 12);
|
|
26
|
+
buf.writeUInt32LE(16, 16); // fmt chunk size
|
|
27
|
+
buf.writeUInt16LE(1, 20); // PCM
|
|
28
|
+
buf.writeUInt16LE(1, 22); // mono
|
|
29
|
+
buf.writeUInt32LE(sampleRate, 24);
|
|
30
|
+
buf.writeUInt32LE(sampleRate * 2, 28); // byte rate
|
|
31
|
+
buf.writeUInt16LE(2, 32); // block align
|
|
32
|
+
buf.writeUInt16LE(16, 34); // bits per sample
|
|
33
|
+
buf.write('data', 36);
|
|
34
|
+
buf.writeUInt32LE(dataSize, 40);
|
|
35
|
+
return buf;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=silent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"silent.js","sourceRoot":"","sources":["../../src/tts/silent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,WAAW,GAAG,KAAK,CAAC;AAE1B;;;GAGG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO;QACL,QAAQ,EAAE,WAAW;QACrB,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAe;YAC5C,MAAM,EAAE,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACpC,MAAM,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1C,CAAC;KACF,CAAC;AACJ,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,SAAS,CAAC,UAAkB,EAAE,UAAU,GAAG,WAAW;IACpE,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,OAAO,GAAG,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,QAAQ,CAAC,CAAC;IACxC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACrB,GAAG,CAAC,aAAa,CAAC,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC;IACpC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACrB,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACtB,GAAG,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,iBAAiB;IAC5C,GAAG,CAAC,aAAa,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM;IAChC,GAAG,CAAC,aAAa,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO;IACjC,GAAG,CAAC,aAAa,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAClC,GAAG,CAAC,aAAa,CAAC,UAAU,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY;IACnD,GAAG,CAAC,aAAa,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc;IACxC,GAAG,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,kBAAkB;IAC7C,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACtB,GAAG,CAAC,aAAa,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChC,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
/** Gets the target app into a known, recordable state. The only app-specific code. */
|
|
3
|
+
export interface TutorialAdapter {
|
|
4
|
+
/** Base URL of the running app, e.g. http://localhost:3000 */
|
|
5
|
+
baseURL: string;
|
|
6
|
+
/** Auth, seeding, navigation to a starting screen. Runs after page creation, before step 1. Excluded from the final video by default. */
|
|
7
|
+
setup(page: Page): Promise<void>;
|
|
8
|
+
/** Optional cleanup after recording (delete seeded data, logout). Never recorded. */
|
|
9
|
+
teardown?(page: Page): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface Step {
|
|
12
|
+
/** Stable id, auto-derived from index if omitted. Used in manifest, cache keys, logs. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** The narration line spoken over this step. Plain text; may be ''. */
|
|
15
|
+
narration: string;
|
|
16
|
+
/** The action. Receives the raw Playwright Page. May be a no-op for pure-narration steps. */
|
|
17
|
+
run: (page: Page) => Promise<void>;
|
|
18
|
+
/** Optional readiness hook awaited after run(); use when auto-waiting isn't enough. */
|
|
19
|
+
waitFor?: (page: Page) => Promise<void>;
|
|
20
|
+
/** Extra hold time (ms) after both narration and action complete. Default 400. */
|
|
21
|
+
settleMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface Tutorial {
|
|
24
|
+
/** Slug, used for output filenames. */
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
steps: Step[];
|
|
29
|
+
}
|
|
30
|
+
export interface TTSProvider {
|
|
31
|
+
/** Unique key for cache partitioning, e.g. "elevenlabs:daniel:eleven_turbo_v2" */
|
|
32
|
+
cacheKey: string;
|
|
33
|
+
/** Synthesize one narration line to a WAV/MP3 file at outPath. Duration is measured by the pipeline via ffprobe, not trusted from the provider. */
|
|
34
|
+
synthesize(text: string, outPath: string): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export interface RenderOptions {
|
|
37
|
+
tts: TTSProvider;
|
|
38
|
+
/** Path to final .mp4 */
|
|
39
|
+
output: string;
|
|
40
|
+
/** Default: .forge/<tutorial-id>/ */
|
|
41
|
+
workDir?: string;
|
|
42
|
+
/** Default 1920x1080 */
|
|
43
|
+
viewport?: {
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
};
|
|
47
|
+
/** Default true */
|
|
48
|
+
headless?: boolean;
|
|
49
|
+
/** Inject fake cursor, default true */
|
|
50
|
+
cursor?: boolean;
|
|
51
|
+
/** Highlight clicked elements, default true */
|
|
52
|
+
callouts?: boolean;
|
|
53
|
+
/** Default 'sidecar' (writes .srt next to mp4) */
|
|
54
|
+
subtitles?: 'burn' | 'sidecar' | 'off';
|
|
55
|
+
/** Silence before step narration starts, default 300 */
|
|
56
|
+
leadInMs?: number;
|
|
57
|
+
/** Default false on success, true on failure */
|
|
58
|
+
keepWorkDir?: boolean;
|
|
59
|
+
/** Directory for the content-hashed TTS cache. Default: ~/.cache/tutorial-forge/tts */
|
|
60
|
+
ttsCacheDir?: string;
|
|
61
|
+
/** TTS synthesis concurrency, default 4 */
|
|
62
|
+
ttsConcurrency?: number;
|
|
63
|
+
/** Which phases to run. Default 'all'. */
|
|
64
|
+
phase?: 'tts' | 'record' | 'post' | 'all';
|
|
65
|
+
}
|
|
66
|
+
export interface CalloutRecord {
|
|
67
|
+
atMs: number;
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
w: number;
|
|
71
|
+
h: number;
|
|
72
|
+
}
|
|
73
|
+
export interface ManifestStep {
|
|
74
|
+
id: string;
|
|
75
|
+
narration: string;
|
|
76
|
+
audioFile: string | null;
|
|
77
|
+
/** 0 for silent steps */
|
|
78
|
+
audioDurationMs: number;
|
|
79
|
+
/** Offset from video start */
|
|
80
|
+
startMs: number;
|
|
81
|
+
/** When run() began */
|
|
82
|
+
actionStartMs: number;
|
|
83
|
+
/** When run()+waitFor resolved */
|
|
84
|
+
actionEndMs: number;
|
|
85
|
+
/** When the step's hold completed */
|
|
86
|
+
endMs: number;
|
|
87
|
+
callouts: CalloutRecord[];
|
|
88
|
+
}
|
|
89
|
+
/** Written to workDir as manifest.json; the contract between record and post phases. */
|
|
90
|
+
export interface TimingManifest {
|
|
91
|
+
tutorialId: string;
|
|
92
|
+
fps: number;
|
|
93
|
+
recordingStartEpochMs: number;
|
|
94
|
+
/** Offset (ms) into the raw webm where the recording clock's zero falls, derived from the calibration flash. 0 if undetected. */
|
|
95
|
+
videoClockOffsetMs?: number;
|
|
96
|
+
steps: ManifestStep[];
|
|
97
|
+
totalDurationMs: number;
|
|
98
|
+
}
|
|
99
|
+
/** Thrown when a step's run()/waitFor() rejects during recording. */
|
|
100
|
+
export declare class StepError extends Error {
|
|
101
|
+
readonly tutorialId: string;
|
|
102
|
+
readonly stepId: string;
|
|
103
|
+
readonly cause: unknown;
|
|
104
|
+
constructor(tutorialId: string, stepId: string, cause: unknown);
|
|
105
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Thrown when a step's run()/waitFor() rejects during recording. */
|
|
2
|
+
export class StepError extends Error {
|
|
3
|
+
tutorialId;
|
|
4
|
+
stepId;
|
|
5
|
+
cause;
|
|
6
|
+
constructor(tutorialId, stepId, cause) {
|
|
7
|
+
super(`Step "${stepId}" of tutorial "${tutorialId}" failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
8
|
+
this.tutorialId = tutorialId;
|
|
9
|
+
this.stepId = stepId;
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
this.name = 'StepError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAwGA,qEAAqE;AACrE,MAAM,OAAO,SAAU,SAAQ,KAAK;IAEhB;IACA;IACS;IAH3B,YACkB,UAAkB,EAClB,MAAc,EACL,KAAc;QAEvC,KAAK,CACH,SAAS,MAAM,kBAAkB,UAAU,aAAa,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACjH,CAAC;QANc,eAAU,GAAV,UAAU,CAAQ;QAClB,WAAM,GAAN,MAAM,CAAQ;QACL,UAAK,GAAL,KAAK,CAAS;QAKvC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function ensureDir(dir: string): Promise<void>;
|
|
2
|
+
export declare function removeDir(dir: string): Promise<void>;
|
|
3
|
+
export declare function exists(path: string): Promise<boolean>;
|
|
4
|
+
/** Run tasks with bounded concurrency, preserving result order. */
|
|
5
|
+
export declare function mapLimit<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
package/dist/util/fs.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { mkdir, rm, access } from 'node:fs/promises';
|
|
2
|
+
export async function ensureDir(dir) {
|
|
3
|
+
await mkdir(dir, { recursive: true });
|
|
4
|
+
}
|
|
5
|
+
export async function removeDir(dir) {
|
|
6
|
+
await rm(dir, { recursive: true, force: true });
|
|
7
|
+
}
|
|
8
|
+
export async function exists(path) {
|
|
9
|
+
try {
|
|
10
|
+
await access(path);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Run tasks with bounded concurrency, preserving result order. */
|
|
18
|
+
export async function mapLimit(items, limit, fn) {
|
|
19
|
+
const results = new Array(items.length);
|
|
20
|
+
let next = 0;
|
|
21
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
22
|
+
while (next < items.length) {
|
|
23
|
+
const i = next++;
|
|
24
|
+
results[i] = await fn(items[i], i);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
await Promise.all(workers);
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=fs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/util/fs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW;IACzC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW;IACzC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,IAAY;IACvC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,KAAU,EACV,KAAa,EACb,EAA0C;IAE1C,MAAM,OAAO,GAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,KAAK,IAAI,EAAE;QAC5F,OAAO,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAC3B,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;YACjB,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,CAAM,EAAE,CAAC,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC3B,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sha256(...parts: string[]): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/util/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,UAAU,MAAM,CAAC,GAAG,KAAe;IACvC,MAAM,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
2
|
+
function threshold() {
|
|
3
|
+
const env = (process.env.FORGE_LOG ?? 'info');
|
|
4
|
+
return LEVELS[env] ?? 20;
|
|
5
|
+
}
|
|
6
|
+
function emit(level, msg) {
|
|
7
|
+
if (LEVELS[level] < threshold())
|
|
8
|
+
return;
|
|
9
|
+
const line = `[forge] ${level === 'info' ? '' : level + ': '}${msg}`;
|
|
10
|
+
if (level === 'error' || level === 'warn')
|
|
11
|
+
process.stderr.write(line + '\n');
|
|
12
|
+
else
|
|
13
|
+
process.stdout.write(line + '\n');
|
|
14
|
+
}
|
|
15
|
+
export const logger = {
|
|
16
|
+
debug: (msg) => emit('debug', msg),
|
|
17
|
+
info: (msg) => emit('info', msg),
|
|
18
|
+
warn: (msg) => emit('warn', msg),
|
|
19
|
+
error: (msg) => emit('error', msg),
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/util/logger.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAA0B,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AAEnF,SAAS,SAAS;IAChB,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM,CAAU,CAAC;IACvD,OAAO,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,SAAS,IAAI,CAAC,KAAY,EAAE,GAAW;IACrC,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,SAAS,EAAE;QAAE,OAAO;IACxC,MAAM,IAAI,GAAG,WAAW,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,GAAG,EAAE,CAAC;IACrE,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;;QACxE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC;IAC1C,IAAI,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;IACxC,IAAI,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;IACxC,KAAK,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC;CAC3C,CAAC"}
|