Aller au contenu

launch-kit: launch and update videos for your own product

Cette page n’est pas encore traduite : voici la version anglaise.

Use when a maker wants to announce a product or a new version: a short demo video of the real product, a looping GIF for the README, one short clip per feature for X / LinkedIn / Shorts / TikTok / 小红书, Product Hunt gallery stills and an OG image, post copy per platform in English and 中文, and a posting schedule. It is not for a talking-head promo of something you made (that is promo-recut) or a concept explainer (explainer).

Inputs → Outputs. A product (name, one-liner, site), release notes (a CHANGELOG section, a git range of Conventional Commits, or pasted notes), the product itself to record (a URL or an Electron app, plus a shot list), or screenshots / recordings you already have, and the brand (logo, icon, colours, fonts) → kit/:

Folder What
demo/ demo-<lang>-16x9 / 9x16 / 1x1.mp4, 30–60 s: title card, one scene per feature (kinetic caption + the product window, the camera punching in on each click and typed field), logo end card, a soft music bed; a .cover.jpg per video
readme/ loop-<lang>.mp4 + .gif, 15 s, seamless (the last frame crossfades into the first)
clips/ <feature>-<lang>-1x1 / 9x16.mp4, 10–20 s each, one feature + end card
stills/ ph-gallery-1-hero.png, ph-gallery-N-<feature>.png (1270×760), og-1200x630.png
copy/ COPY.md (Product Hunt fields, the launch post and every clip post, international platforms first), <platform>.<lang>.md
schedule/ SCHEDULE.md + schedule.json: launch day = the demo everywhere, then one feature clip per weekday
firstpass.json vstudio.firstpass on every video (+ <video>.firstpass.md)

Everything content-specific lives in ONE file, launch.config.yaml; start from $VSTUDIO/workflows/launch-kit/examples/launch.config.example.yaml, which documents every key.

Pipeline ($VSTUDIO = repo root; LK="python3 $VSTUDIO/workflows/launch-kit/scripts/launch_kit.py")

Section titled “Pipeline ($VSTUDIO = repo root; LK="python3 $VSTUDIO/workflows/launch-kit/scripts/launch_kit.py")”
  1. Draft the features from the release.
    Terminal window
    $LK features launch.config.yaml # release.changelog | release.git {repo, range} | release.notes
    vstudio.launch.features keeps feat / fix / perf / breaking commits (chores, ci, docs, tests, refactors never become features) or the bullets of a Keep-a-Changelog section (“Mac app:” style intro bullets contribute their nested bullets). Pick 3–6. For each, write ONE on-screen line (≤ ~50 characters in 16:9; the caption is fitted and wrapped by measuring the real font, and the build fails on a line that cannot fit) and mark the word to highlight with 【】. Captions say what the viewer gets, not what the code does.
  2. Record the product. Write shots.yaml (one shot per feature: the actions to perform, 5–12 s each) and run
    Terminal window
    $LK capture launch.config.yaml # = node $VSTUDIO/workflows/launch-kit/scripts/capture.mjs --shots shots.yaml --out capture ...
    capture.mjs drives the product with Playwright (a web URL in Chrome, or an Electron app with a throwaway profile), at 2× device scale, records the page with CDP screencast frames assembled at a constant 30 fps (not Playwright’s low-bitrate recorder), draws a smooth cursor with a click ripple, and types at a human pace. It writes capture/<shot>.mp4, a still per shot, and capture/shots.json with the focus boxes: where and when each click / typed field happened. An Electron app gets its throwaway profile through capture.env / capture.env_json (--env-json); examples/reelfold/ is a full example (Reelfold recording itself: seed.py builds a temp profile with real engine data on a synthetic talk, shots.yaml drives Home → plan → project → transcript → chat → publish). The camera uses them for the punch-ins. Seed the product with sample data that is safe to show (no real names, emails, handles or other people). Already have screenshots? List them under capture.images with a focus box; the camera then makes a slow move to it.
  3. Build + render.
    Terminal window
    $LK build launch.config.yaml && (cd kit/build/demo-en-16x9 && npx hyperframes lint)
    $LK render launch.config.yaml [--only demo|loop|clip|<feature>] [--quality draft]
    One HyperFrames project per cut × language × canvas (kit/build/<name>/index.html, plan.json, cues.json). Layout per canvas keeps every caption inside the safe area of every platform that canvas is posted to (vstudio.platform safe boxes: 16:9 X / LinkedIn / YouTube / B站, 9:16 TikTok / Shorts / Reels / 小红书 / 抖音, 1:1). The look is the brand on top of a vstudio.theme preset (editorial by default: paper, ink, one accent, a marker sweep under the highlighted word); fonts are the brand’s Latin fonts, Chinese uses Noto Serif / Sans SC subset to the characters on screen. Delivery: BT.709 tags + faststart; loudness to −14 LUFS only when there is audio. Render one draft first and look at frames mid-scene and mid-transition before the full set.
  4. Stills, copy, schedule, check.
    Terminal window
    $LK stills launch.config.yaml # PH gallery + OG from the shot stills, rendered at 2x then resampled
    $LK copy launch.config.yaml # vstudio.publish.platform_post per platform x language -> copy/COPY.md
    $LK schedule launch.config.yaml [--apply]
    $LK check launch.config.yaml # firstpass, format "launch"
    Copy comes from the config’s post block (and features[].post), laid out per platform by the shared post engine (title fields, hashtag caps, X weighted 280, 小红书 / B站 conventions) and checked with vstudio.platform.check_text; without a post block the routed model writes it (llm task copy), and with no model routed the step fails instead of inventing text. --apply adds the schedule to the publish calendar (vstudio.project.pubcal) as planned posts for the accounts in schedule.accounts. Nothing is ever posted. $LK all launch.config.yaml runs steps 3–4.
  • No voice by default; a license-safe music bed. music: auto (default) takes a built-in bed from the shared library (vstudio.music: generated in code, free for any use; mood tech for a product video). A mood name picks another built-in bed, none turns it off, and your own track needs music.file + music.license. A TTS voice-over (voiceover.enabled, one vo line per feature) is labelled “AI voice” on screen; say so in the post too. With audio the delivery is loudness-normalised to −14 LUFS.
  • Real product, real data you may show. Numbers in the gallery facts must be ones you can stand behind; sample data gets gallery.note (“Sample data”).
  • International platforms first, Chinese after, everywhere platforms are listed (copy, schedule, captions).
  • Red is not a brand accent here. brand.warnings flags a reddish accent and low ink/paper contrast.
  • Demo length: scenes are 4.5–9 s; the total is squeezed into demo.max_seconds (never under 4 s per scene) or padded by holding each shot’s last frame up to demo.min_seconds.

Real recordings wait: a plan being made, a render finishing. ffmpeg freezedetect (a very low noise threshold, so typing still counts as motion) finds the frozen stretches of each shot; they play at 4× after a 0.35 s lead-in at normal speed, the actions play at the feature’s rate. When the scene is still shorter than the ramped take, every segment speeds up evenly. Each segment is its own back-to-back clip in the composition. Detection is cached next to the shot (<shot>.mp4.idle.json); ramp: false on a feature turns it off.

The shot is cover-fitted into the product window. Each focus event becomes a punch-in (zoom ≤ the point where the acted-on element fills ~60 % of the window, ≤ 2.6×) that starts 0.55 s before the action, holds ≥ 1.5 s, and pulls back when the next action is far off. On a narrow window (9:16) the camera follows the action instead of showing the whole screen. Events closer than 0.6 s share one move. Translations are clamped so the shot always covers the window.

  • Record at 2× and extract video frames as PNG in the render (--video-frame-format png): UI text turns to mush with JPEG frame extraction or Playwright’s default recorder.
  • HyperFrames: never put a CSS transform on an element GSAP moves (gsap_css_transform_conflict); the camera’s first pose is a tl.set at 0. Overlay start states are opacity: 0 in CSS.
  • A loop needs its last frame to be its first: the last scene crossfades into a still of the first scene’s opening frame, and the first scene has no entrance animation.
  • Fit captions by measuring the actual font (PIL on the woff2), not by character counts: Newsreader and Noto Serif SC differ by 40 % in width at the same size.