feat(videos): add kigi-graph-bauhaus HyperFrames promo video project

Keep env.md (local MiniMax API key read by tts.py) out of the repo.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 15:43:10 -04:00
1 parent 719638875b
commit cdeae6db89
32 files changed
+1201

No files matched your search

+3
View File
@@ -14,3 +14,6 @@ cred.md
# Upstream xai-org/grok-build reference clone
/grok-build/
# Local API keys (MiniMax TTS key for kigi-videos)
env.md
+107
View File
@@ -0,0 +1,107 @@
# HyperFrames Composition Project
## Skills — USE THESE FIRST
**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
**Doing anything with HyperFrames?** Start at `/hyperframes` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), confirms your brief up front (the intent layer), and routes every "make me a…" request (a video, a deck, a composition port) to the right workflow. Read it first, especially when there's no project context to orient you. The workflows it routes to:
- `/product-launch-video` — any **website** URL or brief / script → a product launch / SaaS / promo video, or a site tour / showcase featuring the site's own captured visuals.
- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
- `/talking-head-recut` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
- `/music-to-video` — a **music track** (audio file, video to pull audio from, or one generated from a mood brief) → beat-synced video (lyric / slideshow / kinetic promo). Music drives pacing; user-supplied images / videos are cut onto the same beat grid.
- `/slideshow` — a **presentation / pitch deck / interactive deck** — discrete slides, fragment reveals, branching, hotspot navigation, presenter mode. Output is a navigable deck, not a rendered video.
- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition) and the home of **companion mode** — co-create with the full HyperFrames toolbox; the original hyperframes authoring flow, any length.
**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-keyframes`, `/hyperframes-creative`, `/hyperframes-cli`, `/media-use`, `/hyperframes-audio`, `/hyperframes-registry`, `/figma`) and the full capability map live inside `/hyperframes` — it is the single source of truth for which skill handles which intent.
**Changing how real footage or images look or reveal?** Load `/media-use` and read its `references/media-treatments.md` before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” It governs how footage is treated, never whether media may be used. Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
> **Using a HyperFrames plugin?** Load skills from that installed bundle and follow
> its `hyperframes/references/plugin-installation.md` execution rules. Update via
> the plugin manager, not the standalone commands below.
>
> **Standalone skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
> the specific skill you need (the `/hyperframes` router does this automatically before
> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
> everything already installed — neither pulls the full set. Restart the agent session so
> newly installed skills load.
## Commands
```bash
npm run dev # human-operated foreground preview (blocks until stopped)
npx hyperframes preview --background # agent-safe persistent Studio preview
npx hyperframes preview --status # verify the persistent preview is listening
npx hyperframes preview --stop # stop it when review is finished
npm run check # lint + runtime + layout + motion + contrast (one command)
npm run render # render to MP4
npm run publish # publish and get a shareable link
npx hyperframes lint --verbose # include info-level findings
npx hyperframes lint --json # machine-readable output for CI
npx hyperframes docs <topic> # reference docs in terminal
```
> **Agents must use `npx hyperframes preview --background` for Studio handoff.** Do not rely
> on a shell/tool `run_in_background` wrapper around `npm run dev`: that foreground process
> remains owned by the invoking session and can disappear while the browser stays open,
> leaving refreshes at `ERR_CONNECTION_TIMED_OUT`. Verify with `preview --status`, keep it
> alive through review, and stop it explicitly with `preview --stop` afterward.
> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself.
## Documentation
**For quick reference**, use the local CLI docs command (no network required):
```bash
npx hyperframes docs <topic>
```
Topics: `data-attributes`, `gsap`, `compositions`, `rendering`, `examples`, `troubleshooting`
**For full documentation**, discover pages via the machine-readable index — do NOT guess URLs:
```
https://hyperframes.heygen.com/llms.txt
```
## Project Structure
- `index.html` — main composition (root timeline)
- `compositions/` — sub-compositions referenced via `data-composition-src`
- `meta.json` — project metadata (id, name)
- `transcript.json` — whisper word-level transcript (if generated)
## Linting — ALWAYS RUN AFTER CHANGES
After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
```bash
npm run check
```
Fix all errors before presenting the result. Warnings should be reviewed before rendering.
## Key Rules
1. Every timed element needs `data-start` and a duration. `data-start` is what marks it as timed; `data-track-index` is an optional Studio display lane the render never reads
2. Give timed visual elements `class="clip"`. The framework keys visibility off `data-start`, not the class, but the shared `.clip` CSS is what gives a scene its full-frame box, and `lint` warns without it
3. Register one paused root timeline per composition on `window.__timelines`:
```js
window.__timelines = window.__timelines || {};
window.__timelines["composition-id"] = gsap.timeline({ paused: true });
```
Scene timelines manually added to this root must not be paused. A paused
child does not advance when the root is seeked. The runtime activates
registered composition siblings, not arbitrary nested scene timelines.
4. A video with sound keeps it on the `<video>` (`data-has-audio="true"`, no `muted`). Use a separate `<audio>` for music, voiceover, replacement audio, J/L cuts, or audio detached in Studio. Silent footage and b-roll: `muted`.
5. Sub-compositions use `data-composition-src="compositions/file.html"` to reference other HTML files
6. Only deterministic logic — no `Date.now()`, no `Math.random()`, no network fetches
+107
View File
@@ -0,0 +1,107 @@
# HyperFrames Composition Project
## Skills — USE THESE FIRST
**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
**Doing anything with HyperFrames?** Start at `/hyperframes` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), confirms your brief up front (the intent layer), and routes every "make me a…" request (a video, a deck, a composition port) to the right workflow. Read it first, especially when there's no project context to orient you. The workflows it routes to:
- `/product-launch-video` — any **website** URL or brief / script → a product launch / SaaS / promo video, or a site tour / showcase featuring the site's own captured visuals.
- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
- `/talking-head-recut` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
- `/music-to-video` — a **music track** (audio file, video to pull audio from, or one generated from a mood brief) → beat-synced video (lyric / slideshow / kinetic promo). Music drives pacing; user-supplied images / videos are cut onto the same beat grid.
- `/slideshow` — a **presentation / pitch deck / interactive deck** — discrete slides, fragment reveals, branching, hotspot navigation, presenter mode. Output is a navigable deck, not a rendered video.
- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition) and the home of **companion mode** — co-create with the full HyperFrames toolbox; the original hyperframes authoring flow, any length.
**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-keyframes`, `/hyperframes-creative`, `/hyperframes-cli`, `/media-use`, `/hyperframes-audio`, `/hyperframes-registry`, `/figma`) and the full capability map live inside `/hyperframes` — it is the single source of truth for which skill handles which intent.
**Changing how real footage or images look or reveal?** Load `/media-use` and read its `references/media-treatments.md` before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” It governs how footage is treated, never whether media may be used. Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
> **Using a HyperFrames plugin?** Load skills from that installed bundle and follow
> its `hyperframes/references/plugin-installation.md` execution rules. Update via
> the plugin manager, not the standalone commands below.
>
> **Standalone skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
> the specific skill you need (the `/hyperframes` router does this automatically before
> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
> everything already installed — neither pulls the full set. Restart the agent session so
> newly installed skills load.
## Commands
```bash
npm run dev # human-operated foreground preview (blocks until stopped)
npx hyperframes preview --background # agent-safe persistent Studio preview
npx hyperframes preview --status # verify the persistent preview is listening
npx hyperframes preview --stop # stop it when review is finished
npm run check # lint + runtime + layout + motion + contrast (one command)
npm run render # render to MP4
npm run publish # publish and get a shareable link
npx hyperframes lint --verbose # include info-level findings
npx hyperframes lint --json # machine-readable output for CI
npx hyperframes docs <topic> # reference docs in terminal
```
> **Agents must use `npx hyperframes preview --background` for Studio handoff.** Do not rely
> on a shell/tool `run_in_background` wrapper around `npm run dev`: that foreground process
> remains owned by the invoking session and can disappear while the browser stays open,
> leaving refreshes at `ERR_CONNECTION_TIMED_OUT`. Verify with `preview --status`, keep it
> alive through review, and stop it explicitly with `preview --stop` afterward.
> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself.
## Documentation
**For quick reference**, use the local CLI docs command (no network required):
```bash
npx hyperframes docs <topic>
```
Topics: `data-attributes`, `gsap`, `compositions`, `rendering`, `examples`, `troubleshooting`
**For full documentation**, discover pages via the machine-readable index — do NOT guess URLs:
```
https://hyperframes.heygen.com/llms.txt
```
## Project Structure
- `index.html` — main composition (root timeline)
- `compositions/` — sub-compositions referenced via `data-composition-src`
- `meta.json` — project metadata (id, name)
- `transcript.json` — whisper word-level transcript (if generated)
## Linting — ALWAYS RUN AFTER CHANGES
After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
```bash
npm run check
```
Fix all errors before presenting the result. Warnings should be reviewed before rendering.
## Key Rules
1. Every timed element needs `data-start` and a duration. `data-start` is what marks it as timed; `data-track-index` is an optional Studio display lane the render never reads
2. Give timed visual elements `class="clip"`. The framework keys visibility off `data-start`, not the class, but the shared `.clip` CSS is what gives a scene its full-frame box, and `lint` warns without it
3. Register one paused root timeline per composition on `window.__timelines`:
```js
window.__timelines = window.__timelines || {};
window.__timelines["composition-id"] = gsap.timeline({ paused: true });
```
Scene timelines manually added to this root must not be paused. A paused
child does not advance when the root is seeked. The runtime activates
registered composition siblings, not arbitrary nested scene timelines.
4. A video with sound keeps it on the `<video>` (`data-has-audio="true"`, no `muted`). Use a separate `<audio>` for music, voiceover, replacement audio, J/L cuts, or audio detached in Studio. Silent footage and b-roll: `muted`.
5. Sub-compositions use `data-composition-src="compositions/file.html"` to reference other HTML files
6. Only deterministic logic — no `Date.now()`, no `Math.random()`, no network fetches
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
import json, pathlib, concurrent.futures as cf
from tts import tts
VOICE = "moss_audio_ce44fc67-7ce3-11f0-8de5-96e35d26fb85"
script = json.load(open("script.json", encoding="utf-8"))
def run(sc):
text = "".join(s.get("tts", s["zh"]) for s in sc["segs"])
info, subs = tts(text, f"audio/vo_{sc['id']}.mp3", VOICE, speed=1.05)
words = [w for seg in subs for w in seg["timestamped_words"]]
# map segment char ranges -> times
out, pos = [], 0
for s in sc["segs"]:
t = s.get("tts", s["zh"]); a, b = pos, pos + len(t); pos = b
ws = [w for w in words if w["word_begin"] >= a and w["word_end"] <= b and w["word"].strip(",。:;——?!、“” ")]
out.append({"zh": s["zh"], "en": s["en"], "t0": ws[0]["time_begin"]/1000, "t1": ws[-1]["time_end"]/1000})
return sc["id"], {"len": info["audio_length"]/1000, "segs": out}
with cf.ThreadPoolExecutor(6) as ex:
res = dict(ex.map(run, script))
json.dump({k: res[k] for k in sorted(res)}, open("vo_timing.json","w",encoding="utf-8"), ensure_ascii=False, indent=1)
for k in sorted(res): print(k, res[k]["len"], [ (round(s["t0"],2), round(s["t1"],2)) for s in res[k]["segs"]])
@@ -0,0 +1,12 @@
{
"$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
"paths": {
"blocks": "compositions",
"components": "compositions/components",
"assets": "assets"
},
"media": {
"autoProxy": true
}
}
+514
View File
@@ -0,0 +1,514 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Kigi /graph — Bauhaus</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@font-face { font-family: "Jost"; src: url("fonts/Jost.ttf") format("truetype"); font-weight: 100 900; }
@font-face { font-family: "Noto Sans SC"; src: url("fonts/NotoSansSC.ttf") format("truetype"); font-weight: 100 900; }
@font-face { font-family: "JetBrains Mono"; src: url("fonts/JetBrainsMono.ttf") format("truetype"); font-weight: 100 800; }
:root { --paper:#EEE6D6; --ink:#141414; --red:#D42A1E; --yellow:#F2B300; --blue:#1D4FA0; --grey:#BDB5A6; }
* { margin:0; padding:0; box-sizing:border-box; }
html, body { width:1920px; height:1080px; overflow:hidden; background:var(--paper); }
#root { position:relative; width:100%; height:100%; overflow:hidden; font-family:"Jost", sans-serif; color:var(--ink); }
.layer { position:absolute; inset:0; }
.abs { position:absolute; display:block; }
.mono { font-family:"JetBrains Mono", monospace; }
.zh { font-family:"Noto Sans SC", sans-serif; }
.sq { background:var(--red); }
.ci { background:var(--blue); border-radius:50%; }
.tri { background:var(--yellow); clip-path:polygon(50% 0, 100% 100%, 0 100%); }
.nowrap { white-space:nowrap; }
/* grid */
.gl { position:absolute; background:var(--ink); opacity:.08; }
.glv { width:1px; top:0; height:1080px; transform-origin:50% 0; }
.glh { height:1px; left:0; width:1920px; transform-origin:0 50%; }
/* s1 */
#s1-goal { left:1080px; top:120px; width:600px; height:600px; }
.s1-cell { position:absolute; width:200px; height:200px; background:var(--ink); }
#s1-goal-txt { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; color:var(--paper); }
#s1-goal-zh { font-size:180px; font-weight:900; line-height:1; }
#s1-goal-en { font-size:44px; font-weight:600; letter-spacing:.4em; margin-top:24px; padding-left:.4em; }
#s1-agent { left:240px; top:600px; width:120px; height:120px; }
#s1-agent-in { width:120px; height:120px; }
#s1-agent-dot { width:120px; height:120px; }
#s1-agent-lbl { left:240px; top:744px; font-size:22px; letter-spacing:.2em; font-weight:600; }
#s1-svg { position:absolute; left:0; top:0; }
#s1-floor { left:120px; top:720px; width:1680px; height:8px; background:var(--ink); transform-origin:0 50%; }
/* s2 */
#s2-field { left:0; top:0; width:1080px; height:1080px; background:var(--red); }
#s2-kigi { left:104px; top:70px; display:flex; font-size:330px; font-weight:800; line-height:1; color:var(--paper); letter-spacing:-.01em; }
#s2-kigi span { display:block; }
#s2-slash { left:120px; top:470px; height:170px; overflow:hidden; font-size:150px; font-weight:700; line-height:170px; color:var(--ink); width:0; }
#s2-cursor { left:120px; top:490px; width:80px; height:130px; background:var(--ink); }
#s2-tu { left:1188px; top:60px; font-size:560px; font-weight:900; line-height:1; }
#s2-circle { left:840px; top:420px; width:360px; height:360px; mix-blend-mode:multiply; }
#s2-tri { left:1500px; top:600px; width:300px; height:240px; transform-origin:50% 100%; }
#s2-sub { left:120px; top:720px; font-size:30px; font-weight:600; letter-spacing:.32em; color:var(--paper); }
/* s3 */
#s3-cam { position:absolute; inset:0; transform-origin:960px 360px; }
.s3-node { position:absolute; top:240px; width:240px; height:240px; }
.s3-ring { position:absolute; left:40px; top:40px; width:160px; height:160px; border:3px solid var(--paper); border-radius:50%; opacity:.9; }
.s3-orb { position:absolute; left:0; top:0; width:240px; height:240px; }
.s3-ball { position:absolute; left:96px; top:16px; width:48px; height:48px; }
.s3-tag { position:absolute; font-size:11px; color:var(--paper); letter-spacing:.12em; }
.s3-edge { position:absolute; top:356px; height:8px; width:360px; background:var(--ink); transform-origin:0 50%; }
.s3-ruler { position:absolute; top:372px; height:14px; width:360px; transform-origin:0 50%;
background:repeating-linear-gradient(90deg, var(--ink) 0 2px, transparent 2px 30px); }
.s3-arrow { position:absolute; top:340px; width:28px; height:40px; background:var(--ink); clip-path:polygon(0 0, 100% 50%, 0 100%); }
.s3-elbl { position:absolute; top:396px; font-size:22px; font-weight:500; }
#s3-big1 { left:120px; top:540px; font-size:104px; font-weight:800; line-height:1; color:var(--blue); letter-spacing:-.01em; }
#s3-big2 { left:120px; top:660px; font-size:104px; font-weight:800; line-height:1; letter-spacing:-.01em; }
.s3-leg { position:absolute; left:1200px; display:flex; align-items:center; gap:28px; height:64px; }
.s3-leg-shape { width:56px; height:56px; flex:none; }
.s3-leg-txt { font-size:30px; font-weight:700; letter-spacing:.08em; }
.s3-leg-txt .zh { font-size:26px; font-weight:500; margin-left:14px; letter-spacing:0; }
#s3-foot { left:1200px; top:780px; font-size:17px; opacity:.75; letter-spacing:.04em; }
/* s4 */
#s4-ground { background:var(--ink); }
#s4-head { left:120px; top:84px; font-size:24px; color:var(--paper); letter-spacing:.06em; overflow:hidden; height:32px; width:0; }
#s4-x3 { left:1560px; top:40px; font-size:150px; font-weight:800; line-height:1; color:var(--yellow); }
.s4-lane { position:absolute; left:360px; width:1320px; height:3px; background:var(--paper); transform-origin:0 50%; }
.s4-lbl { position:absolute; left:360px; font-size:20px; color:var(--paper); opacity:.8; }
.s4-node { position:absolute; width:96px; height:96px; }
.s4-w { position:absolute; width:72px; height:72px; }
.s4-commit { position:absolute; width:24px; height:24px; background:var(--paper); }
.s4-tri { position:absolute; left:1584px; width:96px; height:84px; transform-origin:50% 100%; }
.s4-check { position:absolute; left:1584px; width:96px; text-align:center; font-size:40px; font-weight:800; color:var(--ink); }
.s4-verdict { position:absolute; left:1176px; font-size:20px; text-align:right; width:380px; }
.s4-round { position:absolute; left:1700px; font-size:22px; color:var(--paper); width:100px; }
/* s5 */
#s5-yellow { background:var(--yellow); clip-path:circle(0% at 960px 690px); }
#s5-svg { position:absolute; left:0; top:0; }
.s5-node { position:absolute; width:96px; height:96px; }
.s5-name { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:var(--paper); font-size:22px; font-weight:600; letter-spacing:.06em; }
.s5-id { position:absolute; width:160px; text-align:center; font-size:14px; }
.s5-flesh { position:absolute; left:18px; top:18px; width:60px; height:60px; }
#s5-final { left:810px; top:560px; width:300px; height:260px; transform-origin:50% 100%; }
#s5-final-lbl { left:1150px; top:700px; font-size:22px; }
#s5-final-lbl b { display:block; font-family:"Jost"; font-size:34px; font-weight:700; letter-spacing:.12em; }
#s5-final-lbl i { display:block; font-style:normal; font-family:"Noto Sans SC"; font-size:24px; font-weight:700; }
#s5-bones-wrap { left:-220px; top:410px; width:800px; height:100px; transform:rotate(-90deg); }
#s5-bones { font-size:62px; font-weight:800; line-height:100px; text-align:center; letter-spacing:.02em; }
#s5-flesh-wrap { left:1340px; top:410px; width:800px; height:100px; transform:rotate(90deg); }
#s5-flesh-t { font-size:62px; font-weight:800; line-height:100px; text-align:center; color:var(--blue); letter-spacing:.02em; }
#s5-zh1 { left:270px; top:120px; writing-mode:vertical-rl; font-size:44px; font-weight:900; letter-spacing:.2em; }
#s5-zh2 { left:1594px; top:120px; writing-mode:vertical-rl; font-size:44px; font-weight:900; letter-spacing:.2em; color:var(--blue); }
/* logo */
#s5-logo { background:var(--paper); }
#s5-kigi { left:104px; top:110px; font-size:340px; font-weight:800; line-height:1; letter-spacing:-.01em; }
#s5-kigi span { display:inline-block; }
#s5-graph { left:120px; top:470px; font-size:130px; font-weight:700; color:var(--red); line-height:1; overflow:hidden; height:150px; width:0; }
#s5-l-sq { left:1080px; top:300px; width:200px; height:200px; }
#s5-l-ci { left:1320px; top:300px; width:200px; height:200px; }
#s5-l-tri { left:1560px; top:300px; width:220px; height:200px; transform-origin:50% 100%; }
#s5-rule { left:1080px; top:560px; width:700px; height:8px; background:var(--ink); transform-origin:0 50%; }
#s5-tag { left:1080px; top:596px; font-size:22px; letter-spacing:.06em; }
#s5-tag2 { left:1080px; top:640px; font-size:28px; font-weight:700; }
#s5-cursor { width:22px; height:40px; background:var(--red); display:inline-block; vertical-align:-6px; margin-left:8px; }
/* transitions */
.wipe { position:absolute; left:0; top:0; width:1920px; height:1080px; }
/* HUD */
#hud { position:absolute; inset:0; pointer-events:none; }
#hud .hud-l { left:40px; top:22px; font-size:15px; letter-spacing:.14em; font-weight:500; }
#hud .hud-l span { font-weight:800; }
.beat { position:absolute; top:24px; width:14px; height:14px; border:2px solid var(--ink); }
/* captions */
#band { position:absolute; left:0; top:880px; width:1920px; height:200px; background:var(--paper); }
#band-rule { left:120px; top:888px; width:1680px; height:3px; background:var(--ink); transform-origin:0 50%; }
#band-mark { left:120px; top:922px; width:28px; height:28px; background:var(--red); }
.cap { position:absolute; left:180px; top:906px; width:1620px; height:150px; }
.cap-zh { position:absolute; left:0; top:0; font-family:"Noto Sans SC"; font-size:42px; font-weight:700; line-height:60px; white-space:nowrap; }
.cap-en { position:absolute; left:0; top:68px; font-size:28px; font-weight:500; line-height:40px; letter-spacing:.02em; opacity:.72; white-space:nowrap; }
</style>
</head>
<body>
<div id="root" data-composition-id="main" data-start="0" data-duration="34" data-width="1920" data-height="1080">
<div id="bg" class="clip layer" data-start="0" data-duration="34" data-track-index="0" style="background:#EEE6D6"></div>
<div id="grid" class="clip layer" data-start="0" data-duration="34" data-track-index="1"></div>
<!-- ============ S1 · HOOK ============ -->
<section id="sc1" class="clip layer" data-start="0" data-duration="6" data-track-index="2">
<div id="s1-floor" class="abs"></div>
<svg id="s1-svg" width="1920" height="1080" viewBox="0 0 1920 1080">
<path id="s1-path" d="M840 660 C 700 300, 300 820, 420 420 S 900 180, 660 560 S 180 780, 300 300 S 780 120, 560 690 S 200 520, 380 640"
fill="none" stroke="#141414" stroke-width="6" stroke-linecap="round" />
</svg>
<div id="s1-goal" class="abs">
<div class="s1-cell" style="left:0;top:0"></div><div class="s1-cell" style="left:200px;top:0"></div><div class="s1-cell" style="left:400px;top:0"></div>
<div class="s1-cell" style="left:0;top:200px"></div><div class="s1-cell" style="left:200px;top:200px"></div><div class="s1-cell" style="left:400px;top:200px"></div>
<div class="s1-cell" style="left:0;top:400px"></div><div class="s1-cell" style="left:200px;top:400px"></div><div class="s1-cell" style="left:400px;top:400px"></div>
<div id="s1-goal-txt"><div id="s1-goal-zh" class="zh">目标</div><div id="s1-goal-en">GOAL</div></div>
</div>
<div id="s1-agent" class="abs"><div id="s1-agent-in" class="abs"><div id="s1-agent-dot" class="ci abs"></div></div></div>
<div id="s1-agent-lbl" class="abs nowrap">AGENT <span class="zh">智能体</span></div>
</section>
<!-- ============ S2 · TITLE ============ -->
<section id="sc2" class="clip layer" data-start="6" data-duration="4" data-track-index="2">
<div id="s2-field" class="abs"></div>
<div id="s2-kigi" class="abs"><span>K</span><span>I</span><span>G</span><span>I</span></div>
<div id="s2-slash" class="abs mono nowrap">/graph</div>
<div id="s2-cursor" class="abs"></div>
<div id="s2-tu" class="abs zh">图</div>
<div id="s2-circle" class="abs ci"></div>
<div id="s2-tri" class="abs tri"></div>
<div id="s2-sub" class="abs nowrap">GRAPH ENGINE · <span class="zh">图引擎</span></div>
</section>
<!-- ============ S3 · PRINCIPLE ============ -->
<section id="sc3" class="clip layer" data-start="10" data-duration="8" data-track-index="2">
<div id="s3-cam">
<div class="s3-edge" id="s3-e1" style="left:480px"></div><div class="s3-ruler" id="s3-r1" style="left:480px"></div>
<div class="s3-edge" id="s3-e2" style="left:1080px"></div><div class="s3-ruler" id="s3-r2" style="left:1080px"></div>
<div class="s3-arrow" id="s3-a1" style="left:812px"></div><div class="s3-arrow" id="s3-a2" style="left:1412px"></div>
<div class="s3-elbl mono nowrap" id="s3-l1" style="left:492px">fn ready() → bool</div>
<div class="s3-elbl mono nowrap" id="s3-l2" style="left:1092px">DepKind::Blocks</div>
<div class="s3-node sq" id="s3-n1" style="left:240px"><div class="s3-ring"></div><div class="s3-orb" id="s3-o1"><div class="s3-ball ci"></div></div></div>
<div class="s3-node sq" id="s3-n2" style="left:840px"><div class="s3-ring"></div><div class="s3-orb" id="s3-o2"><div class="s3-ball ci"></div></div>
<div class="s3-tag mono nowrap" id="s3-t1" style="left:98px;top:96px">PLAN</div>
<div class="s3-tag mono nowrap" id="s3-t2" style="left:103px;top:116px">ACT</div>
<div class="s3-tag mono nowrap" id="s3-t3" style="left:94px;top:136px">CHECK</div>
<div class="s3-tag nowrap" id="s3-t4" style="left:12px;top:210px;font-family:Jost;font-weight:700">AGENT LOOP · <span class="zh">智能体循环</span></div>
</div>
<div class="s3-node sq" id="s3-n3" style="left:1440px"><div class="s3-ring"></div><div class="s3-orb" id="s3-o3"><div class="s3-ball ci"></div></div></div>
</div>
<div id="s3-big1" class="abs nowrap">LOOP INSIDE.</div>
<div id="s3-big2" class="abs nowrap">EDGES: RUST.</div>
<div class="s3-leg" id="s3-g1" style="top:530px"><div class="s3-leg-shape ci"></div><div class="s3-leg-txt nowrap">AGENT<span class="zh">智能体</span></div></div>
<div class="s3-leg" id="s3-g2" style="top:610px"><div class="s3-leg-shape sq"></div><div class="s3-leg-txt nowrap">NODE<span class="zh">结构</span></div></div>
<div class="s3-leg" id="s3-g3" style="top:690px"><div class="s3-leg-shape tri"></div><div class="s3-leg-txt nowrap">SKEPTIC<span class="zh">审查者</span></div></div>
<div id="s3-foot" class="abs mono nowrap">KANDINSKY · BAUHAUS 1923 · ▲ YELLOW ■ RED ● BLUE</div>
</section>
<!-- ============ S4 · FAN-OUT ============ -->
<section id="sc4" class="clip layer" data-start="18" data-duration="6" data-track-index="2">
<div id="s4-ground" class="layer"></div>
<div id="s4-head" class="abs mono nowrap">$ KIGI_GRAPH_CONCURRENCY=3 · git worktree add ×3</div>
<div id="s4-x3" class="abs nowrap">×3</div>
<div id="s4-lanes"></div>
</section>
<!-- ============ S5 · gn-final → bones/flesh → logo ============ -->
<section id="sc5" class="clip layer" data-start="24" data-duration="10" data-track-index="2">
<div id="s5-yellow" class="layer"></div>
<svg id="s5-svg" width="1920" height="1080" viewBox="0 0 1920 1080"></svg>
<div id="s5-nodes"></div>
<div id="s5-final" class="abs tri"></div>
<div id="s5-final-lbl" class="abs mono nowrap">gn-final<b>FINAL VERIFICATION</b><i>最终验收 · 整个目标</i></div>
<div id="s5-bones-wrap" class="abs"><div id="s5-bones" class="nowrap">DETERMINISTIC BONES</div></div>
<div id="s5-flesh-wrap" class="abs"><div id="s5-flesh-t" class="nowrap">INTELLIGENT FLESH</div></div>
<div id="s5-zh1" class="abs zh">确定性的骨架</div>
<div id="s5-zh2" class="abs zh">智能的血肉</div>
<div id="s5-logo" class="layer">
<div id="s5-kigi" class="abs nowrap"><span>K</span><span>I</span><span>G</span><span>I</span></div>
<div id="s5-graph" class="abs mono nowrap">/graph</div>
<div id="s5-l-sq" class="abs sq"></div>
<div id="s5-l-ci" class="abs ci"></div>
<div id="s5-l-tri" class="abs tri"></div>
<div id="s5-rule" class="abs"></div>
<div id="s5-tag" class="abs mono nowrap">/graph &lt;objective&gt;<span id="s5-cursor"></span></div>
<div id="s5-tag2" class="abs nowrap">Kimi Code CLI · <span class="zh">社区构建版</span></div>
</div>
</section>
<!-- transitions -->
<div id="wipes" class="clip layer" data-start="0" data-duration="34" data-track-index="3">
<div id="w6" class="wipe" style="background:#D42A1E;width:1080px"></div>
<div id="w10" class="wipe" style="background:#F2B300"></div>
<div id="w18" class="wipe" style="background:#1D4FA0;border-radius:50%"></div>
<div id="w24" class="wipe" style="background:#D42A1E"></div>
<div id="w30" class="wipe" style="background:#141414"></div>
</div>
<!-- captions band -->
<div id="capband" class="clip layer" data-start="0" data-duration="34" data-track-index="4">
<div id="band"></div>
<div id="band-rule" class="abs"></div>
<div id="band-mark" class="abs"></div>
</div>
<div id="cap0" class="clip cap" data-start="1.44" data-duration="3.4" data-track-index="5"><div class="cap-zh" id="cap0-zh">一个智能体,扛不动一个庞大的目标。</div><div class="cap-en" id="cap0-en">One agent can't carry one huge goal.</div></div>
<div id="cap1" class="clip cap" data-start="6.44" data-duration="2.26" data-track-index="5"><div class="cap-zh" id="cap1-zh">Kigi 的答案:一张图。</div><div class="cap-en" id="cap1-en">Kigi's answer: a graph.</div></div>
<div id="cap2" class="clip cap" data-start="10.44" data-duration="2.42" data-track-index="5"><div class="cap-zh" id="cap2-zh">智能体的循环,活在节点内部;</div><div class="cap-en" id="cap2-en">The agentic loop lives inside each node;</div></div>
<div id="cap3" class="clip cap" data-start="12.88" data-duration="3.52" data-track-index="5"><div class="cap-zh" id="cap3-zh">节点之间的边,是确定性的 Rust。</div><div class="cap-en" id="cap3-en">the edges between them are deterministic Rust.</div></div>
<div id="cap4" class="clip cap" data-start="18.19" data-duration="3.03" data-track-index="5"><div class="cap-zh" id="cap4-zh">就绪的节点三路并行,各占一棵工作树,</div><div class="cap-en" id="cap4-en">Ready nodes run three at a time, each in its own worktree,</div></div>
<div id="cap5" class="clip cap" data-start="21.24" data-duration="2.87" data-track-index="5"><div class="cap-zh" id="cap5-zh">由怀疑论者逐一审查。</div><div class="cap-en" id="cap5-en">each checked by a skeptic.</div></div>
<div id="cap6" class="clip cap" data-start="24.44" data-duration="3.03" data-track-index="5"><div class="cap-zh" id="cap6-zh">最后,gn-final 重新验收整个目标。</div><div class="cap-en" id="cap6-en">Finally, gn-final re-verifies the whole objective.</div></div>
<div id="cap7" class="clip cap" data-start="27.49" data-duration="2.46" data-track-index="5"><div class="cap-zh" id="cap7-zh">确定性的骨架,智能的血肉。</div><div class="cap-en" id="cap7-en">Deterministic bones. Intelligent flesh.</div></div>
<div id="cap8" class="clip cap" data-start="29.97" data-duration="3.03" data-track-index="5"><div class="cap-zh" id="cap8-zh">Kigi /graph。</div><div class="cap-en" id="cap8-en">Kigi /graph.</div></div>
<!-- HUD -->
<div id="hud" class="clip" data-start="0" data-duration="34" data-track-index="6">
<div id="hud-0" class="abs mono nowrap hud-l">KIGI <span>/GRAPH</span> — <span>01</span>/05 · HOOK</div>
<div id="hud-1" class="abs mono nowrap hud-l">KIGI <span>/GRAPH</span> — <span>02</span>/05 · TITLE</div>
<div id="hud-2" class="abs mono nowrap hud-l">KIGI <span>/GRAPH</span> — <span>03</span>/05 · PRINCIPLE</div>
<div id="hud-3" class="abs mono nowrap hud-l">KIGI <span>/GRAPH</span> — <span>04</span>/05 · FAN-OUT</div>
<div id="hud-4" class="abs mono nowrap hud-l">KIGI <span>/GRAPH</span> — <span>05</span>/05 · VERIFY</div>
<div class="beat" id="bt0" style="left:1806px"></div><div class="beat" id="bt1" style="left:1828px"></div>
<div class="beat" id="bt2" style="left:1850px"></div><div class="beat" id="bt3" style="left:1872px"></div>
</div>
<audio id="mix" src="audio/mix.wav" data-start="0" data-duration="34" data-track-index="9" data-volume="1"></audio>
</div>
<script>
(function () {
const tl = gsap.timeline({ paused: true });
const $ = (s) => document.querySelector(s);
const mk = (parent, cls, css, html) => { const d = document.createElement("div"); d.className = cls; Object.assign(d.style, css); if (html) d.innerHTML = html; parent.appendChild(d); return d; };
const SVGNS = "http://www.w3.org/2000/svg";
/* ---------- grid (16×9 modules of 120px) ---------- */
const grid = $("#grid");
for (let i = 1; i < 16; i++) { const l = mk(grid, "gl glv", { left: i * 120 + "px" }); tl.fromTo(l, { scaleY: 0 }, { scaleY: 1, duration: 0.5, ease: "expo.out" }, (i % 4) * 0.125); }
for (let j = 1; j < 9; j++) { const l = mk(grid, "gl glh", { top: j * 120 + "px" }); tl.fromTo(l, { scaleX: 0 }, { scaleX: 1, duration: 0.5, ease: "expo.out" }, (j % 4) * 0.125); }
/* ---------- HUD: scene index + beat meter ---------- */
const sceneT = [0, 6, 10, 18, 24];
sceneT.forEach((t, i) => { if (i > 0) tl.fromTo("#hud-" + i, { opacity: 0 }, { opacity: 1, duration: 0.01 }, t); if (i < 4) tl.to("#hud-" + i, { opacity: 0, duration: 0.01 }, sceneT[i + 1]); });
for (let b = 0; b < 68; b++) {
const t = b * 0.5, k = b % 4;
tl.set("#bt" + k, { backgroundColor: "#D42A1E" }, t);
tl.set("#bt" + k, { backgroundColor: "rgba(0,0,0,0)" }, t + 0.5);
}
tl.set([".hud-l", ".beat"], { color: "#EEE6D6", borderColor: "#EEE6D6" }, 18);
tl.set([".hud-l", ".beat"], { color: "#141414", borderColor: "#141414" }, 24);
/* ---------- S1 · HOOK (0–6) ---------- */
tl.fromTo("#s1-floor", { scaleX: 0 }, { scaleX: 1, duration: 0.5, ease: "expo.out" }, 0);
tl.fromTo("#s1-goal", { y: -900 }, { y: 0, duration: 0.25, ease: "power3.in" }, 0.25);
tl.fromTo("#s1-goal", { scaleY: 1 }, { keyframes: [{ scaleY: 0.9, duration: 0.06 }, { scaleY: 1, duration: 0.44, ease: "elastic.out(1,0.4)" }], transformOrigin: "50% 100%" }, 0.5);
tl.fromTo("#s1-agent", { scale: 0 }, { scale: 1, duration: 0.5, ease: "back.out(2)" }, 1.0);
tl.fromTo("#s1-agent-lbl", { opacity: 0, x: -30 }, { opacity: 1, x: 0, duration: 0.25, ease: "expo.out" }, 1.0);
tl.to("#s1-agent-lbl", { opacity: 0, duration: 0.25 }, 3.5);
// push, push — the goal doesn't move
tl.to("#s1-agent", { x: 720, rotation: 360, duration: 0.5, ease: "power2.in" }, 1.5);
tl.to("#s1-agent-dot", { keyframes: [{ scaleX: 0.7, scaleY: 1.2, duration: 0.08 }, { scaleX: 1, scaleY: 1, duration: 0.3, ease: "elastic.out(1,0.3)" }], transformOrigin: "100% 50%" }, 2.0);
tl.to("#s1-goal", { keyframes: [{ x: 14, duration: 0.05 }, { x: 0, duration: 0.2, ease: "power2.out" }] }, 2.0);
tl.to("#s1-agent", { x: 480, rotation: 120, duration: 0.5, ease: "power2.out" }, 2.0);
tl.to("#s1-agent", { x: 720, rotation: 360, duration: 0.5, ease: "power2.in" }, 2.5);
tl.to("#s1-agent-dot", { keyframes: [{ scaleX: 0.7, scaleY: 1.2, duration: 0.08 }, { scaleX: 1, scaleY: 1, duration: 0.3, ease: "elastic.out(1,0.3)" }], transformOrigin: "100% 50%" }, 3.0);
tl.to("#s1-goal", { keyframes: [{ x: 14, duration: 0.05 }, { x: 0, duration: 0.2, ease: "power2.out" }] }, 3.0);
tl.to("#s1-agent", { x: 540, rotation: 200, duration: 0.5, ease: "power2.out" }, 3.0);
// lost: the agent wanders off the grid, leaving a scribble
const path = $("#s1-path"), L = path.getTotalLength();
gsap.set(path, { strokeDasharray: L, strokeDashoffset: L });
tl.fromTo(path, { opacity: 0 }, { opacity: 1, duration: 0.01 }, 3.5);
tl.fromTo(path, { strokeDashoffset: L }, { strokeDashoffset: 0, duration: 1.5, ease: "none" }, 3.5);
const p0 = path.getPointAtLength(0), kf = [];
for (let i = 1; i <= 48; i++) { const p = path.getPointAtLength((L * i) / 48); kf.push({ x: p.x - p0.x, y: p.y - p0.y, duration: 1.5 / 48, ease: "none" }); }
tl.fromTo("#s1-agent-in", { x: 0, y: 0 }, { keyframes: kf, immediateRender: false }, 3.5);
tl.to("#s1-agent-dot", { backgroundColor: "#BDB5A6", scale: 0.6, duration: 0.5, ease: "power2.inOut" }, 4.5);
tl.to(path, { stroke: "#BDB5A6", duration: 0.5 }, 5.0);
// the goal decomposes into nodes
tl.to("#s1-goal-txt", { opacity: 0, duration: 0.25 }, 5.0);
tl.to(".s1-cell", { backgroundColor: "#D42A1E", scale: 0.6, duration: 0.25, ease: "back.out(2)", stagger: 0.0625 }, 5.0);
/* ---------- S2 · TITLE (6–10) ---------- */
tl.fromTo("#w6", { x: -1080 }, { x: 0, duration: 0.25, ease: "power3.in" }, 5.75);
tl.set("#w6", { opacity: 0 }, 6.0);
tl.set("#w6", { opacity: 1 }, 0);
["6.5", "6.75", "7.0", "7.25"].forEach((t, i) =>
tl.fromTo(`#s2-kigi span:nth-child(${i + 1})`, { opacity: 0, scale: 1.5, y: -40 }, { opacity: 1, scale: 1, y: 0, duration: 0.125, ease: "power4.in" }, +t));
tl.fromTo("#s2-slash", { width: 0 }, { width: 560, duration: 0.5, ease: "steps(6)" }, 7.5);
tl.fromTo("#s2-cursor", { x: 0, opacity: 0 }, { x: 560, opacity: 1, duration: 0.5, ease: "steps(6)" }, 7.5);
tl.to("#s2-cursor", { opacity: 0, duration: 0.01, repeat: 7, yoyo: true, repeatDelay: 0.24 }, 8.0);
tl.fromTo("#s2-tu", { y: -900 }, { y: 0, duration: 0.25, ease: "power4.in" }, 7.75);
tl.to("#s2-tu", { keyframes: [{ scaleY: 0.92, duration: 0.05 }, { scaleY: 1, duration: 0.4, ease: "elastic.out(1,0.35)" }], transformOrigin: "50% 100%" }, 8.0);
tl.fromTo("#s2-circle", { scale: 0 }, { scale: 1, duration: 0.5, ease: "back.out(1.8)" }, 8.5);
tl.fromTo("#s2-tri", { scaleY: 0 }, { scaleY: 1, duration: 0.25, ease: "expo.out" }, 9.0);
tl.fromTo("#s2-sub", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(8)" }, 9.0);
tl.to("#s2-circle", { x: 120, duration: 0.5, ease: "power2.inOut" }, 9.5);
/* ---------- transition 10 (yellow, left→right) ---------- */
const wipe = (id, t, from, mid, to) => {
tl.fromTo(id, from, { ...mid, duration: 0.25, ease: "power3.in", immediateRender: true }, t - 0.25);
tl.to(id, { ...to, duration: 0.25, ease: "power3.out" }, t);
};
wipe("#w10", 10, { x: -1920 }, { x: 0 }, { x: 1920 });
/* ---------- S3 · PRINCIPLE (10–18) ---------- */
[1, 2, 3].forEach((n, i) => {
tl.fromTo("#s3-n" + n, { y: -480 }, { y: 0, duration: 0.25, ease: "power3.in" }, 10 + i * 0.25);
tl.fromTo("#s3-o" + n, { rotation: i * 120 }, { rotation: i * 120 + 360 * 4, duration: 8, ease: "none" }, 10);
});
// zoom into the middle node: the loop lives inside
tl.fromTo("#s3-cam", { scale: 1 }, { scale: 2.6, duration: 0.5, ease: "expo.inOut" }, 11.0);
tl.fromTo(["#s3-t1", "#s3-t2", "#s3-t3"], { opacity: 0 }, { opacity: 1, duration: 0.125, stagger: 0.25 }, 11.5);
tl.fromTo("#s3-t4", { opacity: 0 }, { opacity: 1, duration: 0.25 }, 12.0);
tl.to("#s3-cam", { scale: 1, duration: 0.5, ease: "expo.inOut" }, 12.5);
tl.to(["#s3-t1", "#s3-t2", "#s3-t3", "#s3-t4"], { opacity: 0, duration: 0.25 }, 12.75);
tl.fromTo("#s3-big1", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(12)" }, 12.0);
// edges: ruler-straight, stepped — deterministic
tl.fromTo(["#s3-e1", "#s3-e2"], { scaleX: 0 }, { scaleX: 1, duration: 1.0, ease: "steps(12)", stagger: 0.5 }, 13.0);
tl.fromTo(["#s3-r1", "#s3-r2"], { scaleX: 0 }, { scaleX: 1, duration: 1.0, ease: "steps(12)", stagger: 0.5 }, 13.0);
tl.fromTo(["#s3-a1", "#s3-a2"], { scale: 0 }, { scale: 1, duration: 0.25, ease: "back.out(3)", stagger: 0.5 }, 14.0);
tl.fromTo(["#s3-l1", "#s3-l2"], { opacity: 0, y: 12 }, { opacity: 1, y: 0, duration: 0.25, ease: "expo.out", stagger: 0.5 }, 14.0);
tl.fromTo("#s3-big2", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(12)" }, 14.5);
// legend: Kandinsky's triad
[1, 2, 3].forEach((n, i) => tl.fromTo("#s3-g" + n, { x: 80, opacity: 0 }, { x: 0, opacity: 1, duration: 0.25, ease: "expo.out" }, 16 + i * 0.5));
tl.fromTo("#s3-foot", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(16)" }, 17.0);
/* ---------- transition 18 (blue circle iris) ---------- */
tl.fromTo("#w18", { scale: 0, x: 0 }, { scale: 2.4, duration: 0.25, ease: "power3.in", immediateRender: true }, 17.75);
tl.to("#w18", { scale: 0, duration: 0.25, ease: "power3.out" }, 18.0);
/* ---------- S4 · FAN-OUT (18–24) ---------- */
tl.fromTo("#s4-head", { width: 0 }, { width: 900, duration: 1.0, ease: "steps(36)" }, 18.0);
tl.fromTo("#s4-x3", { scale: 0 }, { scale: 1, duration: 0.25, ease: "back.out(2.2)" }, 18.5);
const lanesEl = $("#s4-lanes");
const lanes = [{ y: 300, id: "gn-386aaa87", name: "api" }, { y: 500, id: "gn-0d903d89", name: "cli" }, { y: 700, id: "gn-527d0336", name: "schema" }];
lanes.forEach((ln, i) => {
const off = i * 0.125;
const lane = mk(lanesEl, "s4-lane", { top: ln.y + "px" });
const lbl = mk(lanesEl, "s4-lbl mono nowrap", { top: ln.y - 90 + "px" }, `worktree/${ln.id} · ${ln.name}`);
const node = mk(lanesEl, "s4-node sq", { left: "360px", top: ln.y - 48 + "px" });
const w = mk(lanesEl, "s4-w ci", { left: "372px", top: ln.y - 36 + "px" });
const tri = mk(lanesEl, "s4-tri tri", { top: ln.y - 84 + "px" });
const chk = mk(lanesEl, "s4-check", { top: ln.y - 66 + "px" }, "✓");
const verdict = mk(lanesEl, "s4-verdict mono nowrap", { top: ln.y + 50 + "px", color: "#F2B300" }, "NODE_VERDICT: achieved");
const round = mk(lanesEl, "s4-round mono nowrap", { top: ln.y - 14 + "px" }, "1/3");
tl.fromTo(lane, { scaleX: 0 }, { scaleX: 1, duration: 0.5, ease: "expo.out" }, 18.0 + off);
tl.fromTo(lbl, { opacity: 0 }, { opacity: 0.8, duration: 0.25 }, 18.25 + off);
tl.fromTo(node, { x: -240 }, { x: 0, duration: 0.25, ease: "expo.out" }, 18.5);
tl.fromTo(tri, { scaleY: 0 }, { scaleY: 1, duration: 0.25, ease: "expo.out" }, 18.75 + off);
tl.fromTo(round, { opacity: 0 }, { opacity: 1, duration: 0.25 }, 18.75 + off);
tl.fromTo(w, { scale: 0 }, { scale: 1, duration: 0.25, ease: "back.out(2)" }, 18.75);
// the worker builds: four steps, a commit dropped on every step
const failRound = i === 1;
const steps = [0, 1, 2, 3];
steps.forEach((s) => {
const t = 19.0 + s * 0.5 + off;
tl.to(w, { x: 240 * (s + 1), duration: 0.25, ease: "power2.inOut" }, t);
const c = mk(lanesEl, "s4-commit", { left: 480 + 240 * s + 24 + "px", top: ln.y - 12 + "px" });
tl.fromTo(c, { scale: 0 }, { scale: 1, duration: 0.125, ease: "back.out(3)" }, t + 0.125);
});
// skeptic reviews
if (failRound) {
const gaps = mk(lanesEl, "s4-verdict mono nowrap", { top: ln.y + 50 + "px", color: "#D42A1E" }, "NODE_VERDICT: not_achieved");
tl.fromTo(tri, { backgroundColor: "#F2B300" }, { backgroundColor: "#D42A1E", duration: 0.01, immediateRender: false }, 21.0);
tl.fromTo(gaps, { opacity: 0 }, { opacity: 1, duration: 0.01 }, 21.0);
tl.to(gaps, { opacity: 0, duration: 0.01 }, 22.0);
tl.to(tri, { backgroundColor: "#F2B300", duration: 0.01 }, 21.5);
tl.to(w, { x: 480, duration: 0.25, ease: "power2.inOut" }, 21.25);
const round2 = mk(lanesEl, "s4-round mono nowrap", { top: ln.y - 14 + "px", color: "#F2B300" }, "2/3");
tl.fromTo(round2, { opacity: 0 }, { opacity: 1, duration: 0.01 }, 21.5);
tl.to(round, { opacity: 0, duration: 0.01 }, 21.5);
tl.to(w, { x: 960, duration: 0.5, ease: "power2.inOut" }, 22.0);
tl.to(tri, { keyframes: [{ scale: 1.3, duration: 0.06 }, { scale: 1, duration: 0.2 }] }, 23.0);
tl.fromTo(chk, { opacity: 0, scale: 2 }, { opacity: 1, scale: 1, duration: 0.125, ease: "power4.in" }, 23.0);
tl.fromTo(verdict, { opacity: 0 }, { opacity: 1, duration: 0.01 }, 23.0);
} else {
const tv = i === 0 ? 22.0 : 22.5;
tl.to(tri, { keyframes: [{ scale: 1.3, duration: 0.06 }, { scale: 1, duration: 0.2 }] }, tv);
tl.fromTo(chk, { opacity: 0, scale: 2 }, { opacity: 1, scale: 1, duration: 0.125, ease: "power4.in" }, tv);
tl.fromTo(verdict, { opacity: 0 }, { opacity: 1, duration: 0.01 }, tv);
}
// sequential merge-back
tl.to(node, { x: 1320 - 96, backgroundColor: "#F2B300", duration: 0.25, ease: "expo.inOut" }, 23.5 + i * 0.125);
});
/* ---------- transition 24 (red, top→down) ---------- */
wipe("#w24", 24, { y: -1080 }, { y: 0 }, { y: 1080 });
/* ---------- S5 · gn-final (24–30) ---------- */
const svg = $("#s5-svg"), nodesEl = $("#s5-nodes");
const N = { core: [960, 100, "gn-dd5e607e"], api: [600, 260, "gn-386aaa87"], cli: [960, 260, "gn-0d903d89"], schema: [1320, 260, "gn-527d0336"], docs: [780, 420, "gn-28eb34d2"], tests: [1140, 420, "gn-5b22cf22"] };
const layerT = { core: 24.0, api: 24.5, cli: 24.5, schema: 24.5, docs: 25.0, tests: 25.0 };
const path5 = (d, w, t, dur, color) => {
const p = document.createElementNS(SVGNS, "path");
p.setAttribute("d", d); p.setAttribute("fill", "none"); p.setAttribute("stroke", color || "#141414");
p.setAttribute("stroke-width", w); p.setAttribute("stroke-linecap", "square"); svg.appendChild(p);
const len = p.getTotalLength(); p.style.strokeDasharray = len;
tl.fromTo(p, { strokeDashoffset: len }, { strokeDashoffset: 0, duration: dur, ease: "power2.inOut" }, t);
return p;
};
// structural edges (orthogonal routing via a bus between layers)
const E = [["core", "api"], ["core", "cli"], ["core", "schema"], ["api", "docs"], ["cli", "docs"], ["cli", "tests"], ["schema", "tests"]];
E.forEach(([a, b], k) => {
const [ax, ay] = N[a], [bx, by] = N[b], busY = ay + 96 + (by - ay - 96) / 2;
path5(`M${ax} ${ay + 96} V${busY} H${bx} V${by}`, 6, (layerT[b]) - 0.25, 0.5);
});
// every node → gn-final
Object.entries(N).forEach(([k, [x, y]], i) => {
const busY = 530 - 0;
path5(`M${x + (k === "core" || k === "cli" ? 30 : 0)} ${y + 96} V${busY} H960 V560`, 3, 26.0, 0.5, "#141414");
});
Object.entries(N).forEach(([k, [x, y, id]]) => {
const n = mk(nodesEl, "s5-node sq", { left: x - 48 + "px", top: y + "px" }, `<div class="s5-name">${k}</div>`);
const lab = mk(nodesEl, "s5-id mono nowrap", { left: x + 52 + "px", top: y + 4 + "px", width: "auto", textAlign: "left" }, id);
const flesh = mk(n, "s5-flesh ci", {});
tl.fromTo(n, { scale: 0 }, { scale: 1, duration: 0.25, ease: "back.out(2.2)" }, layerT[k]);
tl.fromTo(lab, { opacity: 0 }, { opacity: 1, duration: 0.125 }, layerT[k] + 0.25);
// bones & flesh: nodes become outlined bones, then fill with agents
tl.to(n, { backgroundColor: "rgba(0,0,0,0)", boxShadow: "inset 0 0 0 6px #141414", duration: 0.01 }, 27.5);
tl.to(n.querySelector(".s5-name"), { color: "#141414", opacity: 0, duration: 0.01 }, 27.5);
tl.fromTo(flesh, { scale: 0 }, { scale: 1, duration: 0.25, ease: "back.out(2.4)" }, 28.5 + Object.keys(N).indexOf(k) * 0.125);
tl.to(lab, { opacity: 0, duration: 0.01 }, 27.5);
});
tl.fromTo("#s5-final", { scaleY: 0 }, { scaleY: 1, duration: 0.5, ease: "expo.out" }, 26.5);
tl.fromTo("#s5-final-lbl", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(10)" }, 26.5);
// the verdict: yellow floods out of the triangle
tl.fromTo("#s5-yellow", { clipPath: "circle(0% at 960px 690px)" }, { clipPath: "circle(150% at 960px 690px)", duration: 0.5, ease: "expo.out" }, 27.5);
tl.to("#s5-final", { backgroundColor: "#141414", duration: 0.01 }, 27.5);
tl.to("#s5-final-lbl", { opacity: 0, duration: 0.01 }, 27.5);
tl.fromTo("#s5-bones", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(19)" }, 27.75);
tl.fromTo("#s5-zh1", { clipPath: "inset(0 0 100% 0)" }, { clipPath: "inset(0 0 0% 0)", duration: 0.5, ease: "steps(6)" }, 28.0);
tl.fromTo("#s5-flesh-t", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(17)" }, 28.75);
tl.fromTo("#s5-zh2", { clipPath: "inset(0 0 100% 0)" }, { clipPath: "inset(0 0 0% 0)", duration: 0.5, ease: "steps(5)" }, 29.0);
/* ---------- transition 30 (ink, right→left) + LOGO (30–34) ---------- */
wipe("#w30", 30, { x: 1920 }, { x: 0 }, { x: -1920 });
tl.fromTo("#s5-logo", { opacity: 0 }, { opacity: 1, duration: 0.01 }, 30.0);
["30.25", "30.375", "30.5", "30.625"].forEach((t, i) =>
tl.fromTo(`#s5-kigi span:nth-child(${i + 1})`, { y: -500, opacity: 0 }, { y: 0, opacity: 1, duration: 0.125, ease: "power4.in" }, +t));
tl.fromTo("#s5-graph", { width: 0 }, { width: 500, duration: 0.5, ease: "steps(6)" }, 30.5);
tl.fromTo("#s5-l-sq", { y: -700 }, { y: 0, duration: 0.25, ease: "power4.in" }, 30.75);
tl.fromTo("#s5-l-ci", { scale: 0 }, { scale: 1, duration: 0.5, ease: "back.out(2)" }, 31.25);
tl.fromTo("#s5-l-tri", { scaleY: 0 }, { scaleY: 1, duration: 0.25, ease: "expo.out" }, 32.0);
tl.fromTo("#s5-rule", { scaleX: 0 }, { scaleX: 1, duration: 0.5, ease: "expo.out" }, 32.0);
tl.fromTo("#s5-tag", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "steps(19)" }, 32.25);
tl.fromTo("#s5-tag2", { opacity: 0, y: 16 }, { opacity: 1, y: 0, duration: 0.25, ease: "expo.out" }, 32.75);
tl.to("#s5-cursor", { opacity: 0, duration: 0.01, repeat: 3, yoyo: true, repeatDelay: 0.49 }, 32.75);
// final downbeat: the three shapes nudge together on 33.0
tl.to(["#s5-l-sq", "#s5-l-ci", "#s5-l-tri"], { y: -20, duration: 0.125, ease: "power2.out", yoyo: true, repeat: 1 }, 33.0);
/* ---------- caption band ---------- */
tl.fromTo("#band-rule", { scaleX: 0 }, { scaleX: 1, duration: 1.0, ease: "expo.out" }, 0.5);
tl.fromTo("#band-mark", { scale: 0 }, { scale: 1, duration: 0.25, ease: "back.out(2)" }, 1.5);
tl.fromTo("#cap0-zh", { clipPath: "inset(0 0 100% 0)", y: 24 }, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 0.25, ease: "expo.out" }, 1.44);
tl.fromTo("#cap0-en", { opacity: 0, x: -24 }, { opacity: 0.72, x: 0, duration: 0.25, ease: "expo.out" }, 1.565);
tl.fromTo("#cap1-zh", { clipPath: "inset(0 0 100% 0)", y: 24 }, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 0.25, ease: "expo.out" }, 6.44);
tl.fromTo("#cap1-en", { opacity: 0, x: -24 }, { opacity: 0.72, x: 0, duration: 0.25, ease: "expo.out" }, 6.565);
tl.fromTo("#cap2-zh", { clipPath: "inset(0 0 100% 0)", y: 24 }, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 0.25, ease: "expo.out" }, 10.44);
tl.fromTo("#cap2-en", { opacity: 0, x: -24 }, { opacity: 0.72, x: 0, duration: 0.25, ease: "expo.out" }, 10.565);
tl.fromTo("#cap3-zh", { clipPath: "inset(0 0 100% 0)", y: 24 }, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 0.25, ease: "expo.out" }, 12.88);
tl.fromTo("#cap3-en", { opacity: 0, x: -24 }, { opacity: 0.72, x: 0, duration: 0.25, ease: "expo.out" }, 13.005);
tl.fromTo("#cap4-zh", { clipPath: "inset(0 0 100% 0)", y: 24 }, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 0.25, ease: "expo.out" }, 18.19);
tl.fromTo("#cap4-en", { opacity: 0, x: -24 }, { opacity: 0.72, x: 0, duration: 0.25, ease: "expo.out" }, 18.315);
tl.fromTo("#cap5-zh", { clipPath: "inset(0 0 100% 0)", y: 24 }, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 0.25, ease: "expo.out" }, 21.24);
tl.fromTo("#cap5-en", { opacity: 0, x: -24 }, { opacity: 0.72, x: 0, duration: 0.25, ease: "expo.out" }, 21.365);
tl.fromTo("#cap6-zh", { clipPath: "inset(0 0 100% 0)", y: 24 }, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 0.25, ease: "expo.out" }, 24.44);
tl.fromTo("#cap6-en", { opacity: 0, x: -24 }, { opacity: 0.72, x: 0, duration: 0.25, ease: "expo.out" }, 24.565);
tl.fromTo("#cap7-zh", { clipPath: "inset(0 0 100% 0)", y: 24 }, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 0.25, ease: "expo.out" }, 27.49);
tl.fromTo("#cap7-en", { opacity: 0, x: -24 }, { opacity: 0.72, x: 0, duration: 0.25, ease: "expo.out" }, 27.615);
tl.fromTo("#cap8-zh", { clipPath: "inset(0 0 100% 0)", y: 24 }, { clipPath: "inset(0 0 0% 0)", y: 0, duration: 0.25, ease: "expo.out" }, 29.97);
tl.fromTo("#cap8-en", { opacity: 0, x: -24 }, { opacity: 0.72, x: 0, duration: 0.25, ease: "expo.out" }, 30.095);
window.__timelines["main"] = tl;
})();
</script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
{
"id": "kigi-graph-bauhaus",
"name": "kigi-graph-bauhaus",
"createdAt": "2026-10-04T17:49:43.118Z"
}
+211
View File
@@ -0,0 +1,211 @@
"""Procedural 120 BPM score for the 34 s cut: every hit is placed on the same beat grid the picture uses."""
import json, subprocess, numpy as np, soundfile as sf
from scipy.signal import fftconvolve, butter, sosfilt
FFMPEG = r"C:\Users\zhang\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-9.0.2-full_build\bin\ffmpeg.exe"
SR, TOTAL, BEAT = 48000, 34.0, 0.5
N = int(SR * TOTAL)
rng = np.random.default_rng(7)
bus = {k: np.zeros(N) for k in ["drums", "music", "fx"]}
def env(n, decay, attack=0.002):
t = np.arange(n) / SR
a = np.minimum(1, t / attack) if attack > 0 else 1
return a * np.exp(-t / decay)
def put(b, t, sig, gain=1.0):
i = int(round(t * SR))
if i >= N: return
sig = sig[: N - i]
bus[b][i : i + len(sig)] += sig * gain
def hz(m): return 440 * 2 ** ((m - 69) / 12)
def lp(x, f, order=2): return sosfilt(butter(order, f, "low", fs=SR, output="sos"), x)
def hp(x, f, order=2): return sosfilt(butter(order, f, "high", fs=SR, output="sos"), x)
def bp(x, lo, hi): return sosfilt(butter(2, [lo, hi], "band", fs=SR, output="sos"), x)
# ---------- instruments ----------
def kick(g=1.0):
n = int(0.45 * SR); t = np.arange(n) / SR
f = 45 + 110 * np.exp(-t / 0.03)
s = np.sin(2 * np.pi * np.cumsum(f) / SR) * env(n, 0.22)
s += hp(rng.standard_normal(n), 2000) * env(n, 0.004) * 0.4
return np.tanh(s * 1.6) * g
def hat(open_=False):
n = int((0.25 if open_ else 0.06) * SR)
return hp(rng.standard_normal(n), 7000, 4) * env(n, 0.09 if open_ else 0.018) * 0.35
def clap():
n = int(0.3 * SR); s = bp(rng.standard_normal(n), 900, 3000)
e = sum(env(n, 0.008) * (np.arange(n) >= int(k * SR)) for k in (0, 0.011, 0.022)) + env(n, 0.09) * 0.6
return s * e * 0.55
def tick():
n = int(0.03 * SR); return hp(rng.standard_normal(n), 4000) * env(n, 0.004) * 0.5
def marimba(m, dur=0.35, g=0.3):
n = int(dur * 2 * SR); t = np.arange(n) / SR; f = hz(m)
s = np.sin(2*np.pi*f*t)*env(n, dur*0.6) + 0.35*np.sin(2*np.pi*f*3.93*t)*env(n, dur*0.12) + 0.1*np.sin(2*np.pi*f*9.2*t)*env(n, 0.02)
return s * g
def bass(m, dur=0.24, g=0.5):
n = int(dur * SR); t = np.arange(n) / SR; f = hz(m)
saw = 2 * ((f * t) % 1) - 1
s = lp(saw, 500) + 0.6 * np.sin(2 * np.pi * f * t)
e = env(n, dur * 0.8, 0.004) * np.minimum(1, (n - np.arange(n)) / (0.01 * SR))
return np.tanh(s * e * 1.4) * g
def pad(ms, dur, g=0.11):
n = int(dur * SR); t = np.arange(n) / SR; s = np.zeros(n)
for m in ms:
for d in (-0.08, 0.0, 0.08):
f = hz(m + d); s += 2 * ((f * t + rng.random()) % 1) - 1
s = lp(s, 1400)
a = np.minimum(1, t / 0.4) * np.minimum(1, (dur - t) / 0.5)
return s * a * g / len(ms)
def impact(g=1.0):
n = int(2.5 * SR); t = np.arange(n) / SR
boom = np.sin(2 * np.pi * (38 + 40 * np.exp(-t / 0.08)) * t) * env(n, 0.7)
crash = hp(rng.standard_normal(n), 3000) * env(n, 0.9) * 0.25
return (np.tanh(boom * 1.4) * 0.9 + crash) * g
def riser(dur=1.0, g=0.35):
n = int(dur * SR); t = np.arange(n) / SR
x = rng.standard_normal(n); out = np.zeros(n); seg = int(0.05 * SR)
for i in range(0, n, seg): # stepped filter sweep
f = 400 + 7000 * (i / n) ** 2
out[i:i+seg] = bp(x[i:i+seg], f, min(f * 1.6, 20000))
return out * (t / dur) ** 2 * g
def whoosh(g=0.5):
n = int(0.35 * SR); t = np.arange(n) / SR
s = bp(rng.standard_normal(n), 600, 6000)
return s * np.sin(np.pi * t / t[-1]) ** 2 * g
def stamp(g=0.8, pitch=60):
n = int(0.4 * SR); t = np.arange(n) / SR
s = np.sin(2 * np.pi * (hz(pitch - 24) + 80 * np.exp(-t / 0.02)) * t) * env(n, 0.12)
s += bp(rng.standard_normal(n), 1500, 5000) * env(n, 0.01) * 0.6
return np.tanh(s * 2) * g
def buzz(g=0.35):
n = int(0.45 * SR); t = np.arange(n) / SR
s = np.sign(np.sin(2 * np.pi * 110 * t)) + np.sign(np.sin(2 * np.pi * 116.5 * t))
return lp(s, 1800) * env(n, 0.18) * g
# ---------- arrangement (A minor; bar = 2 s) ----------
CHORDS = {0: (57, [57, 60, 64]), 1: (53, [53, 57, 60]), 2: (48, [52, 55, 60]), 3: (55, [55, 59, 62])} # Am F C G
def chord(bar): return CHORDS[bar % 4]
def on(t0, t1, step):
return [round(x, 4) for x in np.arange(t0, t1 - 1e-9, step)]
# intro: metronome eighths 0-2, kick at 0.5 (square drop)
for t in on(0, 2, 0.25): put("drums", t, tick(), 0.6 if (t * 4) % 2 == 0 else 0.3)
put("fx", 0.5, impact(0.7)); put("drums", 0.5, kick(1.0))
def groove(t0, t1, hats=0.5, claps=True, kicks=True, hat16=False):
for t in on(t0, t1, BEAT):
if kicks: put("drums", t, kick())
if claps and round((t / BEAT)) % 2 == 1: put("drums", t, clap())
for t in on(t0, t1, 0.125 if hat16 else 0.25):
off = abs((t / 0.5) % 1 - 0.5) < 1e-6
put("drums", t, hat(open_=off and not hat16), hats * (1.0 if off else 0.6))
def ostinato(t0, t1, octave=12, g=0.26, step=0.125, cycle=3):
for k, t in enumerate(on(t0, t1, step)):
root, tri = chord(int(t // 2)); note = tri[[0, 1, 2, 1, 2][k % cycle] if cycle == 5 else k % 3]
put("music", t, marimba(note + octave, 0.3, g * (1.0 if k % 4 == 0 else 0.7)))
def bassline(t0, t1, g=0.5):
for t in on(t0, t1, 0.25):
root, _ = chord(int(t // 2)); pos = round((t % 0.5) / 0.25)
if pos == 1 or (t % 2) < 1e-6: put("music", t, bass(root - 12 + (12 if pos == 1 and (t % 1) > 0.5 else 0), 0.22, g))
def pads(t0, t1):
for b in range(int(t0 // 2), int(t1 // 2)):
put("music", b * 2.0, pad(chord(b)[1], 2.05))
# hook 2-6
groove(2, 6, hats=0.35, claps=False); ostinato(2, 6, g=0.2); bassline(4, 6, 0.4)
put("fx", 5.0, riser(1.0, 0.3))
# title 6-10
put("fx", 6.0, impact(1.0))
groove(6, 10); ostinato(6, 10); bassline(6, 10); pads(6, 10)
for t in (6.5, 6.75, 7.0, 7.25): put("fx", t, stamp(0.45, 72))
for t in on(7.5, 8.0, 0.0625): put("fx", t, tick(), 0.5)
put("fx", 8.0, stamp(0.9, 48))
# principle 10-18
put("fx", 10.0, impact(0.55))
groove(10, 18, hats=0.45); ostinato(10, 18); bassline(10, 18); pads(10, 18)
for t in on(13.0, 15.5, 0.25): put("fx", t, tick(), 0.35) # ruler ticks while edges draw
for t in (16.0, 16.5, 17.0): put("fx", t, marimba(84, 0.4, 0.35))
put("fx", 17.0, riser(1.0, 0.35))
# fan-out 18-24 (peak)
put("fx", 18.0, impact(0.9))
groove(18, 24, hats=0.45, hat16=True); ostinato(18, 24, g=0.24); ostinato(18.125, 24, octave=24, g=0.12, step=0.25, cycle=5)
bassline(18, 24, 0.55); pads(18, 24)
put("fx", 21.0, buzz())
for t in (22.0, 22.5, 23.0): put("fx", t, stamp(0.8, 67))
put("fx", 23.0, riser(1.0, 0.35))
# gn-final 24-27.5: suspense (no kick), then the verdict hit
put("fx", 24.0, impact(0.8))
ostinato(24, 27.5, g=0.2); pads(24, 28)
for t in on(24, 27.5, 0.5): put("drums", t, hat(), 0.25)
for t in (24.0, 24.5, 25.0, 25.5): put("fx", t, marimba(88, 0.25, 0.25))
put("fx", 26.5, riser(1.0, 0.4))
put("fx", 27.5, impact(1.1)); put("fx", 27.5, stamp(0.9, 52))
# bones & flesh 27.5-30: full groove
groove(27.5, 30, hats=0.5); ostinato(27.5, 30); bassline(27.5, 30); pads(28, 30)
# logo 30-34
put("fx", 30.0, impact(0.7)); groove(30, 32, hats=0.4); ostinato(30, 32, g=0.22); bassline(30, 32)
for t, m in ((31.0, 76), (31.5, 79), (32.0, 81)): put("fx", t, marimba(m, 0.6, 0.45))
put("fx", 32.0, impact(1.0)); put("drums", 32.0, kick(1.1))
put("music", 32.0, pad([45, 57, 60, 64, 69], 2.0, 0.22))
for m in (57, 60, 64, 69): put("music", 32.0, marimba(m, 1.2, 0.25))
# transition whooshes (end on the cut)
for t in (6.0, 10.0, 18.0, 24.0, 30.0): put("fx", t - 0.3, whoosh(0.45))
# ---------- reverb, voice, ducking, master ----------
def reverb(x, sec=1.4, wet=0.18):
n = int(sec * SR); ir = rng.standard_normal(n) * np.exp(-np.arange(n) / SR / (sec / 5))
ir = lp(ir, 5000); ir /= np.sqrt((ir ** 2).sum())
return x + fftconvolve(x, ir)[: len(x)] * wet
music = reverb(bus["music"], wet=0.25) + bus["drums"] * 0.9 + reverb(bus["fx"], wet=0.2)
vo = np.zeros(N)
tl = json.load(open("timeline.json", encoding="utf-8"))["scenes"]
for sid, (st, dur, off) in tl.items():
raw = subprocess.run([FFMPEG, "-v", "quiet", "-i", f"audio/vo_{sid}.mp3", "-f", "f32le", "-ac", "1", "-ar", str(SR), "-"], capture_output=True).stdout
v = np.frombuffer(raw, dtype=np.float32).astype(float)
i = int((st + off) * SR); vo[i : i + len(v)] += v[: N - i]
vo = hp(vo, 70) / (np.abs(vo).max() + 1e-9) * 0.85
# sidechain: duck music ~7 dB under narration, 30 ms attack, 250 ms release
lvl = np.abs(vo); k = int(0.02 * SR)
lvl = np.convolve(lvl, np.ones(k) / k, mode="same")
gate = (lvl > 0.02).astype(float)
duck = np.zeros(N); a, r = np.exp(-1 / (0.03 * SR)), np.exp(-1 / (0.25 * SR)); g = 0.0
for i in range(0, N, 64): # block-rate envelope follower
target = gate[i]; c = a ** 64 if target > g else r ** 64
g = target + (g - target) * c; duck[i : i + 64] = g
music *= 1 - 0.55 * duck
music = music / (np.abs(music).max() + 1e-9) * 0.62
mix = np.tanh((music + vo) * 1.1)
fade = np.ones(N); fl = int(0.6 * SR); fade[-fl:] = np.linspace(1, 0, fl)
mix *= fade
mix = mix / np.abs(mix).max() * 0.94
stereo = np.stack([mix, mix], 1)
sf.write("audio/mix.wav", stereo, SR, subtype="PCM_16")
sf.write("audio/score.wav", np.stack([music, music], 1) * 0.9, SR, subtype="PCM_16")
print("ok", stereo.shape, "peak", np.abs(mix).max())
@@ -0,0 +1,11 @@
{
"name": "kigi-graph-bauhaus",
"private": true,
"type": "module",
"scripts": {
"dev": "npx --yes hyperframes@0.8.124 preview",
"check": "npx --yes hyperframes@0.8.124 check",
"render": "npx --yes hyperframes@0.8.124 render",
"publish": "npx --yes hyperframes@0.8.124 publish"
}
}
@@ -0,0 +1,7 @@
[
{"id":"s1","segs":[{"zh":"一个智能体,扛不动一个庞大的目标。","en":"One agent can't carry one huge goal."}]},
{"id":"s2","segs":[{"zh":"Kigi 的答案:一张图。","en":"Kigi's answer: a graph."}]},
{"id":"s3","segs":[{"zh":"智能体的循环,活在节点内部;","en":"The agentic loop lives inside each node;"},{"zh":"节点之间的边,是确定性的 Rust。","en":"the edges between them are deterministic Rust."}]},
{"id":"s4","segs":[{"zh":"就绪的节点三路并行,各占一棵工作树,","en":"Ready nodes run three at a time, each in its own worktree,"},{"zh":"由怀疑论者逐一审查。","en":"each checked by a skeptic."}]},
{"id":"s5","segs":[{"zh":"最后,gn-final 重新验收整个目标。","tts":"最后,G N final 重新验收整个目标。","en":"Finally, gn-final re-verifies the whole objective."},{"zh":"确定性的骨架,智能的血肉。","en":"Deterministic bones. Intelligent flesh."},{"zh":"Kigi /graph。","tts":"Kigi,斜杠 graph。","en":"Kigi /graph."}]}
]
@@ -0,0 +1,3 @@
@font-face { font-family: "Jost"; src: url("../fonts/Jost.ttf") format("truetype"); font-weight: 100 900; }
@font-face { font-family: "Noto Sans SC"; src: url("../fonts/NotoSansSC.ttf") format("truetype"); font-weight: 100 900; }
@font-face { font-family: "JetBrains Mono"; src: url("../fonts/JetBrainsMono.ttf") format("truetype"); font-weight: 100 800; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@@ -0,0 +1,85 @@
{
"scenes": {
"s1": [
0,
6,
1.5
],
"s2": [
6,
4,
0.5
],
"s3": [
10,
8,
0.5
],
"s4": [
18,
6,
0.25
],
"s5": [
24,
10,
0.5
]
},
"caps": [
{
"zh": "一个智能体,扛不动一个庞大的目标。",
"en": "One agent can't carry one huge goal.",
"t0": 1.54,
"t1": 4.14
},
{
"zh": "Kigi 的答案:一张图。",
"en": "Kigi's answer: a graph.",
"t0": 6.54,
"t1": 8.0
},
{
"zh": "智能体的循环,活在节点内部;",
"en": "The agentic loop lives inside each node;",
"t0": 10.54,
"t1": 12.82
},
{
"zh": "节点之间的边,是确定性的 Rust。",
"en": "the edges between them are deterministic Rust.",
"t0": 12.98,
"t1": 15.7
},
{
"zh": "就绪的节点三路并行,各占一棵工作树,",
"en": "Ready nodes run three at a time, each in its own worktree,",
"t0": 18.29,
"t1": 21.3
},
{
"zh": "由怀疑论者逐一审查。",
"en": "each checked by a skeptic.",
"t0": 21.34,
"t1": 23.41
},
{
"zh": "最后,gn-final 重新验收整个目标。",
"en": "Finally, gn-final re-verifies the whole objective.",
"t0": 24.54,
"t1": 27.51
},
{
"zh": "确定性的骨架,智能的血肉。",
"en": "Deterministic bones. Intelligent flesh.",
"t0": 27.59,
"t1": 30.03
},
{
"zh": "Kigi /graph。",
"en": "Kigi /graph.",
"t0": 30.07,
"t1": 31.81
}
]
}
@@ -0,0 +1,15 @@
import json, math
vo = json.load(open("vo_timing.json", encoding="utf-8"))
OFF = {"s01": 2.0, "s02": 1.5}
MINTAIL = {"s13": 7.0, "s02": 1.8}
t, out = 0.0, []
for sid, v in vo.items():
off = OFF.get(sid, 0.5)
need = off + v["len"] + MINTAIL.get(sid, 0.6)
dur = math.ceil(need / 2.0) * 2.0
segs = [{**s, "t0": round(s["t0"] + off, 3), "t1": round(s["t1"] + off, 3)} for s in v["segs"]]
out.append({"id": sid, "start": t, "dur": dur, "vo_off": off, "vo_len": v["len"], "segs": segs})
t += dur
json.dump({"bpm": 120, "total": t, "scenes": out}, open("timeline.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
for s in out: print(s["id"], s["start"], s["dur"], "bars", s["dur"]/2)
print("total", t)
+21
View File
@@ -0,0 +1,21 @@
import json, sys, urllib.request, pathlib
KEY = pathlib.Path(r"C:\Users\zhang\Documents\Github\Kigi-CLI\env.md").read_text(encoding="utf-8").splitlines()[1].split(":",1)[1].strip()
def tts(text, out, voice, speed=1.0, extra=None):
body = {"model":"speech-2.8-hd","text":text,"stream":False,"language_boost":"Chinese",
"output_format":"hex","aigc_watermark":False,
"voice_setting":{"voice_id":voice,"speed":speed,"vol":1,"pitch":0},
"audio_setting":{"sample_rate":44100,"bitrate":256000,"format":"mp3","channel":1},
"subtitle_enable":True,"subtitle_type":"word"}
if extra: body.update(extra)
req = urllib.request.Request("https://api.minimax.io/v1/t2a_v2", data=json.dumps(body).encode(),
headers={"Authorization":"Bearer "+KEY,"Content-Type":"application/json"})
d = json.load(urllib.request.urlopen(req, timeout=180))
if d["base_resp"]["status_code"] != 0: raise SystemExit(d["base_resp"])
pathlib.Path(out).write_bytes(bytes.fromhex(d["data"]["audio"]))
subs = None
if d["data"].get("subtitle_file"):
subs = json.load(urllib.request.urlopen(d["data"]["subtitle_file"], timeout=60))
return d["extra_info"], subs
if __name__ == "__main__":
info, subs = tts(sys.argv[2], sys.argv[1], sys.argv[3])
print(info); print(json.dumps(subs, ensure_ascii=False)[:1500])
@@ -0,0 +1,81 @@
{
"s1": {
"len": 3.004,
"segs": [
{
"zh": "一个智能体,扛不动一个庞大的目标。",
"en": "One agent can't carry one huge goal.",
"t0": 0.040634920634920635,
"t1": 2.6412698412698417
}
]
},
"s2": {
"len": 1.985,
"segs": [
{
"zh": "Kigi 的答案:一张图。",
"en": "Kigi's answer: a graph.",
"t0": 0.040634920634920635,
"t1": 1.5034920634920637
}
]
},
"s3": {
"len": 5.72,
"segs": [
{
"zh": "智能体的循环,活在节点内部;",
"en": "The agentic loop lives inside each node;",
"t0": 0.040634920634920635,
"t1": 2.316190476190476
},
{
"zh": "节点之间的边,是确定性的 Rust。",
"en": "the edges between them are deterministic Rust.",
"t0": 2.4787301587301585,
"t1": 5.201269841269841
}
]
},
"s4": {
"len": 5.564,
"segs": [
{
"zh": "就绪的节点三路并行,各占一棵工作树,",
"en": "Ready nodes run three at a time, each in its own worktree,",
"t0": 0.040634920634920635,
"t1": 3.0476190476190474
},
{
"zh": "由怀疑论者逐一审查。",
"en": "each checked by a skeptic.",
"t0": 3.088253968253968,
"t1": 5.16063492063492
}
]
},
"s5": {
"len": 7.915,
"segs": [
{
"zh": "最后,gn-final 重新验收整个目标。",
"en": "Finally, gn-final re-verifies the whole objective.",
"t0": 0.040634920634920635,
"t1": 3.0069841269841273
},
{
"zh": "确定性的骨架,智能的血肉。",
"en": "Deterministic bones. Intelligent flesh.",
"t0": 3.088253968253968,
"t1": 5.526349206349206
},
{
"zh": "Kigi /graph。",
"en": "Kigi /graph.",
"t0": 5.566984126984127,
"t1": 7.314285714285714
}
]
}
}