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:
32 files changed
+1201
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 <objective><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>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "kigi-graph-bauhaus",
|
||||
"name": "kigi-graph-bauhaus",
|
||||
"createdAt": "2026-10-04T17:49:43.118Z"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user