@tandryukha/aidemo 0.14.1 → 0.15.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/docs/AUTHORING.md +142 -20
- package/package.json +1 -1
- package/src/captions.ts +174 -30
- package/src/cli.ts +134 -7
- package/src/compose.ts +340 -36
- package/src/cursor.ts +13 -4
- package/src/ffmpeg.ts +68 -0
- package/src/frames.ts +54 -5
- package/src/import-trace.ts +20 -9
- package/src/inspect.ts +9 -0
- package/src/lint.ts +75 -3
- package/src/mcp/jobs.ts +1 -0
- package/src/mcp/server.ts +67 -3
- package/src/player.ts +317 -15
- package/src/qa.ts +295 -0
- package/src/recorder.ts +65 -7
- package/src/safe-regex.ts +36 -0
- package/src/types.ts +155 -19
package/src/cli.ts
CHANGED
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
import { resetProfile } from "./profile.js";
|
|
12
12
|
import { Project, parseStoryboard } from "./project.js";
|
|
13
13
|
import { record } from "./recorder.js";
|
|
14
|
-
import { parseCookieFlag } from "./setup.js";
|
|
14
|
+
import { collectSeeds, parseCookieFlag, runPreflight } from "./setup.js";
|
|
15
15
|
import { extractFrames } from "./frames.js";
|
|
16
|
+
import { runQa, logQa } from "./qa.js";
|
|
16
17
|
import { inspectPage } from "./inspect.js";
|
|
17
18
|
import { importTrace, scaffoldImported } from "./import-trace.js";
|
|
18
19
|
import { GUIDE_TOPIC_NAMES, guideHeadings, readGuide, sliceGuide } from "./guide.js";
|
|
@@ -314,6 +315,8 @@ function parseParams(pairs?: string[]): Record<string, string> | undefined {
|
|
|
314
315
|
return out;
|
|
315
316
|
}
|
|
316
317
|
|
|
318
|
+
const NO_REPLAY_OPT_DESC =
|
|
319
|
+
"with --from-scene: do not replay the reused scenes' actions — the profile already holds the state (balances earned, a day rolled forward); the resumed scene must open with its own goto";
|
|
317
320
|
const FROM_SCENE_OPT_DESC =
|
|
318
321
|
"resume: keep the previous take's scenes before this id (verified unchanged) and record from it";
|
|
319
322
|
const PARAM_OPT_DESC =
|
|
@@ -330,6 +333,8 @@ const LANGS_OPT_DESC =
|
|
|
330
333
|
* `[undefined]` when neither is given — the default single-language render,
|
|
331
334
|
* byte-for-byte unchanged (undefined = the base storyboard, unsuffixed paths).
|
|
332
335
|
*/
|
|
336
|
+
const STRICT_OPT_DESC =
|
|
337
|
+
"exit 1 when compose reports any warning (anchor-unreachable, overrun-trim, scene-freeze, blank-open …) — each one maps to a defect a viewer will see";
|
|
333
338
|
const STORAGE_STATE_OPT_DESC =
|
|
334
339
|
"seed a Playwright storageState JSON (cookies + localStorage) into the profile before the take";
|
|
335
340
|
const COOKIE_OPT_DESC =
|
|
@@ -574,6 +579,12 @@ program
|
|
|
574
579
|
.option("--limit <n>", "max elements (default 80)", "80")
|
|
575
580
|
.option("--viewport <WxH>", "viewport (default 1280x720)")
|
|
576
581
|
.option("--frame <name=selector>", "also scan this iframe (repeatable)", collectKv, [])
|
|
582
|
+
.option("--storage-state <file>", STORAGE_STATE_OPT_DESC)
|
|
583
|
+
.option("--cookie <spec>", COOKIE_OPT_DESC, collectKv, [])
|
|
584
|
+
.option(
|
|
585
|
+
"--no-setup",
|
|
586
|
+
"with --dir: ignore the storyboard's setup block (cookies/storageState/preflight)"
|
|
587
|
+
)
|
|
577
588
|
.option("--json", "print the full JSON result", false)
|
|
578
589
|
.description(
|
|
579
590
|
"list the page's visible interactive elements with unique selectors (no selector guessing)"
|
|
@@ -588,6 +599,9 @@ program
|
|
|
588
599
|
limit: string;
|
|
589
600
|
viewport?: string;
|
|
590
601
|
frame: string[];
|
|
602
|
+
storageState?: string;
|
|
603
|
+
cookie: string[];
|
|
604
|
+
setup?: boolean;
|
|
591
605
|
json?: boolean;
|
|
592
606
|
}
|
|
593
607
|
) => {
|
|
@@ -600,12 +614,44 @@ program
|
|
|
600
614
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
601
615
|
const project = opts.dir ? new Project(opts.dir) : null;
|
|
602
616
|
if (project) await ensureDir(project.p("logs"));
|
|
617
|
+
// #53: authenticate the scan the same way a take does — the storyboard
|
|
618
|
+
// already declares how (setup.storageState / cookies / preflight), and
|
|
619
|
+
// --storage-state / --cookie layer on top for a project-less inspect.
|
|
620
|
+
const useSetup = opts.setup !== false;
|
|
621
|
+
let storyboard: Awaited<ReturnType<Project["loadStoryboard"]>> | null = null;
|
|
622
|
+
if (project && useSetup) {
|
|
623
|
+
storyboard = await project.loadStoryboard({ relaxed: true }).catch(() => null);
|
|
624
|
+
}
|
|
625
|
+
if (storyboard?.setup?.preflight) {
|
|
626
|
+
await runPreflight(storyboard.setup.preflight, {
|
|
627
|
+
demoDir: project!.dir,
|
|
628
|
+
storyboardPath: project!.storyboardPath,
|
|
629
|
+
profileDir: opts.profile ?? chromeProfileDir(),
|
|
630
|
+
});
|
|
631
|
+
storyboard = await project!.loadStoryboard({ relaxed: true }).catch(() => storyboard);
|
|
632
|
+
}
|
|
633
|
+
const seeds = await collectSeeds(
|
|
634
|
+
project?.dir ?? process.cwd(),
|
|
635
|
+
storyboard ?? ({ setup: undefined } as never),
|
|
636
|
+
{
|
|
637
|
+
storageState: opts.storageState,
|
|
638
|
+
cookies: opts.cookie?.length ? opts.cookie.map(parseCookieFlag) : undefined,
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
for (const [name, selector] of Object.entries(storyboard?.frames ?? {})) {
|
|
642
|
+
if (!(name in frames)) frames[name] = selector;
|
|
643
|
+
}
|
|
603
644
|
const res = await inspectPage({
|
|
604
645
|
url,
|
|
605
646
|
headless: !opts.headed,
|
|
606
647
|
profileDir: opts.profile,
|
|
607
648
|
limit: Number(opts.limit) || 80,
|
|
608
|
-
...(vp
|
|
649
|
+
...(vp
|
|
650
|
+
? { viewport: { width: Number(vp[1]), height: Number(vp[2]) } }
|
|
651
|
+
: storyboard?.video
|
|
652
|
+
? { viewport: storyboard.video }
|
|
653
|
+
: {}),
|
|
654
|
+
seeds,
|
|
609
655
|
frames,
|
|
610
656
|
...(project ? { screenshotPath: project.p("logs", `inspect-${stamp}.png`) } : {}),
|
|
611
657
|
});
|
|
@@ -650,13 +696,22 @@ program
|
|
|
650
696
|
)
|
|
651
697
|
.option("--width <px>", "frame width in px, aspect kept (default 640)", "640")
|
|
652
698
|
.option("--out <dir>", "output directory (default <dir>/output/frames)")
|
|
699
|
+
.option("--sheet", "also tile the frames into one contact-sheet PNG", false)
|
|
700
|
+
.option("--columns <n>", "contact-sheet columns (default 5)", "5")
|
|
653
701
|
.description(
|
|
654
702
|
"dump evenly spaced PNG frames from the final video (or the raw take) for review"
|
|
655
703
|
)
|
|
656
704
|
.action(
|
|
657
705
|
async (
|
|
658
706
|
dir: string,
|
|
659
|
-
opts: {
|
|
707
|
+
opts: {
|
|
708
|
+
every: string;
|
|
709
|
+
source: string;
|
|
710
|
+
width: string;
|
|
711
|
+
out?: string;
|
|
712
|
+
sheet?: boolean;
|
|
713
|
+
columns: string;
|
|
714
|
+
}
|
|
660
715
|
) => {
|
|
661
716
|
const everySec = Number(opts.every);
|
|
662
717
|
if (!(everySec > 0)) throw new Error(`--every must be a positive number of seconds`);
|
|
@@ -669,6 +724,8 @@ program
|
|
|
669
724
|
source: opts.source as "final" | "raw" | "take",
|
|
670
725
|
width: parsePositiveInt("--width", opts.width),
|
|
671
726
|
outDir: opts.out ? resolve(opts.out) : undefined,
|
|
727
|
+
sheet: opts.sheet,
|
|
728
|
+
sheetColumns: parsePositiveInt("--columns", opts.columns),
|
|
672
729
|
});
|
|
673
730
|
step("Frames");
|
|
674
731
|
ok(
|
|
@@ -676,6 +733,7 @@ program
|
|
|
676
733
|
`(${(res.durationMs / 1000).toFixed(1)}s)`
|
|
677
734
|
);
|
|
678
735
|
for (const f of res.files) log(` ${f}`);
|
|
736
|
+
if (res.sheet) ok(`contact sheet → ${res.sheet}`);
|
|
679
737
|
}
|
|
680
738
|
);
|
|
681
739
|
|
|
@@ -698,6 +756,7 @@ program
|
|
|
698
756
|
)
|
|
699
757
|
.option("--param <kv>", PARAM_OPT_DESC, collectKv, [])
|
|
700
758
|
.option("--from-scene <id>", FROM_SCENE_OPT_DESC)
|
|
759
|
+
.option("--no-replay", NO_REPLAY_OPT_DESC)
|
|
701
760
|
.description("drive the storyboard in Chrome and record raw video + timeline.json")
|
|
702
761
|
.action(
|
|
703
762
|
async (
|
|
@@ -712,6 +771,7 @@ program
|
|
|
712
771
|
capture?: string;
|
|
713
772
|
param?: string[];
|
|
714
773
|
fromScene?: string;
|
|
774
|
+
replay?: boolean;
|
|
715
775
|
}
|
|
716
776
|
) => {
|
|
717
777
|
const project = new Project(dir);
|
|
@@ -727,6 +787,7 @@ program
|
|
|
727
787
|
headed: !opts.headless,
|
|
728
788
|
capture: parseCapture(opts.capture),
|
|
729
789
|
fromScene: opts.fromScene,
|
|
790
|
+
noReplay: opts.replay === false,
|
|
730
791
|
});
|
|
731
792
|
}
|
|
732
793
|
);
|
|
@@ -950,26 +1011,66 @@ program
|
|
|
950
1011
|
.option("--param <kv>", PARAM_OPT_DESC, collectKv, [])
|
|
951
1012
|
.option("--lang <code>", LANG_OPT_DESC)
|
|
952
1013
|
.option("--langs <codes>", LANGS_OPT_DESC)
|
|
1014
|
+
.option("--strict", STRICT_OPT_DESC, false)
|
|
953
1015
|
.description("trim, sync, mux and caption into output/final-demo.mp4")
|
|
954
1016
|
.action(
|
|
955
1017
|
async (
|
|
956
1018
|
dir: string,
|
|
957
|
-
opts: { gif?: boolean; param?: string[]; lang?: string; langs?: string }
|
|
1019
|
+
opts: { gif?: boolean; param?: string[]; lang?: string; langs?: string; strict?: boolean }
|
|
958
1020
|
) => {
|
|
959
1021
|
const storyboard = await new Project(dir).loadStoryboard({
|
|
960
1022
|
params: parseParams(opts.param),
|
|
961
1023
|
});
|
|
1024
|
+
let warningTotal = 0;
|
|
962
1025
|
for (const lang of langsFrom(opts)) {
|
|
963
1026
|
const project = new Project(dir, lang);
|
|
964
1027
|
await beginCommand(project, "compose");
|
|
965
1028
|
const sb = lang ? localizeStoryboard(storyboard, lang) : storyboard;
|
|
966
|
-
await compose(project, sb);
|
|
1029
|
+
const report = await compose(project, sb);
|
|
1030
|
+
warningTotal += report.warnings.length;
|
|
967
1031
|
if (opts.gif) await exportGif(project);
|
|
968
1032
|
// `output.walkthrough` is a pure post-process on the final video, so
|
|
969
1033
|
// honour it here too (issue #45): a pipeline that drives the stages
|
|
970
1034
|
// itself got chapters + poster and a silently ignored flag.
|
|
971
1035
|
if (sb.output?.walkthrough) await exportWalkthrough(project, sb);
|
|
972
1036
|
}
|
|
1037
|
+
if (opts.strict && warningTotal > 0) {
|
|
1038
|
+
process.exitCode = 1;
|
|
1039
|
+
fail(
|
|
1040
|
+
`--strict: ${warningTotal} compose warning(s) — see the grouped list above, ` +
|
|
1041
|
+
`output/report.json and the frames in output/warnings/`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
program
|
|
1048
|
+
.command("qa")
|
|
1049
|
+
.argument("<dir>", "demo project directory")
|
|
1050
|
+
.option("--lang <code>", "check the <code> language variant's video")
|
|
1051
|
+
.option("--param <kv>", PARAM_OPT_DESC, collectKv, [])
|
|
1052
|
+
.option("--json", "print the full JSON result", false)
|
|
1053
|
+
.option("--strict", "exit 1 when any check warns", false)
|
|
1054
|
+
.description(
|
|
1055
|
+
"post-render checks on the finished MP4: blank open, blank poster, loudness, music bed, hold shares, cue readability, frame/chapter hygiene"
|
|
1056
|
+
)
|
|
1057
|
+
.action(
|
|
1058
|
+
async (
|
|
1059
|
+
dir: string,
|
|
1060
|
+
opts: { lang?: string; param?: string[]; json?: boolean; strict?: boolean }
|
|
1061
|
+
) => {
|
|
1062
|
+
const base = new Project(dir);
|
|
1063
|
+
const storyboard = await base.loadStoryboard({ params: parseParams(opts.param) });
|
|
1064
|
+
const project = new Project(dir, opts.lang);
|
|
1065
|
+
const sb = opts.lang ? localizeStoryboard(storyboard, opts.lang) : storyboard;
|
|
1066
|
+
const res = await runQa(project, sb);
|
|
1067
|
+
if (opts.json) {
|
|
1068
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
1069
|
+
} else {
|
|
1070
|
+
step(`demo QA: ${res.video}`);
|
|
1071
|
+
logQa(res);
|
|
1072
|
+
}
|
|
1073
|
+
if (opts.strict && res.warnings > 0) process.exitCode = 1;
|
|
973
1074
|
}
|
|
974
1075
|
);
|
|
975
1076
|
|
|
@@ -1063,6 +1164,8 @@ program
|
|
|
1063
1164
|
.option("--lang <code>", LANG_OPT_DESC)
|
|
1064
1165
|
.option("--langs <codes>", `${LANGS_OPT_DESC} — records ONCE, then renders each`)
|
|
1065
1166
|
.option("--from-scene <id>", FROM_SCENE_OPT_DESC)
|
|
1167
|
+
.option("--no-replay", NO_REPLAY_OPT_DESC)
|
|
1168
|
+
.option("--strict", STRICT_OPT_DESC, false)
|
|
1066
1169
|
.description("run the full pipeline: voice → record → captions → compose")
|
|
1067
1170
|
.action(
|
|
1068
1171
|
async (
|
|
@@ -1083,9 +1186,21 @@ program
|
|
|
1083
1186
|
lang?: string;
|
|
1084
1187
|
langs?: string;
|
|
1085
1188
|
fromScene?: string;
|
|
1189
|
+
replay?: boolean;
|
|
1190
|
+
strict?: boolean;
|
|
1086
1191
|
}
|
|
1087
1192
|
) => {
|
|
1088
1193
|
applyTtsFlag(opts.tts);
|
|
1194
|
+
let warningTotal = 0;
|
|
1195
|
+
const strictGate = (): void => {
|
|
1196
|
+
if (opts.strict && warningTotal > 0) {
|
|
1197
|
+
process.exitCode = 1;
|
|
1198
|
+
fail(
|
|
1199
|
+
`--strict: ${warningTotal} compose warning(s) — see the grouped list above, ` +
|
|
1200
|
+
`output/report.json and the frames in output/warnings/`
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1089
1204
|
const base = new Project(dir);
|
|
1090
1205
|
await beginCommand(base, "render");
|
|
1091
1206
|
|
|
@@ -1142,10 +1257,15 @@ program
|
|
|
1142
1257
|
headed: !opts.headless,
|
|
1143
1258
|
capture: parseCapture(opts.capture),
|
|
1144
1259
|
fromScene: opts.fromScene,
|
|
1260
|
+
noReplay: opts.replay === false,
|
|
1145
1261
|
})
|
|
1146
1262
|
);
|
|
1147
1263
|
await stageLog(base, "captions", () => captionsFor(base, storyboard));
|
|
1148
|
-
await stageLog(base, "compose", () =>
|
|
1264
|
+
await stageLog(base, "compose", async () => {
|
|
1265
|
+
const report = await compose(base, storyboard);
|
|
1266
|
+
warningTotal += report.warnings.length;
|
|
1267
|
+
return report;
|
|
1268
|
+
});
|
|
1149
1269
|
if (opts.gif) await stageLog(base, "gif", () => exportGif(base));
|
|
1150
1270
|
// Screenshot mode: emit any `still` PNGs from the clean take (a
|
|
1151
1271
|
// re-extract, not a re-record).
|
|
@@ -1157,6 +1277,7 @@ program
|
|
|
1157
1277
|
}
|
|
1158
1278
|
step("Done");
|
|
1159
1279
|
ok(`▶ open ${base.outputPath}`);
|
|
1280
|
+
strictGate();
|
|
1160
1281
|
return;
|
|
1161
1282
|
}
|
|
1162
1283
|
|
|
@@ -1172,6 +1293,7 @@ program
|
|
|
1172
1293
|
headed: !opts.headless,
|
|
1173
1294
|
capture: parseCapture(opts.capture),
|
|
1174
1295
|
fromScene: opts.fromScene,
|
|
1296
|
+
noReplay: opts.replay === false,
|
|
1175
1297
|
})
|
|
1176
1298
|
);
|
|
1177
1299
|
if (storyboardHasStills(storyboard)) {
|
|
@@ -1186,11 +1308,16 @@ program
|
|
|
1186
1308
|
generateVoice(project, sb, { force: opts.forceVoice })
|
|
1187
1309
|
);
|
|
1188
1310
|
await stageLog(project, "captions", () => captionsFor(project, sb));
|
|
1189
|
-
await stageLog(project, "compose", () =>
|
|
1311
|
+
await stageLog(project, "compose", async () => {
|
|
1312
|
+
const report = await compose(project, sb);
|
|
1313
|
+
warningTotal += report.warnings.length;
|
|
1314
|
+
return report;
|
|
1315
|
+
});
|
|
1190
1316
|
if (opts.gif) await stageLog(project, "gif", () => exportGif(project));
|
|
1191
1317
|
ok(`▶ open ${project.outputPath}`);
|
|
1192
1318
|
}
|
|
1193
1319
|
step("Done");
|
|
1320
|
+
strictGate();
|
|
1194
1321
|
}
|
|
1195
1322
|
);
|
|
1196
1323
|
|