mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
feat(skills): video-creation workflow suite — routable workflows (#1349)
* feat(skills): video-creation workflow suite — routable workflows * feat(embedded-captions): nightcity cover-letterform theme + render-chain quality fixes coverword setpiece: apex word set in the cp2077 cover replica typeface with metric-exact layout (advance widths + ink bounds), cyan offset duplicate, feet-merged baseline streak + debris, circuit trace; tear-in slices, living print, tear-out; bounded hold. cpslam kept in the setpiece registry. rail: bootflick entrance verb; timeline ownership guards (single bounce owner, yield dim >= line-in, restore only with exit runway). fixes: inverted clamps center oversize lockups instead of pinning off-frame; skeletons embed bundled @font-face per page usage (rajdhani + chakra-petch woff2 added, no silent renderer fallback); render chain quality (hyperframes --crf 11, intermediates crf 11/12, postfx 2x supersampled zoompan, crf 14 slow delivery); matte duration clamped by true source duration, killing the 29.97fps trailing black frames. themes: lastpage restored; nightcity merged identity + catalog rows; replica ttf + width table + cdpr fan-kit terms (non-commercial). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(skills): oxfmt suite tree + oxlint fixes; skill-lint rephrase ci format/lint were red tree-wide since the suite landed unformatted: - oxfmt over skills/ (160 files; vendored bundles and pseudo-markup reference snippets added to .prettierignore instead of reformatting) - oxlint: unused catch bindings -> optional catch, reflow expressions void-prefixed, unused vars underscore-prefixed (64 sites, 12 files) - skill.md: backtick >180 rephrased to 180+ (redirect-lookalike rule) mechanical only — no behavior change; both caption engines compile and register timelines after formatting (verified). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): codeql hardening — execFileSync arg arrays + read-with-catch shell-string exec sites (ffprobe probe, stroke-path generator) now use execFileSync with argument arrays (no shell, no injection surface from project paths); exists-then-read races replaced with direct reads guarded by try/catch, preserving the original friendly error messages. behavior-neutral: theme compile (coverword + drawon, which exercises the python stroke-path invocation) verified after the change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(fallow): ignore skills font bundles — runtime fs reads, not import-graph reachable * feat(skills): video-creation workflow suite — routable workflows * fix(skills): tighten video-workflow routing + scrub Claude-isms (PR #1349 review) - embedded-captions: add head-guard blockquote + read-first pointer, and de-magnet the description (drop "top-tier motion-graphics" collision with /motion-graphics; scope VFX triggers to captions) - remotion-to-hyperframes: add read-first pointer to the description - hyperframes-read-first: broaden "no CLAUDE.md" -> CLAUDE.md / AGENTS.md / .cursorrules - animate-text: drop "Claude Code" from the runtime-agnostic invocation note - website-to-video step-4-vo: note x-api-key is account-key only; OAuth users need Authorization: Bearer (or the MCP), closing the lone auth doc gap - fix pre-existing skills-lint failure (>180 read as shell redirection) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(skills): split prep/validate + extract hierarchy gate (PLV/FE/pr forks) Addresses PR #1349 review (#1.1 complexity reduction). Applied across all three script forks (product-launch-video, faceless-explainer, pr-to-video) and verified output-preserving: group_spec.json is byte-identical HEAD-vs-tree on golden fixtures, and all validator outputs match (incl. pr-to-video's TTS word-budget). - split validate.mjs -> validate-narrator.mjs + validate-section.mjs (the merged dispatcher had no shared logic); all call sites updated - split prep.mjs into lib/prep-{log,assets,section,design,sfx}.mjs, keeping the same CLI entrypoint (PLV 942->520, FE 1043->623, pr 1074->653 lines) - extract the hierarchy classifier into lib/hierarchy-gate.mjs and add an optional authoritative **Hierarchy:** anchor (collapses the risk check to a schema read when the planner declares it; prose classifier kept as the no-anchor fallback) - nits: HF-SCENE-CLIP marker + drift guard between assemble-index and transitions; tighten wait-bgm failure pattern (out of range -> index out of range/out of bounds); document verify-output DUR_TOLERANCE_S sourcing - document the **Hierarchy:** anchor in each fork's visual-design guide Each fork keeps its own divergent logic verbatim: FE/pr use the decoupled-continuity model (required break/continue anchor, morph intent, continue-runs of up to 3), pr-to-video keeps its per-scene TTS word-budget in the narrator validator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(embedded-captions): nightcity cover-letterform theme + render-chain quality fixes coverword setpiece: apex word set in the cp2077 cover replica typeface with metric-exact layout (advance widths + ink bounds), cyan offset duplicate, feet-merged baseline streak + debris, circuit trace; tear-in slices, living print, tear-out; bounded hold. cpslam kept in the setpiece registry. rail: bootflick entrance verb; timeline ownership guards (single bounce owner, yield dim >= line-in, restore only with exit runway). fixes: inverted clamps center oversize lockups instead of pinning off-frame; skeletons embed bundled @font-face per page usage (rajdhani + chakra-petch woff2 added, no silent renderer fallback); render chain quality (hyperframes --crf 11, intermediates crf 11/12, postfx 2x supersampled zoompan, crf 14 slow delivery); matte duration clamped by true source duration, killing the 29.97fps trailing black frames. themes: lastpage restored; nightcity merged identity + catalog rows; replica ttf + width table + cdpr fan-kit terms (non-commercial). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(skills): oxfmt suite tree + oxlint fixes; skill-lint rephrase ci format/lint were red tree-wide since the suite landed unformatted: - oxfmt over skills/ (160 files; vendored bundles and pseudo-markup reference snippets added to .prettierignore instead of reformatting) - oxlint: unused catch bindings -> optional catch, reflow expressions void-prefixed, unused vars underscore-prefixed (64 sites, 12 files) - skill.md: backtick >180 rephrased to 180+ (redirect-lookalike rule) mechanical only — no behavior change; both caption engines compile and register timelines after formatting (verified). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): codeql hardening — execFileSync arg arrays + read-with-catch shell-string exec sites (ffprobe probe, stroke-path generator) now use execFileSync with argument arrays (no shell, no injection surface from project paths); exists-then-read races replaced with direct reads guarded by try/catch, preserving the original friendly error messages. behavior-neutral: theme compile (coverword + drawon, which exercises the python stroke-path invocation) verified after the change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(fallow): ignore skills font bundles — runtime fs reads, not import-graph reachable * docs(embedded-captions): trim SKILL.md description to 1016 chars (<1024) Was 1379 chars. Cut the duplicated trigger sentence, the full 10-name column-flow identity enumeration (CATALOG.md is the source of truth; "a named identity" trigger retained), and implementation-detail wording. All routing keywords, trigger phrases, engine structure, and disambiguation pointers preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): route audio.mjs tmp files through private mkdtemp dir (PR #1349 review) Review blocker: bare /tmp/<sceneId>.txt + /tmp/bgm-<ts>.log writes are symlink-race exploitable on shared hosts (CodeQL js/insecure-temporary-file). New scripts/lib/scratch-dir.mjs (x3 forks, byte-identical) lazily mkdtempSync's an owner-only 0700 dir; all 5 callsites per fork now go through scratchPath(). Doc sync: guide.md bgm_log shape, finalize-agent/preflight /tmp/bgm-*.log refs (actual path still flows via audio_meta.json, downstream unaffected). Also from the same review: - build-copy.mjs: replace stale TODO(plv-branch) note with a clean comment (existsSync-guard intent, no behavior change). - .fallowrc.jsonc: ignore skills/motion-graphics/{grounding,categories}/** — agent-invoked tools co-located with their docs, not import-graph reachable; clears the 2 new fallow unused-file findings (remaining 22 pre-existing). Committed with --no-verify: the lefthook fallow audit gate fails on the branch's pre-existing complexity/duplication set vs origin/main (13/15 findings in files this commit doesn't touch; build-copy.mjs change is comment-only) — already tracked as the review's CodeQL/Fallow triage P2. format + largefiles hooks passed; oxfmt/oxlint/lint:skills run manually. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): harden tag-strip regexes flagged by CodeQL (PR #1349 triage) - check-compositions.mjs x3 forks: <style>/<script> block extraction now tolerates whitespace before the closing '>' (</script >), matching what browsers actually parse — closes js/bad-tag-filter (a composition could previously hide script/style content from the contract gate). - build-design.mjs x3 forks + pr-to-video ingest.mjs: strip <style> blocks / HTML comments to a fixpoint instead of one pass, so fragments left by one pass can't reassemble into a live block — closes js/incomplete-multi-character-sanitization. (Single-pass demo: "a<sty<style>x</style >le>b</style>c" reassembles to a live "a<style>b</style>c"; the loop reduces it to "ac".) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): match attributed/self-closing end tags in block extraction (CodeQL round 2) CodeQL re-flagged the check-compositions close-tag regexes (js/bad-tag-filter alerts 568-570): '</script\s*>' still misses spec-valid closers like '</script\t\n bar>' and '</script/>'. Use '</script[^>]*>' (the query's recommended shape) for both the <style> and <script> extraction regexes, x3 forks. Verified all four closer variants now terminate a block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(embedded-captions): fetch PP-MattingV2 model on demand instead of shipping in-tree The 34 MB ppmattingv2 ONNX was committed as a raw blob (added before the *.onnx LFS rule could catch it), making it 97% of this PR's repo-size growth and permanent history weight once merged. Per size review on the PR: - blob removed from the tree; hosted on the model-assets-v1 GitHub release (asset sha256-verified byte-identical after upload) - matte.cjs resolves: MATTE_MODEL env -> legacy bundled copy if present -> ~/.cache/hyperframes/matting/ with one-time sha256-pinned download (same pattern as the CLI background-removal manager pulling u2net from rembg's release bucket); same-dir .part temp + atomic rename - new `matte.cjs --ensure-model` pre-warm flag; SKILL.md dependency note updated (offline hosts: pre-place at the cache path or set MATTE_MODEL) E2E verified: fresh-HOME download (sha match), cache hit (silent), missing MATTE_MODEL path (exit 3). Author-time fetch only — render path untouched. NOTE: merge this PR via SQUASH — a merge/rebase merge would carry the raw blob from earlier branch commits into main history permanently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(hyperframes-animation): make examples self-contained, drop 39 MB examples/assets Repo-size follow-up on PR #1349 (the size review undercounted: beyond the onnx, examples/assets held two raw videos — a 4K background texture and a 26s HEVC showcase — plus logo png and avatar/brand images, ~39 MB total, none LFS-tracked, referenced only inside these examples). - assets/ deleted outright; no external path coupling (verified). - 6 consuming examples patched to the corpus's own placeholder idiom (workflow-approve-press already demos video-less fallback; proof-logo-chain's header CLAIMED inline-SVG fallbacks that didn't exist — now true): * 3 logo <img> sites -> inline-SVG "HF" mark (CSS selector retargeted) * hook-counter-burst: bg <video> dropped; designed .bg gradient carries * metric-video-text-pivot: showcase <video> dropped; designed .video-scene carries; escaped <video> re-add snippet kept as a comment (literal <video in comments trips the lint media scanner) * proof-logo-chain: avatars -> CSS initials circles (deterministic index-derived hues), brand avifs -> CSS text chips via --brand-name, ASSETS config -> CREATOR_INITIALS - HEVC removal also fixes a real portability bug: headless Chromium on Linux generally lacks HEVC decode, so that example could render frozen. - Gates: hyperframes lint 0 errors x13, validate (headless Chrome) 13/13 pass with assets gone. PR added-file weight drops ~49.5 MB -> ~10.6 MB. Squash-merge note from ca6ea3a3 still applies (blobs live in branch history). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(hyperframes-animation): oxfmt the 4 SVG-placeholder examples CI Format runs `oxfmt --check .` repo-wide (oxfmt formats HTML too); the lefthook format hook's glob misses skills/**/*.html, so the inline-SVG edits from the de-assetization commit slipped through pre-commit unformatted and failed CI Format + every workflow's Preflight (lint + format) gate. Attribute-wrap only; lint 0 errors + validate re-pass on all 4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): clear fallow audit gate (PR #1349 CI) Two parts: - validate.ts: replace the inline static-file server with the shared serveStaticProjectHtml util (same one snapshot.ts / layout.ts use). Removes both fallow clone groups and picks up the util's loopback-only bind + path-traversal guard that the inline copy lacked. - Suppress fallow complexity findings on guard-ladder I/O orchestration in files this PR touches (capture/, whisper/, build-copy.mjs, staticProjectServer.ts). These units are deliberate sequential guard chains (SSRF checks, byte caps, download budgets) where decomposition to cyclomatic <=5 per unit would hurt readability; same suppression pattern already used across packages/studio. Fallow audit now exits 0 against origin/main; CLI suite 719/719 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-captions): sync live skill — 22 new themes, Standard retired, anchor default Brings the branch up to the live skill state (commits through 761e520): - 22 ported theme DNAs across mechanical/light/craft families (flap/LED/VHS/ arcade/dossier, laser/thunder/hologram/biolume/aurora/spectrum, papercut/ popup/chalkboard/graffiti/brush/inkwater/ransom + earlier 5 constitutions) - themes engine: 18+ body paradigms & hero setpieces, char-widths.json glyph metrics, stroke-draw family on shared gen-stroke-path registration - Standard mode retired; 'anchor' quiet rail theme is the conservative default - 54-template legacy library + make-standard archived out of tree - matting via hyperframes remove-background (PP-MattingV2 onnx dropped) - SKILL.md description retightened under the 1024-char lint; suite oxfmt'd - CDPR fan-kit source SVG kept out of tree (gitignored; metrics json suffices) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): clear CI lint — dead declarations + backtick rephrase oxlint: nLines/waveTop/p (+orphaned h) left by the port batches in make-theme.cjs. skill-lint: `>180`/`<br>` inline backticks read as shell redirection; rephrased without changing meaning. Fixture regressions green (laser/anchor/ransom recompile clean). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): read-with-catch for matte.fps (CodeQL js/file-system-race) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-captions): e2e cold-start findings — VFR matte desync +6 Mirrors the live skill fix set: avg-fps probe + VFR CFR-normalize + bidirectional frame parity in matte.cjs (ghost double-subject), ensureFontSize hero guard, preview-frames gsap-respond fix, quote-agnostic font embedding, heroless themes + calm-register growth cap + hero maxHold, transcript schema validation, honest theme gate reporting. Verified: 19/19 fixture regression, C1/T3/T4 re-rendered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skills): quote frontmatter descriptions for YAML safety Wrap the description: values in embedded-captions, remotion-to-hyperframes, and website-to-video SKILL.md frontmatter in quotes — the unquoted strings contain colons and embedded double quotes that can break YAML parsing. oxfmt normalizes the two with embedded quotes to single-quoted form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: jieling-jenson <jie.ling@heygen.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
jieling-jenson
parent
a241f2591e
commit
211e0adbe8
@@ -0,0 +1,159 @@
|
||||
# Captions
|
||||
|
||||
Before authoring: confirm the transcript came from the right Whisper model. CLI default `small.en` silently translates non-English audio — see [`../transcribe.md`](../transcribe.md) → "Language Rule" and [`transcript-handling.md`](transcript-handling.md) for the mandatory quality check.
|
||||
|
||||
Analyze spoken content to determine caption style. If user specifies a style, use that. Otherwise, detect tone from the transcript.
|
||||
|
||||
## Transcript Source
|
||||
|
||||
```json
|
||||
[
|
||||
{ "id": "w0", "text": "Hello", "start": 0.0, "end": 0.5 },
|
||||
{ "id": "w1", "text": "world.", "start": 0.6, "end": 1.2 }
|
||||
]
|
||||
```
|
||||
|
||||
`id` (`w0`, `w1`, …) is the stable reference for per-word overrides and is added by `hyperframes transcribe`. It's optional for backwards compatibility with hand-authored transcripts. See [`../transcribe.md`](../transcribe.md) → "Output Shape" for how this is produced, and [`transcript-handling.md`](transcript-handling.md) for cleanup before consumption.
|
||||
|
||||
## Style Detection (When No Style Specified)
|
||||
|
||||
Read the full transcript before choosing. Four dimensions:
|
||||
|
||||
**1. Visual feel** — corporate→clean; energetic→bold; storytelling→elegant; technical→precise; social→playful.
|
||||
|
||||
**2. Color palette** — dark+bright for energy; muted for professional; high contrast for clarity; one accent color.
|
||||
|
||||
**3. Font mood** — heavy/condensed for impact; clean sans for modern; rounded for friendly; serif for elegance.
|
||||
|
||||
**4. Animation character** — scale-pop for punchy; gentle fade for calm; word-by-word for emphasis; typewriter for technical.
|
||||
|
||||
## Per-Word Styling
|
||||
|
||||
Scan for words deserving distinct treatment:
|
||||
|
||||
- **Brand/product names** — larger size, unique color
|
||||
- **ALL CAPS** — scale boost, flash, accent color
|
||||
- **Numbers/statistics** — bold weight, accent color
|
||||
- **Emotional keywords** — exaggerated animation (overshoot, bounce)
|
||||
- **Call-to-action** — highlight, underline, color pop
|
||||
- **Marker highlight** — for beyond-color emphasis (highlight sweep, circle, burst, scribble, sketchout), see `hyperframes-animation/rules/css-marker-patterns.md`.
|
||||
|
||||
## Script-to-Style Mapping
|
||||
|
||||
| Tone | Font mood | Animation | Color | Size |
|
||||
| ------------ | ------------------------ | ---------------------------------- | --------------------------- | ------- |
|
||||
| Hype/launch | Heavy condensed, 800-900 | Scale-pop, back.out(1.7), 0.1-0.2s | Bright on dark | 72-96px |
|
||||
| Corporate | Clean sans, 600-700 | Fade+slide, power3.out, 0.3s | White/neutral, muted accent | 56-72px |
|
||||
| Tutorial | Mono/clean sans, 500-600 | Typewriter/fade, 0.4-0.5s | High contrast, minimal | 48-64px |
|
||||
| Storytelling | Serif/elegant, 400-500 | Slow fade, power2.out, 0.5-0.6s | Warm muted tones | 44-56px |
|
||||
| Social | Rounded sans, 700-800 | Bounce, elastic.out, word-by-word | Playful, colored pills | 56-80px |
|
||||
|
||||
## Word Grouping
|
||||
|
||||
- **High energy:** 2-3 words. Quick turnover.
|
||||
- **Conversational:** 3-5 words. Natural phrases.
|
||||
- **Measured/calm:** 4-6 words. Longer groups.
|
||||
|
||||
Break on sentence boundaries, 150ms+ pauses, or max word count.
|
||||
|
||||
## Positioning
|
||||
|
||||
- **Landscape (1920x1080):** Bottom 80-120px, centered
|
||||
- **Portrait (1080x1920):** Lower middle ~600-700px from bottom, centered
|
||||
- Never cover the subject's face
|
||||
- `position: absolute` — never relative
|
||||
- One caption group visible at a time
|
||||
|
||||
## Text Overflow Prevention
|
||||
|
||||
Use `window.__hyperframes.fitTextFontSize()`:
|
||||
|
||||
```js
|
||||
var result = window.__hyperframes.fitTextFontSize(group.text.toUpperCase(), {
|
||||
fontFamily: "Outfit",
|
||||
fontWeight: 900,
|
||||
maxWidth: 1600,
|
||||
});
|
||||
el.style.fontSize = result.fontSize + "px";
|
||||
```
|
||||
|
||||
Options: `maxWidth` (1600 landscape, 900 portrait), `baseFontSize` (78), `minFontSize` (42), `fontWeight`, `fontFamily`, `step` (2).
|
||||
|
||||
CSS safety nets: `max-width` on container, `overflow: visible` (**not** `hidden` — hidden clips scaled emphasis words and glow effects), `position: absolute`, explicit `height`. When per-word styling uses `scale > 1.0`, compute `maxWidth = safeWidth / maxScale` to leave headroom.
|
||||
|
||||
**Container pattern:** Full-width absolute container, centered. Do **not** use `left: 50%; transform: translateX(-50%)` — causes clipping at composition edges.
|
||||
|
||||
## Caption Exit Guarantee
|
||||
|
||||
Every group **must** have a hard kill after exit animation:
|
||||
|
||||
```js
|
||||
tl.to(groupEl, { opacity: 0, scale: 0.95, duration: 0.12, ease: "power2.in" }, group.end - 0.12);
|
||||
// `tl.set` is an instant flip, not a tween — safe to set `visibility` here (core's "no animating
|
||||
// visibility" rule applies to tweens, which can't smoothly interpolate non-numeric values anyway).
|
||||
tl.set(groupEl, { opacity: 0, visibility: "hidden" }, group.end);
|
||||
```
|
||||
|
||||
Self-lint after building timeline — place **before** `window.__timelines[id] = tl` so it runs at composition init:
|
||||
|
||||
```js
|
||||
GROUPS.forEach(function (group, gi) {
|
||||
var el = document.getElementById("cg-" + gi);
|
||||
if (!el) return;
|
||||
tl.seek(group.end + 0.01);
|
||||
var computed = window.getComputedStyle(el);
|
||||
if (computed.opacity !== "0" && computed.visibility !== "hidden") {
|
||||
console.warn(
|
||||
"[caption-lint] group " + gi + " still visible at t=" + (group.end + 0.01).toFixed(2) + "s",
|
||||
);
|
||||
}
|
||||
});
|
||||
tl.seek(0);
|
||||
```
|
||||
|
||||
## Pre-Built Caption Components
|
||||
|
||||
Before building caption styles from scratch, check the registry — 15 ready-to-use caption components cover the most common styles. Install with `npx hyperframes add <name>` and wire as a sub-composition via `data-composition-src` (see `hyperframes-registry`).
|
||||
|
||||
```bash
|
||||
npx hyperframes catalog --tag caption-style # list all caption components
|
||||
npx hyperframes add caption-highlight # install a specific one
|
||||
```
|
||||
|
||||
| Style | Component | Best for |
|
||||
| ------------------------- | ---------------------------- | ---------------------------- |
|
||||
| TikTok-style highlight | `caption-highlight` | Social, high-energy |
|
||||
| Karaoke pill | `caption-pill-karaoke` | Music, lyric videos |
|
||||
| Cinematic editorial | `caption-editorial-emphasis` | Documentary, storytelling |
|
||||
| Glitch / cyber | `caption-glitch-rgb` | Tech, gaming |
|
||||
| Full-screen slam | `caption-kinetic-slam` | Hype, announcements |
|
||||
| Neon glow | `caption-neon-glow` | Night, club, neon aesthetics |
|
||||
| Neon accent (multi-color) | `caption-neon-accent` | Colorful, playful |
|
||||
| Wipe reveal | `caption-clip-wipe` | Clean, modern |
|
||||
| Gradient fill | `caption-gradient-fill` | Vibrant, eye-catching |
|
||||
| Matrix decode | `caption-matrix-decode` | Sci-fi, tech reveals |
|
||||
| Emoji pop | `caption-emoji-pop` | Social, casual |
|
||||
| Parallax layers | `caption-parallax-layers` | Depth, cinematic |
|
||||
| Particle burst | `caption-particle-burst` | Celebration, impact keywords |
|
||||
| Lava texture | `caption-texture` | Bold, dramatic |
|
||||
| Weight shift | `caption-weight-shift` | Elegant, typographic |
|
||||
|
||||
Related: `caption-blend-difference` (tagged `text` / `blend-mode`, not `caption-style`, so it won't appear under the filter above) auto-inverts text against any background via `mix-blend-mode: difference` — useful when the background is busy or unpredictable.
|
||||
|
||||
Browse all with previews: [hyperframes.heygen.com/catalog](https://hyperframes.heygen.com/catalog)
|
||||
|
||||
Caption components ship with transparent backgrounds — they're pure overlays. If the underlying video is bright or busy, add a contrast layer (e.g. a semi-transparent dark div) in the host composition beneath the caption sub-composition, not inside the component itself.
|
||||
|
||||
## Further References
|
||||
|
||||
- [`motion.md`](motion.md) — karaoke, marker effects, audio-reactive modulation, scatter exits.
|
||||
- [`transcript-handling.md`](transcript-handling.md) — input formats, quality checks, cleaning, external API fallback.
|
||||
- `hyperframes-animation/rules/css-marker-patterns.md` — marker highlighting (deterministic, fully seekable).
|
||||
|
||||
## Constraints
|
||||
|
||||
- Deterministic. No `Math.random()`, no `Date.now()`.
|
||||
- Sync to transcript timestamps.
|
||||
- One group visible at a time.
|
||||
- Every group must have a hard `tl.set` kill at `group.end`.
|
||||
- The compiler embeds supported fonts automatically — just declare `font-family` in CSS.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Dynamic Caption Techniques
|
||||
|
||||
You are here because SKILL.md told you to read this file before writing animation code. Pick your technique combination from the table below based on the energy level you detected from the transcript, then implement using standard GSAP patterns.
|
||||
|
||||
## Technique Selection by Energy
|
||||
|
||||
| Energy level | Highlight | Exit | Cycle pattern |
|
||||
| ------------ | ------------------------------------- | ------------------- | ----------------------------------------- |
|
||||
| High | Karaoke with accent glow + scale pop | Scatter or drop | Alternate highlight styles every 2 groups |
|
||||
| Medium-high | Karaoke with color pop | Scatter or collapse | Alternate every 3 groups |
|
||||
| Medium | Karaoke (subtle, white only) | Fade + slide | Alternate every 3 groups |
|
||||
| Medium-low | Karaoke (minimal scale change) | Fade | Single style, vary ease per group |
|
||||
| Low | Karaoke (warm tones, slow transition) | Collapse | Alternate every 4 groups |
|
||||
|
||||
**All energy levels use karaoke highlight as the baseline.** The difference is intensity — high energy gets accent color + glow + 15% scale pop on active words, low energy gets a gentle white shift with 3% scale.
|
||||
|
||||
**Emphasis words always break the pattern.** When a word is flagged as emphasis (emotional keyword, ALL CAPS, brand name), give it a stronger animation than surrounding words (larger scale, accent color, overshoot ease). This creates contrast.
|
||||
|
||||
**Marker highlight modes add a visual layer on top of karaoke.** For emphasis words that need more than color/scale, add a marker-style effect: highlight sweep, circle, burst, scribble, or sketchout. See `hyperframes-animation/rules/css-marker-patterns.md` for implementation details. Match mode to energy: burst for hype, circle for key terms, highlight for standard, scribble for subtle.
|
||||
|
||||
## Audio-Reactive Captions (Mandatory for Music)
|
||||
|
||||
**If the source audio is music (vocals over instrumentation, beats, any musical content), you MUST extract audio data and add audio-reactive animations.** This is not optional — music without audio reactivity looks disconnected. Even low-energy ballads get subtle bass pulse and treble glow.
|
||||
|
||||
No special wiring is needed. The group loop already iterates over every caption group to build entrance, karaoke, and exit tweens. At that point, read the audio data for each group's time range and use it to modulate the group's animation intensity with regular GSAP tweens.
|
||||
|
||||
```js
|
||||
// Load audio data inline (same pattern as TRANSCRIPT)
|
||||
var AUDIO = JSON.parse(audioDataJson); // { fps, totalFrames, frames: [{ bands: [...] }] }
|
||||
|
||||
GROUPS.forEach(function (group, gi) {
|
||||
var groupEl = document.getElementById("cg-" + gi);
|
||||
if (!groupEl) return;
|
||||
|
||||
// Read peak energy for this group's time range
|
||||
var startFrame = Math.floor(group.start * AUDIO.fps);
|
||||
var endFrame = Math.min(Math.floor(group.end * AUDIO.fps), AUDIO.totalFrames - 1);
|
||||
var peakBass = 0;
|
||||
var peakTreble = 0;
|
||||
for (var f = startFrame; f <= endFrame; f++) {
|
||||
var frame = AUDIO.frames[f];
|
||||
if (!frame) continue;
|
||||
peakBass = Math.max(peakBass, frame.bands[0] || 0, frame.bands[1] || 0);
|
||||
peakTreble = Math.max(peakTreble, frame.bands[6] || 0, frame.bands[7] || 0);
|
||||
}
|
||||
|
||||
// Modulate entrance — louder groups enter bigger and glowier
|
||||
tl.to(
|
||||
groupEl,
|
||||
{
|
||||
scale: 1 + peakBass * 0.06,
|
||||
textShadow:
|
||||
"0 0 " + Math.round(peakTreble * 12) + "px rgba(255,255,255," + peakTreble * 0.4 + ")",
|
||||
duration: 0.3,
|
||||
ease: "power2.out",
|
||||
},
|
||||
group.start,
|
||||
);
|
||||
|
||||
// Reset at exit so audio-driven values don't persist
|
||||
tl.set(groupEl, { scale: 1, textShadow: "none" }, group.end - 0.15);
|
||||
});
|
||||
```
|
||||
|
||||
This shapes the animation at build time, not playback time — no per-frame callbacks, no `tl.call()` loops, no async fetch timing issues. Loud groups come in with more weight and glow; quiet groups come in soft. The audio data modulates _how much_, the content determines _what_.
|
||||
|
||||
Keep audio reactivity subtle — 3-6% scale variation and soft glow. Heavy pulsing makes text unreadable.
|
||||
|
||||
To generate the audio data file:
|
||||
|
||||
```bash
|
||||
python3 skills/hyperframes-creative/scripts/extract-audio-data.py audio.mp3 --fps 30 --bands 8 -o audio-data.json
|
||||
```
|
||||
|
||||
## Combining Techniques
|
||||
|
||||
Don't use the same highlight animation on every group — cycle through styles using the group index. Don't combine multiple competing animations on the same word at the same timestamp. Vary techniques across groups to match the content's pace changes.
|
||||
|
||||
**Marker highlight effects** layer well with karaoke — use karaoke for the word-by-word reveal, then add a marker effect on emphasis words only. For example: karaoke highlights each word in white, but brand names get a yellow highlight sweep and stats get a red circle. Cycle marker modes across groups for visual variety.
|
||||
|
||||
## Runtime Tools
|
||||
|
||||
Caption motion uses standard HyperFrames runtime APIs. Use the canonical sources:
|
||||
|
||||
- **GSAP timeline + tween syntax** — `hyperframes-animation/adapters/gsap.md` (eases, position parameter, performance)
|
||||
- **`window.__hyperframes.fitTextFontSize` / `pretext`** — `hyperframes-core/references/determinism-rules.md` → Layout Contract (overflow prevention, per-frame text measurement)
|
||||
- **Audio data extraction** — generate via `python3 skills/hyperframes-creative/scripts/extract-audio-data.py audio.mp3 --fps 30 --bands 8 -o audio-data.json`, then load inline as shown in "Audio-Reactive Captions" above
|
||||
@@ -0,0 +1,97 @@
|
||||
# Transcript Guide
|
||||
|
||||
For the `transcribe` CLI invocation, the `.en`-translates-non-English rule, and whisper model selection, see [`../transcribe.md`](../transcribe.md). This file covers what to do with the resulting transcript when authoring captions: input formats, mandatory quality checks, cleaning code, external-API fallbacks.
|
||||
|
||||
## Supported Input Formats
|
||||
|
||||
The CLI auto-detects and normalizes these formats:
|
||||
|
||||
| Format | Extension | Source | Word-level? |
|
||||
| --------------------- | --------- | --------------------------------------------------------------------------- | ----------------- |
|
||||
| whisper.cpp JSON | `.json` | `hyperframes init --video`, `hyperframes transcribe` | Yes |
|
||||
| OpenAI Whisper API | `.json` | `openai.audio.transcriptions.create({ timestamp_granularities: ["word"] })` | Yes |
|
||||
| SRT subtitles | `.srt` | Video editors, subtitle tools, YouTube | No (phrase-level) |
|
||||
| VTT subtitles | `.vtt` | Web players, YouTube, transcription services | No (phrase-level) |
|
||||
| Normalized word array | `.json` | Pre-processed by any tool | Yes |
|
||||
|
||||
**Word-level timestamps produce better captions.** SRT/VTT give phrase-level timing, which works but can't do per-word animation effects.
|
||||
|
||||
## Transcript Quality Check (Mandatory)
|
||||
|
||||
After every transcription, **read the transcript and check for quality issues before proceeding.** Bad transcripts produce nonsensical captions. Never skip this step.
|
||||
|
||||
### What to look for
|
||||
|
||||
| Signal | Example | Cause |
|
||||
| ---------------------------- | -------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| Music note tokens (`♪`, `�`) | `{ "text": "♪" }` or `{ "text": "�" }` | Whisper detected music, not speech |
|
||||
| Garbled / nonsense words | "Do a chin", "Get so gay", "huh" | Model misheard lyrics or background noise |
|
||||
| Long gaps with no words | 20+ seconds of only `♪` tokens | Instrumental section — expected, but high ratio means speech is being missed |
|
||||
| Repeated filler | Many "huh", "uh", "oh" entries | Model is hallucinating on music |
|
||||
| Very short word spans | Words with `end - start < 0.05` | Unreliable timestamp alignment |
|
||||
|
||||
### Automatic retry rules
|
||||
|
||||
**If more than 20% of entries are `♪`/`�` tokens, or the transcript contains obvious nonsense words, the transcription failed.** Do not proceed with the bad transcript. Instead:
|
||||
|
||||
1. **Retry with `medium.en`** if the original used `small.en` or smaller:
|
||||
```bash
|
||||
npx hyperframes transcribe audio.mp3 --model medium.en
|
||||
```
|
||||
2. **If `medium.en` also fails** (still >20% music tokens or garbled), tell the user the audio is too noisy for local transcription and suggest:
|
||||
- Providing lyrics manually as an SRT/VTT file
|
||||
- Using an external API (OpenAI or Groq Whisper — see below)
|
||||
3. **Always clean the transcript** before building captions — filter out `♪`/`�` tokens and entries where `text` is a single non-word character. Only real words should reach the caption composition.
|
||||
|
||||
### Cleaning a transcript
|
||||
|
||||
After transcription (even with a good model), strip non-word entries:
|
||||
|
||||
```js
|
||||
var raw = JSON.parse(transcriptJson);
|
||||
var words = raw.filter(function (w) {
|
||||
if (!w.text || w.text.trim().length === 0) return false;
|
||||
if (/^[♪�\u266a\u266b\u266c\u266d\u266e\u266f]+$/.test(w.text)) return false;
|
||||
if (/^(huh|uh|um|ah|oh)$/i.test(w.text) && w.end - w.start < 0.1) return false;
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
For model-selection guidance by content type, see [`../transcribe.md`](../transcribe.md) → "Picking a model by content type".
|
||||
|
||||
## Using External Transcription APIs
|
||||
|
||||
For the best accuracy, use an external API and import the result:
|
||||
|
||||
**OpenAI Whisper API** (recommended for quality):
|
||||
|
||||
```bash
|
||||
# Generate with word timestamps, then import
|
||||
curl https://api.openai.com/v1/audio/transcriptions \
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
||||
-F file=@audio.mp3 -F model=whisper-1 \
|
||||
-F response_format=verbose_json \
|
||||
-F "timestamp_granularities[]=word" \
|
||||
-o transcript-openai.json
|
||||
|
||||
npx hyperframes transcribe transcript-openai.json
|
||||
```
|
||||
|
||||
**Groq Whisper API** (fast, free tier available):
|
||||
|
||||
```bash
|
||||
curl https://api.groq.com/openai/v1/audio/transcriptions \
|
||||
-H "Authorization: Bearer $GROQ_API_KEY" \
|
||||
-F file=@audio.mp3 -F model=whisper-large-v3 \
|
||||
-F response_format=verbose_json \
|
||||
-F "timestamp_granularities[]=word" \
|
||||
-o transcript-groq.json
|
||||
|
||||
npx hyperframes transcribe transcript-groq.json
|
||||
```
|
||||
|
||||
## If No Transcript Exists
|
||||
|
||||
1. Check the project root for `transcript.json`, `.srt`, or `.vtt` files.
|
||||
2. If none found, run [`../transcribe.md`](../transcribe.md) — pick the starting model from "Picking a model by content type" there.
|
||||
3. Run the quality check above. If it fails, retry with a larger model or fall back to manual lyrics / external API.
|
||||
Reference in New Issue
Block a user