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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Brecht
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# tutorial-forge
|
|
2
|
+
|
|
3
|
+
Turn scripted Playwright walkthroughs into narrated tutorial videos (MP4).
|
|
4
|
+
|
|
5
|
+
**Tutorials are source code.** Each tutorial pairs narration lines with raw Playwright actions; the pipeline handles TTS narration (ElevenLabs, OpenAI, Piper, or silent), screen recording, narration-driven pacing, an animated cursor, click callouts, SRT subtitles, and the FFmpeg merge. When your UI changes, re-render instead of re-recording.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { tutorial, step } from 'tutorial-forge';
|
|
9
|
+
|
|
10
|
+
export default tutorial('Getting started', [
|
|
11
|
+
step('Open the Events page from the navigation bar.', async (page) => {
|
|
12
|
+
await page.getByRole('link', { name: 'Events' }).click();
|
|
13
|
+
}),
|
|
14
|
+
]);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This package is the library (types, spec builders, pipeline, TTS providers). Most users also want [`tutorial-forge-cli`](https://www.npmjs.com/package/tutorial-forge-cli) for the `tutorial-forge` command.
|
|
18
|
+
|
|
19
|
+
**Full documentation: [github.com/jbrecht/tutorial-forge](https://github.com/jbrecht/tutorial-forge)**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Click-highlight overlay: a brief rounded-rect ring around the action target,
|
|
3
|
+
* rendered live in the browser so the raw webm already contains it (v1 —
|
|
4
|
+
* the manifest keeps the data so a later version can composite in post).
|
|
5
|
+
*/
|
|
6
|
+
export declare const CALLOUT_INIT_SCRIPT = "\n(() => {\n if (window.__forgeCallout) return;\n window.__forgeCallout = (x, y, w, h) => {\n if (!document.documentElement) return;\n const pad = 6;\n const ring = document.createElement('div');\n ring.style.cssText = [\n 'position:fixed',\n 'left:' + (x - pad) + 'px', 'top:' + (y - pad) + 'px',\n 'width:' + (w + pad * 2) + 'px', 'height:' + (h + pad * 2) + 'px',\n 'border:3px solid rgba(66,133,244,.9)', 'border-radius:8px',\n 'box-shadow:0 0 0 3px rgba(66,133,244,.25)',\n 'z-index:2147483645', 'pointer-events:none',\n 'opacity:0', 'transition:opacity 120ms ease-in',\n ].join(';');\n document.documentElement.appendChild(ring);\n requestAnimationFrame(() => { ring.style.opacity = '1'; });\n setTimeout(() => {\n ring.style.transition = 'opacity 180ms ease-out';\n ring.style.opacity = '0';\n setTimeout(() => ring.remove(), 200);\n }, 600);\n };\n})();\n";
|
|
7
|
+
export declare const CALLOUT_VISIBLE_MS = 600;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Click-highlight overlay: a brief rounded-rect ring around the action target,
|
|
3
|
+
* rendered live in the browser so the raw webm already contains it (v1 —
|
|
4
|
+
* the manifest keeps the data so a later version can composite in post).
|
|
5
|
+
*/
|
|
6
|
+
export const CALLOUT_INIT_SCRIPT = `
|
|
7
|
+
(() => {
|
|
8
|
+
if (window.__forgeCallout) return;
|
|
9
|
+
window.__forgeCallout = (x, y, w, h) => {
|
|
10
|
+
if (!document.documentElement) return;
|
|
11
|
+
const pad = 6;
|
|
12
|
+
const ring = document.createElement('div');
|
|
13
|
+
ring.style.cssText = [
|
|
14
|
+
'position:fixed',
|
|
15
|
+
'left:' + (x - pad) + 'px', 'top:' + (y - pad) + 'px',
|
|
16
|
+
'width:' + (w + pad * 2) + 'px', 'height:' + (h + pad * 2) + 'px',
|
|
17
|
+
'border:3px solid rgba(66,133,244,.9)', 'border-radius:8px',
|
|
18
|
+
'box-shadow:0 0 0 3px rgba(66,133,244,.25)',
|
|
19
|
+
'z-index:2147483645', 'pointer-events:none',
|
|
20
|
+
'opacity:0', 'transition:opacity 120ms ease-in',
|
|
21
|
+
].join(';');
|
|
22
|
+
document.documentElement.appendChild(ring);
|
|
23
|
+
requestAnimationFrame(() => { ring.style.opacity = '1'; });
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
ring.style.transition = 'opacity 180ms ease-out';
|
|
26
|
+
ring.style.opacity = '0';
|
|
27
|
+
setTimeout(() => ring.remove(), 200);
|
|
28
|
+
}, 600);
|
|
29
|
+
};
|
|
30
|
+
})();
|
|
31
|
+
`;
|
|
32
|
+
export const CALLOUT_VISIBLE_MS = 600;
|
|
33
|
+
//# sourceMappingURL=callout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callout.js","sourceRoot":"","sources":["../../src/browser/callout.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;CAyBlC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fake-cursor overlay. Playwright fires real input events but renders no
|
|
3
|
+
* cursor; we inject one and move it explicitly before instrumented actions.
|
|
4
|
+
* Everything is namespaced under __forge_* and pointer-events: none —
|
|
5
|
+
* graceful degradation, never interference.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CURSOR_INIT_SCRIPT = "\n(() => {\n if (window.__forgeCursor) return;\n\n const ensure = () => {\n let el = document.getElementById('__forge_cursor__');\n if (el) return el;\n if (!document.documentElement) return null;\n el = document.createElement('div');\n el.id = '__forge_cursor__';\n el.style.cssText = [\n 'position:fixed', 'left:0', 'top:0', 'width:24px', 'height:24px',\n 'z-index:2147483647', 'pointer-events:none',\n 'transform:translate(-100px,-100px)',\n 'transition:transform 350ms cubic-bezier(.25,.1,.25,1)',\n 'filter:drop-shadow(0 1px 2px rgba(0,0,0,.4))',\n ].join(';');\n el.innerHTML = '<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">'\n + '<path d=\"M5 2 L5 19 L9.5 15 L12.5 21.5 L15 20.3 L12 14 L18 14 Z\"'\n + ' fill=\"white\" stroke=\"black\" stroke-width=\"1.4\" stroke-linejoin=\"round\"/></svg>';\n document.documentElement.appendChild(el);\n return el;\n };\n\n window.__forgeCursor = {\n moveTo(x, y) {\n const el = ensure();\n if (el) el.style.transform = 'translate(' + x + 'px,' + y + 'px)';\n },\n pulse(x, y) {\n if (!document.documentElement) return;\n const p = document.createElement('div');\n p.style.cssText = [\n 'position:fixed', 'left:' + (x - 18) + 'px', 'top:' + (y - 18) + 'px',\n 'width:36px', 'height:36px', 'border-radius:50%',\n 'background:rgba(66,133,244,.45)', 'z-index:2147483646',\n 'pointer-events:none', 'transform:scale(.3)', 'opacity:1',\n 'transition:transform 300ms ease-out, opacity 300ms ease-out',\n ].join(';');\n document.documentElement.appendChild(p);\n requestAnimationFrame(() => {\n p.style.transform = 'scale(1.6)';\n p.style.opacity = '0';\n });\n setTimeout(() => p.remove(), 350);\n },\n };\n\n if (document.readyState !== 'loading') ensure();\n else document.addEventListener('DOMContentLoaded', ensure, { once: true });\n})();\n";
|
|
8
|
+
/** Duration of the cursor's CSS transition; instrumented actions wait this long after moveTo. */
|
|
9
|
+
export declare const CURSOR_TRAVEL_MS = 350;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fake-cursor overlay. Playwright fires real input events but renders no
|
|
3
|
+
* cursor; we inject one and move it explicitly before instrumented actions.
|
|
4
|
+
* Everything is namespaced under __forge_* and pointer-events: none —
|
|
5
|
+
* graceful degradation, never interference.
|
|
6
|
+
*/
|
|
7
|
+
export const CURSOR_INIT_SCRIPT = `
|
|
8
|
+
(() => {
|
|
9
|
+
if (window.__forgeCursor) return;
|
|
10
|
+
|
|
11
|
+
const ensure = () => {
|
|
12
|
+
let el = document.getElementById('__forge_cursor__');
|
|
13
|
+
if (el) return el;
|
|
14
|
+
if (!document.documentElement) return null;
|
|
15
|
+
el = document.createElement('div');
|
|
16
|
+
el.id = '__forge_cursor__';
|
|
17
|
+
el.style.cssText = [
|
|
18
|
+
'position:fixed', 'left:0', 'top:0', 'width:24px', 'height:24px',
|
|
19
|
+
'z-index:2147483647', 'pointer-events:none',
|
|
20
|
+
'transform:translate(-100px,-100px)',
|
|
21
|
+
'transition:transform 350ms cubic-bezier(.25,.1,.25,1)',
|
|
22
|
+
'filter:drop-shadow(0 1px 2px rgba(0,0,0,.4))',
|
|
23
|
+
].join(';');
|
|
24
|
+
el.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24">'
|
|
25
|
+
+ '<path d="M5 2 L5 19 L9.5 15 L12.5 21.5 L15 20.3 L12 14 L18 14 Z"'
|
|
26
|
+
+ ' fill="white" stroke="black" stroke-width="1.4" stroke-linejoin="round"/></svg>';
|
|
27
|
+
document.documentElement.appendChild(el);
|
|
28
|
+
return el;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
window.__forgeCursor = {
|
|
32
|
+
moveTo(x, y) {
|
|
33
|
+
const el = ensure();
|
|
34
|
+
if (el) el.style.transform = 'translate(' + x + 'px,' + y + 'px)';
|
|
35
|
+
},
|
|
36
|
+
pulse(x, y) {
|
|
37
|
+
if (!document.documentElement) return;
|
|
38
|
+
const p = document.createElement('div');
|
|
39
|
+
p.style.cssText = [
|
|
40
|
+
'position:fixed', 'left:' + (x - 18) + 'px', 'top:' + (y - 18) + 'px',
|
|
41
|
+
'width:36px', 'height:36px', 'border-radius:50%',
|
|
42
|
+
'background:rgba(66,133,244,.45)', 'z-index:2147483646',
|
|
43
|
+
'pointer-events:none', 'transform:scale(.3)', 'opacity:1',
|
|
44
|
+
'transition:transform 300ms ease-out, opacity 300ms ease-out',
|
|
45
|
+
].join(';');
|
|
46
|
+
document.documentElement.appendChild(p);
|
|
47
|
+
requestAnimationFrame(() => {
|
|
48
|
+
p.style.transform = 'scale(1.6)';
|
|
49
|
+
p.style.opacity = '0';
|
|
50
|
+
});
|
|
51
|
+
setTimeout(() => p.remove(), 350);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (document.readyState !== 'loading') ensure();
|
|
56
|
+
else document.addEventListener('DOMContentLoaded', ensure, { once: true });
|
|
57
|
+
})();
|
|
58
|
+
`;
|
|
59
|
+
/** Duration of the cursor's CSS transition; instrumented actions wait this long after moveTo. */
|
|
60
|
+
export const CURSOR_TRAVEL_MS = 350;
|
|
61
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/browser/cursor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDjC,CAAC;AAEF,iGAAiG;AACjG,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
import type { CalloutRecord } from '../types.js';
|
|
3
|
+
export interface InstrumentHooks {
|
|
4
|
+
cursor: boolean;
|
|
5
|
+
callouts: boolean;
|
|
6
|
+
/** Recording-clock time, for callout timestamps. */
|
|
7
|
+
nowMs: () => number;
|
|
8
|
+
onCallout: (c: CalloutRecord) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Wrap a Page so locators it produces animate the fake cursor and emit
|
|
12
|
+
* callout records before delegating to real Playwright methods. If an exotic
|
|
13
|
+
* call path escapes the proxy, the action still works — the cursor just
|
|
14
|
+
* doesn't move.
|
|
15
|
+
*/
|
|
16
|
+
export declare function instrumentPage(page: Page, hooks: InstrumentHooks): Page;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { CURSOR_TRAVEL_MS } from './cursor.js';
|
|
2
|
+
import { logger } from '../util/logger.js';
|
|
3
|
+
/** Locator methods that interact with an element (cursor should travel there first). */
|
|
4
|
+
const ACTION_METHODS = new Set([
|
|
5
|
+
'click', 'dblclick', 'hover', 'fill', 'check', 'uncheck', 'setChecked',
|
|
6
|
+
'selectOption', 'tap', 'press', 'pressSequentially', 'type', 'clear',
|
|
7
|
+
]);
|
|
8
|
+
/** Subset that warrants a click pulse + callout ring. */
|
|
9
|
+
const CLICK_METHODS = new Set([
|
|
10
|
+
'click', 'dblclick', 'check', 'uncheck', 'setChecked', 'selectOption', 'tap',
|
|
11
|
+
]);
|
|
12
|
+
/** Locator methods that return another Locator and must stay instrumented. */
|
|
13
|
+
const CHAIN_METHODS = new Set([
|
|
14
|
+
'locator', 'getByRole', 'getByText', 'getByLabel', 'getByPlaceholder',
|
|
15
|
+
'getByTestId', 'getByTitle', 'getByAltText', 'first', 'last', 'nth',
|
|
16
|
+
'filter', 'and', 'or', 'describe',
|
|
17
|
+
]);
|
|
18
|
+
/** Page methods that return a Locator to instrument. */
|
|
19
|
+
const PAGE_LOCATOR_METHODS = new Set([
|
|
20
|
+
'locator', 'getByRole', 'getByText', 'getByLabel', 'getByPlaceholder',
|
|
21
|
+
'getByTestId', 'getByTitle', 'getByAltText',
|
|
22
|
+
]);
|
|
23
|
+
/** Deprecated-but-supported page-level shortcuts taking a selector first arg. */
|
|
24
|
+
const PAGE_ACTION_METHODS = new Set([
|
|
25
|
+
'click', 'dblclick', 'hover', 'fill', 'check', 'uncheck', 'selectOption', 'tap', 'press', 'type',
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* Move the cursor to the target, pulse + ring on click-like actions, record
|
|
29
|
+
* the callout. Any failure here is swallowed: the action must still run.
|
|
30
|
+
*/
|
|
31
|
+
async function presentAction(page, target, method, hooks) {
|
|
32
|
+
if (!hooks.cursor && !hooks.callouts)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
await target.waitFor({ state: 'visible', timeout: 5000 });
|
|
36
|
+
const box = await target.boundingBox();
|
|
37
|
+
if (!box)
|
|
38
|
+
return;
|
|
39
|
+
const cx = box.x + box.width / 2;
|
|
40
|
+
const cy = box.y + box.height / 2;
|
|
41
|
+
if (hooks.cursor) {
|
|
42
|
+
await page.evaluate(([x, y]) => window.__forgeCursor?.moveTo(x, y), [cx, cy]);
|
|
43
|
+
await page.waitForTimeout(CURSOR_TRAVEL_MS + 50);
|
|
44
|
+
}
|
|
45
|
+
if (CLICK_METHODS.has(method)) {
|
|
46
|
+
if (hooks.cursor) {
|
|
47
|
+
await page.evaluate(([x, y]) => window.__forgeCursor?.pulse(x, y), [cx, cy]);
|
|
48
|
+
}
|
|
49
|
+
if (hooks.callouts) {
|
|
50
|
+
await page.evaluate(([x, y, w, h]) => window.__forgeCallout?.(x, y, w, h), [box.x, box.y, box.width, box.height]);
|
|
51
|
+
hooks.onCallout({ atMs: hooks.nowMs(), x: box.x, y: box.y, w: box.width, h: box.height });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
logger.debug(`cursor presentation skipped for ${method}: ${err instanceof Error ? err.message : err}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function wrapLocator(locator, page, hooks) {
|
|
60
|
+
return new Proxy(locator, {
|
|
61
|
+
get(target, prop, receiver) {
|
|
62
|
+
const value = Reflect.get(target, prop, target);
|
|
63
|
+
if (typeof value !== 'function' || typeof prop !== 'string')
|
|
64
|
+
return value;
|
|
65
|
+
if (ACTION_METHODS.has(prop)) {
|
|
66
|
+
return async (...args) => {
|
|
67
|
+
await presentAction(page, target, prop, hooks);
|
|
68
|
+
return value.apply(target, args);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (CHAIN_METHODS.has(prop)) {
|
|
72
|
+
return (...args) => {
|
|
73
|
+
const result = value.apply(target, args);
|
|
74
|
+
return isLocatorLike(result) ? wrapLocator(result, page, hooks) : result;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return value.bind(target);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function isLocatorLike(v) {
|
|
82
|
+
return !!v && typeof v.click === 'function' && typeof v.boundingBox === 'function';
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Wrap a Page so locators it produces animate the fake cursor and emit
|
|
86
|
+
* callout records before delegating to real Playwright methods. If an exotic
|
|
87
|
+
* call path escapes the proxy, the action still works — the cursor just
|
|
88
|
+
* doesn't move.
|
|
89
|
+
*/
|
|
90
|
+
export function instrumentPage(page, hooks) {
|
|
91
|
+
return new Proxy(page, {
|
|
92
|
+
get(target, prop, receiver) {
|
|
93
|
+
const value = Reflect.get(target, prop, target);
|
|
94
|
+
if (typeof value !== 'function' || typeof prop !== 'string')
|
|
95
|
+
return value;
|
|
96
|
+
if (PAGE_LOCATOR_METHODS.has(prop)) {
|
|
97
|
+
return (...args) => {
|
|
98
|
+
const result = value.apply(target, args);
|
|
99
|
+
return isLocatorLike(result) ? wrapLocator(result, target, hooks) : result;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (PAGE_ACTION_METHODS.has(prop)) {
|
|
103
|
+
return async (...args) => {
|
|
104
|
+
if (typeof args[0] === 'string') {
|
|
105
|
+
await presentAction(target, target.locator(args[0]), prop, hooks);
|
|
106
|
+
}
|
|
107
|
+
return value.apply(target, args);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return value.bind(target);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=instrument.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrument.js","sourceRoot":"","sources":["../../src/browser/instrument.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Recording clock: all manifest offsets are ms since zero(). */
|
|
2
|
+
export declare class RecordingClock {
|
|
3
|
+
private zeroEpochMs;
|
|
4
|
+
zero(): void;
|
|
5
|
+
get zeroEpoch(): number;
|
|
6
|
+
/** ms since zero() */
|
|
7
|
+
now(): number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Step pacing: the step holds until both the narration budget
|
|
11
|
+
* (leadIn + audio) and the action have completed, then settles.
|
|
12
|
+
* Pure function so the math is unit-testable.
|
|
13
|
+
*/
|
|
14
|
+
export declare function stepHoldUntilMs(input: {
|
|
15
|
+
startMs: number;
|
|
16
|
+
leadInMs: number;
|
|
17
|
+
audioDurationMs: number;
|
|
18
|
+
actionEndMs: number;
|
|
19
|
+
settleMs: number;
|
|
20
|
+
}): number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Recording clock: all manifest offsets are ms since zero(). */
|
|
2
|
+
export class RecordingClock {
|
|
3
|
+
zeroEpochMs = 0;
|
|
4
|
+
zero() {
|
|
5
|
+
this.zeroEpochMs = Date.now();
|
|
6
|
+
}
|
|
7
|
+
get zeroEpoch() {
|
|
8
|
+
return this.zeroEpochMs;
|
|
9
|
+
}
|
|
10
|
+
/** ms since zero() */
|
|
11
|
+
now() {
|
|
12
|
+
if (this.zeroEpochMs === 0)
|
|
13
|
+
throw new Error('RecordingClock used before zero()');
|
|
14
|
+
return Date.now() - this.zeroEpochMs;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Step pacing: the step holds until both the narration budget
|
|
19
|
+
* (leadIn + audio) and the action have completed, then settles.
|
|
20
|
+
* Pure function so the math is unit-testable.
|
|
21
|
+
*/
|
|
22
|
+
export function stepHoldUntilMs(input) {
|
|
23
|
+
const narrationBudgetEnd = input.startMs + input.leadInMs + input.audioDurationMs;
|
|
24
|
+
return Math.max(narrationBudgetEnd, input.actionEndMs) + input.settleMs;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../../src/browser/timing.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,OAAO,cAAc;IACjB,WAAW,GAAG,CAAC,CAAC;IAExB,IAAI;QACF,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,sBAAsB;IACtB,GAAG;QACD,IAAI,IAAI,CAAC,WAAW,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACjF,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC;IACvC,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAM/B;IACC,MAAM,kBAAkB,GAAG,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC,eAAe,CAAC;IAClF,OAAO,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC;AAC1E,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TutorialAdapter, TTSProvider } from './types.js';
|
|
2
|
+
/** Shape of forge.config.ts in a consumer repo. CLI flags override these; these override defaults. */
|
|
3
|
+
export interface ForgeConfig {
|
|
4
|
+
adapter: TutorialAdapter;
|
|
5
|
+
tts: TTSProvider;
|
|
6
|
+
/** Where final videos land. Default: tutorials/dist */
|
|
7
|
+
outDir?: string;
|
|
8
|
+
/** Globs for tutorial discovery. Default: ["**\/*.tutorial.ts"] */
|
|
9
|
+
tutorials?: string[];
|
|
10
|
+
viewport?: {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
14
|
+
headless?: boolean;
|
|
15
|
+
cursor?: boolean;
|
|
16
|
+
callouts?: boolean;
|
|
17
|
+
subtitles?: 'burn' | 'sidecar' | 'off';
|
|
18
|
+
leadInMs?: number;
|
|
19
|
+
keepWorkDir?: boolean;
|
|
20
|
+
ttsCacheDir?: string;
|
|
21
|
+
ttsConcurrency?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function defineConfig(config: ForgeConfig): ForgeConfig;
|
|
24
|
+
/** Validate a loaded config object (e.g. from forge.config.ts). Throws with a readable message. */
|
|
25
|
+
export declare function validateConfig(config: unknown): ForgeConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const configSchema = z.object({
|
|
3
|
+
adapter: z.object({
|
|
4
|
+
baseURL: z.string().url(),
|
|
5
|
+
setup: z.function(),
|
|
6
|
+
teardown: z.function().optional(),
|
|
7
|
+
}),
|
|
8
|
+
tts: z.object({
|
|
9
|
+
cacheKey: z.string().min(1),
|
|
10
|
+
synthesize: z.function(),
|
|
11
|
+
}),
|
|
12
|
+
outDir: z.string().optional(),
|
|
13
|
+
tutorials: z.array(z.string()).optional(),
|
|
14
|
+
viewport: z.object({ width: z.number().int().positive(), height: z.number().int().positive() }).optional(),
|
|
15
|
+
headless: z.boolean().optional(),
|
|
16
|
+
cursor: z.boolean().optional(),
|
|
17
|
+
callouts: z.boolean().optional(),
|
|
18
|
+
subtitles: z.enum(['burn', 'sidecar', 'off']).optional(),
|
|
19
|
+
leadInMs: z.number().nonnegative().optional(),
|
|
20
|
+
keepWorkDir: z.boolean().optional(),
|
|
21
|
+
ttsCacheDir: z.string().optional(),
|
|
22
|
+
ttsConcurrency: z.number().int().positive().optional(),
|
|
23
|
+
});
|
|
24
|
+
export function defineConfig(config) {
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
/** Validate a loaded config object (e.g. from forge.config.ts). Throws with a readable message. */
|
|
28
|
+
export function validateConfig(config) {
|
|
29
|
+
const parsed = configSchema.safeParse(config);
|
|
30
|
+
if (!parsed.success) {
|
|
31
|
+
const issues = parsed.error.issues.map((i) => ` ${i.path.join('.') || '(root)'}: ${i.message}`);
|
|
32
|
+
throw new Error(`Invalid forge config:\n${issues.join('\n')}`);
|
|
33
|
+
}
|
|
34
|
+
return config;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAsBxB,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5B,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC;QAChB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QACzB,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE;QACnB,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;KAClC,CAAC;IACF,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC;QACZ,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3B,UAAU,EAAE,CAAC,CAAC,QAAQ,EAAE;KACzB,CAAC;IACF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACzC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC1G,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IAC9B,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;IAC7C,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IACnC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACvD,CAAC,CAAC;AAEH,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC9C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,cAAc,CAAC,MAAe;IAC5C,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACjG,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,MAAqB,CAAC;AAC/B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type { Tutorial, Step, TutorialAdapter, TTSProvider, RenderOptions, TimingManifest, ManifestStep, CalloutRecord, } from './types.js';
|
|
2
|
+
export { StepError } from './types.js';
|
|
3
|
+
export { tutorial, step, validateTutorial, stepId } from './spec.js';
|
|
4
|
+
export { defineConfig, validateConfig, type ForgeConfig } from './config.js';
|
|
5
|
+
export { render, type RenderResult } from './pipeline/render.js';
|
|
6
|
+
export { runTTSPhase, loadTTSResult } from './pipeline/tts.js';
|
|
7
|
+
export { runRecordPhase, loadManifest, RAW_VIDEO_FILE, MANIFEST_FILE } from './pipeline/record.js';
|
|
8
|
+
export { runPostPhase } from './pipeline/post.js';
|
|
9
|
+
export { SilentProvider } from './tts/silent.js';
|
|
10
|
+
export { Piper, type PiperOptions } from './tts/piper.js';
|
|
11
|
+
export { ElevenLabs, type ElevenLabsOptions } from './tts/elevenlabs.js';
|
|
12
|
+
export { OpenAITTS, type OpenAITTSOptions } from './tts/openai.js';
|
|
13
|
+
export { estimateDurationMs } from './tts/provider.js';
|
|
14
|
+
export { defaultCacheDir, synthesizeCached } from './tts/cache.js';
|
|
15
|
+
export { generateSrt, srtTime, wrapText } from './post/subtitles.js';
|
|
16
|
+
export { buildMergeArgs, probeDurationMs, detectFlashOffsetMs, parseFlashFromMetadata, normalizeToWav, ffmpegVersion, type MergeArgsInput, } from './post/ffmpeg.js';
|
|
17
|
+
export { stepHoldUntilMs } from './browser/timing.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Public API surface — re-exports only.
|
|
2
|
+
export { StepError } from './types.js';
|
|
3
|
+
export { tutorial, step, validateTutorial, stepId } from './spec.js';
|
|
4
|
+
export { defineConfig, validateConfig } from './config.js';
|
|
5
|
+
export { render } from './pipeline/render.js';
|
|
6
|
+
export { runTTSPhase, loadTTSResult } from './pipeline/tts.js';
|
|
7
|
+
export { runRecordPhase, loadManifest, RAW_VIDEO_FILE, MANIFEST_FILE } from './pipeline/record.js';
|
|
8
|
+
export { runPostPhase } from './pipeline/post.js';
|
|
9
|
+
export { SilentProvider } from './tts/silent.js';
|
|
10
|
+
export { Piper } from './tts/piper.js';
|
|
11
|
+
export { ElevenLabs } from './tts/elevenlabs.js';
|
|
12
|
+
export { OpenAITTS } from './tts/openai.js';
|
|
13
|
+
export { estimateDurationMs } from './tts/provider.js';
|
|
14
|
+
export { defaultCacheDir, synthesizeCached } from './tts/cache.js';
|
|
15
|
+
export { generateSrt, srtTime, wrapText } from './post/subtitles.js';
|
|
16
|
+
export { buildMergeArgs, probeDurationMs, detectFlashOffsetMs, parseFlashFromMetadata, normalizeToWav, ffmpegVersion, } from './post/ffmpeg.js';
|
|
17
|
+
export { stepHoldUntilMs } from './browser/timing.js';
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AAYxC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAoB,MAAM,aAAa,CAAC;AAE7E,OAAO,EAAE,MAAM,EAAqB,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACnG,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAqB,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,UAAU,EAA0B,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAyB,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEnE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EACL,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,aAAa,GAEd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TimingManifest } from '../types.js';
|
|
2
|
+
export interface PostPhaseOptions {
|
|
3
|
+
workDir: string;
|
|
4
|
+
output: string;
|
|
5
|
+
viewport: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
subtitles: 'burn' | 'sidecar' | 'off';
|
|
10
|
+
leadInMs: number;
|
|
11
|
+
}
|
|
12
|
+
export interface PostPhaseResult {
|
|
13
|
+
output: string;
|
|
14
|
+
srtPath: string | null;
|
|
15
|
+
videoClockOffsetMs: number;
|
|
16
|
+
outputDurationMs: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Phase 3 — single ffmpeg invocation: trim pre-roll/tail, lay narration over
|
|
20
|
+
* silence at manifest offsets, downscale 2x→1x, transcode to H.264/AAC.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runPostPhase(manifest: TimingManifest, opts: PostPhaseOptions): Promise<PostPhaseResult>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { join, dirname, basename, extname } from 'node:path';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { RAW_VIDEO_FILE } from './record.js';
|
|
4
|
+
import { buildMergeArgs, detectFlashOffsetMs, probeDurationMs, runFfmpeg } from '../post/ffmpeg.js';
|
|
5
|
+
import { generateSrt } from '../post/subtitles.js';
|
|
6
|
+
import { ensureDir, exists } from '../util/fs.js';
|
|
7
|
+
import { logger } from '../util/logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Phase 3 — single ffmpeg invocation: trim pre-roll/tail, lay narration over
|
|
10
|
+
* silence at manifest offsets, downscale 2x→1x, transcode to H.264/AAC.
|
|
11
|
+
*/
|
|
12
|
+
export async function runPostPhase(manifest, opts) {
|
|
13
|
+
const rawVideo = join(opts.workDir, RAW_VIDEO_FILE);
|
|
14
|
+
if (!(await exists(rawVideo))) {
|
|
15
|
+
throw new Error(`No ${RAW_VIDEO_FILE} in ${opts.workDir} — run the record phase first`);
|
|
16
|
+
}
|
|
17
|
+
const firstStep = manifest.steps[0];
|
|
18
|
+
if (!firstStep)
|
|
19
|
+
throw new Error('Manifest has no steps');
|
|
20
|
+
const trimStartMs = Math.max(0, firstStep.startMs - opts.leadInMs);
|
|
21
|
+
let videoOffsetMs = await detectFlashOffsetMs(rawVideo);
|
|
22
|
+
if (videoOffsetMs === null) {
|
|
23
|
+
logger.warn('calibration flash not found in recording — assuming video starts at clock zero; audio sync may drift by the context-creation gap');
|
|
24
|
+
videoOffsetMs = 0;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
logger.debug(`calibration flash at ${videoOffsetMs}ms into raw video`);
|
|
28
|
+
}
|
|
29
|
+
manifest.videoClockOffsetMs = videoOffsetMs;
|
|
30
|
+
await ensureDir(dirname(opts.output));
|
|
31
|
+
let srtPath = null;
|
|
32
|
+
if (opts.subtitles !== 'off') {
|
|
33
|
+
const srt = generateSrt(manifest, { leadInMs: opts.leadInMs, trimStartMs });
|
|
34
|
+
srtPath =
|
|
35
|
+
opts.subtitles === 'sidecar'
|
|
36
|
+
? join(dirname(opts.output), basename(opts.output, extname(opts.output)) + '.srt')
|
|
37
|
+
: join(opts.workDir, 'subtitles.srt');
|
|
38
|
+
await writeFile(srtPath, srt);
|
|
39
|
+
}
|
|
40
|
+
const args = buildMergeArgs({
|
|
41
|
+
rawVideo,
|
|
42
|
+
manifest,
|
|
43
|
+
audioFiles: manifest.steps.map((s) => s.audioFile),
|
|
44
|
+
output: opts.output,
|
|
45
|
+
leadInMs: opts.leadInMs,
|
|
46
|
+
trimStartMs,
|
|
47
|
+
videoOffsetMs,
|
|
48
|
+
targetWidth: opts.viewport.width,
|
|
49
|
+
targetHeight: opts.viewport.height,
|
|
50
|
+
burnSrt: opts.subtitles === 'burn' && srtPath ? srtPath : undefined,
|
|
51
|
+
});
|
|
52
|
+
logger.info('post: merging audio + video (ffmpeg)');
|
|
53
|
+
await runFfmpeg(args);
|
|
54
|
+
const outputDurationMs = await probeDurationMs(opts.output);
|
|
55
|
+
logger.info(`post: wrote ${opts.output} (${(outputDurationMs / 1000).toFixed(1)}s)`);
|
|
56
|
+
return {
|
|
57
|
+
output: opts.output,
|
|
58
|
+
srtPath: opts.subtitles === 'sidecar' ? srtPath : null,
|
|
59
|
+
videoClockOffsetMs: videoOffsetMs,
|
|
60
|
+
outputDurationMs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=post.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post.js","sourceRoot":"","sources":["../../src/pipeline/post.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TimingManifest, Tutorial, TutorialAdapter } from '../types.js';
|
|
2
|
+
import type { TTSPhaseResult } from './tts.js';
|
|
3
|
+
export declare const RAW_VIDEO_FILE = "raw.webm";
|
|
4
|
+
export declare const MANIFEST_FILE = "manifest.json";
|
|
5
|
+
export interface RecordPhaseOptions {
|
|
6
|
+
workDir: string;
|
|
7
|
+
viewport: {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
headless: boolean;
|
|
12
|
+
cursor: boolean;
|
|
13
|
+
callouts: boolean;
|
|
14
|
+
leadInMs: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Phase 2 — drive the browser through the tutorial while Playwright records
|
|
18
|
+
* video, pacing each step to its narration budget. Writes raw.webm and
|
|
19
|
+
* manifest.json into workDir.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runRecordPhase(tutorial: Tutorial, adapter: TutorialAdapter, tts: TTSPhaseResult, opts: RecordPhaseOptions): Promise<TimingManifest>;
|
|
22
|
+
/** Load a previous run's manifest.json (for `--phase post`). */
|
|
23
|
+
export declare function loadManifest(workDir: string): Promise<TimingManifest>;
|