# Generative Chat UI

> A music-video template. Give it an MP3 and a page of text; it renders a 1080x1920 kinetic-typography video of one chat conversation carried across a dozen invented messaging apps, cut to the beat, and exports a frame-accurate MP4 or GIF. Runs as a plain web page — no build step, no framework, no API keys.

## Read this first
`docs/AGENTS.md` is the procedure. It is written for you, in order, with the exact commands. This file is the index; that file is the job.

## The two inputs
- `song.mp3` — the user's track, in the project root, under exactly that name. A 120s demo click track ships in its place so the template runs before you change anything.
- `js/lyrics.js` (or `lyrics.txt`) — the user's words, one line per entry, in sung order. The sheet CYCLES to fill every slot, so check the printed slot-count against the line count and trim `lyric:true` beats in `js/timeline.js` if the words would repeat.

## The one file you edit for everything else
`js/config.js`. Nothing else is hardcoded. It holds: audio filename + duration, per-skin contact name/subtitle/avatar, the photo pool, the seven lyric hooks, the strobe rate, the takeover rotation, and the app cycle.

## The idea you must understand before editing
The words drive the choreography. Each entry in `config.hooks` is a regex tested against every lyric line; a line that matches gets that treatment **wherever it lands**, so editing words never means retiming beats. Set a hook to `null` to disable it.

| hook | what a matching line does |
| --- | --- |
| `strobe` | rips the UI through every skin and lands back where it started |
| `cutApp` | hard-cuts to the next app; the thread is never cleared |
| `loud` | renders full-frame at full weight instead of as a bubble |
| `takeover` | detonates the next full-screen interruption from the rotation |
| `photo` | attaches the next image from `config.photos` |
| `pinLeft` / `pinRight` | pins the thread to one side for that line |

Pick words the track actually **repeats**. A hook matching once fires once. The chorus word is usually right for `cutApp`; the loudest repeated word for `loud`.

## Console API (`window.MV`, in the page)
- `MV.slots()` — table of all 88 lyric slots: time, bubble-or-slam, content, filled?
- `MV.analyze()` — detect tempo + downbeat of song.mp3
- `MV.fit(seconds)` — linearly rescale the whole timeline to a new song length. **Run this first** when the track isn't 120s.
- `MV.snap(2)` — quantise every beat to the nearest 1/2 beat of the detected grid. Run after `fit`. `1` = whole beats, `4` = sixteenths.
- `MV.preview('radar-plan')` / `MV.clearPreview()` — drop a takeover on screen without scrubbing
- `MV.play() / .pause() / .seek(t) / .restart()`

Both `fit` and `snap` print a retimed `TIMELINE` and copy it to the clipboard. Paste it over the array in `js/timeline.js` to make it permanent.

## Keys (in the page)
`Space` play/pause · `R` restart · `←/→` ±5s · `H` hide HUD (do this before recording) · `S` tap-sync

## Rendering
`node render.mjs` — frame-accurate 1080x1920 MP4, driven off a virtual clock so it is identical on every machine and never drops a frame. `--from`/`--to`/`--fps` to scope it, `--gif` for a GIF. Needs Chrome at the standard macOS path and `ffmpeg` on PATH.

## Skins
Thirteen, all invented, none real: Radar, Vista, VistaLt, Orbit, Loop, Flare, Beacon, Verde, Bubble, Guild, Pigeon, Assistant, AssistantAlt. Defined in `css/skins.css` as CSS custom properties over one shared layout — status bar → app bar → thread → composer. A skin change is a token swap, so nothing reflows and the cut lands hard on the beat.

## Constraints
- Do not add real platform names or logos. The skins are deliberately invented; see LICENSE.
- Do not commit the user's `song.mp3` anywhere public unless they hold the rights.
- The engine reads `window.MV_CONFIG` once at load. Changing config means a reload, not a hot-swap.
