diff --git a/docs/guides/color-grading.mdx b/docs/guides/color-grading.mdx index 6961d959ff..56216efbdd 100644 --- a/docs/guides/color-grading.mdx +++ b/docs/guides/color-grading.mdx @@ -322,7 +322,7 @@ When grading a video, animate opacity on a wrapper element instead of directly o | Build a terminal/editorial character treatment | `effects.ascii` + `effects.asciiSize` + an optional two-color `palette` | | Build a restrained multi-color pixel treatment | `effects.dither` + `effects.ditherSize` + a two-to-six-color `palette` | | Add an organic light leak | Install the finite `organic-light-leak-overlay` Registry block | -| Build a freeze-frame cutout | Existing background removal + the `freeze-frame-cutout` Registry overlay block + host GSAP | +| Build a freeze-frame cutout | Existing background removal + the `freeze-frame-dressing` Registry overlay block + host GSAP | | Make a presenter float over graphics | Existing [Remove Background](/guides/remove-background) workflow | | Put text behind a presenter | Existing `remove-background --background-output` workflow | | Render HDR delivery files | Existing [HDR Rendering](/guides/hdr) workflow | diff --git a/packages/cli/src/registry/registryBlocks.test.ts b/packages/cli/src/registry/registryBlocks.test.ts index 05f0fb3974..b21e79784c 100644 --- a/packages/cli/src/registry/registryBlocks.test.ts +++ b/packages/cli/src/registry/registryBlocks.test.ts @@ -1,6 +1,8 @@ import { readFileSync, readdirSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { bundleToSingleHtml } from "@hyperframes/core/compiler"; +import { parseHTML } from "linkedom"; import { describe, expect, it } from "vitest"; const blocksDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../registry/blocks"); @@ -29,7 +31,7 @@ function findMissingLocalScripts(itemDir: string, manifest: RegistryManifest): s return missing; } -describe("registry block manifests", () => { +describe("registry blocks", () => { it("installs every local script referenced by a block composition", () => { const missing: string[] = []; @@ -48,4 +50,18 @@ describe("registry block manifests", () => { expect(missing).toEqual([]); }); + + it("keeps the Camcorder HUD seekable inside a differently named host composition", async () => { + const bundled = await bundleToSingleHtml(resolve(blocksDir, "camcorder-hud"), { + entryFile: "demo.html", + }); + const { document } = parseHTML(bundled); + const demo = document.getElementById("camcorder-hud-demo"); + const hud = document.getElementById("ch-demo-overlay"); + + expect(demo?.getAttribute("data-composition-id")).toBe("camcorder-hud-demo"); + expect(hud?.getAttribute("data-composition-id")).toBe("camcorder-hud"); + expect(hud?.hasAttribute("data-composition-src")).toBe(false); + expect(bundled).toContain('var __hfTimelineCompId = "camcorder-hud";'); + }); }); diff --git a/registry/blocks/camcorder-hud/camcorder-hud.html b/registry/blocks/camcorder-hud/camcorder-hud.html new file mode 100644 index 0000000000..f5bb2d024a --- /dev/null +++ b/registry/blocks/camcorder-hud/camcorder-hud.html @@ -0,0 +1,163 @@ + + + + + + + + +
+ +
+ + + + diff --git a/registry/blocks/camcorder-hud/demo-backdrop.jpg b/registry/blocks/camcorder-hud/demo-backdrop.jpg new file mode 100644 index 0000000000..63bd79fb30 Binary files /dev/null and b/registry/blocks/camcorder-hud/demo-backdrop.jpg differ diff --git a/registry/blocks/camcorder-hud/demo.html b/registry/blocks/camcorder-hud/demo.html new file mode 100644 index 0000000000..2b5e68cc74 --- /dev/null +++ b/registry/blocks/camcorder-hud/demo.html @@ -0,0 +1,73 @@ + + + + + + Camcorder HUD - Demo + + + + +
+ Creator recording a video +
+
+ + + + diff --git a/registry/blocks/camcorder-hud/registry-item.json b/registry/blocks/camcorder-hud/registry-item.json new file mode 100644 index 0000000000..41ebe3722e --- /dev/null +++ b/registry/blocks/camcorder-hud/registry-item.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://hyperframes.heygen.com/schema/registry-item.json", + "name": "camcorder-hud", + "type": "hyperframes:block", + "title": "Camcorder HUD", + "description": "Responsive REC, battery, editable date placeholder, and timeline-driven counter overlay for creator-camera and camcorder treatments", + "tags": [ + "camcorder", + "camera", + "recording", + "hud", + "overlay", + "creator", + "media-treatment-overlay" + ], + "dimensions": { + "width": 1920, + "height": 1080 + }, + "duration": 4, + "files": [ + { + "path": "camcorder-hud.html", + "target": "compositions/camcorder-hud.html", + "type": "hyperframes:composition" + } + ], + "preview": { + "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camcorder-hud.png", + "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camcorder-hud.mp4" + } +} diff --git a/registry/blocks/editorial-flash-overlay/demo-backdrop.jpg b/registry/blocks/editorial-flash-overlay/demo-backdrop.jpg new file mode 100644 index 0000000000..077705a002 Binary files /dev/null and b/registry/blocks/editorial-flash-overlay/demo-backdrop.jpg differ diff --git a/registry/blocks/editorial-flash-overlay/demo.html b/registry/blocks/editorial-flash-overlay/demo.html new file mode 100644 index 0000000000..4705557b2c --- /dev/null +++ b/registry/blocks/editorial-flash-overlay/demo.html @@ -0,0 +1,73 @@ + + + + + + + + +
+ +
+
+ + + + diff --git a/registry/blocks/editorial-flash-overlay/editorial-flash-overlay.html b/registry/blocks/editorial-flash-overlay/editorial-flash-overlay.html new file mode 100644 index 0000000000..9b2c8d4502 --- /dev/null +++ b/registry/blocks/editorial-flash-overlay/editorial-flash-overlay.html @@ -0,0 +1,129 @@ + + + + + + + + +
+ +
+ + + + diff --git a/registry/blocks/editorial-flash-overlay/registry-item.json b/registry/blocks/editorial-flash-overlay/registry-item.json new file mode 100644 index 0000000000..35f1ea891a --- /dev/null +++ b/registry/blocks/editorial-flash-overlay/registry-item.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://hyperframes.heygen.com/schema/registry-item.json", + "name": "editorial-flash-overlay", + "type": "hyperframes:block", + "title": "Editorial Flash Overlay", + "description": "Finite neutral-warm light layers for a seek-safe camera-flash cut or social reveal", + "tags": [ + "flash", + "editorial", + "social", + "light", + "transition", + "overlay", + "media-treatment-overlay" + ], + "dimensions": { + "width": 1920, + "height": 1080 + }, + "duration": 4, + "files": [ + { + "path": "editorial-flash-overlay.html", + "target": "compositions/editorial-flash-overlay.html", + "type": "hyperframes:composition" + } + ], + "preview": { + "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/editorial-flash-overlay.png", + "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/editorial-flash-overlay.mp4" + } +} diff --git a/registry/blocks/freeze-frame-dressing/demo-background.jpg b/registry/blocks/freeze-frame-dressing/demo-background.jpg new file mode 100644 index 0000000000..4f944ae6b4 Binary files /dev/null and b/registry/blocks/freeze-frame-dressing/demo-background.jpg differ diff --git a/registry/blocks/freeze-frame-dressing/demo-cutout.webp b/registry/blocks/freeze-frame-dressing/demo-cutout.webp new file mode 100644 index 0000000000..383d554f34 Binary files /dev/null and b/registry/blocks/freeze-frame-dressing/demo-cutout.webp differ diff --git a/registry/blocks/freeze-frame-dressing/demo.html b/registry/blocks/freeze-frame-dressing/demo.html new file mode 100644 index 0000000000..cd0f2e5c84 --- /dev/null +++ b/registry/blocks/freeze-frame-dressing/demo.html @@ -0,0 +1,102 @@ + + + + + + + + +
+ + +
+
+ + + + diff --git a/registry/blocks/freeze-frame-dressing/freeze-frame-dressing.html b/registry/blocks/freeze-frame-dressing/freeze-frame-dressing.html new file mode 100644 index 0000000000..458db7e892 --- /dev/null +++ b/registry/blocks/freeze-frame-dressing/freeze-frame-dressing.html @@ -0,0 +1,120 @@ + + + + + + + + +
+ + + + +
+ + + + diff --git a/registry/blocks/freeze-frame-dressing/registry-item.json b/registry/blocks/freeze-frame-dressing/registry-item.json new file mode 100644 index 0000000000..b2596eb7a7 --- /dev/null +++ b/registry/blocks/freeze-frame-dressing/registry-item.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://hyperframes.heygen.com/schema/registry-item.json", + "name": "freeze-frame-dressing", + "type": "hyperframes:block", + "title": "Freeze-Frame Dressing", + "description": "Timeline-driven paper, tape, and flash dressing for a freeze-frame or background-removed subject", + "tags": [ + "freeze-frame", + "cutout", + "scrapbook", + "paper", + "social", + "overlay", + "media-treatment-overlay" + ], + "dimensions": { + "width": 1920, + "height": 1080 + }, + "duration": 4, + "files": [ + { + "path": "freeze-frame-dressing.html", + "target": "compositions/freeze-frame-dressing.html", + "type": "hyperframes:composition" + } + ], + "preview": { + "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/freeze-frame-dressing.png", + "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/freeze-frame-dressing.mp4" + } +} diff --git a/registry/blocks/organic-light-leak-overlay/demo-backdrop.jpg b/registry/blocks/organic-light-leak-overlay/demo-backdrop.jpg new file mode 100644 index 0000000000..e7f2ba592a Binary files /dev/null and b/registry/blocks/organic-light-leak-overlay/demo-backdrop.jpg differ diff --git a/registry/blocks/organic-light-leak-overlay/demo.html b/registry/blocks/organic-light-leak-overlay/demo.html new file mode 100644 index 0000000000..f8d15dbaea --- /dev/null +++ b/registry/blocks/organic-light-leak-overlay/demo.html @@ -0,0 +1,77 @@ + + + + + + + + +
+ +
+
+ + + + diff --git a/registry/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html b/registry/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html new file mode 100644 index 0000000000..206e92f847 --- /dev/null +++ b/registry/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html @@ -0,0 +1,125 @@ + + + + + + + + +
+ +
+ + + + diff --git a/registry/blocks/organic-light-leak-overlay/registry-item.json b/registry/blocks/organic-light-leak-overlay/registry-item.json new file mode 100644 index 0000000000..1c5d2b2611 --- /dev/null +++ b/registry/blocks/organic-light-leak-overlay/registry-item.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://hyperframes.heygen.com/schema/registry-item.json", + "name": "organic-light-leak-overlay", + "type": "hyperframes:block", + "title": "Organic Light Leak Overlay", + "description": "Finite CSS light-leak overlay for memory beats and motivated transitions", + "tags": ["light-leak", "film", "memory", "transition", "overlay", "media-treatment-overlay"], + "dimensions": { + "width": 1920, + "height": 1080 + }, + "duration": 4, + "files": [ + { + "path": "organic-light-leak-overlay.html", + "target": "compositions/organic-light-leak-overlay.html", + "type": "hyperframes:composition" + } + ], + "preview": { + "poster": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/organic-light-leak-overlay.png", + "video": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/organic-light-leak-overlay.mp4" + } +} diff --git a/registry/registry.json b/registry/registry.json index 5513973ee9..19d8d68ae7 100644 --- a/registry/registry.json +++ b/registry/registry.json @@ -75,6 +75,22 @@ "name": "grain-overlay", "type": "hyperframes:component" }, + { + "name": "camcorder-hud", + "type": "hyperframes:block" + }, + { + "name": "editorial-flash-overlay", + "type": "hyperframes:block" + }, + { + "name": "organic-light-leak-overlay", + "type": "hyperframes:block" + }, + { + "name": "freeze-frame-dressing", + "type": "hyperframes:block" + }, { "name": "shimmer-sweep", "type": "hyperframes:component" diff --git a/scripts/generate-catalog-previews.ts b/scripts/generate-catalog-previews.ts index 70ec84f624..d4fecf2d97 100644 --- a/scripts/generate-catalog-previews.ts +++ b/scripts/generate-catalog-previews.ts @@ -42,6 +42,7 @@ import { createRenderJob, executeRenderJob, } from "../packages/producer/src/index.js"; +import { compileForRender } from "../packages/producer/src/services/htmlCompiler.js"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, ".."); @@ -90,10 +91,12 @@ function discoverItems(kindFilter: ItemKind | null, nameFilter: string | null): const manifestPath = join(sourceDir, "registry-item.json"); if (!existsSync(manifestPath)) continue; - // Blocks: find the first composition file. Components: use demo.html. + // Authored demos show transparent overlays against representative media. let entryFile: string; - if (kind === "component") { + if (existsSync(join(sourceDir, "demo.html"))) { entryFile = "demo.html"; + } else if (kind === "component") { + continue; } else { const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); const compFile = manifest.files?.find( @@ -123,7 +126,7 @@ function outputDir(kind: ItemKind): string { return resolve(repoRoot, "docs/images/catalog", typeDir); } -function prepareProjectDir(item: CatalogItem): string { +async function prepareProjectDir(item: CatalogItem): Promise { const tmpDir = join(tmpdir(), `hf-catalog-${item.name}-${Date.now()}`); mkdirSync(tmpDir, { recursive: true }); cpSync(item.sourceDir, tmpDir, { recursive: true }); @@ -170,7 +173,6 @@ function prepareProjectDir(item: CatalogItem): string { } } writeFileSync(join(tmpDir, "index.html"), content, "utf-8"); - return tmpDir; } } if (!existsSync(join(tmpDir, "index.html"))) { @@ -217,6 +219,13 @@ function prepareProjectDir(item: CatalogItem): string { writeFileSync(join(tmpDir, "index.html"), wrapper, "utf-8"); } + const indexPath = join(tmpDir, "index.html"); + const indexHtml = readFileSync(indexPath, "utf-8"); + if (indexHtml.includes("data-composition-src")) { + const compiled = await compileForRender(tmpDir, indexPath, join(tmpDir, "_downloads")); + writeFileSync(indexPath, compiled.html, "utf-8"); + } + return tmpDir; } @@ -259,9 +268,7 @@ async function generateThumbnail(item: CatalogItem, projectDir: string): Promise duration = 5; } - // Capture at 40% of duration for a representative frame - // Capture at 60% of duration so the animation is well underway. - // Cap at 3s to avoid overly-late captures on long compositions. + // Capture after the treatment appears, capped for long compositions. const captureTime = Math.min(3.0, duration * 0.6); const result = await captureFrame(session, 0, captureTime); cpSync(result.path, join(outDir, `${item.name}.png`)); @@ -327,7 +334,7 @@ async function main(): Promise { for (const item of items) { console.log(`[${item.kind}] ${item.name}`); - const projectDir = prepareProjectDir(item); + const projectDir = await prepareProjectDir(item); try { await generateThumbnail(item, projectDir); if (!skipVideo) { diff --git a/skills-manifest.json b/skills-manifest.json index 2dec1c9972..eaf2d70205 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -46,7 +46,7 @@ "files": 10 }, "media-use": { - "hash": "1234dcf993bbe1fc", + "hash": "470beb651f5a5068", "files": 151 }, "motion-graphics": { diff --git a/skills/media-use/references/media-treatment-recipes.md b/skills/media-use/references/media-treatment-recipes.md index 2a92a34436..37903ab58f 100644 --- a/skills/media-use/references/media-treatment-recipes.md +++ b/skills/media-use/references/media-treatment-recipes.md @@ -826,7 +826,7 @@ Extract the exact deterministic source frame first, then remove its background: ffmpeg -ss -i -frames:v 1 -y .media/generated/freeze-source.png npx hyperframes remove-background .media/generated/freeze-source.png \ -o .media/generated/freeze-cutout.png --json -npx hyperframes add freeze-frame-cutout --no-clipboard +npx hyperframes add freeze-frame-dressing --no-clipboard ``` Add the transparent result as a direct-root timed media layer and insert the