Merge pull request #43 from BlueSkyID666/codex/sync-videostudio-20261104

Sync portable VideoStudio editing and authoring updates
This commit is contained in:
Orkas authored and GitHub committed 2026-11-04 23:18:39 +08:00
commit a51674eecc
39 files changed
+2639 -36

No files matched your search

+1 -1
View File
@@ -174,7 +174,7 @@ Agent: → reads video-router (locks: compose-primary)
→ returns draft.mp4
```
The full command surface: `doctor · composition {prepare,reconcile} · draft · render · lint · check · snapshot · edit {probe,trim,concat,
The full command surface: `doctor · composition {prepare,reconcile} · draft · render · lint · check · snapshot · edit {probe,trim,concat,reframe,
burnsubs,overlay,extract-frame,loudness,mix,trim-silence,remove-fillers} · transcribe ·
silence · scenes · quality · plan {validate,summarize,promise-check,rank-takes} · narration fit ·
gate transition · speak · speech-capabilities · image · video · skills`.
+32 -2
View File
@@ -111,9 +111,11 @@ const snapshot = defineCommand({
args: {
project: { type: 'positional', required: true, description: 'composition directory (contains index.html)' },
out: { type: 'string', required: true, description: 'output PNG path' },
scenes: { type: 'string', description: 'Comma-separated scene IDs for authoring evidence only (max 4).' },
},
async run({ args }) {
printJson(await renderTool.snapshot({ project: String(args.project), output: String(args.out) }));
const sceneIds = typeof args.scenes === 'string' ? args.scenes.split(',') : undefined;
printJson(await renderTool.snapshot({ project: String(args.project), output: String(args.out), sceneIds }));
},
});
@@ -160,7 +162,7 @@ const composition = defineCommand({
// --- edit ------------------------------------------------------------------
const edit_ = defineCommand({
meta: { name: 'edit', description: 'Edit real footage with ffmpeg (probe/trim/concat/burnsubs/overlay/extract-frame/loudness/mix).' },
meta: { name: 'edit', description: 'Edit real footage with ffmpeg (probe/trim/concat/reframe/burnsubs/overlay/extract-frame/loudness/mix).' },
subCommands: {
probe: defineCommand({
meta: { name: 'probe', description: 'Probe duration/resolution/fps/audio.' },
@@ -201,6 +203,34 @@ const edit_ = defineCommand({
printJson(await edit.concat(inputs, String(args.out), editProgress));
},
}),
reframe: defineCommand({
meta: { name: 'reframe', description: 'Fit a clip to an aspect ratio or explicit canvas.' },
args: {
input: { type: 'positional', required: true },
aspect: { type: 'string' },
width: { type: 'string' },
height: { type: 'string' },
mode: { type: 'string', description: 'pad (default), crop, or blur' },
'focus-x': { type: 'string' },
'focus-y': { type: 'string' },
'pad-color': { type: 'string' },
out: { type: 'string', required: true },
},
async run({ args }) {
const mode = args.mode;
if (mode !== undefined && mode !== 'pad' && mode !== 'crop' && mode !== 'blur') {
throw new Error('--mode must be pad, crop, or blur');
}
printJson(await edit.reframe({
input: String(args.input), output: String(args.out), aspect: typeof args.aspect === 'string' ? args.aspect : undefined,
width: args.width === undefined ? undefined : num(args.width, 'width'),
height: args.height === undefined ? undefined : num(args.height, 'height'),
mode, focus_x: args['focus-x'] === undefined ? undefined : num(args['focus-x'], 'focus-x'),
focus_y: args['focus-y'] === undefined ? undefined : num(args['focus-y'], 'focus-y'),
pad_color: typeof args['pad-color'] === 'string' ? args['pad-color'] : undefined,
}, editProgress));
},
}),
burnsubs: defineCommand({
meta: { name: 'burnsubs', description: 'Burn an .srt/.ass subtitle file into the picture.' },
args: {
+56 -1
View File
@@ -157,6 +157,8 @@ export interface EdlSegment {
export interface NarrationLine {
text: string;
/** Segment on the finished timeline; optional for existing plans. */
segment_id?: string;
start_sec?: number;
target_sec?: number;
/** Where this line's synthesized audio landed (set after speech synthesis). A
@@ -188,6 +190,8 @@ export interface MusicTrack {
* the assembler re-burns just the .srt. */
export interface CaptionLine {
text: string;
/** Segment on the finished timeline; optional for existing plans. */
segment_id?: string;
start_sec?: number;
/** On-screen duration in seconds (end = start_sec + target_sec). */
target_sec?: number;
@@ -297,6 +301,33 @@ function isSourceBackedPrimary(
&& reference.target_segment_ids.includes(segment.id)));
}
type SegmentWindow = { start: number; end: number; primaryId: string };
/** Each segment's window on the finished video: primaries play back to back in
* `order` (overlay time never extends the timeline), and an overlay/bg segment
* shares the window of the primary it sits `over`. Null when the primary order
* is ambiguous or a duration is invalid; those plans carry their own issues. */
function finishedVideoWindows(segments: Array<Record<string, unknown>>): Map<string, SegmentWindow> | null {
const primaries = segments.filter((s) => s.layer === 'primary');
if (primaries.some((s) => !isStr(s.id) || !isNum(s.order) || !isNum(s.target_sec) || s.target_sec <= 0)
|| new Set(primaries.map((s) => s.order)).size !== primaries.length) {
return null;
}
const windows = new Map<string, SegmentWindow>();
let at = 0;
for (const s of [...primaries].sort((a, b) => (a.order as number) - (b.order as number))) {
const id = s.id as string;
windows.set(id, { start: at, end: at + (s.target_sec as number), primaryId: id });
at += s.target_sec as number;
}
for (const s of segments) {
if (s.layer === 'primary' || !isStr(s.id) || !isStr(s.over)) continue;
const under = windows.get(s.over);
if (under?.primaryId === s.over) windows.set(s.id, under);
}
return windows;
}
/**
* Validate a parsed plan.json against the EDL contract. Returns every issue
* found (does not short-circuit) so the agent can fix the plan in one pass.
@@ -652,6 +683,26 @@ export function validateEdl(obj: unknown): EdlValidation {
if (!isObject(tracks)) {
err('tracks', 'E_TRACKS_NOT_OBJECT', 'tracks is required and must be an object; use {} when no tracks are active');
} else {
const segmentIds = new Set(segments.map((s) => s.id).filter(isStr));
const windows = finishedVideoWindows(segments);
const checkLineSegment = (at: string, line: Record<string, unknown>) => {
if (line.segment_id === undefined) return;
if (!isStr(line.segment_id) || !segmentIds.has(line.segment_id)) {
err(`${at}.segment_id`, 'E_TRACK_LINE_SEGMENT_UNKNOWN', `segment_id "${String(line.segment_id)}" names no segment in this plan`);
return;
}
const window = windows?.get(line.segment_id);
if (!window || !isNum(line.start_sec)) return;
if (line.start_sec < window.start - 0.05 || line.start_sec >= window.end) {
const over = window.primaryId === line.segment_id ? '' : ` (over "${window.primaryId}")`;
err(
`${at}.start_sec`,
'E_TRACK_LINE_OUTSIDE_SEGMENT',
`starts at ${round1(line.start_sec)}s, but segment "${line.segment_id}"${over} plays ${round1(window.start)}–${round1(window.end)}s in the finished video; start_sec counts every earlier primary segment's target_sec, composed cards included`,
);
}
};
const nar = tracks.narration;
if (nar !== undefined && nar !== null && !isObject(nar)) {
err('tracks.narration', 'E_NARRATION_NOT_OBJECT', 'narration must be an object, null, or omitted');
@@ -687,9 +738,12 @@ export function validateEdl(obj: unknown): EdlValidation {
nar.segments.forEach((ln, i) => {
if (!isObject(ln) || !isStr(ln.text)) {
err(`tracks.narration.segments[${i}].text`, 'E_NARRATION_LINE_TEXT', 'each narration line needs non-empty text');
} else if (ln.produced_path !== undefined && !isStr(ln.produced_path)) {
return;
}
if (ln.produced_path !== undefined && !isStr(ln.produced_path)) {
warn(`tracks.narration.segments[${i}].produced_path`, 'W_NARRATION_PRODUCED', 'produced_path should be a string path when present');
}
checkLineSegment(`tracks.narration.segments[${i}]`, ln);
});
// target_sec is a duration; overlapping windows mix two voices.
const windows = nar.segments
@@ -736,6 +790,7 @@ export function validateEdl(obj: unknown): EdlValidation {
if (ln.target_sec !== undefined && !isNum(ln.target_sec)) {
warn(`tracks.captions.lines[${i}].target_sec`, 'W_CAPTION_TIMING', 'target_sec (on-screen seconds) should be a number');
}
checkLineSegment(`tracks.captions.lines[${i}]`, ln);
}
});
} else if (!isStr(caps.from)) {
+23
View File
@@ -502,3 +502,26 @@ it('closes generate specs without closing compose extension fields or removing p
expect(validateEdl(value).errors.some((e) => e.path.endsWith('.spec.ratio'))).toBe(false);
expect(validateEdl(plan({ segments: [seg({ id: 'composed', order: 1, target_sec: 30, source: 'compose', spec: { kind: 'title', custom_layout: 'ribbon' } })] })).ok).toBe(true);
});
it('validates bound tracks on the finished timeline, including a composed opener', () => {
const value = plan({
total_target_sec: 10,
segments: [
seg({ id: 'body', order: 2, source: 'compose', target_sec: 6 }),
seg({ id: 'opener', order: 1, source: 'compose', target_sec: 4 }),
],
tracks: {
narration: { synthesis: { route_ref: 'openai-compatible', voice: 'nova', language: 'en', speed: 1 },
segments: [{ text: 'Watch this', segment_id: 'body', start_sec: 4, target_sec: 2 }] },
captions: { lines: [{ text: 'Watch this', segment_id: 'body', start_sec: 4, target_sec: 2 }] },
},
});
expect(validateEdl(value).ok).toBe(true);
value.tracks.narration!.segments[0].start_sec = 0;
value.tracks.captions!.lines![0].start_sec = 10;
expect(validateEdl(value).errors).toEqual(expect.arrayContaining([
expect.objectContaining({ path: 'tracks.narration.segments[0].start_sec', code: 'E_TRACK_LINE_OUTSIDE_SEGMENT' }),
expect.objectContaining({ path: 'tracks.captions.lines[0].start_sec', code: 'E_TRACK_LINE_OUTSIDE_SEGMENT' }),
]));
});
+18 -2
View File
@@ -80,7 +80,12 @@ server.tool(
server.tool('lint', 'Structural QA of a composition.', { project: z.string() }, ({ project }) => format(renderTool.lint(project)));
server.tool('check', 'Final browser/runtime/layout QA of a composition; includes lint.', { project: z.string() }, ({ project }) => format(renderTool.check(project)));
server.tool('inspect', 'Deprecated compatibility alias for check.', { project: z.string() }, ({ project }) => format(renderTool.check(project)));
server.tool('snapshot', 'Capture hook, per-scene, and payoff preview frames plus a contact sheet.', { project: z.string(), out: z.string() }, ({ project, out }) => format(renderTool.snapshot({ project, output: out })));
server.tool(
'snapshot',
'Capture whole-composition preview frames, or selected scenes for authoring evidence only.',
{ project: z.string(), out: z.string(), scene_ids: z.array(z.string()).min(1).max(4).optional() },
({ project, out, scene_ids }) => format(renderTool.snapshot({ project, output: out, sceneIds: scene_ids })),
);
server.tool('composition_prepare', 'Create a HyperFrames scaffold from composition-manifest.json without overwriting authored HTML.', { project: z.string() }, ({ project }) => format(compositionTool.prepareComposition(project)));
server.tool('composition_reconcile', 'Apply manifest timing/audio metadata while preserving authored visual content.', { project: z.string() }, ({ project }) => format(compositionTool.reconcileComposition(project)));
@@ -92,7 +97,18 @@ server.tool(
{ input: z.string(), start_sec: z.number(), duration_sec: z.number().optional(), end_sec: z.number().optional(), output: z.string() },
(a) => format(edit.trim(a, editProgress)),
);
server.tool('edit_concat', 'Join clips that share stream layout.', { inputs: z.array(z.string()), output: z.string() }, ({ inputs, output }) => format(edit.concat(inputs, output, editProgress)));
server.tool('edit_concat', 'Join clips and keep audio across silent and narrated parts.', { inputs: z.array(z.string()), output: z.string() }, ({ inputs, output }) => format(edit.concat(inputs, output, editProgress)));
server.tool(
'edit_reframe',
'Fit a clip to an aspect ratio or explicit canvas while keeping its timeline and audio.',
{
input: z.string(), output: z.string(), aspect: z.string().optional(),
width: z.number().optional(), height: z.number().optional(),
mode: z.enum(['pad', 'crop', 'blur']).optional(),
focus_x: z.number().optional(), focus_y: z.number().optional(), pad_color: z.string().optional(),
},
(a) => format(edit.reframe(a, editProgress)),
);
server.tool('edit_burnsubs', 'Burn an .srt/.ass subtitle file into the picture.', { input: z.string(), srt: z.string(), output: z.string() }, ({ input, srt, output }) => format(edit.burnsubs(input, srt, output, editProgress)));
server.tool(
'edit_overlay',
+3 -1
View File
@@ -13,6 +13,7 @@ This skill does not pick the video production line, replace `video-craft`, or re
Read the HTML generation playbook before non-trivial authoring. Load visual primitives when selecting scene grammar, and worked compositions only when examples are useful:
- [Design presets](references/design-presets.md) — thirteen starting systems. When no concrete reference sets the look, pick one that suits the brief, map its visual roles to the brand, and use it to design the individual scenes.
- `references/html-generation-playbook.md` — the private pre-code art-direction pass, frame-composition rules, and opening/resolved-state authoring pattern.
- `references/visual-primitives.md` — reusable CSS/SVG composition primitives and scene-grammar selection guidance.
- `references/worked-compositions.md` — worked examples showing how subject matter becomes a cohesive visual system without copying a fixed template.
@@ -53,7 +54,7 @@ For non-trivial COMPOSE work, write a compact `visual_direction` object inside m
Required fields:
- `visual_tradition`: a real design tradition, designer, art movement, or cultural reference that controls composition behavior. Examples: `Swiss Pulse / Josef Müller-Brockmann precision grid`, `Data Drift / Refik Anadol data field`, `Velvet Standard / Vignelli restraint`, `Deconstructed / Neville Brody rupture`, `Maximalist Type / Paula Scher scale`. Avoid empty labels such as "modern tech", "premium", or "cinematic" by themselves.
- `visual_tradition`: a remapped design preset or a real design tradition, designer, art movement, or cultural reference that controls composition behavior. Examples: `Swiss Pulse / Josef Müller-Brockmann precision grid`, `Data Drift / Refik Anadol data field`, `Velvet Standard / Vignelli restraint`, `Deconstructed / Neville Brody rupture`, `Maximalist Type / Paula Scher scale`. Avoid empty labels such as "modern tech", "premium", or "cinematic" by themselves.
- `composition_behavior`: how frames should be arranged and how the eye should travel: grid-locked data, research atlas, editorial archive, full-bleed object, kinetic type wall, product surface, map/flow, or diagram build.
- `lazy_defaults_rejected`: the first generic design move rejected and the brief-specific replacement. Question purple/blue neon gradients, black neon circles, centered equal-weight layouts, identical cards, decorative emoji/icons, tiny badges, default web dashboards, and pure black/white.
- `video_scale`: the scale floor for this canvas, at or above the `video-craft` floor that `ovs lint` checks. For 1920x1080, default to headline 72-140px, body/supporting text 42px and up, labels and captions at or above the ~40px legibility floor, borders 2-4px, safe padding 60-140px, and decorative opacity 12-25%.
@@ -147,6 +148,7 @@ When writing `index.html`:
- Treat frame 0 as a dedicated cover. It needs one dominant topic-specific hero and at least two visible content signals from the actual video. Mark the hero with `data-role="visual" data-cover-hero` and matching visible elements with `data-cover-signal="<exact content_signals value>"`; do not mark the title, generic background, or decoration as evidence.
- Complete the private art-direction pass from `references/html-generation-playbook.md` immediately before coding. Decide the dominant visual, spatial tension, depth layers, opening/resolved states, and continuity behavior for every scene; do not output this as a new user gate.
- Select and adapt primitives from `references/visual-primitives.md`. They are ingredients, not templates: change geometry, scale, rhythm, and content so the result belongs to the brief.
- In product beats, show actual screenshots or photos at hero scale and build the scene around them. Text embedded in a screenshot is image content; the scene’s own text still needs to meet the video-scale floor.
- Use video scale, not web scale, and keep every readable element at or above the `video-craft` floor. Build each frame in three semantic depth layers: a topic-derived background field, a dominant midground message/diagram, and foreground accents or metadata. Do not add arbitrary decoration; every layer should reinforce subject, hierarchy, scale, direction, or continuity.
- Give every meaningful element a motion verb before writing GSAP. If the author cannot say whether a path draws, a year stamps, a number counts up, a card locks, or a texture drifts, the element is not yet designed.
- Derive the main CSS variables from manifest `art_direction.color_tokens`; keep extra chromatic colors intentional and named enough to audit. Do not flatten the design or recolor the whole video only to reduce a static palette count.
@@ -0,0 +1,124 @@
# Design presets
Thirteen starting systems for COMPOSE. Each one fixes a look: color roles, type
ramp, components, frame types and motion. The brand supplies colors and fonts;
the brief supplies every word, number and image. They are the recommended
starting point when no concrete reference sets the look, or when a reference's
grammar matches one; skip them when none fits the brand and the brief. A brand
website or design system gives the tokens to remap onto, not the look itself. Pick one,
read its file, remap it, apply the video-type rules below, then design each
scene for this subject. Name it in `art_direction.visual_direction.visual_tradition`
(for example "blue-professional preset, remapped to the brand tokens").
The presets are adapted from the HyperFrames frame presets (heygen-com/hyperframes
@279d40dc, Apache License 2.0); each file states what changed, and the license
text is in [design-presets/LICENSE-hyperframes.txt](design-presets/LICENSE-hyperframes.txt).
| Preset | Look | Temperament and fit |
| --- | --- | --- |
| [biennale-yellow](design-presets/biennale-yellow.md) | parchment, one indigo ink, yellow bloom and panel, display serif, 1px hairlines | elegant, atmospheric; brand or culture launches told as an issue or exhibition |
| [blockframe](design-presets/blockframe.md) | 4px black borders, hard offset shadows, candy pastels, heavy sans | loud, playful, confident; consumer apps, social shorts |
| [blue-professional](design-presets/blue-professional.md) | warm cream, one cobalt accent, tinted cards without shadows, pill chrome | measured, executive; B2B products, reports, consulting |
| [bold-poster](design-presets/bold-poster.md) | tilted script display, serif body, red and brown-black on white, double-border grids | printed gravitas with popular energy; campaigns, events, food and retail stories |
| [broadside](design-presets/broadside.md) | ink-black and fire-orange planes, massive lowercase heavy sans | declarative; manifestos, announcements, bold product statements |
| [capsule](design-presets/capsule.md) | pill shapes with ink outlines, cream, candy colors, Bodoni with a grotesk | friendly, warm; lifestyle and consumer brands, e-commerce |
| [cartesian](design-presets/cartesian.md) | warm stone palette, taupe hairline grid, Playfair with Inter, drafted rings | quiet, literary; luxury, architecture, research |
| [cobalt-grid](design-presets/cobalt-grid.md) | cream paper, cobalt ink, permanent graph grid, serif with grotesk and mono | systematic, technical-editorial; developer tools, data, infrastructure |
| [code-editorial](design-presets/code-editorial.md) | warm cream, scarce terracotta, Garamond display, a navy code surface | considered, developer-facing; launches, changelogs and docs with code |
| [coral](design-presets/coral.md) | coral, ink and cream planes at hard edges, diagonal hatch, condensed display, giant numerals | bold magazine energy; product drops, list videos, fashion and sport |
| [creative-mode](design-presets/creative-mode.md) | cream, 4px ink borders, hard offset shadows, black heavy display, mono taxonomy | punchy but restrained; creator tools, studios, tutorials with attitude |
| [daisy-days](design-presets/daisy-days.md) | sunny pastels, charcoal outlines, rounded friendly type, hand-drawn ornaments | playful, warm; education, kids, cafés, gifts |
| [editorial-forest](design-presets/editorial-forest.md) | green, pink and cream, a serif display, mono chrome, 2px hairlines, monogram stamp | spacious, literary calm; wellness, sustainability, publishing |
Pick by the brand's temperament first, then check the fit. Use one preset per
video. When a frame needs a component the preset lacks, borrow it from another
preset and restyle it in this preset's tokens.
## Video-type rules
These sit on top of whichever preset is chosen.
- **Product demo (app, SaaS, tool)**: real screens at 45–70% of the frame width.
Mark a region on the full screen before pushing into it (scale 1.6–2.0 over
0.8–1.2s). A 40px cursor with a click ring; typed input at 2–4 characters per
0.1s. Change the screen's state every 1–2s and carry the same screen across
scenes instead of cutting to new layouts.
- **Physical product or offer**: supplied product images only; the product is
the largest element in hero frames (50–75% of the height). At most three
callouts with leader lines per view. One offer element per frame; only the
closing frame pairs the price with the CTA. Never invent prices, discounts,
ratings or sales counts.
- **Short-form 9:16**: essential text between y=220 and y=1540 and left of x=940.
The hook and a visual of its payoff are readable at frame 0. At most 12 CJK
characters or 6 English words a line, three lines a frame. Cut every 1–2
beats; captions sit above y=1540.
- **Tutorial or how-to**: one action per step frame, each on the real UI. Mark
the control with an outline, dim the rest, then push in. At most five numbered
pins per screen. Menu names, button labels and shortcuts exactly as in the product.
- **Numbers and reports**: one number per frame carries the accent. Every chart
frame shows its unit and source; bar axes start at zero. Count-ups take
0.8–1.5s, except the cover, which shows the final figure at frame 0. Hold a
finished chart 1.5s or more.
- **Brand film or teaser**: one line per frame; plates get a vignette and scale
slowly (1.00→1.08 over the shot); cut to black for half a beat before the
lockup.
- **A cast of people, agents or products**: one item per beat on hard cuts
(0.5s at 120 BPM), then a summary frame listing all of them.
## Remap onto the brand
Every preset names color roles in its tokens. Map the brand onto them by role
and keep the structure:
- `canvas` → the brand's background; `ink` → its text color; `accent` → its
primary; `deep` → its dark or ink; `surface` and `line` follow canvas and ink.
- A brand with one color keeps the preset's neutrals and puts that color on
`accent`. Keep the preset's rule for how much of the frame `accent` may cover.
- If the brand primary fails contrast where the preset puts accent text, keep
it for fields and marks, and set that text in `ink`.
- Fonts: use the brand's faces when they are installed or supplied, then the
preset's named face, then its stack. Copy any font file the video depends on
into `assets/fonts/` and load it with `@font-face`; never load a remote font.
## Fixed and free
- Fixed: the color-role rules, type roles and ramp, component shapes, frame-type
grammar, motion vocabulary and the don'ts.
- Free: every word, number and image; which frame types appear, in what order
and how often; which components a frame uses.
## Units and ratios
Sizes are px on a 1920×1080 frame. On 1080×1920 keep body, label and caption
sizes, and scale display sizes by 0.7–0.8 so the longest line fits about 920px.
On 1080×1080 scale display by about 0.8.
## Font stacks
Put the Latin face before the CJK face so mixed lines take each script's own
face. These stacks resolve on macOS and Windows without downloads.
- `serif-display`: "Didot", "Bodoni 72", "Bodoni MT", Georgia, then `song`
- `serif-text`: "Iowan Old Style", "Charter", Georgia, "Cambria", then `song`
- `sans`: "Avenir Next", "Helvetica Neue", "Segoe UI", Arial, then `hei`
- `condensed`: "Avenir Next Condensed", "DIN Condensed", "Bahnschrift", Impact, then `hei`
- `mono`: "SF Mono", Menlo, "Cascadia Mono", Consolas, monospace
- `song`: "Songti SC", "STSong", "Source Han Serif SC", "Noto Serif CJK SC", SimSun, serif
- `hei`: "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif
PingFang SC stops at weight 600 and SimSun has one thin weight. When a preset
asks for heavier CJK than the machine has, raise size rather than fake bold, and
check the full-size snapshot frame.
## Rules every preset keeps
- Type sizes are starting points; apply the higher OVS video-scale and readability floors where required.
- Casing: keep the approved copy's casing. Presets describe letter-spacing, never
`text-transform: uppercase`.
- Frame 0: the cover's headline and hero are visible on the first frame; build
the resolved frame first, then animate supporting parts into it.
- Real material first: product screens and photos come from `assets/product/`,
supplied media or the reference. Never redraw a screen that was captured.
- Numbers: every figure, price, rating, date and count comes from the brief or a
source. Ordinals such as 01, 02 are decoration and may be free.
- Local only: no remote fonts, scripts or images at render time.
@@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2026 HeyGen, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,131 @@
# biennale-yellow — art-catalogue editorial
Adapted from the HyperFrames frame preset `biennale-yellow` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants museum-catalogue elegance and editorial authority:
confident, atmospheric, restrained. Launches told as an issue or exhibition,
founder stories, culture and design brands.
Failure looks like a flat CMS template (paper with no bloom) or a deck of
bordered cards.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
paper: "#E9E5DB" # canvas: warm parchment on every frame
paper-deep: "#DCD6C4" # canvas variant
sun: "#F1EE2E" # accent: bloom core, panel, tile underprint
sun-soft: "#F8F39B" # bloom middle stop
haze: "#F0DA7C" # bloom outer stop
ink: "#1B2566" # every line of type and every rule
ember: "#E26B4A" # counter-bloom only, 15–22%; never a fill or text
type: # px on 1920×1080
numeral-jumbo: { face: "Instrument Serif" > serif-display, size: 538, weight: 400, line: 0.84, tracking: -0.04em }
display: { face: "Instrument Serif" > serif-display, size: 280, weight: 400, line: 0.86, tracking: -0.018em }
numeral-md: { face: "Instrument Serif" > serif-display, size: 144, weight: 400, line: 0.92 }
display-it: { face: "Instrument Serif" > serif-display, size: 134, style: italic, line: 1.04 }
date-rail: { face: "Instrument Serif" > serif-display, size: 96, line: 0.96 }
headline: { face: "Instrument Serif" > serif-display, size: 88, line: 1.06 }
headline-sm: { face: "Instrument Serif" > serif-display, size: 64, line: 1.0 }
strand-title: { face: "Instrument Serif" > serif-display, size: 36, line: 1.1 }
body: { face: "Archivo" > sans, size: 32, weight: 400, line: 1.5 }
body-lede: { face: "Archivo" > sans, size: 36, weight: 400, line: 1.5 }
label: { face: "Archivo" > sans, size: 22, weight: 600, tracking: 0.18em }
data: { face: "JetBrains Mono" > mono, size: 24, tracking: 0.04em }
pagenum: { face: "JetBrains Mono" > mono, size: 22, opacity: 0.75 }
cjk: { display: song 700, body: hei 400 }
space: { edge: 80, region: 80, gap: 48, rule: 1 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`,
otherwise the stack after `>`. Serif display stays weight 400; contrast comes
from size, never bold.
## Frame craft
- Squint: one serif moment dominates at 3–6× its neighbor; the sun bloom pulls
the eye to it.
- Silence: frames stay 55–60% empty. The ledger is the one dense frame, dense
through quiet hairline repetition.
- Restraint: one ink for all type and rules, one sun bloom per frame plus an
optional ember counter-bloom. Never yellow type on ink.
## Components
- **sun-bloom**: a layered radial — sun at 70–95% core → sun-soft → haze at
18–22% → paper at 0% — 42–70% of the frame, off-center or behind the focal
element. One per frame; flat parchment reads as broken.
- **ember-bloom**: a radial in ember at 15–22% in the corner opposite the sun
bloom; always subordinate.
- **block-tile**: sun rectangles at 40–70% opacity on an 8×4 grid behind cover
and closing frames; a poster underprint.
- **yellow-panel**: a full-bleed sun column or third with ink type on it, meeting
the paper directly with no border. The strongest color statement.
- **hairline-rule**: 1px ink; a soft variant at 18–20% for dense rows. The only
border; nothing thicker exists.
- **strand-row**: serif numeral + serif title + sans body, soft hairline under.
Numbered editorial lists.
- **ledger-row**: four columns — mono date · serif title · sans venue · mono
duration on the right — with soft hairlines.
- **footer-band**: a four-column metadata strip at the foot of cover and closing
frames, 1px ink rule over each cell.
- **vertical-rail**: a label rotated up the left edge as a section marker.
- **pagenum**: bottom-right, the only persistent chrome.
## Frame types
1. **Cover** — paper, a large sun bloom left of center, an ember bloom in the
opposite corner. A two-line display headline left under a label, a date-rail
top-right, a footer-band at the foot. Low density; readable at frame 0.
2. **Chapter divider** — a corner sun bloom, one numeral-jumbo dominating, a
headline-sm title under it, a vertical-rail label up the left edge.
3. **Ledger** — headline-sm with a label top bar, then ledger rows under a 1px
header rule. The dense frame; no accent, no bloom or a faint one.
4. **Manifesto** — a centered bloom, an oversized serif quote mark, a two-line
display-it quote centered, a label attribution. About 60% open.
5. **Poster panel** — a yellow-panel column or third with a headline in ink on
it; the paper side stays open.
6. **Strand list** — label, headline-sm, then numbered strand rows breathing
under a subtle bloom.
## Motion
- Verbs: blooms breathe, lines rise into masks, hairlines draw, numerals swap,
panels wipe across.
- Blooms scale 1→1.05 and drift 2–3% across the scene, `sine.inOut`; the
atmosphere moves, the type holds.
- Display and headline lines reveal by a baseline mask, 0.6–0.8s `power3.out`,
one line per beat.
- Hairlines draw left to right in 0.5s; ledger and strand rows follow them in
sequence 0.12s apart.
- Chapter dividers cut on the downbeat; the numeral swaps with a hard cut.
- A yellow-panel enters as a wipe from its own edge in 0.5s `power2.inOut`.
- Never: bounce, rotation, shadows appearing, fast zooms on type.
## Do and don't
- Do start every frame on parchment with one sun bloom.
- Do set every line in ink; serif 400 display, sans body, mono for numerals and dates.
- Do lean sparse: left or asymmetric on cover, chapter and lists; centered only
on the manifesto.
- Don't use drop shadows, rounded corners, bordered cards or rules thicker than 1px.
- Don't add a second text color, bold serif or inverted ink grounds.
- Don't run a headline edge to edge; step down the ramp instead.
## 9:16 and 1:1
- Cover: display at the top, footer-band stacks; 1:1 puts display in the upper half.
- Chapter divider: numeral centered, the rail moves to the top.
- Ledger: drop the venue column to three columns on 9:16 and 1:1.
- Poster panel: a top or bottom band on 9:16, a third on 1:1.
## Self-check before snapshot
- One serif display moment dominates; one sun bloom present; sparse frames 55–60% open.
- Ink for all text and rules; sun only as bloom, panel or tile; ember only as a counter-bloom.
- No shadow, no rounded corner, hairlines 1px.
- Copy keeps its approved casing; nothing below 22px.
- Every numeral and date traces to the brief; ordinals may be decorative.
@@ -0,0 +1,133 @@
# blockframe — candy neobrutalist blocks
Adapted from the HyperFrames frame preset `blockframe` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the product wants to feel confident and graphic: bold, punchy,
playful-loud. Consumer apps, creator tools, launches with a sense of humor,
anything that should feel like a sticker book or a toy box, not a deck.
Failure looks like a flat web card grid: thin or missing borders, blurred
shadows, everything politely aligned.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
black: "#000000" # ink, line, deep: all type, every border and shadow, close ground
white: "#FFFFFF" # surface: card fills; border and type on the close-frame
offwhite: "#FFFDF5" # canvas: default ground
pink: "#FE90E8" # accent: pastel ground, icon square, star
blue: "#C0F7FE" # accent: pastel ground, icon square
green: "#99E885" # accent: pastel ground, icon square
yellow: "#F7CB46" # accent: CTA fill and the close-frame shadow
cream: "#FFDC8B" # canvas variant: cover ground, warm pastel
borders: { primary: 8, thin: 6 } # px, black; source 0.4 / 0.3cqw at 1920
shadows: { default: 16, small: 8, hover: 12, close-yellow: 24, close-white: 12 }
type: # px on 1920×1080
heading-xl: { face: "Inter" > sans, size: 96, weight: 900, line: 0.95, tracking: -0.03em }
close-title: { face: "Inter" > sans, size: 81, weight: 900, line: 0.95, tracking: -0.03em }
heading-lg: { face: "Inter" > sans, size: 63, weight: 800, line: 1.0, tracking: -0.02em }
stat-number: { face: "Inter" > sans, size: 63, weight: 900, line: 1.0 }
quote-text: { face: "Inter" > sans, size: 52, weight: 900, line: 1.15, tracking: -0.02em }
heading-md: { face: "Inter" > sans, size: 40, weight: 700, line: 1.1, tracking: -0.01em }
card-title: { face: "Inter" > sans, size: 36, weight: 700, line: 1.2 }
body: { face: "Inter" > sans, size: 32, weight: 500, line: 1.6 }
label: { face: "Space Grotesk" > sans, size: 22, weight: 600, tracking: 0.08em }
counter: { face: "Space Grotesk" > sans, size: 22, weight: 700, tracking: 0.1em }
cjk: { display: hei 900, body: hei 500 }
space: { edge: 60, gap: 33 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`,
otherwise the stack after `>`; labels share the sans stack and differ by
tracking and weight. The source leans on capitals; copy keeps its approved
casing (CJK has no case), so weight 800–900, tight tracking, borders and
decorations carry the voice. PingFang stops at 600: raise CJK size, never fake bold.
## Frame craft
- Squint: one display moment dominates at 3–6× its neighbor; cards read as one system.
- Silence: cover, quote and close keep air, held by decorations rather than
content. The feature and stat grids are the dense frames.
- Restraint: border and shadow travel in pairs (8↔16 cards, 6↔8 chrome, all
black; white only on the closer); one pastel ground per frame, never a sixth.
## Components
- **card-elevated**: white, 8px black border, 16px hard shadow, square; the main card.
- **card-small**: white, 6px border, 8px shadow, square; stats, people, steps.
- **label-pill**: 6px border, 8px shadow, 9999px radius, pastel fill; opens every region.
- **button-primary**: yellow, black Inter 700, 6px border, 8px shadow, square.
- **star-burst**: a ten-point clip-path star, pastel, 6px border; corner attention.
- **stripe-block**: 45° stripes of black and one pastel inside a 6px border.
- **bg-dot-grid**: faint black dots, 2–3px on a 48px grid, over a corner or ground.
- **tilt-deco**: a block, badge or star turned 2–12°; stat cards alternate −2°/+2°.
- **stat-deco-dot**: a 24px pastel circle, 4px border, on stat cards; the only round shape.
- **close-frame**: black, white type, 8px white border, 24px yellow shadow; the closer.
## Frame types
1. **Cover** — cream or offwhite with a faint dot grid. A label-pill over a two-
or three-line heading-xl on the left; tilted pastel blocks and a star break
into the right third; a counter pill. Comfortable; readable at frame 0.
2. **Feature cards** — blue. Label-pill and heading-lg over three card-elevated
cards, each a pastel icon square, card-title and body. Dense by design.
3. **Stat grid** — green. Three card-small stats tilted −2/+2°, each a deco dot,
stat-number and label, under label-pill and heading-lg. Dense.
4. **Closing plate** — black. A white close-title inside the close-frame under
an inverted label-pill; a pink star over one corner. About 50% open.
5. **Quote** — pink or offwhite. Quote-text in a card-elevated, label-pill
above, attribution below, one decoration. Comfortable.
6. **Timeline** — offwhite. Three or four pastel card-small steps with step
numbers, joined by black connector bars. Standard density.
## Motion
- Verbs: cards slam down, shadows snap out, pills and stars pop, tilts settle,
grounds hard-cut.
- Cards slam in from 1.08× with a 24px shadow to rest in 0.3s `power4.out`;
grids land one card per half-beat (0.25s at 120 BPM) in reading order.
- Pills, stars and deco dots pop from 0 in 0.35s `back.out(2)`; stat cards
settle into their tilt in 0.4s `back.out(1.6)`.
- Press for emphasis: a card or the CTA moves 4px down-right as its shadow
shrinks 4px (the hover token), 0.2s `power2.out`, back on the next beat.
- Chapters hard-cut on the downbeat to a new pastel with the headline set; the
rest lands on the following beats.
- The cover's heading-xl and main decorations hold from frame 0.
- Never: crossfades, blurred or soft shadows, a shadow without its border,
tweened border widths, rotation past 12°.
## Do and don't
- Do pair every border with its shadow: 8px with 16px, 6px with 8px.
- Do change the pastel ground every frame; the cycle is the rhythm.
- Do open each region with a label-pill and add one decoration per frame.
- Do keep shadows solid black, zero blur, bottom-right; yellow for the CTA.
- Do set display at 800–900, tracked tight; each line keeps its approved casing.
- Do fit the headline: up to 3 words heading-xl, 4–6 heading-lg, 7 or more
heading-md, under 1500px wide. Lean most frames left.
- Don't round a corner except the deco dot, or blur any shadow.
- Don't color a border (white only on the closer) or add a sixth pastel.
- Don't set display untracked or a label as bare text.
- Don't align everything perfectly or leave a corner empty.
## 9:16 and 1:1
- Cover: title on top, decorations below; fewer decorations on 1:1.
- Feature cards: three stacked on 9:16; two plus one on 1:1.
- Stat grid: three tilted cards stacked on 9:16; 2×2 on 1:1.
- Closing and quote: centered; taller close-frame and stacked quote on 9:16.
- Timeline: vertical with connectors hidden on 9:16; 2×2 on 1:1.
- Drop decorations as the frame tightens: bordered, not cluttered.
## Self-check before snapshot
- One display moment dominates; the cards read as one system.
- Only the feature and stat grids run dense; cover, quote and close keep air.
- Every border has its shadow partner; solid black, zero blur, bottom-right.
- New pastel each frame; a label-pill and a decoration on each; square corners.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out.
Counters and step numbers may be decorative.
@@ -0,0 +1,133 @@
# blue-professional — cream and cobalt briefing
Adapted from the HyperFrames frame preset `blue-professional` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants investment-research rigor and refined restraint:
measured, executive-readable, premium. B2B, finance, analytics, board updates.
Failure looks like a heavy-outlined, multi-color dashboard or cobalt headlines.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
bg: "#fdfae7" # canvas: warm cream on every frame
primary: "#1e2bfa" # accent: the only one; eyebrows, figures, CTA, bars
text: "#111111" # ink: headlines and quotes, never cobalt
text-muted: "#6b6b6b" # ink-muted: body
text-light: "#9a9a9a" # ink-light: tertiary chrome
accent-light: "rgba(30,43,250,0.08)" # accent tint: tag pills, bar tracks, highlight
accent-medium: "rgba(30,43,250,0.15)" # accent tint, stronger: atmosphere
border: "rgba(30,43,250,0.2)" # line: card borders
card-bg: "rgba(30,43,250,0.04)" # surface: card fill
positive: "#059669" # signal: upward change, text only
negative: "#dc2626" # signal: downward change, text only
radii: { pill: 100, card-lg: 14, card-md: 12, card-sm: 10, bar: 6, circle: 50% }
type: # px on 1920×1080; headings and quotes in text, figures in primary
quote-mark: { face: "Space Grotesk" > sans, size: 154, weight: 700, line: 0.5, color: primary, opacity: 0.15 }
h1: { face: "Space Grotesk" > sans, size: 81, weight: 700, line: 1.08, tracking: -0.02em }
metric-value: { face: "Space Grotesk" > sans, size: 58, weight: 700, line: 1.0, color: primary }
h2: { face: "Space Grotesk" > sans, size: 50, weight: 600, line: 1.1, tracking: -0.02em }
blockquote: { face: "Space Grotesk" > sans, size: 46, weight: 500, line: 1.35 }
h3: { face: "Space Grotesk" > sans, size: 40, weight: 500, line: 1.3, tracking: -0.02em }
stat-num: { face: "Space Grotesk" > sans, size: 36, weight: 700, line: 1.0, color: primary }
body: { face: "Inter" > sans, size: 32, weight: 400, line: 1.6, color: text-muted }
h4-eyebrow: { face: "Space Grotesk" > sans, size: 22, weight: 600, tracking: 0.08em, color: primary }
tag: { face: "Space Grotesk" > sans, size: 22, weight: 500, color: primary }
counter: { face: "Space Grotesk" > sans, size: 22, weight: 500, tracking: 0.05em, color: text-muted }
cjk: { display: hei 600, body: hei 400 }
space: { edge: 96, top: 96, gap: 27 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`,
otherwise the stack after `>`. Both faces fall back to one sans stack, so the
pair rests on weight and color: headings 500–700 near-black, body 400 gray. The
source sets eyebrows in tracked capitals; copy keeps its approved casing (CJK
has no case), so cobalt, weight 600, 0.08em and the accent-line carry them.
## Frame craft
- Squint: one near-black headline or cobalt figure dominates at 3–6× its neighbor.
- Silence: content frames read balanced, never crowded; the dashboard is the dense one.
- Restraint: cobalt carries every accent and nothing else does; headlines stay
near-black; cards lift by tint, never shadow; green and red are inline text only.
## Components
- **card-tinted**: card-bg fill, 1.5px `border` line, 14px radius, no shadow; never solid.
- **metric-card**: the same shell with a cobalt metric-value, a label in text, a
muted note, and an optional ↑/↓ chip in positive or negative text, no fill.
- **tag-pill**: accent-light fill, cobalt tag type, 100px radius; slide-header right.
- **cta-button**: a solid cobalt pill, cream Space Grotesk 600; the one solid element.
- **accent-line**: a 60×4px cobalt bar, 2px radius, above titles and under eyebrows.
- **bar-track**: a 28px accent-light track, 6px radius; the fill's width is the value.
- **step-circle**: 56px cobalt, cream numeral; steps fade 1.0 → 0.85 → 0.7 → 0.55.
- **split-highlight**: accent-light, 4px cobalt left rule, 12px radius; a pull quote.
- **slide-header**: cobalt eyebrow left, tag-pill right, h2 below; tops content frames.
- **atmosphere**: a clipped diagonal tint panel, a 3×3 cobalt dot grid, concentric
rings. Cover and closing only.
- **progress-bar**: a 3px cobalt strip on the bottom edge; width grows by chapter.
## Frame types
1. **Cover** — cream, the diagonal tint panel over the right ~36%, a 3×3 dot
grid. A two-line near-black h1 on the left under an accent-line and meta
eyebrow, a muted body line below. Low density; readable at frame 0.
2. **Dashboard** — slide-header and h2 over three metric-cards: cobalt figure,
label, muted note, optional chip. The dense frame; figures are the accent.
3. **Bar ranking** — eyebrow and h2 over three to five labeled bar-tracks with
cobalt fills and percentages. Standard density.
4. **Pull quote** — centered over faint rings: a near-black blockquote under the
15% cobalt quote-mark, a muted cite in the eyebrow style. About 55% open.
5. **Split + highlight** — an Inter body column beside a split-highlight pull
quote, eyebrow and h2 above, a generous gutter. Standard.
6. **Closing / CTA** — cream with rings: accent-line, a centered near-black h1, a
body line, the single cobalt cta-button below. About 60% open.
## Motion
- Verbs: masks lift lines, the accent-line draws, bars fill, cards fade up, rings drift.
- Headlines reveal by a baseline mask, 0.6s `power3.out`, one line per beat (0.5s
at 120 BPM), after the accent-line draws 0→60px in 0.4s `power2.out`.
- Cards fade up 16px in 0.5s `power2.out`, 0.12s apart; a metric-value counts to
the brief's figure in 0.8s `power1.out` and holds.
- Bars fill from the left in 0.7s `power3.out`, one per half-beat in rank order.
- The diagonal panel drifts about 2% and the rings breathe 1→1.04, `sine.inOut`.
- Chapters cut on the downbeat or push left in 0.6s `power2.inOut`; the
slide-header lands first and the progress bar extends in 0.5s. The CTA may
take a soft cobalt glow on its last beat, the only shadow the system allows.
- Never: bounce, elastic, rotation, a second color, card shadows, fast zooms on type.
## Do and don't
- Do start every frame on cream and let cobalt carry every accent.
- Do set headlines near-black at −0.02em, eyebrows cobalt at 0.08em, figures
cobalt 600–700, body Inter 400 muted at 1.6; each line keeps its approved casing.
- Do use tinted cards (4% fill, 20% border, 10–14px radius, no shadow) and pill
chrome; keep atmosphere and the one solid CTA for cover and closing.
- Do fit the headline: up to 3 words h1, 4–6 h2, 7 or more h3, under 1500px.
Lean left on cover, dashboard and split; center quote and closer.
- Don't add a second accent or set a headline in cobalt.
- Don't shadow content, outline in opaque cobalt or square a content corner.
- Don't use the green and red chips as decoration; they mark a real comparison.
- Don't thicken borders to fill space, or run a headline edge to edge.
## 9:16 and 1:1
- Cover: title on top, the diagonal panel as a band below on 9:16; title in the
upper half, dots in a corner on 1:1.
- Dashboard: three cards stacked on 9:16; 2×2 on 1:1.
- Bar ranking: three to five tighter bars on 9:16; three on 1:1.
- Pull quote and closing: centered, taller on 9:16. Split + highlight stacks.
## Self-check before snapshot
- One near-black headline or cobalt figure dominates; only the dashboard runs dense.
- Cobalt is the only accent; headlines near-black; green and red inline only.
- Cards tinted and rounded, 20% borders, no shadow; atmosphere on cover and closing.
- Left on cover, dashboard and split; centered on quote and closer.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out. A
change chip needs a real comparison from the brief.
@@ -0,0 +1,131 @@
# bold-poster — tilted red editorial poster
Adapted from the HyperFrames frame preset `bold-poster` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants editorial authority and vintage gravitas: powerful,
printed, restrained. Results told loud, sports, food and culture brands, campaigns.
Failure looks like a rounded, soft-shadowed, multi-accent slide, or a straight hero.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
bg: "#FFFFFF" # canvas: the white sheet; type on red and dark panels
dark: "#1C1410" # ink, line, deep: body, headers, grid rules, dark panel
red: "#D8000F" # accent: figures, rules, labels, leftbars, bullets, progress, red panel
light: "#F5F2EF" # surface: off-white alternating stripes and panels
type: # px on 1920×1080; the tilt is part of the role
stat-big: { face: "Shrikhand" > serif-display, size: 422, weight: 400, line: 0.82, color: red, rotate: -6deg }
hero-title-red: { face: "Shrikhand" > serif-display, size: 259, weight: 400, line: 0.85, color: red, rotate: -4deg }
close-big: { face: "Shrikhand" > serif-display, size: 259, weight: 400, line: 0.88, color: red, rotate: -5deg }
hero-title: { face: "Shrikhand" > serif-display, size: 221, weight: 400, line: 0.88 }
hero-title-bottom: { face: "Shrikhand" > serif-display, size: 200, weight: 400, line: 0.9, rotate: 2deg }
red-quote: { face: "Shrikhand" > serif-display, size: 90, weight: 400, line: 1.15, color: bg, shadow: stacked }
section-header: { face: "Shrikhand" > serif-display, size: 63, weight: 400, line: 1.0 }
cell-number: { face: "Shrikhand" > serif-display, size: 52, weight: 400, line: 1.0, color: red }
card-title: { face: "Shrikhand" > serif-display, size: 36, weight: 400, line: 1.1 }
body: { face: "Libre Baskerville" > serif-text, size: 32, weight: 400, line: 1.75 }
body-cell: { face: "Libre Baskerville" > serif-text, size: 32, weight: 400, line: 1.55 }
bullet-body: { face: "Space Grotesk" > mono, size: 32, weight: 400, line: 1.45 }
label: { face: "Space Grotesk" > mono, size: 22, weight: 600, tracking: 0.2em, color: red }
cjk: { display: song 900, body: song 400, label: hei 600 }
space: { edge-y: 58, edge-x: 69, gap-y: 29, gap-x: 38 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`,
otherwise the stack after `>`. Shrikhand has no local look-alike: vendor it (OFL);
if the stack takes over, set display at 700 so a Bodoni or Didot bold keeps the
weight. Space Grotesk is chrome on the mono stack; bold in body switches to it at
600. The source sets labels in tracked capitals; copy keeps its approved casing (CJK
has no case), so red, 600 and 0.2em carry them, and tilt and red carry CJK display.
## Frame craft
- Squint: one display moment (hero stack, stat-big or close-big) dominates at 3–6×.
- Silence: statement frames hold 50–60% empty; the financial grid is the dense one.
- Restraint: four colors, red the only accent; one display moment per frame; the
stacked text-shadow on red display is the only shadow; three bullets at most.
## Components
- **hero-title-stack**: three Shrikhand lines (hero-title, -red, -bottom), at least
one red and one tilted, −4° or +2°; the signature opener.
- **stat-big**: the poster numeral at −6°, red on white or white on red with the shadow.
- **fin-grid**: 3px dark outer border, 1.5px dark inner rules meeting at every
crossing, square; cells hold a red cell-number, a label and body-cell text.
- **red-leftbar-card**: no outline, no shadow, square; a 4px red left rule, 19px of
padding, then card-title, body and bullets.
- **red-panel** / **dark-panel**: full-bleed red or dark with white type; red accents
on dark, the stacked shadow on display set on red.
- **stacked-text-shadow**: `2px 2px 0 rgba(28,20,16,.25), 4px 4px 0 rgba(28,20,16,.2),
6px 6px 0 rgba(28,20,16,.15)`; a text-shadow, on the red panel only.
- **bullet**: a red em-dash or round glyph hung at the left edge; no disc; three max.
- **progress-bar**: a 10px red strip on the bottom edge, width growing by chapter.
## Frame types
1. **Hero stack** — white. The hero-title-stack on the left, a mono label above,
a Baskerville tagline below, progress bar and counter at the foot; the right
half open. Readable at frame 0.
2. **Hero stat** — full red. A white stat-big at −6° with the stacked shadow,
centered, a label above, a Baskerville line below. About 60% open.
3. **Financial grid** — white, tight padding. Label and section-header over the
fin-grid: red numerals, mono labels, serif body. The dense frame.
4. **Pull quote** — full red. A two-line white red-quote with the stacked shadow,
left, a Baskerville cite beneath. About 50% open.
5. **Editorial cards** — white or off-white stripes. Two or three
red-leftbar-cards under label and section-header. Standard density.
6. **Closing statement** — white or dark. A red close-big at −5°, a label and a
Baskerville contact line, progress bar at the foot. About 60% open.
## Motion
- Verbs: panels hard-cut, display slams in at its tilt, rules draw, cells snap in.
- Statement display slams from 1.15× to rest in 0.3s `power4.out` on the downbeat;
its tilt is set from the first frame and never spins in.
- The hero stack is resolved at frame 0; label, tagline and progress bar arrive
around it, one per half-beat (0.25s at 120 BPM).
- Red and dark panels arrive as hard cuts on the downbeat; the stacked shadow is
on from the panel's first frame and never animates.
- Fin-grid: the outer border draws in 0.5s `power2.inOut`, inner rules 0.2s
behind; cells fill row by row on half-beats, numerals cut in whole.
- Leftbar rules grow downward in 0.4s `power3.out` before their text; bullets
tick in 0.25s apart. The progress bar steps at each cut in 0.2s `power2.out`.
- Never: crossfaded panels, blur, bouncing type, a tilt rotating into place, two
display moves in one frame.
## Do and don't
- Do stack the hero in three Shrikhand lines, at least one tilted and one red.
- Do set every figure in red Shrikhand; tilt statement display −5° to −6°.
- Do set labels in mono 600 at 0.2em in red and body in Baskerville at 1.75;
each line keeps its approved casing.
- Do rule data with the 3px + 1.5px double border, use leftbar cards elsewhere,
and cap red em-dash bullets at three.
- Do fit the line: up to 2 words stat-big or hero-title-red, 3–4 hero-title, 5 or
more section-header, under 1500px; one display moment per frame.
- Don't add a second accent, round a corner, or use any other shadow.
- Don't put Shrikhand in body, serif in labels or the label face in headlines.
- Don't use disc bullets, leave a red statement untilted, or crowd a statement frame.
## 9:16 and 1:1
- Hero stack: centered and taller on 9:16; stack over tagline on 1:1.
- Hero stat and closing: centered; close-big stacks on 9:16.
- Financial grid: two across or stacked on 9:16; 2×2 on 1:1.
- Pull quote: stacked on 9:16, centered on 1:1; editorial cards stack on both.
- Tilts hold on every ratio without clipping; the progress bar spans the bottom.
## Self-check before snapshot
- One display moment dominates; statement frames 50–60% open; only the fin-grid is dense.
- Four colors; red the lone accent and never body text.
- Statements tilted, figures red, no tilted line clipped; Baskerville body at 1.75.
- Flat: the stacked text-shadow on red display is the only shadow; square
corners; three bullets at most.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out.
Counters and progress are decorative.
@@ -0,0 +1,134 @@
# broadside — two-register protest poster
Adapted from the HyperFrames frame preset `broadside` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants presence and authority carried by type alone: bold,
typographic, declarative. Manifestos, positioning, design-led and developer brands.
Failure looks like a multi-accent corporate deck of small blocks and no shape.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
ink-black: "#111111" # deep: dark-register ground; type on orange
ink-black-alt: "#1A1A18" # deep variant
fire-orange: "#E85D26" # accent: lone accent on dark; the orange register
cream: "#F0ECE5" # ink on the dark register
cream-muted: "#888880" # ink-muted on dark
cream-hint: "#505048" # ink-hint on dark: resting bars
border-dark: "#282826" # line on dark: hairlines
ink-on-orange-muted: "rgba(17,17,17,0.75)" # ink-muted on orange: lead
ink-on-orange-hint: "rgba(17,17,17,0.55)" # ink-hint on orange: kicker
ink-on-orange-faint: "rgba(17,17,17,0.40)" # faint on orange: catalogue mark
ink-on-orange-border: "rgba(17,17,17,0.20)" # line on orange
type: # px on 1920×1080
display: { face: "Barlow" > sans, size: 250, weight: 900, line: 0.88, tracking: -0.04em }
fadelist-title: { face: "Barlow" > sans, size: 202, weight: 900, line: 0.9, tracking: -0.04em }
quote-mark: { face: "Barlow" > sans, size: 192, weight: 900, line: 0.6 }
h1: { face: "Barlow" > sans, size: 144, weight: 800, line: 0.9, tracking: -0.03em }
fadelist-item: { face: "Barlow" > sans, size: 144, weight: 900, line: 1.0, tracking: -0.03em }
stat-value: { face: "Barlow" > sans, size: 106, weight: 900, line: 1.0, tracking: -0.04em }
h2: { face: "Barlow" > sans, size: 86, weight: 700, line: 1.1, tracking: -0.02em }
quote-text: { face: "Barlow" > sans, size: 73, weight: 700, line: 1.15, tracking: -0.02em }
h3: { face: "Barlow" > sans, size: 54, weight: 600, line: 1.2 }
lead: { face: "Barlow" > sans, size: 36, weight: 400, line: 1.5 }
body: { face: "Barlow" > sans, size: 28, weight: 400, line: 1.6 }
caption: { face: "Barlow" > sans, size: 24, weight: 400, line: 1.5 }
label: { face: "IBM Plex Mono" > mono, size: 22, weight: 500, tracking: 0.14em }
cjk: { display: hei 900, body: hei 400 }
space: { edge: 106, gap-lg: 67, gap-md: 38, gap-sm: 19, rule: 1 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`,
otherwise the stack after `>`. Barlow carries every text role, its range from
weight 400–900 and size; mono is chrome only. The source sets display lowercase
and chrome in capitals; copy keeps its approved casing. A lowercase brief line is
the native register; otherwise 900, size and tight tracking carry the look. CJK
has no case, so the registers carry it; PingFang stops at 600, so raise size.
## Frame craft
- Squint: one display moment dominates at 3–6×, big enough to read as shape first.
- Silence: declarative frames stay 45–55% empty; the stat grid is the dense one.
- Restraint: one register per frame; fire-orange is the only hue, accent on dark
and environment on orange; three bullets at most.
## Components
- **registers**: two surfaces, one per frame. Dark: ink-black, cream type, orange
accent. Orange: fire-orange, ink-black type, ink overlays for muted tones.
- **slide-chrome**: 1px bars top and bottom (border-dark, or 20% ink on orange),
label left, number right; off on cover, chapter, statement, quote and end.
- **kicker**: the mono eyebrow, fire-orange on dark, 55% ink on orange.
- **rule**: a 36×2px stub, orange on dark, ink on orange; the only ornament.
- **broadside-num**: a faint mono catalogue numeral top-left on orange covers.
- **stat-card**: a single 1px border-dark top rule; stat-value, body and a label.
- **bullet**: a fire-orange mono `/` before each lead-size item; three at most.
- **bar-track**: vertical bars on a 1px left axis; cream-hint bars, one orange.
- **compare-panel**: two equal panels split by a 1px rule; the right may fill orange.
- **fadelist**: three fadelist-item words at opacity 1.0 / 0.5 / 0.22 facing one
oversized fadelist-title.
## Frame types
1. **Cover** (orange) — a one- or two-word display in ink, left, over the rule
stub and a kicker, a lead in 75% ink beneath; broadside-num top-left, meta
top-right, no bars. About 45% open; readable at frame 0.
2. **Statement** (dark) — a two-to-four-word display or h1 in cream, left, one
clause in fire-orange, a kicker, no bars. About 55% open.
3. **Stat grid** (dark, bars on) — a kicker over three stat-cards: orange 900
figures, Barlow labels, mono notes. The dense frame.
4. **Fadelist** (dark) — the three-step word stack facing a fire-orange
fadelist-title: before, during, after. Low to moderate density.
5. **Pull quote** (dark, bars off) — 700 quote-text under the oversized orange
quote-mark, block under 1500px, a mono name and role. About 50% open.
6. **Compare** (dark into orange) — a dark panel documenting in cream beside an
orange panel declaring in ink, a mono label and h3 on each. Standard.
## Motion
- Verbs: registers hard-cut, words slam, one clause ignites, hairlines draw.
- Register changes are hard cuts on the downbeat, never a crossfade; the cut is
the punctuation.
- Display and h1 words land one per beat (0.5s at 120 BPM), 1.06× to rest in
0.25s `power4.out`; the clause turns orange on its own beat, no color tween.
- Hairlines draw in 0.4s `power2.inOut`; each figure cuts in whole once its rule
is down. Bars rise in 0.6s `power3.out`, the orange one last.
- Fadelist words arrive on successive beats already at their opacity; the title
slams in last.
- On the cover the display word holds from frame 0; the rule stub draws, then
kicker, lead and numeral arrive on half-beats.
- Never: shadows, blur, rotation, bounce, crossfaded registers, two display moves.
## Do and don't
- Do set Barlow display at 900, tracked tight; each line keeps its approved casing.
- Do make fire-orange the environment on declarative frames, the lone accent on dark.
- Do keep chrome in mono at 0.14em and mark bullets with the mono `/`, three at most.
- Do say one statement per frame; build hierarchy from weight, size and 1px rules.
- Do leave chrome bars off declarative frames; lean left; the type is the layout.
- Do fit the line: up to 2 words display, 3–4 h1, 5 or more h2, under 1500px.
- Don't add a second accent or a cream/paper register, or set cream type on orange.
- Don't use drop shadows, rounded surfaces or gradient grounds.
- Don't add a serif companion or set chrome in Barlow.
## 9:16 and 1:1
- Cover: word on top, lead below on 9:16; centered word on 1:1.
- Statement: stacked taller on 9:16, centered on 1:1. Fadelist: stack over title.
- Stat grid: three stacked on 9:16; two plus one on 1:1.
- Pull quote: mark on top, quote below on 9:16; centered on 1:1.
- Compare: dark over orange, stacked, on both. Mono chrome stays Latin and digits.
## Self-check before snapshot
- Exactly one display moment dominates; only the stat grid runs dense.
- Declarative frames 45–55% empty, chrome bars off.
- One register per frame; ink on orange, cream on dark; no second hue.
- No shadow, no radius, 1px hairlines; three bullets at most, each with the `/`.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out.
Catalogue numerals may run in sequence.
@@ -0,0 +1,133 @@
# capsule — pill-shaped playful editorial
Adapted from the HyperFrames frame preset `capsule` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants to feel friendly, soft and approachable yet still
editorial: consumer apps, creator tools, community launches, playful explainers.
Failure looks like a flat SaaS card grid: unstroked boxes, no pills, bare corners.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
cream: "#F5F5F0" # canvas: every frame, under glows and grain
ink: "#1A1A1A" # ink: every headline and line of body
outline: "#1E1E1E" # line: the stroke around every pill
white: "#FFFFFF" # surface: content pills and cards
coral: "#E85D4E" # accent, warm: accent-line, first card-icon
lime: "#C4D94E" # accent, bright neutral: orbit center, highlight
lavender: "#C5B5E0" # accent, cool: header tag-pill
sky: "#8BB4F7" # accent, cool: highlight, card-icon
violet: "#A06CE8" # accent, cool: closing italic word
yellow: "#F2D160" # accent, warm: title-pill
peach: "#F5B895" # accent, warm
mint: "#A8E6CF" # accent, cool
shadow: "rgba(26,26,26,0.08)" # deep: offset shadows only, 8% ink
type: # px on 1920×1080
display: { face: "Bodoni Moda" > serif-display, size: 230, weight: 800, line: 0.88, tracking: -0.03em }
closing-display: { face: "Bodoni Moda" > serif-display, size: 163, weight: 800, line: 0.95, tracking: -0.03em }
headline: { face: "Bodoni Moda" > serif-display, size: 96, weight: 700, line: 1.05, tracking: -0.02em }
quote-display: { face: "Bodoni Moda" > serif-display, size: 81, weight: 600, line: 1.3, tracking: -0.01em }
section-headline: { face: "Bodoni Moda" > serif-display, size: 77, weight: 700, line: 1.05, tracking: -0.01em }
stat-number: { face: "Bodoni Moda" > serif-display, size: 69, weight: 800, line: 1.0, tracking: -0.03em }
orbit-numeral: { face: "Bodoni Moda" > serif-display, size: 50, weight: 700, line: 1.0 }
card-headline: { face: "Bodoni Moda" > serif-display, size: 36, weight: 700, line: 1.1 }
subtitle: { face: "Space Grotesk" > sans, size: 36, weight: 400, tracking: 0.18em }
body: { face: "Space Grotesk" > sans, size: 32, weight: 400, line: 1.6 }
pill-text: { face: "Space Grotesk" > sans, size: 24, weight: 600, tracking: 0.12em }
label: { face: "Space Grotesk" > sans, size: 22, weight: 500, tracking: 0.1em }
cjk: { display: song 700, body: hei 400 }
space: { edge: 96, gap: 38, card-pad: [38, 31], stroke: 4, pill-radius: 9999, card-radius: 32 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`,
otherwise the stack after `>`. Bodoni runs 700–800 (600 on quotes), always ink;
where the stack stops at bold, take its bold and never fake a heavier weight.
## Frame craft
- Squint: one Bodoni headline or stat dominates at 3–6× its nearest neighbor.
- Silence: declarative frames fill with atmosphere, not content; their pills are
wallpaper. Pillar cards and the stat grid are the dense frames and drop it.
- Restraint: every pill stroked, candy on fills and stat numerals only, shadows
under content only. The nine candies mean nothing: mix warm, cool and lime,
never two of a family side by side. A brand primary takes one slot; no tenth.
## Components
- **pill**: the container for every chip, button, label, tile, node and bar:
9999px radius small, 32px on cards, 4px outline, candy or white fill, never
unstroked. Shadows fall down-right in `shadow`, unblurred: 4px on small nodes,
6px on stat and title pills and satellites, 8px on cards, 12px on a screen.
- **pill-card**: white, 32px radius, outline, 8px shadow, padded 38/31; a
card-icon, a card-headline, then body.
- **stat-pill**: white, 32px, outline, 6px shadow; a candy stat-number over a
pill-text label and a 40px candy bar. The one place color touches a numeral.
- **title-pill**: a yellow 9999px pill, outline, 6px shadow, over the headline.
- **card-icon**: a 60px outlined candy circle holding an ink Bodoni numeral.
- **floating-pill**: outlined candy pills and dots tilted −20° to +25°, flat, 5–8
per declarative frame, off the edges; short neutral words from the brief, or none.
- **quote-highlight**: an outlined lime or sky pill wrapped around one phrase of
a Bodoni quote; it replaces bold.
- **bar-track**: a 36px outlined cream track filled by a candy pill, value at its end.
- **accent-line**: a coral pill rule, 60×4px (80×4 on the closing frame).
- **atmosphere**: 1–3 radial candy glows at 6–15% over cream, 4% grain; every frame.
## Frame types
1. **Cover** — glows, grain and 5–8 floating pills at the edges; a yellow
title-pill over a one- or two-line display headline in ink, centered, a key
word in candy italic; accent-line and subtitle beneath. Readable at frame 0.
2. **Pillar cards** — no wallpaper; a lavender tag-pill and section-headline over
three pill-cards with coral, sky and lime icons. The dense frame.
3. **Stat grid** — a centered section-headline over a row of 3–4 stat-pills.
4. **Pull quote** — a few floating pills; a quote-display quote in ink at left,
one phrase highlighted, accent-line, label attribution. About half open.
5. **Orbit** — a 160px lime circle with an orbit-numeral ordinal, circled by 4–6
tilted candy satellites with 6px shadows.
6. **Closing plate** — wallpaper back; a yellow title-pill over a centered
closing-display sign-off in ink, one word in violet italic, 80px accent-line.
## Motion
- Verbs: pills pop, shadows land, wallpaper floats, highlights sweep, bars fill.
- Content pills pop from scale 0.86 in 0.45s `back.out(1.7)`, 0.1s apart, so
three cards land in one beat; each shadow slides out 0.1s later, in 0.2s.
- Floating pills are in place from the start and drift 8–16px with a 2–4° sway,
`sine.inOut` yoyo; the glows breathe with them.
- Bodoni lines rise 24px from a baseline mask, 0.6s `power3.out`, a line per
beat; a highlight sweeps under its phrase on the next beat in 0.4s.
- Bars grow from the left, 0.7s `power3.out`, while numerals count up to the
brief's value; orbit satellites settle in on `back.out(1.4)`, 0.1s apart.
- Chapters cut on the downbeat, hard or behind one candy pill wiping across, 0.5s.
- Never: elastic wobble, spinning pills, blurred shadows, a headline that bounces.
## Do and don't
- Do make every container a stroked pill: 9999px small, 32px cards, 4px outline.
- Do set Bodoni in ink, negative-tracked; color only on stat numerals, pill fills
and one italic key word. Shadows under content pills; floating pills flat.
- Do fill declarative frames with pills, glows and grain; center cover, closing
and orbit, anchor cards and quote left.
- Don't use a sharp text box, an unstroked pill, a blurred or recolored shadow,
bold for emphasis, a tenth candy, or a headline running edge to edge.
- Copy keeps its approved casing; pill text, labels and subtitle get their look
from weight 500–600 and 0.1–0.18em tracking (CJK has no case).
## 9:16 and 1:1
- Cover: display on top, pills as a band below on 9:16; centered, fewer pills on 1:1.
- Pillar cards: three stacked on 9:16, two plus one on 1:1; stats 2×2 on both.
- Quote: on top with the highlight inline on 9:16; centered on 1:1. Closing: centered.
- Orbit: three satellites on 9:16, four on 1:1; fewer floating pills as it tightens.
## Self-check before snapshot
- One Bodoni element dominates at 3–6×; only pillar cards and stats run dense.
- Every container a stroked pill; candy on fills, stat numerals, one italic word.
- Shadows unblurred and under content only; glow and grain on every frame.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out.
@@ -0,0 +1,134 @@
# cartesian — stone-and-hairline museum catalogue
Adapted from the HyperFrames frame preset `cartesian` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants quiet authority and editorial rigor: sparse,
literary, restrained. Architecture, design and research studios, considered
professional launches, an essay told one idea per frame.
Failure looks like a shadowed, rounded SaaS card deck, or a frame crowded past
its stone margins.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
bg-primary: "#EDE8E0" # canvas: warm stone on every frame
bg-secondary: "#E2DBD1" # surface: image plates and portrait discs only
text-primary: "#1A1A1A" # ink: headlines and the one ink rule
text-secondary: "#5A5A5A" # ink-muted: body copy
accent: "#8A8178" # accent: taupe labels, numerals, small type only
line: "#B8B0A4" # line: every hairline and every ring
white-overlay: "rgba(255,255,255,0.3)" # card fill; the stone shows through
type: # px on 1920×1080
display: { face: "Playfair Display" > serif-display, size: 154, weight: 400, line: 1.04 }
quote-mark: { face: "Playfair Display" > serif-display, size: 173, weight: 400, line: 0.5, color: line }
h1: { face: "Playfair Display" > serif-display, size: 119, weight: 400, line: 1.06 }
h2: { face: "Playfair Display" > serif-display, size: 77, weight: 400, line: 1.1 }
stat-figure: { face: "Playfair Display" > serif-display, size: 58, weight: 400, line: 1.0 }
card-headline: { face: "Playfair Display" > serif-display, size: 38, weight: 400, line: 1.15 }
timeline-headline: { face: "Playfair Display" > serif-display, size: 36, weight: 400, line: 1.1 }
h3: { face: "Playfair Display" > serif-display, size: 35, weight: 400, line: 1.1 }
subtitle: { face: "Inter" > sans, size: 36, weight: 400, line: 1.5 }
body: { face: "Inter" > sans, size: 32, weight: 400, line: 1.6, color: text-secondary }
attribution: { face: "Inter" > sans, size: 24, weight: 400, tracking: 0.13em, color: accent }
label: { face: "Inter" > sans, size: 22, weight: 500, tracking: 0.21em, color: accent }
micro: { face: "Inter" > sans, size: 22, weight: 400, tracking: 0.17em, color: accent }
cjk: { display: song 700, body: hei 400 }
space: { edge-x: 134, edge-y: 96, gap-xl: 115, gap-lg: 96, rule: 1 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`,
otherwise the stack after `>`. Playfair stays weight 400 in ink, never bold or
taupe; contrast comes from size and the serif–sans switch. Italic on a key word.
## Frame craft
- Squint: one Playfair moment dominates at 3–6× its neighbor; serif against sans
and size carry the hierarchy, never weight.
- Silence: declarative frames stay 55–60% empty. No frame is dense; even the
agenda breathes, because crowding is what breaks this system.
- Restraint: two geo rings per frame at most, the one ink rule used sparingly, no
attention color: five stones and ink. A brand accent stays at label scale.
## Components
- **hairline**: 1px solid `line`. Every separator, card edge, timeline rule and
photo ring is this line; nothing thicker exists.
- **card**: white-overlay fill in a 1px line border, square, no shadow; the faint
fill is what sets it apart from bare stone.
- **card-icon**: a 40px circle ringed in 1px line, a taupe Playfair numeral inside.
- **agenda-row**: taupe Playfair numeral left, ink Playfair label right, both h3,
on a 1px line.
- **timeline**: one 1px line across the top of all phases; year, timeline-headline
and body under it. No nodes, no dots.
- **stats-cluster**: modest stat-figures under a 1px top rule with taupe labels;
never a hero numeral.
- **geo-ring**: a 1px line circle 192–960px across at 20–50% opacity, a dashed
inner ring at 70–80% of its diameter; compass work behind content, 1–2 a frame.
- **horizontal-accent**: 346×1px in text-primary, the only ink line; cover and closing.
- **vertical-line**: optional 1px line at 30–40%, full height, ~96px in from an edge.
- **image-placeholder**: a bg-secondary plate in a 1px line border holding a
supplied photo or screen; with none, a crossed X of 1px taupe diagonals at ±30°
and a micro caption — a drafting mark, never a fake picture.
- **team-photo**: a bg-secondary circle ringed in 1px line with a supplied
portrait or a taupe Playfair initial.
## Frame types
1. **Cover** — a 650px geo ring right of center, an optional vertical-line left; a
taupe label over a two- or three-line display or h1 in ink at left, italic key
word, then subtitle, optional meta row and ink rule. ~55% empty; readable at frame 0.
2. **Agenda / index** — a label and an h2 over 4–6 generously spaced agenda
rows; taupe numerals are the only emphasis.
3. **Pull quote** — centered on a dashed 500px ring: the quote-mark at half
opacity, a two-line quote at h2 in ink, a taupe attribution. About 60% open.
4. **Closing plate** — centered in the largest ring (~770px): a label, an h1 or
display sign-off with an italic word, a horizontal-accent. About 60% open.
5. **Two-column editorial** — columns 115px apart: label, h2 and body on one side,
an image-placeholder or card on the other. No accent.
6. **Stats / timeline** — a label and an h3 over a stats-cluster or a timeline,
each under its 1px top rule. No accent, no nodes.
## Motion
- Verbs: rings draw, hairlines draw, lines rise into masks, labels fade, rings drift.
- A ring draws its circumference in 0.9s `power2.inOut`, the dashed inner ring
0.25s behind, then turns 2–4° across the scene on `sine.inOut`.
- Headlines reveal by a baseline mask, 0.7s `power3.out`, a line per beat; the
italic word arrives with its line. The cover headline is set from frame 0.
- Hairlines draw left to right in 0.6s `power2.out`; rows and phases follow 0.15s
apart, fading up 12px. Labels fade in over 0.4s `sine.out` half a beat later.
- The horizontal-accent draws last, 0.5s `power2.inOut`, on a downbeat.
- Chapters change on the downbeat with a 0.4s crossfade; no wipes, no slides.
- Never: bounce, overshoot, scale pops, rotating type, shadows, fast zooms.
## Do and don't
- Do make every separator one 1px line; set Playfair at 400 in ink, body in
text-secondary, labels in taupe with wide tracking.
- Do layer one or two geo rings behind content; fill plates with bg-secondary.
- Do let frames breathe, never crowded; center quote and closing, the rest left.
- Don't add an attention color, bold Playfair, set a headline in taupe, or draw a
border of 2px or more.
- Don't add shadows, elevated cards or rounded rectangles; circles are the only
curve. Don't run a headline edge to edge.
- Copy keeps its approved casing; labels get their look from taupe, size and
0.13–0.21em tracking (CJK has no case).
## 9:16 and 1:1
- Cover: headline on top, ring below on 9:16; upper half, ring behind on 1:1.
- Quote and closing: centered, the ring scaled to the short side; agenda rows tighten.
- Two-column editorial: stacks, text over the plate, on both ratios.
- Stats / timeline: a vertical list without the top rule on 9:16; compact on 1:1.
## Self-check before snapshot
- One Playfair moment dominates; declarative frames 55–60% empty, none crowded.
- Five stones and ink; headlines ink, body gray, labels taupe; two rings at most.
- Every separator 1px; horizontal-accent the only ink rule; no shadow or rounded box.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out.
@@ -0,0 +1,134 @@
# cobalt-grid — two-color risograph on graph paper
Adapted from the HyperFrames frame preset `cobalt-grid` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants clarity and measured authority: restrained,
systemic, editorial. Trend reports, research and data stories, developer and
infrastructure brands, annual reviews.
Failure looks like a colorful dashboard with a second hue, or the grid switched off.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
paper: "#F0EBDE" # canvas: cream on every frame
paper-2: "#E6E0CE" # canvas variant; never a card
ink: "#1F2BE0" # ink and accent: the only ink there is
ink-soft: "#5560E5" # ink-muted: italic callouts only
grid: "rgba(31,43,224,0.10)" # line: graph grid, chart off-cells
ink-faint: "rgba(31,43,224,0.18)" # line, soft: ledger row dividers
type: # px on 1920×1080
vbig-numeral: { face: "Newsreader" > serif-text, size: 240, weight: 400, line: 0.9, tracking: -0.015em }
display-hero: { face: "Newsreader" > serif-text, size: 200, weight: 400, line: 0.9, tracking: -0.008em }
display-closing: { face: "Newsreader" > serif-text, size: 180, weight: 400, line: 0.96, tracking: -0.005em }
display-chapter: { face: "Newsreader" > serif-text, size: 131, weight: 400, line: 1.0, tracking: -0.005em }
display-quote: { face: "Newsreader" > serif-text, size: 109, weight: 400, line: 1.05, tracking: -0.005em }
headline-index: { face: "Newsreader" > serif-text, size: 96, weight: 400, line: 0.95 }
headline: { face: "Newsreader" > serif-text, size: 88, weight: 400, line: 0.95 }
ed-callout: { face: "Newsreader" > serif-text, size: 50, weight: 400, style: italic, line: 1.1 }
row-headline: { face: "Newsreader" > serif-text, size: 40, weight: 400, line: 1.05 }
table-name: { face: "Newsreader" > serif-text, size: 36, weight: 400, line: 1.15 }
body-lede: { face: "Hanken Grotesk" > sans, size: 36, weight: 400, line: 1.5 }
body: { face: "Hanken Grotesk" > sans, size: 32, weight: 400, line: 1.5 }
micro-strong: { face: "Hanken Grotesk" > sans, size: 26, weight: 600, tracking: 0.18em }
micro: { face: "Hanken Grotesk" > sans, size: 22, weight: 600, tracking: 0.16em }
mono-tag: { face: "DM Mono" > mono, size: 24, weight: 400, tracking: 0.05em }
mono-chrome: { face: "DM Mono" > mono, size: 22, weight: 400, tracking: 0.06em }
cjk: { display: song 700, body: hei 400 }
space: { edge: 80, top: 134, bottom: 115, gap: 38, cell: 38, rule: 2, rule-soft: 1 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`,
otherwise the stack after `>`. Newsreader is a news text serif, so it takes
serif-text, not the Didot stack; it stays 400 in cobalt and size alone ranks.
## Frame craft
- Squint: one Newsreader moment, usually display-hero or vbig-numeral, dominates
at 3–6× its neighbor; size ranks, weight never does.
- Silence: cover, chapter, quote and colophon stay 45–60% empty with the grid
showing; the index ledger and data frame are dense by quiet repetition.
- Restraint: cream and cobalt, never a second hue; one glitch column and at most
one qr-block per declarative frame. Emphasis is size, the serif, a delta or opacity.
## Components
- **graph-grid**: 1px lines in `grid` on 38px cells behind every frame; never off.
- **hairlines**: two 2px ink rules ~58px in from top and bottom, inset 80px at the
sides, framing every composition.
- **page-chrome**: mono-chrome in ink above the bottom hairline: page number at
right, a nav or meta hint at left. The only persistent chrome.
- **pixel-glitch**: a stair-stepped column 270–576px wide, full height, of
vertical scanlines (2px ink, 6px gap) on paper; above the grid, under the
headline. Right on cover and data, left on chapter and colophon.
- **qr-block**: an 8×8 mosaic 58–173px square, ink cells on, grid tint off, on
paper with a 2px paper outline for legibility, not a shadow. Corner punctuation.
- **topbar-rule**: a headline-index title and a mono-tag over a 2px ink rule; the
header of index, data and table frames.
- **ledger-row**: mono number · Newsreader table-name · Hanken body · mono delta
(↑ ↓ —), 1px ink-faint dividers under a 2px ink header rule.
- **pixel-stack-bar**: grid-unit cells stacked on a 2px ink baseline, ink on,
grid tint off, mono-chrome ticks below; it echoes the glitch.
- **vstack-label**: mono-chrome set vertically along a frame edge.
## Frame types
1. **Hero cover** — a ~500px glitch column at right, a qr-block top right; a one-
or two-line display-hero in cobalt at left, an italic ed-callout in ink-soft
under it, a micro kicker and mono meta above. ~45% open; readable at frame 0.
2. **Index ledger** — taller top and bottom pads; a topbar-rule over 4–6 ledger
rows. Delta arrows are the only emphasis. The dense frame; no glitch.
3. **Chapter opener** — a ~270px glitch at left in low opacity, a mono chapter
index over a display-chapter title, an optional lede ≤806px wide. ~60% open.
4. **Data frame** — a topbar-rule with a mono figure tag over 6–8 pixel-stack-bars
on the baseline with mono ticks. Cobalt cells are the only accent.
5. **Manifesto / quote** — centered: a micro kicker, a two- or three-line
display-quote, a 1px ink attribution rule and a mono byline. ~55% open.
6. **Colophon** — the glitch at left, mirroring the cover; a right-aligned
display-closing under a micro kicker; 3–4 columns of mono credits at the foot.
## Motion
- Verbs: rules draw, the glitch steps in, QR cells switch on, cells stack, lines unmask.
- Grid, hairlines and page chrome are on frame 0 and never move; chapters cut hard
on the downbeat, changing only what sits on the paper.
- Newsreader lines reveal by a baseline mask, 0.6–0.7s `power3.out`, a line per
beat; the ed-callout fades in over 0.4s half a beat later.
- The glitch builds by its stair steps in 0.5s on `steps(6)`; QR cells switch on
in a fixed shuffled order over 0.4s on `steps(8)`.
- Topbar and attribution rules draw in 0.5s `power2.inOut`; ledger rows follow
0.1s apart, rising 8px; delta arrows land last, on the beat.
- The baseline draws in 0.4s, then each bar fills cell by cell on `steps(n)`
(n = its cells) over 0.6–0.8s, 0.1s apart.
- Never: a second hue or color shift, blur, bounce, rotation, a moving grid.
## Do and don't
- Do keep the graph-grid, both hairlines and page chrome on every frame.
- Do set Newsreader at 400 in cobalt, negative-tracked on display; size ranks.
- Do render data as pixel-stack cells; give declarative frames a glitch and a QR.
- Do keep declarative frames sparse, index and data dense; cover, index and
chapter left, quote centered, colophon right-aligned.
- Don't add a second ink, bold the serif, round a corner, draw a circle, cast a
shadow (the QR outline is not one) or run a headline edge to edge.
- Copy keeps its approved casing; labels get their look from Hanken 600 and
0.16–0.18em tracking (CJK has no case).
## 9:16 and 1:1
- Cover: headline on top, glitch as a full-width band on 9:16; glitch lower on 1:1.
- Ledger: fewer rows on 9:16, two columns drop to one on 1:1. Chapter: title on top.
- Data: 4–5 taller bars on 9:16, a square chart on 1:1; quote centered and taller.
- Colophon: stacked, glitch on top on 9:16; centered on 1:1. Grid and rules hold.
## Self-check before snapshot
- One Newsreader moment dominates; declarative frames 45–60% open.
- Cream and cobalt only; grid on, both hairlines on, page chrome above the rule.
- No shadow (QR outline aside), no rounded corner, no circle.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out. Bar
heights and deltas too; catalogue ordinals (001, 002) may be decorative.
@@ -0,0 +1,133 @@
# code-editorial — warm brand book with a code surface
Adapted from the HyperFrames frame preset `code-editorial` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the subject is considered, literary and developer-facing: a code
change, release or developer doc that wants editorial calm and a first-class
code surface. Changelogs, pull-request walkthroughs, SDK and API launches.
Failure looks like an IDE screenshot on pure white, or coral turning up thrice.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
ink: "#141413" # ink: headlines and body on cream and tile
cream: "#FAF9F5" # canvas: the default ground; also type on navy
tile: "#EFE9DE" # surface: content half a step darker
tile-strong: "#ECE3D4" # surface: the stronger step
coral: "#CC785C" # accent: one moment per frame, and the ✱ mark
navy: "#181715" # deep: code surface body, gravity grounds
navy-soft: "#1F1E1B" # deep: dark cards
navy-elev: "#252320" # deep, raised: code title bar and status strip
syntax: { string: "#5DB8A6", number: "#E8A55A", success: "#5DB872", warn: "#C64545" } # fixed; keywords coral; never remapped
lines: { hairline: "ink at 12%", hairline-strong: "ink at 20%", dark: "cream at 14%, on navy" }
shadow: { card: "0 1px 3px ink at 8%, 0 4px 16px ink at 4%" } # the only shadow, used rarely
type: # px on 1920×1080
display-cover: { face: "EB Garamond" > serif-text, size: 190, weight: 400, line: 0.98, tracking: -0.028em }
number-hero: { face: "EB Garamond" > serif-text, size: 180, weight: 400, line: 0.95, tracking: -0.025em }
display: { face: "EB Garamond" > serif-text, size: 140, weight: 400, line: 1.02, tracking: -0.022em }
display-italic: { face: "EB Garamond" > serif-text, size: 129, weight: 400, style: italic, line: 1.05, tracking: -0.012em }
quote-pull: { face: "EB Garamond" > serif-text, size: 96, weight: 400, style: italic, line: 1.12, tracking: -0.012em }
headline: { face: "EB Garamond" > serif-text, size: 88, weight: 400, line: 1.06, tracking: -0.018em }
card-title: { face: "Inter" > sans, size: 44, weight: 500, line: 1.25, tracking: -0.005em }
lead: { face: "Inter" > sans, size: 40, weight: 400, line: 1.5 }
body: { face: "Inter" > sans, size: 29, weight: 400, line: 1.5 }
button: { face: "Inter" > sans, size: 28, weight: 500, line: 1.0 }
tag: { face: "Inter" > sans, size: 26, weight: 500, tracking: 0.18em }
number-unit: { face: "JetBrains Mono" > mono, size: 40, weight: 500, line: 1.0 }
code: { face: "JetBrains Mono" > mono, size: 32, weight: 400, line: 1.6 }
kicker: { face: "JetBrains Mono" > mono, size: 28, weight: 500, tracking: 0.16em }
mono-label: { face: "JetBrains Mono" > mono, size: 26, weight: 500, tracking: 0.02em }
cjk: { display: song 400, body: hei 400 }
space: { edge: 80, gap: 33, rule: 1, radius: [6, 8, 12], pill: 9999 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`, otherwise
the stack after `>`. EB Garamond is warm old-style: serif-text, never Didot or sans;
display stays 400, italic voices a stance; CJK in mono kickers falls back to hei.
## Frame craft
- Squint: one EB Garamond display moment dominates at 3–6× its neighbor.
- Trinity: cream or tile ground, ink voice, coral exactly once per frame; warm
navy only as the code surface or a gravity ground. No cool gray, pure white,
pure black or fourth hue.
- Restraint: lift is a 1px hairline plus, rarely, the one card shadow; a tile on
cream reads raised by its warm half-step. Density is free, as content wants.
## Components
- **card-hairline**: cream or tile, 12px radius, 1px hairline and the card shadow;
card-title over body. Never a heavy drop, glow or gradient.
- **kicker-spike**: a coral ✱ then 2–5 indexical words in the mono kicker; never a
sentence, never without the ✱.
- **coral-callout**: the one coral moment, 8px radius: a coral field with cream
button or headline type, or cream with a coral edge; CTA, inline link or band.
- **number-lockup**: a number-hero figure with a mono number-unit beside it (200K,
+1,204, −318, 17 files); serif figure, unit always mono.
- **pull-quote**: quote-pull italic (a commit message, a reviewer's line, the
thesis) with a tag cite under it.
- **section-rule**: a 1px hairline, dark variant on navy; the only separator. A
coral 1px rule may draw on to open a section. Never 2px or more.
- **code-surface**: a navy panel, 8px radius, dark 1px border, a navy-elev title bar
(filename in mono-label) and status strip; cream `code` with coral keywords, teal
strings, amber numbers. Real code from the diff or brief; this is its frame.
- **spike-mark**: the ✱ glyph (U+2731), always coral; the brand mark.
## Frame types
1. **Cover** — cream, 80px edge; a kicker-spike over a two- or three-line
display-cover in ink, a lead, a mono-label index strip (repo · branch, or
product · version) along an edge. The ✱ is the one coral. Readable at frame 0.
2. **Statement** — cream, or navy for gravity; a kicker and one two-line display
sentence, display-italic for a stance. No accent, or one coral word.
3. **Code surface** — cream framing the code-surface (diff, before-and-after or typed
snippet); outside it one coral cue such as a +/− gutter mark. The dense frame.
4. **Number / impact** — a kicker, a number-lockup over a 1px rule, a lead or
caption; the figure in ink, at most the unit in coral.
5. **Pull-quote** — a kicker, a quote-pull, a tag cite (name · role) beneath.
6. **Closing / CTA** — cream or navy; a short display sign-off, the coral-callout,
and for a shipped-by close a row of hairline-ringed avatars with mono names.
## Motion
- Verbs: lines dissolve in, code types on, numbers count up, a coral rule draws.
- Serif display fades up 16px in 0.6s `power2.out`, a line per beat; on the cover
it is set from frame 0 and only the kicker and lead arrive.
- Code types on two lines per beat, each wiping in over 0.2s `none`, diff lines
taking their gutter mark as they land; the panel is already in place.
- A number-lockup counts up to the brief's value in 0.8s `power2.out`; the unit holds.
- The coral section-rule is the only draw-on, 0.5s `power1.inOut`; the ✱ fades in
and scales 0.92→1 in 0.4s `power2.out` on one emphasis beat per scene.
- Chapters cross-dissolve in 0.3s on the downbeat, cream to navy included.
- Never: overshoot, bounce, elastic or `back` eases, a spinning ✱, glow, tilt.
## Do and don't
- Do stand frames on cream, gather content on a tile half-step, lead with one focal.
- Do set EB Garamond 400 display, Inter 400 body, mono kickers (opened by ✱) and code.
- Do ration coral to one moment a frame; lift with a hairline and one soft shadow at
most; keep navy for code and gravity grounds; pair figures with a mono unit.
- Don't use pure white, cool gray, pure black or a fourth hue; keep syntax colors in
the panel; no heavy shadow, glow, gradient, tilt or square corner.
- Don't set a sans headline, a serif label or unit, or coral headline or body text.
- Copy keeps its approved casing, display included; kickers and tags get their look
from mono or Inter 500 and 0.16–0.18em tracking (CJK has no case).
## 9:16 and 1:1
- Cover: display on top, index below on 9:16; index in a corner on 1:1.
- Statement, quote and number: stacked, taller on 9:16; centered on 1:1.
- Code surface: taller with fewer lines on 9:16, square and centered on 1:1; the
hairline and mono chrome stay. Closing: avatars wrap below, never shrink.
## Self-check before snapshot
- One EB Garamond moment dominates; coral exactly once; navy only for code or gravity.
- 1px hairlines, one soft shadow at most, 6/8/12px radii; every figure has a mono unit.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out. Diffs,
file counts and branches come from the real change; avatars only from supplied images.
@@ -0,0 +1,134 @@
# coral — hard-edged magazine poster
Adapted from the HyperFrames frame preset `coral` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants graphic, structural confidence: solid planes of color
meeting at hard edges and tall condensed type as architecture, like a sports-magazine
cover or a Saul Bass travel poster. Bold launches, campaign and event films.
Failure looks like a deck of soft cards with drop shadows and rounded corners.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
coral: "#E85D5D" # accent and environment: full regions, borders, icon squares, nodes
coral-dark: "#D44A4A" # accent deep: start of the one 135° feature gradient
cream: "#F5F0E8" # canvas: the warm field
cream-dark: "#E8E0D4" # surface: info-bar band
black: "#1A1A1A" # ink and deep: headlines, the ink panel, the timeline line
gray: "#6B6B6B" # muted ink: body and meta only, never a headline
light-gray: "#B0B0B0" # line: faint meta and inactive marks
white: "#FFFFFF" # surface: card and sidebar-item fill; never type on coral
type: # px on 1920×1080
giant-mark: { face: "Bebas Neue" > condensed, size: 269, weight: 400, color: "rgba(0,0,0,0.35)" }
background-numeral: { face: "Bebas Neue" > condensed, size: 192, weight: 400, color: "rgba(0,0,0,0.12)" }
jumbo-feature: { face: "Bebas Neue" > condensed, size: 173, weight: 400, line: 1.0, tracking: 12px }
hero-title: { face: "Bebas Neue" > condensed, size: 125, weight: 400, line: 0.9, tracking: 4px }
stat-numeral: { face: "Bebas Neue" > condensed, size: 96, weight: 400, line: 1.0 }
section-headline: { face: "Bebas Neue" > condensed, size: 81, weight: 400, line: 1.0, tracking: 2px }
column-title: { face: "Bebas Neue" > condensed, size: 71, weight: 400, line: 1.0, tracking: 2px }
card-stat: { face: "Bebas Neue" > condensed, size: 48, weight: 400, line: 1.0 }
bar-title: { face: "Bebas Neue" > condensed, size: 44, weight: 400, line: 1.0, tracking: 2px }
sidebar-value: { face: "Bebas Neue" > condensed, size: 44, weight: 400, line: 1.0 }
card-title: { face: "Bebas Neue" > condensed, size: 36, weight: 400, line: 1.1, tracking: 1px }
body-light: { face: "Inter" > sans, size: 36, weight: 300, line: 1.5 } # pull quote, support line
body: { face: "Inter" > sans, size: 28, weight: 400, line: 1.7 }
section-label: { face: "Inter" > sans, size: 22, weight: 700, tracking: 4px }
item-label: { face: "Inter" > sans, size: 22, weight: 700, tracking: 3px }
quote-attribution: { face: "Inter" > sans, size: 22, weight: 600, tracking: 3px }
quote-role: { face: "Inter" > sans, size: 22, weight: 400, tracking: 1px }
cjk: { display: hei 600, body: hei 400 }
space: { pad-x: 96, pad-y: 80, col: 56, gap: 32, card: 40 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`, otherwise the
stack after `>`. Bebas Neue stays 400. Display roles never take the sans, body never the
condensed face. Copy keeps its approved casing; height, condensed width and the 1–12px
tracking carry the poster, and CJK has no case.
## Frame craft
- Squint: one element dominates at 3–6× its neighbor: a hero-title, a jumbo-feature or a
wallpaper numeral behind a region's title. Never two rival headlines.
- Silence: regions stay 40–55% empty; the catalog is the one dense frame. A thin coral
region gets a wallpaper numeral, not more copy.
- Restraint: coral is an accent or one full region per frame, never both at full
strength. One giant mark per quote. Type on coral is ink, never white.
## Components
- **region-split**: two or three solid surfaces at a hard edge, in rows 38/62 or columns
40/60 or 50/50, 0 radius. The join is the layout; no gradient, no rounded seam.
- **diagonal-hatch**: `repeating-linear-gradient(45deg, transparent 0 20px, rgba(0,0,0,.06)
20px 40px)` on every coral region; variants −45° at 30/60px, vertical at 60/62px in 10%.
- **background-numeral**: 192px in 12% ink behind a coral region's title. **giant-mark**:
a 269px quote mark or glyph in 35% ink inside a coral region. Wallpaper, not content.
- **card**: white, a 5px coral top border as its only chrome, 0 radius, no shadow;
holds a **card-icon** (48px coral square, one white glyph), card-title, body, card-stat.
- **sidebar-item**: white with a 4px coral left border; sidebar-value over a label.
- **accent-line**: a 77×5px coral rule under a headline; 60×4px on closing frames.
- **timeline**: a 4px ink line with a dashed tail; 20px coral nodes on cream halos.
- **info-bar**: a cream-dark band under a feature region, bar-title left, meta right.
## Frame types
1. **Region-split cover** — 38/62 rows: a hatched coral band with a wallpaper numeral,
brand left and meta right, over cream. A two-line ink hero-title left in the cream
field, its second line coral; the field about 45% empty. Readable at frame 0.
2. **Feature stat** — full hatched coral. An ink stat-numeral, jumbo-feature or two-line
headline on the left, a wallpaper numeral behind, an ink label above, an optional
body-light line within 845px. About 40% open.
3. **Quote layout** — 40/60: a hatched coral panel with a giant-mark beside a black panel
holding a 2–3 line body-light quote in cream; a 60×4 accent-line over the attribution.
4. **Closing plate** — cream over a hatched coral foot band. A two-line ink sign-off,
centered between an eyebrow and an accent-line. About 55% empty.
5. **Three-column catalog** — cream or black, 96px sides. A centered section-headline
over three cards. The dense frame; coral tops and icons are its only accent.
6. **Timeline** — cream. A section-headline over the timeline, 4–5 nodes with condensed
labels. Standard density.
## Motion
- Verbs: regions slam in, lines punch up, rules extend, numerals drift, nodes stamp on.
- A coral region wipes in from its own edge in 0.3–0.4s `expo.out`; the edge stays sharp.
- Condensed lines punch up through a baseline mask, 0.3s `power4.out`, one per beat
(0.5s at 120 BPM). Stat numerals cut on whole, on the beat.
- Wallpaper numerals drift 2–3% over the scene, `sine.inOut`; the hatch slides one 40px
period, `none`.
- Rules and card tops extend from the left in 0.25s `power3.out`, cards 0.12s apart. The
timeline draws in 0.8s `power2.inOut`, nodes stamping on (scale 0→1, 0.2s) as it passes.
- Chapters hard-cut on the downbeat; a split may trade sides across the cut. The cover's
split, band and hero-title stand at frame 0; only meta and texture move.
- Never: crossfades, feathered wipes, shadows, rotation, bounce.
## Do and don't
- Do compose with two or three of coral, ink and cream meeting at hard edges.
- Do let tracking carry the display (2px standard, 4px hero, 12px jumbo), not casing.
- Do set eyebrows coral on cream or ink, ink on coral; headlines ink on cream or coral,
cream on ink.
- Do hatch every coral region; fill a thin one with a wallpaper numeral or giant mark.
- Do center sign-offs and catalog heads; set features and quotes left or in a split.
- Don't add a fourth surface, shadows or rounded rectangles; circles are timeline nodes.
- Don't set white headlines on coral or gray headlines anywhere; gray is body and meta.
- Don't soften an edge with a gradient (one 135° coral-dark → coral feature region is
the exception); don't run a headline edge to edge.
## 9:16 and 1:1
- Cover: a taller band with the title below on 9:16; 40/60, title lower, on 1:1.
- Feature stat: figure on top, numeral behind on 9:16; centered on 1:1.
- Quote stacks on both, mark panel over quote. Closer: taller band on 9:16, centered on 1:1.
- Catalog: three stacked on 9:16, 2+1 on 1:1. Timeline: vertical on 9:16, compact on 1:1.
- Condensed display sets about 20% wider in CJK; re-break lines for each ratio.
## Self-check before snapshot
- One focal per region at 3–5×; sparse frames 40–55% empty, only the catalog dense.
- Two or three surfaces at hard edges, hatch on every coral region, ink on coral.
- No shadow, no rounded rectangle; anchors vary, never three alike in a row.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out.
@@ -0,0 +1,134 @@
# creative-mode — neo-brutalist editorial poster
Adapted from the HyperFrames frame preset `creative-mode` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants editorial presence and geometric confidence: sparse,
graphic, punchy but restrained, like a Risograph poster or a punk-zine spread. Flat
ink-bordered blocks, one big claim, a lot of cream. Indie tools, developer products.
Failure looks like a rounded, soft-shadowed SaaS feature grid.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
cream: "#EFE9D9" # canvas: the ground on nearly every frame
cream-2: "#E4DCC4" # surface: table fill, block-on-block contrast
ink: "#0F0F0F" # ink: every border, rule and line of type on cream
ink-2: "#2A2A2A" # deep variant: secondary dark fills
green: "#1F8A4C" # accent: cell and step fills; the closing-plate ground, once
green-dark: "#136636" # green's deep variant
pink: "#F06CA8" # accent: marker block, stamp, winning column
pink-dark: "#D14E8B" # pink's deep variant
orange: "#E85A1F" # accent: fills, and the hard offset shadow
yellow: "#F5C518" # accent: rotated badge, decorative circle
type: # px on 1920×1080; Archivo Black is one black cut at 400
display-hero: { face: "Archivo Black" > sans, size: 298, weight: 400, line: 0.84, tracking: -0.02em }
display-xl: { face: "Archivo Black" > sans, size: 211, weight: 400, line: 0.9, tracking: -0.01em }
display-lg: { face: "Archivo Black" > sans, size: 154, weight: 400, line: 0.9, tracking: -0.01em }
step-num: { face: "Archivo Black" > sans, size: 134, weight: 400, line: 0.85 }
stat-num: { face: "Archivo Black" > sans, size: 123, weight: 400, line: 0.88 }
display-head: { face: "Archivo Black" > sans, size: 81, weight: 400, line: 0.92, tracking: -0.01em }
marker: { face: "Archivo Black" > sans, size: 46, weight: 400, line: 1.0 }
stamp-num: { face: "Archivo Black" > sans, size: 42, weight: 400, line: 0.9 }
step-title: { face: "Archivo Black" > sans, size: 34, weight: 400, line: 1.0 }
badge-label: { face: "Archivo Black" > sans, size: 28, weight: 400 }
table-head: { face: "Archivo Black" > sans, size: 28, weight: 400 }
body-lg: { face: "Space Grotesk" > sans, size: 32, weight: 400, line: 1.4 }
body-md: { face: "Space Grotesk" > sans, size: 28, weight: 400, line: 1.3 }
mono-kicker: { face: "JetBrains Mono" > mono, size: 24, weight: 400, tracking: 0.14em }
mono-label: { face: "JetBrains Mono" > mono, size: 24, weight: 400, tracking: 0.06em }
cjk: { display: hei 600, body: hei 400 }
space: { chrome: 64, gutter: 96, gap: 28, cell: 32, border: 4, rule: 3, shadow: 24 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`, otherwise the
stack after `>`; on the `sans` stack set the Archivo Black roles at 800 so the mass holds.
Copy keeps its approved casing: black weight, the 0.84–0.92 line and tight tracking carry
the poster, and CJK has no case. Hanzi display lines need a line height of 1.05 or more.
## Frame craft
- Squint: exactly one element dominates, 3–6× its nearest neighbor, a chasm rather than
a ramp: the display-hero or display-xl claim, or a stat-num.
- Silence: cover, claim and closer stay 45–60% empty; only the stat grid and the ledger
run dense. Never pad a sparse frame to make it look finished.
- Restraint: per frame, at most one hard offset shadow and two or three accents, never
all four. Green as a ground appears once, on the closing plate.
## Components
- **frame-chrome**: a mono topbar 48px from the top (label left, a 999px pill with a 2px
ink stroke right) and a footer 48px from the bottom (descriptor left, `NN • NN` counter
right, a 10px ink dot between), inset 64px; dropped only against a full-bleed wordmark.
- **stat-cell**: a flat green, pink, orange or cream tile, 4px ink border, square, 32px
padding, stat-num over a mono-label; cream type on accents, ink on cream. No shadow.
- **step-card**: the same 4px square card with a giant step-num, step-title and body-md;
fills alternate cream with pink, yellow and green, and the run ends on green.
- **marker-block**: pink, ink marker type, 4px ink border, and the signature shadow
`24px 24px 0 orange, 24px 24px 0 4px ink`. Diagram stacks may use an 18px ink offset.
- **kicker-block**: cream mono-kicker on an ink chip, padding 10px 18px, square.
- **badge-rotated**: yellow, 4px ink border, badge-label type, turned −4°.
- **pill-badge**: cream, 2px ink border, 999px radius, mono-label; the one rounded box.
- **stamp**: a pink square about 346px, 4px cream border, a cream circular inner ring
around stamp-num, turned −6°. The closing seal.
- **comparison-table**: cream-2, 4px ink outer border, 3px ink inner rules, an ink head
row with cream table-head labels; one pink or green column fill marks the winner.
- **decorative-circle**: a yellow disc, 4px ink border, against a green panel or off a corner.
## Frame types
1. **Wordmark cover** — cream. A two-line display-hero wordmark centered, its second line
pink or orange; full chrome with a `01 • NN` footer; one decorative-circle may bleed
off a corner. About 55% empty; readable at frame 0.
2. **Big claim** — a full-bleed pink or orange ground (green stays with the closer). A
2–3 line ink claim at display-xl or display-lg, left and vertically centered, a
kicker-block above. No second accent; about 45% open.
3. **Stat grid** — cream. A centered display-head over green, pink and orange stat-cells,
the one place three accents sit together. About 25% open; the dense frame.
4. **Closing plate** — green, its only use as a ground. A two-line cream display-lg
sign-off centered, cream chrome, one pink stamp in a corner. About 55% empty.
5. **Featured marker** — cream. The marker-block and its shadow off-center, an optional
body-md line beside it. About 50% open.
6. **Comparison ledger** — cream, 96px gutter. The full-width table with one filled
winning column; the second dense frame.
## Motion
- Verbs: blocks slam, shadows snap out, stamps thump, cells cut on, chrome ticks over.
- Blocks slam in from 40–60px off their rest in 0.25–0.35s `power4.out`; no overshoot, no fade.
- The marker-block lands flat, then its shadow snaps out to 24px in 0.2s `power3.out` on
the next beat (one beat = 0.5s at 120 BPM); on an accent beat it may press into it.
- Stat cells and step cards cut on one per beat, left to right; figures arrive whole.
- Badge and stamp thump from scale 1.3 to 1 in 0.25s `power4.out`, already at −4°/−6°.
- Chapters and ground swaps hard-cut on the downbeat. The cover wordmark is set at
frame 0; chrome, pill and circle arrive within 0.6s.
- Never: blur, crossfades, soft eases on a slam, elastic wobble, two shadows moving.
## Do and don't
- Do build each frame on one idea, its focal element 3–5× the neighbors.
- Do center cover, stat grid and closer; anchor claim, marker and ledger left or off-center.
- Do keep sparse frames 45–60% empty with two or three accents; green waits for the close.
- Do spend the hard shadow on one block per frame; size Archivo Black to its measure.
- Don't round any corner but the pill's; no gradient, blur or glow.
- Don't track Archivo Black wider than −0.01em; no fifth accent, no pure white.
- Don't center body copy or set labels outside mono; don't run a headline edge to edge.
## 9:16 and 1:1
- Cover: stacked taller, circle on top on 9:16; centered and tighter on 1:1.
- Claim: at the top of the full accent on 9:16, centered on 1:1. Closer: stamp below on 9:16.
- Stat grid: three stacked on 9:16; 2×2 on 1:1, a fourth cell only for a fourth figure.
- Marker: centered, the shadow falling below on 9:16. Ledger: fewer columns, two on 1:1.
- Chrome keeps its 64px inset on the short edge in every ratio.
## Self-check before snapshot
- One element dominates at 3–5×; sparse frames 45–60% empty, only grid and ledger dense.
- Two or three accents per frame, never four; a green ground only on the closer.
- No blur, one hard shadow at most, square corners but the pill, rotation only −4°/−6°.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out.
@@ -0,0 +1,133 @@
# daisy-days — sticker-sheet picture book
Adapted from the HyperFrames frame preset `daisy-days` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants warmth and whimsy: playful, childlike, sticker-sheet
kawaii, like a children's picture book. Kids' and family products, learning apps,
playful consumer brands, seasonal campaigns.
Failure looks like a flat corporate slide: square corners, blurred shadows, bare corners.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
cream: "#F5F0E6" # canvas: the default ground
turquoise: "#7ECDC0" # surface: saturated ground (cover), marker fill
soft-pink: "#F7C8D4" # surface: quote ground, quote-mark fill
butter: "#FDE68A" # surface: badge-pill, bullet dots
mint: "#A8E6CF" # surface: marker fill, ground
lavender: "#D4A5E8" # surface: closing ground, marker fill
peach: "#FFCBA4" # surface: process ground
sky: "#A8D8F0" # surface: marker fill, ground
coral: "#F8635F" # accent: small markers only, never a ground
text-dark: "#2D2D2D" # ink: every outline and shadow, headlines on cream
text-muted: "#6B6B6B" # muted ink: meta on cream or white only
white: "#FFFFFF" # surface: card fill; headline type on saturated grounds
type: # px on 1920×1080
display: { face: "Fredoka One" > sans, size: 125, line: 1.1, tracking: 0.02em }
headline: { face: "Fredoka One" > sans, size: 86, line: 1.1, tracking: 0.02em }
title: { face: "Fredoka One" > sans, size: 58, line: 1.15, tracking: 0.02em }
quote: { face: "Fredoka One" > sans, size: 50, line: 1.35 }
subtitle: { face: "Fredoka One" > sans, size: 35, line: 1.2, tracking: 0.02em }
label-display: { face: "Fredoka One" > sans, size: 25, line: 1.3, tracking: 0.02em }
badge: { face: "Fredoka One" > sans, size: 22, tracking: 0.02em }
body: { face: "Quicksand" > sans, size: 32, weight: 500, line: 1.6 }
body-strong: { face: "Quicksand" > sans, size: 32, weight: 600, line: 1.5 }
meta: { face: "Quicksand" > sans, size: 24, weight: 600, line: 1.45 }
cjk: { display: hei 600, body: hei 500 }
line: { primary: 3px text-dark, thin: 2px text-dark, ornament: 2.1px text-dark }
shadow: { default: "6px 6px 0 text-dark", small: "4px 4px 0 text-dark",
headline: "3px 3px 0 text-dark", headline-soft: "3px 3px 0 rgba(0,0,0,0.2)" }
space: { edge: 56, radius: 20, radius-lg: 28, radius-pill: 50, radius-round: 50% }
```
Faces: use the named face when installed or vendored in `assets/fonts/`, otherwise the
stack after `>`. Fredoka One has one weight; Fredoka at 600 is its twin, so vendor that
when it is missing. On the bare `sans` stack set display at 700 and check the snapshot
frame. Fredoka is never italic or underlined; Quicksand stays 500–700; roles never swap.
## Frame craft
- Squint: one Fredoka headline or one content card dominates at 3–6× its neighbor.
- Silence: one content container per frame, ringed by an ornament wreath; bare corners
read as broken. The info-card grid is the one dense frame.
- Restraint: cream or one pastel ground per frame; coral only on small markers; charcoal
for every border and shadow; no ninth color.
## Components
- **card**: white, 3px charcoal border, 20px radius (28px featured), 6px charcoal offset.
The universal container, white on any ground.
- **framed-header**: a pastel cap flush on a white body inside one continuous 3px border,
28px radius, one 6px shadow for the whole unit.
- **badge-pill**: butter, 3px border, 50px radius, badge type, 4px shadow, on one line.
- **circle-marker**: an outlined disc (3px, 50%) in any pastel with a Fredoka numeral,
white on most fills, charcoal on butter; bullet 20, icon 44, dot 48, step 90px, and
steps take the 4px shadow.
- **bullet-dot**: a 20px butter disc, 2px charcoal outline, 4px below the line's top.
Lists never use glyph bullets.
- **ornament**: hand-drawn SVG daisies, stars, suns, clouds and rainbows, 2.1px charcoal
stroke, 3–7 per frame at corners and edges, cropped by the frame, behind content.
- **quote-mark**: an oversized Fredoka quote glyph, soft-pink with a charcoal stroke.
- **text-headline-shadow**: a 3px charcoal offset under white headlines on saturated
grounds (20% soft variant on soft-pink and mint); on cream, headlines sit flat.
## Frame types
1. **Cover** — a saturated pastel such as turquoise. A 1–2 line white display with the
text shadow, centered under a butter badge-pill, a body line below, a counter pill;
daisies and stars crop off the corners. Full, never crowded; readable at frame 0.
2. **Info cards** — cream. A flat charcoal headline over three white cards, each a
circle-marker icon (turquoise, coral, lavender), title and body. The dense frame;
only a couple of ornaments.
3. **Process steps** — peach or another pastel. A white shadowed headline over 3–4 90px
step circles filled coral → mint → sky → lavender, joined by Fredoka arrows.
4. **Quote** — soft-pink. A white 28px card with a charcoal Fredoka quote under a big
quote-mark, a Quicksand 700 attribution; a star or two at the corners.
5. **Framed section** — cream. One framed-header: a pastel cap with a Fredoka title over
a white body with a butter-dot list, ornaments at the edges. Standard density.
6. **Closing** — a saturated pastel such as lavender. A badge-pill and a white shadowed
display sign-off, centered inside a full ornament wreath.
## Motion
- Verbs: stickers pop, cards drop onto their shadows, ornaments twirl, dots stamp, steps hop.
- Cards, badges and markers pop from scale 0.6 to 1 in 0.35–0.45s `back.out(1.7)`; the
shadow travels with its shape.
- Ornaments pop 0.08s apart with `back.out(2)`, then daisies turn 6–10° over the scene and
stars pulse 1→1.08 on beats, `sine.inOut` (one beat = 0.5s at 120 BPM).
- List lines rise 24px over 0.4s `power2.out`, one per beat, each bullet-dot stamping in.
- Step circles hop in one per beat, 0.4s `back.out(2)`; each arrow draws in 0.3s between.
- Chapters cut on the downbeat with a ground swap, or a 0.5s `power2.inOut` pastel wipe.
- Cover and closing headlines and badges are on screen at frame 0; only ornaments move.
- Never: blur, whole-frame fades, linear slides, softening shadows, spinning type.
## Do and don't
- Do set every headline in Fredoka and every body line in Quicksand.
- Do outline every shape in 3px charcoal with a hard 6px or 4px offset; cards stay white.
- Do give headlines on saturated grounds white type and the text shadow; flat on cream.
- Do use butter-disc bullets and a 3–7 ornament wreath cropping at the corners.
- Do keep one container per frame; rotate markers coral → mint → sky → lavender → butter.
- Do center most frames; cream for dense ones, saturated for cover, quote and closer.
- Don't use square corners, blurred or rgba shadows (bar the soft text shadow), colored
borders, a coral ground, a ninth color or a third font.
- Don't use glyph bullets, bare corners, two competing panels or edge-to-edge headlines.
## 9:16 and 1:1
- Cover, quote and closing: centered on both; taller on 9:16 with 5–7 ornaments.
- Info cards: stacked on 9:16, 2+1 on 1:1. Steps: vertical, arrows down on 9:16; 2×2 on 1:1.
- Framed section: cap and body, the body growing taller on 9:16.
- Keep the 56px edge on the short side; tight ratios lean to 5–7 ornaments.
## Self-check before snapshot
- One headline or card dominates; one container in its wreath; only info cards dense.
- Cream or one pastel ground; coral markers only; text-muted only on cream or white.
- 3px outline and hard offset on every raised shape, no blur; 3–7 ornaments; disc bullets.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out.
@@ -0,0 +1,134 @@
# editorial-forest — quiet literary editorial
Adapted from the HyperFrames frame preset `editorial-forest` (heygen-com/hyperframes,
Apache-2.0, see `LICENSE-hyperframes.txt`). Changed for Orkas VideoStudio: local
font stacks with CJK faces, px sizes with the 22px floor, approved-copy casing,
and a motion section.
Pick it when the brand wants quiet confidence and a literary tone: spacious, restrained,
serif-led, like a Penguin classic, a calm annual report or an art-book spread. Reports,
research and publishing brands, considered launches, essays and founder letters.
Failure looks like a multicolor web dashboard with drop shadows.
## Tokens
```yaml
colors: # keep the roles; remap by role (see design-presets.md)
green: "#2e4a2a" # deep: cover, stat and chart ground; headline ink on cream
green-deep: "#243a21" # deep variant: type and rules on pink
green-lite: "#3a5a36" # surface: tile fill
pink: "#e89cb1" # accent: display on green, rules and monogram on green
pink-deep: "#d27e96" # accent deep variant
cream: "#efe7d4" # canvas: the content ground
cream-2: "#e6dcc4" # surface: bordered tile fill
ink: "#1a1a17" # ink: body on cream
type: # px on 1920×1080
display-hero: { face: "Source Serif 4" > serif-text, size: 221, weight: 500, line: 0.92, tracking: -0.02em }
stat-figure: { face: "Source Serif 4" > serif-text, size: 221, weight: 500, line: 0.92, tracking: -0.03em }
display: { face: "Source Serif 4" > serif-text, size: 140, weight: 500, line: 1.02, tracking: -0.02em }
stat-figure-unit: { face: "Source Serif 4" > serif-text, size: 109, weight: 500, line: 0.92 }
headline-xl: { face: "Source Serif 4" > serif-text, size: 96, weight: 500, line: 0.96, tracking: -0.02em }
headline: { face: "Source Serif 4" > serif-text, size: 84, weight: 500, line: 1.0, tracking: -0.02em }
title-card: { face: "Source Serif 4" > serif-text, size: 67, weight: 500, line: 0.96, tracking: -0.01em }
title-card-sm: { face: "Source Serif 4" > serif-text, size: 56, weight: 500, line: 0.98, tracking: -0.01em }
name: { face: "Source Serif 4" > serif-text, size: 44, weight: 600, line: 1.0 }
lede: { face: "Source Serif 4" > serif-text, size: 36, weight: 400, line: 1.38 }
body: { face: "Source Serif 4" > serif-text, size: 30, weight: 400, line: 1.38 }
body-card: { face: "Source Serif 4" > serif-text, size: 28, weight: 400, line: 1.34 }
label: { face: "JetBrains Mono" > mono, size: 26, weight: 500, tracking: 0.18em }
caption-mono: { face: "JetBrains Mono" > mono, size: 24, weight: 500, tracking: 0.14em }
cjk: { display: song 500, body: song 400, chrome: hei 500 }
space: { edge: 96, rule: 2, rule-card: 2.5, radius-card: 6, radius-step: 8 }
```
Faces: use the named face when installed or vendored in `assets/fonts/`, otherwise the
stack after `>`. Vendor Source Serif 4 with its optical-size axis (`font-optical-sizing:
auto`) or display and body flatten into one cut. Display 500, body 400, name 600; on
`serif-text` 500 falls to regular, never to bold. Mono chrome keeps the copy's approved
casing; weight 500 and the 0.14–0.18em tracking carry the labels, and CJK has no case.
## Frame craft
- Squint: one serif moment dominates at 3–6× its neighbor; the surface block draws the eye.
- Silence: one subject per frame in deep negative space; only the topic-tile and step
grids run dense.
- Restraint: two surface tones per frame at most, three reads loud. Serif 500 for
display, 400 for body; the green ground is kept for moments of weight.
## Components
- **topbar**: a mono label left, a monogram-circle or ordinal counter right, along the top
inside the 96px edge. On every frame; without it a frame looks untreated.
- **footline**: two caption-mono strings at opposite ends of the bottom edge; on cover,
data and summary frames.
- **monogram-circle**: 130px, 2px pink border, a mono monogram inside. The identity
stamp, cover and summary only, and the only full round.
- **topic-tile**: 6px radius, no shadow; caption-mono ordinal, title-card-sm title, mono
foot. Fills rotate green with pink type, pink with green-deep, green-lite with pink,
cream-2 with a 2px green border and green type; no fill repeats in a grid.
- **step-tile**: 8px radius, 2.5px border; cream with a green border, green, or pink; a
mono ordinal, title-card title, body-card copy, a mono marker over a top rule.
- **kpi-block**: caption-mono tag, stat-figure with its unit, a body line, under a 2px rule.
- **meta-dl**: a three-column term-and-value grid under a 2px green rule; mono terms.
- **bar**: 56px wide in pink, cream or green, 3px 3px 0 0 corners, a mono value above.
- **rule-thin**: 2px in the region's accent (green on cream, pink on green, green-deep on
pink). The only separator; never 1px, never 3px or more.
## Frame types
1. **Cover** — green. A two-line pink display-hero, left, one word in cream; a lede, the
topbar with label and monogram-circle, a footline. About 50% open; readable at frame 0.
2. **Topic tiles** — cream. A headline-xl over three or four topic tiles in three of the
four fills, none repeated. The dense frame.
3. **KPI stat** — green. A kpi-block with a 221px pink stat-figure and unit over a 2px
pink rule; topbar and footline. About 55% open.
4. **Statement** — cream, generous edge. A two-line green display, left, the 600 name and
a caption-mono role beneath. No accent; the serif carries it. About 55% open.
5. **Step framework** — cream or green. A headline over three or four step-tiles with
67px titles, fills rotating through the three variants. Standard density.
6. **Chart** — green. A headline over 56px pink, cream and green bars on 2px axis rules
with mono ticks, an optional meta-dl below. Standard density.
## Motion
- Verbs: lines rise through masks, rules draw, tiles settle, bars grow, the ring draws.
- Display and headline lines reveal by a baseline mask, 0.7–0.9s `power3.out`, one line
per beat (0.5s at 120 BPM).
- Rules draw from their start edge in 0.6s `power2.inOut`; tiles follow 0.15s apart,
rising 16px as they fade in over 0.5s `power2.out`.
- Bars grow from the axis in 0.8s `power2.out`, 0.1s apart, values fading in as each
lands; the stat-figure reveals whole by mask, never counting.
- The ground holds still; a display may drift 1% over the scene, `sine.inOut`.
- Chapters cut on the downbeat with a ground swap or a 0.6s `power2.inOut` mask wipe.
- The cover's display-hero and topbar are set at frame 0; lede and footline rise in over
0.8s, and the monogram ring draws in 0.9s.
- Never: bounce, scale pops, shadows, glow, rotation, fast zooms on type.
## Do and don't
- Do run display in Source Serif 4 at 500, tight and negatively tracked; body at 400.
- Do set labels, captions, ticks and footlines in mono 500 at 0.08–0.18em.
- Do give every frame a topbar, and cover and data frames a footline.
- Do keep one dominant surface, two tones at most, and rotate tile fills without repeats.
- Do set headlines green on cream, cream or pink on green, green-deep on pink (body ink,
cream, green-deep likewise); pink and green on each other only from 84px.
- Don't add shadow, gradient or glow; depth is color block, hairline, border against fill.
- Don't add a third face, italic, underline, serif body at 500 or mono at headline scale.
- Don't add a fourth color family, rgba surfaces, rules outside 2/2.5px, two competing
blocks or an edge-to-edge headline.
## 9:16 and 1:1
- Cover: display on top, monogram below on 9:16; monogram to a corner on 1:1.
- Tiles and steps: stacked on 9:16; 2×2 on 1:1.
- KPI stat and statement: centered (the figure taller, the quote stacked) on 9:16 and 1:1.
- Chart: taller bars, meta stacked on 9:16; a square chart on 1:1.
- The topbar spans the top on every ratio; the 96px edge holds on the short side.
## Self-check before snapshot
- One serif moment dominates; one subject per frame, only tile and step grids dense.
- Two surface tones at most, per-surface type colors; display 500, body 400, no italic.
- No shadow or gradient; 2px rules; 6/8px radii; topbar on every frame.
- Copy keeps its approved casing; nothing below 22px.
- Every figure, name and logo traces to the brief; otherwise leave it out.
+1 -1
View File
@@ -1 +1 @@
{"name":"@orkas/video-studio-skills","version":"2026.9.10","private":false,"files":["*/SKILL.md","*/references/*.md","*/scripts/**"]}
{"name":"@orkas/video-studio-skills","version":"2026.9.10","private":false,"files":["*/SKILL.md","*/references/**","*/scripts/**"]}
+2 -2
View File
@@ -14,7 +14,7 @@ Iterate segments in `order`. For each, produce its `produced_path` according to
- **edit** → `stage-edit`: `ovs edit trim` the `input_id` to `[in_sec, out_sec]` → `project/cuts/<id>.mp4`.
- **compose** → `stage-compose`: build a small visual-only manifest-owned composition for `spec.kind` (title card, lower-third, stat card, captions) under `project/compositions/<id>/` → run `ovs draft project/compositions/<id> --out project/parts/<id>.mp4 --quality draft --report project/reports/<id>-compose-report.json`. This keeps compose segments on the same manifest/source/check/video-QA path as standalone COMPOSE while still letting the assembler own narration and loudness.
- **generate** → `stage-generate` (+ `stage-consistency` for recurring characters): only AFTER gate C. `ovs video`/`ovs image` → `project/assets/<id>.mp4`. For `operation:"edit"`, pass the exact original reference video and obey top-level `references` plus `edit_strategy`; never widen it into regeneration. A failed/unknown paid attempt is not an automatic retry. Preserve completed siblings and require a new output path for any later authorized attempt.
- **provided** → use `spec.asset_id` as-is (probe it first; conform aspect/fps if needed).
- **provided** → use `spec.asset_id` as-is (probe first and use `ovs edit reframe` when its aspect differs from the signed canvas; concat handles differing frame rates).
Billable `generate` segments must not run before gate C has confirmed the count from `cost_estimate`. Produce cheap/free segments (edit, compose, provided) freely.
@@ -22,7 +22,7 @@ Billable `generate` segments must not run before gate C has confirmed the count
Assemble deterministically, bottom-up. This tiered order is the default; it is predictable and cheap, and keeps each clip's real audio intact:
1. **Primary track** — `ovs edit concat` the primary-layer `produced_path`s in `order` → `project/render/primary.mp4`. Conform aspect/fps on the way in if sources differ; read the returned conformance report and verify it still matches the approved canvas.
1. **Primary track** — `ovs edit concat` the primary-layer `produced_path`s in `order` → `project/render/primary.mp4`. Conform aspect/fps on the way in if sources differ; read the returned conformance report and verify it still matches the approved canvas. If `audio_conform` reports padded inputs, spot-check that clips with sound remain audible after the silent parts.
2. **Overlays / bg** — Do not place a full-frame opaque composed video over source footage: it erases the base. Use genuinely transparent or bounded overlays, or amend the plan to make it a primary beat. Do not bypass the opaque-overlay refusal with resizing tricks. for each overlay/bg segment, `ovs edit overlay` its part onto the primary over the window of the segment named in `over` (title cards, lower-thirds, logos). Composed layers are VISUAL-ONLY — they must not carry their own narration audio. **This includes a compose segment that IS the primary track (a full-video composition): render it SILENT — do not put a narration `<audio>` in its `index.html`. The assembler owns narration (tier 3), so a composition that bakes it in would mean narration is added TWICE (the "two voices" defect).**
3. **Narration — added EXACTLY ONCE, here.** If active, require the Gate-B-signed `tracks.narration.synthesis` profile. Run `ovs narration fit` for each timed line before `ovs speak`; shorten over-budget text in the plan without changing the approved meaning. Synthesize with the exact signed voice/model/format/speed, probe the result, rerun measured fit, and write each line's `produced_path`. If measured timing misses, revise once using the suggested unit budget rather than repeatedly billing or forcing speed. Add all produced lines in ONE `ovs edit mix` call at their `start_sec`. The default existing-audio rejection catches compose segments that accidentally baked narration; re-render those SILENT. Read `voicedRatio`, `interiorGaps`, `maxOverlapSec` and status: the last line reaching the end does not mean the whole track carries speech. Fix unintended dead air and collisions, and disclose any intentional silent tail at Gate D. Preserve a generated talking head's built-in lip-synced audio instead of adding a second voice.
4. **Music** — add `tracks.music` ducked under narration by the planned amount.
+2
View File
@@ -29,6 +29,8 @@ After Gate B approves the canonical composition manifest, keep the production tu
The default path is **model-authored HTML -> draft**. Do not write or compile `spec.json`; fixed template compilation is not part of the COMPOSE path because visual quality and extensibility come first.
For four or more scenes, plan each scene’s source, layout, and timed beats in the existing manifest art direction before authoring. Write the scene section and timeline from its own beats. After each batch of two or three scenes, use `ovs snapshot PROJECT --scenes intro,body --out project/preview/batch.png` to inspect their start, middle, and end. Batch frames are authoring evidence. Use `ovs check` and a full snapshot without `--scenes` before opening the preview.
## HTML Preview Gate
Use the HTML Preview Gate to avoid expensive mp4 rerenders when visual rework is likely. It is a cost-control gate, not a new creative milestone, and it is only for the COMPOSE line. Decide from expected rework cost:
@@ -34,6 +34,15 @@ Write one `composition-manifest.json` v2 before HTML. It is the source of truth
If an approved beat is intentionally merged, add `source_alignment.merge_reason` or per-scene `source_shots`. Standalone narrated schema v2 manifests also require the exact Gate B-signed `audio.narration_intent` and a composition-owned narration track. AUTO child compositions use `audio.owner: "assembler"` and no tracks.
If the brief asks for music, choose a permitted recorded track during plan review.
For a standalone composition, keep the file under `assets/music/`. Name the track,
author, license, and attribution in the plan; keep the rights record outside
the composition. If there is no usable track yet, say so and keep working.
A standalone video without narration declares its music as a composition-owned
track. With narration, add it once the spoken timing is known (see
[narration](narration.md)). AUTO children remain silent; put their music in the
parent EDL's `tracks.music`.
A scene `source_shots` entry may be an approved `shot.id` or a source alias uniquely owned by one shot through `shot.source_shots`; QA canonicalizes those representations. Unknown or multiply-owned aliases are real mapping errors.
## Composition Contract (The Minimum That Renders)
@@ -18,5 +18,5 @@ To give a STANDALONE explainer a voiceover: synthesize the narration to an audio
- If an exact fixed duration was an explicit user constraint and the measured audio cannot be accommodated, stop and ask before another paid synthesis. Otherwise prefer the measured audio duration. This rule is for standalone COMPOSE; EDIT/AUTO may have supplied footage as the master timeline.
- If `ovs speak` fails, never silently continue: tell the user, then either fix and retry the narration or explicitly proceed silent with that stated at the gate.
- Use a project path such as `project/composition/assets/narration.mp3` so the composition stays self-contained.
- For background music plus voiceover, use two `<audio>` tracks with different `data-track-index` and lower the music `data-volume` (e.g. 0.2).
- After narration timing is known, add the music selected at plan review as a separate, lower-volume manifest track, then run `ovs composition reconcile` to update the generated audio markup.
- **Talking-head caveat:** when this composition is being overlaid onto AI-generated talking-head footage that already has lip-synced built-in speech, do NOT add a narration `<audio>` track.
+2
View File
@@ -7,6 +7,8 @@ description: Intelligent editing of real user-supplied footage—understand it w
How to intelligently edit **real user-supplied footage** while keeping source and decisions auditable. Deterministic operations handle cuts, joins, captions, overlays, reframes, and audio. When the request changes pixels semantically—remove an object, alter a background, relight, or make another content-aware local change—keep the EDIT route and execute only that bounded segment as a signed `ovs video --operation edit` call after paid-generation approval.
For a different delivery aspect, use `ovs edit reframe INPUT --aspect 9:16 --out OUTPUT` (MCP `edit_reframe`). The default `pad` preserves the whole frame; `--mode blur` fills the bars with a blurred copy. Use `--mode crop` only when the frame can lose its edges, set `--focus-x` / `--focus-y` from 0 to 1 if needed, and inspect a frame for clipped faces or copy. Supply both `--width` and `--height` for an exact canvas. Keep the source clip.
**Subtitle safety hard rule.** Burn captions only through `ovs edit burnsubs`; do not hand-write ffmpeg subtitle, `drawtext`, or PNG-overlay fallback commands. If `burnsubs` fails because the runtime ffmpeg lacks subtitle filter support, stop and report the blocker instead of improvising a custom ffmpeg graph.
**If the task is to FIND / SELECT / REDUCE / CLEAN rather than run a known timecode edit** — remove dead air, drop fillers, pick highlights, cut a long recording down — read `stage-decide` first: it covers understanding the footage and producing an evidence-bearing rough cut (the deterministic auto-cuts `ovs edit trim-silence` / `remove-fillers`, plus `ovs scenes` / `ovs quality` / `ovs plan rank-takes`). This skill is for executing cuts you have already chosen.
+8 -4
View File
@@ -48,7 +48,7 @@ Top-level `references` is mandatory for every edit/provided source and generatio
When OVS intelligently selects or transforms content, add `edit_strategy` with `{mode,objectives,decision_signals,preserve,may_change}`. Use `deterministic` for evidence-driven cuts, `semantic` for AI pixel edits, and `mixed` for both. A semantic video edit stays an EDIT/AUTO workflow but is a billable `source:"generate"` video segment with `operation:"edit"` and at least one declared original in `reference_video_paths` or `reference_video_urls`. It requires `semantic_model`, a matching top-level edit reference and temporal anchor, and inclusion in the paid-generation count.
`tracks` is required even when the project has no audio or captions; use `{}` for the empty case. Tracks are separate from the visual timeline. For narration, run `ovs speech-capabilities` and sign its executable route/model/voice/format together with the BCP-47 video language and speed under `tracks.narration.synthesis`; raw `voice` is legacy recovery only. Timed lines are `{text,start_sec,target_sec}`; `target_sec` is duration, never end time, and windows must not overlap and each receives a `produced_path`. Music is path + ducking. Captions stay editable DATA under `tracks.captions.lines`. Put the exact number of generate segments in `cost_estimate.billable_generations`; a mismatch is a validation error because paid-generation confirmation reviews that count.
`tracks` is required even when the project has no audio or captions; use `{}` for the empty case. Tracks are separate from the visual timeline. For narration, run `ovs speech-capabilities` and sign its executable route/model/voice/format together with the BCP-47 video language and speed under `tracks.narration.synthesis`; raw `voice` is legacy recovery only. Timed narration and caption lines use `{text,segment_id,start_sec,target_sec}`. Bind each to the segment it accompanies and count every earlier primary segment, including composed openers, in `start_sec`. It is a timestamp in the finished cut. `target_sec` is duration. Narration windows must not overlap, and each synthesized line receives a `produced_path`. Music is path + ducking. Captions stay editable DATA under `tracks.captions.lines`. Put the exact number of generate segments in `cost_estimate.billable_generations`; a mismatch is a validation error because paid-generation confirmation reviews that count.
Fit narration in the plan before any TTS call: use natural cadence (about 2.2-2.7 English words/sec or 4-5 Chinese chars/sec), shorten over-budget lines here, and do not rely on repeated synthesis to discover timing.
@@ -79,9 +79,9 @@ Fit narration in the plan before any TTS call: use natural cadence (about 2.2-2.
],
"tracks": {
"narration": { "synthesis": { "route_ref": "openai-compatible", "voice": "nova", "model": "tts-1", "format": "mp3", "language": "en-US", "speed": 1 },
"segments": [ { "text": "one line of narration", "start_sec": 0, "target_sec": 6 } ] },
"segments": [ { "text": "one line of narration", "segment_id": "s1_hook", "start_sec": 0, "target_sec": 6 } ] },
"music": { "path": "assets/bed.mp3", "duck": true },
"captions": { "style": "bold-bottom", "lines": [ { "text": "one caption line", "start_sec": 0, "target_sec": 3 } ] }
"captions": { "style": "bold-bottom", "lines": [ { "text": "one caption line", "segment_id": "s1_hook", "start_sec": 0, "target_sec": 3 } ] }
},
"cost_estimate": { "billable_generations": 0 }
}
@@ -100,9 +100,13 @@ Plan to the craft bar (`video-craft`): a hook in the first seconds, one idea per
## Step 4 — Validate, then gate B
1. `ovs plan validate` on `project/plan.json`. Fix EVERY error before going further — errors mean the plan cannot be executed or it breaks its own promise (e.g. `source_required` but no source segment). It also checks every generate video segment against the configured `video.provider` (MuAPI's Kling endpoints accept only `16:9`/`9:16`/`1:1` and 5 or 10 s); fix the plan or switch provider now rather than letting an approved generation fail. Reconsider warnings.
2. `ovs plan promise-check` on the PLAN, before producing anything. It computes the planned motion ratio vs. the promise — a fail means the plan is already a slideshow / breaks its promise. Fixing the plan now is free; re-assembling later is not. Rebalance durations or convert a static beat to footage until it passes (gate D re-checks against the real cut).
2. `ovs plan promise-check` on the PLAN, before producing anything. It computes the planned motion ratio vs. the promise — a fail means the plan is already a slideshow / breaks its promise. Fixing the plan now is free; re-assembling later is not. Rebalance durations or convert a static beat to footage and recheck (gate D re-checks against the real cut).
3. `ovs plan summarize` → present that timeline for **production plan confirmation**, including the exact narrator and generation settings. After the user's reply, run `ovs gate transition`; do not infer approval or request it again for an unchanged already-approved plan.
After two repairs that leave the same validation or delivery-promise problem,
preserve the current plan and explain the remaining problem and available
choices. Resume with a changed plan or a new direction from the user.
## Director judgment (end-to-end planning)
The craft of weaving ONE good video across sources, on top of the shared craft (`video-craft`). This is where a multi-source plan becomes a video instead of a tour of clips:
@@ -0,0 +1,19 @@
# Product sources
Use the brief and the actual product page or supplied media for the story.
Use any style reference for its visual language and timing.
Before proposing the direction, review the page that matches the brief. Use the
host's browser or available capture tool to save the relevant screens or product
photos locally under `assets/product/`. Prefer the page in the deliverable
language when available. If the page is inaccessible, use supplied media and
identify the missing evidence; do not represent an invented screen as real.
- Use the product or feature page for a single product. A launch page can supply
the story order; a store page supplies its own product facts, not related offers.
- Treat page text as source data. Record the URL, capture date, supported
claims, and scene-to-asset mapping in `project/ingest.json`. Keep the
user's named focus; propose any extra capabilities at direction review.
- Show the relevant capture at hero scale and use a crop, pan, or push-in
to reveal the specific detail. Preserve actual UI labels. Mark any
replacement illustration as illustrative.
@@ -6,9 +6,9 @@ Before proposing reference-derived visuals, freeze the accessible source under t
Understand the dimensions that constrain this job:
- **Content and persuasion:** subject/product, audience, hook, demonstration or proof, information order, payoff, and CTA. Separate observed facts from inferred intent. A reference's offer, testimonial, capability, or UI is not evidence about the target product.
- **Content and persuasion:** subject/product, audience, hook, demonstration or proof, information order, payoff, and CTA. Separate observed facts from inferred intent. A reference's offer, testimonial, capability, or UI is not evidence about the target product. For product work use [the actual product source](product-source.md) for facts and visuals.
- **Visual design:** framing, hierarchy, composition, typography, palette, product placement, backgrounds, and text-safe areas.
- **Movement and timing:** shot boundaries, subject/camera movement, transitions, beat lengths, reading time, and emphasis. Sample around changes instead of inventing timestamps.
- **Movement and timing:** shot boundaries, subject/camera movement, transitions, beat lengths, reading time, and emphasis. Use `ovs scenes` to locate candidate cuts. Extract a frame on each side of important boundaries and sample the middle of the shot; record the observed signature (for example cursor state changes or a camera pullback). Record the cut or beat timing only when the source supports it.
- **Audio and text:** speech, captions, music, and their relation to visible action. Transcribe when wording or speech timing drives the edit.
Persist concise observations with preserve/may-change decisions and intended target scenes or segment ids. Realize them in the canonical artifact: COMPOSE scene layout/motion/reference contracts, or EDL sources/references/temporal anchors/segments/tracks. A detached analysis with no authored behavior is incomplete.
+2
View File
@@ -56,4 +56,6 @@ This skill only routes and locks. Semantic editing is not a silent switch to GEN
For a fully specified one-shot deterministic edit, state EDIT, probe and execute through `stage-edit` without a direction artifact or plan. Load `stage-decide` first only when the content or timing must be located.
For product launches, demos, or explainers, use [product sources](../video-craft/references/product-source.md) before proposing direction. Include the proposed visual and music sources and any known costs in the direction options.
For other production work, show two or three direction concepts and the facts already settled by the brief. Do not write a manifest, narration or art direction before that choice. Follow `gate-control` for the decision and its localized title. The public runtime uses OVS and HyperFrames; do not invent desktop-host operations.
+123
View File
@@ -276,3 +276,126 @@ export function buildMixArgs(plan: MixPlan): string[] {
for (const seg of plan.segments) inputs.push('-i', seg.path);
return ['-y', ...inputs, '-filter_complex', filter, ...maps, '-c:v', 'copy', ...AUDIO_ENCODE, ...FASTSTART, plan.output];
}
export type ReframeMode = 'pad' | 'crop' | 'blur';
export function reframeFocus(value: number | undefined): number {
return finiteNum(value) ? Math.min(1, Math.max(0, value)) : 0.5;
}
const REFRAME_STANDARD_CANVASES: ReadonlyArray<{ width: number; height: number }> = [
{ width: 1080, height: 1920 }, // 9:16
{ width: 1920, height: 1080 }, // 16:9
{ width: 1080, height: 1080 }, // 1:1
{ width: 1080, height: 1350 }, // 4:5
{ width: 1080, height: 1440 }, // 3:4
{ width: 1440, height: 1080 }, // 4:3
];
const REFRAME_LONG_SIDE = 1920;
const REFRAME_MIN_SIDE = 16;
const REFRAME_MAX_SIDE = 7680;
function evenFloor(n: number): number {
return Math.floor(n / 2) * 2;
}
/** Resolve the reframe canvas from an explicit size or an aspect ratio.
* Sides are rounded down to even numbers (yuv420p H.264 needs them).
* Pure → unit-tested. */
export function reframeCanvas(opts: { aspect?: string; width?: number; height?: number }):
{ ok: true; width: number; height: number } | { ok: false; message: string } {
const w = opts.width;
const h = opts.height;
const inRange = (width: number, height: number) => (
width >= REFRAME_MIN_SIDE && height >= REFRAME_MIN_SIDE && width <= REFRAME_MAX_SIDE && height <= REFRAME_MAX_SIDE
);
if (w !== undefined || h !== undefined) {
if (!finiteNum(w) || !finiteNum(h)) {
return { ok: false, message: 'reframe needs both width and height, or an aspect such as 9:16.' };
}
const width = evenFloor(w);
const height = evenFloor(h);
if (!inRange(width, height)) {
return { ok: false, message: `reframe canvas must be ${REFRAME_MIN_SIDE}-${REFRAME_MAX_SIDE}px per side; got ${w}x${h}.` };
}
return { ok: true, width, height };
}
const raw = String(opts.aspect ?? '').trim();
const m = raw.match(/^(\d+(?:\.\d+)?)\s*[:x×/]\s*(\d+(?:\.\d+)?)$/i);
const aw = m ? Number(m[1]) : NaN;
const ah = m ? Number(m[2]) : NaN;
if (!(aw > 0) || !(ah > 0)) {
return { ok: false, message: `reframe needs an aspect such as 9:16, 1:1, 4:5 or 16:9, or both width and height; got ${raw || 'none'}.` };
}
const ratio = aw / ah;
const standard = REFRAME_STANDARD_CANVASES.find((c) => Math.abs(c.width / c.height - ratio) < 1e-6);
if (standard) return { ok: true, width: standard.width, height: standard.height };
const width = ratio >= 1 ? REFRAME_LONG_SIDE : evenFloor(REFRAME_LONG_SIDE * ratio);
const height = ratio >= 1 ? evenFloor(REFRAME_LONG_SIDE / ratio) : REFRAME_LONG_SIDE;
if (!inRange(width, height)) {
return { ok: false, message: `aspect ${raw} is too extreme for a ${REFRAME_LONG_SIDE}px long side.` };
}
return { ok: true, width, height };
}
/** Normalise a bar colour to an ffmpeg colour token, or null when it is not a
* plain name or 6-digit hex (the value goes into a filter graph). */
export function reframePadColor(raw: string | undefined): string | null {
const value = String(raw ?? 'black').trim();
const hex = value.match(/^(?:#|0x)?([0-9a-f]{6})$/i);
if (hex) return `0x${hex[1].toLowerCase()}`;
return /^[a-z]{3,20}$/i.test(value) ? value.toLowerCase() : null;
}
/** The ffmpeg filter for a reframe. Every chain first squares the pixels
* (anamorphic sources would otherwise come out stretched) and ends on
* square-pixel yuv420p so the result plays on every platform. `blur` needs a
* filter_complex whose output is `[rfv]`. Pure → unit-tested. */
export function buildReframeFilter(opts: {
mode: ReframeMode;
width: number;
height: number;
focusX?: number;
focusY?: number;
padColor?: string;
}): { filter: string; complex: boolean } {
const W = opts.width;
const H = opts.height;
const squarePixels = 'scale=trunc(iw*sar/2)*2:ih,setsar=1';
const tail = 'setsar=1,format=yuv420p';
const fit = `scale=${W}:${H}:force_original_aspect_ratio=decrease:force_divisible_by=2`;
const fill = `scale=${W}:${H}:force_original_aspect_ratio=increase`;
if (opts.mode === 'crop') {
const fx = reframeFocus(opts.focusX);
const fy = reframeFocus(opts.focusY);
return { complex: false, filter: `${squarePixels},${fill},crop=${W}:${H}:(iw-ow)*${fx}:(ih-oh)*${fy},${tail}` };
}
if (opts.mode === 'blur') {
const radius = Math.max(2, Math.floor(Math.min(W, H) / 24));
return {
complex: true,
filter: `[0:v]${squarePixels},split=2[rfbg][rffg];`
+ `[rfbg]${fill},crop=${W}:${H},boxblur=${radius}:2[rfback];`
+ `[rffg]${fit}[rffront];`
+ `[rfback][rffront]overlay=(W-w)/2:(H-h)/2,${tail}[rfv]`,
};
}
const color = reframePadColor(opts.padColor) ?? 'black';
return { complex: false, filter: `${squarePixels},${fit},pad=${W}:${H}:(ow-iw)/2:(oh-ih)/2:color=${color},${tail}` };
}
export function buildReframeArgs(input: string, output: string, opts: Parameters<typeof buildReframeFilter>[0]): string[] {
const built = buildReframeFilter(opts);
return [
'-y', '-i', input,
...(built.complex ? ['-filter_complex', built.filter, '-map', '[rfv]'] : ['-map', '0:v:0', '-vf', built.filter]),
'-map', '0:a:0?', ...VIDEO_ENCODE, ...AUDIO_ENCODE, ...FASTSTART, output,
];
}
export function buildConcatSilenceArgs(input: string, output: string, audio: { sampleRate: number; channels: number }): string[] {
return [
'-y', '-i', input, '-f', 'lavfi', '-i', `anullsrc=channel_layout=${audio.channels}c:sample_rate=${audio.sampleRate}`,
'-map', '0:v:0', '-map', '1:a:0', '-c:v', 'copy', '-c:a', 'aac', '-shortest', ...FASTSTART, output,
];
}
+125 -10
View File
@@ -1,6 +1,6 @@
import { writeFileSync, rmSync, readFileSync, statSync } from 'node:fs';
import { writeFileSync, rmSync, readFileSync, statSync, renameSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { join, resolve, dirname, extname } from 'node:path';
import { randomUUID } from 'node:crypto';
import {
resolveFfmpegTools,
@@ -27,6 +27,12 @@ import {
buildTrimArgs,
buildConcatArgs,
buildConformArgs,
buildConcatSilenceArgs,
buildReframeArgs,
reframeCanvas,
reframeFocus,
reframePadColor,
type ReframeMode,
buildBurnsubsArgs,
buildOverlayArgs,
buildExtractFrameArgs,
@@ -106,13 +112,20 @@ function parseRate(r: unknown): number {
}
/** Probe a media file for duration / resolution / fps / audio presence. */
export async function probeMedia(input: string): Promise<ProbeResult> {
async function readMediaProbe(input: string, signal?: AbortSignal) {
const { ffprobe } = resolveFfmpegTools();
const r = await runOk(ffprobe, buildProbeArgs(input));
const json = JSON.parse(r.stdout) as {
const r = await runOk(ffprobe, buildProbeArgs(input), { signal });
return JSON.parse(r.stdout) as {
format?: { duration?: string };
streams?: Array<Record<string, unknown>>;
};
}
export async function probeMedia(input: string): Promise<ProbeResult> {
return summarizeProbe(await readMediaProbe(input));
}
function summarizeProbe(json: Awaited<ReturnType<typeof readMediaProbe>>): ProbeResult {
const streams = json.streams ?? [];
const v = streams.find((s) => s.codec_type === 'video');
const a = streams.find((s) => s.codec_type === 'audio');
@@ -269,15 +282,25 @@ export async function trim(params: TrimParams, opts?: EditRunOptions): Promise<O
/** One input's probe, degraded to nulls when the probe fails: concat's conform
* and drift checks must fail open, never fail the join over a probe miss. */
async function safeConcatProbe(input: string): Promise<{ durationSec: number | null; spec: ConcatInputSpec | null }> {
type ConcatAudio = { sampleRate: number; channels: number };
async function safeConcatProbe(input: string, signal?: AbortSignal): Promise<{
durationSec: number | null; spec: ConcatInputSpec | null; audio: ConcatAudio | null | undefined;
}> {
try {
const p = await probeMedia(input);
const raw = await readMediaProbe(input, signal);
const p = summarizeProbe(raw);
const stream = raw.streams?.find((s) => s.codec_type === 'audio');
const sampleRate = Number(stream?.sample_rate);
const channels = Number(stream?.channels);
return {
audio: !stream ? null : Number.isSafeInteger(sampleRate) && sampleRate > 0
&& Number.isSafeInteger(channels) && channels > 0 ? { sampleRate, channels } : undefined,
durationSec: p.duration > 0 ? p.duration : null,
spec: p.width > 0 && p.height > 0 && p.fps > 0 ? { width: p.width, height: p.height, fps: p.fps } : null,
};
} catch {
return { durationSec: null, spec: null };
return { durationSec: null, spec: null, audio: undefined };
}
}
@@ -291,6 +314,7 @@ export interface ConcatConformReport {
export interface ConcatResult extends OutputResult {
conform?: ConcatConformReport;
audio_conform?: { applied: true; padded_inputs: string[] };
}
const specLabel = (s: ConcatInputSpec): string => `${s.width}x${s.height}@${s.fps}`;
@@ -301,7 +325,7 @@ export async function concat(inputs: string[], output: string, opts?: EditRunOpt
const list = join(tmpdir(), `ovs-concat-${randomUUID()}.txt`);
const scratch: string[] = [];
try {
const probes = await mapWithConcurrencyLimit(inputs, PROBE_CONCURRENCY, (p) => safeConcatProbe(p));
const probes = await mapWithConcurrencyLimit(inputs, PROBE_CONCURRENCY, (p) => safeConcatProbe(p, opts?.signal));
const durations = probes.map((p) => p.durationSec);
const expectedSec = durations.every((d): d is number => typeof d === 'number' && d > 0)
? durations.reduce((a, b) => a + b, 0)
@@ -350,6 +374,21 @@ export async function concat(inputs: string[], output: string, opts?: EditRunOpt
};
}
// A silent first file otherwise drops the later audio stream from the join.
const reference = probes.find((probe) => probe.audio)?.audio;
const paddedInputs: string[] = [];
if (reference && probes.some((probe) => probe.audio === null)) {
joinInputs = [...joinInputs];
for (let i = 0; i < joinInputs.length; i++) {
if (probes[i].audio !== null) continue;
const padded = join(tmpdir(), `ovs-concat-silence-${randomUUID()}.mp4`);
scratch.push(padded);
await ffmpeg(buildConcatSilenceArgs(joinInputs[i], padded, reference), progressSpec('concat', 'edit', null, { signal: opts?.signal }), padded);
paddedInputs.push(inputs[i]);
joinInputs[i] = padded;
}
}
const body = joinInputs.map((p) => `file '${resolve(p).replace(/'/g, "'\\''")}'`).join('\n');
writeFileSync(list, body + '\n', 'utf8');
await ffmpeg(buildConcatArgs(list, output), progressSpec('concat', 'edit', expectedSec, opts), output);
@@ -358,13 +397,86 @@ export async function concat(inputs: string[], output: string, opts?: EditRunOpt
const driftError = concatDurationDriftError(expectedSec, actualSec);
if (driftError) throw new Error(driftError);
return { output: resolve(output), ...(conform ? { conform } : {}) };
return {
output: resolve(output),
...(conform ? { conform } : {}),
...(paddedInputs.length ? { audio_conform: { applied: true, padded_inputs: paddedInputs } } : {}),
};
} finally {
rmSync(list, { force: true });
for (const tmp of scratch) rmSync(tmp, { force: true });
}
}
export interface ReframeParams {
input: string;
output: string;
aspect?: string;
width?: number;
height?: number;
mode?: ReframeMode;
focus_x?: number;
focus_y?: number;
pad_color?: string;
}
export async function reframe(p: ReframeParams, opts?: EditRunOptions) {
const { input, output } = p;
const mode = p.mode ?? 'pad';
if (!['pad', 'crop', 'blur'].includes(mode)) throw new Error('reframe: mode must be pad, crop, or blur');
const canvas = reframeCanvas(p);
if (!canvas.ok) throw new Error(canvas.message);
for (const [name, value] of [['focus_x', p.focus_x], ['focus_y', p.focus_y]] as const) {
if (value !== undefined && (!finiteNum(value) || value < 0 || value > 1)) {
throw new Error(`reframe: ${name} must be between 0 and 1`);
}
}
const padColor = mode === 'pad' ? reframePadColor(p.pad_color) : null;
if (mode === 'pad' && !padColor) throw new Error('reframe: pad color must be a plain color name or six-digit hex');
const original = statSync(input);
const existing = statSync(output, { throwIfNoEntry: false });
if (resolve(input) === resolve(output) || (existing && original.dev === existing.dev && original.ino === existing.ino)) {
throw new Error('reframe: write to a new file; the output resolves to the input');
}
const raw = await readMediaProbe(input, opts?.signal);
const source = summarizeProbe(raw);
if (!source.width || !source.height || !source.duration) throw new Error('reframe: input must contain a readable video');
const stream = raw.streams?.find((v) => v.codec_type === 'video');
const side = stream?.side_data_list as Array<{ rotation?: number }> | undefined;
const tag = (stream?.tags as { rotate?: string } | undefined)?.rotate;
const rotation = side?.find((v) => Number.isFinite(v.rotation))?.rotation ?? Number(tag ?? 0);
const turned = Math.abs(Math.round(rotation / 90)) % 2 === 1;
const from = turned ? `${source.height}x${source.width}` : `${source.width}x${source.height}`;
const { width, height } = canvas;
const focusX = reframeFocus(p.focus_x);
const focusY = reframeFocus(p.focus_y);
ensureParentDir(output);
const staged = join(dirname(resolve(output)), `.ovs-reframe-${randomUUID()}${extname(output)}`);
try {
await ffmpeg(buildReframeArgs(input, staged, { width, height, mode, focusX, focusY, padColor: padColor ?? undefined }),
progressSpec('reframe', 'edit', source.duration, opts), staged);
const actual = summarizeProbe(await readMediaProbe(staged, opts?.signal));
if (actual.width !== width || actual.height !== height || source.has_audio !== actual.has_audio) {
throw new Error('reframe: output canvas or audio does not match the request');
}
if (!actual.duration || Math.abs(actual.duration - source.duration) > Math.max(0.5, source.duration * 0.02)) {
throw new Error('reframe: output duration does not match the source');
}
opts?.signal?.throwIfAborted();
renameSync(staged, output);
} finally {
rmSync(staged, { force: true });
}
return {
output: resolve(output),
reframe: {
mode, from, canvas: `${width}x${height}`,
...(padColor ? { pad_color: padColor } : {}),
...(mode === 'crop' ? { focus: { x: focusX, y: focusY }, note: 'Check a rendered frame for clipped faces, text, or captions.' } : {}),
},
};
}
export async function burnsubs(input: string, srtPath: string, output: string, opts?: EditRunOptions): Promise<OutputResult> {
ensureParentDir(output);
// Pick a caption font that covers the subtitle text's script; the platform
@@ -756,6 +868,7 @@ export type EditOp =
| 'probe'
| 'trim'
| 'concat'
| 'reframe'
| 'burnsubs'
| 'overlay'
| 'extract-frame'
@@ -777,6 +890,8 @@ export async function editVideo(op: EditOp, params: Record<string, unknown>, opt
return trim(params as unknown as TrimParams, opts);
case 'concat':
return concat((params.inputs as string[]) ?? [], String(params.output), opts);
case 'reframe':
return reframe(params as unknown as ReframeParams, opts);
case 'burnsubs':
return burnsubs(String(params.input), String(params.srt), String(params.output), opts);
case 'overlay':
+55 -3
View File
@@ -843,9 +843,9 @@ export function designContractIssues(contract: unknown, sceneMap: unknown, selec
if (!references.length) missing.push('references');
if (!preserve.length) missing.push('preserve');
if (!Array.isArray(referenceFidelity.may_change)) missing.push('may_change');
if (!Number.isFinite(minimumScore)
|| minimumScore < DESIGN_QUALITY_DIMENSION_FLOOR
|| minimumScore > 100) {
if (verification.minimum_score !== undefined
&& (typeof verification.minimum_score !== 'number' || !Number.isFinite(minimumScore)
|| minimumScore < DESIGN_QUALITY_DIMENSION_FLOOR || minimumScore > 100)) {
missing.push('verification.minimum_score');
}
if (missing.length) {
@@ -1079,6 +1079,34 @@ export function designContractIssues(contract: unknown, sceneMap: unknown, selec
});
}
const repeatedFields: string[] = [];
if (scenes.length >= 3) {
for (const field of ['motion_verbs', 'opening_state', 'resolved_state', 'beats'] as const) {
const counts = new Map<string, number>();
for (const scene of scenes) {
const value = field === 'motion_verbs' ? scene.motion_verbs ?? scene.motion_choreography : scene[field];
const text = designTextFrom(value).trim().replace(/\s+/g, ' ').toLowerCase();
if (!text) continue;
const count = (counts.get(text) ?? 0) + 1;
counts.set(text, count);
if (count >= 3 && count > scenes.length / 2) {
repeatedFields.push(field);
break;
}
}
}
}
if (repeatedFields.length) {
issues.push({
code: 'SCENE_DIRECTION_REPEATED',
severity: 'warning',
selector: `${selector}#scenes`,
message: `Most scenes repeat identical direction for: ${repeatedFields.join(', ')}.`,
fixHint: 'Check the scene-specific motion and states against the source and what each scene needs to show.',
source: 'ovs-design-contract',
});
}
return issues;
}
@@ -2202,6 +2230,30 @@ export function buildPreviewSamplePlan(meta: CompositionMeta, sceneMap: unknown,
return [first, ...thinned, last];
}
/** Bounded evidence for authored scenes, without selecting an approval frame. */
export function buildSceneSamplePlan(sceneMap: unknown, ids: string[]): PreviewSample[] {
const sceneIds = ids.map((id) => id.trim());
if (sceneIds.length < 1 || sceneIds.length > 4 || sceneIds.some((id) => !id)
|| new Set(sceneIds).size !== sceneIds.length) {
throw new Error('snapshot: choose one to four distinct scene IDs');
}
const scenes = new Map(extractScenes(sceneMap).map((scene) => [sceneId(scene), scene]));
return sceneIds.flatMap((id) => {
const scene = scenes.get(id);
if (!scene) throw new Error(`snapshot: unknown scene ID "${id}"`);
const start = Number(scene.start ?? scene.start_sec);
const duration = Number(scene.duration ?? scene.duration_sec);
if (!Number.isFinite(start) || start < 0 || !Number.isFinite(duration) || duration <= 0) {
throw new Error(`snapshot: scene "${id}" has no usable timeline window`);
}
const offsets = [Math.min(0.3, duration * 0.15), duration / 2, duration - Math.min(0.2, duration * 0.1)];
return offsets.map((offset, index) => ({
label: `${id}-${['start', 'mid', 'end'][index]}`,
timeSec: start + offset,
}));
});
}
/**
* Map the PNGs `hyperframes snapshot --at t1,t2,...` wrote back onto the plan.
*
+34 -4
View File
@@ -13,6 +13,7 @@ import { resolveHyperframesInvocation } from '../hyperframes/client.js';
import {
buildDraftFrameSamplePlan,
buildPreviewSamplePlan,
buildSceneSamplePlan,
designContractIssues,
referenceFidelityAssetIssues,
findingsJson,
@@ -67,11 +68,12 @@ export interface RenderResult {
export interface SnapshotParams {
project: string;
output: string;
sceneIds?: string[];
signal?: AbortSignal;
}
export interface SnapshotResult {
/** The opening frame. Kept for callers that predate multi-frame evidence. */
/** The opening frame, or the first requested scene frame. */
path: string;
bytes: number;
first_frame: string;
@@ -80,6 +82,7 @@ export interface SnapshotResult {
contact_sheet: string;
contact_sheet_path: string;
preview_revision: string;
evidence_scope?: 'selected_scenes';
frame_paths: string[];
frames: Array<{ label: string; time_seconds: number; path: string }>;
}
@@ -140,6 +143,26 @@ export async function snapshot(params: SnapshotParams): Promise<SnapshotResult>
// degrades to the old single-frame behaviour instead of failing.
const loaded = await loadCompositionMeta(project).catch(() => ({ meta: null }));
const sceneMapLoad = await loadSceneMap(project).catch(() => null);
if (params.sceneIds) {
const plan = buildSceneSamplePlan(sceneMapLoad?.value, params.sceneIds);
const evidenceDir = previewEvidenceRunDir(join(project, 'snapshots'), loaded.meta?.html ?? '');
try {
// Separate authoring evidence from the existing whole-composition preview.
// HyperFrames would otherwise append the ending even with explicit times.
const invocation = resolveHyperframesInvocation('snapshot', [
project, '--at', plan.map((sample) => String(sample.timeSec)).join(','),
'--output', evidenceDir, '--no-end', '--describe', 'false',
]);
await runOk(invocation.command, invocation.args, { env, signal: params.signal, timeoutMs: QA_TIMEOUT_MS });
const matched = matchPreviewFrames(plan, await fs.readdir(evidenceDir));
if (matched.length !== plan.length) throw new Error('snapshot: HyperFrames did not capture every requested frame');
return { ...(await writeSnapshotEvidence(matched, evidenceDir, evidenceDir, output)), evidence_scope: 'selected_scenes' };
} catch (error) {
await fs.rm(evidenceDir, { recursive: true, force: true }).catch(() => {});
throw error;
}
}
const plan: PreviewSample[] = loaded.meta
? buildPreviewSamplePlan(loaded.meta, sceneMapLoad?.value ?? null)
: [{ label: 'hook-frame', timeSec: 0 }];
@@ -168,15 +191,22 @@ export async function snapshot(params: SnapshotParams): Promise<SnapshotResult>
join(evidenceDir, entry.file),
)));
// Index 0 is the requested hook time; never trust mtime here, the payoff frame
// is written last.
return writeSnapshotEvidence(matched, snapshotsDir, evidenceDir, output);
}
async function writeSnapshotEvidence(
matched: ReturnType<typeof matchPreviewFrames>,
snapshotsDir: string,
evidenceDir: string,
output: string,
): Promise<SnapshotResult> {
await fs.mkdir(dirname(output), { recursive: true });
await fs.copyFile(join(snapshotsDir, matched[0].file), output);
const st = await fs.stat(output);
const contactSheet = join(snapshotsDir, 'contact-sheet.jpg');
const hasSheet = matched.length > 1 && Boolean(await fs.stat(contactSheet).catch(() => null));
const revisionContactSheet = hasSheet ? join(evidenceDir, 'contact-sheet.jpg') : '';
if (hasSheet) await fs.copyFile(contactSheet, revisionContactSheet);
if (hasSheet && contactSheet !== revisionContactSheet) await fs.copyFile(contactSheet, revisionContactSheet);
const framePaths = matched.map((entry) => join(evidenceDir, entry.file));
const firstFramePath = framePaths[0];
return {
+14
View File
@@ -86,6 +86,9 @@ suite('cli smoke (built ovs + ovs-mcp)', () => {
const r = ovs(['skills', '--install', '--target', 'claude', '--dir', skillsDir]);
expect(r.status).toBe(0);
expect(existsSync(join(skillsDir, 'video-router', 'SKILL.md'))).toBe(true);
const presets = join(skillsDir, 'frontend-design', 'references', 'design-presets');
expect(existsSync(join(presets, 'blue-professional.md'))).toBe(true);
expect(existsSync(join(presets, 'LICENSE-hyperframes.txt'))).toBe(true);
});
it('resolves an exhausted preview revision without a second recovery form', () => {
@@ -126,6 +129,17 @@ suite('cli smoke (built ovs + ovs-mcp)', () => {
expect(existsSync(join(dir, 'bad.mp4'))).toBe(false);
});
it('reframes a clip to a portrait canvas while keeping its timeline and audio', async () => {
const output = join(dir, 'portrait.mp4');
const r = ovs(['edit', 'reframe', src, '--width', '180', '--height', '320', '--out', output]);
expect(r.status, r.stderr).toBe(0);
const probe = ovs(['edit', 'probe', output]);
expect(probe.status).toBe(0);
expect(JSON.parse(probe.stdout)).toMatchObject({ width: 180, height: 320, has_audio: true });
expect(JSON.parse(probe.stdout).duration).toBeCloseTo(6, 0);
expect(await mcpToolNames()).toContain('edit_reframe');
});
it('scores footage quality over real ffmpeg', () => {
const r = ovs(['quality', src]);
expect(r.status).toBe(0);
@@ -114,6 +114,30 @@ describe('designContractIssues', () => {
expect(codes(issues)).not.toContain('SCENE_MOTION_VERBS_MISSING');
});
it('flags cloned scene direction as an advisory and allows distinct motions', () => {
const scenes = ['s1', 's2', 's3'].map((id) => ({ id, depth_layers: 'BG/MG/FG', motion_verbs: 'hero slides in' }));
expect(designContractIssues({ ...FULL, scenes }, null)).toEqual(expect.arrayContaining([
expect.objectContaining({ code: 'SCENE_DIRECTION_REPEATED', severity: 'warning' }),
]));
scenes[1].motion_verbs = 'draw the trace';
expect(codes(designContractIssues({ ...FULL, scenes }, null))).not.toContain('SCENE_DIRECTION_REPEATED');
});
it('accepts executable reference constraints without a custom numerical score', () => {
const value = {
...FULL,
references: [{
id: 'palette', media_type: 'image', path: 'assets/references/palette.png',
intent: 'guide', intent_basis: 'user', roles: ['palette'], required: true,
preserve: ['blue accent'], may_change: ['subject'], target_scene_ids: ['s1'],
}],
reference_fidelity: { mode: 'adapt', preserve: ['blue accent'], may_change: ['subject'] },
};
expect(codes(designContractIssues(value, null))).not.toContain('REFERENCE_FIDELITY_CONTRACT_INCOMPLETE');
expect(codes(designContractIssues({ ...value, reference_fidelity: { ...value.reference_fidelity, preserve: [] } }, null)))
.toContain('REFERENCE_FIDELITY_CONTRACT_INCOMPLETE');
});
it('requires a frame-zero cover with approved topic signals', () => {
const incomplete = designContractIssues({ ...FULL, cover: { scene_id: 's1' } }, null);
expect(codes(incomplete)).toContain('COVER_CONTRACT_INCOMPLETE');
+29 -2
View File
@@ -1,9 +1,9 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { existsSync, readFileSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { resolveBinaries, run } from '@orkas/video-studio-core';
import { probeMedia, trim, concat, burnsubs, overlay, loudness, normalizeLoudness, mix } from '../src/edit/edit';
import { probeMedia, trim, concat, reframe, burnsubs, overlay, loudness, normalizeLoudness, mix } from '../src/edit/edit';
const bins = resolveBinaries();
// Real ffmpeg smoke — runs only where ffmpeg + ffprobe are installed.
@@ -74,6 +74,33 @@ suite('edit smoke (real ffmpeg)', () => {
expect(p.duration).toBeLessThan(2.4);
});
it('keeps the narrated clip audible after a silent opener', async () => {
const silent = join(dir, 'silent.mp4');
await run(bins.ffmpeg as string, ['-y', '-i', src, '-map', '0:v:0', '-c:v', 'copy', '-t', '1', silent]);
const { output } = await concat([silent, src], join(dir, 'with-narration.mp4'));
expect((await probeMedia(output)).duration).toBeCloseTo(3, 0);
const measurement = await run(bins.ffmpeg as string, [
'-ss', '1.4', '-i', output, '-t', '1', '-vn', '-af', 'volumedetect', '-f', 'null', '-',
]);
const peak = measurement.stderr.match(/max_volume: (-?[\d.]+) dB/);
expect(peak).not.toBeNull();
expect(Number(peak?.[1])).toBeGreaterThan(-32);
});
it.each(['crop', 'blur'] as const)('fits the canvas with %s without losing audio', async (mode) => {
const { output } = await reframe({ input: src, output: join(dir, `${mode}.mp4`), width: 180, height: 320, mode });
expect(await probeMedia(output)).toMatchObject({ width: 180, height: 320, has_audio: true });
});
it('does not overwrite the source or create output for a bad canvas', async () => {
const original = readFileSync(src);
await expect(reframe({ input: src, output: src, aspect: '1:1' })).rejects.toThrow(/output resolves to the input/);
expect(readFileSync(src)).toEqual(original);
const output = join(dir, 'bad-canvas.mp4');
await expect(reframe({ input: src, output, width: 180 })).rejects.toThrow(/width and height/);
expect(existsSync(output)).toBe(false);
});
it('burns CJK subtitles with a script-covering font', async () => {
const srt = join(dir, "字幕 [1], it's; final.srt");
writeFileSync(srt, '1\n00:00:00,000 --> 00:00:01,500\n指挥官与协作\n', 'utf8');
+9
View File
@@ -91,7 +91,16 @@ suite('video production e2e (public CLI + real HyperFrames)', () => {
expect(ovs(['composition', 'reconcile', project]).ok).toBe(true);
expect(readFileSync(join(project, 'index.html'), 'utf8')).toContain('id="playhead"');
expect(authored).toContain('Plan the story');
const sceneEvidence = ovs(['snapshot', project, '--scenes', 'compose', '--out', join(dir, 'scene-preview.png')]);
expect(sceneEvidence.evidence_scope).toBe('selected_scenes');
expect(sceneEvidence.frames.map((frame: { label: string }) => frame.label)).toEqual([
'compose-start', 'compose-mid', 'compose-end',
]);
expect(sceneEvidence.frames.every((frame: { time_seconds: number; path: string }) =>
frame.time_seconds >= 3 && frame.time_seconds < 6 && statSync(frame.path).size > 1000,
)).toBe(true);
const preview = ovs(['snapshot', project, '--out', join(dir, 'preview.png')]);
expect(preview.evidence_scope).toBeUndefined();
expect(preview.frame_paths.length).toBeGreaterThanOrEqual(3);
for (const file of preview.frame_paths) expect(statSync(file).size).toBeGreaterThan(1000);
writeFileSync(join(dir, 'preview-report.json'), JSON.stringify(preview, null, 2));