@tandryukha/aidemo 0.14.1 → 0.15.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/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 ? { viewport: { width: Number(vp[1]), height: Number(vp[2]) } } : {}),
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: { every: string; source: string; width: string; out?: string }
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", () => compose(base, storyboard));
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", () => compose(project, sb));
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