mirror of
https://github.com/Orkas-AI/Orkas-VideoStudio.git
synced 2026-11-07 06:15:57 -10:00
Merge pull request #43 from BlueSkyID666/codex/sync-videostudio-20261104
Sync portable VideoStudio editing and authoring updates
This commit is contained in:
39 files changed
+2639
-36
No files matched your search
@@ -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`.
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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' }),
|
||||
]));
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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 @@
|
||||
{"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/**"]}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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':
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user