Files
hyperframes/skills/faceless-explainer/scripts/lib/dimensions.mjs
WaterrrForeverandClaude Opus 4.8 7cb8386539 refactor(skills): rebuild faceless-explainer + pr-to-video on the shot-sequence architecture (#1778)
* refactor(skills): rebuild faceless-explainer + pr-to-video on the shot-sequence architecture

Move both skills onto the current shot-sequence authoring architecture and
the latest shared engine, then re-narrate to each domain. The engine fixes
(pre-assembly frame guards + BGM loop-extend in assemble-index, dark-ground
caption contrast, brand-accent selection + mono role in tokens, dark-mode
polarity invert / weight-clamp / icon-font filter in build-frame) had only
landed in one copy; both skills were a generation behind.

- Authoring model: visual-design now writes a time-coded shot sequence (Scene
  windows paced to the voiceover) instead of the older effects-id phased note;
  motion-language carries the move vocabulary + the tightened motion doctrine
  (smooth over bouncy) + the seek-safe core (fromTo entrances, no CSS-transition
  motion); add cut-catalog (within-frame velocity-matched seams); frame-worker
  and SKILL Step 4/5 move to blueprint instantiation + shot-sequence fidelity.
- faceless-explainer: fold the standalone composition.md into visual-design
  (inventing-the-visual / portrait / caption geometry); keep the explainer
  story doctrine; graft cue-segmented VO + candidate-blueprint-from-Step-3.
- pr-to-video: keep the ingest pipeline (fetch-pr / ingest / fetch-people-avatars)
  and code-vocabulary; preserve the code-beat (code-* block as focal, Scenes
  choreograph the surround) and mechanism-beat treatments under the new model.
- Drop stage-assets from both (no captured assets to stage); remove derivation
  references so each skill reads standalone.

bun run scripts/lint-skills.ts passes; all scripts node --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(skills): resync skills-manifest after format pass

The pre-commit skills-manifest hook hashed the skills before the format hook
reformatted cut-catalog.md, so the committed manifest lagged the on-disk
content and CI's "Skills: manifest in sync" check failed. Regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 00:02:24 +08:00

46 lines
2.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// dimensions.mjs — canvas size + caption-band geometry for the video
// pipeline. Single source of truth = the STORYBOARD frontmatter `format` global
// ("1920x1080" / "1080x1920" / "1080x1080", or a named orientation). Every
// script and the index assembler reads the size from here; none hardcodes it.
// Named orientation presets. Square/portrait are 1080-based so they share the
// long-edge pixel budget with landscape (same render-cost ballpark).
export const ORIENTATION_PRESETS = {
landscape: { width: 1920, height: 1080 }, // 16:9 — default
portrait: { width: 1080, height: 1920 }, // 9:16 — reels / shorts / TikTok
square: { width: 1080, height: 1080 }, // 1:1 — feed
};
export const DEFAULT_DIMENSIONS = ORIENTATION_PRESETS.landscape;
function sane(w, h) {
return Number.isFinite(w) && Number.isFinite(h) && w >= 240 && h >= 240 && w <= 8192 && h <= 8192;
}
// Parse a STORYBOARD `format` global into { width, height, source }. Accepts
// "WxH" (e.g. "1920x1080"; `x` or `×`, any inner spacing) or a named orientation;
// falls back to landscape so a storyboard with a missing/garbled format still
// renders (no behavior change vs the old landscape lock).
export function parseFormat(format) {
const s = typeof format === "string" ? format.trim().toLowerCase() : "";
if (ORIENTATION_PRESETS[s]) return { ...ORIENTATION_PRESETS[s], source: `orientation=${s}` };
const m = s.match(/^(\d+)\s*[x×]\s*(\d+)$/);
if (m) {
const w = parseInt(m[1] ?? "", 10);
const h = parseInt(m[2] ?? "", 10);
if (sane(w, h)) return { width: w, height: h, source: "format" };
}
return { ...DEFAULT_DIMENSIONS, source: "default(landscape)" };
}
// Caption band geometry, derived from canvas height: the bottom ~16.67% (180px
// at h=1080). Frame content must end `safetyPx` above the band top. Holds even
// when captions are disabled (bottom-edge consistency).
export const CAPTION_BAND_FRACTION = 0.1667;
export function captionBand(height, safetyPx = 20) {
const h = Number.isFinite(height) ? height : DEFAULT_DIMENSIONS.height;
const bandHeight = Math.round(h * CAPTION_BAND_FRACTION);
const bandTopY = h - bandHeight; // foreground must end at/above this y
return { bandHeight, bandTopY, foregroundMaxY: bandTopY - safetyPx };
}