Claude Skill

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

LLM Mart · 0 points · 7 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download initlabsai-vibekit-skills_marketing-content-654c02d.zip · 241 KB
Part of initlabsai/vibekit — 5 skills

Install

skills CLI npx skills add https://github.com/initlabsai/vibekit/tree/main/skills/marketing-content
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install initlabsai-vibekit@llmmart
Git 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:

  • 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.

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)">&nbsp;</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>&middot;</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>&ldquo;$QH&rdquo;</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.

No comments yet.

Reviews (0)

No reviews yet.

Related