poppy-shorts
Open-source Remotion pipeline for vertical YouTube Shorts.
Product / repo: Lucky9 Labs / poppy-shorts
Primary use case: posting for the Hullscape mech-game channel — youtube.com/@Hullscape
Copyright: Lakshya Bakshi / Lucky9 Labs · MIT (this repo). Remotion itself has a separate company license.
Style target: DaniDev / Megabonk viral energy — smash cuts, whooshes, text pops, impact SFX, comedy of juxtapositions (serious game, goofy process).
The example Short renders from generated color plates. You do not need gameplay footage or SFX binaries to preview or export ExampleShort.
0.2.1 — installable preserve gate: Cursor / Claude Code / Codex hooks mark capture signals and ask “Preserve this work for content?” on Stop. Plugin manifests, package.json, and the Cursor marketplace entry share this version. Version as we go: patch (0.2.x) is the default for incremental ships (hooks, docs, fixes, small skills); minor (0.x.0) only for a bigger feature cut or API-shape change.
What you get
| Piece | Role |
|---|---|
PoppyShort | Reusable 1080×1920 @ 30fps composition. Beats are props. |
ExampleShort | Hullscape demo: smash cut + caption pop + whoosh/impact/comedy slots. |
sfx/ | License notes + empty whoosh / impact / comedy folders. |
public/footage/ | Drop cleared clips/stills (no private assets). |
contentSources | Dynamic list of s3://bucket/prefix/ folders to catalog. |
src/story/ | Ingest → transcribe → select → optional Fish VO → Remotion assemble. |
skills/poppy-journey | Start→finish visual proof (host ShowMe / Playwright); journey JSON + inventory stubs. |
skills/poppy-preserve-content | After merge / sendit: “Preserve this work for content?” |
skills/tag-poppy-clips | Sync inventory, then ask a human for nature + richness / engagement. |
hooks/ + scripts/preserve-gate.mjs | Lifecycle mark + Stop nudge for “Preserve this work for content?” |
A beat is one hard cut: a plate (placeholder / image / video), an optional caption, and optional SFX cues with timestamps.
Quick start
git clone https://github.com/Lucky9-Labs/poppy-shorts.git
cd poppy-shorts
npm install
npm run studio # Remotion Studio
npm run render:example # writes out/example.mp4
Node 20+ is required. First render downloads a headless browser if needed.
Scripts
| Script | Command |
|---|---|
npm run studio | Open Remotion Studio (remotion studio) |
npm run render | Render PoppyShort (starter 2-beat Short) |
npm run render:example | Render the Hullscape ExampleShort to out/example.mp4 |
npm test | Timeline / schema / SFX unit tests |
npm run catalog | List S3 prefixes → public/content-catalog.json |
npm run story | Offline storytelling dry-run → public/story-short.json |
npm run inventory:sync | Upsert S3 clip stubs in inventory/clips.json (tags persist) |
npm run inventory:journey | Upsert a journey package (local GIF / stills or published prefix) |
npm run inventory:tag | Patch richness / engagement / nature on one clip |
npm run lint | ESLint + tsc |
How to add a Short
- Copy the beat list in
src/shorts/example-short.ts(or theExampleShortdefaultPropsinsrc/Root.tsx). - Register a
<Composition>insrc/Root.tsxwith a newid(Studio andnpx remotion render <id>use that id). - Keep
width={1080}height={1920}fps={30}. Duration is computed from beats viacalculateShortMetadata. - For each beat, set:
source—{type: "placeholder", color, label}or{type: "image" \| "video", src}underpublic/caption—{text, tone: "serious" \| "goofy", delaySeconds?}sfx—{src, atSeconds, kind: "whoosh" \| "impact" \| "comedy"}
srcis a path relative tosfx/(for examplewhoosh/cut-01.wav) or anhttps://URL.
- Alternate serious and goofy captions. That juxtaposition is the joke.
- Preview in Studio, then:
npx remotion render YourCompositionId out/your-slug.mp4
Hook in the first 1–2 seconds. Most beats should be 0.8–2.0s. Stay under 60s for YouTube Shorts.
Beat shape (TypeScript)
{
id: "forge",
durationInSeconds: 1.2,
source: {type: "placeholder", color: "#0b0f14", label: "HANGER 04"},
caption: {text: "WE FORGED A MECH", tone: "serious", delaySeconds: 0.06},
sfx: [{src: "whoosh/cut-01.wav", atSeconds: 0, kind: "whoosh"}],
}
Drop licensed audio in sfx/whoosh, sfx/impact, or sfx/comedy. See sfx/README.md for Mixkit, Sonniss GDC, and bfxr. Missing files are skipped; the render still succeeds.
Dynamic S3 content sources
The pipeline does not hardcode a bucket. Pass as many folder prefixes as you need for one Short (gameplay captures + WIP evidence + brand stills):
contentSources: [
"s3://your-bucket/gameplay/",
"s3://your-bucket/wip-evidence",
"s3://your-bucket/brand-stills/",
]
Trailing slashes are optional. src/lib/content-sources.ts also reads CONTENT_SOURCES as a JSON array or a comma-separated list (see .env.example).
# list + merge every prefix into public/content-catalog.json
npm run catalog -- s3://your-bucket/gameplay/ s3://your-bucket/wip-evidence/
# optional HTTPS URLs for private objects (default credential chain)
npm run catalog -- --presign s3://your-bucket/gameplay/
calculateShortMetadata attaches that JSON to composition props when present. Beats can use {type: "catalog", kind: "video", index: 0} and fall back to a color plate if the catalog is empty — ExampleShort stays offline with placeholders when S3 is not configured.
Do not import src/lib/s3-client.ts from Remotion compositions (AWS SDK / Node only). Compositions consume the catalog; the prep script lists S3.
Storytelling (auto-story over time)
v0 is a pluggable pipeline, not a Restart clone. Adapters live in src/story/. Full sketch: docs/STORYTELLING.md.
ingest (S3 prefixes) → transcribe (Whisper/offline) → select beats
→ optional Fish Audio TTS (voice.referenceId) → PoppyShort
{
contentSources: ["s3://bucket/gameplay/", "s3://bucket/wip-evidence/"],
voice: {provider: "fish_audio", referenceId: "your-clone-id"},
transcribe: {provider: "offline"},
narration: "We forged a mech. In a group chat named oops.",
}
voice.provider is fish_audio or offline. The Fish adapter calls the public TTS API (POST /v1/tts + reference_id). Keys stay in FISH_AUDIO_API_KEY — never in git. Without AWS or Fish, npm run story still writes a placeholder Short plan; ExampleShort still renders.
Clip inventory
Editors and agents tag each object’s nature and engagement in inventory/clips.json. Sync / journey upsert stubs; they never wipe scores. The story selector prefers visualRichness + engagement + 0.5 * hookPotential. Full guide: docs/INVENTORY.md.
After you install the plugin on a consumer repo, finishing a feature is: show the journey (/poppy-journey) → optional preserve for Shorts (/poppy-preserve-content) → tag (/tag-poppy-clips). See docs/INSTALL-PLUGIN.md (alias: docs/INSTALL-SKILL.md).
That is how Hullscape (and later other channels) can move from hand-authored beats to “drop a week of captures in S3 + a voice id → cut a Short,” while this repo stays MIT and secret-free.
AWS auth (no secrets in this repo)
The prep script uses the AWS default credential chain:
- Environment (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, optionalAWS_SESSION_TOKEN) AWS_PROFILE/~/.aws/credentials/ AWS SSO- Task / instance role when running on AWS
Copy .env.example to .env for AWS_REGION and CONTENT_SOURCES only. Never commit .env or access keys. IAM needs s3:ListBucket on each bucket (prefix-scoped if you can) and s3:GetObject if you --presign. public/content-catalog.json is gitignored because presigned URLs expire and can leak object paths.
How to render
npm run studio
npm run render:example
npx remotion still ExampleShort out/frame.png --frame=36
npx remotion render ExampleShort out/example.mp4 --codec=h264
Upload out/example.mp4 (or your composition output) to YouTube as a Short. Vertical 9:16 is already set.
Agents
Use this repo when the job is “cut and post a Hullscape Short”, not when the job is “write gameplay code”.
- Do not add private mech-game assets, unreleased builds, API keys, or
.envsecrets. - Start from
ExampleShort/src/shorts/example-short.ts. Keep smash cuts and the serious/goofy flip. - Author beats as data. Prefer placeholders until a clip is public-cleared, then point
source.srcatfootage/...or runnpm run catalogwithcontentSourcesand{type: "catalog", ...}. - Put SFX only in
sfx/<category>/with a license you can defend on a monetized YouTube channel. Leave binaries uncommitted. - Run
npm testafter changing timeline or schema helpers. Runnpm run render:example(ornpx remotion still) before you claim a Short is ready. - Composition ids:
PoppyShort(blank pipeline) andExampleShort(Hullscape demo). New posts get a new id + beat list. - If you only have stills, use
type: "image". If you have a clip,type: "video"withobject-fit: cover. - Caption copy: first three words are the hook. All-caps for serious; the goofy line can stay conversational.
Lucky9 Labs agents should treat poppy-shorts as the shared render library and Hullscape as the first channel that calls it. Do not copy Restart (or other product) voice/Whisper files into this repo — implement VoiceProvider / Transcriber adapters here instead. See docs/STORYTELLING.md.
Install the plugin on the consumer repo (docs/INSTALL-PLUGIN.md). After a feature ships: /poppy-journey (host ShowMe / Playwright — do not vendor a Unity recorder) → /poppy-preserve-content (“Preserve this work for content?”) → /tag-poppy-clips. When the plugin hooks are trusted, Stop/stop does that preserve ask from capture signals (merge/sendit/export/journey) instead of a markdown reminder. Tagging is those skills + inventory:journey / inventory:sync / inventory:tag + a human who watched the clip.
Project layout
src/lib/ timeline, schema, SFX matching, S3 catalog, metadata
src/inventory/ clip inventory merge, tag, rank
src/story/ ingest / transcribe / select / Fish VO / assemble
src/scripts/ `prep-content`, `run-story`, `inventory-*`
inventory/clips.json
src/journey/ journey package, host ShowMe locator, preserve uses
skills/ poppy-journey, poppy-preserve-content, tag-poppy-clips
plugin.json Agent Plugins manifest
.cursor-plugin/ Cursor marketplace wrapper + hooks pointer
.claude-plugin/ Claude Code plugin manifest
.codex-plugin/ Codex plugin manifest
hooks/ Cursor + Claude/Codex preserve-gate lifecycle
scripts/preserve-gate.mjs mark pending flag / Stop followup (no deps)
docs/INSTALL-PLUGIN.md how consumers install the full plugin
docs/INVENTORY.md
src/components/ smash flash, caption pop, beat media, SFX + VO slots
src/compositions/ PoppyShort (prop-driven)
src/shorts/ example beat list (Hullscape)
src/Root.tsx composition registry
docs/STORYTELLING.md
sfx/ SFX kit + licensing (no binaries)
public/footage/ optional cleared clips
License
MIT © 2026 Lakshya Bakshi / Lucky9 Labs.
This repository’s TypeScript, React compositions, and docs are MIT. Third-party SFX and footage keep their licenses. Remotion is not MIT for every company — read remotion.dev/docs/license before you ship this inside a larger org.