HyperFrames Invitation Card Pipeline
Sister skill of hyperframes-video and hyperframes-composition-rules. Those cover the framework contracts; this one covers the personal-invitation application pattern.
Read The Core Skills First (MANDATORY)
Before writing HTML, load both:
skill_view hyperframes-video— general pipeline, rules, transitions, verificationskill_view hyperframes-composition-rules— hard rules (clip class, timeline registry, track overlap, no exit animations except final)
Do NOT write an invitation composition from memory. The 5 non-negotiable rules apply to invitations just as much as to marketing reels, and the wedding-invitation smoke test on 2026-04-21 passed lint 0/0 on first try ONLY because both skills were loaded first.
When To Use This Skill
Use for personal/event invitations where:
- Format is vertical 1080x1920 (phone-native, WhatsApp/Instagram Story friendly)
- A user-provided photo needs to feature in the composition
- A music bed (user-uploaded, often from Discord/voice-memo, often mislabeled .ogg) needs to be mixed in
- Sometimes a second language version is needed (common: Polish + English, Spanish + English, etc.)
Do NOT use this skill for:
- Marketing reels, carousels-as-video, explainers → use
hyperframes-videoor domain-specific content engines - Landscape 16:9 or square 1:1 → different templates needed, this pattern is portrait-first
- Print invitations → this is MP4 output; use a PDF/design pipeline instead
The 6-Scene Wedding/Event Template (tested, ship-ready)
Timing budget: 22.5s total. Each scene has entrance animations only; transitions are 0.4s blur-crossfades between adjacent tracks. Alternate data-track-index between 0 and 1 so no overlap occurs on the same track.
| # | Scene | Start | Dur | Track | Background | Purpose |
|---|---|---|---|---|---|---|
| 1 | Hero / "Save the Date" | 0.0 | 3.3 | 0 | Ivory radial | Grab attention; eyebrow + script + sub |
| 2 | Names + circular photo cameo | 3.2 | 4.2 | 1 | Parchment | Couple's names + their selfie in a gold-ringed circle |
| 3 | Date | 7.3 | 4.0 | 0 | Night (dark) | Huge day number, month, year; cinematic hero moment |
| 4 | Ceremony/venue | 11.2 | 4.0 | 1 | Ivory | Church name, location, time |
| 5 | Reception | 15.1 | 4.0 | 0 | Gold/brass | Reception venue + "follows the ceremony" tag |
| 6 | Closing | 19.0 | 3.5 | 1 | Deep brown | Heartfelt line + names in calligraphy + place-date mark. ONLY scene allowed an exit fade. |
Transitions fire at t=3.0, 7.0, 11.0, 14.9, 18.8 — each gsap.to on outgoing scene at that time, paired with gsap.from on incoming scene 0.1-0.2s later. The outgoing scene MUST be fully visible at transition start (no preemptive exits — the transition IS the exit).
The Circular Photo Cameo Pattern
<div class="cameo-frame s2-frame">
<img class="cameo-photo s2-photo" src="assets/couple.jpg" alt="..." />
<div class="cameo-ring s2-ring"></div>
</div>
.cameo-frame {
position: relative;
width: 720px;
height: 720px;
}
.cameo-photo {
width: 640px;
height: 640px;
border-radius: 50%;
object-fit: cover;
object-position: 50% 40%; /* TUNE THIS — for selfies, 40% lifts faces into center */
border: 6px solid #C9A864;
box-shadow: 0 20px 60px rgba(42, 26, 20, 0.35), inset 0 0 0 14px #F5EBDB;
}
.cameo-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px dashed #C9A864;
opacity: 0.6;
}
Animation pair (nice entrance):
tl.from('.s2-frame', { scale: 0.75, opacity: 0, duration: 0.9, ease: 'back.out(1.4)' }, 3.40);
tl.from('.s2-ring', { rotation: -180, opacity: 0, duration: 1.4, ease: 'power3.out',
transformOrigin: '50% 50%' }, 3.50);
Pitfall: selfie face-centering. Default object-position: 50% 50% crops heads off for typical selfies (taken from below, faces near top). Use 50% 40% or 50% 35% for selfies. Verify via frame sampling, don't trust the CSS default.
Music Bed Preprocessing (CRITICAL — do this BEFORE the render)
User-provided music is almost always too long and at full volume. HyperFrames' data-volume attribute works but is crude. Pre-process with ffmpeg for clean fades + correct length:
# Trim to composition length, lower volume, add fade-in and fade-out
ffmpeg -y -i assets/music_raw.mp3 \
-t 22.5 \
-af "volume=0.35,afade=t=in:st=0:d=0.8,afade=t=out:st=21.5:d=1.0" \
-codec:a libmp3lame -b:a 192k \
assets/music_bed.mp3
Then wire into composition:
<audio id="music-bed" data-start="0" data-duration="22.5"
data-track-index="9" data-volume="1.0"
src="assets/music_bed.mp3"></audio>
Audio elements do NOT need class="clip". Put them on a high data-track-index (9+) to stay out of the way of visual tracks.
Tuning: 0.35 volume is gentle background bed that doesn't fight vocals or text legibility. Fade-in 0.8s feels natural, fade-out 1.0s at 21.5s catches the final scene's settle.
Discord Voice Memo / Audio File Quirks
Users frequently say "I attached an MP3" but the file in ~/.hermes/cache/audio/ has a .ogg extension (Discord transcoding quirk). Verify with ffprobe:
ffprobe -v error -show_entries stream=codec_name,codec_type \
-show_entries format=format_name,duration \
${HOME}/.hermes/cache/audio/audio_<hash>.ogg
If codec_name=mp3 and format_name=mp3, it's actually an MP3 — just copy to assets/music.mp3 (ffmpeg will handle either way, but name it correctly for humans reading the project later).
Finding the recently-uploaded file: find ${HOME}/.hermes/cache -type f \( -name "*.mp3" -o -name "*.ogg" -o -name "*.m4a" -o -name "*.wav" \) -mmin -60 — look for recent audio drops regardless of extension. ${HOME}/.hermes/cache/audio/ is the canonical drop directory for Discord audio.
Bilingual Fork Workflow
When the user wants the same invitation in two languages (common: PL + EN for Polish weddings, ES + EN for Latin American weddings):
- Build and verify the first language first. Do the full lint + render + vision-check cycle. Get user approval before forking.
- Duplicate the project — do NOT try to parameterize one HTML with string swapping. HyperFrames is deterministic and static; duplication is cheaper than a template engine.
cd /home/adam && rm -rf wedding-invitation-pl && \ npx hyperframes init wedding-invitation-pl --no-install mkdir -p wedding-invitation-pl/assets cp wedding-invitation/assets/couple.jpg wedding-invitation-pl/assets/ cp wedding-invitation/assets/music_bed.mp3 wedding-invitation-pl/assets/ cp wedding-invitation/index.html wedding-invitation-pl/index.html - Three ID changes in the new project (miss any and render fails silently or produces stale animations):
<html lang="pl">on the root elementdata-composition-id="wedding-invitation-pl"on the#stagedivwindow.__timelines['wedding-invitation-pl'] = tl;at the end of the GSAP blockmeta.json→{"id": "wedding-invitation-pl", "name": "wedding-invitation-pl", ...}
- Translate ONLY visible strings. Do NOT translate class names, IDs, or GSAP selectors. The 11 typical translation targets for a wedding invitation:
| Slot | EN | PL | ES |
|---|---|---|---|
| Eyebrow | "You are invited" | "Zapraszamy" | "Estás invitado" |
| Hero script | "Save the Date" | "Zapisz datę" | "Reserva la fecha" |
| Names tag | "are getting married" | "biorą ślub" | "se casan" |
| Date eyebrow | "Saturday" | "Sobota" | "Sábado" |
| Month | "August" | "Sierpnia" (genitive!) | "Agosto" |
| Ceremony chip | "The Ceremony" | "Ceremonia" | "La Ceremonia" |
| Ceremony label | "Holy Mass" | "Msza Święta" | "Santa Misa" |
| Reception chip | "Reception" | "Wesele" | "Recepción" |
| Reception label | "The celebration" | "Przyjęcie weselne" | "La celebración" |
| Follow-up tag | "to follow the ceremony" | "bezpośrednio po ceremonii" | "tras la ceremonia" |
| Closing line | "with joy in our hearts, we invite you to share this day with us" | "z radością w sercu, zapraszamy, abyście byli z nami tego dnia" | "con alegría en el corazón, los invitamos a compartir este día" |
Polish genitive gotcha: Months take genitive after a day number — "15 sierpnia" NOT "15 sierpień". Similarly Russian uses genitive, Czech uses genitive. English and Spanish don't. Get this right on first try by using the native speaker form.
Typography Defaults That Work
These three Google fonts render Polish, Spanish, French, German diacritics cleanly via HyperFrames' font pipeline — verified 2026-04-21 with ą, ę, ś, ł, ż, ń, ó, Ś, Ż all correct:
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,400&family=Great+Vibes&display=swap" rel="stylesheet" />
Size floors for 1080x1920 portrait:
- Hero script (Great Vibes): 220px
- Names (Playfair Display bold): 96px
- Date day number (Playfair Display bold): 300px
- Venue name (Playfair Display semibold): 72px
- Venue time (Playfair Display bold): 180px
- Body italic (Cormorant Garamond): 48px
- Eyebrow (Cormorant Garamond italic, letter-spaced): 34px
Going smaller than these for the main focal element of a scene produces frames that feel empty on a phone screen.
Culturally-Flavored Ornaments
Simple SVG ornaments beat stock vector imports. For Kraków/Polish weddings, inline wycinanki-inspired SVGs in crimson (#8E1824) + gold (#C9A864):
<svg class="ornament-top" viewBox="0 0 420 120" xmlns="http://www.w3.org/2000/svg">
<g fill="#8E1824">
<circle cx="210" cy="60" r="14"/>
<path d="M210 46 C 195 20, 170 20, 165 45 C 160 65, 185 70, 210 60 Z"/>
<path d="M210 46 C 225 20, 250 20, 255 45 C 260 65, 235 70, 210 60 Z"/>
<circle cx="120" cy="60" r="8"/>
<circle cx="300" cy="60" r="8"/>
</g>
</svg>
Replaceable: swap motifs for the culture (papel picado for Mexican weddings, mehndi patterns for South Asian, hand-drawn fern fronds for modern-minimal). Keep them as inline SVG — external files slow the render worker and risk missing-asset warnings.
Frame Sampling — When Early Sampling Lies
The first HyperFrames render sampling pass on the wedding-invitation smoke test returned "year 2026 not visible" and "time 15:30 not visible" as false negatives. Root cause: sampled at t=9.3 and t=13.3, but the year enters at t=9.3 and the time enters at t=13.3 — the frame captured the scene mid-entrance.
Rule: sample at data-start + (scene_duration * 0.75), not at the middle. For a 4.0s scene starting at 7.3s, sample at 10.3s (7.3 + 3.0). This catches the scene fully settled, all entrance animations complete.
Typical sample timestamps for the 6-scene template:
Scene 1 (0.0, 3.3s dur): sample at 1.8
Scene 2 (3.2, 4.2s dur): sample at 5.3
Scene 3 (7.3, 4.0s dur): sample at 10.5 # NOT 9.3
Scene 4 (11.2, 4.0s dur): sample at 14.5 # NOT 13.3
Scene 5 (15.1, 4.0s dur): sample at 18.5 # NOT 17.3
Scene 6 (19.0, 3.5s dur): sample at 21.0 # NOT 20.5
If you sample earlier and something "looks missing," re-sample later before concluding the render is broken.
Verification Before Shipping
cd <project>
npx hyperframes lint --strict # MUST exit 0
npx hyperframes render --strict
# Verify audio stream present:
ffprobe -v error -show_entries stream=codec_type,codec_name \
renders/<latest>.mp4 # expect h264 + aac
# Sample and vision-check:
for t in 1.8 5.3 10.5 14.5 18.5 21.0; do
ffmpeg -y -ss $t -i renders/<latest>.mp4 -frames:v 1 verify/s_${t}.png
done
# Vision-check each frame for its expected scene content
Only ship when:
- Lint strict passes (0/0)
- File has both h264 + aac streams (if music bed added)
- Every sampled frame shows the expected scene content (vision pass)
- Diacritics render correctly in the target language
Delivery Format
The rendered MP4 is typically 3-4MB for a 22.5s invitation with music. That fits inside:
- WhatsApp attachment limit (16MB)
- Messenger attachment limit (25MB)
- Instagram Story (must be 9:16 which 1080x1920 is — direct upload works)
- Email attachment for Gmail/Outlook/Apple Mail (20MB+)
Use MEDIA:<absolute-path> in the assistant response — Discord/Telegram/Messenger bridges auto-upload.
Iteration Levers (common user asks after first draft)
When shipping the first draft, expect these follow-up asks — have the answer ready:
- Add music → see Music Bed Preprocessing section above
- Crop photo differently → change
object-positionpercentage on.cameo-photo - Longer/shorter → scale all scene
data-startanddata-durationproportionally; update GSAP offsets to match - Different language → see Bilingual Fork Workflow section
- Change palette → swap CSS custom properties or replace the 6
.bg-*class backgrounds - Swap culture motifs → replace inline SVG ornaments
- Add QR code for RSVP → use
qrencode -o assets/rsvp.png -s 10 "https://..."then<img src="assets/rsvp.png">in the closing scene
Related Skills
hyperframes-video— parent skill with render pipeline details, prerequisites, Docker, CLI flagshyperframes-composition-rules— the 5 non-negotiable rules (load this first, always)nano-banana-pro/ image-gen skills — for generating venue illustrations if user has no photolocal-tts-kokoro— if the user wants voiceover narration (rare for invitations, common for explainers)
Field Lessons (2026-04-21 Adam & Weronika wedding invitation)
First draft: 6-scene template, 22.5s, 1080x1920, rendered in 3 min 11s on QEMU 4-core. Lint 0/0 on first try because hyperframes-video and hyperframes-composition-rules were both loaded before HTML authoring. All six scenes verified via vision-check of mid-scene frames.
First iteration (user-requested): add music bed from Discord-uploaded MP3 (saved as .ogg extension), generate Polish version. Music preprocessing (ffmpeg trim + fade + volume) took 3 seconds; bilingual fork took under 2 minutes for string translation + project duplication; second render added another 3 minutes. Total "music + second language" cycle: ~8 minutes including both re-renders.
Two false-negative vision checks caught on first pass (year + time sampling too early) reinforced the "sample at 75% of scene duration" rule now in this skill.
Polish diacritics (ą, ę, ś, ł, ż, ń, ó + capitals) all rendered correctly via Playfair Display + Cormorant Garamond + Great Vibes from Google Fonts without any extra configuration — the HyperFrames font pipeline caches them correctly.