@tandryukha/aidemo 0.17.0 → 0.17.1

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 CHANGED
@@ -164,7 +164,9 @@ 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 records the take: `aidemo doctor` checks for it
169
+ and prints the exact install command (`npx playwright install ffmpeg`).
168
170
 
169
171
  [![Quickstart output — the bundled fixture rendered end-to-end](docs/quickstart.gif)](https://github.com/tandryukha/aidemo/releases/download/v0.3.0/quickstart-demo.mp4)
170
172
 
@@ -251,7 +253,9 @@ Codex).
251
253
 
252
254
  ## Setup
253
255
 
254
- Prereqs: **Node 20+**, **Google Chrome**, **ffmpeg + ffprobe** on `PATH`.
256
+ Prereqs: **Node 20+**, **Google Chrome**, **ffmpeg + ffprobe** on `PATH`, and
257
+ Playwright's ~2 MB ffmpeg helper that records the take (one per Playwright
258
+ version; `aidemo doctor` prints the exact install command).
255
259
  Developed and tested on macOS; Linux works for the default (Playwright) capture
256
260
  and `--capture obs`. Run `aidemo doctor` to check your setup.
257
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tandryukha/aidemo",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "license": "MIT",
5
5
  "description": "AI Demo Engine — turn a storyboard into a narrated, captioned product demo video",
6
6
  "type": "module",
package/src/browser.ts CHANGED
@@ -8,10 +8,72 @@
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 { readFileSync } from "node:fs";
12
+ import { createRequire } from "node:module";
13
+ import { homedir } from "node:os";
14
+ import { dirname, isAbsolute, join, resolve } from "node:path";
11
15
  import { BrowserSetupError, chromeLaunchOptions, type ChromeLaunchTarget } from "./config.js";
12
16
 
13
17
  type PersistentOptions = NonNullable<Parameters<typeof chromium.launchPersistentContext>[1]>;
14
18
 
19
+ /** Executable name of Playwright's ffmpeg helper, per platform. */
20
+ const FFMPEG_HELPER_EXE: Partial<Record<NodeJS.Platform, string>> = {
21
+ linux: "ffmpeg-linux",
22
+ darwin: "ffmpeg-mac",
23
+ win32: "ffmpeg-win64.exe",
24
+ };
25
+
26
+ /**
27
+ * Playwright's own ffmpeg helper: the encoder recordVideo writes every take
28
+ * with. It is neither the system ffmpeg (compose) nor a browser — a ~2 MB
29
+ * download per Playwright ffmpeg *revision*, and no npm install fetches it
30
+ * (playwright has no install script). So a fresh npx install, or a Playwright
31
+ * minor that bumps the revision (1.64: 1011 → 1013, with VP9 encoding), leaves
32
+ * it missing and the take fails at launch. Mirrors playwright-core's
33
+ * documented lookup (PLAYWRIGHT_BROWSERS_PATH, else the per-OS
34
+ * ms-playwright cache dir). `install` is the exact command for THIS engine's
35
+ * Playwright. null when the location can't be determined.
36
+ */
37
+ export function recorderFfmpeg(): { path: string; revision: string; install: string } | null {
38
+ try {
39
+ const exe = FFMPEG_HELPER_EXE[process.platform];
40
+ if (!exe) return null;
41
+ const req = createRequire(import.meta.url);
42
+ const playwrightPkg = req.resolve("playwright/package.json");
43
+ // playwright-core is playwright's dependency: resolve it from there.
44
+ const corePkg = createRequire(playwrightPkg).resolve("playwright-core/package.json");
45
+ const coreDir = dirname(corePkg);
46
+ const browsers = JSON.parse(readFileSync(join(coreDir, "browsers.json"), "utf8")) as {
47
+ browsers: Array<{ name: string; revision: string }>;
48
+ };
49
+ const revision = browsers.browsers.find((b) => b.name === "ffmpeg")?.revision;
50
+ if (!revision) return null;
51
+ const env = process.env.PLAYWRIGHT_BROWSERS_PATH;
52
+ let registry =
53
+ env === "0"
54
+ ? join(coreDir, ".local-browsers")
55
+ : env || join(defaultPlaywrightCacheDir(), "ms-playwright");
56
+ if (!isAbsolute(registry)) registry = resolve(process.env.INIT_CWD || process.cwd(), registry);
57
+ const cli = join(dirname(playwrightPkg), "cli.js");
58
+ return {
59
+ path: join(registry, `ffmpeg-${revision}`, exe),
60
+ revision,
61
+ install: `node "${cli}" install ffmpeg`,
62
+ };
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function defaultPlaywrightCacheDir(): string {
69
+ if (process.platform === "darwin") return join(homedir(), "Library", "Caches");
70
+ if (process.platform === "win32") return process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
71
+ return process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
72
+ }
73
+
74
+ /** A launch that failed on Playwright's missing ffmpeg helper, not on the browser. */
75
+ const FFMPEG_HELPER_RE = /playwright install ffmpeg|[\\/]ffmpeg-\d+[\\/]ffmpeg/i;
76
+
15
77
  /** Human-readable description of the configured browser, for logs/doctor. */
16
78
  export function describeChromeTarget(target: ChromeLaunchTarget = chromeLaunchOptions()): string {
17
79
  if ("executablePath" in target) return `executable ${target.executablePath} (AIDEMO_CHROME_EXECUTABLE)`;
@@ -31,6 +93,20 @@ const MISSING_RE =
31
93
  export function explainLaunchError(err: unknown, target: ChromeLaunchTarget): Error {
32
94
  const e = err instanceof Error ? err : new Error(String(err));
33
95
  if (!MISSING_RE.test(e.message)) return e;
96
+ if (FFMPEG_HELPER_RE.test(e.message)) {
97
+ // The browser is fine — Playwright's recording helper is what's missing.
98
+ // Blaming Chrome here sent users to reinstall a browser that works.
99
+ const helper = recorderFfmpeg();
100
+ const out = new BrowserSetupError(
101
+ `Playwright's ffmpeg helper${helper ? ` (revision ${helper.revision})` : ""} is missing — ` +
102
+ `the ~2 MB encoder Playwright records the take with (not the system ffmpeg, not a ` +
103
+ `browser; no npm install fetches it). Install it once per Playwright version: ` +
104
+ `${helper?.install ?? "npx playwright install ffmpeg"}` +
105
+ `\nOriginal error: ${e.message}`
106
+ );
107
+ (out as Error & { cause?: unknown }).cause = e;
108
+ return out;
109
+ }
34
110
  let hint: string;
35
111
  if ("executablePath" in target) {
36
112
  hint =
package/src/distribute.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  chromeLaunchOptions,
26
26
  type ChromeLaunchTarget,
27
27
  } from "./config.js";
28
- import { describeChromeTarget, probeConfiguredBrowser } from "./browser.js";
28
+ import { describeChromeTarget, probeConfiguredBrowser, recorderFfmpeg } from "./browser.js";
29
29
  import { localTtsStatus } from "./voice-local.js";
30
30
  import { Project } from "./project.js";
31
31
  import { STARTER_BRIEF, STARTER_STORYBOARD, briefFromInspect, storyboardFromInspect } from "./starter.js";
@@ -659,6 +659,12 @@ export interface DoctorReport {
659
659
  };
660
660
  };
661
661
  playwright: boolean;
662
+ /**
663
+ * Playwright's ffmpeg helper for THIS engine's Playwright — the encoder the
664
+ * built-in capture records every take with (see recorderFfmpeg). null when
665
+ * its location can't be determined.
666
+ */
667
+ recorderFfmpeg: { found: boolean; path: string; revision: string; install: string } | null;
662
668
  skill: {
663
669
  target: string;
664
670
  installed: { version: string; stableAvailable: string | null } | null;
@@ -694,6 +700,7 @@ export async function doctorReport(dir: string | undefined): Promise<DoctorRepor
694
700
  const localTts = ttsProvider() === "local" ? await localTtsStatus() : null;
695
701
  const chrome = await probeChrome();
696
702
  const browser = await probeBrowserTarget(chrome);
703
+ const helper = recorderFfmpeg();
697
704
  const report: DoctorReport = {
698
705
  engineVersion: latest,
699
706
  node: process.version,
@@ -725,6 +732,7 @@ export async function doctorReport(dir: string | undefined): Promise<DoctorRepor
725
732
  : {}),
726
733
  },
727
734
  playwright: playwrightOk,
735
+ recorderFfmpeg: helper ? { found: await exists(helper.path), ...helper } : null,
728
736
  skill: {
729
737
  target,
730
738
  installed: manifest
@@ -741,6 +749,7 @@ export async function doctorReport(dir: string | undefined): Promise<DoctorRepor
741
749
  report.ffmpeg &&
742
750
  report.browser.found &&
743
751
  report.playwright &&
752
+ (report.recorderFfmpeg?.found ?? true) &&
744
753
  // Local TTS needs no key: voice is in-process, captions auto-fall-back
745
754
  // to the offline derivation in `render`.
746
755
  (report.apiKey !== "missing" || report.tts.provider === "local") &&
@@ -806,6 +815,15 @@ export async function doctor(dir: string | undefined): Promise<void> {
806
815
  );
807
816
  if (r.playwright) ok(`playwright: resolvable`);
808
817
  else fail(`playwright: not installed`);
818
+ if (r.recorderFfmpeg?.found) {
819
+ ok(`playwright ffmpeg helper: revision ${r.recorderFfmpeg.revision} (records the take)`);
820
+ } else if (r.recorderFfmpeg) {
821
+ fail(
822
+ `playwright ffmpeg helper: revision ${r.recorderFfmpeg.revision} NOT FOUND at ` +
823
+ `${r.recorderFfmpeg.path} — every take fails without it (~2 MB, once per ` +
824
+ `Playwright version): ${r.recorderFfmpeg.install}`
825
+ );
826
+ }
809
827
 
810
828
  if (r.skill.installed) {
811
829
  ok(
package/src/mcp/server.ts CHANGED
@@ -556,7 +556,8 @@ 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, installed skill.",
559
+ "LLM-only servers like Ollama), API key, playwright + its ffmpeg " +
560
+ "helper (records the take; missing = every take fails), installed skill.",
560
561
  inputSchema: {
561
562
  dir: z.string().optional().describe("repo to check for an installed skill"),
562
563
  },
@@ -586,6 +587,15 @@ export function buildMcpServer(): { server: McpServer; jobs: JobManager } {
586
587
  elevenLabsKey: z.enum(["set", "missing"]).optional(),
587
588
  }),
588
589
  playwright: z.boolean(),
590
+ recorderFfmpeg: z
591
+ .object({
592
+ found: z.boolean(),
593
+ path: z.string(),
594
+ revision: z.string(),
595
+ install: z.string().describe("exact command to install it"),
596
+ })
597
+ .nullable()
598
+ .describe("Playwright's ffmpeg helper the built-in capture records with"),
589
599
  skill: z.object({
590
600
  target: z.string(),
591
601
  installed: z