agent-screenplay 0.0.0-stage → 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 +141 -2
- package/assets/editor/index.html +536 -0
- package/assets/fonts/Inter-Bold.ttf +0 -0
- package/assets/fonts/Inter-LICENSE.txt +92 -0
- package/assets/fonts/Inter-Medium.ttf +0 -0
- package/assets/fonts/Inter-Regular.ttf +0 -0
- package/assets/fonts/Inter-SemiBold.ttf +0 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +163 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.ts +16 -0
- package/dist/core.js +73 -0
- package/dist/core.js.map +1 -0
- package/dist/direct/plan.d.ts +100 -0
- package/dist/direct/plan.js +274 -0
- package/dist/direct/plan.js.map +1 -0
- package/dist/drivers/browser.d.ts +9 -0
- package/dist/drivers/browser.js +44 -0
- package/dist/drivers/browser.js.map +1 -0
- package/dist/drivers/desktop.d.ts +53 -0
- package/dist/drivers/desktop.js +650 -0
- package/dist/drivers/desktop.js.map +1 -0
- package/dist/drivers/pace.d.ts +11 -0
- package/dist/drivers/pace.js +44 -0
- package/dist/drivers/pace.js.map +1 -0
- package/dist/drivers/web.d.ts +37 -0
- package/dist/drivers/web.js +570 -0
- package/dist/drivers/web.js.map +1 -0
- package/dist/editor/server.d.ts +96 -0
- package/dist/editor/server.js +303 -0
- package/dist/editor/server.js.map +1 -0
- package/dist/ffmpeg.d.ts +5 -0
- package/dist/ffmpeg.js +44 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/guide.d.ts +6 -0
- package/dist/guide.js +25 -0
- package/dist/guide.js.map +1 -0
- package/dist/init.d.ts +19 -0
- package/dist/init.js +147 -0
- package/dist/init.js.map +1 -0
- package/dist/login.d.ts +2 -0
- package/dist/login.js +50 -0
- package/dist/login.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +132 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/render/compose.d.ts +68 -0
- package/dist/render/compose.js +361 -0
- package/dist/render/compose.js.map +1 -0
- package/dist/render/presets.d.ts +42 -0
- package/dist/render/presets.js +111 -0
- package/dist/render/presets.js.map +1 -0
- package/dist/render/render.d.ts +25 -0
- package/dist/render/render.js +257 -0
- package/dist/render/render.js.map +1 -0
- package/dist/render/sources.d.ts +15 -0
- package/dist/render/sources.js +146 -0
- package/dist/render/sources.js.map +1 -0
- package/dist/render/style.d.ts +37 -0
- package/dist/render/style.js +118 -0
- package/dist/render/style.js.map +1 -0
- package/dist/scenario/config.d.ts +43 -0
- package/dist/scenario/config.js +69 -0
- package/dist/scenario/config.js.map +1 -0
- package/dist/scenario/fingerprint.d.ts +10 -0
- package/dist/scenario/fingerprint.js +58 -0
- package/dist/scenario/fingerprint.js.map +1 -0
- package/dist/scenario/load.d.ts +36 -0
- package/dist/scenario/load.js +101 -0
- package/dist/scenario/load.js.map +1 -0
- package/dist/scenario/schema.d.ts +553 -0
- package/dist/scenario/schema.js +262 -0
- package/dist/scenario/schema.js.map +1 -0
- package/dist/scenario/target.d.ts +26 -0
- package/dist/scenario/target.js +40 -0
- package/dist/scenario/target.js.map +1 -0
- package/dist/take.d.ts +100 -0
- package/dist/take.js +15 -0
- package/dist/take.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/dist/voice/cartesia.d.ts +12 -0
- package/dist/voice/cartesia.js +103 -0
- package/dist/voice/cartesia.js.map +1 -0
- package/package.json +65 -4
- package/schema/scenario.schema.json +1098 -0
- package/skills/screenplay/SKILL.md +127 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { check, record, render } from "./core.js";
|
|
4
|
+
import { guideText, schemaText } from "./guide.js";
|
|
5
|
+
import { init } from "./init.js";
|
|
6
|
+
import { login } from "./login.js";
|
|
7
|
+
import { ScenarioError } from "./scenario/load.js";
|
|
8
|
+
import { VERSION } from "./version.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("screenplay")
|
|
12
|
+
.description("Screen Studio-quality screen recordings for AI agents.")
|
|
13
|
+
.version(VERSION);
|
|
14
|
+
const log = (msg) => process.stderr.write(`${msg}\n`);
|
|
15
|
+
function fail(e) {
|
|
16
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
17
|
+
process.stderr.write(`\n✗ ${msg}\n`);
|
|
18
|
+
if (process.env.SCREENPLAY_DEBUG && e instanceof Error && !(e instanceof ScenarioError))
|
|
19
|
+
process.stderr.write(`${e.stack}\n`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
program
|
|
23
|
+
.command("record")
|
|
24
|
+
.argument("<scenario>", "scenario JSON file")
|
|
25
|
+
.description("Film the scenario and edit the video (MP4 + storyboard).")
|
|
26
|
+
.option("--no-voice", "skip the voice-over even if the scenario enables it")
|
|
27
|
+
.option("--gif", "also write a GIF")
|
|
28
|
+
.option("--json", "print the result as JSON")
|
|
29
|
+
.action(async (file, o) => {
|
|
30
|
+
try {
|
|
31
|
+
const r = await record(file, { noVoice: !o.voice, gif: o.gif, log });
|
|
32
|
+
if (o.json)
|
|
33
|
+
console.log(JSON.stringify(r, null, 2));
|
|
34
|
+
else {
|
|
35
|
+
log(`\n✓ ${r.mp4} (${r.duration.toFixed(1)}s, ${r.steps} steps, edited in ${r.seconds.toFixed(0)}s)`);
|
|
36
|
+
log(` storyboard: ${r.storyboard}`);
|
|
37
|
+
if (r.gif)
|
|
38
|
+
log(` gif: ${r.gif}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
fail(e);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
program
|
|
46
|
+
.command("render")
|
|
47
|
+
.argument("<scenario>", "scenario JSON file")
|
|
48
|
+
.description("Edit the last recording again without filming (style, zoom, captions, narration).")
|
|
49
|
+
.option("--no-voice", "skip the voice-over even if the scenario enables it")
|
|
50
|
+
.option("--gif", "also write a GIF")
|
|
51
|
+
.option("--json", "print the result as JSON")
|
|
52
|
+
.action(async (file, o) => {
|
|
53
|
+
try {
|
|
54
|
+
const r = await render(file, { noVoice: !o.voice, gif: o.gif, log });
|
|
55
|
+
if (o.json)
|
|
56
|
+
console.log(JSON.stringify(r, null, 2));
|
|
57
|
+
else
|
|
58
|
+
log(`\n✓ ${r.mp4} (${r.duration.toFixed(1)}s)\n storyboard: ${r.storyboard}`);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
fail(e);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
program
|
|
65
|
+
.command("style")
|
|
66
|
+
.argument("<scenario>", "scenario JSON file")
|
|
67
|
+
.description("Open the style editor in your browser: background, margin, corners and shadow, on a frame of the recording.")
|
|
68
|
+
.option("--port <port>", "port to listen on (default: any free port)")
|
|
69
|
+
.option("--no-open", "print the link instead of opening the browser")
|
|
70
|
+
.action(async (file, o) => {
|
|
71
|
+
try {
|
|
72
|
+
const { startStyleEditor } = await import("./editor/server.js");
|
|
73
|
+
await startStyleEditor(file, { port: o.port ? Number(o.port) : undefined, open: o.open, log });
|
|
74
|
+
log("Press Ctrl+C to stop.");
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
fail(e);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
program
|
|
81
|
+
.command("check")
|
|
82
|
+
.argument("<scenario>", "scenario JSON file")
|
|
83
|
+
.description("Play the scenario quickly without filming: every element exists and the flow completes.")
|
|
84
|
+
.option("--json", "print the report as JSON")
|
|
85
|
+
.action(async (file, o) => {
|
|
86
|
+
try {
|
|
87
|
+
const r = await check(file, { log: o.json ? undefined : log });
|
|
88
|
+
if (o.json)
|
|
89
|
+
console.log(JSON.stringify(r, null, 2));
|
|
90
|
+
else {
|
|
91
|
+
for (const s of r.steps) {
|
|
92
|
+
log(`${s.ok ? "✓" : "✗"} ${s.index + 1}. ${s.step}${s.error && s.error !== "not reached" ? `\n ${s.error}` : ""}`);
|
|
93
|
+
if (s.candidates?.length)
|
|
94
|
+
log(` visible targets: ${s.candidates.slice(0, 15).join(", ")}`);
|
|
95
|
+
}
|
|
96
|
+
if (r.screenshot)
|
|
97
|
+
log(`\nScreenshot where it stopped: ${r.screenshot}`);
|
|
98
|
+
log(r.ok ? "\n✓ The scenario plays to the end." : "\n✗ The scenario stops before the end.");
|
|
99
|
+
}
|
|
100
|
+
if (!r.ok)
|
|
101
|
+
process.exit(2);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
fail(e);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
program
|
|
108
|
+
.command("init")
|
|
109
|
+
.description("Set up Screenplay: download Chromium, add the MCP server to Claude Code, Codex and Cursor, install the guide.")
|
|
110
|
+
.option("--no-browser", "do not download Chromium")
|
|
111
|
+
.option("--no-mcp", "do not add the MCP server to agents")
|
|
112
|
+
.option("--no-skill", "do not install the guide as a skill")
|
|
113
|
+
.option("--dry-run", "show what would change")
|
|
114
|
+
.action(async (o) => {
|
|
115
|
+
try {
|
|
116
|
+
const r = await init({ browser: o.browser, mcp: o.mcp, skill: o.skill, dryRun: o.dryRun, log });
|
|
117
|
+
for (const d of r.done)
|
|
118
|
+
log(`✓ ${d}`);
|
|
119
|
+
for (const s of r.skipped)
|
|
120
|
+
log(`· ${s}`);
|
|
121
|
+
for (const w of r.warnings)
|
|
122
|
+
log(`! ${w}`);
|
|
123
|
+
log("\nReady. Ask your agent: \"Make a screen recording that shows how <feature> works.\"");
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
fail(e);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
program
|
|
130
|
+
.command("login")
|
|
131
|
+
.argument("<url>", "page to sign in on")
|
|
132
|
+
.option("-o, --out <file>", "where to save the session", ".screenplay/auth.json")
|
|
133
|
+
.description("Open a browser to sign in once; the session is saved for scenarios (auth.storageState).")
|
|
134
|
+
.action(async (url, o) => {
|
|
135
|
+
try {
|
|
136
|
+
const file = await login(url, o.out, log);
|
|
137
|
+
log(`✓ Session saved to ${file}. Use it with "auth": { "storageState": "${o.out}" }. Keep it out of git.`);
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
fail(e);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
program
|
|
144
|
+
.command("mcp")
|
|
145
|
+
.description("Run the MCP server on stdio (for agents).")
|
|
146
|
+
.action(async () => {
|
|
147
|
+
const { startMcpServer } = await import("./mcp/server.js");
|
|
148
|
+
await startMcpServer();
|
|
149
|
+
});
|
|
150
|
+
program
|
|
151
|
+
.command("guide")
|
|
152
|
+
.description("Print the guide to writing good scenarios.")
|
|
153
|
+
.action(() => {
|
|
154
|
+
process.stdout.write(guideText());
|
|
155
|
+
});
|
|
156
|
+
program
|
|
157
|
+
.command("schema")
|
|
158
|
+
.description("Print the JSON schema of scenarios.")
|
|
159
|
+
.action(() => {
|
|
160
|
+
process.stdout.write(schemaText());
|
|
161
|
+
});
|
|
162
|
+
program.parseAsync(process.argv).catch(fail);
|
|
163
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type CheckReport } from "./drivers/web.js";
|
|
2
|
+
import { type RenderResult } from "./render/render.js";
|
|
3
|
+
export interface RunOptions {
|
|
4
|
+
/** Force the voice-over off (true) regardless of the scenario. */
|
|
5
|
+
noVoice?: boolean;
|
|
6
|
+
gif?: boolean;
|
|
7
|
+
log?: (msg: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface RecordResult extends RenderResult {
|
|
10
|
+
take: string;
|
|
11
|
+
steps: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function record(file: string, opts?: RunOptions): Promise<RecordResult>;
|
|
14
|
+
export declare function render(file: string, opts?: RunOptions): Promise<RenderResult>;
|
|
15
|
+
export declare function check(file: string, opts?: RunOptions): Promise<CheckReport>;
|
|
16
|
+
export declare function takeExists(file: string): boolean;
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadScenario } from "./scenario/load.js";
|
|
4
|
+
import { runWeb } from "./drivers/web.js";
|
|
5
|
+
import { readingTime } from "./drivers/pace.js";
|
|
6
|
+
import { readTake, writeTake } from "./take.js";
|
|
7
|
+
import { captureInputs } from "./scenario/fingerprint.js";
|
|
8
|
+
import { renderTake } from "./render/render.js";
|
|
9
|
+
import { synthesizeAll } from "./voice/cartesia.js";
|
|
10
|
+
async function narration(loaded, opts) {
|
|
11
|
+
const voice = loaded.scenario.voice;
|
|
12
|
+
if (!voice?.enabled || opts.noVoice)
|
|
13
|
+
return new Map();
|
|
14
|
+
const texts = new Map();
|
|
15
|
+
loaded.scenario.steps.forEach((s, i) => {
|
|
16
|
+
const say = s.say;
|
|
17
|
+
if (say)
|
|
18
|
+
texts.set(i, say);
|
|
19
|
+
});
|
|
20
|
+
if (texts.size === 0)
|
|
21
|
+
return new Map();
|
|
22
|
+
opts.log?.(`Generating narration for ${texts.size} step(s)`);
|
|
23
|
+
return synthesizeAll(texts, voice, path.join(loaded.workDir, "cache", "voice"));
|
|
24
|
+
}
|
|
25
|
+
/** How long each step must last so its narration or caption can be heard or read. */
|
|
26
|
+
function minDurations(loaded, audio) {
|
|
27
|
+
const captions = loaded.scenario.style?.captions ?? true;
|
|
28
|
+
return loaded.scenario.steps.map((s, i) => {
|
|
29
|
+
const a = audio.get(i);
|
|
30
|
+
if (a)
|
|
31
|
+
return a.duration + 0.35;
|
|
32
|
+
const say = s.say;
|
|
33
|
+
return say && captions ? readingTime(say) : 0;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export async function record(file, opts = {}) {
|
|
37
|
+
const loaded = loadScenario(file);
|
|
38
|
+
const audio = await narration(loaded, opts);
|
|
39
|
+
let take;
|
|
40
|
+
if (loaded.scenario.url) {
|
|
41
|
+
const res = await runWeb(loaded, { mode: "record", minDurations: minDurations(loaded, audio), log: opts.log });
|
|
42
|
+
take = res.take;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const { runDesktop } = await import("./drivers/desktop.js");
|
|
46
|
+
take = await runDesktop(loaded, { mode: "record", minDurations: minDurations(loaded, audio), log: opts.log });
|
|
47
|
+
}
|
|
48
|
+
take.inputs = captureInputs(loaded.scenario);
|
|
49
|
+
const takeFile = writeTake(loaded.takeDir, take);
|
|
50
|
+
opts.log?.("Editing the video");
|
|
51
|
+
const result = await renderTake(loaded, take, { audio, gif: opts.gif, log: opts.log });
|
|
52
|
+
return { ...result, take: takeFile, steps: take.steps.length };
|
|
53
|
+
}
|
|
54
|
+
export async function render(file, opts = {}) {
|
|
55
|
+
const loaded = loadScenario(file);
|
|
56
|
+
const take = readTake(loaded.takeDir);
|
|
57
|
+
const audio = await narration(loaded, opts);
|
|
58
|
+
return renderTake(loaded, take, { audio, gif: opts.gif, log: opts.log });
|
|
59
|
+
}
|
|
60
|
+
export async function check(file, opts = {}) {
|
|
61
|
+
const loaded = loadScenario(file);
|
|
62
|
+
if (loaded.scenario.url) {
|
|
63
|
+
const res = await runWeb(loaded, { mode: "check", log: opts.log });
|
|
64
|
+
return res.check;
|
|
65
|
+
}
|
|
66
|
+
const { checkDesktop } = await import("./drivers/desktop.js");
|
|
67
|
+
return checkDesktop(loaded, { log: opts.log });
|
|
68
|
+
}
|
|
69
|
+
export function takeExists(file) {
|
|
70
|
+
const loaded = loadScenario(file, { env: false });
|
|
71
|
+
return fs.existsSync(path.join(loaded.takeDir, "take.json"));
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=core.js.map
|
package/dist/core.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Scenario } from "../scenario/schema.js";
|
|
2
|
+
import type { Box, Point, Take, TakeStep } from "../take.js";
|
|
3
|
+
/**
|
|
4
|
+
* The edit of a take: how output time maps to recording time, where the
|
|
5
|
+
* camera looks, where the cursor is, and which overlays show when.
|
|
6
|
+
* Pure data, computed once, then drawn frame by frame by the compositor.
|
|
7
|
+
*/
|
|
8
|
+
export type Piece = {
|
|
9
|
+
kind: "capture";
|
|
10
|
+
o0: number;
|
|
11
|
+
o1: number;
|
|
12
|
+
s0: number;
|
|
13
|
+
s1: number;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "card";
|
|
16
|
+
o0: number;
|
|
17
|
+
o1: number;
|
|
18
|
+
step: TakeStep;
|
|
19
|
+
card: CardContent;
|
|
20
|
+
};
|
|
21
|
+
export interface CardContent {
|
|
22
|
+
kind: "title" | "end";
|
|
23
|
+
title: string;
|
|
24
|
+
subtitle?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface Camera {
|
|
27
|
+
z: number;
|
|
28
|
+
cx: number;
|
|
29
|
+
cy: number;
|
|
30
|
+
}
|
|
31
|
+
export interface Caption {
|
|
32
|
+
o0: number;
|
|
33
|
+
o1: number;
|
|
34
|
+
text: string;
|
|
35
|
+
}
|
|
36
|
+
export interface Annotation {
|
|
37
|
+
kind: "callout" | "highlight";
|
|
38
|
+
o0: number;
|
|
39
|
+
o1: number;
|
|
40
|
+
box: Box;
|
|
41
|
+
text?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface VoiceClip {
|
|
44
|
+
file: string;
|
|
45
|
+
at: number;
|
|
46
|
+
duration: number;
|
|
47
|
+
}
|
|
48
|
+
export interface Plan {
|
|
49
|
+
fps: number;
|
|
50
|
+
duration: number;
|
|
51
|
+
frameCount: number;
|
|
52
|
+
pieces: Piece[];
|
|
53
|
+
/** Camera state for each output frame. */
|
|
54
|
+
cameras: Camera[];
|
|
55
|
+
captions: Caption[];
|
|
56
|
+
annotations: Annotation[];
|
|
57
|
+
/** Output times of clicks. */
|
|
58
|
+
clicks: number[];
|
|
59
|
+
voice: VoiceClip[];
|
|
60
|
+
/** Output time span of each step. */
|
|
61
|
+
stepTimes: Array<{
|
|
62
|
+
o0: number;
|
|
63
|
+
o1: number;
|
|
64
|
+
oAction?: number;
|
|
65
|
+
}>;
|
|
66
|
+
movements: Movement[];
|
|
67
|
+
typing: Array<[number, number]>;
|
|
68
|
+
width: number;
|
|
69
|
+
height: number;
|
|
70
|
+
}
|
|
71
|
+
export interface Movement {
|
|
72
|
+
s0: number;
|
|
73
|
+
s1: number;
|
|
74
|
+
from: Point;
|
|
75
|
+
to: Point;
|
|
76
|
+
/** Side of the arc (+1/-1). */
|
|
77
|
+
bend: number;
|
|
78
|
+
}
|
|
79
|
+
export interface PlanOptions {
|
|
80
|
+
fps: number;
|
|
81
|
+
audio: Map<number, {
|
|
82
|
+
file: string;
|
|
83
|
+
duration: number;
|
|
84
|
+
}>;
|
|
85
|
+
autoZoom: boolean;
|
|
86
|
+
maxZoom: number;
|
|
87
|
+
captions: boolean;
|
|
88
|
+
}
|
|
89
|
+
export declare const OVERLAY_DELAY = 0.55;
|
|
90
|
+
export declare function planTake(take: Take, scenario: Scenario, opts: PlanOptions): Plan;
|
|
91
|
+
/** Zoom that frames a box with comfortable context. */
|
|
92
|
+
export declare function autoZoom(box: Box, W: number, H: number, maxZoom: number): number;
|
|
93
|
+
/** Where the cursor is at recording time s (minimum-jerk motion along a gentle arc). */
|
|
94
|
+
export declare function cursorAt(movements: Movement[], start: Point, s: number): Point;
|
|
95
|
+
/** Locate output time o: the piece, and the recording time when it is a capture. */
|
|
96
|
+
export declare function locate(plan: Plan, o: number): {
|
|
97
|
+
piece: Piece;
|
|
98
|
+
index: number;
|
|
99
|
+
s?: number;
|
|
100
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { cardDuration } from "../drivers/pace.js";
|
|
2
|
+
const WAIT_KEEP = 0.9; // seconds of a wait shown at normal speed
|
|
3
|
+
const WAIT_RATE = 8; // the rest of the wait plays this many times faster
|
|
4
|
+
const WAIT_MAX = 2.4; // a wait never lasts longer than this in the video
|
|
5
|
+
export const OVERLAY_DELAY = 0.55; // callouts and highlights appear after the camera framed them
|
|
6
|
+
export function planTake(take, scenario, opts) {
|
|
7
|
+
const steps = take.steps;
|
|
8
|
+
const pieces = [];
|
|
9
|
+
let op = 0; // output pointer
|
|
10
|
+
let sp = 0; // source pointer
|
|
11
|
+
const emitCapture = (sEnd) => {
|
|
12
|
+
if (sEnd <= sp + 1e-6)
|
|
13
|
+
return;
|
|
14
|
+
const waits = mergeSpans(steps.flatMap((s) => s.waits)).filter(([a, b]) => b > sp && a < sEnd);
|
|
15
|
+
let cur = sp;
|
|
16
|
+
for (const [a0, b0] of waits) {
|
|
17
|
+
const a = Math.max(a0, cur);
|
|
18
|
+
const b = Math.min(b0, sEnd);
|
|
19
|
+
if (a > cur) {
|
|
20
|
+
pieces.push({ kind: "capture", o0: op, o1: op + (a - cur), s0: cur, s1: a });
|
|
21
|
+
op += a - cur;
|
|
22
|
+
}
|
|
23
|
+
if (b > a) {
|
|
24
|
+
const len = b - a;
|
|
25
|
+
const out = len <= WAIT_KEEP ? len : Math.min(WAIT_MAX, WAIT_KEEP + (len - WAIT_KEEP) / WAIT_RATE);
|
|
26
|
+
pieces.push({ kind: "capture", o0: op, o1: op + out, s0: a, s1: b });
|
|
27
|
+
op += out;
|
|
28
|
+
}
|
|
29
|
+
cur = Math.max(cur, b);
|
|
30
|
+
}
|
|
31
|
+
if (sEnd > cur) {
|
|
32
|
+
pieces.push({ kind: "capture", o0: op, o1: op + (sEnd - cur), s0: cur, s1: sEnd });
|
|
33
|
+
op += sEnd - cur;
|
|
34
|
+
}
|
|
35
|
+
sp = sEnd;
|
|
36
|
+
};
|
|
37
|
+
const srcToOut = (s) => {
|
|
38
|
+
for (const p of pieces) {
|
|
39
|
+
if (p.kind === "capture" && s >= p.s0 - 1e-9 && s <= p.s1 + 1e-9) {
|
|
40
|
+
if (p.s1 === p.s0)
|
|
41
|
+
return p.o0;
|
|
42
|
+
return p.o0 + ((s - p.s0) / (p.s1 - p.s0)) * (p.o1 - p.o0);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return op;
|
|
46
|
+
};
|
|
47
|
+
const stepTimes = [];
|
|
48
|
+
const voice = [];
|
|
49
|
+
for (const st of steps) {
|
|
50
|
+
const audio = opts.audio.get(st.index);
|
|
51
|
+
if (st.card) {
|
|
52
|
+
// An opening card starts the video: drop the still lead-in recorded before it.
|
|
53
|
+
if (pieces.length === 0)
|
|
54
|
+
sp = st.start;
|
|
55
|
+
emitCapture(st.start);
|
|
56
|
+
// Card text and length come from the scenario as it is now: they can change without filming again.
|
|
57
|
+
const sc = scenario.steps[st.index];
|
|
58
|
+
const title = (st.card.kind === "title" ? sc.title : sc.end) ?? st.card.title;
|
|
59
|
+
const base = sc.duration ?? cardDuration(title, sc.subtitle);
|
|
60
|
+
const dur = Math.max(base, audio ? audio.duration + 0.6 : 0);
|
|
61
|
+
pieces.push({ kind: "card", o0: op, o1: op + dur, step: st, card: { kind: st.card.kind, title, subtitle: sc.subtitle } });
|
|
62
|
+
stepTimes[st.index] = { o0: op, o1: op + dur };
|
|
63
|
+
if (audio)
|
|
64
|
+
voice.push({ file: audio.file, at: op + 0.25, duration: audio.duration });
|
|
65
|
+
op += dur;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
emitCapture(st.start);
|
|
69
|
+
const o0 = op;
|
|
70
|
+
emitCapture(st.end);
|
|
71
|
+
// Narration longer than the step: hold the last image until it ends.
|
|
72
|
+
if (audio && op - o0 < audio.duration + 0.35) {
|
|
73
|
+
const extra = audio.duration + 0.35 - (op - o0);
|
|
74
|
+
pieces.push({ kind: "capture", o0: op, o1: op + extra, s0: st.end, s1: st.end });
|
|
75
|
+
op += extra;
|
|
76
|
+
}
|
|
77
|
+
stepTimes[st.index] = { o0, o1: op, oAction: st.actionAt !== undefined ? srcToOut(st.actionAt) : undefined };
|
|
78
|
+
if (audio)
|
|
79
|
+
voice.push({ file: audio.file, at: o0 + 0.05, duration: audio.duration });
|
|
80
|
+
}
|
|
81
|
+
emitCapture(take.duration);
|
|
82
|
+
const duration = op;
|
|
83
|
+
const frameCount = Math.max(1, Math.ceil(duration * opts.fps));
|
|
84
|
+
// ---- cursor movements (recording time) ---------------------------------
|
|
85
|
+
const movements = [];
|
|
86
|
+
let prev = take.cursorStart;
|
|
87
|
+
let bend = 1;
|
|
88
|
+
for (const st of steps) {
|
|
89
|
+
if (!st.point)
|
|
90
|
+
continue;
|
|
91
|
+
const s0 = st.travelStart ?? st.actionAt ?? st.start;
|
|
92
|
+
const s1 = st.actionAt ?? s0 + 0.5;
|
|
93
|
+
if (Math.hypot(st.point.x - prev.x, st.point.y - prev.y) > 2) {
|
|
94
|
+
movements.push({ s0, s1: Math.max(s1, s0 + 0.05), from: prev, to: st.point, bend });
|
|
95
|
+
bend = -bend;
|
|
96
|
+
}
|
|
97
|
+
prev = st.point;
|
|
98
|
+
}
|
|
99
|
+
const clicks = steps
|
|
100
|
+
.filter((s) => s.actionAt !== undefined && (s.kind === "click" || s.kind === "select" || (s.kind === "type" && s.point)))
|
|
101
|
+
.map((s) => srcToOut(s.actionAt));
|
|
102
|
+
const typing = steps
|
|
103
|
+
.filter((s) => s.kind === "type" && s.actionAt !== undefined && s.actionEnd !== undefined)
|
|
104
|
+
.map((s) => [srcToOut(s.actionAt), srcToOut(s.actionEnd)]);
|
|
105
|
+
// ---- overlays -----------------------------------------------------------
|
|
106
|
+
const captions = [];
|
|
107
|
+
const annotations = [];
|
|
108
|
+
for (const st of steps) {
|
|
109
|
+
const sc = scenario.steps[st.index];
|
|
110
|
+
const t = stepTimes[st.index];
|
|
111
|
+
if (!t)
|
|
112
|
+
continue;
|
|
113
|
+
if (opts.captions && sc.say && !st.card) {
|
|
114
|
+
const audio = opts.audio.get(st.index);
|
|
115
|
+
captions.push({ o0: t.o0 + 0.05, o1: Math.max(t.o1, t.o0 + (audio?.duration ?? 0) + 0.2), text: sc.say });
|
|
116
|
+
}
|
|
117
|
+
if ((st.kind === "callout" || st.kind === "highlight") && st.box) {
|
|
118
|
+
// Appear once the camera has had time to frame the element.
|
|
119
|
+
annotations.push({ kind: st.kind, o0: Math.min(t.o0 + OVERLAY_DELAY, t.o1 - 0.5), o1: t.o1, box: st.box, text: sc.text });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ---- camera -------------------------------------------------------------
|
|
123
|
+
const W = take.width;
|
|
124
|
+
const H = take.height;
|
|
125
|
+
const shots = buildShots(take, scenario, stepTimes, srcToOut, opts);
|
|
126
|
+
const cameras = simulateCamera(shots, frameCount, opts.fps, W, H);
|
|
127
|
+
return {
|
|
128
|
+
fps: opts.fps,
|
|
129
|
+
duration,
|
|
130
|
+
frameCount,
|
|
131
|
+
pieces,
|
|
132
|
+
cameras,
|
|
133
|
+
captions,
|
|
134
|
+
annotations,
|
|
135
|
+
clicks,
|
|
136
|
+
voice,
|
|
137
|
+
stepTimes,
|
|
138
|
+
movements,
|
|
139
|
+
typing,
|
|
140
|
+
width: W,
|
|
141
|
+
height: H,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function buildShots(take, scenario, stepTimes, srcToOut, opts) {
|
|
145
|
+
const W = take.width;
|
|
146
|
+
const H = take.height;
|
|
147
|
+
const shots = [];
|
|
148
|
+
for (const st of take.steps) {
|
|
149
|
+
if (!st.box || st.card)
|
|
150
|
+
continue;
|
|
151
|
+
const sc = scenario.steps[st.index];
|
|
152
|
+
if (sc.zoom === false)
|
|
153
|
+
continue;
|
|
154
|
+
const forced = typeof sc.zoom === "number" ? sc.zoom : undefined;
|
|
155
|
+
const isOverlay = st.kind === "callout" || st.kind === "highlight" || st.kind === "zoomTo";
|
|
156
|
+
if (forced === undefined && !opts.autoZoom && st.kind !== "zoomTo")
|
|
157
|
+
continue;
|
|
158
|
+
// Typing happens at the start of a field: frame that part rather than a very wide input.
|
|
159
|
+
const box = st.kind === "type" && st.typed !== undefined && st.box.width > W / 3
|
|
160
|
+
? { ...st.box, width: Math.min(st.box.width, Math.max(260, st.typed.length * 7.5 + 60)) }
|
|
161
|
+
: st.box;
|
|
162
|
+
let z = forced ?? autoZoom(box, W, H, isOverlay ? Math.min(opts.maxZoom, 1.6) : opts.maxZoom);
|
|
163
|
+
if (st.kind === "zoomTo" && forced === undefined)
|
|
164
|
+
z = autoZoom(box, W, H, opts.maxZoom);
|
|
165
|
+
if (z <= 1.02)
|
|
166
|
+
continue;
|
|
167
|
+
const t = stepTimes[st.index];
|
|
168
|
+
const begin = isOverlay ? t.o0 - 0.1 : srcToOut(st.travelStart ?? st.actionAt ?? st.start) - 0.12;
|
|
169
|
+
let end = t.o1;
|
|
170
|
+
if (st.navigated && st.actionAt !== undefined)
|
|
171
|
+
end = Math.min(end, srcToOut(st.actionAt) + 0.45);
|
|
172
|
+
shots.push({ o0: Math.max(0, begin), o1: end, z, cx: box.x + box.width / 2, cy: box.y + box.height / 2 });
|
|
173
|
+
}
|
|
174
|
+
// Stay zoomed between close shots instead of bouncing out and back in.
|
|
175
|
+
for (let i = 0; i + 1 < shots.length; i++) {
|
|
176
|
+
if (shots[i + 1].o0 - shots[i].o1 < 1.3)
|
|
177
|
+
shots[i].o1 = shots[i + 1].o0;
|
|
178
|
+
}
|
|
179
|
+
return shots;
|
|
180
|
+
}
|
|
181
|
+
/** Zoom that frames a box with comfortable context. */
|
|
182
|
+
export function autoZoom(box, W, H, maxZoom) {
|
|
183
|
+
const wantW = Math.max(box.width * 2.4, W / maxZoom);
|
|
184
|
+
const wantH = Math.max(box.height * 3.2, H / maxZoom);
|
|
185
|
+
return clampNum(Math.min(W / wantW, H / wantH), 1, maxZoom);
|
|
186
|
+
}
|
|
187
|
+
/** Critically damped spring on log(zoom) and the center: smooth starts, no overshoot. */
|
|
188
|
+
function simulateCamera(shots, frameCount, fps, W, H) {
|
|
189
|
+
const omega = 5.2;
|
|
190
|
+
const sub = 4;
|
|
191
|
+
const dt = 1 / fps / sub;
|
|
192
|
+
let lz = 0, vz = 0, cx = W / 2, vx = 0, cy = H / 2, vy = 0;
|
|
193
|
+
const out = [];
|
|
194
|
+
let si = 0;
|
|
195
|
+
for (let f = 0; f < frameCount; f++) {
|
|
196
|
+
const o = f / fps;
|
|
197
|
+
while (si < shots.length && shots[si].o1 < o)
|
|
198
|
+
si++;
|
|
199
|
+
const shot = si < shots.length && shots[si].o0 <= o ? shots[si] : undefined;
|
|
200
|
+
const tz = shot ? Math.log(shot.z) : 0;
|
|
201
|
+
const z = Math.exp(tz);
|
|
202
|
+
const tx = shot ? clampCenter(shot.cx, W, z) : W / 2;
|
|
203
|
+
const ty = shot ? clampCenter(shot.cy, H, z) : H / 2;
|
|
204
|
+
for (let k = 0; k < sub; k++) {
|
|
205
|
+
vz += (omega * omega * (tz - lz) - 2 * omega * vz) * dt;
|
|
206
|
+
lz += vz * dt;
|
|
207
|
+
vx += (omega * omega * (tx - cx) - 2 * omega * vx) * dt;
|
|
208
|
+
cx += vx * dt;
|
|
209
|
+
vy += (omega * omega * (ty - cy) - 2 * omega * vy) * dt;
|
|
210
|
+
cy += vy * dt;
|
|
211
|
+
}
|
|
212
|
+
const zz = Math.max(1, Math.exp(lz));
|
|
213
|
+
out.push({ z: zz, cx: clampCenter(cx, W, zz), cy: clampCenter(cy, H, zz) });
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
function clampCenter(c, size, z) {
|
|
218
|
+
const half = size / (2 * z);
|
|
219
|
+
return clampNum(c, half, size - half);
|
|
220
|
+
}
|
|
221
|
+
function clampNum(v, lo, hi) {
|
|
222
|
+
return Math.min(hi, Math.max(lo, v));
|
|
223
|
+
}
|
|
224
|
+
function mergeSpans(spans) {
|
|
225
|
+
const sorted = spans.filter(([a, b]) => b > a).sort((x, y) => x[0] - y[0]);
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const [a, b] of sorted) {
|
|
228
|
+
const last = out[out.length - 1];
|
|
229
|
+
if (last && a <= last[1])
|
|
230
|
+
last[1] = Math.max(last[1], b);
|
|
231
|
+
else
|
|
232
|
+
out.push([a, b]);
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
/** Where the cursor is at recording time s (minimum-jerk motion along a gentle arc). */
|
|
237
|
+
export function cursorAt(movements, start, s) {
|
|
238
|
+
let pos = start;
|
|
239
|
+
for (const m of movements) {
|
|
240
|
+
if (s < m.s0)
|
|
241
|
+
break;
|
|
242
|
+
if (s >= m.s1) {
|
|
243
|
+
pos = m.to;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const u = (s - m.s0) / (m.s1 - m.s0);
|
|
247
|
+
const e = u * u * u * (10 - 15 * u + 6 * u * u);
|
|
248
|
+
const dx = m.to.x - m.from.x;
|
|
249
|
+
const dy = m.to.y - m.from.y;
|
|
250
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
251
|
+
const arc = Math.min(80, len * 0.12) * m.bend;
|
|
252
|
+
const ctrl = { x: (m.from.x + m.to.x) / 2 - (dy / len) * arc, y: (m.from.y + m.to.y) / 2 + (dx / len) * arc };
|
|
253
|
+
const a = 1 - e;
|
|
254
|
+
return {
|
|
255
|
+
x: a * a * m.from.x + 2 * a * e * ctrl.x + e * e * m.to.x,
|
|
256
|
+
y: a * a * m.from.y + 2 * a * e * ctrl.y + e * e * m.to.y,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return pos;
|
|
260
|
+
}
|
|
261
|
+
/** Locate output time o: the piece, and the recording time when it is a capture. */
|
|
262
|
+
export function locate(plan, o) {
|
|
263
|
+
const { pieces } = plan;
|
|
264
|
+
let i = pieces.findIndex((p) => o >= p.o0 && o < p.o1);
|
|
265
|
+
if (i < 0)
|
|
266
|
+
i = pieces.length - 1;
|
|
267
|
+
const piece = pieces[i];
|
|
268
|
+
if (piece.kind === "capture") {
|
|
269
|
+
const u = piece.o1 > piece.o0 ? (o - piece.o0) / (piece.o1 - piece.o0) : 0;
|
|
270
|
+
return { piece, index: i, s: piece.s0 + Math.min(1, Math.max(0, u)) * (piece.s1 - piece.s0) };
|
|
271
|
+
}
|
|
272
|
+
return { piece, index: i };
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=plan.js.map
|