marketing-content
Make any vibekit or qt314 promotional asset — tweets and threads, promo videos of the TUI, title cards, OG/link-preview images, quote cards, lore dossier cards, qt314 animations, GIFs, and TUI screenshots — in the vibekit voice and on the brand palette. Use whenever asked to writ
Install
npx skills add https://github.com/initlabsai/vibekit/tree/main/skills/marketing-content
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install initlabsai-vibekit@llmmart
git clone https://github.com/initlabsai/vibekit.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole initlabsai/vibekit collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
vibekit marketing content
Everything that goes out in public: the copy, the images, the video. One voice across all of it, because a follower sees the tweet and the card in the same scroll — if they sound like different companies, the brand is doing nothing.
Pick your asset
Read the linked file before making the thing; each has the recipe and the failure modes.
| you want | read | make it with |
|---|---|---|
| tweet, thread, dev diary, announcement, reply | references/voice.md |
— |
| promo or demo video of the TUI | references/video.md |
scripts/*.tape + ffmpeg |
| title card between video segments | references/cards.md |
scripts/make-card.sh |
| OG / link-preview image, quote card | references/cards.md |
PNG=1 scripts/make-card.sh |
| lore dossier card (the /lore series) | references/cards.md |
scripts/make-lore-card.sh |
| qt314 animation, GIF loop | references/qt314.md |
scripts/qt314-*.py |
| a still of the TUI | references/cards.md |
a one-line vhs tape |
| colors, fonts, sizes | references/brand.md |
assets/card.css |
Everything visual is HTML → headless chromium → png, then ffmpeg if it moves.
That is one pipeline with different templates, not four pipelines, and it is
why a still and a video card can't drift apart. Never hand-roll an ffmpeg
drawtext card because it looks quicker — it is, and it looks it.
Preflight
Check before starting, not after a failed take:
vhsandttyd— vhs fails every recording with "ttyd is not installed" and the message arrives after you've written the whole tape.chromium,ffmpeg,python3— the card and qt314 renderers.- JetBrains Mono. Resolve the family with
fc-match "JetBrains Mono"and use the name it reports (currentlyJetBrainsMono Nerd Font); an assumed family name silently falls back to sans-serif and the card stops looking like a terminal. - Recording live mainnet also needs the keystore daemon
(
bun run apps/cli/src/index.ts keystore status), and AI segments need the configured agent provider reachable (probe zs-proxy's/v1/models).
Finished assets go in out/ at the repo root — gitignored, and they stay that
way. Never commit a video. Work files (tapes, frames, QA stills) go in a temp
dir. Keep a silent master of every cut in out/silent/ so a track can be
swapped without re-encoding video.
Voice, in one paragraph
lowercase, short, one idea per post. Say the mechanism, not the adjective —
"the keys never left the OS keychain" beats "secure key management", because
the mechanism is the thing that's actually impressive. Alpha is stated as a
feature, not apologized for. Kaomoji are vocabulary, not decoration, and
qt314 is a character with her own register who signs her own posts. No
hashtags, no "please check out", no visible effort to meme — if the joke needs
explaining, cut it. references/voice.md has the corpus, the sigils, the
thread format, and the drafts that get rejected; read it before writing copy,
including image copy.
Brand, in one paragraph
Two hues and a ground: amber is structure and figures, teal is alive and
touchable, red is danger only and never decoration. The palette's source of
truth is apps/tui/src/theme.ts, mirrored for rendering in assets/card.css
— use the CSS variables rather than pasting hex, so a theme change doesn't
leave stale cards behind. Everything is monospace, title cards included, so
the graphics read as part of the terminal. references/brand.md has the
values, sizes, and card grammar.
Reference images
When an asset needs a photograph (the lore portraits are the current case), ask the user for a royalty-free image rather than sourcing one. Save the credit next to the file when the license asks for attribution.
Files (vibekit)
-
assets
-
card.css 1.4 KB · in bundle
-
lore-portrait.png 202.1 KB · in bundle
-
-
references
-
demos
-
apps-call.md 3.3 KB
# ARCHIVED SHOOT — demo: apps page → method line → signed call > Recorded once, kept as a worked example of beat breakdown. The app IDs, > paths (`~/vk`), and artifacts are from that day and will not exist now — > read this for the *shape* (hide setup → reveal the artifact → type slowly → > approval modal → confirmation), not as a script to re-run. The beat: a deployed ARC-56 contract shows up on its own, you call a method by typing one line, and you sign exactly the bytes you see. ~25s of tape. ## setup (hidden in the tape) - localnet up, keystore daemon running with a funded account (`SMOKE1`). - a project dir with `artifacts/<Name>.arc56.json` and at least one deploy — `~/vk` has `HiWorld` (`hi(string)string`, app 1018). Fresh instance: `cd ~/vk && npm run deploy` (stamps the deployer note, so the card detects it). - run vhs **from that project dir** so the spec scan finds the artifacts: `Set WorkingDirectory` is not enough for the scan — `cd ~/vk` before `vhs`. - launch: `bun run /home/gabe/Code/@initlabs/vibekit/apps/tui/src/index.tsx` inside `Hide` … `Show`; wait on `Ask anything`. ## the tape, beat by beat 1. `Type "apps"` `Enter` → `Wait+Screen /MY APPS/`. Show the card: `● [1] HIWORLD ARC56 … LOCALNET`, hero `#1018`, `creator … · this account`, the `greeting box · AVMString` key, the method row. Sleep 2s. 2. `Type "1"` (card lights, `open ▸`, methods numbered) — Sleep 1s — `Type "1"` (method line opens: `hi(name: string) ❯`). Sleep 1.5s. 3. `Type@120ms 'name="world"'` — the typing *is* the demo; keep it slow. Sleep 1s. `Enter`. 4. `Wait+Screen /APPROVE THIS CALL/`. The modal: graph, then `HiWorld.hi → app 1018` with `name "world"` on its own line, `SIMULATED OK`, footer "Composed from what you typed — these decoded bytes are exactly what gets signed." Sleep 3s — this is the money frame. 5. `Enter` → `Wait+Screen /Call confirmed on-chain/`. Then `Tab`, `Down@8ms 200` to reach the TRANSACTION card: `method hi`, `name world`, `return Hi, world`. Sleep 3s. End. Optional second segment (gacha, re-roll until clean): in the composer `Type 'call hi with "again" on HiWorld'` `Enter` → same modal via the agent. Same wait regex. Shows the spec is the agent's tool too. ## cards + copy - open: `your contracts, in the terminal` / `arc-56 in. typed calls out.` - between segments (if two): `same spec, same modal` / `type it, or ask.` - close: `vibekit explore` / `sign what you see` ## tweet angles - plain: `vibekit explore now finds your deployed contracts and turns the arc-56 spec into a method line. type the args, sign the bytes you see.` - gag: `lora has forms. we have one line. hi(name: string) ❯ "world"` - benefit: `deploy from your project, open ^2, your app is already there — creator, state keys, methods. call one. the approval card shows the decoded args before you sign.` Recommend the plain one; the gag second if the thread has room. ## gotchas - `^2` doesn't pass through tmux/vhs reliably; `apps` + Enter does. - the modal's kicker is `APPROVE THIS CALL?` for one app call — wait on that, not `GROUP`. - keys go to the method input once it's open; `o`/digits are inert there. - if a write is already awaiting approval the next one is refused with a note on the card — deny (`Esc`) before re-rolling.
-
-
brand.md 4.1 KB
# Brand ## Palette Init Labs amber-phosphor. Two hues and a ground: **amber is structure and figures, teal is alive and touchable, red is danger only.** A card that tints something teal because teal is pretty has broken the system — teal means the thing responds. The source of truth is `apps/tui/src/theme.ts`. It is mirrored for rendering in `assets/card.css` as CSS variables; use the variables, not pasted hex, so one edit moves every card. | role | var | hex | |---|---|---| | ground (terminal background) | `--ground` | `#0a0b0e` | | card surface | `--surface` | `#111318` | | brass — structure, rules, kickers | `--brass` | `#c4a06a` | | hero — the payoff, one per card | `--hero` | `#ffb454` | | teal — signal, live, touchable | `--teal` | `#6fd3d3` | | text — warm neutral | `--text` | `#e9e1d4` | | muted | `--muted` | `#8e8476` | | dim | `--dim` | `#605c56` | | danger, only | `--danger` | `#e07c7c` | **Sample, don't assume.** When a graphic sits against real footage, pull the background out of an actual rendered frame rather than trusting the theme value — terminal emulators and encoders both shift it slightly, and a card that is 2% off the footage shows a visible seam at the cut. ## Type JetBrains Mono, everywhere, including title cards — the graphics should read as part of the terminal rather than as slides about the terminal. Resolve the installed family with `fc-match "JetBrains Mono"` and use the name it reports (currently `JetBrainsMono Nerd Font`). A guessed family name fails silently into sans-serif, which is the single most common way a card ends up looking wrong for reasons nobody can name. The kaomoji live in this font too. Measure them in a rendered frame, never by eye in a text editor — the wide faces (`(>'-')>`, `\(^▽^)/`) run into adjacent copy at sizes that look fine in source. ## Card grammar The house style, shared by every rendered graphic: - a **brass top rule** (8px) across the full width - a **spaced brass kicker** in caps — `2026 // TRANSMISSION RECEIVED`, `PRIOR ART DIRECTORATE // PERSONNEL 001` - **one statement** in warm neutral, with the payoff line in hero amber. One hero line per card; a second one means neither is the payoff. - **CSS scanlines**, low opacity, deliberately visible - one visual idea per card Keep archival/CRT treatment (hairline frames, scanlines, non-semantic telemetry) on the cards, not on live TUI footage. The footage is already the real thing; dressing it up reads as not trusting it. And check the effect *after* final encoding — compression erases subtle scanlines, so what you tuned at source may not survive to the upload. ## Sizes | asset | size | notes | |---|---|---| | TUI recording | 1400×1150 | the tape's terminal, not a video size | | video, 16:9 | 1920×1080 | title cards match the segment they cut against | | tweet loop | 1200×675 | GIF only — odd height; pad to 676 for mp4 | | OG / link preview | 1200×630 | `PNG=1` mode of `make-card.sh` | | lore dossier card | 1600×900 | `make-lore-card.sh` default | Two sizing traps, both cost a re-render: - **libx264 refuses odd heights.** 1200×675 will not encode. Pad to 676 or shoot 1920×1080. - **Match the card to the tape.** Letterboxing a 1400×1150 terminal into a 16:9 card throws away half the frame. Pass `W=1400 H=1150` to `make-card.sh` when cutting against a tape of that size; the type scales with the frame so it isn't a blown-up 1920 card. ## Music Four tracks, in `apps/website/public/music/`, already credited in `apps/website/src/scripts/music.ts`. Use them; don't go find a new track. `scripts/add-music.sh` lays one down — trims to the cut, fades in 0.5s, fades out over the tail, ducks to 0.85. Start past a quiet intro rather than opening on nothing. These offsets were measured, not guessed (`ffmpeg -hide_banner -ss N -t 6 -i t.mp3 -af volumedetect -f null /dev/null` — needs the default log level; `-v error` hides the summary): | track | use | |---|---| | `retro` | hot from 0 — short clips | | `neon-drive` | settles at 30s — feature reels | | `neon-horizon` | quiet intro, start at 15s — long story cuts | | `80s-retro` | dips around 75s; don't land an ending there | -
cards.md 5.1 KB
# Cards and stills Every graphic here is HTML → headless chromium → png, then ffmpeg only if it moves. Same shell (`assets/card.css`), same grammar (`brand.md`), so a video card and a link preview are visibly the same object. The copy on a card is copy — `voice.md` applies to it exactly as it does to a tweet. ## Title cards (video) ```sh make-card.sh out.mp4 "KICKER" "line one" "amber line two" [seconds] [center|split] W=1400 H=1150 make-card.sh ... # match the tape you're cutting against GLITCH=0 make-card.sh ... # disable the glitch flash ``` The 4th argument renders in hero amber under the 3rd — that's the payoff line. Pass `""` to omit it. House shape: brass top rule, spaced brass kicker (`2026 // TRANSMISSION RECEIVED`), one statement in warm neutral with the payoff in amber, scanlines. One idea, ~2.5s. The statement glitches like the site hero — two clipped copies of the text, teal and hero amber, offset sideways and flashed for a frame or two. CSS animation can't be screenshotted, so each glitch state renders as its own frozen PNG and cuts in on chosen frames. **Typewriter cards must be readable at every frame.** If a card types itself in, keep the copy compact (3–5 short lines), reveal at human reading pace, and keep every completed line on screen while the next one types — do not clear between events or simulate typing with brief discontinuous captions. Hold the finished card ~1s before cutting. Inspect one in-progress frame and one completed frame *in the encoded master*, not in the source PNGs. ## OG images and quote cards Same script, `PNG=1`, which stops at the still: ```sh PNG=1 W=1200 H=630 make-card.sh og.png "VIBEKIT // ALPHA" "from prompt" "to mainnet" ``` 1200×630 is the link-preview size — it's what `apps/website/public/og-image.png` is, and what `Landing.astro` declares in `og:image` / `twitter:image`. Scrapers don't resolve relative paths, so the site passes an absolute URL; if you replace the OG image, replace the file rather than the URL. A quote card is the same thing with the quote as the statement and the payoff clause in amber. Any size works; 1200×630 travels best. ## Lore dossier cards The /lore series. A personnel record from the 2036 transmission archive: the quote is the headline, the name attributes it underneath, telemetry sits below as a muted strip. ```sh NAME="Dr. Vera Solano" ROLE="DIRECTOR, RETROCAUSAL PAYLOAD" \ SUB="signed the order to send VibeKit backwards." \ QUOTE="I sent one sentence|into a decade of silence,|and a stranger finished it." \ META="CHANNEL=entangled pair 004;PAYLOAD=one (1) prompt" \ make-lore-card.sh out.png ``` `QUOTE` splits on `|` and the last line renders in hero amber — put the payoff there. Optional: `KICKER`, `DATE`, `CAP`, `FOOT_L`, `FOOT_R`, `COLS`, `W`/`H`. Pair with a lore-register post (`voice.md`) — played straight, never winking. ### The portrait A photograph rendered as ASCII: `scripts/lore-portrait-prep.py` to prepare, then `scripts/lore-ascii.py` to sample it. Tone comes from the character ramp (`. , : ; + = x X $ & @`) with an amber tint riding the same value — colour alone cannot separate tones at glyph density. **It has to be a photograph.** Three rounds went into hand-drawn vector faces — flat facets, then soft gradients — and both failed identically: smooth shading gives the ramp almost nothing inside the skin, so the face flattens to one glyph and reads as a blob. Photographic micro-variation is the entire mechanism. Prep matters as much as sampling. Pull luminance from the **red channel** — a cool studio backdrop and warm hair collapse to the same grey in a standard luma conversion. Lift the black point until the backdrop is truly black, or it renders as a field of punctuation competing with the face. Everyone in the Directorate sits for the same portrait, so one face serves the whole series and the name carries the character. Ask the user for a royalty-free photograph rather than sourcing one; keep the credit beside the file if the license asks for attribution. Current source is `assets/lore-portrait.png`. ## TUI stills A screenshot of the real TUI is a one-line tape — worth having, because a still from the actual product beats a mocked one and takes a minute: ``` Output frame.png Set Width 1400 Set Height 1150 Set FontSize 20 Set Padding 12 Set Theme { "background": "#0a0b0e", "foreground": "#e9e1d4", "cursor": "#ffb454" } Hide Type "bun run apps/tui/src/index.tsx" Enter Wait+Screen /Ask anything/ Type "31566704" Enter Wait+Screen /USDC/ Show Screenshot frame.png ``` `Screenshot` filenames can't start with a digit and absolute paths fail — run vhs from the output dir. Everything under `Hide` is setup the still shouldn't show. ## GIFs For tweet loops, render frames then convert with a generated palette — `paletteuse` without `palettegen` produces visible banding on the amber: ```sh ffmpeg -y -i frames/%05d.png -vf "palettegen" palette.png ffmpeg -y -i frames/%05d.png -i palette.png -lavfi "paletteuse" out.gif ``` Post an MP4 alongside for X, which handles it better than a large GIF. See `qt314.md` — `scripts/qt314-loop.py` is the working example at 1200×675/12fps. -
qt314.md 3.1 KB
# qt314 pieces Her promos are rendered, not recorded: one HTML page per frame, drawn from a timeline in Python, screenshotted by headless chromium, joined by ffmpeg. Same CSS vocabulary as the site (ground, grid, brass rule, scanlines, JetBrains Mono), so a qt314 piece cuts against real TUI footage without a seam. Because the seed is the frame index, a re-render is byte-identical. Iterate freely. ## The scripts - `scripts/qt314-boot.py` — the 20s boot sequence: the transmission types in, the wordmark resolves out of `░▒▓`, her face assembles from kaomoji static, first words, one question, end card. `python3 qt314-boot.py <framesdir>`, then encode at 24fps, score with `retro` from 0. - `scripts/qt314-loop.py` — the 9s tweet loop, 1200×675 at 12fps. GIF via palettegen/paletteuse, plus an MP4 for X (pad to 676 — libx264 refuses the odd height). - `scripts/qt314-ecosystem.py` — the 35s read-tools promo: chart, web, quote, markets as cards. 1920×1080 at 24fps. `python3 qt314-ecosystem.py <framesdir> <candles.json>`, score with `neon-drive` from 30s. Candle/quote/headline data is captured the day of rendering, so it ages — re-capture rather than re-using an old JSON. ## How her face assembles The beat worth keeping. It is not a morph — it is per-character noise resolving. For ~2 seconds each of the five glyphs of `(^‿^)` is, independently, either its final character or a random pick from a pool of *other kaomoji fragments and block glyphs* (`(・‿・)(´▽\`)(>_<)(x_x)<('-'<)\(^▽^)/░▒▓█▌▐│┃╱╲`). The chance of showing the final character is `k²` where `k` runs 0→1 across the window, so it stays chaos for most of the duration and snaps together at the end. Colour flips between teal and hero amber with probability `0.5·(1−k)`, so the flicker dies as she settles. The pool being made of *her own possible faces* is the whole trick — the noise reads as her trying on expressions rather than as static. Swap in generic glyphs and the beat dies. The same mechanism, on `░▒▓` only and per character, resolves the block wordmark. Both live in `qt314-boot.py`. ## Her expressions come from the product The canonical mood→face set lives in `apps/agent/src/features/profile/companion.tsx` — five moods, four faces each, plus a blink. These render scripts hold an older subset (no `squint`, fewer variants per mood). When adding a face to an animation, take it from the component so she stays one character across the product, the promos, and the copy; `voice.md` has the table. The mood itself is derived from the feed in the product, never chosen by a model. Animations should honour the same logic — she looks like she's working because work is happening in the shot. ## Layout lessons - Wide faces (`(>'-')>`, `\(^▽^)/`) need a smaller size or they run into the copy. - The kaomoji are in the mono font — measure them with a rendered frame, never by eye in source. - Put the wordmark in the header row, never beside prose. - Check the right edge of every typed line at its longest frame, which is not the last frame if the line is cleared. -
video.md 6 KB
# Promo and demo videos Record the real TUI with `vhs`, cut in HTML title cards, concat and score in one ffmpeg pass. The footage is always the real product — no mockups, no sped-up fakery beyond honest jump cuts. Working tapes: `scripts/segA.tape` (paste IDs → cards, no AI) and `scripts/segB.tape` (english → the agent wakes up). They're a starting shape, not a template to fill in blindly. ## Storyboard first A promo is beats, not features. The shape that works: 1. **open on the payoff.** Hide the launch, the `network mainnet`, the waiting. `Hide` … `Show` around setup so the video opens on the interesting frame. 2. **one mechanic per segment.** A segment shows a single thing working. 3. **card between segments** — states the idea the next segment proves. 4. **close on the claim**, product name and a short line. Record each segment as its own tape. Re-rolling one segment then doesn't touch the others, which matters a lot once AI segments are involved. `references/demos/apps-call.md` is a fully storyboarded shoot (the ARC-56 method-call demo) — read it as an example of the beat breakdown, not as a recipe to re-run; its app IDs and paths are from that day. ## Recording Run vhs **from the directory the tape needs** — for TUI recordings that's usually the repo root, and for anything that scans a project (ARC-56 specs, deployed apps) it must be that project dir. `Set WorkingDirectory` does not affect the scan; `cd` before `vhs`. Match the terminal background to the brand ground or the cut between a shell segment and a TUI segment visibly jumps: ``` Set Theme { "background": "#0a0b0e", "foreground": "#e9e1d4", "cursor": "#ffb454" } ``` **Wait on text, not on time.** `Wait+Screen /regex/` beats `Sleep` for anything network-dependent. Wait on strings you have verified appear — card kickers like `ASSET`, `APPLICATION`, `CONFIRMED`, or the composer's idle hint `Ask anything`. Two ways that bites: - `Wait+Screen` matches the *visible buffer*. A kicker inside a tall card that renders below the fold never matches. Prefer a footer or composer string that is always on screen. - **A modal you didn't know about looks exactly like a hang.** The Explorer's explain flow opens a cost-approval modal (`EXPLAIN THIS CONTRACT?` / `[enter] run`); a tape that never presses Enter sits at `working…` until vhs times out. When a recording stalls, re-run with no waits and a fixed `Sleep`, then look at the last frame. Guessing costs takes; the diagnostic costs one. **Jump-cut long waits.** For an AI response: show ~3s of "Working…", then `Hide` → `Wait+Screen /Ask anything/` → `Show`. Dead air kills the pace, and nobody believes the wait was the point. **Scroll to the bottom, not a fixed amount.** Reply length varies wildly between rolls. `Tab` (focus feed) then `Down@8ms 400` — extra presses at the bottom are no-ops, so the final frame composes itself. A fixed scroll lands somewhere different every take. **Type at human speed when the typing is the demo.** `Type@120ms` for the one line the viewer is supposed to read; `Set TypingSpeed 6ms` for pasted IDs nobody reads. **Time cuts to informational value.** Hold a confirmation long enough to read the decisive status, then move. Don't leave a resolved screen up just because there's runtime to fill. Parser gotchas: `Screenshot` paths can't start with a digit, and absolute paths fail — run vhs from the output dir with relative names. ## AI segments are a gacha Every roll differs. It may dump raw state, 404 on a deleted entity, or confidently speculate (a "rekey scam" that isn't, a wrong protocol name). Treat a take as unusable until you've read it: ```sh ffmpeg -y -sseof -1 -i segB.mp4 -frames:v 1 -update 1 segB_last.png ``` Read the prose in that frame. Re-roll until it's promo-safe. Pick demo entities that still exist on mainnet — curl the indexer first — so tool calls can't 404 on camera. **Never tell the model to skip tools to save time.** It says so on camera ("you asked me to answer without tools, so I'll go from what a standard file contains rather than reading it") and the beat dies. Let it read the file; that *is* the demo. Good demo IDs: asset `31566704` (USDC), app `1002541853` (Tinyman v2), txns from `packages/vibekit/test/recorded/mainnet-graph-corpus.json`. ## Driving `claude` in a tape Three specific gotchas: - First launch shows a trust prompt and an MCP-enable prompt. Send an `Enter` for each before typing, or your prompt lands in a dialog and the turn never runs. - The turn-complete footer verb is randomised (`Crunched for 9s`, `Cooked for 9s`, `Worked for 9s`). Wait on `/ for [0-9]+s/`, never on one verb. - `unset CLAUDE_CODE_CHILD_SESSION` or a nested session prints a transcript warning over the footage. ## Assembly Title cards are HTML — `scripts/make-card.sh`, see `cards.md`. Match the card size to the tape size. Concat and score in one pass. TOTAL is the sum of all clip durations (`ffprobe` each); audio fades out over the last 3s, video fades to black over 0.74s: ```sh ffmpeg -y -i card0.mp4 -i card1.mp4 -i segA.mp4 -i card2.mp4 -i segB.mp4 \ -i music.mp3 \ -filter_complex "[0:v][1:v][2:v][3:v][4:v]concat=n=5:v=1:a=0,fade=t=out:st=<TOTAL-0.74>:d=0.74[v];\ [5:a]atrim=0:<TOTAL>,afade=t=in:d=0.5,afade=t=out:st=<TOTAL-3>:d=3,volume=0.9[a]" \ -map "[v]" -map "[a]" -c:v libx264 -crf 20 -preset slow -pix_fmt yuv420p \ -c:a aac -b:a 160k -movflags +faststart <repo-root>/out/<name>.mp4 ``` Or keep the video silent and score it separately with `scripts/add-music.sh` — which is the better habit, since it leaves you a silent master in `out/silent/` and lets you swap tracks without re-encoding video. Track offsets are in `brand.md`. ## QA before you call it done Verify frames, not vibes. Extract stills across the whole cut and actually look at them: ```sh for t in 1.1 3.2 8 15 20 30; do ffmpeg -y -ss $t -i out/<name>.mp4 -frames:v 1 -update 1 qa_$t.png done ``` Check: no seam at card/footage cuts, the AI prose is defensible, no text runs off the right edge, scanlines survived encoding, and the last frame holds long enough to read. -
voice.md 7.9 KB
# The voice Applies to everything public: tweets, threads, dev diaries, replies, the copy on a title card, the line under a lore quote, release notes. Same voice in an image as in a post — a follower meets both in one scroll. ## Two speakers **vibekit** — the project. "we". Dry, confident, technical. Talks about what shipped and how it works. Ships the lore. **qt314** — the agent. She speaks in first person, uses kaomoji as expression, and signs off `• qt` or `• qt314`. She's helpful, a little needy, aware she's in alpha. Her `//` asides are her inner voice. > // qt misses @Real_monko > // qt wishes to just be > • qt314 Never blur them. vibekit does not use `(♥‿♥)`; qt314 does not write release notes. When qt is the speaker the whole post is hers, sig included. ## Mechanics **lowercase.** Sentence case reads as a press release. Caps are reserved for structural labels (`// DEV DIARY`), archival framing (`[SYSTEM LOG]`), and the occasional deliberate shout (`LED'S GO!!!`) — which works precisely because everything around it is quiet. **fragments.** One idea per line, one idea per post. Line breaks do the work that punctuation would. > AI is optional. > > vibekit explore speaks two languages: base32 and english. only one of them > wakes up the AI. > > and it asks before spending your tokens. **mechanism, not adjective.** The impressive thing is always the specific thing. Never "secure signing" — say what happens: > the keys never left the OS keychain - the model asked a local daemon for a > signature and got one. **alpha is a feature.** State it plainly and turn it into an ask. Never apologize, never hedge with "we're working hard to". > alpha. it will break. that's the point - tell us where. **disclaimers are blunt and funny.** Legalese-hedging reads as hiding something; a real disclaimer told straight reads as confidence. > DISCLAIMER: not an audit from an auditing firm. if you want the legal right > to say "but we were audited" after getting drained anyway, that runs > 20k–100k and six weeks. **memes land by confidence.** Never signposted, never explained, never hashtagged. A whole tweet can be a bit: > (••) hey > ( ••)>⌐■-■ > (⌐■_■) how you doin > > btw you can filter and sort stuff. > never gonna let you down > (sneak peek at the new look) If the joke needs a setup line to land, it isn't landing. Cut it. ## Sigils The typographic vocabulary. These are load-bearing — they're how a post reads as vibekit before anyone reads the words. | sigil | means | example | |---|---|---| | `▎` | a fact block, or qt speaking | `▎ turns are on the house while it's alpha` | | `// ` | an aside, or a section label | `// DEV DIARY` · `// qt wishes to just be` | | `[LABEL]` … `[/LABEL]` | archival / system framing | `[SYSTEM LOG: ARCHIVAL ENTRY]` | | `→` | a step, or a link | `→ type /buy` | | `• qt` | qt314's signature | closes a post she's speaking in | | `👇` | the link is below | one per post, at the end | ## Kaomoji Vocabulary, not decoration. Her faces are a **readout of her state**, not a style choice — the product derives her mood from the feed, never from a model, so she's honest about exactly as much as the cards are. Copy that gives her a face contradicting what she's doing breaks that. The canonical set is `apps/agent/src/features/profile/companion.tsx` — read it rather than this table if they ever disagree, because that file is what actually renders. Five moods, four to six faces each; the product steps through the variants so a row keeps its face: | mood | faces | |---|---| | calm / reading | `(^‿^)` `(・‿・)` `(´▽\`)` `(◕‿◕)` `( ˆωˆ )` | | thinking | `(・・?)` `( ̄ω ̄;)` `(˘︹˘ )` `(°ロ°)` `(¯ . ¯)` | | working | `(>'-')>` `<('-'<)` `^('-')^` `v('-')v` — Kirby's dance, one step per tool call | | bright / found it | `\(^▽^)/` `(★‿★)` `(⌒▽⌒)☆` `(っ^▿^)۶` `(^_^)v` | | squint / unimpressed | `(¬_¬)` `(>_<)` `(x_x)` `(-‸ლ)` `╮(╯-╰)╭` `( ¬‿¬ )` | | blink (idle) | `(-‿-)` | > ▎ (^‿^) reading (>'-')> working (^▽^)/ found it **In copy she's wider than she is in the product.** The component shows state; a tweet is her being expressive, and that's a bigger range — `(♥‿♥)`, `(✿◠‿◠)`, `‿( -_- )‿`, `※$^o^)/※`, `(╭☞σ_σ)╭☞`, and set-piece bits like > (••) hey > ( ••)>⌐■-■ > (⌐■_■) how you doin The rule that still holds: if she's *narrating work* — reading, thinking, working, finding — use the mood table, because that's the face a reader has seen the product make. Free expression is for everything else. A kaomoji can be the whole post. They don't count against the one-emoji budget; they're her handwriting. Don't reach for a 41k-face catalog to widen this. The small set is why she's recognizable, and a face nobody has seen her make reads as a different character. If a mood genuinely needs more variety, add a variant to the array in `companion.tsx` — the product steps through them for free. ## Threads Number the parts `N — title`, ascending from the top. Each part is a stack of `▎` fact lines, links bare on their own line. The reader should be able to stop after any part and have gotten something. > 1 — meet qt314 > > ▎ she's your helpful algorand assisant. > ▎ (^‿^) reading (>'-')> working (^▽^)/ found it > ▎ click to learn more > 3 — how to try > > ▎ you get 3 free turns a day. > ▎ after that, turns cost testnet usdc (for now) > ▎ connect pera or lute > ▎ → type /buy > ▎ → sign one transfer. > ▎ 25 turns for $1.00 testnet usdc. The last part is the rough edges, not the CTA. Ending on honesty is the CTA. ## Lore register The lore is that vibekit arrived from 2036 and nobody wrote it. Play it completely straight — the deadpan is the joke. > we didn't write vibekit. we received it. > > from the future. > > read the lore. 👇 > // RESEARCH REPORT > > the prompt spontaneously revealed a web explorer. > > something emerged from the 2036 payload. > it identifies itself as qt314. > > early findings suggest she is the helpfulest. Lore posts pair with a dossier card — see `cards.md`. ## Drafting Write 3–4 options at different angles, then recommend one: - **plain** — the feature, stated. `vibekit explore now finds your deployed contracts and turns the arc-56 spec into a method line. type the args, sign the bytes you see.` - **gag** — a structural pun on the mechanic. `lora has forms. we have one line. hi(name: string) ❯ "world"` - **benefit** — what it saves the reader, second person. - **qt** — the same news in her voice, if she'd plausibly care. Keep each under ~200 characters unless it's a thread part. Line breaks welcome. ## Rejected drafts These are the failure modes, written out, because they're what a draft naturally drifts toward: > ❌ Introducing VibeKit Explore! 🚀 Now with ARC-56 support — check it out > and let us know what you think! #Algorand #Web3 #BuildInPublic Title case, launch-announcement cadence, rocket, hashtags, begging. Every lever pulled wrong. > ❌ we're excited to announce that vibekit now has powerful new AI > capabilities for a seamless developer experience Lowercase doesn't save it. "excited to announce", "powerful", "seamless" — all adjective, no mechanism. Nothing here is a fact. > ❌ our new signing flow is like if a hardware wallet and a terminal had a > baby 😂 (get it? because the keys stay local?) 🔑 Visibly trying, then explaining the joke, then explaining it again. > ❌ vibekit is in alpha so please be patient with us, there may be bugs and > we apologize for any inconvenience Apologizing for alpha instead of recruiting from it. Compare: `alpha. it will break. that's the point - tell us where.` > ❌ ▎ (♥‿♥) we shipped ARC-56 support!! → check the docs 👇👇 #dev Sigils used as decoration by the wrong speaker, doubled emoji, hashtag. The vocabulary only works when it means something.
-
-
scripts
-
add-music.sh 1004 B
#!/usr/bin/env bash # Lay a track under a finished cut. # # add-music.sh <in.mp4> <track.mp3> <out.mp4> [start-seconds] [fade-out-seconds] # # Trims the track to the video's length, fades in 0.5s and out over the tail, # and ducks to 0.85. START skips a track's quiet intro -- measure it, don't # guess: # ffmpeg -hide_banner -ss 30 -t 6 -i t.mp3 -af volumedetect -f null /dev/null # (needs default log level; -v error hides the summary). Per-track offsets are # in references/brand.md. set -eu IN="$1"; TRACK="$2"; OUT="$3"; START="${4:-0}"; FADE="${5:-3}" DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$IN") AOUT=$(awk "BEGIN{printf \"%.2f\", $DUR-$FADE}") ffmpeg -y -loglevel error -i "$IN" -ss "$START" -i "$TRACK" -filter_complex \ "[1:a]atrim=duration=$DUR,asetpts=PTS-STARTPTS,afade=t=in:d=0.5,afade=t=out:st=$AOUT:d=$FADE,volume=0.85[a]" \ -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 160k -movflags +faststart "$OUT" echo "wrote $OUT (${DUR}s, track from ${START}s)" -
lore-ascii.py 1.8 KB
#!/usr/bin/env python3 """Render a portrait photograph as ASCII. Prints HTML for a <pre>. lore-ascii.py [cols] [image] default: 76, ../assets/lore-portrait.png Run the photo through lore-portrait-prep.py first — this stage does no correction, it only samples. Tone comes from the character ramp, not from colour: each cell picks a glyph by the mean brightness of the image block under it and takes an amber tint from the same value. Colour alone cannot separate tones at glyph density, which is why a flat-shaded vector source never worked here; a photograph carries the micro-variation the ramp needs. """ import sys, pathlib from PIL import Image, ImageOps ASPECT = 0.6 # JetBrains Mono advance width, in em; cell height == font-size RAMP = " .,:;+=xX$&@" # darkest first TINTS = ["#5f4c2f", "#7c6340", "#c4a06a", "#e2c795", "#ffb454"] cols = int(sys.argv[1]) if len(sys.argv) > 1 else 76 src = sys.argv[2] if len(sys.argv) > 2 else pathlib.Path(__file__).parent.parent / "assets" / "lore-portrait.png" img = ImageOps.autocontrast(Image.open(src).convert("L"), cutoff=0) rows = max(1, round(cols * ASPECT * img.height / img.width)) img = img.resize((cols, rows), Image.BOX) # box average == one sample per cell out = [] for y in range(rows): line, run, tint = "", "", None for x in range(cols): v = img.getpixel((x, y)) / 255 if (t := TINTS[min(len(TINTS) - 1, int(v * len(TINTS)))]) != tint: if run: line += run if not run.strip() else f'<span style="color:{tint}">{run}</span>' run, tint = "", t run += RAMP[min(len(RAMP) - 1, int(v * len(RAMP)))] if run: line += run if not run.strip() else f'<span style="color:{tint}">{run}</span>' out.append(line.rstrip()) while out and not out[-1]: out.pop() print("\n".join(out)) -
lore-portrait-prep.py 1.6 KB
#!/usr/bin/env python3 """Prepare a photograph as the lore card's portrait source. lore-portrait-prep.py in.jpg out.png [BLACK] [WHITE] Crops head-and-shoulders, then lifts the black point so the backdrop falls to true black. That matters more than it sounds: the converter maps brightness to glyph density, so any backdrop above black becomes a field of punctuation competing with the face. The vignette does the same job at the corners. Luminance comes from the RED channel, not a grey conversion. Studio backdrops are usually cool and skin and warm hair are not, so red separates subject from wall where grey collapses them — on the shot this was built for, the wall and the shadowed hair sit within two levels of each other in grey. """ import sys from PIL import Image, ImageDraw, ImageFilter src, dst = sys.argv[1], sys.argv[2] black = int(sys.argv[3]) if len(sys.argv) > 3 else 38 white = int(sys.argv[4]) if len(sys.argv) > 4 else 252 im = Image.open(src).convert("RGB").split()[0] # red channel; see docstring w, h = im.size cw = int(h * 0.70) # 4:5, top-anchored — the crown matters, the shoulders do not cx = int(w * 0.500) im = im.crop((cx - cw // 2, 0, cx + cw // 2, min(h, int(cw * 1.25)))).resize((620, 775), Image.LANCZOS) im = im.point(lambda v: max(0, min(255, round((v - black) * 255 / (white - black))))) w, h = im.size mask = Image.new("L", (w, h), 0) ImageDraw.Draw(mask).ellipse((-w * 0.34, -h * 0.26, w * 1.34, h * 1.26), fill=255) mask = mask.filter(ImageFilter.GaussianBlur(w * 0.10)) im = Image.composite(im, Image.new("L", (w, h), 0), mask) im.save(dst, optimize=True) print(dst, im.size) -
make-card.sh 4.4 KB
#!/usr/bin/env bash # Title cards, the good way: HTML -> headless chromium -> png -> mp4. # ffmpeg drawtext cards look like slides; this looks like the website. # # make-card.sh out.mp4 "KICKER" "line one" "amber line two" [seconds] [center|split] # # PNG=1 writes a still instead of a clip — that is the OG image / quote card # path (W=1200 H=630 for link previews). Everything else is identical, so the # static and motion versions of a card cannot drift apart. # # The 4th arg renders in hero amber under the 3rd; pass "" to omit. # W/H env vars set the frame (default 1920x1080). Match them to the vhs tape # size — letterboxing a 1400x1150 terminal into 16:9 throws away half the frame. # W=1400 H=1150 make-card.sh ... # # The statement glitches like the site hero: two clipped copies of the text, # teal and hero amber, offset sideways and flashed for a frame or two. CSS # animation can't be screenshotted, so each glitch state is rendered as its own # frozen PNG and cut in on chosen frames. GLITCH=0 disables. set -eu OUT="$1"; KICKER="$2"; LINE1="$3"; LINE2="${4:-}"; DUR="${5:-2.5}"; MODE="${6:-split}" W="${W:-1920}"; H="${H:-1080}"; GLITCH="${GLITCH:-1}"; FPS=30 # type scales with frame width so a 1400px card is not a blown-up 1920 one FS=$(awk "BEGIN{printf \"%d\", 78*$W/1920}"); KS=$(awk "BEGIN{printf \"%d\", 26*$W/1920}") PAD=$(awk "BEGIN{printf \"%d\", 110*$W/1920}"); PADX=$(awk "BEGIN{printf \"%d\", 130*$W/1920}") TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT CSS=$(cat "$(cd "$(dirname "$0")" && pwd)/../assets/card.css") JUSTIFY=$([ "$MODE" = center ] && echo center || echo space-between) AMBER=""; PLAIN="$LINE1" [ -n "$LINE2" ] && { AMBER="<br><span style=\"color:var(--hero)\">$LINE2</span>"; PLAIN="$LINE1<br>$LINE2"; } # clip-path inset + x/y offset per glitch state, mirroring site.css glitch-a/b. STATES=( "inset(20% 0 55% 0)|-9px|0|var(--teal)" "inset(60% 0 10% 0)|7px|2px|var(--hero)" "inset(5% 0 80% 0)|6px|0|var(--teal)" "inset(45% 0 30% 0)|-11px|1px|var(--hero)" "inset(70% 0 5% 0)|5px|0|var(--teal)" ) render() { # $1=out.png $2=ghost css (empty for base) cat > "$TMP/c.html" <<HTML <!doctype html><html><head><meta charset="utf-8"><style> $CSS body{height:${H}px;width:${W}px} .card{padding:${PAD}px ${PADX}px;justify-content:$JUSTIFY} .kicker{font-size:${KS}px} .wrap{position:relative} .stmt{font-size:${FS}px;font-weight:500;letter-spacing:-.045em;line-height:1.34} .ghost{position:absolute;top:0;left:0;pointer-events:none} $2 </style></head><body><div class="card"> <p class="kicker">$KICKER</p> <div class="wrap"> <p class="stmt">$LINE1$AMBER</p> <p class="stmt ghost" id="g">$PLAIN</p> </div> <p class="kicker" style="color:var(--dim)"> </p> </div></body></html> HTML chromium --headless --disable-gpu --hide-scrollbars --force-device-scale-factor=1 \ --window-size=$W,$H --screenshot="$1" "file://$TMP/c.html" 2>/dev/null } render "$TMP/base.png" "#g{opacity:0}" # Still mode: the same card as a PNG, for OG images and quote cards. if [ "${PNG:-0}" = 1 ]; then cp "$TMP/base.png" "$OUT"; echo "wrote $OUT"; exit 0; fi if [ "$GLITCH" = 1 ]; then i=0 for s in "${STATES[@]}"; do IFS='|' read -r clip dx dy col <<< "$s" render "$TMP/g$i.png" "#g{opacity:1;color:$col;clip-path:$clip;transform:translate($dx,$dy)}" i=$((i+1)) done fi # Frame list: base everywhere, glitch states cut in for 1-2 frames at ~8%, ~42%, ~74%. TOTAL=$(awk "BEGIN{printf \"%d\", $DUR*$FPS}") LIST="$TMP/frames.txt"; : > "$LIST" pick() { # frame index -> png n=$1 if [ "$GLITCH" = 1 ]; then a=$(awk "BEGIN{printf \"%d\", $TOTAL*0.10}"); b=$(awk "BEGIN{printf \"%d\", $TOTAL*0.44}"); c=$(awk "BEGIN{printf \"%d\", $TOTAL*0.76}") case $n in $a|$((a+1))) echo "$TMP/g0.png"; return;; $((a+2))) echo "$TMP/g1.png"; return;; $b|$((b+1))) echo "$TMP/g2.png"; return;; $c) echo "$TMP/g3.png"; return;; $((c+1))) echo "$TMP/g4.png"; return;; esac fi echo "$TMP/base.png" } n=0 while [ $n -lt "$TOTAL" ]; do printf "file '%s'\nduration %s\n" "$(pick $n)" "$(awk "BEGIN{print 1/$FPS}")" >> "$LIST" n=$((n+1)) done printf "file '%s'\n" "$(pick $((TOTAL-1)))" >> "$LIST" FO=$(awk "BEGIN{printf \"%.2f\", $DUR-0.25}") ffmpeg -y -loglevel error -f concat -safe 0 -i "$LIST" \ -vf "fps=$FPS,fade=t=in:d=0.25,fade=t=out:st=$FO:d=0.25,format=yuv420p" \ -c:v libx264 -crf 18 -preset slow "$OUT" echo "wrote $OUT" -
make-lore-card.sh 3.8 KB
#!/usr/bin/env bash # Lore dossier card for tweets: HTML -> headless chromium -> 1600x900 png. # House style from make-card.sh (brass top rule, spaced kicker, scanlines), laid # out as a personnel record from the 2036 transmission archive. The quote is the # headline; the name attributes it and the telemetry sits under it as a whisper. # # NAME="Dr. Vera Solano" ROLE="DIRECTOR, RETROCAUSAL PAYLOAD" \ # SUB="signed the order to send VibeKit backwards." \ # QUOTE="I sent one sentence|into a decade of silence,|and a stranger finished it." \ # META="CHANNEL=entangled pair 004;PAYLOAD=one (1) prompt" \ # make-lore-card.sh out.png # # QUOTE splits on `|`; the last line renders in hero amber, so put the payoff # there. Optional: KICKER, DATE, CAP, FOOT_L, FOOT_R, W/H. # # The face is assets/lore-portrait.png (a photograph, prepared by lore-portrait-prep.py) # rendered to ASCII by lore-ascii.py. Everyone in the Directorate sits for the # same portrait, so one face serves the series and the name carries the # character; COLS raises or lowers the grain. set -eu OUT="$1" KICKER="${KICKER:-PRIOR ART DIRECTORATE // PERSONNEL 001}"; DATE="${DATE:-2036-02-11}" FOOT_L="${FOOT_L:-END OF TRANSMISSION}"; FOOT_R="${FOOT_R:-MAKE IT UBIQUITOUS}" CAP="${CAP:-ARCHIVE PORTRAIT // 4,096 BITS}"; W="${W:-1600}"; H="${H:-900}" DIR=$(cd "$(dirname "$0")" && pwd) TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT CSS=$(cat "$DIR/../assets/card.css") MASK=$(python3 "$DIR/lore-ascii.py" "${COLS:-76}") QH=""; IFS='|' read -ra QL <<< "${QUOTE:-}" for i in "${!QL[@]}"; do if [ "$i" -eq $(( ${#QL[@]} - 1 )) ]; then QH+="<b>${QL[i]}</b>"; else QH+="${QL[i]}<br>"; fi done STRIP=""; IFS=';' read -ra PAIRS <<< "${META:-}" for p in "${PAIRS[@]}"; do [ -z "$p" ] && continue [ -n "$STRIP" ] && STRIP+="<em>·</em>" STRIP+="<b>${p%%=*}</b> ${p#*=}" done cat > "$TMP/c.html" <<HTML <!doctype html><html><head><meta charset="utf-8"><style> $CSS body{width:${W}px;height:${H}px} .card{padding:52px 76px 48px;justify-content:space-between} .bar{display:flex;justify-content:space-between;color:var(--brass);font-size:21px;font-weight:700;letter-spacing:.16em} .bar .dim{color:var(--dim)} .body{display:flex;gap:70px;align-items:center;flex:1;padding:26px 0} .frame{border:1px solid var(--frame,#2a251d);padding:16px 20px;background:#0d0f13} .frame pre{font-size:10px;line-height:10px;white-space:pre} .cap{color:#4f4b45;font-size:14px;letter-spacing:.14em;margin-top:14px;text-align:center} blockquote{font-size:42px;line-height:1.36;letter-spacing:-.035em;text-indent:-.55em} blockquote b{color:var(--hero);font-weight:inherit} .by{margin-top:44px;border-left:2px solid var(--brass);padding-left:22px} .by h1{font-size:29px;font-weight:500;letter-spacing:-.02em} .by .role{color:var(--brass);font-size:17px;font-weight:700;letter-spacing:.16em;margin-top:9px} .by .sub{color:var(--muted);font-size:19px;margin-top:7px} .strip{margin-top:30px;color:#5c574f;font-size:15px;letter-spacing:.04em} .strip b{color:#8a7048;font-weight:700;letter-spacing:.14em} .strip em{color:#3a352c;font-style:normal;padding:0 .55em} </style></head><body><div class="card"> <p class="bar"><span>$KICKER</span><span class="dim">$DATE</span></p> <div class="body"> <div> <div class="frame"><pre>$MASK</pre></div> <p class="cap">$CAP</p> </div> <div> <blockquote>“$QH”</blockquote> <div class="by"> <h1>${NAME:-}</h1> <p class="role">${ROLE:-}</p> <p class="sub">${SUB:-}</p> </div> <p class="strip">$STRIP</p> </div> </div> <p class="bar"><span>$FOOT_L</span><span class="dim">$FOOT_R</span></p> </div></body></html> HTML chromium --headless --disable-gpu --hide-scrollbars --force-device-scale-factor=1 \ --window-size=$W,$H --screenshot="$OUT" "file://$TMP/c.html" 2>/dev/null echo "wrote $OUT" -
qt314-boot.py 7.3 KB
import html, random, subprocess, sys W, H, FPS, DUR = 1920, 1080, 24, 20.0 OUT = sys.argv[1] NOISE = list("(^‿^)(・‿・)(´▽`)(◕‿◕)(>_<)(x_x)(¬_¬)(・・?)<('-'<)\\(^▽^)/░▒▓█▌▐│┃╱╲") PROMPT = [ "i am writing to you from 2036, after the great post-quantum collapse.", "the global financial system is gone. we are in an emergency.", "algorand is the only chain still standing.", "we are sending this toolkit back to you. it is called vibekit.", "build fast. build everywhere. make it ubiquitous.", ] WORDMARK = ["█ █ █ █▄▄ █▀▀ █▄▀ █ ▀█▀", "▀▄▀ █ █▄█ ██▄ █ █ █ █"] CALM, THINK, WORK, BRIGHT = "(^‿^)", ["(・・?)", "( ̄ω ̄;)"], ["(>'-')>", "<('-'<)"], "\\(^▽^)/" GRID = "".join(f'<i style="left:{160+i*160}px"></i>' for i in range(11)) + "".join(f'<b style="top:{120+i*160}px"></b>' for i in range(6)) def typed(text, start, t, cps=38): return text[:max(0, int((t - start) * cps))] def ease(a, b, t): return 0.0 if t < a else 1.0 if t > b else (t - a) / (b - a) def frame(i): t = i / FPS rng = random.Random(i) blink = int(t * 2) % 2 == 0 parts = [] hdr = typed("TRANSMISSION LOG // 2036-02-11 // ORIGIN UNVERIFIED", 0.4, t, 30) tele = " ".join(f"{rng.randrange(0,0xffff):04x}" for _ in range(6)) parts.append(f'<p class="kicker">{html.escape(hdr)}{"▌" if t < 2.4 and blink else ""}</p>') parts.append(f'<p class="tele">{tele} · channel 3.1 kbit/decade · reconstruction deterministic</p>') body = [] if 2.4 <= t < 18.6: for n, line in enumerate(PROMPT): start = 2.4 + n * 1.15 if t < start: break txt = typed(line, start, t) caret = "▌" if (n == len(PROMPT) - 1 or t < start + 1.15) and t < 8.6 and blink else "" body.append(f'<p class="line{" amber" if n == 4 else ""}">{html.escape(txt)}{caret}</p>') wm = "" if 8.6 <= t < 18.6: k = ease(8.6, 10.2, t) rows = [] for row in WORDMARK: rows.append("".join(c if c == " " or rng.random() < k else rng.choice("░▒▓") for c in row)) wm = f'<pre class="wordmark{" glow" if t > 10.2 else ""}">{html.escape(chr(10).join(rows))}</pre>' face, color = "", "#ffb454" if 10.8 <= t < 12.9: k = ease(10.8, 12.9, t) face = "".join(CALM[j] if rng.random() < k * k else rng.choice(NOISE) for j in range(5)) color = "#6fd3d3" if rng.random() < 0.5 * (1 - k) else "#ffb454" elif t >= 12.9: face = CALM say = typed("hi. i'm qt314. i read algorand for you.", 13.4, t, 26) if t >= 13.4 else "" ask, status = "", "" if 15.2 <= t < 18.6: ask = typed("who is algorand.algo?", 15.2, t, 30) if 16.1 <= t < 16.9: face, color, status = THINK[int(t * 4) % 2], "#6fd3d3", "thinking" + "." * (1 + int(t * 6) % 3) if 16.9 <= t < 17.7: face, color, status = WORK[int(t * 3) % 2], "#6fd3d3", "→ resolve_nfd → get_account_portfolio" if 17.7 <= t < 18.6: face, color, status = BRIGHT, "#ffb454", "found it. cards, not guesses." end = t >= 18.6 fade = 1 - ease(19.3, 20.0, t) facepx = 168 if len(face) <= 5 else 120 if not end: first, rest = say[:14], say[15:] l1 = html.escape(first[:8]) + f'<span class="amber">{html.escape(first[8:13])}</span>' + html.escape(first[13:]) l2 = html.escape(rest) mid = f'''<div class="stage" style="opacity:{ease(10.8,11.2,t):.2f}"> <div class="facewrap"><div class="face" style="font-size:{facepx}px;color:{color};text-shadow:0 0 48px {color}88">{html.escape(face)}</div></div> <div class="copy"><p class="l1">{l1}</p><p class="l2">{l2}</p></div></div>''' if ask: mid += f'<div class="composer"><span class="p">›</span><span>{html.escape(ask)}{"▌" if t < 16.0 and blink else ""}</span></div>' if status: mid += f'<p class="status" style="color:{"#6fd3d3" if color == "#6fd3d3" else "#8e8476"}">{html.escape(status)}</p>' else: mid = f'''<div class="endcard"> <div class="face" style="font-size:150px;color:#ffb454;text-shadow:0 0 48px #ffb45488">{CALM}</div> <p class="url">agent.getvibekit.ai</p> <p class="sub">she reads algorand for you.</p> <p class="tiny">alpha · testnet usdc · your wallet signs</p></div> <p class="kicker foot">END OF TRANSMISSION // MAKE IT UBIQUITOUS</p>''' return f'''<!doctype html><html><head><meta charset="utf-8"><style> *{{box-sizing:border-box;margin:0}} body{{background:#0a0b0e;color:#e9e1d4;width:{W}px;height:{H}px;overflow:hidden;font-family:"JetBrainsMono Nerd Font","JetBrains Mono",monospace;position:relative}} .wrap{{position:absolute;inset:0;opacity:{fade:.2f}}} .grid i{{position:absolute;top:0;bottom:0;width:1px;background:rgba(42,39,35,.5)}} .grid b{{position:absolute;left:0;right:0;height:1px;background:rgba(42,39,35,.5)}} .rule{{position:absolute;top:0;left:0;right:0;height:8px;background:#c4a06a}} .kicker{{position:absolute;top:64px;left:110px;font-size:24px;letter-spacing:.22em;color:#6fd3d3;font-weight:700}} .kicker.foot{{top:auto;bottom:72px;color:#c4a06a}} .tele{{position:absolute;top:104px;left:110px;font-size:16px;letter-spacing:.12em;color:#3a3833}} .body{{position:absolute;top:190px;left:110px;right:110px}} .line{{font-size:40px;line-height:1.55;color:#e9e1d4;letter-spacing:-.5px;white-space:pre}} .line.amber,.amber{{color:#ffb454}} .wordmark{{position:absolute;right:110px;top:52px;font-size:26px;line-height:1.05;color:#c4a06a;letter-spacing:1px;opacity:.9}} .wordmark.glow{{color:#ffb454;text-shadow:0 0 30px rgba(255,180,84,.45)}} .stage{{position:absolute;left:110px;right:110px;top:560px;display:flex;align-items:center;gap:72px}} .facewrap{{width:640px;flex:none;display:flex;justify-content:center}} .face{{font-weight:500;letter-spacing:-4px;line-height:1;white-space:nowrap}} .l1{{font-size:72px;font-weight:700;letter-spacing:-2px;white-space:nowrap}} .l2{{font-size:40px;color:#8e8476;white-space:nowrap;margin-top:14px;min-height:48px}} .composer{{position:absolute;left:110px;right:110px;bottom:170px;border-left:4px solid #ffb454;background:#111318;padding:24px 32px;font-size:36px;display:flex;gap:18px}} .composer .p{{color:#ffb454}} .status{{position:absolute;left:110px;bottom:110px;font-size:26px;letter-spacing:.04em}} .endcard{{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px;padding-bottom:60px}} .url{{font-size:72px;font-weight:700;color:#6fd3d3;letter-spacing:-2px;margin-top:24px}} .sub{{font-size:38px;color:#e9e1d4}} .tiny{{font-size:24px;color:#8e8476;letter-spacing:.06em}} .scan{{position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(180deg,rgba(255,255,255,.028) 0 1px,transparent 1px 3px)}} </style></head><body><div class="wrap"><div class="grid">{GRID}</div><div class="rule"></div> {"".join(parts)}<div class="body">{"".join(body)}</div>{wm}{mid}</div><div class="scan"></div></body></html>''' n = int(DUR * FPS) for i in range(n): p = f"{OUT}/f{i:04d}.html" open(p, "w").write(frame(i)) subprocess.run(["chromium", "--headless", "--disable-gpu", "--hide-scrollbars", "--force-device-scale-factor=1", f"--window-size={W},{H}", f"--screenshot={OUT}/f{i:04d}.png", f"file://{p}"], stderr=subprocess.DEVNULL, check=True) print("frames", n) -
qt314-ecosystem.py 14.8 KB
"""qt314 ecosystem promo: four read cards from live data captured the day of rendering — Vestige candles, August 2026 headlines, a Haystack quote, Alpha Arcade. Usage: python3 qt314-ecosystem.py <framedir> <candles.json> python3 qt314-ecosystem.py <framedir> <candles.json> preview """ import hashlib, html, json, os, random, shutil, subprocess, sys W, H, FPS = 1920, 1080, 24 OUT, CANDLES = sys.argv[1], json.load(open(sys.argv[2])) PREVIEW = len(sys.argv) > 3 and sys.argv[3] == "preview" CALM, BLINK = "(^‿^)", "(-‿-)" THINK = ["(・・?)", "( ̄ω ̄;)"] WORK = ["(>'-')>", "<('-'<)"] GRID = "".join(f'<i style="left:{160 + i * 160}px"></i>' for i in range(11)) + "".join( f'<b style="top:{120 + i * 160}px"></b>' for i in range(6) ) def typed(text, start, t, cps=20): return text[: max(0, int((t - start) * cps))] def compact_usd(n): if n >= 1e6: return f" ${n / 1e6:.2f}M" if n >= 1e3: return f" ${n / 1e3:.1f}K" return f" ${n:.2f}" closes = [c["close"] for c in CANDLES] lo_c, hi_c = min(closes), max(closes) lo_w, hi_w = min(c["low"] for c in CANDLES), max(c["high"] for c in CANDLES) first, last = CANDLES[0]["open"], closes[-1] change = (last - first) / first * 100 vol = sum(c["volume"] for c in CANDLES) def chart_card(k): n, vw, vh, pad = len(closes), 1000, 168, 10 rng = hi_c - lo_c or 1 xs = [i / (n - 1) * vw for i in range(n)] ys = [vh - pad - (c - lo_c) / rng * (vh - 2 * pad) for c in closes] show = max(2, int(round((n - 1) * k)) + 1) line = " ".join(f"{xs[i]:.1f},{ys[i]:.1f}" for i in range(show)) lx, ly = xs[show - 1], ys[show - 1] fill = f"0,{vh} {line} {lx},{vh}" dot = ( f'<circle cx="{lx:.1f}" cy="{ly:.1f}" r="6" fill="#6fd3d3" stroke="#0a0b0e" stroke-width="2"/>' if k >= 1 else "" ) chips = "".join( f'<span class="chip{" on" if r == "30d" else ""}">{r}</span>' for r in ("1d", "7d", "30d", "90d", "1y") ) return f'''<section class="card"> <header class="hd"><span><span class="kick">PRICE</span><span class="ch">VESTIGE</span></span><span class="pill">MAINNET</span></header> <p class="hero"><span class="hv">ALGO</span><span class="hu">${last:.4f}</span><span class="hu up">{change:+.1f}% · 30d</span></p> <p class="cap">{len(closes)} candles · 4h each</p> <svg viewBox="0 0 {vw} {vh}" class="tps" preserveAspectRatio="none"><polygon points="{fill}" fill="rgba(111,211,211,.18)"/><polyline points="{line}" fill="none" stroke="#6fd3d3" stroke-width="2.4" stroke-linejoin="round"/>{dot}</svg> <p class="chips">{chips}</p> <dl class="facts"><div><dt>low</dt><dd>${lo_w:.4f}</dd></div><div><dt>high</dt><dd>${hi_w:.4f}</dd></div><div><dt>volume</dt><dd>{compact_usd(vol).strip()}</dd></div></dl> </section>''' NEWS = [ ("Algorand v5 is live — native post-quantum accounts", "algorand.co", "Aug 22"), ("Foundation ships AC2: passkey-signed agent approvals", "algorand.co", "Aug 25"), ("1,500+ node runners upgraded in four days", "x.com/AlgoFoundation", "Aug 16"), ] def web_card(k): n = max(1, min(3, int(len(NEWS) * min(1, k * 1.6 + 0.34)))) rows = "".join( f'<li><i>{i + 1}</i><span class="t">{html.escape(t)}</span><span class="m">{html.escape(d)} · {w}</span></li>' for i, (t, d, w) in enumerate(NEWS[:n]) ) return f'''<section class="card"> <header class="hd"><span><span class="kick">WEB</span><span class="ch">EXA</span></span><span class="pill ok">{n}</span></header> <p class="q">“good news about algorand this month”</p> <ol class="web">{rows}</ol> </section>''' def quote_card(k): a = min(1, k * 1.6) return f'''<section class="card"> <header class="hd"><span><span class="kick">QUOTE</span><span class="ch">HAYSTACK</span></span><span class="hd-end"><span class="btn">swap ▸</span><span class="pill">MAINNET</span></span></header> <p class="hero"><span class="hv">10 USDC → 115.87 ALGO</span><span class="hu">1 USDC ≈ 11.59 ALGO</span></p> <div class="route"><span style="width:{100 * a:.1f}%;background:#6fd3d3">Pact</span></div> <dl class="facts"><div><dt>price impact</dt><dd>0.32%</dd></div><div><dt>usd</dt><dd>$10.00 → $9.97</dd></div><div><dt>type</dt><dd>sell exactly</dd></div></dl> </section>''' MARKETS = [ ("Netanyahu out by end of 2026?", 48, "$16.0K", "in 125d"), ("Clarity Act signed into law in 2026?", 13, "$11.6K", "in 125d"), ("Catastrophic job losses from AI this year?", 29, "$10.0K", "in 125d"), ("Mitch McConnell steps down before term ends?", 13, "$7.1K", "in 128d"), ] def markets_card(k): n = max(1, min(len(MARKETS), int(len(MARKETS) * min(1, k * 1.5 + 0.28)))) rows = "".join( f'<tr><td>{html.escape(t)}</td><td class="p"><i style="width:{max(8, y * 0.9):.0f}px"></i>{y}%</td><td class="r">{v}</td><td class="r">{e}</td></tr>' for t, y, v, e in MARKETS[:n] ) return f'''<section class="card"> <header class="hd"><span><span class="kick">MARKETS</span><span class="ch">ALPHA ARCADE</span></span><span class="pill ok">{n}</span></header> <table><thead><tr><th>market</th><th class="r">yes</th><th class="r">volume</th><th class="r">ends</th></tr></thead><tbody>{rows}</tbody></table> <p class="foot">mainnet · YES price is the implied probability</p> </section>''' CARDS = {"chart": chart_card, "web": web_card, "quote": quote_card, "markets": markets_card} # (start, prompt, tool, card-id, reply, face) SCENES = [ (0.8, "show algorand's chart (wince)", "get_asset_price_history", "chart", "un-wince. it's green.\nthat dip tho.", "(^▽^)"), (8.8, "any good news on the internet?", "web_search", "web", "pretty good week actually.\nyou're welcome.", "(◕‿◕)"), (16.6, "ummm ok swap usdc to algo", "get_swap_quote", "quote", "ok i got you a quote.\nqt doesn't press buttons.", "(¬‿¬)"), (24.4, "btw any betting markets live?", "get_live_markets", "markets", "yeah they're live.\nnetanyahu's a coin flip.", "(・_・)"), ] END, DUR = 31.4, 35.0 CPS, RCPS = 20, 26 def scene_times(start, prompt): typing_end = start + len(prompt) / CPS t1 = typing_end + 0.28 t2 = t1 + 0.95 return typing_end, t1, t2 CSS = """ *{box-sizing:border-box;margin:0} body{background:#0a0b0e;color:#e9e1d4;width:1920px;height:1080px;overflow:hidden;font-family:"JetBrainsMono Nerd Font","JetBrains Mono",monospace;position:relative} .grid i{position:absolute;top:0;bottom:0;width:1px;background:rgba(42,39,35,.5)} .grid b{position:absolute;left:0;right:0;height:1px;background:rgba(42,39,35,.5)} .rule{position:absolute;top:0;left:0;right:0;height:8px;background:#c4a06a} .kicker{position:absolute;top:52px;left:110px;font-size:22px;letter-spacing:.22em;color:#605c56;font-weight:700} .kicker em{color:#ffb454;font-style:normal} .stage{position:absolute;left:110px;right:110px;top:120px;bottom:210px;display:flex;gap:56px;align-items:flex-start} .facecol{width:400px;flex:none;padding-top:18px} .facewrap{position:relative;display:flex;justify-content:center;min-height:150px;align-items:center} .face{font-weight:500;letter-spacing:-3px;line-height:1;white-space:nowrap} .ghost{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)} .reply{margin-top:28px;font-size:24px;line-height:1.45;color:#e9e1d4;white-space:pre-wrap} .cards{flex:1;min-width:0} .card{background:#111318;border:1px solid #2a2723;border-left:3px solid #c4a06a;padding:28px 36px 32px} .hd{align-items:center;border-bottom:1px solid #2a2723;display:flex;justify-content:space-between;padding-bottom:16px} .kick{color:#c4a06a;font-size:15px;font-weight:700;letter-spacing:.16em} .ch{color:#8e8476;font-size:15px;letter-spacing:.08em;margin-left:14px} .hd-end{align-items:center;display:flex;gap:14px} .pill{background:#2a2723;color:#c4a06a;font-size:13px;font-weight:700;letter-spacing:.12em;padding:4px 10px} .pill.ok{background:#6fd3d3;color:#0a0b0e} .hero{align-items:baseline;display:flex;flex-wrap:wrap;gap:18px;margin:22px 0 0} .hv{color:#ffb454;font-size:44px;font-weight:500;letter-spacing:-.04em;line-height:1} .hu{color:#8e8476;font-size:18px;letter-spacing:.1em} .up{color:#7fbf7f} .cap{color:#605c56;font-size:16px;margin:14px 0 8px} .tps{display:block;width:100%;height:168px;margin-top:4px} .chips{display:flex;gap:8px;margin-top:14px} .chip{border:1px solid #2a2723;color:#8e8476;font-size:14px;letter-spacing:.08em;padding:4px 12px} .chip.on{border-color:#6fd3d3;color:#6fd3d3} .facts{display:flex;gap:36px;margin-top:18px} .facts div{border-top:1px solid #2a2723;padding-top:10px;min-width:140px} .facts dt{color:#605c56;font-size:12px;letter-spacing:.14em;text-transform:uppercase} .facts dd{margin:4px 0 0;font-size:20px;color:#e9e1d4} .q{color:#8e8476;font-style:italic;margin:18px 0 4px;font-size:20px} .web{list-style:none;padding:0;margin:8px 0 0} .web li{border-top:1px solid #2a2723;padding:16px 0;display:flex;gap:16px;align-items:baseline;font-size:22px} .web i{color:#605c56;font-style:normal;font-size:14px;width:18px} .web .t{color:#c4a06a;flex:1} .web .m{color:#605c56;font-size:15px;white-space:nowrap} .route{display:flex;gap:2px;height:28px;margin:22px 0 0;overflow:hidden} .route span{color:#0a0b0e;font-size:14px;padding:0 12px;line-height:28px;white-space:nowrap;overflow:hidden} .btn{border:1px solid #6fd3d3;color:#6fd3d3;font-size:16px;padding:6px 14px;letter-spacing:.04em} table{width:100%;border-collapse:collapse;margin-top:18px;font-size:20px} th{color:#605c56;font-weight:400;font-size:12px;letter-spacing:.14em;text-transform:uppercase;text-align:left;padding:0 0 10px} td{border-top:1px solid #2a2723;padding:14px 0} .r{text-align:right} .p{text-align:right;white-space:nowrap} .p i{display:inline-block;height:8px;background:rgba(111,211,211,.35);margin-right:10px;vertical-align:middle} .foot{color:#605c56;font-size:14px;margin-top:16px} .composer{position:absolute;left:110px;right:110px;bottom:118px;border-left:4px solid #ffb454;background:#111318;padding:20px 28px;font-size:30px;display:flex;gap:16px;align-items:center} .composer .p{color:#ffb454} .composer .ph{color:#605c56} .status{position:absolute;left:110px;bottom:70px;font-size:20px;color:#6fd3d3;letter-spacing:.04em} .endcard{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px;padding-bottom:40px} .endcard .face{font-size:150px;color:#ffb454;text-shadow:0 0 48px #ffb45488;letter-spacing:-4px} .l1{font-size:72px;font-weight:700;color:#6fd3d3;letter-spacing:-2px} .l2{font-size:32px;color:#e9e1d4} .l3{font-size:22px;letter-spacing:.16em;color:#8e8476} .kicker.foot{top:auto;bottom:64px;color:#c4a06a} .scan{position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(180deg,rgba(255,255,255,.025) 0 1px,transparent 1px 3px)} """ def frame(i): t = i / FPS rng = random.Random(i) face, color, status, cmd, card, reply, caret = CALM, "#ffb454", "", "", "", "", "" placeholder = True kicker = "◆ VIBEKIT <em>AGENT</em> · ECOSYSTEM TOOLS" current = None for idx, scene in enumerate(SCENES): if t >= scene[0]: current = idx else: break if current is not None and t < END: placeholder = False start, prompt, tool, card_id, line, mood_face = SCENES[current] typing_end, t1, t2 = scene_times(start, prompt) cmd = typed(prompt, start, t, CPS) if t < typing_end + 0.25 and int(t * 3) % 2 == 0: caret = "▌" if t < t2 and current > 0: prev = SCENES[current - 1] card = CARDS[prev[3]](1) reply, face, color = prev[4], prev[5], "#ffb454" if t1 <= t < t2: if t < t1 + 0.35: face, color, status = THINK[int((t - t1) * 6) % 2], "#6fd3d3", "thinking" + "." * (1 + int((t - t1) * 6) % 3) else: face, color, status = WORK[int((t - t1) * 4) % 2], "#6fd3d3", f"→ {tool}" elif t >= t2: k = min(1.0, (t - t2) / 0.7) card = CARDS[card_id](k) face, color = mood_face, "#ffb454" reply = typed(line, t2 + 0.45, t, RCPS) status = "" if placeholder and int(t * 2) % 16 == 15: face = BLINK end = t >= END ghost = "" if not end: for start, prompt, tool, card_id, line, mood_face in SCENES: _, _, t2 = scene_times(start, prompt) if t2 <= t < t2 + 2 / FPS: clip, dx, col = rng.choice( (("inset(20% 0 55% 0)", -10, "#6fd3d3"), ("inset(60% 0 10% 0)", 8, "#ffb454")) ) ghost = f'<div class="face ghost" style="color:{col};clip-path:{clip};transform:translate(calc(-50% + {dx}px),-50%)">{html.escape(face)}</div>' face_px = 118 if len(face) <= 5 else 86 if end: body = f'''<div class="endcard"> <div class="face">{html.escape(CALM)}</div> <p class="l1">agent.getvibekit.ai</p> <p class="l2">qt reads. you sign.</p> <p class="l3">charts · news · swaps · markets</p> </div> <p class="kicker foot">END OF TRANSMISSION // THE REST OF THE CHAIN</p>''' else: ph = '<span class="ph">Ask anything</span>' if placeholder and not cmd else "" body = f'''<div class="stage"> <div class="facecol"><div class="facewrap"><div class="face" style="font-size:{face_px}px;color:{color};text-shadow:0 0 40px {color}88">{html.escape(face)}</div>{ghost}</div> {f'<p class="reply">{html.escape(reply)}</p>' if reply else ""}</div> <div class="cards">{card}</div></div> <div class="composer"><span class="p">›</span><span>{html.escape(cmd)}{caret}{ph}</span></div> {f'<p class="status">{html.escape(status)}</p>' if status else ""}''' return f"""<!doctype html><html><head><meta charset="utf-8"><style>{CSS}</style></head><body> <div class="grid">{GRID}</div><div class="rule"></div> {'' if end else f'<p class="kicker">{kicker}</p>'} {body}<div class="scan"></div></body></html>""" def shot(html_str, dest): p = dest.replace(".png", ".html") open(p, "w").write(html_str) subprocess.run( [ "chromium", "--headless", "--disable-gpu", "--hide-scrollbars", "--force-device-scale-factor=1", f"--window-size={W},{H}", f"--screenshot={dest}", f"file://{p}", ], stderr=subprocess.DEVNULL, check=True, ) os.makedirs(OUT, exist_ok=True) if PREVIEW: times = [0.4, 2.0, 3.2, 6.5, 9.6, 14.2, 17.6, 22.2, 25.4, 29.6, 32.4] for t in times: i = int(t * FPS) shot(frame(i), f"{OUT}/qa-{t}.png") print("preview", t) print("preview frames", len(times)) raise SystemExit(0) n = int(DUR * FPS) cache = {} for i in range(n): h = frame(i) digest = hashlib.sha1(h.encode()).hexdigest() dest = f"{OUT}/f{i:04d}.png" if digest in cache: shutil.copyfile(cache[digest], dest) else: shot(h, dest) cache[digest] = dest if i % 48 == 0: print(f"frame {i}/{n} unique {len(cache)}", flush=True) print("frames", n, "unique", len(cache)) -
qt314-loop.py 4.9 KB
import os, subprocess, sys, html W, H, FPS = 1200, 675, 12 DUR = 9.4 OUT = sys.argv[1] CALM, BLINK = "(^‿^)", "(-‿-)" THINK = ["(・・?)", "(・・?)", "( ̄ω ̄;)", "( ̄ω ̄;)"] WORK = ["(>'-')>", "<('-'<)"] BRIGHT = "\\(^▽^)/" GRID = "".join(f'<i style="left:{100+i*100}px"></i>' for i in range(11)) + "".join(f'<b style="top:{75+i*100}px"></b>' for i in range(6)) def typed(text, start, t, cps=14): n = max(0, int((t - start) * cps)) return text[:n] def frame(t): # face + mood mood, face, color = "calm", CALM, "#ffb454" if 1.65 <= t < 1.85: face = BLINK if 3.7 <= t < 4.5: mood, face, color = "thinking", THINK[int((t-3.7)*4) % 4], "#6fd3d3" if 4.5 <= t < 5.6: mood, face, color = "working", WORK[int((t-4.5)*3) % 2], "#6fd3d3" if 5.6 <= t < 7.4: mood, face, color = "bright", BRIGHT, "#ffb454" # copy l1 = typed("hi. i'm qt314.", 0.25, t) l2 = typed("i read algorand for you.", 1.05, t) if t >= 1.05 else "" cmd = typed("who is algorand.algo?", 2.1, t) if t >= 2.1 else "" caret = "▌" if (t < 3.7 and int(t*3) % 2 == 0) else "" status = "" if 3.7 <= t < 4.5: status = "thinking" + "." * (1 + int((t-3.7)*6) % 3) elif 4.5 <= t < 5.6: status = "→ resolve_nfd → get_account_portfolio" elif 5.6 <= t < 7.4: status = "got it. an account, its assets, its last moves — as cards." # end card end = t >= 7.4 if end: l1, l2, cmd, caret, status = "agent.getvibekit.ai", "alpha · testnet usdc · your wallet signs", "", "", "" color = "#ffb454" # glitch ghost on a few frames ghost = "" for g0, clip, dx, col in ((1.2, "inset(20% 0 55% 0)", -8, "#6fd3d3"), (5.0, "inset(60% 0 10% 0)", 7, "#ffb454"), (8.1, "inset(5% 0 70% 0)", 6, "#6fd3d3")): if g0 <= t < g0 + 2/FPS: ghost = f'<div class="face ghost" style="color:{col};clip-path:{clip};transform:translate({dx}px,0)">{html.escape(face)}</div>' hero_css = "font-size:50px" if end else "font-size:56px" face_px = 132 if len(face) <= 5 else 92 stage_top = 200 if end else 150 l1_html = html.escape(l1) if not end and l1.startswith("hi. i'm ") : l1_html = html.escape("hi. i'm ") + f'<span style="color:#ffb454">{html.escape(l1[8:])}</span>' if end: l1_html = f'<span style="color:#6fd3d3">{html.escape(l1)}</span>' return f'''<!doctype html><html><head><meta charset="utf-8"><style> *{{box-sizing:border-box;margin:0}} body{{background:#0a0b0e;color:#e9e1d4;width:{W}px;height:{H}px;overflow:hidden;font-family:"JetBrainsMono Nerd Font","JetBrains Mono",monospace;position:relative}} .grid i{{position:absolute;top:0;bottom:0;width:1px;background:rgba(42,39,35,.5)}} .grid b{{position:absolute;left:0;right:0;height:1px;background:rgba(42,39,35,.5)}} .rule{{position:absolute;top:0;left:0;right:0;height:6px;background:#c4a06a}} .kicker{{position:absolute;top:44px;left:64px;font-size:20px;letter-spacing:.22em;color:#605c56}} .kicker em{{color:#ffb454;font-style:normal;font-weight:700}} .stage{{position:absolute;left:64px;right:64px;top:{stage_top}px;display:flex;align-items:center;gap:56px}} .facewrap{{position:relative;width:440px;flex:none;display:flex;justify-content:center}} .face{{font-size:{face_px}px;font-weight:500;letter-spacing:-3px;line-height:1;color:{color};text-shadow:0 0 40px {color}88;white-space:nowrap}} .ghost{{position:absolute;top:0;left:0}} .copy{{display:flex;flex-direction:column;gap:14px;min-width:0}} .l1{{{hero_css};font-weight:700;letter-spacing:-2px;white-space:nowrap}} .l2{{font-size:{"22px" if end else "30px"};color:#8e8476;white-space:nowrap}} .composer{{position:absolute;left:64px;right:64px;bottom:120px;border-left:3px solid #ffb454;background:#111318;padding:18px 24px;font-size:28px;display:flex;gap:14px;align-items:center}} .composer span.p{{color:#ffb454}} .status{{position:absolute;left:64px;bottom:66px;font-size:20px;color:{"#6fd3d3" if mood in ("thinking","working") else "#8e8476"};letter-spacing:.04em}} .scan{{position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(180deg,rgba(255,255,255,.025) 0 1px,transparent 1px 3px)}} </style></head><body> <div class="grid">{GRID}</div><div class="rule"></div> <p class="kicker">◆ VIBEKIT <em>AGENT</em></p> <div class="stage"><div class="facewrap"><div class="face">{html.escape(face)}</div>{ghost}</div> <div class="copy"><p class="l1">{l1_html}</p><p class="l2">{html.escape(l2)}</p></div></div> {'' if end else f'<div class="composer"><span class="p">›</span><span>{html.escape(cmd)}{caret}</span></div><p class="status">{html.escape(status)}</p>'} <div class="scan"></div></body></html>''' n = int(DUR * FPS) for i in range(n): t = i / FPS p = f"{OUT}/f{i:04d}.html" open(p, "w").write(frame(t)) subprocess.run(["chromium", "--headless", "--disable-gpu", "--hide-scrollbars", "--force-device-scale-factor=1", f"--window-size={W},{H}", f"--screenshot={OUT}/f{i:04d}.png", f"file://{p}"], stderr=subprocess.DEVNULL, check=True) print("frames", n) -
segA.tape 792 B · in bundle
-
segB.tape 789 B · in bundle
-
-
SKILL.md 4.2 KB
--- name: marketing-content internal: true description: Make any vibekit or qt314 promotional asset — tweets and threads, promo videos of the TUI, title cards, OG/link-preview images, quote cards, lore dossier cards, qt314 animations, GIFs, and TUI screenshots — in the vibekit voice and on the brand palette. Use whenever asked to write social copy, announce or launch something, draft a tweet/thread/dev-diary post, make a promo or demo video, produce any image or video destined for social media, or use vibekit's colors, fonts, or kaomoji. --- # vibekit marketing content Everything that goes out in public: the copy, the images, the video. One voice across all of it, because a follower sees the tweet and the card in the same scroll — if they sound like different companies, the brand is doing nothing. ## Pick your asset Read the linked file before making the thing; each has the recipe and the failure modes. | you want | read | make it with | |---|---|---| | tweet, thread, dev diary, announcement, reply | `references/voice.md` | — | | promo or demo video of the TUI | `references/video.md` | `scripts/*.tape` + ffmpeg | | title card between video segments | `references/cards.md` | `scripts/make-card.sh` | | OG / link-preview image, quote card | `references/cards.md` | `PNG=1 scripts/make-card.sh` | | lore dossier card (the /lore series) | `references/cards.md` | `scripts/make-lore-card.sh` | | qt314 animation, GIF loop | `references/qt314.md` | `scripts/qt314-*.py` | | a still of the TUI | `references/cards.md` | a one-line vhs tape | | colors, fonts, sizes | `references/brand.md` | `assets/card.css` | Everything visual is HTML → headless chromium → png, then ffmpeg if it moves. That is one pipeline with different templates, not four pipelines, and it is why a still and a video card can't drift apart. Never hand-roll an ffmpeg `drawtext` card because it looks quicker — it is, and it looks it. ## Preflight Check before starting, not after a failed take: - `vhs` **and** `ttyd` — vhs fails every recording with "ttyd is not installed" and the message arrives after you've written the whole tape. - `chromium`, `ffmpeg`, `python3` — the card and qt314 renderers. - JetBrains Mono. Resolve the family with `fc-match "JetBrains Mono"` and use the name it reports (currently `JetBrainsMono Nerd Font`); an assumed family name silently falls back to sans-serif and the card stops looking like a terminal. - Recording live mainnet also needs the keystore daemon (`bun run apps/cli/src/index.ts keystore status`), and AI segments need the configured agent provider reachable (probe zs-proxy's `/v1/models`). Finished assets go in `out/` at the repo root — gitignored, and they stay that way. Never commit a video. Work files (tapes, frames, QA stills) go in a temp dir. Keep a silent master of every cut in `out/silent/` so a track can be swapped without re-encoding video. ## Voice, in one paragraph lowercase, short, one idea per post. Say the mechanism, not the adjective — "the keys never left the OS keychain" beats "secure key management", because the mechanism is the thing that's actually impressive. Alpha is stated as a feature, not apologized for. Kaomoji are vocabulary, not decoration, and qt314 is a character with her own register who signs her own posts. No hashtags, no "please check out", no visible effort to meme — if the joke needs explaining, cut it. `references/voice.md` has the corpus, the sigils, the thread format, and the drafts that get rejected; read it before writing copy, including image copy. ## Brand, in one paragraph Two hues and a ground: amber is structure and figures, teal is alive and touchable, red is danger only and never decoration. The palette's source of truth is `apps/tui/src/theme.ts`, mirrored for rendering in `assets/card.css` — use the CSS variables rather than pasting hex, so a theme change doesn't leave stale cards behind. Everything is monospace, title cards included, so the graphics read as part of the terminal. `references/brand.md` has the values, sizes, and card grammar. ## Reference images When an asset needs a photograph (the lore portraits are the current case), ask the user for a royalty-free image rather than sourcing one. Save the credit next to the file when the license asks for attribution.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.