@tandryukha/aidemo 0.8.0 → 0.14.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/.claude/skills/record-demo/SKILL.md +5 -2
- package/README.md +16 -4
- package/docs/AUTHORING.md +609 -45
- package/package.json +3 -2
- package/src/anchors.ts +128 -0
- package/src/attention.ts +365 -0
- package/src/caption-render.ts +17 -8
- package/src/captions.ts +70 -6
- package/src/capture.ts +113 -0
- package/src/cards.ts +15 -6
- package/src/cli.ts +580 -22
- package/src/compose.ts +1006 -93
- package/src/cursor-overlay.ts +7 -113
- package/src/cursor.ts +9 -3
- package/src/distribute.ts +37 -4
- package/src/expr.ts +130 -0
- package/src/ffmpeg.ts +156 -0
- package/src/frame.ts +205 -0
- package/src/frames.ts +159 -0
- package/src/golden.ts +27 -0
- package/src/guide.ts +102 -0
- package/src/i18n.ts +7 -4
- package/src/import-trace.ts +629 -0
- package/src/inspect.ts +307 -0
- package/src/lint.ts +601 -0
- package/src/login.ts +143 -0
- package/src/mcp/jobs.ts +4 -1
- package/src/mcp/server.ts +403 -26
- package/src/player.ts +975 -32
- package/src/profile.ts +128 -0
- package/src/project.ts +12 -2
- package/src/recorder.ts +252 -15
- package/src/setup.ts +248 -0
- package/src/starter.ts +244 -0
- package/src/stills.ts +15 -4
- package/src/types.ts +643 -5
- package/src/variants.ts +1 -1
- package/src/voice-local.ts +7 -1
- package/src/voice.ts +60 -1
- package/src/walkthrough.ts +250 -0
- package/src/zoom.ts +85 -39
package/src/capture.ts
CHANGED
|
@@ -572,3 +572,116 @@ export async function cropCaptureToViewport(
|
|
|
572
572
|
dst,
|
|
573
573
|
]);
|
|
574
574
|
}
|
|
575
|
+
|
|
576
|
+
// ---------------------------------------------------------------------------
|
|
577
|
+
// post-capture content verification
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
|
|
580
|
+
/** Downscaled grayscale thumbnail size for the content comparison. */
|
|
581
|
+
const VERIFY_W = 64;
|
|
582
|
+
const VERIFY_H = 36;
|
|
583
|
+
/** Mean |Δgray| below this = same content even before correlating. */
|
|
584
|
+
const VERIFY_MAD_PASS = 15;
|
|
585
|
+
/** Minimum Pearson correlation of the two thumbnails to pass. */
|
|
586
|
+
const VERIFY_CORR_PASS = 0.7;
|
|
587
|
+
|
|
588
|
+
/** Decode one frame of `input` to a raw grayscale VERIFY_W×VERIFY_H buffer. */
|
|
589
|
+
async function grayThumb(input: string, preArgs: string[]): Promise<Buffer> {
|
|
590
|
+
const out = `${input}.gray`;
|
|
591
|
+
await runFfmpeg([
|
|
592
|
+
...preArgs,
|
|
593
|
+
"-i",
|
|
594
|
+
input,
|
|
595
|
+
"-vf",
|
|
596
|
+
`scale=${VERIFY_W}:${VERIFY_H}:flags=area,format=gray`,
|
|
597
|
+
"-frames:v",
|
|
598
|
+
"1",
|
|
599
|
+
"-f",
|
|
600
|
+
"rawvideo",
|
|
601
|
+
out,
|
|
602
|
+
]);
|
|
603
|
+
const buf = await fs.readFile(out);
|
|
604
|
+
await fs.rm(out, { force: true });
|
|
605
|
+
return buf;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Verify that the CONTENT of the cropped capture is the page that was driven
|
|
610
|
+
* (issue #21). The geometry gates above prove the crop *rectangle* is right,
|
|
611
|
+
* but not that the right *pixels* were under it: a display-region screen grab
|
|
612
|
+
* records whatever is painted at those coordinates, so a browser window that
|
|
613
|
+
* is occluded (another app raised mid-take), on another display, or behind a
|
|
614
|
+
* notification produces a take that passes every geometry check and still
|
|
615
|
+
* shows someone else's window — silent garbage, and a privacy hazard, since
|
|
616
|
+
* the leaked pixels ship in an otherwise "publish-ready" video.
|
|
617
|
+
*
|
|
618
|
+
* The reference is a CDP screenshot of the page taken at capture-stop time
|
|
619
|
+
* (the page's own buffer — immune to occlusion). Both it and the tail frame
|
|
620
|
+
* of the cropped capture are reduced to a small grayscale thumbnail (ffmpeg
|
|
621
|
+
* rawvideo — no image deps) and compared: near-identical mean gray delta
|
|
622
|
+
* passes outright; otherwise the Pearson correlation must clear a threshold
|
|
623
|
+
* (robust to the gamma/color-profile shift between a screen grab and a CDP
|
|
624
|
+
* screenshot). A mismatch ABORTS like the geometry gates
|
|
625
|
+
* (AIDEMO_NATIVE_CROP_UNSAFE=1 downgrades to a loud warning).
|
|
626
|
+
*/
|
|
627
|
+
export async function verifyCaptureMatchesPage(
|
|
628
|
+
croppedVideo: string,
|
|
629
|
+
referencePng: string
|
|
630
|
+
): Promise<void> {
|
|
631
|
+
// Tail frame: the reference screenshot was taken just before capture stop,
|
|
632
|
+
// and the page is static by then (the last scene has ended).
|
|
633
|
+
const vid = await grayThumb(croppedVideo, ["-sseof", "-0.3"]);
|
|
634
|
+
const ref = await grayThumb(referencePng, []);
|
|
635
|
+
const n = Math.min(vid.length, ref.length);
|
|
636
|
+
if (n === 0) {
|
|
637
|
+
log("⚠ capture verification skipped (empty thumbnail) — frame-review the take");
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
let sumAbs = 0;
|
|
641
|
+
let sumA = 0;
|
|
642
|
+
let sumB = 0;
|
|
643
|
+
for (let i = 0; i < n; i++) {
|
|
644
|
+
sumAbs += Math.abs(vid[i] - ref[i]);
|
|
645
|
+
sumA += vid[i];
|
|
646
|
+
sumB += ref[i];
|
|
647
|
+
}
|
|
648
|
+
const mad = sumAbs / n;
|
|
649
|
+
const meanA = sumA / n;
|
|
650
|
+
const meanB = sumB / n;
|
|
651
|
+
let cov = 0;
|
|
652
|
+
let varA = 0;
|
|
653
|
+
let varB = 0;
|
|
654
|
+
for (let i = 0; i < n; i++) {
|
|
655
|
+
const da = vid[i] - meanA;
|
|
656
|
+
const db = ref[i] - meanB;
|
|
657
|
+
cov += da * db;
|
|
658
|
+
varA += da * da;
|
|
659
|
+
varB += db * db;
|
|
660
|
+
}
|
|
661
|
+
// Degenerate (near-uniform) thumbnails make correlation meaningless — the
|
|
662
|
+
// MAD pass above is the only meaningful check for e.g. a blank white page.
|
|
663
|
+
const corr =
|
|
664
|
+
varA > 1e-6 && varB > 1e-6 ? cov / Math.sqrt(varA * varB) : NaN;
|
|
665
|
+
log(
|
|
666
|
+
`capture verification: mean |Δgray| ${mad.toFixed(1)} (pass ≤ ${VERIFY_MAD_PASS}), ` +
|
|
667
|
+
`correlation ${Number.isNaN(corr) ? "n/a (uniform frame)" : corr.toFixed(3)} (pass ≥ ${VERIFY_CORR_PASS})`
|
|
668
|
+
);
|
|
669
|
+
if (mad <= VERIFY_MAD_PASS || (!Number.isNaN(corr) && corr >= VERIFY_CORR_PASS)) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
gateGeometryProblems(
|
|
673
|
+
[
|
|
674
|
+
`the captured pixels do not look like the page that was driven ` +
|
|
675
|
+
`(mean gray delta ${mad.toFixed(1)}, correlation ${Number.isNaN(corr) ? "n/a" : corr.toFixed(3)}) — ` +
|
|
676
|
+
`the browser window was likely occluded by another window, on a different ` +
|
|
677
|
+
`display than the captured one, or covered by a notification during the take`,
|
|
678
|
+
],
|
|
679
|
+
`Native capture verification failed: the recording shows something other than ` +
|
|
680
|
+
`the driven page. The cropped take (${croppedVideo}) and the page reference ` +
|
|
681
|
+
`(${referencePng}) were kept — compare them side by side, and treat the take ` +
|
|
682
|
+
`as sensitive until reviewed (it may show other windows). Re-record with the ` +
|
|
683
|
+
`browser window front-most and unoccluded on the captured display, or use the ` +
|
|
684
|
+
`default --capture playwright (records the page buffer directly — immune to ` +
|
|
685
|
+
`occlusion).`
|
|
686
|
+
);
|
|
687
|
+
}
|
package/src/cards.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { chromium } from "playwright";
|
|
2
|
-
import type { Card } from "./types.js";
|
|
2
|
+
import type { Brand, Card } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Rasterizes an intro/outro title card to a PNG with headless Chrome — same
|
|
@@ -13,7 +13,9 @@ export async function renderCardPng(
|
|
|
13
13
|
outPath: string,
|
|
14
14
|
logicalW: number,
|
|
15
15
|
logicalH: number,
|
|
16
|
-
scale: number
|
|
16
|
+
scale: number,
|
|
17
|
+
brand?: Brand,
|
|
18
|
+
logoDataUri?: string
|
|
17
19
|
): Promise<void> {
|
|
18
20
|
const browser = await chromium.launch({ channel: "chrome", headless: true });
|
|
19
21
|
const context = await browser.newContext({
|
|
@@ -22,7 +24,7 @@ export async function renderCardPng(
|
|
|
22
24
|
});
|
|
23
25
|
const page = await context.newPage();
|
|
24
26
|
try {
|
|
25
|
-
await page.setContent(html(card, logicalW), { waitUntil: "load" });
|
|
27
|
+
await page.setContent(html(card, logicalW, brand, logoDataUri), { waitUntil: "load" });
|
|
26
28
|
await page.screenshot({ path: outPath });
|
|
27
29
|
} finally {
|
|
28
30
|
await browser.close();
|
|
@@ -36,21 +38,27 @@ function esc(text: string): string {
|
|
|
36
38
|
.replace(/>/g, ">");
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
function html(card: Card, logicalW: number): string {
|
|
41
|
+
function html(card: Card, logicalW: number, brand?: Brand, logoDataUri?: string): string {
|
|
40
42
|
const background =
|
|
41
43
|
card.background ??
|
|
42
44
|
"radial-gradient(120% 140% at 20% 0%, #182036 0%, #0b0d16 55%, #07080f 100%)";
|
|
43
|
-
const accent = card.accent ?? "#6c8cff";
|
|
45
|
+
const accent = card.accent ?? brand?.accent ?? "#6c8cff";
|
|
46
|
+
const font = brand?.font ?? '-apple-system,"Helvetica Neue",Arial,sans-serif';
|
|
44
47
|
const titleSize = Math.round(logicalW / 17);
|
|
45
48
|
const subSize = Math.round(logicalW / 40);
|
|
46
49
|
const subtitle = card.subtitle
|
|
47
50
|
? `<div class="sub">${esc(card.subtitle)}</div>`
|
|
48
51
|
: "";
|
|
52
|
+
const logo = logoDataUri
|
|
53
|
+
? `<img class="logo" src="${logoDataUri}" alt="">`
|
|
54
|
+
: "";
|
|
49
55
|
return `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
50
56
|
html,body{margin:0;padding:0;width:100%;height:100%;}
|
|
51
57
|
body{background:${background};display:flex;align-items:center;justify-content:center;}
|
|
58
|
+
.logo{display:block;margin:0 auto ${Math.round(subSize * 1.4)}px;
|
|
59
|
+
max-width:${Math.round(logicalW * 0.18)}px;max-height:${Math.round(logicalW * 0.08)}px;}
|
|
52
60
|
.box{max-width:78%;text-align:center;
|
|
53
|
-
font-family
|
|
61
|
+
font-family:${font};
|
|
54
62
|
-webkit-font-smoothing:antialiased;}
|
|
55
63
|
.rule{width:${Math.round(logicalW / 22)}px;height:5px;border-radius:3px;
|
|
56
64
|
background:${accent};margin:0 auto ${Math.round(subSize * 1.2)}px;}
|
|
@@ -60,6 +68,7 @@ function html(card: Card, logicalW: number): string {
|
|
|
60
68
|
.sub{margin-top:${Math.round(subSize * 0.9)}px;font-size:${subSize}px;
|
|
61
69
|
line-height:1.4;font-weight:450;color:rgba(226,231,244,.72);}
|
|
62
70
|
</style></head><body><div class="box">
|
|
71
|
+
${logo}
|
|
63
72
|
<div class="rule"></div>
|
|
64
73
|
<div class="title">${esc(card.title)}</div>
|
|
65
74
|
${subtitle}
|