| name | promo-from-brief |
| description | Use when the user gives a text brief for a branded promo short -- "promo for the <event> webinar", "componme el promo de X con la marca Y". Composes scene-spec.json from the promo-* library (promo-intro-card, promo-hero, promo-card-speaker, promo-blur-cta, promo-agenda, promo-quote, promo-details), binds a brand pack via spec.brand, then assembles, runs per-scene QA, and renders. Defer to this skill for any brand-pack-driven promo. Skip for non-promo shorts (canonical-short) and when the brand pack does not exist yet (extract-brand-pack first).
|
Promo from brief
CWD: all bash commands below assume cd apps/hyperframe first. Paths like src/episodes/<slug>/, brands/<slug>/ are app-relative.
A promo episode is a typed scene-spec.json composed from the promo-* scene-type library, with a top-level "brand": "<brand-slug>" binding it to brands/<brand-slug>/brand.json. The assembler stamps the brand pack's --brand-* CSS variables into the generated index.html -- never hand-edit index.html; edit the spec (or the brand pack) and re-run bun run assemble <slug>. No Figma reference is needed: everything comes from the text brief + the brand pack + photo composites.
Scope
- IN: composing a complete branded promo episode from a text brief (campaign goal, speaker, date, language, CTA) using only the existing promo-* scene-types.
- OUT: extracting a new brand (
.agents/skills/extract-brand-pack/SKILL.md), building a new scene-type from a design reference (.agents/skills/figma-to-scene-type/SKILL.md), non-promo shorts (canonical-short).
Inputs
Collect before touching the spec; ask the user for anything missing:
- Brief -- campaign goal, event name, speaker name + role, date/time, language of the copy, CTA wording.
- Brand slug -- must exist as
brands/<brand-slug>/brand.json. ls brands/ to verify; if missing, stop and run extract-brand-pack first (assemble hard-errors on a declared-but-missing brand pack).
- Photo composites -- per-campaign inputs, NOT generated by this skill. Sources, in order of preference: existing exports under the episode's
assets/ from a prior Figma session, exports produced during the extract-brand-pack flow, or AI generation via .agents/skills/generated-raster-assets/SKILL.md. Needed kinds: speaker photo card(s), blurred full-bleed background composite(s), circular avatar crop.
- Logo files -- brand wordmark in dark-on-light and light-on-dark variants (SVG preferred), usually copied from
brands/<brand-slug>/.
Scene grammar
The seven promo-* types and their required slots (full detail: read templates/scenes/<type>/v1/manifest.json). All are 1080x1920 story-ad replicas sharing the same family geometry.
| Type | Purpose | Required slots |
|---|
promo-intro-card | Opener on a white frame: centered rounded photo card with notch badge, name/role, logo + headline + date | badge, speakerName, speakerRole, title, date, photoImage, logoImage |
promo-hero | Photo card pinned top over dimmed blur bg, light-on-dark lower block with logo + title + body + CTA | badge, title, body, cta, bgImage, photoImage, logoImage |
promo-card-speaker | White frame: photo card upper half, badge + name/role on top, dark-on-light lower block with CTA | badge, speakerName, speakerRole, title, body, cta, photoImage, logoImage |
promo-blur-cta | Closer: full-bleed blur bg, circular avatar + name/role mid-frame, light-on-dark lower block with CTA | badge, speakerName, speakerRole, title, body, cta, bgImage, avatarImage, logoImage |
promo-agenda | White frame: badge, display title, 2-4 numbered topic rows, logo + date footer | badge, title, items (repeat 2-4, each { "text" }), date, logoImage |
promo-quote | Pull quote on blur bg, circular avatar + name/role below, logo footer | badge, quote, speakerName, speakerRole, bgImage, avatarImage, logoImage |
promo-details | White frame recap: badge, logo, display title, 2-4 info chips (date/time/format), solid CTA | badge, title, chips (repeat 2-4, each { "text" }), cta, logoImage |
Narrative ordering (default beat sequence; cut beats for shorter promos, never reorder against the funnel):
- Announce --
promo-intro-card (speaker-led) or promo-hero (event-led).
- Agenda --
promo-agenda: what attendees get.
- Quote --
promo-quote: speaker credibility beat.
- Urgency --
promo-hero or promo-card-speaker with scarcity copy ("last seats", date proximity).
- Recap --
promo-details: date/time/format chips.
- CTA closer --
promo-blur-cta, always last.
Durations: 4-5s per scene (type defaults already sit in that band; closer gets 5s). A 6-beat promo lands at ~27s.
Skeleton (light vs dark logo variants follow the frame: dark-on-light scenes take the black logo, blur-bg scenes the white one):
{
"slug": "<episode-slug>",
"lang": "es",
"width": 1080,
"height": 1920,
"brand": "<brand-slug>",
"palette": { "accent": "#111111", "accent2": "#8a8f98" },
"scenes": [
{ "id": "intro", "type": "promo-intro-card", "duration": 4.5, "slots": { "badge": "Webinar gratuito", "speakerName": "...", "speakerRole": "...", "title": "Linea uno<br>linea dos", "date": "...", "photoImage": "assets/photo-card.png", "logoImage": "assets/logo-black.svg" } },
{ "id": "agenda", "type": "promo-agenda", "duration": 4.5, "slots": { "badge": "...", "title": "...", "items": [{ "text": "..." }, { "text": "..." }, { "text": "..." }], "date": "...", "logoImage": "assets/logo-black.svg" } },
{ "id": "quote", "type": "promo-quote", "duration": 4, "slots": { "badge": "...", "quote": "...", "speakerName": "...", "speakerRole": "...", "bgImage": "assets/bg-blur.png", "avatarImage": "assets/avatar.png", "logoImage": "assets/logo-white.svg" } },
{ "id": "urgency", "type": "promo-hero", "duration": 4.5, "slots": { "badge": "...", "title": "...", "body": "... <strong>...</strong>", "cta": "...", "bgImage": "assets/bg-blur.png", "photoImage": "assets/photo-hero.png", "logoImage": "assets/logo-white.svg" } },
{ "id": "details", "type": "promo-details", "duration": 4.5, "slots": { "badge": "...", "title": "...", "chips": [{ "text": "..." }, { "text": "..." }], "cta": "...", "logoImage": "assets/logo-black.svg" } },
{ "id": "closer", "type": "promo-blur-cta", "duration": 5, "slots": { "badge": "...", "speakerName": "...", "speakerRole": "...", "title": "...", "body": "...", "cta": "...", "bgImage": "assets/bg-blur.png", "avatarImage": "assets/avatar.png", "logoImage": "assets/logo-white.svg" } }
]
}
Reference implementation: src/episodes/vidext-webinar-ita/scene-spec.json.
Copy rules
- Copy is a draft. Present the full per-scene copy table to the user for approval BEFORE assembling. Never invent factual claims -- no made-up stats, bios, attendee numbers, or company history; everything factual must come from the brief or be asked for.
- Write in the brief's language. For Spanish: español de España, never argentinisms (no "cerrás/tenés/vos/fijate").
- Display titles (
title, quote) are richText: break lines with explicit <br> where the layout needs two lines -- the scene-types do not auto-wrap display type. body accepts <strong> for the CTA-adjacent emphasis.
- Keep the
badge text identical across scenes (it is the campaign label, e.g. "Webinar gratuito").
Workflow
bun run new:episode <episode-slug> --intent=social
- Scaffold with the command above, then replace the starter
scene-spec.json entirely with the composed promo spec (skeleton above). Promo episodes do not use the house outro -- promo-blur-cta is the brand sign-off.
- Add
"brand": "<brand-slug>" at the top level. Leave meta.json alone: meta.brand is a render-time override only; assemble-time stamping carries into QA and render on its own.
- Stage assets. Copy photo composites, avatar, and logo variants into
src/episodes/<episode-slug>/assets/. For a non-Inter brand, also copy the brand woff2 there AND confirm the pack declares it in its fonts array (brands/<brand-slug>/brand.json, e.g. "fonts": [{ "family": "Sora", "file": "SoraVariable.woff2" }]) -- the assembler emits the @font-face from that field; the shell itself only registers Inter and Berkeley Mono.
- Validate + assemble:
bun run scene:check src/episodes/<episode-slug>/scene-spec.json
bun run assemble <episode-slug>
Confirm the brand actually bound: grep -o 'data-brand="[^"]*"' src/episodes/<episode-slug>/index.html must print the brand slug (a typo'd spec key like "brnad" is silently ignored and renders unbranded).
- Per-scene QA (HITL):
bun run scripts/scene-qa.ts <episode-slug> [--scenes=id1,id2]
Show the contact sheet; iterate only rejected scenes via --scenes=. Get explicit approval per scene before rendering.
- Render:
bun run render:episode <episode-slug> --format=mp4
The render log should show the brand slug; a publishable: false pack warns here and only here.
Gotchas
assemble hard-errors on a missing brand pack. "brand": "x" with no brands/x/brand.json stops the build with a named error -- run extract-brand-pack first, or remove the field.
- Editing
brand.json requires re-assemble. Brand values are baked into the generated html, same rule as spec edits; QA and render read whatever index.html is on disk.
meta.json brand stays unset. Setting it creates a double source; it exists only to override the brand at render time.
- Photo radii may be baked into the image assets.
.pin-photo/.pcs-photo have no CSS border-radius -- the rounded corners live in the exported PNG. Prepare speaker composites already rounded (and avatars already circular), or they render square.
- Composites are inputs, not outputs. This skill never generates photos; missing composites means pausing to source them (Figma export, extract-brand-pack leftovers, or generated-raster-assets).
- Brand font needs file + declaration.
--brand-font alone changes only the stack; the woff2 must sit in the episode assets/ and the pack's fonts array must declare it (that emits the @font-face). Missing either means the render silently uses system-ui and line wraps drift.
Final checklist
Handoff
Publishing copy (YouTube/TikTok/Reels/LinkedIn) for the rendered promo: .agents/skills/generate-distribution-copy/SKILL.md.