mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
feat(cli): capture-video on-demand fetcher + capture pipeline robustness
For the hyperframes.dev website-to-video flow. Real-AI-test runs against
heygen.com, huly.io, and heygen-showcase surfaced two gaps: (1) capture's
logo / asset-captioning signals missed modern React/Tailwind builds; and
(2) there was no CLI surface to pull the videos the manifest references.
New command:
• `hyperframes capture-video <project>` — on-demand downloader for
entries in capture/extracted/video-manifest.json. Capture writes the
manifest + preview PNGs but skips the mp4s; this pulls one entry by
`--index N` (matched against the entry's `index` field, NOT array
offset — gaps are possible when a preview screenshot fails). SSRF-safe
via safeFetch, 250 MB cap, content-type whitelist, race-free
exclusive-create write. Layout-aware (handles both standalone capture
and W2H project layouts).
Capture pipeline fixes:
• Structural logo signals (assetCataloger + tokenExtractor): inBanner /
inHomeLink / matchesTitleBrand. Class-substring alone caught 0/32 SVGs
on heygen.com — modern builds don't put 'logo' / 'brand' in any
className.
• Content-hash SVG slugs (assetDownloader): `svg-<8char-sha1>.svg` —
label-derived slugs mis-attributed partner-logo carousels
(heygen-logo.svg actually contained Google, hubspot-logo.svg contained
Trivago, etc.). Content-hash names are invariant by construction.
• SVG → PNG rasterization before Gemini Vision (contentExtractor): the
raw-SVG-as-text path was hallucinating wordmarks (VIVIENNE for HubSpot,
'wrestling' for Workday). Adds polarity detection so a white-glyph SVG
flattened to a blank PNG gets inverted before captioning. LOGO tag in
asset-descriptions.md when structural signals fire (independent of
Gemini key presence).
• Double-escape \/ inside the page.evaluate template literal in
assetCataloger + tokenExtractor: the original `/^https?:\/\/.../`
collapsed to `/` mid-template and threw `Unexpected token ^`. Capture
was 100% blocked on this until the escape was fixed.
• `asset-descriptions.md` header branches on Gemini-key presence with
an explicit 'Vision OFF — catalog-derived descriptions' warning.
New lint rule:
• `lintMissingLocalAsset` (cli/utils/lintProject): scans <video> / <img>
/ <source> src for local files that don't exist in the project.
Empirically the most common sub-agent mistake across multi-URL runs
(~5+ per run). Uses `resolveExistingLocalAsset` so the existence check
matches the bundler's notion of 'resolves'. Masks comment / style /
script ranges before scanning so a literal `<img src=missing.png>`
inside a tutorial comment isn't reported.
Tests: 17 new for capture-video (safeFilename decoding/sanitization,
VIDEO_CONTENT_TYPE_RE accept/reject, pickManifestEntry index-field lookup
with gaps, URL-mismatch + bad-index rejection, --index over --url
priority); 70 cases under lintProject.test.ts covering the new rule and
existing rules.
Sibling PRs in this stack:
• #PR_A1 — fix(producer): __dirname ESM banner shim
• #PR_A2 — fix(core/lint): findRootTag masks comment/style/script
This commit is contained in:
@@ -6,6 +6,10 @@ export const examples: Example[] = [
|
||||
["Capture a website", "hyperframes capture https://stripe.com"],
|
||||
["Capture to a specific directory", "hyperframes capture https://linear.app -o linear-video"],
|
||||
["JSON output for AI agents", "hyperframes capture https://example.com --json"],
|
||||
[
|
||||
"Pull a video from the captured manifest by index",
|
||||
"hyperframes capture video ./linear-video --index 0",
|
||||
],
|
||||
];
|
||||
|
||||
export default defineCommand({
|
||||
@@ -13,6 +17,9 @@ export default defineCommand({
|
||||
name: "capture",
|
||||
description: "Capture a website as editable HyperFrames components",
|
||||
},
|
||||
subCommands: {
|
||||
video: () => import("./capture/video.js").then((m) => m.default),
|
||||
},
|
||||
args: {
|
||||
url: {
|
||||
type: "positional",
|
||||
@@ -46,7 +53,9 @@ export default defineCommand({
|
||||
async run({ args }) {
|
||||
const url = args.url as string;
|
||||
|
||||
// Validate URL
|
||||
// citty fires parent's run AFTER routing to a subcommand; skip when args.url is a subcommand name.
|
||||
if (url === "video") return;
|
||||
|
||||
try {
|
||||
new URL(url);
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user