@tandryukha/aidemo 0.17.0 → 0.18.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/README.md +12 -5
- package/bin/aidemo.mjs +14 -0
- package/docs/AUTHORING.md +8 -3
- package/package.json +5 -2
- package/src/browser.ts +151 -0
- package/src/distribute.ts +53 -3
- package/src/mcp/server.ts +15 -1
- package/src/recorder.ts +5 -1
package/README.md
CHANGED
|
@@ -154,7 +154,7 @@ A bundled fixture store (search → results → cart → checkout) that renders
|
|
|
154
154
|
finished demo with zero external dependencies:
|
|
155
155
|
|
|
156
156
|
```bash
|
|
157
|
-
npm install # Node
|
|
157
|
+
npm install # Node 22+, system Chrome, ffmpeg on PATH
|
|
158
158
|
node examples/local-demo/serve.mjs # terminal 1: fixture on :8787
|
|
159
159
|
node bin/aidemo.mjs render examples/local-demo --headless # terminal 2
|
|
160
160
|
open examples/local-demo/output/final-demo.mp4 # xdg-open on Linux, start on Windows
|
|
@@ -164,7 +164,10 @@ Voice/captions need `OPENAI_API_KEY` in `.env` — **or** `AIDEMO_TTS_PROVIDER=l
|
|
|
164
164
|
(no key, offline), **or** `OPENAI_BASE_URL` at a local server. See
|
|
165
165
|
[docs/LOCAL_MODELS.md](docs/LOCAL_MODELS.md). No Playwright browser download is
|
|
166
166
|
needed — the engine uses your system Chrome (`channel: "chrome"`; see
|
|
167
|
-
[Setup](#setup) to use a different browser).
|
|
167
|
+
[Setup](#setup) to use a different browser). The one Playwright download is
|
|
168
|
+
its ~2 MB ffmpeg helper, which encodes the take: the first take installs it
|
|
169
|
+
automatically (`AIDEMO_NO_AUTO_INSTALL=1` to manage it yourself with
|
|
170
|
+
`npx playwright install ffmpeg`).
|
|
168
171
|
|
|
169
172
|
[](https://github.com/tandryukha/aidemo/releases/download/v0.3.0/quickstart-demo.mp4)
|
|
170
173
|
|
|
@@ -251,7 +254,9 @@ Codex).
|
|
|
251
254
|
|
|
252
255
|
## Setup
|
|
253
256
|
|
|
254
|
-
Prereqs: **Node
|
|
257
|
+
Prereqs: **Node 22+**, **Google Chrome**, **ffmpeg + ffprobe** on `PATH`. The
|
|
258
|
+
first take also fetches Playwright's ~2 MB ffmpeg helper, which encodes the
|
|
259
|
+
recording (once per Playwright version; `AIDEMO_NO_AUTO_INSTALL=1` opts out).
|
|
255
260
|
Developed and tested on macOS; Linux works for the default (Playwright) capture
|
|
256
261
|
and `--capture obs`. Run `aidemo doctor` to check your setup.
|
|
257
262
|
|
|
@@ -294,9 +299,11 @@ demos/<name>/ ← your working area (untracked; scaffold with `aidemo i
|
|
|
294
299
|
`postinstall`/`preinstall`).
|
|
295
300
|
- **Network is user-initiated only:** `api.openai.com` (only `voice`/`captions`,
|
|
296
301
|
your key — or a local server via `OPENAI_BASE_URL`), `api.elevenlabs.io` (opt-in),
|
|
297
|
-
`huggingface.co` (download-only, once, for `AIDEMO_TTS_PROVIDER=local`),
|
|
302
|
+
`huggingface.co` (download-only, once, for `AIDEMO_TTS_PROVIDER=local`),
|
|
303
|
+
`cdn.playwright.dev` (download-only, once per Playwright version: the ~2 MB
|
|
304
|
+
ffmpeg helper that encodes the take; off with `AIDEMO_NO_AUTO_INSTALL=1`), and
|
|
298
305
|
`github.com` (your own `gh`, for `aidemo feedback`). Recording/composing are
|
|
299
|
-
fully local. The MCP server is **stdio-only** — no listener.
|
|
306
|
+
otherwise fully local. The MCP server is **stdio-only** — no listener.
|
|
300
307
|
- **Small, auditable surface:** ~20 source files, 7 runtime deps, MIT. Pin an
|
|
301
308
|
immutable ref if you're wary of the moving `#stable` tag:
|
|
302
309
|
`npx -y github:tandryukha/aidemo#v0.14.0` (any released tag).
|
package/bin/aidemo.mjs
CHANGED
|
@@ -6,6 +6,20 @@ import { createRequire } from "node:module";
|
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
8
|
|
|
9
|
+
// Fail fast on an unsupported Node with the fix, instead of an arbitrary crash
|
|
10
|
+
// deep inside a dependency (openai 7 needs Node 22). Node 20 reached
|
|
11
|
+
// end-of-life on 2026-04-30. Keep in sync with package.json "engines".
|
|
12
|
+
const MIN_NODE_MAJOR = 22;
|
|
13
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
14
|
+
if (nodeMajor < MIN_NODE_MAJOR) {
|
|
15
|
+
console.error(
|
|
16
|
+
`aidemo: Node.js ${MIN_NODE_MAJOR}+ is required (found ${process.version}). ` +
|
|
17
|
+
`Install Node ${MIN_NODE_MAJOR} LTS or newer (nvm install ${MIN_NODE_MAJOR}; in CI, ` +
|
|
18
|
+
`actions/setup-node with node-version: ${MIN_NODE_MAJOR}) and re-run.`
|
|
19
|
+
);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
24
|
const cli = resolve(here, "..", "src", "cli.ts");
|
|
11
25
|
const require = createRequire(import.meta.url);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -1256,9 +1256,14 @@ CLI. If nothing came up, skip this.
|
|
|
1256
1256
|
…) is set; the executable wins. Use `chromium` when a managed Chrome closes
|
|
1257
1257
|
the automated session after a few seconds ("Target page, context or browser
|
|
1258
1258
|
has been closed").
|
|
1259
|
-
- `doctor` checks Node, ffmpeg, the browser in use,
|
|
1260
|
-
(and flags LLM-only servers like Ollama, which
|
|
1261
|
-
`OPENAI_BASE_URL` at a speech server such as
|
|
1259
|
+
- `doctor` checks Node (22+), ffmpeg, the browser in use, Playwright's ffmpeg
|
|
1260
|
+
helper, the TTS/STT endpoint (and flags LLM-only servers like Ollama, which
|
|
1261
|
+
have no audio endpoints — point `OPENAI_BASE_URL` at a speech server such as
|
|
1262
|
+
speaches instead).
|
|
1263
|
+
- The first take installs Playwright's ~2 MB ffmpeg helper (it encodes the
|
|
1264
|
+
recording; once per Playwright version) and logs `installing Playwright's
|
|
1265
|
+
ffmpeg helper`. If that download is blocked, the error names the manual
|
|
1266
|
+
command; `AIDEMO_NO_AUTO_INSTALL=1` turns the download off.
|
|
1262
1267
|
|
|
1263
1268
|
## Import from Playwright (trace or test file)
|
|
1264
1269
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tandryukha/aidemo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "AI Demo Engine — turn a storyboard into a narrated, captioned product demo video",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"aidemo": "bin/aidemo.mjs"
|
|
9
9
|
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22"
|
|
12
|
+
},
|
|
10
13
|
"publishConfig": {
|
|
11
14
|
"access": "public"
|
|
12
15
|
},
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
"dependencies": {
|
|
38
41
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
39
42
|
"commander": "^12.1.0",
|
|
40
|
-
"openai": "^
|
|
43
|
+
"openai": "^7.47.0",
|
|
41
44
|
"playwright": "^1.55.1",
|
|
42
45
|
"re2js": "2.9.1",
|
|
43
46
|
"tsx": "^4.23.1",
|
package/src/browser.ts
CHANGED
|
@@ -8,10 +8,147 @@
|
|
|
8
8
|
* the options are exactly the engine's historical `{ channel: "chrome", ... }`.
|
|
9
9
|
*/
|
|
10
10
|
import { chromium, type Browser, type BrowserContext, type LaunchOptions } from "playwright";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
11
16
|
import { BrowserSetupError, chromeLaunchOptions, type ChromeLaunchTarget } from "./config.js";
|
|
17
|
+
import { log, ok } from "./util.js";
|
|
12
18
|
|
|
13
19
|
type PersistentOptions = NonNullable<Parameters<typeof chromium.launchPersistentContext>[1]>;
|
|
14
20
|
|
|
21
|
+
/** Executable name of Playwright's ffmpeg helper, per platform. */
|
|
22
|
+
const FFMPEG_HELPER_EXE: Partial<Record<NodeJS.Platform, string>> = {
|
|
23
|
+
linux: "ffmpeg-linux",
|
|
24
|
+
darwin: "ffmpeg-mac",
|
|
25
|
+
win32: "ffmpeg-win64.exe",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Playwright's own ffmpeg helper: the encoder recordVideo writes every take
|
|
30
|
+
* with. It is neither the system ffmpeg (compose) nor a browser — a ~2 MB
|
|
31
|
+
* download per Playwright ffmpeg *revision*, and no npm install fetches it
|
|
32
|
+
* (playwright has no install script). So a fresh npx install, or a Playwright
|
|
33
|
+
* minor that bumps the revision (1.64: 1011 → 1013, with VP9 encoding), leaves
|
|
34
|
+
* it missing and the take fails at launch. Mirrors playwright-core's
|
|
35
|
+
* documented lookup (PLAYWRIGHT_BROWSERS_PATH, else the per-OS
|
|
36
|
+
* ms-playwright cache dir). `install` is the exact command for THIS engine's
|
|
37
|
+
* Playwright. null when the location can't be determined.
|
|
38
|
+
*/
|
|
39
|
+
export function recorderFfmpeg(): {
|
|
40
|
+
path: string;
|
|
41
|
+
revision: string;
|
|
42
|
+
/** This engine's playwright/cli.js — runs Playwright's own installer. */
|
|
43
|
+
cli: string;
|
|
44
|
+
install: string;
|
|
45
|
+
} | null {
|
|
46
|
+
try {
|
|
47
|
+
const exe = FFMPEG_HELPER_EXE[process.platform];
|
|
48
|
+
if (!exe) return null;
|
|
49
|
+
const req = createRequire(import.meta.url);
|
|
50
|
+
const playwrightPkg = req.resolve("playwright/package.json");
|
|
51
|
+
// playwright-core is playwright's dependency: resolve it from there.
|
|
52
|
+
const corePkg = createRequire(playwrightPkg).resolve("playwright-core/package.json");
|
|
53
|
+
const coreDir = dirname(corePkg);
|
|
54
|
+
const browsers = JSON.parse(readFileSync(join(coreDir, "browsers.json"), "utf8")) as {
|
|
55
|
+
browsers: Array<{ name: string; revision: string }>;
|
|
56
|
+
};
|
|
57
|
+
const revision = browsers.browsers.find((b) => b.name === "ffmpeg")?.revision;
|
|
58
|
+
if (!revision) return null;
|
|
59
|
+
const env = process.env.PLAYWRIGHT_BROWSERS_PATH;
|
|
60
|
+
let registry =
|
|
61
|
+
env === "0"
|
|
62
|
+
? join(coreDir, ".local-browsers")
|
|
63
|
+
: env || join(defaultPlaywrightCacheDir(), "ms-playwright");
|
|
64
|
+
if (!isAbsolute(registry)) registry = resolve(process.env.INIT_CWD || process.cwd(), registry);
|
|
65
|
+
const cli = join(dirname(playwrightPkg), "cli.js");
|
|
66
|
+
return {
|
|
67
|
+
path: join(registry, `ffmpeg-${revision}`, exe),
|
|
68
|
+
revision,
|
|
69
|
+
cli,
|
|
70
|
+
install: `node "${cli}" install ffmpeg`,
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function defaultPlaywrightCacheDir(): string {
|
|
78
|
+
if (process.platform === "darwin") return join(homedir(), "Library", "Caches");
|
|
79
|
+
if (process.platform === "win32") return process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
|
|
80
|
+
return process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** AIDEMO_NO_AUTO_INSTALL=1: never download the helper (air-gapped / managed caches). */
|
|
84
|
+
export const recorderAutoInstall = (): boolean =>
|
|
85
|
+
!/^(1|true|yes)$/i.test(process.env.AIDEMO_NO_AUTO_INSTALL?.trim() ?? "");
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Before a built-in capture launches, make sure Playwright's ffmpeg helper is
|
|
89
|
+
* there, installing it with Playwright's own installer when it is not, so a
|
|
90
|
+
* fresh install records out of the box (the GitHub Action runs the same step).
|
|
91
|
+
* ~2 MB from Playwright's CDN, once per helper revision.
|
|
92
|
+
* PLAYWRIGHT_SKIP_BROWSER_GC=1 keeps the installer from pruning browsers other
|
|
93
|
+
* Playwright projects on the machine installed. With AIDEMO_NO_AUTO_INSTALL=1,
|
|
94
|
+
* an unknown layout or an already-installed helper this is a no-op (a missing
|
|
95
|
+
* helper then fails the launch with explainLaunchError's install command).
|
|
96
|
+
*/
|
|
97
|
+
export async function ensureRecorderFfmpeg(): Promise<void> {
|
|
98
|
+
const helper = recorderFfmpeg();
|
|
99
|
+
if (!helper || existsSync(helper.path) || !recorderAutoInstall()) return;
|
|
100
|
+
log(
|
|
101
|
+
`installing Playwright's ffmpeg helper (revision ${helper.revision}, ~2 MB, once per ` +
|
|
102
|
+
`Playwright version; it encodes the take — AIDEMO_NO_AUTO_INSTALL=1 skips this)`
|
|
103
|
+
);
|
|
104
|
+
let output = "";
|
|
105
|
+
const keep = (d: Buffer | string): void => {
|
|
106
|
+
output = (output + d.toString()).slice(-4000);
|
|
107
|
+
};
|
|
108
|
+
const code = await new Promise<number | null>((done) => {
|
|
109
|
+
const child = spawn(process.execPath, [helper.cli, "install", "ffmpeg"], {
|
|
110
|
+
env: { ...process.env, PLAYWRIGHT_SKIP_BROWSER_GC: "1" },
|
|
111
|
+
// Off stdout: under `aidemo mcp`, stdout is the JSON-RPC channel.
|
|
112
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
113
|
+
});
|
|
114
|
+
child.stdout.on("data", keep);
|
|
115
|
+
child.stderr.on("data", keep);
|
|
116
|
+
const timer = setTimeout(() => child.kill(), 5 * 60_000);
|
|
117
|
+
child.on("error", (err) => {
|
|
118
|
+
keep(String(err));
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
done(null);
|
|
121
|
+
});
|
|
122
|
+
child.on("close", (c) => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
done(c);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
if (code !== 0 || !existsSync(helper.path)) {
|
|
128
|
+
// The installer prints the cause as "Error: …" between stack frames and an
|
|
129
|
+
// error-object dump; surface that line, not the frames.
|
|
130
|
+
const lines = output
|
|
131
|
+
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
132
|
+
.split("\n")
|
|
133
|
+
.map((l) => l.trim())
|
|
134
|
+
.filter((l) => l && !l.startsWith("at ") && !/^[{}]$/.test(l));
|
|
135
|
+
// Root cause first ("connect ECONNREFUSED …", "server returned code 403"),
|
|
136
|
+
// then the installer's own summary line.
|
|
137
|
+
const errors = [...new Set(lines.filter((l) => /error/i.test(l)))];
|
|
138
|
+
const why = (errors.length ? errors.slice(0, 2) : lines.slice(-1))
|
|
139
|
+
.map((l) => l.slice(0, 200))
|
|
140
|
+
.join(" — ");
|
|
141
|
+
throw new BrowserSetupError(
|
|
142
|
+
`could not install Playwright's ffmpeg helper (revision ${helper.revision}), which the ` +
|
|
143
|
+
`take is recorded with${why ? `: ${why}` : ""}. Install it yourself: ${helper.install}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
ok(`Playwright ffmpeg helper installed (revision ${helper.revision})`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A launch that failed on Playwright's missing ffmpeg helper, not on the browser. */
|
|
150
|
+
const FFMPEG_HELPER_RE = /playwright install ffmpeg|[\\/]ffmpeg-\d+[\\/]ffmpeg/i;
|
|
151
|
+
|
|
15
152
|
/** Human-readable description of the configured browser, for logs/doctor. */
|
|
16
153
|
export function describeChromeTarget(target: ChromeLaunchTarget = chromeLaunchOptions()): string {
|
|
17
154
|
if ("executablePath" in target) return `executable ${target.executablePath} (AIDEMO_CHROME_EXECUTABLE)`;
|
|
@@ -31,6 +168,20 @@ const MISSING_RE =
|
|
|
31
168
|
export function explainLaunchError(err: unknown, target: ChromeLaunchTarget): Error {
|
|
32
169
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
33
170
|
if (!MISSING_RE.test(e.message)) return e;
|
|
171
|
+
if (FFMPEG_HELPER_RE.test(e.message)) {
|
|
172
|
+
// The browser is fine — Playwright's recording helper is what's missing.
|
|
173
|
+
// Blaming Chrome here sent users to reinstall a browser that works.
|
|
174
|
+
const helper = recorderFfmpeg();
|
|
175
|
+
const out = new BrowserSetupError(
|
|
176
|
+
`Playwright's ffmpeg helper${helper ? ` (revision ${helper.revision})` : ""} is missing — ` +
|
|
177
|
+
`the ~2 MB encoder Playwright records the take with (not the system ffmpeg, not a ` +
|
|
178
|
+
`browser; no npm install fetches it). Install it once per Playwright version: ` +
|
|
179
|
+
`${helper?.install ?? "npx playwright install ffmpeg"}` +
|
|
180
|
+
`\nOriginal error: ${e.message}`
|
|
181
|
+
);
|
|
182
|
+
(out as Error & { cause?: unknown }).cause = e;
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
34
185
|
let hint: string;
|
|
35
186
|
if ("executablePath" in target) {
|
|
36
187
|
hint =
|
package/src/distribute.ts
CHANGED
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
chromeLaunchOptions,
|
|
26
26
|
type ChromeLaunchTarget,
|
|
27
27
|
} from "./config.js";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
describeChromeTarget,
|
|
30
|
+
probeConfiguredBrowser,
|
|
31
|
+
recorderAutoInstall,
|
|
32
|
+
recorderFfmpeg,
|
|
33
|
+
} from "./browser.js";
|
|
29
34
|
import { localTtsStatus } from "./voice-local.js";
|
|
30
35
|
import { Project } from "./project.js";
|
|
31
36
|
import { STARTER_BRIEF, STARTER_STORYBOARD, briefFromInspect, storyboardFromInspect } from "./starter.js";
|
|
@@ -659,6 +664,19 @@ export interface DoctorReport {
|
|
|
659
664
|
};
|
|
660
665
|
};
|
|
661
666
|
playwright: boolean;
|
|
667
|
+
/**
|
|
668
|
+
* Playwright's ffmpeg helper for THIS engine's Playwright — the encoder the
|
|
669
|
+
* built-in capture records every take with (see recorderFfmpeg). Missing is
|
|
670
|
+
* only fatal with `autoInstall` off: the first take installs it otherwise.
|
|
671
|
+
* null when its location can't be determined.
|
|
672
|
+
*/
|
|
673
|
+
recorderFfmpeg: {
|
|
674
|
+
found: boolean;
|
|
675
|
+
path: string;
|
|
676
|
+
revision: string;
|
|
677
|
+
install: string;
|
|
678
|
+
autoInstall: boolean;
|
|
679
|
+
} | null;
|
|
662
680
|
skill: {
|
|
663
681
|
target: string;
|
|
664
682
|
installed: { version: string; stableAvailable: string | null } | null;
|
|
@@ -667,6 +685,11 @@ export interface DoctorReport {
|
|
|
667
685
|
ok: boolean;
|
|
668
686
|
}
|
|
669
687
|
|
|
688
|
+
/** Oldest supported Node major (bin/aidemo.mjs fails fast below it; package.json "engines"). */
|
|
689
|
+
const MIN_NODE_MAJOR = 22;
|
|
690
|
+
const nodeSupported = (version: string): boolean =>
|
|
691
|
+
Number(version.replace(/^v/, "").split(".")[0]) >= MIN_NODE_MAJOR;
|
|
692
|
+
|
|
670
693
|
/** Build the structured preflight report (used by CLI doctor + MCP doctor). */
|
|
671
694
|
export async function doctorReport(dir: string | undefined): Promise<DoctorReport> {
|
|
672
695
|
const base = openAiBaseUrl();
|
|
@@ -694,6 +717,7 @@ export async function doctorReport(dir: string | undefined): Promise<DoctorRepor
|
|
|
694
717
|
const localTts = ttsProvider() === "local" ? await localTtsStatus() : null;
|
|
695
718
|
const chrome = await probeChrome();
|
|
696
719
|
const browser = await probeBrowserTarget(chrome);
|
|
720
|
+
const helper = recorderFfmpeg();
|
|
697
721
|
const report: DoctorReport = {
|
|
698
722
|
engineVersion: latest,
|
|
699
723
|
node: process.version,
|
|
@@ -725,6 +749,15 @@ export async function doctorReport(dir: string | undefined): Promise<DoctorRepor
|
|
|
725
749
|
: {}),
|
|
726
750
|
},
|
|
727
751
|
playwright: playwrightOk,
|
|
752
|
+
recorderFfmpeg: helper
|
|
753
|
+
? {
|
|
754
|
+
found: await exists(helper.path),
|
|
755
|
+
path: helper.path,
|
|
756
|
+
revision: helper.revision,
|
|
757
|
+
install: helper.install,
|
|
758
|
+
autoInstall: recorderAutoInstall(),
|
|
759
|
+
}
|
|
760
|
+
: null,
|
|
728
761
|
skill: {
|
|
729
762
|
target,
|
|
730
763
|
installed: manifest
|
|
@@ -738,9 +771,11 @@ export async function doctorReport(dir: string | undefined): Promise<DoctorRepor
|
|
|
738
771
|
ok: false,
|
|
739
772
|
};
|
|
740
773
|
report.ok = Boolean(
|
|
741
|
-
report.
|
|
774
|
+
nodeSupported(report.node) &&
|
|
775
|
+
report.ffmpeg &&
|
|
742
776
|
report.browser.found &&
|
|
743
777
|
report.playwright &&
|
|
778
|
+
(report.recorderFfmpeg ? report.recorderFfmpeg.found || report.recorderFfmpeg.autoInstall : true) &&
|
|
744
779
|
// Local TTS needs no key: voice is in-process, captions auto-fall-back
|
|
745
780
|
// to the offline derivation in `render`.
|
|
746
781
|
(report.apiKey !== "missing" || report.tts.provider === "local") &&
|
|
@@ -758,7 +793,8 @@ export async function doctor(dir: string | undefined): Promise<void> {
|
|
|
758
793
|
const line = (label: string, val: string | null) =>
|
|
759
794
|
val ? ok(`${label}: ${val}`) : fail(`${label}: NOT FOUND`);
|
|
760
795
|
|
|
761
|
-
|
|
796
|
+
if (nodeSupported(r.node)) ok(`node: ${r.node}`);
|
|
797
|
+
else fail(`node: ${r.node} — aidemo needs Node ${MIN_NODE_MAJOR}+ (Node 20 is end-of-life)`);
|
|
762
798
|
line("ffmpeg", r.ffmpeg);
|
|
763
799
|
if (r.browser.source === "default") {
|
|
764
800
|
line("chrome", r.chrome);
|
|
@@ -806,6 +842,20 @@ export async function doctor(dir: string | undefined): Promise<void> {
|
|
|
806
842
|
);
|
|
807
843
|
if (r.playwright) ok(`playwright: resolvable`);
|
|
808
844
|
else fail(`playwright: not installed`);
|
|
845
|
+
const helper = r.recorderFfmpeg;
|
|
846
|
+
if (helper?.found) {
|
|
847
|
+
ok(`playwright ffmpeg helper: revision ${helper.revision} (records the take)`);
|
|
848
|
+
} else if (helper?.autoInstall) {
|
|
849
|
+
log(
|
|
850
|
+
`playwright ffmpeg helper: revision ${helper.revision} not installed yet — the first ` +
|
|
851
|
+
`take installs it (~2 MB from Playwright's CDN); or now: ${helper.install}`
|
|
852
|
+
);
|
|
853
|
+
} else if (helper) {
|
|
854
|
+
fail(
|
|
855
|
+
`playwright ffmpeg helper: revision ${helper.revision} NOT FOUND at ${helper.path} and ` +
|
|
856
|
+
`AIDEMO_NO_AUTO_INSTALL is set — every take fails without it: ${helper.install}`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
809
859
|
|
|
810
860
|
if (r.skill.installed) {
|
|
811
861
|
ok(
|
package/src/mcp/server.ts
CHANGED
|
@@ -556,7 +556,9 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
556
556
|
description:
|
|
557
557
|
"Check prereqs: node, ffmpeg, Chrome (or the AIDEMO_CHROME_CHANNEL / " +
|
|
558
558
|
"AIDEMO_CHROME_EXECUTABLE browser in use), TTS/STT endpoint (flags " +
|
|
559
|
-
"LLM-only servers like Ollama), API key, playwright
|
|
559
|
+
"LLM-only servers like Ollama), API key, playwright + its ffmpeg " +
|
|
560
|
+
"helper (records the take; installed on the first take unless " +
|
|
561
|
+
"AIDEMO_NO_AUTO_INSTALL=1), installed skill.",
|
|
560
562
|
inputSchema: {
|
|
561
563
|
dir: z.string().optional().describe("repo to check for an installed skill"),
|
|
562
564
|
},
|
|
@@ -586,6 +588,18 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
|
|
|
586
588
|
elevenLabsKey: z.enum(["set", "missing"]).optional(),
|
|
587
589
|
}),
|
|
588
590
|
playwright: z.boolean(),
|
|
591
|
+
recorderFfmpeg: z
|
|
592
|
+
.object({
|
|
593
|
+
found: z.boolean(),
|
|
594
|
+
path: z.string(),
|
|
595
|
+
revision: z.string(),
|
|
596
|
+
install: z.string().describe("exact command to install it"),
|
|
597
|
+
autoInstall: z
|
|
598
|
+
.boolean()
|
|
599
|
+
.describe("the first take installs it (off with AIDEMO_NO_AUTO_INSTALL=1)"),
|
|
600
|
+
})
|
|
601
|
+
.nullable()
|
|
602
|
+
.describe("Playwright's ffmpeg helper the built-in capture records with"),
|
|
589
603
|
skill: z.object({
|
|
590
604
|
target: z.string(),
|
|
591
605
|
installed: z
|
package/src/recorder.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describeChromeTarget, launchChromePersistent } from "./browser.js";
|
|
1
|
+
import { describeChromeTarget, ensureRecorderFfmpeg, launchChromePersistent } from "./browser.js";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import {
|
|
4
4
|
TimelineSchema,
|
|
@@ -316,6 +316,10 @@ export async function record(
|
|
|
316
316
|
const screenCapPath = join(videoDir, "capture.mkv");
|
|
317
317
|
await fs.rm(screenCapPath, { force: true });
|
|
318
318
|
|
|
319
|
+
// The built-in capture encodes the take with Playwright's ffmpeg helper,
|
|
320
|
+
// which a fresh install (or a Playwright upgrade) may not have yet.
|
|
321
|
+
if (!external) await ensureRecorderFfmpeg();
|
|
322
|
+
|
|
319
323
|
let context;
|
|
320
324
|
try {
|
|
321
325
|
context = await launchChromePersistent(profileDir, {
|