diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8ca4bae --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: npm ci + - run: npx tsc --noEmit + - run: pip install pytest + - run: python tts/gen.py --mock + - run: pytest diff --git a/CLAUDE.md b/CLAUDE.md index 6e30652..b9be65b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # explainer-kit -Remotion 讲解视频模板:旁白里写 `[[cue]]` 标记,edge-tts 的逐词时间戳驱动字幕和动画。当前任务见 `TASK.md`。 +Remotion 讲解视频模板:旁白里写 `[[cue]]` 标记,edge-tts 的逐词时间戳驱动字幕和动画。用法见 `README.md`。 - 面向用户的文档用简体中文;代码、命令、标识符用英文。 - 本机是 Windows(渲染、真 TTS 验证在那里做);云端沙箱只做重构、类型检查、`--mock` 时间轴和单测。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..243285d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 runanshao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5828c8 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# explainer-kit + +用 Remotion 做讲解视频的模板。旁白写在 `tts/script.json` 里,用 `[[cue]]` 标出动画触发点;edge-tts 合成语音时记下每个词的出口时间,生成 `src/timings.json`。场景代码只问「这个 cue 在第几帧」「这个词在第几帧」,不写死任何秒数,所以改旁白、重跑语音之后,动画和字幕自动跟上。 + +仓库自带一支约 60 秒的演示片(2 场):第一场用 `film` 风格包,第二场用 `slides` 风格包和一条 KaTeX 公式。演示片的时间轴和音频是用 `--mock` 生成的静音占位,克隆下来不用联网合成就能预览。 + +## 快速开始 + +```bash +npm i +npm run studio # 打开 Remotion Studio,直接看演示片(静音占位音轨) +``` + +想要真实配音和正式字体: + +```bash +npm run fonts # 下载 Noto Sans SC / Noto Serif SC 可变字体到 public/fonts/(OFL,不进仓库) +pip install -r requirements.txt # edge-tts、numpy、Pillow、pytest +python tts/gen.py # 真 edge-tts,需要联网和 ffmpeg;写 public/audio/*.mp3 + src/timings.json +``` + +不下载字体也能跑:字体栈会退回系统字体(微软雅黑、苹方等),日志里那两条 `fonts/*.ttf` 404 属于预期。 + +## 目录 + +| 路径 | 作用 | +|---|---| +| `kit.config.json` | 唯一配置:合成 ID、尺寸、帧率、旁白音色与语速、引语音色、停顿时长、字幕行长、`--mock` 语速、每场前后留白、字体 | +| `tts/script.json` | 旁白稿,每场一个 `{id, chapter, text}` | +| `tts/gen.py` | 旁白 → 音频 + `src/timings.json`;`--mock` 离线估算 | +| `src/core/` | 与风格无关的核心:时间轴 API、`Video` 外壳、布局助手、`Tex`、配色与字体栈 | +| `src/film/` | 电影风格包(可删) | +| `src/slides/` | 幻灯片风格包(可删) | +| `src/scenes/` | 你的场景;`index.ts` 是登记表 | +| `tools/` | 审图、拼图、配乐、混音、下载字体 | +| `public/fx/` | 胶片颗粒和纸张纹理 | + +## 配置 `kit.config.json` + +| 字段 | 含义 | +|---|---| +| `id` `width` `height` `fps` | 合成参数。`npm run render` 默认输出 `out/.mp4` | +| `voice` `rate` | 旁白音色和语速,例如 `zh-CN-YunjianNeural`、`+10%` | +| `voices` | 引语音色表,键名就是 `<>` 里的 key | +| `pause` | `||` 的停顿秒数 | +| `maxLine` | 字幕每行最多字数 | +| `mockCharsPerSec` | `--mock` 估算语速,默认 4.2 字/秒 | +| `pace.lead` / `pace.tail` | 每场旁白前(放章节卡)和旁白后的留白帧数;`leadOverride` / `tailOverride` 按场覆盖 | +| `fonts` | `sans` / `serif` / `mono` 的字体名、文件、下载地址和后备字体 | + +`src/Root.tsx`、`tools/*.mjs`、`tools/*.py`、`tts/gen.py` 都从这里读,不要在别处写死这些值。 + +## 旁白标记 + +```json +{"id": "s01", "chapter": "第一场 · 旁白就是时间轴", + "text": "[[open]]做讲解视频……[[quote]]有位剪辑师说得好:<>||[[ask]]画面只需要问一句……"} +``` + +| 标记 | 含义 | +|---|---| +| `[[name]]` | cue:动画或镜头切换点,记录的是它后面第一个词的出口时间。名字只能是字母、数字、下划线;`end` 是保留名 | +| `\|\|` | 戏剧停顿,时长取 `pause`,可以连写叠加 | +| `<>` | 这一段换成 `voices[key]` 的音色读;字幕里这一行会用衬线体加「」 | + +`chapter` 用 ` · ` 分成小标题和大标题,显示在章节卡上。lead 不超过 30 帧的场景不出章节卡(适合冷开场)。 + +## 时间轴 API + +场景里调用 `useScene()`,返回的帧数都以本场开头为 0: + +| 函数 | 返回 | +|---|---| +| `c('cue')` | 这个 cue 所在的帧 | +| `w('词', n?)` | 旁白第 n 次(从 0 数)说到这个词的帧 | +| `rel('词', 'cue')` | 从这个 cue 到它之后第一次说到这个词,隔了多少帧 | +| `f` `total` `end` `lead` | 当前帧、本场总帧数、旁白结束帧、开场留白帧数 | + +配套的动画助手:`ease(f, at, dur)` 缓动 0→1,`pop(f, at)` 带回弹,`fu(p)` 上浮淡入样式,`lerp(p, a, b)`,`window_(f, a, b)` 在 a..b 之间可见并带淡入淡出,`num(v)` 千分位。 + +```tsx +const {f, c, w, rel} = useScene(); +const title = ease(f, c('idea')); // cue 一到,标题淡入 +const stampAt = w('重新对一遍'); // 说到这个词时盖章 +// 在 film 的 Shots 里,镜头内时间 t 从 cue 起算,所以用 rel: + +``` + +`w()` 找不到词、`c()` 找不到 cue 都会直接抛错,改稿后 Studio 里会立刻报出来。 + +## 工作流 + +1. **写稿**:编辑 `tts/script.json`。 +2. **合成**:`python tts/gen.py`(只重做某几场:`python tts/gen.py s02`;离线占位:`python tts/gen.py --mock`)。 +3. **预览**:`npm run studio`。 +4. **审图**:`node tools/stills.mjs s01 s02` 在每个 cue 后约 2.5 秒出一张图(`OFF=30 node tools/stills.mjs` 改偏移),`python tools/sheet.py s01` 拼成 2×2 的检查表;只看某几帧用 `node tools/pick.mjs s01:quote:40 s02:end:-10`(`场景:cue:偏移帧`,`end` 表示旁白结束)。输出在 `out/stills`、`out/sheets`、`out/pick`。 +5. **渲染**:`npm run render`(带 `--gl=angle`)→ `out/.mp4`。 +6. **配乐**:`python tools/music.py` → `out/music.wav`。按 timings 每场一段和声,每个章节开头一声轻击,重配音后重跑即可对齐。 +7. **混音**:`python tools/mix.py` → `out/-mixed.mp4`。配乐低通后按旁白做 sidechain 压缩,再整体 `loudnorm=I=-16:TP=-1.5:LRA=11`。 + +## 风格包 + +外壳 `src/core/Video.tsx` 按场景登记表里的 `look` 给每一场「穿衣服」:底色、背景层、画面上的叠加层(颗粒、暗角)、最上层的框(黑边)、章节卡、字幕样式。核心代码从不 import 风格包,不用的包整个删掉即可。 + +- **film**(`src/film/`):`Shots` 按 cue 切镜头(`fade`/`cut`/`black`/`flash`/`slow` 转场),`Cam` 缓推运镜,`Graded` 调色(past/warm/cold/dusk/neutral),`Grain`、`Vignette`、`Letterbox`(2.2:1),`Place` 年份地点字幕,`Tag`、`Line`、`BigQuote`,`Paper`、`Doc`、`Stamp`、`Mark`、`InkBars`、`Count`,背景 `Sky`、`Glow`、`Void`、`Desk`,章节卡 `Chapter`。字幕放在下黑边里。 +- **slides**(`src/slides/`):`Panel`、`Chip`、`Kicker`、`H`,点阵背景和简单章节卡;字幕是半透明底条,按逐词时间点亮。 + +两个包共用 `src/core` 的 `At`(`center` 时居中位移会和你传入的 `transform` 叠加)、`Full`、`Svg`、`DrawLine`、`Tex`,配色统一在 `src/core/theme.ts`。 + +## 加一个场景 + +1. 在 `tts/script.json` 里加一项,比如 `{"id": "s03", "chapter": "第三场 · 标题", "text": "[[a]]……[[b]]……"}`,跑 `python tts/gen.py s03`(或 `--mock`)。 +2. 新建 `src/scenes/s03.tsx`: + + ```tsx + import React from 'react'; + import {At, ease, fu, useScene} from '../core'; + import {H, Panel} from '../slides'; + + export const S03: React.FC = () => { + const {f, c, w} = useScene(); + return ( + + {f >= w('关键词') ? '说到了' : '还没说到'} + + ); + }; + ``` +3. 在 `src/scenes/index.ts` 登记:`s03: {component: S03, look: slides}`。场景顺序以 `tts/script.json` 为准。 + +## 踩过的坑 + +- **渲染一定带 `--gl=angle`**。走 GPU 合成;纯软件 GL 大约慢 5 倍(上一个 21 分钟的片子:25 分钟对两个多小时)。`npm run render` 已经带上。 +- **超过 10 分钟左右的渲染要脱离终端跑,看日志**,别挂在会超时的后台 shell 上,超时会把渲染一起杀掉。Git Bash:`nohup npm run render > out/render.log 2>&1 &`;PowerShell:`Start-Process npm.cmd -ArgumentList 'run','render' -RedirectStandardOutput out/render.log -RedirectStandardError out/render.err -WindowStyle Hidden`,然后看 `out/render.log`。 +- **edge-tts 原始输出只有 -24 LUFS 左右**,直接发布会明显偏小声。发布前一定跑 `tools/mix.py`(或至少做一遍 loudnorm)。 +- **一场的第一个 cue 太晚,开场会空 3–6 秒**。镜头从 cue 才开始画的话,章节卡淡出后到第一个 cue 之间什么都没有。第一个 cue 放在文稿最前面,或者让第一个镜头从第 0 帧开始(演示片 s01 就是这样)。 +- **`zh-CN-YunyangNeural` 在 +6% 时只有约 3.3 字/秒**,听着拖。用它的话语速调到 +15% 左右。 +- **`--mock` 的时间只是估算**(每字 1/4.2 秒),用来搭画面足够,但换成真配音后每个 cue 的位置都会变;按 cue 和词取帧的写法不受影响,写死的帧数会错位。 +- **只重做某几场时,其他场沿用旧 timings**:`python tts/gen.py s02` 不会动 s01 的音频和时间。 +- **cue 名 `end` 是保留的**(工具里表示旁白结束),`gen.py` 遇到会直接报错。 +- **场景里别用 `Math.random()`**,要随机就用 film 包的 `rng(seed)` 或 Remotion 的 `random(seed)`。渲染是多进程逐帧并行的,同一帧必须画出同样的东西。 +- **edge-tts 偶尔断线**,`gen.py` 每段会重试 10 次,间隔递增。 + +## English + +explainer-kit is a Remotion template for narrated explainer videos. Narration lives in `tts/script.json` with inline markers: `[[cue]]` marks an animation/shot trigger, `||` inserts a dramatic pause, and `<>` reads a quote with an alternate voice. `tts/gen.py` synthesizes each scene with edge-tts, records word boundaries and writes `public/audio/.mp3` plus `src/timings.json`. Scenes read frames through `useScene()` — `c('cue')`, `w('word')`, `rel('word', 'cue')` — so durations and animations follow the audio after any rewrite. + +Quick start: `npm i && npm run studio` shows the bundled ~60 s demo, whose timings and silent audio were produced by `python tts/gen.py --mock` (no network; word times estimated from character count). `npm run fonts` downloads the Noto Sans SC / Noto Serif SC variable fonts (OFL) into `public/fonts/`; without them the stacks fall back to system fonts. All size, fps, voice, pace and font settings live in `kit.config.json`. Two optional style packs are included: `film` (letterbox, grain, grade, cue-driven shots, paper props) and `slides` (panels, chips, karaoke subtitles). Review with `tools/stills.mjs`, `tools/pick.mjs` and `tools/sheet.py`; render with `npm run render` (uses `--gl=angle`); add a synthesized score with `tools/music.py` and mix/normalize to -16 LUFS with `tools/mix.py`. + +## License + +MIT,见 `LICENSE`。Noto 字体不随仓库分发,由 `npm run fonts` 下载,遵循 SIL Open Font License。 diff --git a/TASK.md b/TASK.md deleted file mode 100644 index 5949877..0000000 --- a/TASK.md +++ /dev/null @@ -1,40 +0,0 @@ -# TASK: turn `reference/` into a clean, reusable explainer-video template - -## Background - -`reference/` holds the generic parts of a Remotion explainer-video pipeline that has been copy-pasted across 7 projects: - -- `reference/zzm/` — the most recent project (cinematic style: letterbox, grain, color grade, cue-driven shot switching). Only 2 sample scenes and the s01 narration are kept. Read its `README.md` first: it documents the pipeline. -- `reference/ontology/` — an earlier project (slide/whiteboard style) whose only extra worth keeping is the KaTeX `Tex` component in `src/lib.tsx` (scene `D.tsx` shows usage). -- `reference/fx/` — grain and paper textures. - -The pipeline: narration lives in `tts/script.json` with inline markers (`[[cue]]` shot/animation trigger, `||` dramatic pause, `<>` alternate voice for quotes). `tts/gen.py` calls edge-tts, records WordBoundary events and writes `public/audio/.mp3` + `src/timings.json` (per-scene duration, cue seconds, subtitle lines with per-word times). Scenes read timings through `useScene()` → `c('cue')` (frame of a cue), `w('word')` (frame when a word is spoken), `rel('word','cue')`. Durations follow the audio automatically, so rewriting narration never requires re-timing animations by hand. - -## Deliverable - -A template repo at the repo root that someone can clone, run `npm i`, `npm run studio`, and see a working ~60-second demo video, then replace the demo with their own scenes. - -1. **One config file** (`kit.config.json` or similar) for composition id, size, fps, default voice/rate, alternate voices, pause length, fonts. `Root.tsx`, `tools/*.mjs`, `tts/gen.py` must all read it. Nothing hardcodes `ZhangZhongmou`. -2. **Core** (style-agnostic): timeline helpers (`useScene`, `c`, `w`, `rel`, `ease`, `pop`, `fu`, `lerp`, `window_`), pace (`lead`/`tail` per scene), the `Video` shell (scene sequencing, chapter card, audio, subtitles, font loading via `delayRender`), layout helpers (`At`, `Full`, `Svg`, `DrawLine`), `Tex` (KaTeX). -3. **Two style packs**, each optional: `film` (Letterbox, Grain, Vignette, Graded, Shots, Cam, Place, BigQuote, Paper, Stamp, Chapter) and `slides` (Panel, Chip, Kicker, H, simple chapter card). Strip subject-specific art (Morris Chang silhouettes, TSMC chessboard, Taiwan strait, `SIDE`/`CARD` constants). Keep generic primitives from `art.tsx`/`sets.tsx`/`props.tsx` only if they are clearly reusable (sky gradient, paper document, stamp, bar chart); drop the rest. -4. **Fix the known inconsistencies**: `lib.tsx` references theme tokens that do not exist in the zzm theme (`C.orange`, `C.panel`, `C.panelLine`, `C.teal`, `C.bg`). `At center` must compose its centering translate with the caller's `transform` (zzm version already does — keep that). `npx tsc --noEmit` must pass with zero errors. -5. **`tts/gen.py`**: keep edge-tts behavior and marker syntax. Add `--mock`: no network; estimate word times from character count (configurable chars/sec, default 4.2 for Chinese), write silent audio with the stdlib `wave` module (`.wav`; the Video shell must accept either extension), and produce a valid `timings.json`. Factor `parse()` so it is importable and add `tests/test_parse.py` (pytest) covering cues, `||`, `<>`, and line splitting. -6. **Demo**: 2 scenes, ~60 s total, original Chinese narration (topic: how this template works — markers, cues, timing). Scene 1 uses the `film` pack, scene 2 the `slides` pack with one `Tex` formula. Demonstrate `c()`, `w()`, `rel()`, `||` and one `<>` quote. Commit the mock `timings.json` and mock audio so `npm run studio` works with no TTS run. -7. **Tools** (all read the config): `tools/stills.mjs` (bundle once, a still per cue + offset, `OFF` env), `tools/pick.mjs` (`scene:cue:offset`), `tools/sheet.py` (2×2 contact sheet), `tools/music.py` (synthesized pad; remove the zzm-specific act/scene map and drive it from timings: one section per scene, a soft hit at each chapter start), `tools/mix.py` (wraps the ffmpeg sidechain + `loudnorm=I=-16:TP=-1.5:LRA=11` recipe from the zzm README). `npm run fonts` downloads Noto Sans SC / Noto Serif SC variable fonts (OFL) into `public/fonts/` (gitignored); font stacks must fall back to system fonts when they are absent. -8. **Docs**: `README.md` in Chinese (quick start; marker syntax; timing API with a small example; workflow: script → tts → studio → stills/sheet review → render → music → mix; style packs; how to add a scene). Include a "踩过的坑" section built from the zzm README and these notes: render with `--gl=angle` (software GL is ~5× slower); renders over ~10 min should be detached (nohup) and watched via the log, not a timed-out background shell; edge-tts raw output is about -24 LUFS so always run mix/loudnorm before publishing; if a scene's first cue is late the scene opens on 3–6 s of empty frame; `zh-CN-YunyangNeural` speaks only ~3.3 chars/s at +6%, use about +15%. Add a short English section at the end. `LICENSE` MIT. -9. **CI**: a GitHub Action running `npm ci`, `npx tsc --noEmit`, `python tts/gen.py --mock`, `pytest`. -10. Delete `reference/` and this `TASK.md` in the final commit. - -## Verification you must run in the sandbox (paste the real output in the PR body) - -- `npx tsc --noEmit` -- `python tts/gen.py --mock` then `pytest` -- Try `npx remotion still` on 2–3 frames of the demo. If Chromium download or rendering is blocked by the sandbox network policy, say so explicitly in the PR body — do not claim it rendered. Real render verification will be done locally on Windows afterwards. - -## Out of scope - -No real edge-tts calls are required. No npm publishing, no `create-*` scaffolder, no HyperFrames port. Do not add features beyond this list. - -## Output - -Work on a branch, open a PR against `main`. PR body: what was kept/dropped from `reference/` and why, verification output, anything left undone. diff --git a/kit.config.json b/kit.config.json new file mode 100644 index 0000000..45eb729 --- /dev/null +++ b/kit.config.json @@ -0,0 +1,39 @@ +{ + "id": "Explainer", + "width": 1920, + "height": 1080, + "fps": 30, + "voice": "zh-CN-YunjianNeural", + "rate": "+10%", + "voices": { + "q": {"voice": "zh-CN-YunxiNeural", "rate": "+0%"} + }, + "pause": 0.75, + "maxLine": 20, + "mockCharsPerSec": 4.2, + "pace": { + "lead": 48, + "tail": 34, + "leadOverride": {}, + "tailOverride": {} + }, + "fonts": { + "sans": { + "family": "NotoSansSC", + "file": "NotoSansSC-VF.ttf", + "weight": "100 900", + "url": "https://github.com/google/fonts/raw/main/ofl/notosanssc/NotoSansSC%5Bwght%5D.ttf", + "fallback": ["Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", "sans-serif"] + }, + "serif": { + "family": "NotoSerifSC", + "file": "NotoSerifSC-VF.ttf", + "weight": "200 900", + "url": "https://github.com/google/fonts/raw/main/ofl/notoserifsc/NotoSerifSC%5Bwght%5D.ttf", + "fallback": ["Noto Serif SC", "Songti SC", "SimSun", "serif"] + }, + "mono": { + "fallback": ["Cascadia Code", "Consolas", "Menlo", "monospace"] + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6fdb820 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4241 @@ +{ + "name": "explainer-kit", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "explainer-kit", + "dependencies": { + "@remotion/bundler": "4.0.532", + "@remotion/cli": "4.0.532", + "@remotion/renderer": "4.0.532", + "katex": "^0.18.9", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "remotion": "4.0.532" + }, + "devDependencies": { + "@types/katex": "^0.16.8", + "@types/node": "^24.0.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "typescript": "^7.0.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mediabunny/aac-encoder": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.56.1.tgz", + "integrity": "sha512-a+OY/BctP7zC4G/prHikooOVaoWXMD4+bUyEwHg2dtQiQdIwC6TDJdVYJu5dfrdSNOiAFkv1HKcYEeROdwMezg==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@mediabunny/flac-encoder": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.56.1.tgz", + "integrity": "sha512-zbmorMVlF1izwtlz5mWnu7A1rJLM+V26I0pEFr9Td1EDTwE5uAGRSW+/UZlnjwj1t00Woe3Zonfh9kb7lRRBqg==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@mediabunny/mp3-encoder": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.56.1.tgz", + "integrity": "sha512-p66ovEq6mZt3ejje45NOju84hIJI4v8rHfsmc1D2J34ziV0USE8AdJBzmiwqxl6nZkIAQLNB19HzbVRcQfc51g==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@module-federation/error-codes": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", + "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", + "license": "MIT" + }, + "node_modules/@module-federation/runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", + "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/runtime-core": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-core": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", + "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-tools": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", + "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/webpack-bundler-runtime": "0.22.0" + } + }, + "node_modules/@module-federation/sdk": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", + "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", + "license": "MIT" + }, + "node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", + "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@remotion/bundler": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.532.tgz", + "integrity": "sha512-3F0yU4jw8I0VYr4CXznxqafCb0IcM5SlJjmt6bZfTPDIeP3in/CKU6ZL0L2xE4W0ppMUU6QFMroi0VnMi9NlSw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/media-parser": "4.0.532", + "@remotion/studio": "4.0.532", + "@remotion/studio-shared": "4.0.532", + "@remotion/timeline-utils": "4.0.532", + "@rspack/core": "1.7.11", + "@rspack/plugin-react-refresh": "1.6.1", + "css-loader": "7.1.4", + "esbuild": "0.28.1", + "react-refresh": "0.18.0", + "remotion": "4.0.532", + "style-loader": "4.0.0", + "webpack": "5.105.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/canvas": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/canvas/-/canvas-4.0.532.tgz", + "integrity": "sha512-7HwcyUBCLmE1oQD+mtV7HjC/7d+jXEk3ZiX0XvdjuBBhTeb381jiC0LZoObEk307x6LV5d63B4ZNMAyA/Kapjw==", + "license": "Remotion License", + "dependencies": { + "@remotion/player": "4.0.532", + "@remotion/studio-shared": "4.0.532", + "remotion": "4.0.532" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/captions": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/captions/-/captions-4.0.532.tgz", + "integrity": "sha512-1DzWEnv5DCGESIq1ux1C1M/z/sJMv08A7Z+1bHAnGW3gVrg37QtoRyhyDvWKJZHnNIYiEmBeICSeQpgu5p0Bdw==", + "license": "MIT" + }, + "node_modules/@remotion/cli": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.532.tgz", + "integrity": "sha512-1Iy4IFK1KN4LOBjGcJh56ZlW/NQdbTEMhFG0H1g1rHvx8Vva9aQdF/rtFi7cvFon99WvPJPc8k2F1jFNba9BIQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@babel/parser": "7.24.1", + "@remotion/bundler": "4.0.532", + "@remotion/media-utils": "4.0.532", + "@remotion/player": "4.0.532", + "@remotion/renderer": "4.0.532", + "@remotion/studio": "4.0.532", + "@remotion/studio-server": "4.0.532", + "@remotion/studio-shared": "4.0.532", + "dotenv": "17.3.1", + "minimist": "1.2.6", + "prompts": "2.4.2", + "remotion": "4.0.532", + "semver": "7.5.3" + }, + "bin": { + "remotion": "remotion-cli.js", + "remotionb": "remotionb-cli.js", + "remotiond": "remotiond-cli.js" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/codemods": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/codemods/-/codemods-4.0.532.tgz", + "integrity": "sha512-WEOuUHYc6vNx4pheNsB0jqdfNuqgDk1Xz7cgCdHihPRpwZpUgJgl1mxH0f+KVIL3+ef9+gagqYScEkuS2q3pdw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@babel/types": "7.24.0", + "@remotion/studio-protocol": "4.0.532", + "@remotion/studio-shared": "4.0.532", + "ast-types": "0.16.1", + "recast": "0.23.21", + "remotion": "4.0.532" + } + }, + "node_modules/@remotion/compositor-darwin-arm64": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.532.tgz", + "integrity": "sha512-K/RID6hvhRqiE1q/DTurkmG/6SrD9b918fgqvmntFpFiU3bc8Ti9ZaYQCOwTwOA6mVBh6oneuHuoA2QpFlP0Qg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-darwin-x64": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.532.tgz", + "integrity": "sha512-mggi24c5As/H6wYoCr22oTRPVeblvNMdenh+bi7316lvBpBDgSXeIUkXvUEG4379lJQtshks0UfwkBkqnePyvg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-gnu": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.532.tgz", + "integrity": "sha512-7ZPI1t6k0NEbJHP6fSjgdJJymyOQCtExeC6nqs6P5jYQ6UT5xKZyIEiytAAXJ6KQBOWlyuhvU1iHgYpAHYKXlA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-musl": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.532.tgz", + "integrity": "sha512-DatMaiMqiO+2rAXSgE+61V3WmmL6sRQPJd15PTI7k6F905ewM0TlXhyoEp1P/vCS1M0xBEgI/XNeEcr0yVEJXQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.532.tgz", + "integrity": "sha512-XmXrGroGga9pYJ4CfexCun2rh64WvsIVuTPYEWSpfFnHGsD9Pa7ljOio9A1B+6qAoz6iZeXZFKAOTH1COD5KJg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-musl": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.532.tgz", + "integrity": "sha512-y05BgTyOw3KbavIWm3n8AxsnmIt2PgoqH1ZFuZySkEvrGWY2oq+t1i7kBqMvNvCDRzv+wca5wb0FaGi4GXXIjg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-win32-x64-msvc": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.532.tgz", + "integrity": "sha512-OES5+mlcBTxcQc7VXszEqwbpnAU593P5uuXDGPz6VC/FMivFtOGmBObE8LQ3azAkVfls4Arjxh33U44inlCweg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@remotion/elevenlabs": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/elevenlabs/-/elevenlabs-4.0.532.tgz", + "integrity": "sha512-WTnWQ+obrURlIXZqKaTehd69eYwMpWLwlVv/QRnd7XKicMzhIsxw6XXK3muFXn3kyltJV6A97NTiF0g7T92TQw==", + "license": "MIT", + "dependencies": { + "@remotion/captions": "4.0.532", + "zod": "4.5.4" + } + }, + "node_modules/@remotion/licensing": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.532.tgz", + "integrity": "sha512-rVFt+XpEn4X5aOiy1DDCjJeUKgGey1ftYK812W8iEBZkcl6hlb1eq9SyQTIX0JlTtMzdJrev6+jLSnLCOj69OQ==", + "license": "MIT" + }, + "node_modules/@remotion/media": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/media/-/media-4.0.532.tgz", + "integrity": "sha512-tY8GhDaqwhPQz0/e2eioe2EBUSQsYhpkQSYvwBdl+VrhPFfJ3t4YpEIeUJx0rJm1rH1/zhQISkMIS92WHUMjRQ==", + "dependencies": { + "mediabunny": "1.56.1", + "remotion": "4.0.532", + "zod": "4.5.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/media-parser": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.532.tgz", + "integrity": "sha512-xOhwauHbVnXPRqyz5tI5tGtdacp1POZuFjAp8NALw/0g+8Bp99YXjgUjjkXBHNqV6r8Eym7eHA+EBWfmqvIJAQ==", + "license": "Remotion License https://remotion.dev/license" + }, + "node_modules/@remotion/media-utils": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.532.tgz", + "integrity": "sha512-ZjHDqNxGYHj4rOMVDvQQaoc4pn4t1fmlemVcJCCKrElYUih/gn+bV8NlSddYnAB9KSS2GyRXbfqvYgUdQZbG1g==", + "license": "MIT", + "dependencies": { + "mediabunny": "1.56.1", + "remotion": "4.0.532" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/openai-whisper": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/openai-whisper/-/openai-whisper-4.0.532.tgz", + "integrity": "sha512-CXTAIabqX0pS38SOselqR0wM1OyKsnsDk+iLSVrALdD/47jajWV0t2Ap9nYtB441dzrpWINPvW5hIvmw4xVvZQ==", + "license": "MIT", + "dependencies": { + "@remotion/captions": "4.0.532", + "zod": "4.5.4" + } + }, + "node_modules/@remotion/paths": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/paths/-/paths-4.0.532.tgz", + "integrity": "sha512-JyO0zYT47lbqfsDriFl69RfmUmjXm4h/h1TAvDt5XnChIZMlf9DlpUdiBYkOUvO7NHOXku8XsG/LKyTP/jnZmg==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.532" + } + }, + "node_modules/@remotion/player": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.532.tgz", + "integrity": "sha512-McCswN8cb3Y6BEg1iCRc6yBUoxjODoStvOsAHZ6uReHEP+mqzor3p1qO9zKIm9aL/evEF7bumwo43nKoO1klxA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "remotion": "4.0.532" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.532.tgz", + "integrity": "sha512-Iy5JVGKBglJNBzu5uT9qKG8ZK2HqK9dZWn1gVG+BjyH0QAlvry3E5drra6hRmI0e9nhu2HxT9MrKQ0oLIfX5hA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/licensing": "4.0.532", + "@remotion/streaming": "4.0.532", + "execa": "5.1.1", + "remotion": "4.0.532", + "source-map": "0.8.0", + "ws": "8.21.0" + }, + "optionalDependencies": { + "@remotion/compositor-darwin-arm64": "4.0.532", + "@remotion/compositor-darwin-x64": "4.0.532", + "@remotion/compositor-linux-arm64-gnu": "4.0.532", + "@remotion/compositor-linux-arm64-musl": "4.0.532", + "@remotion/compositor-linux-x64-gnu": "4.0.532", + "@remotion/compositor-linux-x64-musl": "4.0.532", + "@remotion/compositor-win32-x64-msvc": "4.0.532" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/streaming": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.532.tgz", + "integrity": "sha512-1Lw5HQzznHEfSq2HjXEJHbWsXLHLjqiQ2CfUaeMHtpFCR2AqUle13vbnqw+yG96e2GmnHhRPwQONSMtGHSjnfw==", + "license": "MIT" + }, + "node_modules/@remotion/studio": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.532.tgz", + "integrity": "sha512-GMxdgX5xhwcyXBdx0JKQs9qRN6zTVASFfBs5N6qcqgg1ep8GWI/7mGWSFoH9wYjB3YxtEAA2eo0+vb+0ZE+g/g==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@remotion/canvas": "4.0.532", + "@remotion/captions": "4.0.532", + "@remotion/codemods": "4.0.532", + "@remotion/elevenlabs": "4.0.532", + "@remotion/media": "4.0.532", + "@remotion/media-utils": "4.0.532", + "@remotion/openai-whisper": "4.0.532", + "@remotion/paths": "4.0.532", + "@remotion/player": "4.0.532", + "@remotion/renderer": "4.0.532", + "@remotion/studio-protocol": "4.0.532", + "@remotion/studio-shared": "4.0.532", + "@remotion/timeline-utils": "4.0.532", + "@remotion/web-renderer": "4.0.532", + "@remotion/zod-types": "4.0.532", + "@tanstack/react-virtual": "3.14.9", + "mediabunny": "1.56.1", + "memfs": "3.4.3", + "open": "8.4.2", + "prismjs": "1.30.0", + "remotion": "4.0.532", + "semver": "7.5.3", + "zod": "4.5.4" + }, + "peerDependencies": { + "@remotion/video-matting": "4.0.532", + "@remotion/whisper-webgpu": "4.0.532", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@remotion/video-matting": { + "optional": true + }, + "@remotion/whisper-webgpu": { + "optional": true + } + } + }, + "node_modules/@remotion/studio-protocol": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/studio-protocol/-/studio-protocol-4.0.532.tgz", + "integrity": "sha512-rv+5OT6ORlQbTyWwvXngF8f81JJFbIy6HBYlT4uXELwcvODMkARs3yRa2WCxFyT8otd8HH9J0pcYQlfJkW/E9g==", + "license": "Remotion License", + "dependencies": { + "zod": "4.5.4" + } + }, + "node_modules/@remotion/studio-server": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.532.tgz", + "integrity": "sha512-TAipO/dRiomewFXooOZwzUEz/4Jdgm7Ng5jvsaEv8XgC8UfYchXBe5rFY9url8XKuvjxAQDUc9Jok2fvlmlihA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@babel/types": "7.24.0", + "@remotion/bundler": "4.0.532", + "@remotion/codemods": "4.0.532", + "@remotion/renderer": "4.0.532", + "@remotion/studio-protocol": "4.0.532", + "@remotion/studio-shared": "4.0.532", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "kiwi-schema": "0.5.0", + "memfs": "3.4.3", + "open": "8.4.2", + "recast": "0.23.21", + "remotion": "4.0.532", + "semver": "7.5.3", + "zod": "4.5.4" + } + }, + "node_modules/@remotion/studio-shared": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.532.tgz", + "integrity": "sha512-wUgce2FQ56lDNlMXHyEjz2aAI7fFYyUTmNorjghOyC4pGt+lzcuNPifjNQivLi8OkWWdaUjZLeLLcHs8IkVLVA==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.532" + } + }, + "node_modules/@remotion/timeline-utils": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/timeline-utils/-/timeline-utils-4.0.532.tgz", + "integrity": "sha512-DXWm1yoZ7lntTsk1syabokIfKgh8B3ivQbZb4qpJf+gO5mLltWr9O731T6ky3xE38WYl5IEREDgItjxQLM1ESQ==", + "license": "MIT", + "dependencies": { + "mediabunny": "1.56.1", + "remotion": "4.0.532" + } + }, + "node_modules/@remotion/web-renderer": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.532.tgz", + "integrity": "sha512-rA5Tbz+QLTMZbdJFP6XuBg/Xb5zSs6JxTCPuom3n+BFI/Wdzu63JhCjLe3MkIfEj38yQV0gvOcwFzvLcPFwmhA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@mediabunny/aac-encoder": "1.56.1", + "@mediabunny/flac-encoder": "1.56.1", + "@mediabunny/mp3-encoder": "1.56.1", + "@remotion/licensing": "4.0.532", + "mediabunny": "1.56.1", + "remotion": "4.0.532" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@remotion/zod-types": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.532.tgz", + "integrity": "sha512-I/hKQp4UmrYKCjbOI/MBzojPbnwXW2lOPRhq6/AWBDdlNzxrgKz9cZV+R2jhzaXcjHQ+DgklixBhbROynIlDRw==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.532" + } + }, + "node_modules/@rspack/binding": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.11.tgz", + "integrity": "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==", + "license": "MIT", + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.7.11", + "@rspack/binding-darwin-x64": "1.7.11", + "@rspack/binding-linux-arm64-gnu": "1.7.11", + "@rspack/binding-linux-arm64-musl": "1.7.11", + "@rspack/binding-linux-x64-gnu": "1.7.11", + "@rspack/binding-linux-x64-musl": "1.7.11", + "@rspack/binding-wasm32-wasi": "1.7.11", + "@rspack/binding-win32-arm64-msvc": "1.7.11", + "@rspack/binding-win32-ia32-msvc": "1.7.11", + "@rspack/binding-win32-x64-msvc": "1.7.11" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.11.tgz", + "integrity": "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.11.tgz", + "integrity": "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.11.tgz", + "integrity": "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.11.tgz", + "integrity": "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.11.tgz", + "integrity": "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.11.tgz", + "integrity": "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.11.tgz", + "integrity": "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "1.0.7" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.11.tgz", + "integrity": "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.11.tgz", + "integrity": "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.11.tgz", + "integrity": "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/core": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.11.tgz", + "integrity": "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime-tools": "0.22.0", + "@rspack/binding": "1.7.11", + "@rspack/lite-tapable": "1.1.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", + "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", + "license": "MIT" + }, + "node_modules/@rspack/plugin-react-refresh": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.6.1.tgz", + "integrity": "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw==", + "license": "MIT", + "dependencies": { + "error-stack-parser": "^2.1.4", + "html-entities": "^2.6.0" + }, + "peerDependencies": { + "react-refresh": ">=0.10.0 <1.0.0", + "webpack-hot-middleware": "2.x" + }, + "peerDependenciesMeta": { + "webpack-hot-middleware": { + "optional": true + } + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.14.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.9.tgz", + "integrity": "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.7", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.7.tgz", + "integrity": "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/dom-mediacapture-transform": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.12.tgz", + "integrity": "sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw==", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.444", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", + "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "license": "Unlicense" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/katex": { + "version": "0.18.10", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.18.10.tgz", + "integrity": "sha512-/B6p9eY9DX7aHBfpkHdpirDTZ5QH9xTwL9y837PWuzuv41O5jFdqNQwVQEQsimY0/iVNkwBY4+4hMfhQ7d4Dbw==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^15.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/kiwi-schema": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/kiwi-schema/-/kiwi-schema-0.5.0.tgz", + "integrity": "sha512-X+FpfU0yTEtc6aTHS7VwbOpvQwRt70+pXXWRI5fd6CvWhe7pSVC854TVo4Zo0x5/wwcWj+/9KUlXpdcP0dY9AA==", + "license": "MIT", + "bin": { + "kiwic": "cli.js" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/mediabunny": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.56.1.tgz", + "integrity": "sha512-DNOm0bRBmeOxJgCK/g3LxNWbxY5morTauNdlychujGmJ9L37ezVezkNoI1WtO2Xdb4WlWWJlYfIKnOEzd5phRw==", + "license": "MPL-2.0", + "workspaces": [ + ".", + "packages/*" + ], + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + } + }, + "node_modules/memfs": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remotion": { + "version": "4.0.532", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.532.tgz", + "integrity": "sha512-YsxSVz4E7HDKhC9v+O9sNCA9ud05VuWbjTX/ko51lf4MV+uyyrvsAyVNbPW2pW9Nk5Ty+JPdW3osRr5A+hXOoA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.5.0.tgz", + "integrity": "sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/svg-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.1.0.tgz", + "integrity": "sha512-bwLf38YmY+TDYHJw1Ex0Co8c4yeXuJAo8YnXGZrscxrvYoVVIvLeniEkV1Ks/54VteMnL4FtyN1+P+TZJdUPmQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.6.0.tgz", + "integrity": "sha512-EIMmPVvNI0CYXyRPp34F2Qk7W37/BZVZIofS6LAUnrNjCVahS+hO4mxwFJDjQjODTGZDtVzLYoN2+1dYkhk9qA==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a22f4a0 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "explainer-kit", + "private": true, + "type": "module", + "scripts": { + "studio": "remotion studio src/index.ts", + "render": "remotion render src/index.ts --gl=angle", + "still": "remotion still src/index.ts", + "tts": "python tts/gen.py", + "tts:mock": "python tts/gen.py --mock", + "fonts": "node tools/fonts.mjs", + "stills": "node tools/stills.mjs", + "pick": "node tools/pick.mjs", + "sheet": "python tools/sheet.py", + "music": "python tools/music.py", + "mix": "python tools/mix.py", + "typecheck": "tsc --noEmit", + "test": "pytest" + }, + "dependencies": { + "@remotion/bundler": "4.0.532", + "@remotion/cli": "4.0.532", + "@remotion/renderer": "4.0.532", + "katex": "^0.18.9", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "remotion": "4.0.532" + }, + "devDependencies": { + "@types/katex": "^0.16.8", + "@types/node": "^24.0.0", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "typescript": "^7.0.2" + } +} diff --git a/public/audio/s01.wav b/public/audio/s01.wav new file mode 100644 index 0000000..ee60c85 Binary files /dev/null and b/public/audio/s01.wav differ diff --git a/public/audio/s02.wav b/public/audio/s02.wav new file mode 100644 index 0000000..39057a3 Binary files /dev/null and b/public/audio/s02.wav differ diff --git a/reference/fx/grain0.png b/public/fx/grain0.png similarity index 100% rename from reference/fx/grain0.png rename to public/fx/grain0.png diff --git a/reference/fx/grain1.png b/public/fx/grain1.png similarity index 100% rename from reference/fx/grain1.png rename to public/fx/grain1.png diff --git a/reference/fx/grain2.png b/public/fx/grain2.png similarity index 100% rename from reference/fx/grain2.png rename to public/fx/grain2.png diff --git a/reference/fx/grain3.png b/public/fx/grain3.png similarity index 100% rename from reference/fx/grain3.png rename to public/fx/grain3.png diff --git a/reference/fx/paper.png b/public/fx/paper.png similarity index 100% rename from reference/fx/paper.png rename to public/fx/paper.png diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0205c16 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["tts"] diff --git a/reference/ontology/package.json b/reference/ontology/package.json deleted file mode 100644 index a979fdc..0000000 --- a/reference/ontology/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "ontology-video", - "private": true, - "scripts": { - "tts": "python tts/gen.py", - "studio": "remotion studio src/index.ts", - "render": "remotion render src/index.ts Ontology out/ontology.mp4", - "tts:en": "python tts/gen.py --lang en", - "render:en": "remotion render src/index.ts OntologyEN out/ontology-en.mp4" - }, - "dependencies": { - "@remotion/cli": "^4.0.529", - "katex": "^0.18.9", - "react": "^19.3.0", - "react-dom": "^19.3.0", - "remotion": "^4.0.529" - }, - "devDependencies": { - "@types/katex": "^0.16.8", - "@types/react": "^19.3.0", - "@types/react-dom": "^19.3.0", - "typescript": "^7.0.2" - } -} diff --git a/reference/ontology/src/Video.tsx b/reference/ontology/src/Video.tsx deleted file mode 100644 index 0547399..0000000 --- a/reference/ontology/src/Video.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, {useEffect, useState} from 'react'; -import {AbsoluteFill, Audio, continueRender, delayRender, interpolate, Sequence, staticFile, useCurrentFrame} from 'remotion'; -import {C, FONT} from './theme'; -import {FPS, LEAD, Lang, SCENE_IDS, SceneProvider, TIMINGS, sceneFrames} from './lib'; -import {SCENES} from './scenes'; - -export const totalFrames = (lang: Lang) => SCENE_IDS.reduce((a, id) => a + sceneFrames(id, lang), 0); -const starts = (lang: Lang) => { - const out: Record = {}; - SCENE_IDS.reduce((acc, id) => ((out[id] = acc), acc + sceneFrames(id, lang)), 0); - return out; -}; -const STARTS: Record> = {zh: starts('zh'), en: starts('en')}; - -const fontCss = ` -@font-face { font-family: "NotoSansSC"; src: url("${staticFile('fonts/NotoSansSC-VF.ttf')}") format("truetype"); font-weight: 100 900; } -@font-face { font-family: "NotoSerifSC"; src: url("${staticFile('fonts/NotoSerifSC-VF.ttf')}") format("truetype"); font-weight: 200 900; } -.katex { font-size: 1em !important; } -`; - -const useFonts = () => { - const [handle] = useState(() => delayRender('fonts')); - useEffect(() => { - Promise.all([ - document.fonts.load('400 40px "NotoSansSC"', '本体'), - document.fonts.load('800 40px "NotoSansSC"', '本体'), - document.fonts.load('800 40px "NotoSerifSC"', '本体'), - document.fonts.load('40px KaTeX_Main', 'x'), - document.fonts.load('italic 40px KaTeX_Math', 'x'), - document.fonts.load('40px KaTeX_Size1', '('), - document.fonts.load('40px KaTeX_Size2', '('), - ]) - .catch(() => undefined) - .then(() => continueRender(handle)); - }, [handle]); -}; - -const Background: React.FC = () => { - const f = useCurrentFrame(); - const drift = (f * 0.25) % 60; - return ( - - - - - ); -}; - -const Subtitles: React.FC<{id: string; lang: Lang}> = ({id, lang}) => { - const f = useCurrentFrame(); - const t = (f - LEAD) / FPS; - const line = TIMINGS[lang][id].lines.find((l: any) => t >= l.start && t < l.end); - if (!line) return null; - const text: string = line.text.replace(lang === 'en' ? /[,;:]$/ : /[,。;:、]$/, '').trimEnd(); - const charTime: number[] = []; - for (let i = 0; i < text.length; i++) charTime[i] = Infinity; - for (const w of line.words) for (let i = w.s; i < Math.min(w.e, text.length); i++) charTime[i] = w.t; - let last = line.start; - for (let i = 0; i < text.length; i++) charTime[i] = charTime[i] === Infinity ? last : (last = charTime[i]); - const age = t - line.start; - const op = interpolate(age, [0, 0.12], [0, 1], {extrapolateRight: 'clamp'}); - return ( -
-
- {text.split('').map((ch, i) => ( - = charTime[i] ? C.ink : 'rgba(237,232,220,0.42)'}}> - {ch} - - ))} -
-
- ); -}; - -const Chrome: React.FC<{id: string; idx: number; lang: Lang}> = ({id, idx, lang}) => { - const f = useCurrentFrame(); - const S = STARTS[lang]; - const TOTAL = totalFrames(lang); - const g = S[id] + f; - const op = interpolate(f, [0, 15], [0, 1], {extrapolateRight: 'clamp'}); - return ( - <> -
- {String(idx + 1).padStart(2, '0')} - - {TIMINGS[lang][id].chapter} -
-
- {lang === 'en' ? 'WHAT IS AN ONTOLOGY?' : '什么是「本体」· ONTOLOGY'} -
-
-
- {SCENE_IDS.map((sid) => ( -
- ))} -
- - ); -}; - -const SceneShell: React.FC<{id: string; idx: number; lang: Lang}> = ({id, idx, lang}) => { - const f = useCurrentFrame(); - const n = sceneFrames(id, lang); - const Comp = SCENES[id]; - const op = interpolate(f, [0, 10, n - 10, n], [0, 1, 1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); - return ( - - {Comp ? : null} - - - - - - ); -}; - -export const Video: React.FC<{lang: Lang}> = ({lang}) => { - useFonts(); - return ( - - - - {SCENE_IDS.map((id, i) => ( - - - - ))} - - ); -}; diff --git a/reference/ontology/src/lib.tsx b/reference/ontology/src/lib.tsx deleted file mode 100644 index 1193e75..0000000 --- a/reference/ontology/src/lib.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import React, {createContext, useContext, useMemo} from 'react'; -import {Easing, interpolate, spring, useCurrentFrame} from 'remotion'; -import katex from 'katex'; -import timingsZh from './timings.json'; -import timingsEn from './timings.en.json'; -import {C, FONT} from './theme'; - -export const FPS = 30; -export const LEAD = 10; -export const TAIL = 26; - -export type Lang = 'zh' | 'en'; -type Timings = Record; lines: any[]}>; -export const TIMINGS: Record = {zh: timingsZh as unknown as Timings, en: timingsEn as unknown as Timings}; -export const SCENE_IDS = Object.keys(TIMINGS.zh); -export const sceneFrames = (id: string, lang: Lang) => LEAD + Math.ceil(TIMINGS[lang][id].duration * FPS) + TAIL; - -const SceneCtx = createContext<{id: string; lang: Lang}>({id: 's01', lang: 'zh'}); -export const SceneProvider = SceneCtx.Provider; -export const useLang = () => useContext(SceneCtx).lang; -/** pick the Chinese or English variant of any value */ -export const pick = (lang: Lang, zh: V, en: V): V => (lang === 'en' ? en : zh); - -export const useScene = () => { - const {id, lang} = useContext(SceneCtx); - const f = useCurrentFrame(); - const T = TIMINGS[lang]; - const c = (k: string) => { - const v = T[id].cues[k]; - if (v === undefined) throw new Error(`cue ${k} missing in ${id} (${lang})`); - return LEAD + Math.round(v * FPS); - }; - const L = (zh: V, en: V): V => (lang === 'en' ? en : zh); - return {f, c, id, lang, L, en: lang === 'en', total: sceneFrames(id, lang)}; -}; - -const EASE = Easing.bezier(0.16, 0.84, 0.24, 1); -/** 0→1 eased progress starting at frame `at`. */ -export const ease = (f: number, at: number, dur = 18) => - interpolate(f, [at, at + dur], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASE}); -/** springy 0→1 (slight overshoot). */ -export const pop = (f: number, at: number) => - f < at ? 0 : spring({frame: f - at, fps: FPS, config: {damping: 13, stiffness: 140, mass: 0.8}}); -/** fade-up style. */ -export const fu = (p: number, dy = 28): React.CSSProperties => ({ - opacity: p, - transform: `translateY(${(1 - p) * dy}px)`, -}); -/** progress between two cues, for moving things. */ -export const lerp = (p: number, a: number, b: number) => a + (b - a) * p; - -export const Tex: React.FC<{tex: string; size?: number; color?: string; style?: React.CSSProperties; display?: boolean}> = ({ - tex, - size = 48, - color = C.ink, - style, - display = false, -}) => { - const html = useMemo( - () => katex.renderToString(tex, {displayMode: display, throwOnError: false, strict: false, output: 'html'}), - [tex, display], - ); - return ; -}; - -export const Kicker: React.FC<{children: React.ReactNode; color?: string; style?: React.CSSProperties}> = ({ - children, - color = C.orange, - style, -}) => ( -
{children}
-); - -export const H: React.FC<{children: React.ReactNode; size?: number; style?: React.CSSProperties; serif?: boolean}> = ({ - children, - size = 64, - style, - serif, -}) => ( -
- {children} -
-); - -export const Panel: React.FC<{children: React.ReactNode; style?: React.CSSProperties; accent?: string}> = ({ - children, - style, - accent, -}) => ( -
- {children} -
-); - -export const Chip: React.FC<{children: React.ReactNode; color?: string; fill?: boolean; style?: React.CSSProperties; size?: number}> = ({ - children, - color = C.teal, - fill, - style, - size = 34, -}) => ( - - {children} - -); - -/** Absolutely positioned box helper. */ -export const At: React.FC<{x: number; y: number; w?: number; h?: number; center?: boolean; style?: React.CSSProperties; children?: React.ReactNode}> = ({ - x, - y, - w, - h, - center, - style, - children, -}) => ( -
- {children} -
-); - -/** Line that draws itself; p in [0,1]. */ -export const DrawLine: React.FC<{x1: number; y1: number; x2: number; y2: number; p: number; color?: string; w?: number; dash?: boolean; arrow?: boolean}> = ({ - x1, - y1, - x2, - y2, - p, - color = C.teal, - w = 4, - dash, - arrow, -}) => { - const len = Math.hypot(x2 - x1, y2 - y1); - const ex = x1 + (x2 - x1) * p; - const ey = y1 + (y2 - y1) * p; - const ang = Math.atan2(y2 - y1, x2 - x1); - const ah = 16; - return ( - 0 ? 1 : 0}> - - {arrow && p > 0.05 && len > 0 ? ( - - ) : null} - - ); -}; - -export const Full: React.FC<{children?: React.ReactNode; style?: React.CSSProperties}> = ({children, style}) => ( -
{children}
-); - -export const Svg: React.FC<{children?: React.ReactNode}> = ({children}) => ( - - {children} - -); - -/** Crossfade helper: visible between frames a..b with fades. */ -export const window_ = (f: number, a: number, b: number, fade = 12) => { - const fd = Math.max(1, Math.min(fade, (b - a) / 2 - 1)); - return interpolate(f, [a, a + fd, b - fd, b], [0, 1, 1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); -}; - -export const num = (v: number, digits = 0) => - v.toLocaleString('en-US', {minimumFractionDigits: digits, maximumFractionDigits: digits}); diff --git a/reference/ontology/src/scenes/D.tsx b/reference/ontology/src/scenes/D.tsx deleted file mode 100644 index cdd80b8..0000000 --- a/reference/ontology/src/scenes/D.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import React from 'react'; -import {random} from 'remotion'; -import {C, FONT} from '../theme'; -import {At, Chip, Full, H, Kicker, Panel, Tex, ease, fu, pop, useLang, useScene, window_} from '../lib'; -import {Onepager, R} from '../Onepager'; -import {camAt} from './A'; -import {Check, Cross} from './B'; - -/* ───────────────────────── s15 · 为什么是设备级 ───────────────────────── */ -const Tangle: React.FC<{p: number}> = ({p}) => { - const pts = Array.from({length: 26}).map((_, i) => [20 + random(`tx${i}`) * 360, 20 + random(`ty${i}`) * 200]); - return ( - - {pts.map((a, i) => { - const b = pts[Math.floor(random(`te${i}`) * pts.length)]; - const b2 = pts[(i * 5 + 3) % pts.length]; - return ( - - - - - - ); - })} - - ); -}; -const Repeat: React.FC<{p: number; f: number}> = ({p, f}) => ( - - {[0, 1, 2].map((k) => ( - - - - - - - - ))} - - - - -); -const BRICKS = [ - {l: '空压机', en: 'Compressor', col: C.tealDeep}, - {l: '萃取槽', en: 'Extraction tank', col: '#3f7f5a'}, - {l: '水泵', en: 'Pump', col: '#2d6680'}, - {l: '换热器', en: 'Heat exchanger', col: '#7a5a2d'}, -]; -const Brick: React.FC<{l: string; en: string; col: string; w?: number}> = ({l, en, col, w = 230}) => { - const isEn = useLang() === 'en'; - return ( -
- {[0.25, 0.75].map((x) => ( -
- ))} - {isEn ? en : l} -
- ); -}; -export const S15: React.FC = () => { - const {f, c, L} = useScene(); - const doc = 1 - ease(f, c('big') - 10, 16); - const cards = window_(f, c('big') - 6, c('lego') + 10, 14); - const lego = window_(f, c('lego'), c('phys') + 8, 14); - const phys = window_(f, c('phys'), c('F') + 8, 14); - const F = ease(f, c('F'), 20); - return ( - - - - {[ - {k: 'big', t: L('全企业大本体', 'One giant enterprise ontology'), d: L('太大,建不完', 'Too big, never finished'), icon: , ok: false}, - {k: 'scene', t: L('按场景的本体', 'One ontology per use case'), d: L('每个项目重来一遍', 'Start over every project'), icon: , ok: false}, - {k: 'dev', t: L('单台设备的本体', 'One per equipment type'), d: L('做一块,到处拼', 'Build once, snap in anywhere'), icon:
, ok: true}, - ].map((k, i) => { - const p = ease(f, c(k.k)); - return ( - - -
{k.t}
-
{k.icon}
-
- {k.ok ? : } {k.d} -
-
-
- ); - })} -
- - {L(['工厂 1 · 智利', '工厂 2 · 印尼'], ['Plant 1 · Chile', 'Plant 2 · Indonesia']).map((name, fi) => ( - -
{name}
-
- ))} - {[0, 1].map((fi) => - BRICKS.map((b, i) => { - const p = pop(f, c('lego') + 10 + fi * 50 + i * 8); - const x = 260 + fi * 860 + (i % 2) * 290; - const y = 350 + Math.floor(i / 2) * 200; - return ( - - - - ); - }), - )} - - {L('同一套设备本体,直接复用', 'The same equipment ontology, reused as-is')} - -
- - - -
- {L('物理规律 · 能复用', 'Physics · reusable')} -
-
-
{L('压力每降 1 bar ≈ 省电 7%', '1 bar lower ≈ 7% less power')}
-
{L('比功率 = 功率 ÷ 排气量', 'Specific power = power ÷ airflow')}
-
{L('空压机在哪国都一样', 'A compressor is a compressor anywhere')}
-
-
-
- - -
- {L('人定规则 · 难复用', 'Man-made rules · hard to reuse')} -
-
-
{L('票据格式', 'Invoice formats')}
-
{L('信用证条款', 'Letter-of-credit terms')}
-
{L('各国税法与合规', 'Tax & compliance, country by country')}
-
-
-
-
- - - - - - - {L('F = 设备本体库:做一次,到处用', 'F = the equipment ontology library: build once, use everywhere')} - - - -
- ); -}; - -/* ───────────────────────── s16 · 清醒一下 ───────────────────────── */ -const COLS = 48, - ROWS = 13; -export const S16: React.FC = () => { - const {f, c, L} = useScene(); - const doc = ease(f, c('platform'), 24); - const n = COLS * ROWS; - const mapped = Math.floor(Math.max(0, f - c('cost') - 20) * 0.9); - const cam = camAt(f, [ - [0, R.unverified], - [c('trust'), R.learn4], - ]); - return ( - - - - {L('本体不是魔法', 'An ontology isn’t magic')} - - - {Array.from({length: n}).map((_, i) => { - const order = Math.floor(random(`m${i}`) * n); - const isMapped = order < mapped; - const drifted = f > c('drift') + 10 && random(`d${i}`) < 0.06 * ease(f, c('drift') + 10, 40) * 1.0 && isMapped; - return
; - })} - - - ■ {L('已对应到本体:', 'Mapped to the ontology: ')}{Math.min(100, Math.round((mapped / n) * 100))}% - ■ {L('几万个数据点,逐个人工映射', 'Tens of thousands of points, mapped by hand')} - ■ {L('改造 / 换传感器 → 要维护', 'Upgrades & new sensors → upkeep')} - - - - - ); -}; - -/* ───────────────────────── s17 · 收尾 ───────────────────────── */ -export const S17: React.FC = () => { - const {f, c, L} = useScene(); - const l3 = ease(f, c('l3'), 24); - const end = ease(f, c('end'), 30); - return ( - - - - - {L('大模型', 'The LLM')} - - {L('负责思考', 'does the thinking')} - - - - {L('本 体', 'The ontology')} - - {L('负责让它知道自己在说什么', 'makes sure it knows what it’s talking about')} - - - {L(['哪一台机器', '哪一个数', '哪一条规律'], ['which machine', 'which number', 'which law of physics']).map((k, i) => ( - - - {k} - - - ))} - - - -
- {L( - <> - 本体不是更聪明的 AI, -
- 而是让 AI 知道自己在说什么。 - , - <> - An ontology isn’t a smarter AI — -
- it’s what lets AI know what it’s talking about. - , - )} -
-
- -
-
{L('客户关心的是:工作完成得怎样,以及为此付出了什么。', 'Customers care about how well the job gets done — and what it cost them.')}
-
- {L( - '内容依据《工业 AI 出海:把「专家的手艺」做成可复制的产品》一页纸(整理自 Woltz 蔡博分享)', - 'Based on the one-pager “Industrial AI Goes Global: Turning Expert Craft into Repeatable Products” (from Dr. Cai’s talk at Woltz)', - )} -
- - - ); -}; diff --git a/reference/ontology/src/theme.ts b/reference/ontology/src/theme.ts deleted file mode 100644 index a756ad9..0000000 --- a/reference/ontology/src/theme.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const C = { - bg: '#08201f', - bg2: '#0c2d2c', - panel: 'rgba(237,232,220,0.045)', - panelLine: 'rgba(237,232,220,0.14)', - ink: '#EDE8DC', - muted: '#8FAAA6', - dim: '#4d6b68', - teal: '#2BB3A6', - tealDeep: '#0f7b78', - tealSoft: 'rgba(43,179,166,0.16)', - orange: '#E8913F', - orangeSoft: 'rgba(232,145,63,0.16)', - red: '#E5645A', - green: '#7BCB6B', -}; - -export const FONT = { - // --latin is set per language on the root: Segoe UI for English, nothing extra for Chinese - sans: 'var(--latin, "NotoSansSC"), "NotoSansSC", "Microsoft YaHei", sans-serif', - serif: 'var(--latin-serif, "NotoSerifSC"), "NotoSerifSC", "Noto Serif SC", serif', - mono: '"Cascadia Code", Consolas, monospace', -}; diff --git a/reference/zzm/README.md b/reference/zzm/README.md deleted file mode 100644 index f4a88cb..0000000 --- a/reference/zzm/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# 三张牌 · 张忠谋与台积电的四十年棋局 - -这是一支讲解视频,根据一份《张忠谋自传》解读文稿制作,主题有两个:张忠谋作为 CEO 的战略,以及台积电夹在中美之间时他怎么下棋。文稿几乎没有地缘政治的内容,这部分全部是联网核对后补上的,逐条出处见 `SOURCES.md`。 - -现在是 v2「故事版」。v1 是按主题分段的讲解版,画面像动态 PPT,存档在 git 第一个提交和 `out/zzm-v1.mp4`。 - -## 结构:一个钩子接一个钩子 - -全片按时间线讲,每一场都停在一个没回答的问题上,下一场开头接住它: - -- **序章**(s01):2022 年亚利桑那沙漠里那句「全球化几乎已死」,然后抛出总悬念——靠全球化起家的人为什么宣布它死亡?片尾一句「那一年,52 岁的张忠谋,丢了工作」,接片名。 -- **崛起**(s02–s07):1983 年失业,接着是 1987 年的 A/B 选择(暂停让观众先猜)、英特尔、1997 年那通电话、2009 年 77 岁复出(第二次暂停)、苹果敲门、请黄仁勋接班。 -- **夹缝**(s08–s13):先摆出棋盘和三张牌,再依次讲中芯官司、去不去大陆、兵家必争之地、断供华为,然后是 2022 年的四句话。第四句回到开场的沙漠,序章埋下的问题在这里收回。最后是亚利桑那。 -- **对账与尾声**(s14–s15):在账簿上逐条盖章,然后留给观众四条原则和一个问题。 - -## 画面 - -画面是电影感插画,不用幻灯片。全片加了 2.2:1 黑边,字幕放在下边的黑边里,叠一层胶片颗粒和暗角,色调按年代分:1983 年是旧照片的棕褐色,八十年代到 2013 年偏暖,地缘政治部分偏冷,沙漠用黄昏色。每一场由多个镜头组成,镜头在旁白的 cue 上切换,每个镜头自带缓推的运镜。 - -视觉母题有三个:张忠谋的剪影带一支烟斗,三股力量画成一张棋盘,三条策略画成三张烫金纸牌。数据都放在片中实物里,比如股权证书、法院和解文书、年报第 105 页、保单和对账簿。 - -## 基本参数 - -- 合成 ID `ZhangZhongmou`,1920×1080,30fps,15 场,约 21.6 分钟 -- 旁白用 edge-tts `zh-CN-YunjianNeural` +10%。张忠谋的原话用 `zh-TW-YunJheNeural` 读,其他人的引语用 `zh-CN-YunxiNeural` -- 成片 `out/zzm.mp4`(已混配乐),未混音的版本是 `out/zzm-raw.mp4` - -## 管线 - -1. 改旁白:编辑 `tts/script.json`。标记有三种:`[[cue]]` 是镜头切换点;`||` 是戏剧停顿,时长取 `pause`;`<>` 表示这段换引语声音读,键名对应 `voices`。只重做某几场用 `python tts/gen.py s04`。 -2. 节奏:`src/pace.json` 控制每场开头章节卡和结尾钩子黑场的帧数。 -3. 审图:`node tools/stills.mjs s01 s02` 在每个 cue 后约 2.5 秒出一张图,再用 `python tools/sheet.py s01` 拼成 2×2。只看指定帧用 `node tools/pick.mjs s04:matrix:40`,输出在 `out/pick/`。 -4. 渲染:`npm run render`(带 `--gl=angle`,走 GPU 合成,约 25 分钟;纯软件渲染要两个多小时)。然后 `python tools/music.py` 生成 `out/music.wav`,再混音: - -``` -ffmpeg -i out/zzm-raw.mp4 -i out/music.wav -filter_complex "[1:a]lowpass=f=2400,volume=0.55[m];[m][0:a]sidechaincompress=threshold=0.02:ratio=5:attack=60:release=800[md];[0:a][md]amix=inputs=2:duration=first:normalize=0,loudnorm=I=-16:TP=-1.5:LRA=11[a]" -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k out/zzm.mp4 -``` - -| 文件 | 作用 | -|---|---| -| `src/film.tsx` | 电影层:`Shots`(按 cue 切镜头)、`Cam`(运镜)、`Graded`(年代调色)、颗粒、黑边、`Place`(年份地点字幕)、`BigQuote`、`Paper`、`Stamp`、`Chapter` | -| `src/art.tsx` | SVG 插画库:天空、山脊、城市天际线、剪影人物(含烟斗和烟)、观众、工厂、门、棋子、棋盘、晶圆、硬币、法院、台湾海峡 | -| `src/sets.tsx` | 复用场景:沙漠、书桌、窗景、棋盘、纸牌、海峡 | -| `src/props.tsx` | 道具:报纸、文书、照片、天平、碗、盾、日历、墨水条形图 | -| `src/scenes/s01–s15.tsx` | 逐场镜头表 | -| `src/lib.tsx` | 时间轴:`c(cue)` 取 cue 帧,`w(词)` 取旁白说到某个词的帧,`rel(词, cue)` 取某个 cue 之后第一次说到这个词的相对帧 | -| `tools/music.py` | 合成配乐:底层氛围音按三幕换和声,紧张段落加心跳脉冲,每场钩子处有上扬音效接低频重击,章节卡响一声钟 | diff --git a/reference/zzm/package.json b/reference/zzm/package.json deleted file mode 100644 index f0ce2f7..0000000 --- a/reference/zzm/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "zhang-zhongmou-video", - "private": true, - "scripts": { - "tts": "python tts/gen.py", - "studio": "remotion studio src/index.ts", - "render": "remotion render src/index.ts ZhangZhongmou out/zzm-raw.mp4 --gl=angle --concurrency=8", - "still": "remotion still src/index.ts ZhangZhongmou" - }, - "dependencies": { - "@remotion/cli": "^4.0.529", - "katex": "^0.18.9", - "react": "^19.3.0", - "react-dom": "^19.3.0", - "remotion": "^4.0.529" - }, - "devDependencies": { - "@types/katex": "^0.16.8", - "@types/react": "^19.3.0", - "@types/react-dom": "^19.3.0", - "typescript": "^7.0.2" - } -} \ No newline at end of file diff --git a/reference/zzm/src/Root.tsx b/reference/zzm/src/Root.tsx deleted file mode 100644 index d6d5bda..0000000 --- a/reference/zzm/src/Root.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import {Composition} from 'remotion'; -import {Video, totalFrames} from './Video'; - -export const Root: React.FC = () => ( - -); diff --git a/reference/zzm/src/Video.tsx b/reference/zzm/src/Video.tsx deleted file mode 100644 index a21d1ba..0000000 --- a/reference/zzm/src/Video.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, {useEffect, useState} from 'react'; -import {AbsoluteFill, Audio, continueRender, delayRender, interpolate, Sequence, staticFile, useCurrentFrame} from 'remotion'; -import {C, FONT} from './theme'; -import {FPS, Lang, SCENE_IDS, SceneProvider, TIMINGS, leadOf, sceneFrames} from './lib'; -import {SCENES} from './scenes'; -import {BAR, Chapter, Grain, Letterbox, Vignette} from './film'; - -export const totalFrames = (lang: Lang) => SCENE_IDS.reduce((a, id) => a + sceneFrames(id, lang), 0); -const starts = (lang: Lang) => { - const out: Record = {}; - SCENE_IDS.reduce((acc, id) => ((out[id] = acc), acc + sceneFrames(id, lang)), 0); - return out; -}; -const STARTS: Record> = {zh: starts('zh'), en: starts('zh')}; - -const fontCss = ` -@font-face { font-family: "NotoSansSC"; src: url("${staticFile('fonts/NotoSansSC-VF.ttf')}") format("truetype"); font-weight: 100 900; } -@font-face { font-family: "NotoSerifSC"; src: url("${staticFile('fonts/NotoSerifSC-VF.ttf')}") format("truetype"); font-weight: 200 900; } -`; - -const useFonts = () => { - const [handle] = useState(() => delayRender('fonts')); - useEffect(() => { - Promise.all([ - document.fonts.load('400 40px "NotoSansSC"', '台积'), - document.fonts.load('800 40px "NotoSansSC"', '张忠谋'), - document.fonts.load('800 40px "NotoSerifSC"', '张忠谋'), - document.fonts.load('500 40px "NotoSerifSC"', '张忠谋'), - ]) - .catch(() => undefined) - .then(() => continueRender(handle)); - }, [handle]); -}; - -/** Film subtitles, sitting in the bottom letterbox bar. Quoted lines are set in serif gold. */ -const Subtitles: React.FC<{id: string; lang: Lang}> = ({id, lang}) => { - const f = useCurrentFrame(); - const t = (f - leadOf(id)) / FPS; - const line = TIMINGS[lang][id].lines.find((l: any) => t >= l.start && t < l.end); - if (!line) return null; - const text: string = line.text.replace(/[,。;:、]$/, '').trimEnd(); - const q = Boolean(line.q); - const op = interpolate(t - line.start, [0, 0.1], [0, 1], {extrapolateRight: 'clamp'}) * interpolate(line.end - t, [0, 0.08], [0, 1], {extrapolateRight: 'clamp'}); - return ( -
-
- {q ? `「${text}」` : text} -
-
- ); -}; - -const splitChapter = (s: string) => { - const [k, ...rest] = s.split(' · '); - return {kicker: k, title: rest.join(' · ')}; -}; - -const SceneShell: React.FC<{id: string; lang: Lang}> = ({id, lang}) => { - const f = useCurrentFrame(); - const n = sceneFrames(id, lang); - const lead = leadOf(id); - const Comp = SCENES[id]; - const op = interpolate(f, [n - 18, n], [1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); - const ch = splitChapter(TIMINGS[lang][id].chapter); - return ( - - - {Comp ? : null} - - - - {lead > 30 ? : null} - - - - - - ); -}; - -export const Video: React.FC<{lang: Lang}> = ({lang}) => { - useFonts(); - return ( - - - {SCENE_IDS.map((id) => ( - - - - ))} - - ); -}; diff --git a/reference/zzm/src/art.tsx b/reference/zzm/src/art.tsx deleted file mode 100644 index dedf282..0000000 --- a/reference/zzm/src/art.tsx +++ /dev/null @@ -1,562 +0,0 @@ -/** SVG illustration library: skies, ridges, skylines, silhouettes, props. All render inside . */ -import React from 'react'; -import {C} from './theme'; -import {clamp01, mix, rng} from './film'; - -export const Defs: React.FC = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -/** vertical sky gradient filling the frame */ -export const Sky: React.FC<{id: string; stops: [number, string][]}> = ({id, stops}) => ( - <> - - - {stops.map(([o, c]) => ( - - ))} - - - - -); - -export const Glow: React.FC<{x: number; y: number; r: number; id?: string; op?: number}> = ({x, y, r, id = 'glowW', op = 1}) => ( - -); - -export const Stars: React.FC<{t: number; n?: number; seed?: number; maxY?: number; op?: number}> = ({t, n = 140, seed = 3, maxY = 600, op = 1}) => { - const r = rng(seed); - return ( - - {Array.from({length: n}, (_, i) => { - const x = r() * 1920; - const y = r() * maxY; - const s = 0.6 + r() * 1.8; - const tw = 0.5 + 0.5 * Math.sin(t * (0.05 + r() * 0.08) + i); - return ; - })} - - ); -}; - -/** mountain ridge silhouette */ -export const Ridge: React.FC<{y: number; amp: number; seed: number; color: string; shift?: number; rough?: number; op?: number}> = ({ - y, - amp, - seed, - color, - shift = 0, - rough = 1, - op = 1, -}) => { - const r = rng(seed); - const ph = [r() * 6, r() * 6, r() * 6, r() * 6]; - const pts: string[] = []; - for (let x = -200; x <= 2120; x += 16) { - const X = x + shift; - const h = - Math.sin(X * 0.0021 + ph[0]) * 0.55 + - Math.sin(X * 0.0047 + ph[1]) * 0.3 + - Math.sin(X * 0.011 + ph[2]) * 0.12 * rough + - Math.sin(X * 0.031 + ph[3]) * 0.04 * rough; - pts.push(`${x},${(y - amp * (0.5 + 0.5 * h)).toFixed(1)}`); - } - return ; -}; - -/** city skyline with lit windows */ -export const Skyline: React.FC<{ - y: number; - seed: number; - color: string; - win?: string; - minH?: number; - maxH?: number; - lit?: number; - x0?: number; - x1?: number; - t?: number; -}> = ({y, seed, color, win = '#ffd48a', minH = 80, maxH = 360, lit = 0.25, x0 = -100, x1 = 2020, t = 0}) => { - const r = rng(seed); - const out: React.ReactNode[] = []; - let x = x0; - let k = 0; - while (x < x1) { - const w = 50 + r() * 110; - const h = minH + Math.pow(r(), 1.6) * (maxH - minH); - out.push(); - if (r() < 0.2) out.push(); - const cols = Math.floor((w - 12) / 16); - const rows = Math.floor((h - 20) / 22); - for (let i = 0; i < cols; i++) - for (let j = 0; j < rows; j++) { - const v = r(); - if (v < lit) { - const blink = (Math.floor(t / 90 + v * 40) % 23 === 0 ? 0.3 : 1) * (0.55 + v); - out.push(); - } - } - x += w + 4 + r() * 18; - k++; - } - return {out}; -}; - -export const Ground: React.FC<{y: number; color: string; to?: string}> = ({y, color, to}) => ( - <> - {to ? ( - - - - - - - ) : null} - - -); - -export const Saguaro: React.FC<{x: number; y: number; h: number; color: string; flip?: boolean}> = ({x, y, h, color, flip}) => { - const u = h / 100; - const s = flip ? -1 : 1; - return ( - - - - - - ); -}; - -// ───────────────────────── people ───────────────────────── - -export type Pose = 'stand' | 'walk' | 'phone' | 'point' | 'bust' | 'podium' | 'sit' | 'shovel'; - -/** - * Silhouette person. (x,y) = feet (or seat for 'sit'/'bust'), h = standing height in px. - * old=true adds a slight stoop; pipe=true adds Chang's pipe. - */ -export const Figure: React.FC<{ - x: number; - y: number; - h: number; - pose?: Pose; - color?: string; - facing?: 1 | -1; - phase?: number; - old?: boolean; - pipe?: boolean; - rim?: string; - bun?: boolean; -}> = ({x, y, h, pose = 'stand', color = C.silhouette, facing = 1, phase = 0, old, pipe, rim, bun}) => { - const u = h / 100; - const stoop = old ? 3 : 0; - const sw = Math.sin(phase) * (pose === 'walk' ? 16 : 0); - const legW = 7.5; - const armW = 5.6; - const headX = 1.2 + stoop; - const headY = -88 + (old ? 1.5 : 0); - const showLegs = pose !== 'bust' && pose !== 'podium' && pose !== 'sit'; - const leg = (dir: number) => { - const a = (dir * sw * Math.PI) / 180; - const kx = Math.sin(a) * 22; - const fx = Math.sin(a) * 44 + (dir * sw > 0 ? 2 : -1); - return ; - }; - const arm = (side: number) => { - // side: +1 front arm, -1 back arm - if (pose === 'phone' && side === 1) return ; - if (pose === 'point' && side === 1) return ; - if (pose === 'podium') return ; - if (pose === 'shovel') return ; - if (pose === 'sit') return ; - const a = pose === 'walk' ? (-side * sw * Math.PI) / 180 : 0.06 * side; - return ; - }; - return ( - - {showLegs ? ( - <> - {leg(-1)} - {leg(1)} - - ) : null} - {pose === 'sit' ? : null} - {arm(-1)} - {/* torso */} - - {/* neck + head */} - - - {bun ? : null} - {arm(1)} - {pipe ? ( - - - - - ) : null} - {rim ? ( - - ) : null} - - ); -}; - -/** pipe-smoke wisps rising from (x,y) */ -export const Smoke: React.FC<{t: number; x: number; y: number; color?: string; scale?: number; op?: number}> = ({t, x, y, color = '#d8d2c6', scale = 1, op = 0.5}) => { - const out: React.ReactNode[] = []; - const period = 9; - const life = 110; - for (let k = Math.floor((t - life) / period); k <= Math.floor(t / period); k++) { - const age = t - k * period; - if (age < 0 || age > life) continue; - const r = rng(k * 31 + 7); - const p = age / life; - const px = x + (Math.sin(age * 0.05 + r() * 6) * 14 + p * 30 * (r() - 0.3)) * scale; - const py = y - age * 1.1 * scale; - out.push(); - } - return {out}; -}; - -/** seated audience: rows of heads and shoulders */ -export const Crowd: React.FC<{y: number; rows?: number; seed?: number; color?: string; x0?: number; x1?: number; gap?: number; size?: number; t?: number}> = ({ - y, - rows = 3, - seed = 1, - color = C.silhouette, - x0 = -40, - x1 = 1960, - gap = 70, - size = 1, - t = 0, -}) => { - const r = rng(seed); - const out: React.ReactNode[] = []; - for (let j = 0; j < rows; j++) { - const s = size * (1 + j * 0.22); - const yy = y + j * 46 * s; - for (let x = x0 + (j % 2) * gap * 0.5; x < x1; x += gap * s * (0.85 + r() * 0.3)) { - const bob = Math.sin(t * 0.03 + x) * 0.8; - out.push( - - - - , - ); - } - } - return {out}; -}; - -export const Podium: React.FC<{x: number; y: number; s?: number; color?: string; edge?: string}> = ({x, y, s = 1, color = '#1a1714', edge = '#3a332b'}) => ( - - - - - - -); - -/** low wide factory (fab) with optional construction cranes */ -export const Fab: React.FC<{x: number; y: number; w: number; h: number; color?: string; face?: string; cranes?: boolean; lit?: boolean; t?: number}> = ({ - x, - y, - w, - h, - color = '#d8d4cc', - face = '#b9b2a6', - cranes, - lit, - t = 0, -}) => ( - - {cranes ? ( - - - - - - - ) : null} - - - - {Array.from({length: Math.floor(w / 90)}, (_, i) => ( - - ))} - - -); - -export const Door: React.FC<{x: number; y: number; w: number; h: number; open?: number; light?: string; frame?: string}> = ({ - x, - y, - w, - h, - open = 0, - light = '#ffe0a0', - frame = '#3a322a', -}) => ( - - - - {open > 0 ? : null} - - - -); - -/** table seen from the side */ -export const Table: React.FC<{x: number; y: number; w: number; color?: string; top?: string}> = ({x, y, w, color = '#17120e', top = '#3a2c20'}) => ( - - - - - -); - -export const Lamp: React.FC<{x: number; y: number; len?: number; op?: number}> = ({x, y, len = 140, op = 1}) => ( - - - - - - -); - -/** retro desk phone; ring = shake amount */ -export const Phone: React.FC<{x: number; y: number; s?: number; ring?: number; color?: string}> = ({x, y, s = 1, ring = 0, color = '#1d1915'}) => ( - - - - - -); - -// ───────────────────────── board game ───────────────────────── - -export const Piece: React.FC<{kind: 'king' | 'pawn' | 'rook' | 'queen'; x: number; y: number; s?: number; color: string; dark?: string; glow?: string; op?: number}> = ({ - kind, - x, - y, - s = 1, - color, - dark = 'rgba(0,0,0,0.35)', - glow, - op = 1, -}) => { - const body = - kind === 'pawn' - ? 'M-34,0 L34,0 L30,-14 L18,-20 L12,-70 L22,-78 L14,-84 A24,24 0 1 0 -14,-84 L-22,-78 L-12,-70 L-18,-20 L-30,-14 Z' - : kind === 'rook' - ? 'M-40,0 L40,0 L36,-16 L24,-24 L20,-100 L30,-108 L30,-136 L18,-136 L18,-124 L6,-124 L6,-136 L-6,-136 L-6,-124 L-18,-124 L-18,-136 L-30,-136 L-30,-108 L-20,-100 L-24,-24 L-36,-16 Z' - : kind === 'queen' - ? 'M-42,0 L42,0 L38,-16 L24,-26 L16,-120 L30,-132 L24,-150 L12,-136 L0,-160 L-12,-136 L-24,-150 L-30,-132 L-16,-120 L-24,-26 L-38,-16 Z' - : 'M-44,0 L44,0 L40,-18 L26,-28 L18,-130 L32,-140 L18,-150 L8,-150 L8,-166 L18,-166 L18,-176 L8,-176 L8,-188 L-8,-188 L-8,-176 L-18,-176 L-18,-166 L-8,-166 L-8,-150 L-18,-150 L-32,-140 L-18,-130 L-26,-28 L-40,-18 Z'; - return ( - - {glow ? : null} - - - - - - ); -}; - -/** perspective chess board: corners TL TR BR BL */ -export const Board: React.FC<{tl: [number, number]; tr: [number, number]; br: [number, number]; bl: [number, number]; n?: number; a?: string; b?: string; edge?: string}> = ({ - tl, - tr, - br, - bl, - n = 8, - a = '#2b2620', - b = '#1a1612', - edge = '#5a4a32', -}) => { - const P = (u: number, v: number) => { - // perspective-ish: compress v toward the far edge - const vv = Math.pow(v, 1.12); - const top = [mix(tl[0], tr[0], u), mix(tl[1], tr[1], u)]; - const bot = [mix(bl[0], br[0], u), mix(bl[1], br[1], u)]; - return [mix(top[0], bot[0], vv), mix(top[1], bot[1], vv)]; - }; - const cells: React.ReactNode[] = []; - for (let i = 0; i < n; i++) - for (let j = 0; j < n; j++) { - const p = [P(i / n, j / n), P((i + 1) / n, j / n), P((i + 1) / n, (j + 1) / n), P(i / n, (j + 1) / n)]; - cells.push( q.join(',')).join(' L')} Z`} fill={(i + j) % 2 ? a : b} />); - } - return ( - - - - {cells} - - ); -}; - -export const Wafer: React.FC<{x: number; y: number; r: number; spin?: number; op?: number; dim?: boolean}> = ({x, y, r, spin = 0, op = 1, dim}) => { - const id = `wc${Math.round(x)}${Math.round(y)}${Math.round(r)}`; - const n = Math.max(4, Math.round(r / 18)); - const cell = (2 * r) / n; - return ( - - - - - - - - - - {Array.from({length: n + 1}, (_, i) => ( - - - - - ))} - - - - - ); -}; - -/** stack of coins; n coins, top at (x, y-n*step) */ -export const Coins: React.FC<{x: number; y: number; n: number; w?: number; color?: string}> = ({x, y, n, w = 70, color = C.gold}) => ( - - {Array.from({length: Math.max(0, Math.floor(n))}, (_, i) => ( - - - - - ))} - -); - -export const Courthouse: React.FC<{x: number; y: number; w: number; color?: string; shade?: string}> = ({x, y, w, color = '#cfc6b4', shade = '#8f8676'}) => { - const h = w * 0.55; - const cols = 8; - return ( - - - - {Array.from({length: cols}, (_, i) => ( - - ))} - - - - ); -}; - -/** stylised Taiwan island outline in a 100×200 box at (x,y) with scale s */ -export const TAIWAN = 'M62,0 L72,8 L80,30 L78,62 L74,96 L66,130 L56,160 L46,186 L40,200 L32,186 L22,158 L14,128 L12,98 L18,66 L30,36 L44,14 Z'; -export const Taiwan: React.FC<{x: number; y: number; s?: number; fill?: string; stroke?: string}> = ({x, y, s = 1, fill = C.tw, stroke}) => ( - -); -/** mainland coastline (left of x0), as a filled region */ -export const Mainland: React.FC<{x0: number; fill?: string; edge?: string}> = ({x0, fill = '#2a2724', edge = '#4a443c'}) => ( - -); - -/** dashed arc from a to b, drawn to progress p */ -export const Arc: React.FC<{a: [number, number]; b: [number, number]; lift?: number; p: number; color?: string; w?: number; dash?: string}> = ({ - a, - b, - lift = 200, - p, - color = C.gold, - w = 4, - dash = '14 12', -}) => { - const mx = (a[0] + b[0]) / 2; - const my = (a[1] + b[1]) / 2 - lift; - const pts: string[] = []; - const N = 60; - for (let i = 0; i <= N * p; i++) { - const s = i / N; - const x = (1 - s) * (1 - s) * a[0] + 2 * (1 - s) * s * mx + s * s * b[0]; - const y = (1 - s) * (1 - s) * a[1] + 2 * (1 - s) * s * my + s * s * b[1]; - pts.push(`${x.toFixed(1)},${y.toFixed(1)}`); - } - if (pts.length < 2) return null; - const last = pts[pts.length - 1].split(',').map(Number); - return ( - - - - - ); -}; - -export const Pin: React.FC<{x: number; y: number; color?: string; s?: number; pulse?: number}> = ({x, y, color = C.gold, s = 1, pulse = 0}) => ( - - - - - -); diff --git a/reference/zzm/src/lib.tsx b/reference/zzm/src/lib.tsx deleted file mode 100644 index 2496929..0000000 --- a/reference/zzm/src/lib.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React, {createContext, useContext, useMemo} from 'react'; -import {Easing, interpolate, spring, useCurrentFrame} from 'remotion'; -import timingsZh from './timings.json'; -import pace from './pace.json'; -import {C, FONT} from './theme'; - -export const FPS = 30; -/** frames before narration starts (chapter card) and after it ends (hook beat), per scene */ -export const leadOf = (id: string) => (pace.leadOverride as Record)[id] ?? pace.lead; -export const tailOf = (id: string) => (pace.tailOverride as Record)[id] ?? pace.tail; - -export type Lang = 'zh' | 'en'; -type Timings = Record; lines: any[]}>; -export const TIMINGS: Record = {zh: timingsZh as unknown as Timings, en: timingsZh as unknown as Timings}; -export const SCENE_IDS = Object.keys(TIMINGS.zh); -export const sceneFrames = (id: string, lang: Lang) => leadOf(id) + Math.ceil(TIMINGS[lang][id].duration * FPS) + tailOf(id); -export const narrEnd = (id: string) => leadOf(id) + Math.ceil(TIMINGS.zh[id].duration * FPS); - -const SceneCtx = createContext<{id: string; lang: Lang}>({id: 's01', lang: 'zh'}); -export const SceneProvider = SceneCtx.Provider; -export const useLang = () => useContext(SceneCtx).lang; -/** pick the Chinese or English variant of any value */ -export const pick = (lang: Lang, zh: V, en: V): V => (lang === 'en' ? en : zh); - -export const useScene = () => { - const {id, lang} = useContext(SceneCtx); - const f = useCurrentFrame(); - const T = TIMINGS[lang]; - const c = (k: string) => { - const v = T[id].cues[k]; - if (v === undefined) throw new Error(`cue ${k} missing in ${id} (${lang})`); - return leadOf(id) + Math.round(v * FPS); - }; - const L = (zh: V, en: V): V => (lang === 'en' ? en : zh); - /** frame at which the narrator starts saying `sub` (n-th occurrence, 0-based) in this scene */ - const w = (sub: string, nth = 0) => { - let k = 0; - for (const ln of T[id].lines) { - let from = 0; - for (;;) { - const idx = ln.text.indexOf(sub, from); - if (idx < 0) break; - if (k++ === nth) { - const word = ln.words.find((x: any) => x.e > idx) ?? ln.words[ln.words.length - 1]; - return leadOf(id) + Math.round((word ? word.t : ln.start) * FPS); - } - from = idx + 1; - } - } - throw new Error(`"${sub}" not found in ${id}`); - }; - /** frames from cue `cue` until the narrator first says `sub` at or after that cue */ - const rel = (sub: string, cue: string) => { - const c0 = c(cue); - let n = 0; - for (;;) { - const fr = w(sub, n++); - if (fr >= c0 - 2) return fr - c0; - } - }; - return {f, c, w, rel, id, lang, L, en: lang === 'en', total: sceneFrames(id, lang), end: narrEnd(id), lead: leadOf(id)}; -}; - -const EASE = Easing.bezier(0.16, 0.84, 0.24, 1); -/** 0→1 eased progress starting at frame `at`. */ -export const ease = (f: number, at: number, dur = 18) => - interpolate(f, [at, at + dur], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASE}); -/** springy 0→1 (slight overshoot). */ -export const pop = (f: number, at: number) => - f < at ? 0 : spring({frame: f - at, fps: FPS, config: {damping: 13, stiffness: 140, mass: 0.8}}); -/** fade-up style. */ -export const fu = (p: number, dy = 28): React.CSSProperties => ({ - opacity: p, - transform: `translateY(${(1 - p) * dy}px)`, -}); -/** progress between two cues, for moving things. */ -export const lerp = (p: number, a: number, b: number) => a + (b - a) * p; - -export const Kicker: React.FC<{children: React.ReactNode; color?: string; style?: React.CSSProperties}> = ({ - children, - color = C.orange, - style, -}) => ( -
{children}
-); - -export const H: React.FC<{children: React.ReactNode; size?: number; style?: React.CSSProperties; serif?: boolean}> = ({ - children, - size = 64, - style, - serif, -}) => ( -
- {children} -
-); - -export const Panel: React.FC<{children: React.ReactNode; style?: React.CSSProperties; accent?: string}> = ({ - children, - style, - accent, -}) => ( -
- {children} -
-); - -export const Chip: React.FC<{children: React.ReactNode; color?: string; fill?: boolean; style?: React.CSSProperties; size?: number}> = ({ - children, - color = C.teal, - fill, - style, - size = 34, -}) => ( - - {children} - -); - -/** Absolutely positioned box helper. */ -export const At: React.FC<{x: number; y: number; w?: number; h?: number; center?: boolean; style?: React.CSSProperties; children?: React.ReactNode}> = ({ - x, - y, - w, - h, - center, - style, - children, -}) => ( -
- {children} -
-); - -/** Line that draws itself; p in [0,1]. */ -export const DrawLine: React.FC<{x1: number; y1: number; x2: number; y2: number; p: number; color?: string; w?: number; dash?: boolean; arrow?: boolean}> = ({ - x1, - y1, - x2, - y2, - p, - color = C.teal, - w = 4, - dash, - arrow, -}) => { - const len = Math.hypot(x2 - x1, y2 - y1); - const ex = x1 + (x2 - x1) * p; - const ey = y1 + (y2 - y1) * p; - const ang = Math.atan2(y2 - y1, x2 - x1); - const ah = 16; - return ( - 0 ? 1 : 0}> - - {arrow && p > 0.05 && len > 0 ? ( - - ) : null} - - ); -}; - -export const Full: React.FC<{children?: React.ReactNode; style?: React.CSSProperties}> = ({children, style}) => ( -
{children}
-); - -export const Svg: React.FC<{children?: React.ReactNode}> = ({children}) => ( - - {children} - -); - -/** Crossfade helper: visible between frames a..b with fades. */ -export const window_ = (f: number, a: number, b: number, fade = 12) => { - const fd = Math.max(1, Math.min(fade, (b - a) / 2 - 1)); - return interpolate(f, [a, a + fd, b - fd, b], [0, 1, 1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); -}; - -export const num = (v: number, digits = 0) => - v.toLocaleString('en-US', {minimumFractionDigits: digits, maximumFractionDigits: digits}); diff --git a/reference/zzm/src/pace.json b/reference/zzm/src/pace.json deleted file mode 100644 index 379fa5d..0000000 --- a/reference/zzm/src/pace.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "lead": 48, - "tail": 34, - "leadOverride": {"s01": 20}, - "tailOverride": {"s01": 150, "s15": 150} -} diff --git a/reference/zzm/src/props.tsx b/reference/zzm/src/props.tsx deleted file mode 100644 index 1ecef02..0000000 --- a/reference/zzm/src/props.tsx +++ /dev/null @@ -1,268 +0,0 @@ -/** Diegetic props: newspapers, documents, photos, ledgers, scales, calendars. Data lives inside these. */ -import React from 'react'; -import {interpolate} from 'remotion'; -import {C, FONT} from './theme'; -import {Paper, e, mix, Mark, clamp01} from './film'; - -export const Newspaper: React.FC<{ - t: number; - at?: number; - x: number; - y: number; - w?: number; - rot?: number; - mast?: string; - date: string; - head: string; - sub?: string; - headSize?: number; -}> = ({t, at = 0, x, y, w = 760, rot = -3, mast = '晚 报', date, head, sub, headSize = 74}) => ( - -
{mast}
-
- {date} - 第 1 版 -
-
{head}
- {sub ?
{sub}
: null} -
- {[0, 1, 2].map((k) => ( -
- {Array.from({length: 7}, (_, i) => ( -
- ))} -
- ))} -
- -); - -/** Typed document with title, optional subtitle and rows. Rows reveal one by one from `rowAt` frames. */ -export const Doc: React.FC<{ - t: number; - at?: number; - x: number; - y: number; - w: number; - rot?: number; - title: string; - kicker?: string; - rows?: {k: string; v?: string; at?: number; mark?: boolean; strike?: boolean}[]; - size?: number; - children?: React.ReactNode; - tone?: string; -}> = ({t, at = 0, x, y, w, rot = 0, title, kicker, rows = [], size = 34, children, tone}) => ( - - {kicker ?
{kicker}
: null} -
{title}
- {rows.map((r, i) => { - const p = e(t, r.at ?? at + 10 + i * 8, 12); - return ( -
- {r.mark ? {r.k} : r.k} - {r.v ? {r.v} : null} -
- ); - })} - {children} -
-); - -/** Polaroid-style photo containing a 1920×1080 SVG scene, scaled down. */ -export const Photo: React.FC<{t: number; at?: number; x: number; y: number; w: number; rot?: number; caption?: string; children: React.ReactNode; sepia?: boolean}> = ({ - t, - at = 0, - x, - y, - w, - rot = 0, - caption, - children, - sepia = true, -}) => { - const h = (w * 1080) / 1920; - const p = e(t, at, 20); - return ( -
-
- - {children} - -
- {caption ? ( -
{caption}
- ) : null} -
- ); -}; - -/** Balance scale. tilt > 0 means the left pan is heavier. */ -export const Balance: React.FC<{x: number; y: number; tilt: number; left: React.ReactNode; right: React.ReactNode; lc?: string; rc?: string; s?: number}> = ({ - x, - y, - tilt, - left, - right, - lc = C.gold, - rc = '#9aa4b0', - s = 1, -}) => { - const a = (tilt * 12 * Math.PI) / 180; - const L = 360 * s; - const lx = x - Math.cos(a) * L; - const ly = y + Math.sin(a) * L; - const rx = x + Math.cos(a) * L; - const ry = y - Math.sin(a) * L; - const pan = (px: number, py: number, col: string, label: React.ReactNode, key: string) => ( - - - - - -
{label}
-
-
- ); - return ( - - - - - {pan(lx, ly, lc, left, 'l')} - {pan(rx, ry, rc, right, 'r')} - - ); -}; - -/** Bowl of water, side view. */ -export const Bowl: React.FC<{x: number; y: number; s?: number; label?: string; op?: number; color?: string}> = ({x, y, s = 1, label, op = 1, color = '#c9b48a'}) => ( - - - - - - {label ? ( - - {label} - - ) : null} - -); - -export const Shield: React.FC<{x: number; y: number; s?: number; p?: number; color?: string; label?: string}> = ({x, y, s = 1, p = 1, color = '#7FD3CF', label}) => ( - - - - {Array.from({length: 6}, (_, i) => ( - - ))} - {label ? ( - - {label} - - ) : null} - -); - -/** Counting number. */ -export const Count: React.FC<{t: number; at: number; dur?: number; from?: number; to: number; digits?: number; prefix?: string; suffix?: string; style?: React.CSSProperties}> = ({ - t, - at, - dur = 30, - from = 0, - to, - digits = 0, - prefix = '', - suffix = '', - style, -}) => { - const p = interpolate(t, [at, at + dur], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: (x) => 1 - Math.pow(1 - x, 3)}); - const v = mix(from, to, p); - return ( - - {prefix} - {v.toLocaleString('en-US', {minimumFractionDigits: digits, maximumFractionDigits: digits})} - {suffix} - - ); -}; - -/** Tear-off calendar page. */ -export const Calendar: React.FC<{x: number; y: number; year: string; month?: string; s?: number; rot?: number; op?: number; red?: boolean}> = ({ - x, - y, - year, - month, - s = 1, - rot = 0, - op = 1, - red, -}) => ( -
-
{month ?? ' '}
-
{year}
-
-); - -/** Ink bar chart drawn on paper (for the few places where proportion matters). */ -export const InkBars: React.FC<{t: number; at: number; rows: {k: string; v: number; label: string; color: string}[]; max: number; w?: number; size?: number}> = ({ - t, - at, - rows, - max, - w = 560, - size = 30, -}) => ( -
- {rows.map((r, i) => { - const p = e(t, at + i * 8, 24); - return ( -
-
{r.k}
-
-
-
-
{r.label}
-
- ); - })} -
-); diff --git a/reference/zzm/src/scenes/index.ts b/reference/zzm/src/scenes/index.ts deleted file mode 100644 index 4544042..0000000 --- a/reference/zzm/src/scenes/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import {S01} from './s01'; -import {S02} from './s02'; -import {S03} from './s03'; -import {S04} from './s04'; -import {S05} from './s05'; -import {S06} from './s06'; -import {S07} from './s07'; -import {S08} from './s08'; -import {S09} from './s09'; -import {S10} from './s10'; -import {S11} from './s11'; -import {S12} from './s12'; -import {S13} from './s13'; -import {S14} from './s14'; -import {S15} from './s15'; - -export const SCENES: Record = { - s01: S01, - s02: S02, - s03: S03, - s04: S04, - s05: S05, - s06: S06, - s07: S07, - s08: S08, - s09: S09, - s10: S10, - s11: S11, - s12: S12, - s13: S13, - s14: S14, - s15: S15, -}; diff --git a/reference/zzm/src/scenes/s01.tsx b/reference/zzm/src/scenes/s01.tsx deleted file mode 100644 index 381f362..0000000 --- a/reference/zzm/src/scenes/s01.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import React from 'react'; -import {interpolate} from 'remotion'; -import {useScene} from '../lib'; -import {C, FONT} from '../theme'; -import {Abs, BigQuote, Cam, Graded, Line, Place, Shots, Svg, Tag, e, lin, rng} from '../film'; -import {Defs, Figure, Glow, Smoke} from '../art'; -import {BoardSet, Desert, Void, walk} from '../sets'; - -/** Planet-at-night with trade arcs converging on one glowing point. */ -export const WorldHeart: React.FC<{t: number; p?: number}> = ({t, p = 1}) => { - const r = rng(9); - const cx = 1120; - const cy = 520; - const pts = Array.from({length: 26}, () => [r() * 1920, 560 + r() * 380] as [number, number]); - return ( - - - - - - {Array.from({length: 220}, (_, i) => { - const x = r() * 1920; - const y = 640 + r() * 420; - return ; - })} - {pts.map(([x, y], i) => { - const k = lin(t, i * 3, i * 3 + 40) * p; - const mx = (x + cx) / 2; - const my = Math.min(y, cy) - 220 - (i % 5) * 30; - const N = 30; - const path: string[] = []; - for (let j = 0; j <= N * k; j++) { - const s = j / N; - path.push(`${(1 - s) * (1 - s) * x + 2 * (1 - s) * s * mx + s * s * cx},${(1 - s) * (1 - s) * y + 2 * (1 - s) * s * my + s * s * cy}`); - } - return path.length > 1 ? : null; - })} - - - {Array.from({length: 6}, (_, i) => ( - - - - - ))} - - ); -}; - -export const S01: React.FC = () => { - const {f, c, w, end, total, rel} = useScene(); - const shots = [ - { - at: 0, - el: (t: number, d: number) => ( - - - - - - - ), - }, - { - at: c('fab'), - el: (t: number, d: number) => { - const names: [string, string, number][] = [ - ['拜登', '美国总统', 760], - ['库克', '苹果', 930], - ['黄仁勋', '英伟达', 1100], - ['苏姿丰', 'AMD', 1270], - ]; - const at0 = c('fab'); - return ( - - - - - {names.map(([n, s, x], i) => ( - - ))} - - ); - }, - }, - { - at: c('walk'), - el: (t: number, d: number) => { - const wk = walk(t, 6, 80, 560, 900); - const atPod = t >= 80; - return ( - - - - - - - - - - {[480, 1440].map((x) => ( - - - - - ))} -
- {atPod ? ( - - - - - ) : null} -
-
- -
- ); - }, - }, - { - at: c('quote'), - tr: 'black' as const, - el: (t: number) => ( - <> - - - - ), - }, - { - at: c('irony'), - el: (t: number, d: number) => ( - <> - - - - - - - ), - }, - { - at: c('q'), - el: (t: number, d: number) => { - const big = e(t, rel('最大的两个', 'q'), 30); - return ( - <> - - - - - - ); - }, - }, - { - at: c('back'), - tr: 'black' as const, - el: (t: number) => { - const t0 = rel('那一年', 'back'); - const roll = interpolate(t, [10, t0 - 10], [2022, 1983], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: (x) => 1 - Math.pow(1 - x, 3)}); - const yr = Math.round(roll); - const spinning = t > 10 && t < t0 - 10; - return ( - <> - - {spinning - ? Array.from({length: 14}, (_, i) => ( -
- )) - : null} - -
- {yr} -
-
- - - ); - }, - }, - { - at: end + 6, - tr: 'black' as const, - el: (t: number) => ( - <> - - - - - - -
三张牌
-
-
张忠谋与台积电的四十年棋局
- - - ), - }, - ]; - return ; -}; diff --git a/reference/zzm/src/scenes/s05.tsx b/reference/zzm/src/scenes/s05.tsx deleted file mode 100644 index cef6b93..0000000 --- a/reference/zzm/src/scenes/s05.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React from 'react'; -import {useScene} from '../lib'; -import {C, FONT} from '../theme'; -import {Abs, Cam, Graded, Line, Shots, Svg, Tag, e, lin, rng} from '../film'; -import {Defs, Figure, Glow, Lamp, Smoke, Table} from '../art'; -import {Desk, Void, walk} from '../sets'; -import {Balance, Calendar, Count} from '../props'; - -/** Split screen: two offices on two sides of the Pacific. */ -const PhoneSplit: React.FC<{t: number}> = ({t}) => ( - - - - - - -
- - - - {Array.from({length: 8}, (_, i) => ( - - ))} -
- - -
-); - -/** small, crowded office with orders piling up */ -const Swamped: React.FC<{t: number}> = ({t}) => { - const r = rng(4); - return ( - - - - - {Array.from({length: 84}, (_, i) => { - const x = 140 + (i % 21) * 80 + r() * 20; - const y = 600 + Math.floor(i / 21) * 70; - return
; - })} - {Array.from({length: 40}, (_, i) => { - const k = r(); - const y = ((t * (2 + k * 3) + i * 90) % 900) - 50; - return ; - })} -
- ); -}; - -export const S05: React.FC = () => { - const {c, w, total, rel} = useScene(); - const shots = [ - { - at: 0, - el: (t: number, d: number) => ( - <> - - - - - - - - ), - }, - { - at: c('boom'), - el: (t: number, d: number) => ( - <> - - - - -
- -
-
全公司 · 根本忙不过来
-
- - ), - }, - { - at: c('planners'), - el: (t: number, d: number) => { - const a = walk(t, 10, 90, -100, 640); - const b = walk(t, 18, 98, -220, 520); - const labels = ['进货', '存货', '出货', '培训新员工']; - const day = Math.min(30, Math.max(1, Math.floor(lin(t, 60, 220) * 30))); - return ( - <> - - - - - - - - - 英伟达 - -
-
- - -
- -
- {labels.map((l, i) => ( - - ))} - - - ); - }, - }, - { - at: c('grow'), - el: (t: number, d: number) => { - const lanes = [0.55, 1.15, 0.8, 0.4, 0.95]; - return ( - <> - - - - - {lanes.map((sp, i) => { - const y = 260 + i * 120; - const x = 200 + Math.min(1500, t * sp * 3.2); - const lead = i === 1; - return ( - - -
- {lead ?
: null} -
- ); - })} -
-
- - - - ); - }, - }, - { - at: c('rd'), - el: (t: number, d: number) => { - const tilt = e(t, 30, 40); - return ( - <> - - - - - 台积电
+ 前十大客户} right={<>三星
+ 英特尔} /> - -
- - - - ); - }, - }, - { - at: c('hook'), - tr: 'black' as const, - el: (t: number, d: number) => ( - <> - - - - - -
-
-
- - - - - - - - ), - }, - { - at: w('那一年'), - tr: 'cut' as const, - el: (t: number) => { - const p = lin(t, 0, 50); - const pts = Array.from({length: 40}, (_, i) => { - const x = 200 + i * 40; - const y = 300 + Math.pow(i / 39, 1.8) * 520 + Math.sin(i * 1.7) * 18; - return `${x},${y}`; - }).slice(0, Math.max(2, Math.floor(40 * p))); - return ( - <> - - - - - - - - ); - }, - }, - ]; - return ( - - - - ); -}; - diff --git a/reference/zzm/src/sets.tsx b/reference/zzm/src/sets.tsx deleted file mode 100644 index 34d6333..0000000 --- a/reference/zzm/src/sets.tsx +++ /dev/null @@ -1,205 +0,0 @@ -/** Reusable sets (backgrounds with life in them). Each takes the shot's local time t. */ -import React from 'react'; -import {C, CARD, FONT} from './theme'; -import {Svg, e, mix, clamp01} from './film'; -import {Board, Crowd, Defs, Fab, Figure, Glow, Ground, Mainland, Piece, Podium, Ridge, Saguaro, Sky, Skyline, Stars, Taiwan} from './art'; - -/** Arizona desert at dusk, fab under construction. */ -export const Desert: React.FC<{t: number; stage?: boolean; crowd?: boolean; fabs?: number; night?: number}> = ({t, stage, crowd, fabs = 1, night = 0}) => { - const sunY = 640 + t * 0.05; - return ( - - - 0.5 ? '#0b1022' : '#1d1b3a'], - [0.45, night > 0.5 ? '#1b2340' : '#7a3f5a'], - [0.7, night > 0.5 ? '#3a3050' : '#e0784a'], - [0.85, night > 0.5 ? '#6a4a50' : '#f4b264'], - ]} - /> - - - - - - - {fabs >= 1 ? 0.3} t={t} /> : null} - {fabs >= 2 ? 0.3} t={t} /> : null} - {fabs >= 3 ? 0.3} t={t} /> : null} - - - - {stage ? ( - - - - - {[700, 1220].map((x) => ( - - - - - ))} - - ) : null} - {crowd ? : null} - - ); -}; - -/** Overhead dark-wood desk with a pool of lamp light: backdrop for paper props. */ -export const Desk: React.FC<{tone?: 'warm' | 'cold'; lx?: number; ly?: number}> = ({tone = 'warm', lx = 50, ly = 45}) => ( -
-); - -/** Black void with a soft top spotlight. */ -export const Void: React.FC<{tint?: string; x?: number; y?: number}> = ({tint = 'rgba(255,220,170,0.12)', x = 50, y = 35}) => ( -
-); - -/** Night city seen through a big window, warm interior wall. */ -export const Window: React.FC<{t: number; seed?: number; tone?: 'warm' | 'cold'; children?: React.ReactNode}> = ({t, seed = 21, tone = 'warm', children}) => ( - - - - - - - - - - - - - - - - - - - {children} - -); - -/** The board: chessboard in perspective under a lamp. Pieces optional by progress. */ -export const BoardSet: React.FC<{ - t: number; - tsmc?: number; - us?: number; - cn?: number; - tw?: number; - lit?: 'all' | 'tsmc' | 'us' | 'cn' | 'tw'; - children?: React.ReactNode; -}> = ({t, tsmc = 1, us = 1, cn = 1, tw = 1, lit = 'all', children}) => { - const dim = (k: string) => (lit === 'all' || lit === k ? 1 : 0.35); - return ( - - - - - - {tw > 0 ? ( - - - - - ) : null} - {us > 0 ? : null} - {cn > 0 ? : null} - {tsmc > 0 ? : null} - {children} - - ); -}; - -/** A gilded playing card (one of the three). */ -export const Card: React.FC<{i: number; x: number; y: number; s?: number; rot?: number; flip?: number; glow?: number; op?: number}> = ({ - i, - x, - y, - s = 1, - rot = 0, - flip = 1, - glow = 0, - op = 1, -}) => { - const c = CARD[i]; - const face = flip > 0.5; - const sx = Math.abs(Math.cos((1 - flip) * Math.PI)); - return ( -
- {face ? ( - <> -
{c.glyph}
-
{c.glyph}
-
第{['一', '二', '三'][i]}张牌
-
2 ? 58 : 82, fontWeight: 900, margin: '14px 0', letterSpacing: 6}}>{c.zh}
-
{c.sub}
- - ) : ( -
- )} -
- ); -}; - -/** The Taiwan Strait, stylised: mainland on the left, Taiwan island in the middle-right. */ -export const Strait: React.FC<{t: number; children?: React.ReactNode; tone?: 'warm' | 'cold'}> = ({t, children, tone = 'cold'}) => ( - - - - {Array.from({length: 18}, (_, i) => ( - - ))} - - - {children} - -); - -/** walking progress helper: x from a to b while frames in [t0, t1]; returns x and gait phase */ -export const walk = (t: number, t0: number, t1: number, a: number, b: number) => { - const p = clamp01((t - t0) / (t1 - t0)); - return {x: mix(a, b, p), phase: p > 0 && p < 1 ? t * 0.32 : 0}; -}; - -export {e, Figure}; diff --git a/reference/zzm/src/theme.ts b/reference/zzm/src/theme.ts deleted file mode 100644 index 0bc402f..0000000 --- a/reference/zzm/src/theme.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type React from 'react'; -export const C = { - black: '#050505', - night: '#0a0f1a', - ink: '#F2EDE4', - muted: '#B9B1A3', - dim: '#6d675e', - gold: '#E9B949', - goldDeep: '#a87a1e', - amber: '#F0A04B', - dusk: '#E8894A', - paper: '#EFE6D2', - paperDark: '#d9cdb2', - paperInk: '#2a241c', - stamp: '#B8322A', - green: '#4F9A5E', - us: '#6FA8E8', - cn: '#E0644F', - tw: '#7CC48D', - silhouette: '#0c0b0a', -}; - -/** the three forces on the board */ -export const SIDE = { - us: {zh: '美国', color: C.us}, - cn: {zh: '中国大陆', color: C.cn}, - tw: {zh: '台湾', color: C.tw}, - tsmc: {zh: '台积电', color: C.gold}, -} as const; - -/** the three cards Morris Chang plays */ -export const CARD = [ - {zh: '中立', sub: '永远不和客户竞争', color: '#E9B949', glyph: '◆'}, - {zh: '规则', sub: '尊重知识产权 · 按法律办事', color: '#7FD3CF', glyph: '♣'}, - {zh: '不可替代', sub: '技术领先 + 制造优越', color: '#C3AEF0', glyph: '♠'}, -] as const; - -/** colour grade per era: CSS filter on the picture + a tinted overlay */ -export type Grade = 'past' | 'warm' | 'cold' | 'dusk' | 'neutral'; -export const GRADE: Record = { - past: {filter: 'sepia(0.55) saturate(0.85) contrast(1.06) brightness(0.96)', tint: 'rgba(120,80,30,0.16)', blend: 'multiply'}, - warm: {filter: 'saturate(1.02) contrast(1.04)', tint: 'rgba(255,170,80,0.07)', blend: 'soft-light'}, - cold: {filter: 'saturate(0.85) contrast(1.08)', tint: 'rgba(40,80,140,0.16)', blend: 'soft-light'}, - dusk: {filter: 'saturate(1.08) contrast(1.05)', tint: 'rgba(255,120,60,0.06)', blend: 'soft-light'}, - neutral: {filter: 'none', tint: 'transparent', blend: 'normal'}, -}; - -export const FONT = { - sans: '"NotoSansSC", "Microsoft YaHei", sans-serif', - serif: '"NotoSerifSC", "Noto Serif SC", serif', - mono: '"Cascadia Code", Consolas, monospace', -}; diff --git a/reference/zzm/tools/music.py b/reference/zzm/tools/music.py deleted file mode 100644 index 56af2ff..0000000 --- a/reference/zzm/tools/music.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Synthesize the film's underscore → out/music.wav - -Story-film scoring, all synthesized (no samples): -- a low drone pad whose harmony follows the three acts (rise / squeeze / reckoning) -- a soft heartbeat pulse under the tense chapters -- a riser + low boom on every chapter-ending hook (the cut to black), and on the cold-open quote -- a single bell note on each chapter card -Scene boundaries come from src/timings.json + src/pace.json, so it stays in sync after re-TTS. -""" -import json -import wave -from pathlib import Path - -import numpy as np - -ROOT = Path(__file__).resolve().parent.parent -SR = 44100 -FPS = 30 -T = json.loads((ROOT / "src" / "timings.json").read_text(encoding="utf-8")) -P = json.loads((ROOT / "src" / "pace.json").read_text(encoding="utf-8")) -lead = lambda i: P["leadOverride"].get(i, P["lead"]) -tail = lambda i: P["tailOverride"].get(i, P["tail"]) - -starts, narr_end, acc = {}, {}, 0.0 -for sid, v in T.items(): - starts[sid] = acc - narr_end[sid] = acc + (lead(sid) + int(np.ceil(v["duration"] * FPS))) / FPS - acc += (lead(sid) + int(np.ceil(v["duration"] * FPS)) + tail(sid)) / FPS -TOTAL = acc -N = int(TOTAL * SR) + SR -t = np.arange(N) / SR -out = np.zeros(N) -rng = np.random.default_rng(11) - - -def hz(m): - return 440.0 * 2 ** ((m - 69) / 12) - - -def add(i0, sig): - i0 = max(0, i0) - i1 = min(N, i0 + len(sig)) - if i1 > i0: - out[i0:i1] += sig[: i1 - i0] - - -# ── drone pad: (start scene, chord list, brightness) ── -ACTS = [ - ("s01", [[38, 45, 50, 53], [34, 41, 50, 53], [36, 43, 48, 55], [33, 40, 49, 52]], 0.5), # Dm Bb C A - ("s08", [[37, 44, 49, 50], [37, 44, 48, 52], [35, 42, 47, 50], [36, 43, 46, 51]], 0.35), # tense, semitone rubs - ("s14", [[38, 45, 50, 54], [43, 50, 55, 59], [41, 48, 53, 57], [45, 52, 57, 61]], 0.7), # D G F A (lift) -] -act_t = [(starts[s], ch, br) for s, ch, br in ACTS] - - -def act_at(sec): - cur = act_t[0] - for a in act_t: - if sec >= a[0]: - cur = a - return cur - - -BAR = 9.0 -for b in range(int(np.ceil(TOTAL / BAR)) + 1): - t0 = b * BAR - _, chords, bright = act_at(t0) - chord = chords[b % len(chords)] - i0 = int(max(0, t0 - 3) * SR) - i1 = min(N, int((t0 + BAR + 3) * SR)) - if i0 >= N: - break - tt = t[i0:i1] - t0 - env = (np.clip((tt + 3) / 3.5, 0, 1) * np.clip((BAR + 3 - tt) / 3.5, 0, 1)) ** 1.6 - seg = np.zeros(i1 - i0) - for k, m in enumerate(chord): - f = hz(m) - for det in (-0.003, 0.0, 0.0035): - ph = rng.random() * 2 * np.pi - seg += np.sin(2 * np.pi * f * (1 + det) * tt + ph) * (0.7 if k == 0 else 0.32) - seg += bright * 0.06 * np.sin(2 * np.pi * 2 * f * (1 + det) * tt + ph) - seg *= 1 + 0.1 * np.sin(2 * np.pi * 0.09 * tt + b) - out[i0:i1] += 0.55 * seg * env - -# ── heartbeat pulse under tense chapters ── -TENSE = ["s01", "s09", "s11", "s13"] -ids = list(T.keys()) -for sid in TENSE: - a = starts[sid] + lead(sid) / FPS - z = narr_end[sid] - beat = 0.92 - k = a - while k < z - 1.0: - for off, amp in ((0.0, 1.0), (0.24, 0.6)): - n = int(0.35 * SR) - tt = np.arange(n) / SR - f = 55 * np.exp(-tt * 9) + 38 - sig = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-tt * 11) * amp - add(int((k + off) * SR), 0.9 * sig) - k += beat - -# ── riser + boom at every hook (end of narration) ── -def riser(at, dur=2.4, amp=0.5): - n = int(dur * SR) - tt = np.arange(n) / SR - noise = rng.normal(0, 1, n) - # crude band emphasis via differencing, rising envelope - noise = np.convolve(noise, np.ones(30) / 30, mode="same") - env = (tt / dur) ** 2.2 - tone = np.sin(2 * np.pi * np.cumsum(220 + 440 * (tt / dur) ** 2) / SR) * 0.25 - add(int((at - dur) * SR), amp * env * (noise * 1.6 + tone)) - - -def boom(at, amp=1.0): - n = int(3.2 * SR) - tt = np.arange(n) / SR - f = 70 * np.exp(-tt * 2.2) + 28 - body = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-tt * 1.3) - click = rng.normal(0, 1, n) * np.exp(-tt * 40) * 0.5 - add(int(at * SR), amp * (body * 1.4 + click)) - - -for sid in ids: - hit = narr_end[sid] + 0.12 - if sid == "s15": - continue - riser(hit, amp=0.35) - boom(hit, amp=0.9) - -# cold-open quote: boom on the cut to black -q = T["s01"]["cues"]["quote"] -boom(starts["s01"] + lead("s01") / FPS + q, amp=0.8) -# title card after the cold open -boom(narr_end["s01"] + 0.3, amp=0.6) - -# ── bell on each chapter card ── -for i, sid in enumerate(ids): - if lead(sid) < 30: - continue - at = starts[sid] + 0.15 - n = int(5 * SR) - tt = np.arange(n) / SR - m = [74, 69, 72, 67][i % 4] - sig = (np.sin(2 * np.pi * hz(m) * tt) + 0.3 * np.sin(2 * np.pi * hz(m) * 2.76 * tt) * np.exp(-tt * 3)) * np.exp(-tt * 1.1) - add(int(at * SR), 0.28 * sig) - -# fades + normalize -y = out / (np.max(np.abs(out)) + 1e-9) -fade = int(3 * SR) -y[:fade] *= np.linspace(0, 1, fade) -y[-fade:] *= np.linspace(1, 0, fade) -y = (y * 0.9 * 32767).astype(np.int16) -path = ROOT / "out" / "music.wav" -with wave.open(str(path), "wb") as wf: - wf.setnchannels(1) - wf.setsampwidth(2) - wf.setframerate(SR) - wf.writeframes(y.tobytes()) -print(f"{path} {TOTAL:.1f}s") diff --git a/reference/zzm/tools/pick.mjs b/reference/zzm/tools/pick.mjs deleted file mode 100644 index 97f893b..0000000 --- a/reference/zzm/tools/pick.mjs +++ /dev/null @@ -1,22 +0,0 @@ -// Render specific frames: node tools/pick.mjs s04:said:30 s06:capex:120 ... (scene:cue:offsetFrames) -import {bundle} from '@remotion/bundler'; -import {renderStill, selectComposition} from '@remotion/renderer'; -import fs from 'node:fs'; -import path from 'node:path'; -const root = path.resolve(import.meta.dirname, '..'); -const T = JSON.parse(fs.readFileSync(path.join(root, 'src/timings.json'), 'utf8')); -const pace = JSON.parse(fs.readFileSync(path.join(root, 'src/pace.json'), 'utf8')); -const LEADOF = (id) => pace.leadOverride[id] ?? pace.lead, TAILOF = (id) => pace.tailOverride[id] ?? pace.tail; -const FPS = 30; -let acc = 0; const starts = {}; -for (const id of Object.keys(T)) { starts[id] = acc; acc += LEADOF(id) + Math.ceil(T[id].duration * FPS) + TAILOF(id); } -const outDir = path.join(root, 'out/pick'); fs.mkdirSync(outDir, {recursive: true}); -const serveUrl = await bundle({entryPoint: path.join(root, 'src/index.ts')}); -const composition = await selectComposition({serveUrl, id: 'ZhangZhongmou', inputProps: {lang: 'zh'}}); -for (const a of process.argv.slice(2)) { - const [id, cue, off] = a.split(':'); - const fr = starts[id] + (cue === 'end' ? LEADOF(id) + Math.ceil(T[id].duration * FPS) : LEADOF(id) + Math.round(T[id].cues[cue] * FPS)) + Number(off ?? 0); - const output = path.join(outDir, `${id}_${cue}_${off}.png`); - await renderStill({composition, serveUrl, output, frame: fr, inputProps: {lang: 'zh'}, scale: 0.5}); - console.log(output); -} diff --git a/reference/zzm/tools/sheet.py b/reference/zzm/tools/sheet.py deleted file mode 100644 index f67d358..0000000 --- a/reference/zzm/tools/sheet.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Tile review stills into 2x2 sheets: python tools/sheet.py s01 → out/sheets/s01_*.png""" -import sys -from pathlib import Path -from PIL import Image -root = Path(__file__).resolve().parent.parent -out = root / "out" / "sheets" -out.mkdir(parents=True, exist_ok=True) -for sid in sys.argv[1:]: - files = sorted((root / "out" / "stills").glob(f"{sid}_*.png"), key=lambda p: p.stat().st_mtime) - for k in range(0, len(files), 4): - sheet = Image.new("RGB", (1920, 1080), (40, 40, 40)) - for j, fp in enumerate(files[k:k + 4]): - im = Image.open(fp).convert("RGB").resize((958, 538)) - sheet.paste(im, ((j % 2) * 962, (j // 2) * 542)) - p = out / f"{sid}_{k // 4}.png" - sheet.save(p) - print(p, [f.stem for f in files[k:k + 4]]) diff --git a/reference/zzm/tools/stills.mjs b/reference/zzm/tools/stills.mjs deleted file mode 100644 index 678796a..0000000 --- a/reference/zzm/tools/stills.mjs +++ /dev/null @@ -1,36 +0,0 @@ -// Batch review stills: bundle once, render a frame ~2.5s after each cue (or given scenes only). -// usage: node tools/stills.mjs [s01 s02 ...] → out/stills/_.png -import {bundle} from '@remotion/bundler'; -import {renderStill, selectComposition} from '@remotion/renderer'; -import fs from 'node:fs'; -import path from 'node:path'; - -const root = path.resolve(import.meta.dirname, '..'); -const timings = JSON.parse(fs.readFileSync(path.join(root, 'src/timings.json'), 'utf8')); -const pace = JSON.parse(fs.readFileSync(path.join(root, 'src/pace.json'), 'utf8')); -const LEADOF = (id) => pace.leadOverride[id] ?? pace.lead, TAILOF = (id) => pace.tailOverride[id] ?? pace.tail; -const FPS = 30, OFF = Number(process.env.OFF ?? 75); -const only = process.argv.slice(2); -const ids = Object.keys(timings); -let acc = 0; -const starts = {}; -for (const id of ids) { - starts[id] = acc; - acc += LEADOF(id) + Math.ceil(timings[id].duration * FPS) + TAILOF(id); -} -const outDir = path.join(root, 'out/stills'); -fs.mkdirSync(outDir, {recursive: true}); -const serveUrl = await bundle({entryPoint: path.join(root, 'src/index.ts')}); -const composition = await selectComposition({serveUrl, id: 'ZhangZhongmou', inputProps: {lang: 'zh'}}); -for (const id of ids) { - if (only.length && !only.includes(id)) continue; - const len = LEADOF(id) + Math.ceil(timings[id].duration * FPS) + TAILOF(id); - const cues = Object.entries(timings[id].cues); - const frames = cues.map(([k, t]) => [k, Math.min(len - 15, LEADOF(id) + Math.round(t * FPS) + OFF)]); - frames.push(['end', len - 20]); - for (const [k, fr] of frames) { - const output = path.join(outDir, `${id}_${k}.png`); - await renderStill({composition, serveUrl, output, frame: starts[id] + fr, inputProps: {lang: 'zh'}, scale: 0.5}); - console.log(output); - } -} diff --git a/reference/zzm/tts/gen.py b/reference/zzm/tts/gen.py deleted file mode 100644 index ee7ad79..0000000 --- a/reference/zzm/tts/gen.py +++ /dev/null @@ -1,237 +0,0 @@ -"""Generate narration with edge-tts and a timing map for Remotion. - -For each scene: strip [[cue]] markers, synthesize with WordBoundary events, -then map cues and subtitle lines onto audio timestamps. -Output: public/audio/.mp3 and src/timings.json -""" -import asyncio -import json -import re -import subprocess -import sys -from pathlib import Path - -import edge_tts - -ROOT = Path(__file__).resolve().parent.parent -ARGS = sys.argv[1:] -LANG = "en" if "--lang" in ARGS and ARGS[ARGS.index("--lang") + 1] == "en" else "zh" -ARGS = [a for a in ARGS if a not in ("--lang", "en", "zh")] -EN = LANG == "en" -SCRIPT = json.loads((ROOT / "tts" / ("script.en.json" if EN else "script.json")).read_text(encoding="utf-8")) -AUDIO_DIR = ROOT / "public" / "audio" / ("en" if EN else "") -AUDIO_DIR.mkdir(parents=True, exist_ok=True) -TIMINGS = ROOT / "src" / ("timings.en.json" if EN else "timings.json") - -CUE_RE = re.compile(r"\[\[(\w+)\]\]") -BREAK_PUNCT = ",.;:?!" if EN else ",。!?;:" -SENT_END = ".?!" if EN else "。!?" -MAX_LINE = 62 if EN else 20 - - -TOKEN_RE = re.compile(r"\[\[(\w+)\]\]|\|\||<<(\w+)\||>>") - - -def parse(text): - """Strip markers. [[cue]] = animation trigger, || = dramatic pause, - <> = spoken by voice SCRIPT["voices"][key] (a quote). - Returns clean text, cue char offsets, and voice segments.""" - clean, cues, segs = "", {}, [] - cur = {"s": 0, "v": None, "pause": 0.0} - pos = 0 - - def close(newv): - nonlocal cur - cur["e"] = len(clean) - segs.append(cur) - cur = {"s": len(clean), "v": newv, "pause": 0.0} - - for m in TOKEN_RE.finditer(text): - clean += text[pos:m.start()] - pos = m.end() - tok = m.group(0) - if m.group(1): - cues[m.group(1)] = len(clean) - elif tok == "||": - cur["pause"] += SCRIPT.get("pause", 0.7) - close(cur["v"]) - elif tok == ">>": - close(None) - else: - close(m.group(2)) - clean += text[pos:] - close(None) - # merge empty segments' pauses into the previous one - out = [] - for sg in segs: - if not clean[sg["s"]:sg["e"]].strip(): - if out: - out[-1]["pause"] += sg["pause"] - continue - out.append(sg) - return clean, cues, out - - -def split_lines(clean): - """Greedy: split into clauses on punctuation, merge clauses up to MAX_LINE.""" - clauses, cur, start = [], "", 0 - for i, ch in enumerate(clean): - cur += ch - # English: only break on punctuation followed by a space (keeps 7.2 / 61,600 intact) - if ch in BREAK_PUNCT and (not EN or i + 1 >= len(clean) or clean[i + 1] == " "): - clauses.append((start, i + 1)) - start, cur = i + 1, "" - if clean[start:].strip(): - clauses.append((start, len(clean))) - lines = [] - for s, e in clauses: - if lines and (e - lines[-1][0]) <= MAX_LINE and clean[lines[-1][1] - 1] not in SENT_END: - lines[-1] = (lines[-1][0], e) - else: - lines.append((s, e)) - if EN: # split over-long English lines at the space nearest the middle - out = [] - for s, e in lines: - while e - s > MAX_LINE: - mid = s + (e - s) // 2 - sp = min((i for i in range(s + 1, e - 1) if clean[i] == " "), key=lambda i: abs(i - mid), default=None) - if sp is None: - break - out.append((s, sp)) - s = sp + 1 - out.append((s, e)) - lines = out - # trim leading spaces so subtitle text starts clean - trimmed = [] - for s, e in lines: - while s < e and clean[s] == " ": - s += 1 - trimmed.append((s, e)) - return trimmed - - -SR = 24000 - - -def voice_of(key): - if key is None: - return SCRIPT["voice"], SCRIPT["rate"] - v = SCRIPT["voices"][key] - return v["voice"], v["rate"] - - -def decode(mp3_bytes): - raw = subprocess.run( - ["ffmpeg", "-loglevel", "error", "-i", "pipe:0", "-f", "s16le", "-ac", "1", "-ar", str(SR), "pipe:1"], - input=bytes(mp3_bytes), capture_output=True, check=True, - ).stdout - return raw - - -async def synth(text, key): - voice, rate = voice_of(key) - for attempt in range(10): - try: - com = edge_tts.Communicate(text, voice, rate=rate, boundary="WordBoundary") - audio, words = bytearray(), [] - async for chunk in com.stream(): - if chunk["type"] == "audio": - audio.extend(chunk["data"]) - elif chunk["type"] == "WordBoundary": - words.append({"t": chunk["offset"] / 1e7, "d": chunk["duration"] / 1e7, "w": chunk["text"]}) - if not audio: - raise RuntimeError("empty audio") - return decode(audio), words - except Exception as e: # network flakiness - print(f" retry {attempt + 1}: {e}", file=sys.stderr) - await asyncio.sleep(3 + attempt * 3) - raise RuntimeError(f"TTS failed: {text[:20]}") - - -def duration(path): - out = subprocess.check_output( - ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", str(path)] - ) - return float(out.strip()) - - -def align(clean, words): - """Attach a char offset to each word by scanning forward through clean text.""" - ptr, aligned = 0, [] - for w in words: - idx = clean.find(w["w"], ptr) - if idx == -1 or idx - ptr > 12: - continue - aligned.append({**w, "s": idx, "e": idx + len(w["w"])}) - ptr = idx + len(w["w"]) - return aligned - - -def time_at(aligned, char_idx, fallback): - for w in aligned: - if w["e"] > char_idx: - return w["t"] - return fallback - - -async def main(): - only = set(ARGS) - tpath = TIMINGS - old = json.loads(tpath.read_text(encoding="utf-8")) if tpath.exists() else {} - result = {} - for sc in SCRIPT["scenes"]: - sid = sc["id"] - mp3 = AUDIO_DIR / f"{sid}.mp3" - if only and sid not in only and sid in old: - result[sid] = old[sid] - continue - clean, cues, segs = parse(sc["text"]) - print(f"{sid}: {len(clean)} chars, {len(segs)} segments", file=sys.stderr) - pcm, aligned, nwords, offset, lines = bytearray(), [], 0, 0.0, [] - for sg in segs: - raw, words = await synth(clean[sg["s"]:sg["e"]], sg["v"]) - nwords += len(words) - seg_aligned = [{**w, "t": w["t"] + offset, "s": w["s"] + sg["s"], "e": w["e"] + sg["s"]} - for w in align(clean[sg["s"]:sg["e"]], words)] - aligned += seg_aligned - seg_dur = len(raw) / 2 / SR - sub = clean[sg["s"]:sg["e"]] - for s, e in split_lines(sub): - s, e = s + sg["s"], e + sg["s"] - lw = [{"t": round(w["t"], 3), "s": w["s"] - s, "e": w["e"] - s} for w in seg_aligned if s <= w["s"] < e] - st = lw[0]["t"] if lw else offset - ln = {"text": clean[s:e], "start": round(st, 3), "words": lw, "segEnd": round(offset + seg_dur, 3)} - if sg["v"]: - ln["q"] = sg["v"] - lines.append(ln) - pcm += raw - offset += seg_dur - if sg["pause"]: - pcm += bytes(int(sg["pause"] * SR) * 2) - offset += sg["pause"] - subprocess.run( - ["ffmpeg", "-loglevel", "error", "-y", "-f", "s16le", "-ar", str(SR), "-ac", "1", "-i", "pipe:0", - "-c:a", "libmp3lame", "-b:a", "96k", str(mp3)], - input=bytes(pcm), check=True, - ) - dur = duration(mp3) - for i, ln in enumerate(lines): - nxt = lines[i + 1]["start"] if i + 1 < len(lines) else round(dur, 3) - # don't hold a subtitle through a dramatic pause - ln["end"] = round(min(nxt, ln.pop("segEnd") + 0.25), 3) - result[sid] = { - "chapter": sc["chapter"], - "duration": round(dur, 3), - "cues": {k: round(time_at(aligned, v, dur), 3) for k, v in cues.items()}, - "lines": lines, - "aligned": len(aligned), - "words": nwords, - } - tpath.write_text(json.dumps(result, ensure_ascii=False, indent=1), encoding="utf-8") - total = sum(v["duration"] for v in result.values()) - for sid, v in result.items(): - print(f"{sid} {v['duration']:6.1f}s aligned {v['aligned']}/{v['words']} cues {len(v['cues'])}") - print(f"TOTAL {total:.1f}s ({total/60:.1f} min)") - - -asyncio.run(main()) diff --git a/reference/zzm/tts/script.json b/reference/zzm/tts/script.json deleted file mode 100644 index d95d5e6..0000000 --- a/reference/zzm/tts/script.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "voice": "zh-CN-YunjianNeural", - "rate": "+10%", - "voices": { - "zc": { - "voice": "zh-TW-YunJheNeural", - "rate": "-6%" - }, - "q": { - "voice": "zh-CN-YunxiNeural", - "rate": "+0%" - } - }, - "pause": 0.75, - "scenes": [ - { - "id": "s01", - "chapter": "序章 · 沙漠里的一句话", - "text": "[[desert]]2022年12月6日,美国亚利桑那州,凤凰城北边的沙漠。[[fab]]一座还没完工的晶圆厂前面,搭起了一个台子。台下坐着美国总统拜登,苹果的库克、英伟达的黄仁勋、AMD的苏姿丰,都来了。[[walk]]一位91岁的老人,慢慢走上台。他叫张忠谋,台积电的创始人。[[expect]]按惯例,这种场合,说的都是喜庆话。||[[quote]]他说的却是:<>||[[irony]]奇怪的是,说这句话的人,恰恰是全球化最大的赢家之一。他用一家只替别人造芯片的公司,把台湾放进了全世界每一部手机、每一台AI服务器的心脏。[[q]]一个靠全球化起家的人,为什么要亲口宣布它的死亡?他亲手造出来的这家公司,又凭什么夹在世界上最大的两个对手之间活下来,还越活越大?||[[back]]要回答这个问题,得先回到39年前。||那一年,52岁的张忠谋,丢了工作。" - } - ] -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..456c50f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +edge-tts>=7.0 # real narration (tts/gen.py without --mock) +numpy>=1.26 # tools/music.py +Pillow>=10.0 # tools/sheet.py +pytest>=8.0 # tests/ diff --git a/src/Root.tsx b/src/Root.tsx new file mode 100644 index 0000000..da15653 --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import {Composition} from 'remotion'; +import {CFG} from './config'; +import {totalFrames} from './core/timeline'; +import {Video} from './core/Video'; + +export const Root: React.FC = () => ( + +); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..bf35979 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,24 @@ +/** Typed view of kit.config.json — the single source for id, size, fps, pace and fonts. */ +import raw from '../kit.config.json'; + +type FontEntry = {family?: string; file?: string; weight?: string; url?: string; fallback: string[]}; + +export type KitConfig = { + id: string; + width: number; + height: number; + fps: number; + voice: string; + rate: string; + voices: Record; + pause: number; + maxLine: number; + mockCharsPerSec: number; + pace: {lead: number; tail: number; leadOverride: Record; tailOverride: Record}; + fonts: {sans: FontEntry; serif: FontEntry; mono: FontEntry}; +}; + +export const CFG = raw as KitConfig; +export const FPS = CFG.fps; +export const WIDTH = CFG.width; +export const HEIGHT = CFG.height; diff --git a/src/core/Tex.tsx b/src/core/Tex.tsx new file mode 100644 index 0000000..ddca914 --- /dev/null +++ b/src/core/Tex.tsx @@ -0,0 +1,19 @@ +/** KaTeX formula. Fonts are awaited by the Video shell (delayRender) before frames are captured. */ +import React, {useMemo} from 'react'; +import katex from 'katex'; +import 'katex/dist/katex.min.css'; +import {C} from './theme'; + +export const Tex: React.FC<{tex: string; size?: number; color?: string; style?: React.CSSProperties; display?: boolean}> = ({ + tex, + size = 48, + color = C.ink, + style, + display = false, +}) => { + const html = useMemo( + () => katex.renderToString(tex, {displayMode: display, throwOnError: false, strict: false, output: 'html'}), + [tex, display], + ); + return ; +}; diff --git a/src/core/Video.tsx b/src/core/Video.tsx new file mode 100644 index 0000000..3a76117 --- /dev/null +++ b/src/core/Video.tsx @@ -0,0 +1,156 @@ +/** The shell: sequences scenes, plays each scene's narration, draws chapter cards and subtitles, loads fonts. */ +import React, {useEffect, useState} from 'react'; +import {AbsoluteFill, Audio, continueRender, delayRender, interpolate, Sequence, staticFile, useCurrentFrame} from 'remotion'; +import {CFG, FPS} from '../config'; +import {SCENES} from '../scenes'; +import type {ChapterProps, Look, SubtitleStyle} from './look'; +import {C, FONT} from './theme'; +import {SCENE_IDS, SceneProvider, TIMINGS, audioOf, leadOf, sceneFrames, sceneStarts} from './timeline'; + +const STARTS = sceneStarts(); + +const faceCss = (f: {family?: string; file?: string; weight?: string}) => + f.family && f.file + ? `@font-face { font-family: "${f.family}"; src: url("${staticFile(`fonts/${f.file}`)}") format("truetype"); font-weight: ${f.weight ?? '100 900'}; }` + : ''; + +const fontCss = ` +${faceCss(CFG.fonts.sans)} +${faceCss(CFG.fonts.serif)} +.katex { font-size: 1em !important; } +`; + +/** Hold rendering until the fonts are in (or have failed — then the system fallbacks in FONT are used). */ +const useFonts = () => { + const [handle] = useState(() => delayRender('fonts')); + useEffect(() => { + const fams = [CFG.fonts.sans.family, CFG.fonts.serif.family].filter(Boolean); + Promise.allSettled([ + ...fams.flatMap((fam) => [document.fonts.load(`400 40px "${fam}"`, '字体'), document.fonts.load(`800 40px "${fam}"`, '字体')]), + document.fonts.load('40px KaTeX_Main', 'x'), + document.fonts.load('italic 40px KaTeX_Math', 'x'), + document.fonts.load('40px KaTeX_Size1', '('), + document.fonts.load('40px KaTeX_Size2', '('), + ]).then(() => continueRender(handle)); + }, [handle]); +}; + +const DefaultChapter: React.FC = ({f, lead, kicker, title}) => { + const op = interpolate(f, [0, 8, lead - 10, lead], [0, 1, 1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); + return ( + +
{kicker}
+
{title}
+
+ ); +}; + +const DEFAULT_SUBS: SubtitleStyle = {bottom: 58, box: true}; + +const Subtitles: React.FC<{id: string; s: SubtitleStyle}> = ({id, s}) => { + const f = useCurrentFrame(); + const t = (f - leadOf(id)) / FPS; + const line = TIMINGS[id].lines.find((l) => t >= l.start && t < l.end); + if (!line) return null; + const text = line.text.replace(/[,。;:、,.;:]$/, '').trimEnd(); + const q = Boolean(line.q); + const op = + interpolate(t - line.start, [0, 0.1], [0, 1], {extrapolateRight: 'clamp'}) * + interpolate(line.end - t, [0, 0.08], [0, 1], {extrapolateRight: 'clamp'}); + const color = q ? (s.quoteColor ?? C.gold) : (s.color ?? 'rgba(242,237,228,0.94)'); + let body: React.ReactNode = text; + if (s.karaoke) { + // time at which each character is reached; characters without a word inherit the previous word's time + const at: number[] = []; + let last = line.start; + for (let i = 0; i < text.length; i++) { + const wd = line.words.find((x) => i >= x.s && i < x.e); + at[i] = wd ? (last = wd.t) : last; + } + body = text.split('').map((ch, i) => ( + = at[i] ? 1 : 0.45}}> + {ch} + + )); + } + return ( +
+
+ {q ? '「' : null} + {body} + {q ? '」' : null} +
+
+ ); +}; + +const splitChapter = (s: string) => { + const [k, ...rest] = s.split(' · '); + return rest.length ? {kicker: k, title: rest.join(' · ')} : {kicker: '', title: k}; +}; + +const SceneShell: React.FC<{id: string}> = ({id}) => { + const f = useCurrentFrame(); + const n = sceneFrames(id); + const lead = leadOf(id); + const def = SCENES[id]; + const look: Look = def?.look ?? {}; + const Comp = def?.component; + const Bg = look.background; + const Over = look.overlay; + const Frame = look.frame; + const Chapter = look.chapter === undefined ? DefaultChapter : look.chapter; + const op = interpolate(f, [n - 18, n], [1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); + const ch = splitChapter(TIMINGS[id].chapter); + return ( + + + {Bg ? : null} + {Comp ? : null} + {Over ? : null} + + {Chapter && lead > 30 ? : null} + + + {Frame ? : null} + + + ); +}; + +export const Video: React.FC = () => { + useFonts(); + return ( + + + {SCENE_IDS.map((id) => ( + + + + ))} + + ); +}; diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..5f045a6 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,6 @@ +export * from './timeline'; +export * from './layout'; +export * from './theme'; +export * from './look'; +export {Tex} from './Tex'; +export {CFG, FPS, WIDTH, HEIGHT} from '../config'; diff --git a/src/core/layout.tsx b/src/core/layout.tsx new file mode 100644 index 0000000..1f462ff --- /dev/null +++ b/src/core/layout.tsx @@ -0,0 +1,74 @@ +/** Layout helpers shared by every style pack. */ +import React from 'react'; +import {HEIGHT, WIDTH} from '../config'; +import {C} from './theme'; + +/** + * Absolutely positioned box. With `center`, the box is centred on (x, y); + * the centring translate is composed with the caller's own `style.transform`, never replaced by it. + */ +export const At: React.FC<{x: number; y: number; w?: number; h?: number; center?: boolean; style?: React.CSSProperties; children?: React.ReactNode}> = ({ + x, + y, + w, + h, + center, + style, + children, +}) => ( +
+ {children} +
+); + +/** Full-frame layer. */ +export const Full: React.FC<{children?: React.ReactNode; style?: React.CSSProperties}> = ({children, style}) => ( +
{children}
+); + +/** Full-frame SVG canvas in composition pixels. */ +export const Svg: React.FC<{children?: React.ReactNode; style?: React.CSSProperties}> = ({children, style}) => ( + + {children} + +); + +/** Line that draws itself (use inside ); p in [0,1]. */ +export const DrawLine: React.FC<{x1: number; y1: number; x2: number; y2: number; p: number; color?: string; w?: number; dash?: boolean; arrow?: boolean}> = ({ + x1, + y1, + x2, + y2, + p, + color = C.teal, + w = 4, + dash, + arrow, +}) => { + const len = Math.hypot(x2 - x1, y2 - y1); + const ex = x1 + (x2 - x1) * p; + const ey = y1 + (y2 - y1) * p; + const ang = Math.atan2(y2 - y1, x2 - x1); + const ah = 16; + return ( + 0 ? 1 : 0}> + + {arrow && p > 0.05 && len > 0 ? ( + + ) : null} + + ); +}; diff --git a/src/core/look.ts b/src/core/look.ts new file mode 100644 index 0000000..fb01509 --- /dev/null +++ b/src/core/look.ts @@ -0,0 +1,44 @@ +/** A "look" is how the shell dresses a scene. Style packs export one; core never imports a pack. */ +import type React from 'react'; + +export type ChapterProps = { + /** scene-local frame */ + f: number; + /** lead-in frames before narration (the card fades out at the end of it) */ + lead: number; + /** text before " · " in the scene's chapter string */ + kicker: string; + /** text after " · " */ + title: string; +}; + +export type SubtitleStyle = { + /** px from the bottom edge */ + bottom: number; + /** if set, subtitles are vertically centred in a band of this height (e.g. a letterbox bar) */ + height?: number; + /** translucent pill behind the line */ + box?: boolean; + /** dim characters until the narrator reaches them */ + karaoke?: boolean; + size?: number; + color?: string; + /** lines read by an alternate voice (<>) are set in serif, this colour, with 「」 */ + quoteColor?: string; +}; + +export type Look = { + /** under the scene */ + background?: React.FC; + /** over the scene picture, fades out with it (grain, vignette) */ + overlay?: React.FC; + /** always on top of picture and chapter card, under subtitles (letterbox bars) */ + frame?: React.FC; + /** chapter card shown during the lead-in when lead > 30 frames; null = never */ + chapter?: React.FC | null; + subtitles?: SubtitleStyle; + /** canvas colour behind everything */ + base?: string; +}; + +export type SceneDef = {component: React.FC; look?: Look}; diff --git a/src/core/theme.ts b/src/core/theme.ts new file mode 100644 index 0000000..2fadec1 --- /dev/null +++ b/src/core/theme.ts @@ -0,0 +1,41 @@ +/** Shared colour tokens and font stacks. Every token used by core, film and slides lives here. */ +import {CFG} from '../config'; + +export const C = { + // neutrals + black: '#050505', + ink: '#F2EDE4', + muted: '#B9B1A3', + dim: '#6d675e', + // accents + gold: '#E9B949', + orange: '#E8913F', + teal: '#2BB3A6', + tealDeep: '#0f7b78', + red: '#E5645A', + green: '#7BCB6B', + blue: '#6FA8E8', + // slides surfaces + bg: '#08201f', + bg2: '#0c2d2c', + panel: 'rgba(237,232,220,0.045)', + panelLine: 'rgba(237,232,220,0.14)', + // paper props (film) + paper: '#EFE6D2', + paperInk: '#2a241c', + stamp: '#B8322A', +}; + +const GENERIC = new Set(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui']); +const stack = (f: {family?: string; fallback: string[]}) => + [f.family, ...f.fallback] + .filter((x): x is string => Boolean(x)) + .map((x) => (GENERIC.has(x) ? x : `"${x}"`)) + .join(', '); + +/** Font stacks: the downloaded variable font first (if `npm run fonts` was run), then system fonts. */ +export const FONT = { + sans: stack(CFG.fonts.sans), + serif: stack(CFG.fonts.serif), + mono: stack(CFG.fonts.mono), +}; diff --git a/src/core/timeline.ts b/src/core/timeline.ts new file mode 100644 index 0000000..e7a2633 --- /dev/null +++ b/src/core/timeline.ts @@ -0,0 +1,101 @@ +/** Timeline: narration timings → frames. Scenes never hard-code seconds; they ask c()/w()/rel(). */ +import type React from 'react'; +import {createContext, useContext} from 'react'; +import {Easing, interpolate, spring, useCurrentFrame} from 'remotion'; +import timingsJson from '../timings.json'; +import {CFG, FPS} from '../config'; + +export type Word = {t: number; s: number; e: number}; +export type SubLine = {text: string; start: number; end: number; words: Word[]; q?: string}; +export type SceneTiming = { + chapter: string; + duration: number; + /** path under public/, written by tts/gen.py (mp3 for real TTS, wav for --mock) */ + audio?: string; + cues: Record; + lines: SubLine[]; +}; + +export const TIMINGS = timingsJson as unknown as Record; +export const SCENE_IDS = Object.keys(TIMINGS); + +/** frames before narration starts (chapter card) and after it ends, per scene — see kit.config.json "pace" */ +export const leadOf = (id: string) => CFG.pace.leadOverride[id] ?? CFG.pace.lead; +export const tailOf = (id: string) => CFG.pace.tailOverride[id] ?? CFG.pace.tail; +export const narrFrames = (id: string) => Math.ceil(TIMINGS[id].duration * FPS); +export const sceneFrames = (id: string) => leadOf(id) + narrFrames(id) + tailOf(id); +export const narrEnd = (id: string) => leadOf(id) + narrFrames(id); +export const totalFrames = () => SCENE_IDS.reduce((a, id) => a + sceneFrames(id), 0); +export const sceneStarts = () => { + const out: Record = {}; + SCENE_IDS.reduce((acc, id) => ((out[id] = acc), acc + sceneFrames(id)), 0); + return out; +}; +export const audioOf = (id: string) => TIMINGS[id].audio ?? `audio/${id}.mp3`; + +const SceneCtx = createContext<{id: string}>({id: SCENE_IDS[0] ?? ''}); +export const SceneProvider = SceneCtx.Provider; + +/** + * Inside a scene: all frames are scene-local. + * - c(cue): frame of a [[cue]] marker + * - w(sub, nth): frame at which the narrator starts saying `sub` + * - rel(sub, cue): frames from `cue` until `sub` is first said at or after it (for shot-local time) + */ +export const useScene = () => { + const {id} = useContext(SceneCtx); + const f = useCurrentFrame(); + const T = TIMINGS[id]; + const lead = leadOf(id); + const c = (k: string) => { + const v = T.cues[k]; + if (v === undefined) throw new Error(`cue "${k}" missing in ${id}`); + return lead + Math.round(v * FPS); + }; + const w = (sub: string, nth = 0) => { + let k = 0; + for (const ln of T.lines) { + let from = 0; + for (;;) { + const idx = ln.text.indexOf(sub, from); + if (idx < 0) break; + if (k++ === nth) { + const word = ln.words.find((x) => x.e > idx) ?? ln.words[ln.words.length - 1]; + return lead + Math.round((word ? word.t : ln.start) * FPS); + } + from = idx + 1; + } + } + throw new Error(`"${sub}" (occurrence ${nth}) not found in ${id}`); + }; + const rel = (sub: string, cue: string) => { + const c0 = c(cue); + for (let n = 0; ; n++) { + const fr = w(sub, n); + if (fr >= c0 - 2) return fr - c0; + } + }; + return {f, c, w, rel, id, total: sceneFrames(id), end: narrEnd(id), lead}; +}; + +const EASE = Easing.bezier(0.16, 0.84, 0.24, 1); +/** 0→1 eased progress starting at frame `at`. */ +export const ease = (f: number, at: number, dur = 18) => + interpolate(f, [at, at + dur], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASE}); +/** springy 0→1 (slight overshoot). */ +export const pop = (f: number, at: number) => + f < at ? 0 : spring({frame: f - at, fps: FPS, config: {damping: 13, stiffness: 140, mass: 0.8}}); +/** fade-up style. */ +export const fu = (p: number, dy = 28): React.CSSProperties => ({ + opacity: p, + transform: `translateY(${(1 - p) * dy}px)`, +}); +/** linear blend a→b by p. */ +export const lerp = (p: number, a: number, b: number) => a + (b - a) * p; +/** visible between frames a..b, fading in and out. */ +export const window_ = (f: number, a: number, b: number, fade = 12) => { + const fd = Math.max(1, Math.min(fade, (b - a) / 2 - 1)); + return interpolate(f, [a, a + fd, b - fd, b], [0, 1, 1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); +}; +export const num = (v: number, digits = 0) => + v.toLocaleString('en-US', {minimumFractionDigits: digits, maximumFractionDigits: digits}); diff --git a/reference/zzm/src/film.tsx b/src/film/film.tsx similarity index 83% rename from reference/zzm/src/film.tsx rename to src/film/film.tsx index 2072a0e..d50af57 100644 --- a/reference/zzm/src/film.tsx +++ b/src/film/film.tsx @@ -1,28 +1,23 @@ -/** Cinematic building blocks: shots, camera, letterbox, grain, titles, paper props, stamps. */ +/** Film pack: cue-driven shots, camera moves, colour grade, grain, letterbox, documentary titles, paper and stamps. */ import React from 'react'; import {Easing, interpolate, staticFile, useCurrentFrame} from 'remotion'; -import {C, FONT, GRADE, Grade} from './theme'; +import {FPS, HEIGHT} from '../config'; +import {C, FONT} from '../core/theme'; +import {ease, lerp} from '../core/timeline'; -export const W = 1920; -export const H = 1080; -/** letterbox bar height; picture area is y ∈ [BAR, H-BAR] */ -export const BAR = 96; -export const PIC_TOP = BAR; -export const PIC_H = H - 2 * BAR; -export const MID_Y = H / 2; +/** letterbox bar height (≈2.2:1 picture at 1080p); picture area is y ∈ [BAR, HEIGHT - BAR] */ +export const BAR = Math.round(HEIGHT / 11.25); +export const MID_Y = HEIGHT / 2; export const clamp01 = (v: number) => Math.max(0, Math.min(1, v)); -const EASE = Easing.bezier(0.16, 0.84, 0.24, 1); const INOUT = Easing.bezier(0.45, 0, 0.25, 1); -/** eased 0→1 starting at t=a over `dur` frames */ -export const e = (t: number, a: number, dur = 18) => - interpolate(t, [a, a + dur], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASE}); +/** ease-in-out 0→1 from frame `a` over `dur` frames (camera moves, crossfades) */ export const eio = (t: number, a: number, dur = 18) => interpolate(t, [a, a + dur], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: INOUT}); +/** linear 0→1 between frames a and b */ export const lin = (t: number, a: number, b: number) => clamp01((t - a) / Math.max(1, b - a)); -export const mix = (a: number, b: number, p: number) => a + (b - a) * p; -/** deterministic PRNG */ +/** deterministic PRNG (same frame → same picture, which rendering in parallel requires) */ export const rng = (seed: number) => { let a = seed >>> 0; return () => { @@ -64,7 +59,7 @@ export const Shots: React.FC<{shots: Shot[]; end: number}> = ({shots, end}) => {
, ); } - const op = tr === 'fade' || tr === 'slow' ? (i === 0 ? 1 : eio(t, 0, FADE)) : tr === 'black' ? e(t, 4, 12) : 1; + const op = tr === 'fade' || tr === 'slow' ? (i === 0 ? 1 : eio(t, 0, FADE)) : tr === 'black' ? ease(t, 4, 12) : 1; layers.push(
{cur.el(t, d)} @@ -97,7 +92,7 @@ export const Cam: React.FC<{ position: 'absolute', inset: 0, transformOrigin: `${ox}% ${oy}%`, - transform: `translate(${mix(x[0], x[1], p)}px, ${mix(y[0], y[1], p)}px) scale(${mix(z[0], z[1], p)})`, + transform: `translate(${lerp(p, x[0], x[1])}px, ${lerp(p, y[0], y[1])}px) scale(${lerp(p, z[0], z[1])})`, }} > {children} @@ -105,37 +100,18 @@ export const Cam: React.FC<{ ); }; -/** full-frame SVG canvas */ -export const Svg: React.FC<{children?: React.ReactNode; style?: React.CSSProperties}> = ({children, style}) => ( - - {children} - -); - -export const Abs: React.FC<{x: number; y: number; w?: number; center?: boolean; style?: React.CSSProperties; children?: React.ReactNode}> = ({ - x, - y, - w, - center, - style, - children, -}) => ( -
- {children} -
-); - // ───────────────────────── film look ───────────────────────── +/** colour grade: CSS filter on the picture + a tinted overlay */ +export type Grade = 'past' | 'warm' | 'cold' | 'dusk' | 'neutral'; +export const GRADE: Record = { + past: {filter: 'sepia(0.55) saturate(0.85) contrast(1.06) brightness(0.96)', tint: 'rgba(120,80,30,0.16)', blend: 'multiply'}, + warm: {filter: 'saturate(1.02) contrast(1.04)', tint: 'rgba(255,170,80,0.07)', blend: 'soft-light'}, + cold: {filter: 'saturate(0.85) contrast(1.08)', tint: 'rgba(40,80,140,0.16)', blend: 'soft-light'}, + dusk: {filter: 'saturate(1.08) contrast(1.05)', tint: 'rgba(255,120,60,0.06)', blend: 'soft-light'}, + neutral: {filter: 'none', tint: 'transparent', blend: 'normal'}, +}; + export const Graded: React.FC<{grade: Grade; children: React.ReactNode}> = ({grade, children}) => { const g = GRADE[grade]; return ( @@ -198,7 +174,7 @@ export const Type: React.FC<{t: number; at?: number; cps?: number; text: string; style, caret, }) => { - const n = Math.max(0, Math.floor(((t - at) / 30) * cps)); + const n = Math.max(0, Math.floor(((t - at) / FPS) * cps)); const shown = text.slice(0, n); const typing = n > 0 && n < text.length; return ( @@ -210,7 +186,7 @@ export const Type: React.FC<{t: number; at?: number; cps?: number; text: string; ); }; -/** Place/time stamp like a documentary lower-third: "1983 · 美国 达拉斯" */ +/** Documentary lower-third: a big "year" line and a smaller "place" line, typed in. */ export const Place: React.FC<{t: number; at?: number; year: string; place: string; x?: number; y?: number; color?: string}> = ({ t, at = 8, @@ -220,7 +196,7 @@ export const Place: React.FC<{t: number; at?: number; year: string; place: strin y = 790, color = C.ink, }) => { - const p = e(t, at, 20); + const p = ease(t, at, 20); return (
@@ -234,7 +210,7 @@ export const Place: React.FC<{t: number; at?: number; year: string; place: strin ); }; -/** Small caption label that pops in, pinned at (x,y) with a hairline leader. */ +/** Small caption label that pops in at (x, y). */ export const Tag: React.FC<{t: number; at?: number; x: number; y: number; text: string; sub?: string; color?: string; align?: 'left' | 'center' | 'right'; size?: number}> = ({ t, at = 0, @@ -246,7 +222,7 @@ export const Tag: React.FC<{t: number; at?: number; x: number; y: number; text: align = 'center', size = 30, }) => { - const p = e(t, at, 14); + const p = ease(t, at, 14); const tx = align === 'center' ? '-50%' : align === 'right' ? '-100%' : '0'; return (
{ - const p = e(t, at, dur); + const p = ease(t, at, dur); return (
= ({ t, at = 0, @@ -309,7 +285,7 @@ export const BigQuote: React.FC<{t: number; at?: number; text: string; who?: str color = C.ink, y = MID_Y, }) => { - const n = Math.max(0, Math.floor(((t - at) / 30) * cps)); + const n = Math.max(0, Math.floor(((t - at) / FPS) * cps)); const flat = text.replace(/\n/g, ''); const done = n >= flat.length; let k = 0; @@ -319,7 +295,7 @@ export const BigQuote: React.FC<{t: number; at?: number; text: string; who?: str
{rows.map((row, ri) => (
- {ri === 0 ? 「 : null} + {ri === 0 ? 「 : null} {row.split('').map((ch) => { const i = k++; return ( @@ -333,14 +309,15 @@ export const BigQuote: React.FC<{t: number; at?: number; text: string; who?: str ))}
{who ? ( -
— {who}
+
— {who}
) : null}
); }; -// ───────────────────────── paper props ───────────────────────── +// ───────────────────────── paper ───────────────────────── +/** A sheet of paper that slides in at frame `at`; put any content inside. */ export const Paper: React.FC<{ x: number; y: number; @@ -354,7 +331,7 @@ export const Paper: React.FC<{ children?: React.ReactNode; style?: React.CSSProperties; }> = ({x, y, w, h, rot = 0, t = 99, at = 0, tone = C.paper, pad = 44, children, style}) => { - const p = e(t, at, 18); + const p = ease(t, at, 18); return (
= ({ t, at, @@ -439,7 +416,7 @@ export const Stamp: React.FC<{t: number; at: number; text: string; color?: strin /** Hand-drawn marker highlight behind text (on paper). */ export const Mark: React.FC<{t: number; at: number; children: React.ReactNode; color?: string}> = ({t, at, children, color = 'rgba(233,185,73,0.55)'}) => { - const p = e(t, at, 14); + const p = ease(t, at, 14); return ( = ({f, lead, kicker, title}) => { - const out = 1 - e(f, lead - 6, 16); - const p = e(f, 2, 16); + const out = 1 - ease(f, lead - 6, 16); + const p = ease(f, 2, 16); if (out <= 0) return null; return (
{kicker}
-
{title}
+
{title}
); }; diff --git a/src/film/index.tsx b/src/film/index.tsx new file mode 100644 index 0000000..5bb7c0f --- /dev/null +++ b/src/film/index.tsx @@ -0,0 +1,24 @@ +/** Film style pack. Delete this folder if you only use slides (and remove `film` from src/scenes). */ +import React from 'react'; +import type {Look} from '../core/look'; +import {C} from '../core/theme'; +import {BAR, Chapter, Grain, Letterbox, Vignette} from './film'; + +export * from './film'; +export * from './props'; + +const FilmOverlay: React.FC = () => ( + <> + + + +); + +/** Black canvas, vignette + grain over the picture, 2.2:1 letterbox, subtitles inside the bottom bar. */ +export const film: Look = { + base: C.black, + overlay: FilmOverlay, + frame: Letterbox, + chapter: Chapter, + subtitles: {bottom: 0, height: BAR, quoteColor: C.gold}, +}; diff --git a/src/film/props.tsx b/src/film/props.tsx new file mode 100644 index 0000000..e6e6b20 --- /dev/null +++ b/src/film/props.tsx @@ -0,0 +1,151 @@ +/** Generic sets and props for the film pack: backdrops, a typed document, an ink bar chart, a counter. */ +import React from 'react'; +import {interpolate} from 'remotion'; +import {HEIGHT, WIDTH} from '../config'; +import {C, FONT} from '../core/theme'; +import {ease, lerp} from '../core/timeline'; +import {Mark, Paper, clamp01} from './film'; + +/** Vertical sky gradient filling the frame (inside ). `stops` are [offset 0..1, colour]. */ +export const Sky: React.FC<{id: string; stops: [number, string][]}> = ({id, stops}) => ( + <> + + + {stops.map(([o, c]) => ( + + ))} + + + + +); + +/** Soft radial light (inside ). */ +export const Glow: React.FC<{x: number; y: number; r: number; color?: string; op?: number}> = ({x, y, r, color = '#ffd690', op = 1}) => { + const id = `glow-${color.replace(/[^a-zA-Z0-9]/g, '')}`; + return ( + <> + + + + + + + + + + ); +}; + +/** Black void with a soft spotlight — the backdrop for quotes and title beats. */ +export const Void: React.FC<{tint?: string; x?: number; y?: number}> = ({tint = 'rgba(255,220,170,0.12)', x = 50, y = 35}) => ( +
+); + +/** Overhead dark-wood desk with a pool of lamp light: backdrop for paper props. */ +export const Desk: React.FC<{tone?: 'warm' | 'cold'; lx?: number; ly?: number}> = ({tone = 'warm', lx = 50, ly = 45}) => ( +
+); + +export type DocRow = {k: React.ReactNode; v?: string; at?: number; mark?: boolean; strike?: boolean}; + +/** Typed document: title, optional kicker, rows revealed one by one (each row's `at`, else staggered from `at`). */ +export const Doc: React.FC<{ + t: number; + at?: number; + x: number; + y: number; + w: number; + rot?: number; + title: string; + kicker?: string; + rows?: DocRow[]; + size?: number; + children?: React.ReactNode; + tone?: string; +}> = ({t, at = 0, x, y, w, rot = 0, title, kicker, rows = [], size = 34, children, tone}) => ( + + {kicker ?
{kicker}
: null} +
{title}
+ {rows.map((r, i) => { + const rowAt = r.at ?? at + 10 + i * 8; + const p = ease(t, rowAt, 12); + return ( +
+ {r.mark ? {r.k} : r.k} + {r.v ? {r.v} : null} +
+ ); + })} + {children} +
+); + +/** Ink bar chart (put it on a Paper or Doc). Bars grow from frame `at`, staggered. */ +export const InkBars: React.FC<{t: number; at: number; rows: {k: string; v: number; label: string; color: string}[]; max: number; w?: number; size?: number}> = ({ + t, + at, + rows, + max, + w = 560, + size = 30, +}) => ( +
+ {rows.map((r, i) => { + const p = ease(t, at + i * 8, 24); + return ( +
+
{r.k}
+
+
+
+
{r.label}
+
+ ); + })} +
+); + +/** Number that counts from `from` to `to` starting at frame `at`. */ +export const Count: React.FC<{t: number; at: number; dur?: number; from?: number; to: number; digits?: number; prefix?: string; suffix?: string; style?: React.CSSProperties}> = ({ + t, + at, + dur = 30, + from = 0, + to, + digits = 0, + prefix = '', + suffix = '', + style, +}) => { + const p = interpolate(t, [at, at + dur], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: (x) => 1 - Math.pow(1 - x, 3)}); + const v = lerp(p, from, to); + return ( + + {prefix} + {v.toLocaleString('en-US', {minimumFractionDigits: digits, maximumFractionDigits: digits})} + {suffix} + + ); +}; diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..35306c6 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1 @@ +declare module '*.css'; diff --git a/reference/zzm/src/index.ts b/src/index.ts similarity index 98% rename from reference/zzm/src/index.ts rename to src/index.ts index 7ccef43..69bbf50 100644 --- a/reference/zzm/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import {registerRoot} from 'remotion'; - import {Root} from './Root'; registerRoot(Root); diff --git a/src/scenes/index.ts b/src/scenes/index.ts new file mode 100644 index 0000000..e34d2a6 --- /dev/null +++ b/src/scenes/index.ts @@ -0,0 +1,11 @@ +/** Scene registry: id (as in tts/script.json) → component + look. Scenes with no entry render as empty frames. */ +import type {SceneDef} from '../core/look'; +import {film} from '../film'; +import {slides} from '../slides'; +import {S01} from './s01'; +import {S02} from './s02'; + +export const SCENES: Record = { + s01: {component: S01, look: film}, + s02: {component: S02, look: slides}, +}; diff --git a/src/scenes/s01.tsx b/src/scenes/s01.tsx new file mode 100644 index 0000000..b319013 --- /dev/null +++ b/src/scenes/s01.tsx @@ -0,0 +1,161 @@ +/** s01 · film pack demo: shots cut on cues, a stamp lands on a spoken word, a quote in the alternate voice. */ +import React from 'react'; +import {Svg, ease, useScene} from '../core'; +import {C, FONT} from '../core/theme'; +import {BigQuote, Cam, Count, Desk, Doc, Glow, Graded, Line, MID_Y, Place, Shots, Sky, Stamp, Tag, Void} from '../film'; + +const Dusk: React.FC<{t: number}> = ({t}) => ( + + + + +); + +export const S01: React.FC = () => { + const {c, w, rel, total} = useScene(); + const shots = [ + { + // narration starts at c('open'); the shot starts at 0 so it is already there when the chapter card fades + at: 0, + el: (t: number, d: number) => ( + + + + + + + + ), + }, + { + at: c('old'), + el: (t: number, d: number) => ( + + + + ({ + k, + v: `00:${String(3 + i * 4).padStart(2, '0')}.${(i * 37) % 100}`, + at: 6 + i * 6, + // once the narration says 改一句, every hand-timed row is wrong + strike: t >= rel('改一句', 'old') + 8 + i * 4, + }))} + /> + + {/* rel(): frames from the cue to a spoken word — exactly the shot-local time t */} + + + ), + }, + { + at: c('idea'), + tr: 'black' as const, + el: (t: number, d: number) => ( + + + + + + + ), + }, + { + at: c('mark'), + el: (t: number, d: number) => ( + + + + [[cue]], v: '镜头 / 动画触发点', at: rel('方括号', 'mark'), mark: true}, + {k: ||, v: '戏剧停顿', at: rel('合成语音', 'mark')}, + {k: {'<>'}, v: '换一个声音读引语', at: rel('每个字', 'mark')}, + ]} + /> + + + + ), + }, + { + at: c('quote'), + tr: 'black' as const, + el: (t: number) => ( + <> + + + + ), + }, + { + at: c('ask'), + el: (t: number, d: number) => { + // w(): the scene-local frame of a spoken word — shown on screen as the answer + const target = w('第几帧'); + return ( + + + + + +
+ +
+ +
+ ); + }, + }, + ]; + return ; +}; diff --git a/src/scenes/s02.tsx b/src/scenes/s02.tsx new file mode 100644 index 0000000..348d89c --- /dev/null +++ b/src/scenes/s02.tsx @@ -0,0 +1,103 @@ +/** s02 · slides pack demo: c() / w() / rel() shown with their live values, a timeline of cues, one KaTeX formula. */ +import React from 'react'; +import {At, DrawLine, FPS, Full, Svg, Tex, TIMINGS, ease, fu, lerp, pop, useScene, window_} from '../core'; +import {C, FONT} from '../core/theme'; +import {Chip, H, Kicker, Panel} from '../slides'; + +const X0 = 160; +const X1 = 1760; +const AXIS_Y = 820; + +export const S02: React.FC = () => { + const {f, c, w, rel, id, lead, total} = useScene(); + const xOf = (fr: number) => lerp(fr / total, X0, X1); + const cues = Object.keys(TIMINGS[id].cues); + + const head = ease(f, c('api'), 20); + const cards = window_(f, c('c') - 4, c('formula') + 10, 14); + const formula = window_(f, c('formula'), c('done') + 6, 14); + const done = ease(f, c('done'), 20); + const relFrom = c('rel'); + const relTo = relFrom + rel('多少帧', 'rel'); + + const api = [ + {cue: 'c', code: "c('formula')", val: c('formula'), desc: '标记所在的帧', color: C.teal}, + {cue: 'w', code: "w('那一帧')", val: w('那一帧'), desc: '某个词出口的帧', color: C.orange}, + {cue: 'rel', code: "rel('多少帧', 'rel')", val: rel('多少帧', 'rel'), desc: '从标记到这个词隔几帧', color: C.gold}, + ]; + + // the formula, filled in with this scene's real numbers for the `formula` cue + const sec = TIMINGS[id].cues.formula; + + return ( + + + TIMING API · src/core/timeline.ts + + 三个函数,全部按帧 + + + + + {api.map((a, i) => { + const p = ease(f, c(a.cue), 16); + return ( + + + + {a.code} + +
{a.desc}
+
= {a.val}
+
+
+ ); + })} +
+ + + + + + {/* At `center` composes its own translate(-50%,-50%) with fu()'s translateY */} + + + {`${lead} + round(${sec} × ${FPS}) = ${c('formula')}`} + + + + + + {['script.json', 'gen.py', 'timings.json', '动画'].map((s, i) => ( + + {i > 0 ? → : null} + + + {s} + + + + ))} + + + {/* the scene's own timeline: every cue as a tick, a playhead, and the rel() span */} + + + {cues.map((k) => { + const x = xOf(c(k)); + const p = ease(f, c(k), 10); + return ( + + + + {k} + + + ); + })} + + + +
+ ); +}; diff --git a/src/slides/index.tsx b/src/slides/index.tsx new file mode 100644 index 0000000..96a87ec --- /dev/null +++ b/src/slides/index.tsx @@ -0,0 +1,103 @@ +/** Slides style pack: whiteboard-like panels and chips on a dark teal ground. Delete this folder if unused. */ +import React from 'react'; +import {AbsoluteFill, interpolate, useCurrentFrame} from 'remotion'; +import type {ChapterProps, Look} from '../core/look'; +import {C, FONT} from '../core/theme'; +import {ease} from '../core/timeline'; + +export const Kicker: React.FC<{children: React.ReactNode; color?: string; style?: React.CSSProperties}> = ({children, color = C.orange, style}) => ( +
{children}
+); + +/** Heading. */ +export const H: React.FC<{children: React.ReactNode; size?: number; style?: React.CSSProperties; serif?: boolean}> = ({children, size = 64, style, serif}) => ( +
{children}
+); + +export const Panel: React.FC<{children: React.ReactNode; style?: React.CSSProperties; accent?: string}> = ({children, style, accent}) => ( +
+ {children} +
+); + +/** Pill label. `color` must be a #rrggbb hex (a 2-digit alpha is appended for the tint). */ +export const Chip: React.FC<{children: React.ReactNode; color?: string; fill?: boolean; style?: React.CSSProperties; size?: number}> = ({ + children, + color = C.teal, + fill, + style, + size = 34, +}) => ( + + {children} + +); + +/** Dark teal gradient with a slowly drifting dot grid. */ +export const SlideBackground: React.FC = () => { + const f = useCurrentFrame(); + const drift = (f * 0.25) % 60; + return ( + + + + + ); +}; + +/** Simple chapter card: kicker + heading on the slide background, fading out as narration starts. */ +export const SlideChapter: React.FC = ({f, lead, kicker, title}) => { + const out = interpolate(f, [lead - 10, lead], [1, 0], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}); + if (out <= 0) return null; + const p = ease(f, 0, 14); + return ( + + + + {kicker} + {title} + + + ); +}; + +/** Teal slide ground, simple chapter card, boxed karaoke subtitles near the bottom. */ +export const slides: Look = { + base: C.bg, + background: SlideBackground, + chapter: SlideChapter, + subtitles: {bottom: 58, box: true, karaoke: true, size: 40, quoteColor: C.orange}, +}; diff --git a/reference/zzm/src/timings.json b/src/timings.json similarity index 52% rename from reference/zzm/src/timings.json rename to src/timings.json index 9dd0787..e369913 100644 --- a/reference/zzm/src/timings.json +++ b/src/timings.json @@ -1,1110 +1,1156 @@ { "s01": { - "chapter": "序章 · 沙漠里的一句话", - "duration": 69.528, + "chapter": "第一场 · 旁白就是时间轴", + "duration": 34.595, + "audio": "audio/s01.wav", "cues": { - "desert": 0.095, - "fab": 5.466, - "walk": 16.318, - "expect": 22.489, - "quote": 27.197, - "irony": 35.747, - "q": 48.004, - "back": 61.217 + "open": 0.0, + "old": 5.476, + "idea": 11.226, + "mark": 15.988, + "quote": 24.559, + "ask": 30.071 }, "lines": [ { - "text": "2022年12月6日,美国亚利桑那州,", - "start": 0.095, + "text": "做讲解视频,最磨人的往往不是画面,", + "start": 0.0, "words": [ { - "t": 0.095, + "t": 0.0, "s": 0, - "e": 10 - }, - { - "t": 2.061, - "s": 11, - "e": 13 + "e": 1 }, { - "t": 2.414, - "s": 13, - "e": 18 - } - ], - "end": 3.402 - }, - { - "text": "凤凰城北边的沙漠。", - "start": 3.402, - "words": [ - { - "t": 3.402, - "s": 0, + "t": 0.238, + "s": 1, "e": 2 }, { - "t": 3.845, + "t": 0.476, "s": 2, + "e": 3 + }, + { + "t": 0.714, + "s": 3, "e": 4 }, { - "t": 4.3, + "t": 0.952, "s": 4, "e": 5 }, { - "t": 4.436, - "s": 5, - "e": 6 + "t": 1.429, + "s": 6, + "e": 7 }, { - "t": 4.527, - "s": 6, + "t": 1.667, + "s": 7, "e": 8 - } - ], - "end": 5.466 - }, - { - "text": "一座还没完工的晶圆厂前面,", - "start": 5.466, - "words": [ + }, { - "t": 5.466, - "s": 0, - "e": 1 + "t": 1.905, + "s": 8, + "e": 9 }, { - "t": 5.636, - "s": 1, - "e": 2 + "t": 2.143, + "s": 9, + "e": 10 }, { - "t": 5.795, - "s": 2, - "e": 3 + "t": 2.381, + "s": 10, + "e": 11 }, { - "t": 5.943, - "s": 3, - "e": 4 + "t": 2.619, + "s": 11, + "e": 12 }, { - "t": 6.08, - "s": 4, - "e": 6 + "t": 2.857, + "s": 12, + "e": 13 }, { - "t": 6.489, - "s": 6, - "e": 7 + "t": 3.095, + "s": 13, + "e": 14 }, { - "t": 6.614, - "s": 7, - "e": 10 + "t": 3.333, + "s": 14, + "e": 15 }, { - "t": 7.159, - "s": 10, - "e": 12 + "t": 3.571, + "s": 15, + "e": 16 } ], - "end": 7.705 + "end": 4.048 }, { - "text": "搭起了一个台子。", - "start": 7.705, + "text": "而是对时间。", + "start": 4.048, "words": [ { - "t": 7.705, + "t": 4.048, "s": 0, "e": 1 }, { - "t": 7.943, + "t": 4.286, "s": 1, "e": 2 }, { - "t": 8.114, + "t": 4.524, "s": 2, "e": 3 }, { - "t": 8.193, + "t": 4.762, "s": 3, - "e": 5 + "e": 4 }, { - "t": 8.352, - "s": 5, - "e": 7 + "t": 5.0, + "s": 4, + "e": 5 } ], - "end": 9.318 + "end": 5.476 }, { - "text": "台下坐着美国总统拜登,", - "start": 9.318, + "text": "旁白改一句,", + "start": 5.476, "words": [ { - "t": 9.318, + "t": 5.476, "s": 0, + "e": 1 + }, + { + "t": 5.714, + "s": 1, "e": 2 }, { - "t": 9.727, + "t": 5.952, "s": 2, "e": 3 }, { - "t": 9.92, + "t": 6.19, "s": 3, "e": 4 }, { - "t": 10.023, + "t": 6.429, "s": 4, - "e": 6 - }, - { - "t": 10.33, - "s": 6, - "e": 8 - }, - { - "t": 10.693, - "s": 8, - "e": 10 + "e": 5 } ], - "end": 11.284 + "end": 6.905 }, { - "text": "苹果的库克、英伟达的黄仁勋、AMD的苏姿丰,", - "start": 11.284, + "text": "后面几十个动画就得重新对一遍。", + "start": 6.905, "words": [ { - "t": 11.284, + "t": 6.905, "s": 0, + "e": 1 + }, + { + "t": 7.143, + "s": 1, "e": 2 }, { - "t": 11.716, + "t": 7.381, "s": 2, "e": 3 }, { - "t": 11.795, + "t": 7.619, "s": 3, + "e": 4 + }, + { + "t": 7.857, + "s": 4, "e": 5 }, { - "t": 12.386, + "t": 8.095, + "s": 5, + "e": 6 + }, + { + "t": 8.333, "s": 6, + "e": 7 + }, + { + "t": 8.571, + "s": 7, + "e": 8 + }, + { + "t": 8.81, + "s": 8, "e": 9 }, { - "t": 12.92, + "t": 9.048, "s": 9, "e": 10 }, { - "t": 13.023, + "t": 9.286, "s": 10, - "e": 13 - }, - { - "t": 13.864, - "s": 14, - "e": 17 + "e": 11 }, { - "t": 14.364, - "s": 17, - "e": 18 + "t": 9.524, + "s": 11, + "e": 12 }, { - "t": 14.455, - "s": 18, - "e": 20 + "t": 9.762, + "s": 12, + "e": 13 }, { - "t": 14.784, - "s": 20, - "e": 21 + "t": 10.0, + "s": 13, + "e": 14 } ], - "end": 15.205 + "end": 10.726 }, { - "text": "都来了。", - "start": 15.205, + "text": "这个模板换了个思路:让旁白自己当时间轴。", + "start": 11.226, "words": [ { - "t": 15.205, + "t": 11.226, "s": 0, "e": 1 }, { - "t": 15.432, + "t": 11.464, "s": 1, "e": 2 }, { - "t": 15.636, + "t": 11.702, "s": 2, "e": 3 - } - ], - "end": 16.318 - }, - { - "text": "一位91岁的老人,慢慢走上台。", - "start": 16.318, - "words": [ - { - "t": 16.318, - "s": 0, - "e": 1 }, { - "t": 16.432, - "s": 1, - "e": 2 + "t": 11.94, + "s": 3, + "e": 4 }, { - "t": 16.648, - "s": 2, + "t": 12.178, + "s": 4, "e": 5 }, { - "t": 17.273, + "t": 12.416, "s": 5, "e": 6 }, { - "t": 17.364, + "t": 12.655, "s": 6, + "e": 7 + }, + { + "t": 12.893, + "s": 7, "e": 8 }, { - "t": 18.011, - "s": 9, + "t": 13.131, + "s": 8, + "e": 9 + }, + { + "t": 13.607, + "s": 10, "e": 11 }, { - "t": 18.477, + "t": 13.845, "s": 11, "e": 12 }, { - "t": 18.659, + "t": 14.083, "s": 12, - "e": 14 - } - ], - "end": 19.614 - }, - { - "text": "他叫张忠谋,台积电的创始人。", - "start": 19.614, - "words": [ + "e": 13 + }, { - "t": 19.614, - "s": 0, - "e": 1 + "t": 14.321, + "s": 13, + "e": 14 }, { - "t": 19.784, - "s": 1, - "e": 2 + "t": 14.559, + "s": 14, + "e": 15 }, { - "t": 19.943, - "s": 2, - "e": 5 + "t": 14.797, + "s": 15, + "e": 16 }, { - "t": 20.659, - "s": 6, - "e": 9 + "t": 15.036, + "s": 16, + "e": 17 }, { - "t": 21.273, - "s": 9, - "e": 10 + "t": 15.274, + "s": 17, + "e": 18 }, { - "t": 21.375, - "s": 10, - "e": 13 + "t": 15.512, + "s": 18, + "e": 19 } ], - "end": 22.489 + "end": 15.988 }, { - "text": "按惯例,这种场合,说的都是喜庆话。", - "start": 22.489, + "text": "在文稿里插一个方括号标记,合成语音时,", + "start": 15.988, "words": [ { - "t": 22.489, + "t": 15.988, "s": 0, "e": 1 }, { - "t": 22.693, + "t": 16.226, "s": 1, + "e": 2 + }, + { + "t": 16.464, + "s": 2, "e": 3 }, { - "t": 23.239, + "t": 16.702, + "s": 3, + "e": 4 + }, + { + "t": 16.94, "s": 4, + "e": 5 + }, + { + "t": 17.178, + "s": 5, "e": 6 }, { - "t": 23.568, + "t": 17.416, "s": 6, + "e": 7 + }, + { + "t": 17.655, + "s": 7, "e": 8 }, { - "t": 24.273, + "t": 17.893, + "s": 8, + "e": 9 + }, + { + "t": 18.131, "s": 9, "e": 10 }, { - "t": 24.489, + "t": 18.369, "s": 10, "e": 11 }, { - "t": 24.591, + "t": 18.607, "s": 11, "e": 12 }, { - "t": 24.818, - "s": 12, - "e": 13 + "t": 19.083, + "s": 13, + "e": 14 }, { - "t": 24.966, - "s": 13, + "t": 19.321, + "s": 14, "e": 15 }, { - "t": 25.352, + "t": 19.559, "s": 15, "e": 16 - } - ], - "end": 26.602 - }, - { - "text": "他说的却是:", - "start": 27.197, - "words": [ - { - "t": 27.197, - "s": 0, - "e": 1 - }, - { - "t": 27.368, - "s": 1, - "e": 2 - }, - { - "t": 27.584, - "s": 2, - "e": 3 }, { - "t": 27.652, - "s": 3, - "e": 4 + "t": 19.797, + "s": 16, + "e": 17 }, { - "t": 27.857, - "s": 4, - "e": 5 + "t": 20.036, + "s": 17, + "e": 18 } ], - "end": 29.074 + "end": 20.512 }, { - "text": "全球化几乎已死。", - "start": 29.074, + "text": "每个字什么时候出口,都会被记下来。", + "start": 20.512, "words": [ { - "t": 29.074, + "t": 20.512, "s": 0, + "e": 1 + }, + { + "t": 20.75, + "s": 1, "e": 2 }, { - "t": 29.619, + "t": 20.988, "s": 2, "e": 3 }, { - "t": 29.898, + "t": 21.226, "s": 3, "e": 4 }, { - "t": 30.031, + "t": 21.464, "s": 4, "e": 5 }, { - "t": 30.191, + "t": 21.702, "s": 5, "e": 6 }, { - "t": 30.311, + "t": 21.94, "s": 6, "e": 7 - } - ], - "q": "zc", - "end": 31.79 - }, - { - "text": "自由贸易,几乎已死。", - "start": 31.79, - "words": [ - { - "t": 31.79, - "s": 0, - "e": 2 - }, - { - "t": 32.149, - "s": 2, - "e": 3 }, { - "t": 32.335, - "s": 3, - "e": 4 - }, - { - "t": 32.947, - "s": 5, - "e": 6 - }, - { - "t": 33.16, - "s": 6, - "e": 7 - }, - { - "t": 33.319, + "t": 22.178, "s": 7, "e": 8 }, { - "t": 33.452, + "t": 22.416, "s": 8, "e": 9 - } - ], - "q": "zc", - "end": 35.152 - }, - { - "text": "奇怪的是,说这句话的人,", - "start": 35.747, - "words": [ - { - "t": 35.747, - "s": 0, - "e": 2 }, { - "t": 36.122, - "s": 2, - "e": 3 - }, - { - "t": 36.202, - "s": 3, - "e": 4 - }, - { - "t": 36.691, - "s": 5, - "e": 6 + "t": 22.893, + "s": 10, + "e": 11 }, { - "t": 36.861, - "s": 6, - "e": 7 + "t": 23.131, + "s": 11, + "e": 12 }, { - "t": 37.054, - "s": 7, - "e": 8 + "t": 23.369, + "s": 12, + "e": 13 }, { - "t": 37.168, - "s": 8, - "e": 9 + "t": 23.607, + "s": 13, + "e": 14 }, { - "t": 37.361, - "s": 9, - "e": 10 + "t": 23.845, + "s": 14, + "e": 15 }, { - "t": 37.441, - "s": 10, - "e": 11 + "t": 24.083, + "s": 15, + "e": 16 } ], - "end": 37.827 + "end": 24.559 }, { - "text": "恰恰是全球化最大的赢家之一。", - "start": 37.827, + "text": "有位剪辑师说得好:", + "start": 24.559, "words": [ { - "t": 37.827, + "t": 24.559, "s": 0, + "e": 1 + }, + { + "t": 24.797, + "s": 1, "e": 2 }, { - "t": 38.247, + "t": 25.036, "s": 2, "e": 3 }, { - "t": 38.418, + "t": 25.274, "s": 3, - "e": 6 + "e": 4 }, { - "t": 39.032, - "s": 6, - "e": 8 + "t": 25.512, + "s": 4, + "e": 5 }, { - "t": 39.543, - "s": 8, - "e": 9 + "t": 25.75, + "s": 5, + "e": 6 }, { - "t": 39.634, - "s": 9, - "e": 11 + "t": 25.988, + "s": 6, + "e": 7 }, { - "t": 39.952, - "s": 11, - "e": 13 + "t": 26.226, + "s": 7, + "e": 8 } ], - "end": 40.766 + "end": 26.702 }, { - "text": "他用一家只替别人造芯片的公司,", - "start": 40.766, + "text": "别对齐画面,对齐声音。", + "start": 26.702, "words": [ { - "t": 40.766, + "t": 26.702, "s": 0, "e": 1 }, { - "t": 40.902, + "t": 26.94, "s": 1, "e": 2 }, { - "t": 41.027, + "t": 27.178, "s": 2, + "e": 3 + }, + { + "t": 27.416, + "s": 3, "e": 4 }, { - "t": 41.402, + "t": 27.654, "s": 4, "e": 5 }, { - "t": 41.584, - "s": 5, - "e": 6 + "t": 28.131, + "s": 6, + "e": 7 }, { - "t": 41.766, - "s": 6, + "t": 28.369, + "s": 7, "e": 8 }, { - "t": 42.084, + "t": 28.607, "s": 8, "e": 9 }, { - "t": 42.266, + "t": 28.845, "s": 9, - "e": 11 - }, - { - "t": 42.561, - "s": 11, - "e": 12 - }, - { - "t": 42.641, - "s": 12, - "e": 14 + "e": 10 } ], - "end": 43.232 + "q": "q", + "end": 29.571 }, { - "text": "把台湾放进了全世界每一部手机、每一台AI服务器的心脏。", - "start": 43.232, + "text": "画面只需要问一句:这个词,第几帧出现?", + "start": 30.071, "words": [ { - "t": 43.232, + "t": 30.071, "s": 0, "e": 1 }, { - "t": 43.357, + "t": 30.309, "s": 1, + "e": 2 + }, + { + "t": 30.547, + "s": 2, "e": 3 }, { - "t": 43.72, + "t": 30.785, "s": 3, + "e": 4 + }, + { + "t": 31.023, + "s": 4, "e": 5 }, { - "t": 44.016, + "t": 31.261, "s": 5, "e": 6 }, { - "t": 44.118, + "t": 31.5, "s": 6, - "e": 9 + "e": 7 + }, + { + "t": 31.738, + "s": 7, + "e": 8 }, { - "t": 44.629, + "t": 32.214, "s": 9, "e": 10 }, { - "t": 44.811, + "t": 32.452, "s": 10, "e": 11 }, { - "t": 44.959, + "t": 32.69, "s": 11, "e": 12 }, { - "t": 45.095, - "s": 12, + "t": 33.166, + "s": 13, "e": 14 }, { - "t": 45.72, + "t": 33.404, + "s": 14, + "e": 15 + }, + { + "t": 33.642, "s": 15, "e": 16 }, { - "t": 45.879, + "t": 33.881, "s": 16, "e": 17 }, { - "t": 45.982, + "t": 34.119, "s": 17, "e": 18 - }, - { - "t": 46.311, - "s": 18, - "e": 20 - }, - { - "t": 46.652, - "s": 20, - "e": 23 - }, - { - "t": 47.038, - "s": 23, - "e": 24 - }, - { - "t": 47.118, - "s": 24, - "e": 26 } ], - "end": 48.004 - }, + "end": 34.595 + } + ], + "aligned": 122, + "words": 122 + }, + "s02": { + "chapter": "第二场 · 三个函数", + "duration": 22.143, + "audio": "audio/s02.wav", + "cues": { + "api": 0.0, + "c": 2.381, + "w": 5.238, + "rel": 8.333, + "formula": 12.619, + "done": 17.619 + }, + "lines": [ { - "text": "一个靠全球化起家的人,", - "start": 48.004, + "text": "场景里只用三个函数。", + "start": 0.0, "words": [ { - "t": 48.004, + "t": 0.0, "s": 0, + "e": 1 + }, + { + "t": 0.238, + "s": 1, "e": 2 }, { - "t": 48.379, + "t": 0.476, "s": 2, "e": 3 }, { - "t": 48.618, + "t": 0.714, "s": 3, + "e": 4 + }, + { + "t": 0.952, + "s": 4, + "e": 5 + }, + { + "t": 1.19, + "s": 5, "e": 6 }, { - "t": 49.22, + "t": 1.429, "s": 6, + "e": 7 + }, + { + "t": 1.667, + "s": 7, "e": 8 }, { - "t": 49.618, + "t": 1.905, "s": 8, "e": 9 - }, - { - "t": 49.697, - "s": 9, - "e": 10 } ], - "end": 50.095 + "end": 2.381 }, { - "text": "为什么要亲口宣布它的死亡?", - "start": 50.095, + "text": "c,取一个标记所在的帧。", + "start": 2.381, "words": [ { - "t": 50.095, + "t": 2.381, "s": 0, + "e": 1 + }, + { + "t": 2.857, + "s": 2, "e": 3 }, { - "t": 50.504, + "t": 3.095, "s": 3, "e": 4 }, { - "t": 50.709, + "t": 3.333, "s": 4, + "e": 5 + }, + { + "t": 3.571, + "s": 5, "e": 6 }, { - "t": 51.129, + "t": 3.81, "s": 6, + "e": 7 + }, + { + "t": 4.048, + "s": 7, "e": 8 }, { - "t": 51.447, + "t": 4.286, "s": 8, "e": 9 }, { - "t": 51.584, + "t": 4.524, "s": 9, "e": 10 }, { - "t": 51.652, + "t": 4.762, "s": 10, - "e": 12 + "e": 11 } ], - "end": 52.709 + "end": 5.238 }, { - "text": "他亲手造出来的这家公司,", - "start": 52.709, + "text": "w,取某个词出口的那一帧。", + "start": 5.238, "words": [ { - "t": 52.709, + "t": 5.238, "s": 0, "e": 1 }, { - "t": 52.845, - "s": 1, + "t": 5.714, + "s": 2, "e": 3 }, { - "t": 53.277, + "t": 5.952, "s": 3, "e": 4 }, { - "t": 53.47, + "t": 6.19, "s": 4, + "e": 5 + }, + { + "t": 6.429, + "s": 5, "e": 6 }, { - "t": 53.697, + "t": 6.667, "s": 6, "e": 7 }, { - "t": 53.777, + "t": 6.905, "s": 7, "e": 8 }, { - "t": 53.936, + "t": 7.143, "s": 8, "e": 9 }, { - "t": 54.072, + "t": 7.381, "s": 9, + "e": 10 + }, + { + "t": 7.619, + "s": 10, "e": 11 + }, + { + "t": 7.857, + "s": 11, + "e": 12 } ], - "end": 54.641 + "end": 8.333 }, { - "text": "又凭什么夹在世界上最大的两个对手之间活下来,", - "start": 54.641, + "text": "rel,算从标记到这个词隔了多少帧。", + "start": 8.333, "words": [ { - "t": 54.641, + "t": 8.333, "s": 0, - "e": 1 - }, - { - "t": 54.8, - "s": 1, - "e": 2 - }, - { - "t": 55.05, - "s": 2, - "e": 4 + "e": 3 }, { - "t": 55.402, + "t": 9.286, "s": 4, "e": 5 }, { - "t": 55.641, + "t": 9.524, "s": 5, "e": 6 }, { - "t": 55.766, + "t": 9.762, "s": 6, + "e": 7 + }, + { + "t": 10.0, + "s": 7, "e": 8 }, { - "t": 56.084, + "t": 10.238, "s": 8, "e": 9 }, { - "t": 56.277, + "t": 10.476, "s": 9, + "e": 10 + }, + { + "t": 10.714, + "s": 10, "e": 11 }, { - "t": 56.709, + "t": 10.952, "s": 11, "e": 12 }, { - "t": 56.811, + "t": 11.19, "s": 12, "e": 13 }, { - "t": 56.982, + "t": 11.429, "s": 13, "e": 14 }, { - "t": 57.072, + "t": 11.667, "s": 14, - "e": 16 - }, - { - "t": 57.402, - "s": 16, - "e": 18 + "e": 15 }, { - "t": 57.709, - "s": 18, - "e": 19 + "t": 11.905, + "s": 15, + "e": 16 }, { - "t": 57.936, - "s": 19, - "e": 21 + "t": 12.143, + "s": 16, + "e": 17 } ], - "end": 58.482 + "end": 12.619 }, { - "text": "还越活越大?", - "start": 58.482, + "text": "换算只有一行:开场留白,", + "start": 12.619, "words": [ { - "t": 58.482, + "t": 12.619, "s": 0, "e": 1 }, { - "t": 58.629, + "t": 12.857, "s": 1, "e": 2 }, { - "t": 58.913, + "t": 13.095, "s": 2, "e": 3 }, { - "t": 59.152, + "t": 13.333, "s": 3, "e": 4 }, { - "t": 59.322, + "t": 13.571, "s": 4, "e": 5 + }, + { + "t": 13.81, + "s": 5, + "e": 6 + }, + { + "t": 14.286, + "s": 7, + "e": 8 + }, + { + "t": 14.524, + "s": 8, + "e": 9 + }, + { + "t": 14.762, + "s": 9, + "e": 10 + }, + { + "t": 15.0, + "s": 10, + "e": 11 } ], - "end": 60.622 + "end": 15.476 }, { - "text": "要回答这个问题,得先回到39年前。", - "start": 61.217, + "text": "加上秒数乘以帧率。", + "start": 15.476, "words": [ { - "t": 61.217, + "t": 15.476, "s": 0, "e": 1 }, { - "t": 61.411, + "t": 15.714, "s": 1, - "e": 3 - }, - { - "t": 61.752, - "s": 3, - "e": 5 - }, - { - "t": 61.956, - "s": 5, - "e": 7 + "e": 2 }, { - "t": 62.547, - "s": 8, - "e": 9 + "t": 15.952, + "s": 2, + "e": 3 }, { - "t": 62.717, - "s": 9, - "e": 10 + "t": 16.19, + "s": 3, + "e": 4 }, { - "t": 62.888, - "s": 10, - "e": 12 + "t": 16.429, + "s": 4, + "e": 5 }, { - "t": 63.206, - "s": 12, - "e": 14 + "t": 16.667, + "s": 5, + "e": 6 }, { - "t": 63.82, - "s": 14, - "e": 15 + "t": 16.905, + "s": 6, + "e": 7 }, { - "t": 63.979, - "s": 15, - "e": 16 + "t": 17.143, + "s": 7, + "e": 8 } ], - "end": 65.26 + "end": 17.619 }, { - "text": "那一年,52岁的张忠谋,丢了工作。", - "start": 65.855, + "text": "改完文稿,重跑一次语音,动画自己跟上。", + "start": 17.619, "words": [ { - "t": 65.855, + "t": 17.619, "s": 0, "e": 1 }, { - "t": 66.037, + "t": 17.857, "s": 1, + "e": 2 + }, + { + "t": 18.095, + "s": 2, "e": 3 }, { - "t": 66.56, - "s": 4, + "t": 18.333, + "s": 3, + "e": 4 + }, + { + "t": 18.81, + "s": 5, + "e": 6 + }, + { + "t": 19.048, + "s": 6, "e": 7 }, { - "t": 67.23, + "t": 19.286, "s": 7, "e": 8 }, { - "t": 67.321, + "t": 19.524, "s": 8, + "e": 9 + }, + { + "t": 19.762, + "s": 9, "e": 10 }, { - "t": 67.685, + "t": 20.0, "s": 10, "e": 11 }, { - "t": 68.071, + "t": 20.476, "s": 12, "e": 13 }, { - "t": 68.276, + "t": 20.714, "s": 13, "e": 14 }, { - "t": 68.39, + "t": 20.952, "s": 14, + "e": 15 + }, + { + "t": 21.19, + "s": 15, "e": 16 + }, + { + "t": 21.429, + "s": 16, + "e": 17 + }, + { + "t": 21.667, + "s": 17, + "e": 18 } ], - "end": 69.528 + "end": 22.143 } ], - "aligned": 184, - "words": 184 + "aligned": 78, + "words": 78 } -} \ No newline at end of file +} diff --git a/tests/test_parse.py b/tests/test_parse.py new file mode 100644 index 0000000..b255489 --- /dev/null +++ b/tests/test_parse.py @@ -0,0 +1,72 @@ +from gen import mock_synth, parse, split_lines + + +def test_plain_text_is_one_narrator_segment(): + clean, cues, segs = parse("你好,世界。") + assert clean == "你好,世界。" + assert cues == {} + assert segs == [{"s": 0, "v": None, "pause": 0.0, "e": 6}] + + +def test_cues_are_stripped_and_record_char_offsets(): + clean, cues, segs = parse("[[a]]第一句。[[b]]第二句。[[tail]]") + assert clean == "第一句。第二句。" + assert cues == {"a": 0, "b": 4, "tail": 8} + assert len(segs) == 1 + + +def test_pause_splits_segments_and_carries_length(): + clean, _, segs = parse("前半句。||后半句。", pause=0.5) + assert clean == "前半句。后半句。" + assert [(clean[s["s"]:s["e"]], s["v"], s["pause"]) for s in segs] == [("前半句。", None, 0.5), ("后半句。", None, 0.0)] + + +def test_double_pause_accumulates_on_previous_segment(): + _, _, segs = parse("一。||||二。", pause=0.75) + assert [s["pause"] for s in segs] == [1.5, 0.0] + + +def test_alternate_voice_quote(): + clean, cues, segs = parse("他说:<>||[[after]]然后。", pause=0.7) + assert clean == "他说:别对齐画面。然后。" + assert [(clean[s["s"]:s["e"]], s["v"], s["pause"]) for s in segs] == [ + ("他说:", None, 0.0), + ("别对齐画面。", "q", 0.7), + ("然后。", None, 0.0), + ] + assert cues == {"after": clean.index("然后")} + + +def test_cue_inside_quote_keeps_offset(): + clean, cues, segs = parse("<>") + assert clean == "全球化几乎已死。" + assert cues == {"q": 0} + assert [s["v"] for s in segs] == ["zc"] + + +def test_split_lines_merges_short_clauses_until_sentence_end(): + text = "旁白改一句,后面全部重来。下一句。" + lines = [text[s:e] for s, e in split_lines(text, max_line=20)] + assert lines == ["旁白改一句,后面全部重来。", "下一句。"] + + +def test_split_lines_respects_max_line(): + text = "一二三四五,六七八九十,甲乙丙丁戊。" + lines = [text[s:e] for s, e in split_lines(text, max_line=8)] + assert lines == ["一二三四五,", "六七八九十,", "甲乙丙丁戊。"] + assert all(len(x) <= 8 for x in lines) + + +def test_split_lines_keeps_unpunctuated_tail_and_covers_all_text(): + text = "第一句,没有句号的结尾" + spans = split_lines(text, max_line=6) + assert [text[s:e] for s, e in spans] == ["第一句,", "没有句号的结尾"] + assert "".join(text[s:e] for s, e in spans) == text + + +def test_mock_synth_timing(): + pcm, words = mock_synth("你好,ab。", cps=4.0) + # 你 好 , ab(2 chars) 。 = 6 chars at 0.25 s each + assert [w["w"] for w in words] == ["你", "好", "ab"] + assert [w["t"] for w in words] == [0.0, 0.25, 0.75] + assert len(pcm) == int(1.5 * 24000) * 2 diff --git a/tools/fonts.mjs b/tools/fonts.mjs new file mode 100644 index 0000000..f76455b --- /dev/null +++ b/tools/fonts.mjs @@ -0,0 +1,22 @@ +// npm run fonts — download the variable fonts named in kit.config.json into public/fonts/ (gitignored). +// Noto Sans SC / Noto Serif SC are under the SIL Open Font License. Without them the font stacks fall back to system fonts. +import fs from 'node:fs'; +import path from 'node:path'; +import {CFG, root} from './timeline.mjs'; + +const dir = path.join(root, 'public/fonts'); +fs.mkdirSync(dir, {recursive: true}); +for (const [slot, f] of Object.entries(CFG.fonts)) { + if (!f.url || !f.file) continue; + const dest = path.join(dir, f.file); + if (fs.existsSync(dest) && fs.statSync(dest).size > 0) { + console.log(`${slot}: ${f.file} already present`); + continue; + } + process.stdout.write(`${slot}: downloading ${f.url} ... `); + const res = await fetch(f.url, {redirect: 'follow'}); + if (!res.ok) throw new Error(`${f.url} → HTTP ${res.status}`); + const buf = Buffer.from(await res.arrayBuffer()); + fs.writeFileSync(dest, buf); + console.log(`${(buf.length / 1e6).toFixed(1)} MB → public/fonts/${f.file}`); +} diff --git a/tools/kit.py b/tools/kit.py new file mode 100644 index 0000000..f4569bc --- /dev/null +++ b/tools/kit.py @@ -0,0 +1,36 @@ +"""Shared by the Python tools: config, timings and the same frame math as src/core/timeline.ts.""" +import json +import math +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +CFG = json.loads((ROOT / "kit.config.json").read_text(encoding="utf-8")) +FPS = CFG["fps"] + + +def timings(): + return json.loads((ROOT / "src" / "timings.json").read_text(encoding="utf-8")) + + +def lead_of(sid): + return CFG["pace"].get("leadOverride", {}).get(sid, CFG["pace"]["lead"]) + + +def tail_of(sid): + return CFG["pace"].get("tailOverride", {}).get(sid, CFG["pace"]["tail"]) + + +def scene_frames(T, sid): + return lead_of(sid) + math.ceil(T[sid]["duration"] * FPS) + tail_of(sid) + + +def scene_spans(T): + """[(id, start_sec, narration_start_sec, narration_end_sec, end_sec)] in video order.""" + out, acc = [], 0 + for sid in T: + n = scene_frames(T, sid) + narr0 = acc + lead_of(sid) + narr1 = narr0 + math.ceil(T[sid]["duration"] * FPS) + out.append((sid, acc / FPS, narr0 / FPS, narr1 / FPS, (acc + n) / FPS)) + acc += n + return out diff --git a/tools/mix.py b/tools/mix.py new file mode 100644 index 0000000..bf37696 --- /dev/null +++ b/tools/mix.py @@ -0,0 +1,39 @@ +"""Mix the music under the narration, ducking it with a sidechain, and loudness-normalize to -16 LUFS. + + python tools/mix.py # out/.mp4 + out/music.wav → out/-mixed.mp4 + python tools/mix.py VIDEO MUSIC OUTPUT # explicit paths + + is the composition id from kit.config.json (what `npm run render` writes by default). +edge-tts output sits around -24 LUFS, so always run this (or at least the loudnorm) before publishing. +""" +import subprocess +import sys + +from kit import CFG, ROOT + +FILTER = ( + "[1:a]lowpass=f=2400,volume=0.55[m];" + "[m][0:a]sidechaincompress=threshold=0.02:ratio=5:attack=60:release=800[md];" + "[0:a][md]amix=inputs=2:duration=first:normalize=0,loudnorm=I=-16:TP=-1.5:LRA=11[a]" +) + + +def main(argv): + out_dir = ROOT / "out" + video = argv[0] if len(argv) > 0 else str(out_dir / f"{CFG['id']}.mp4") + music = argv[1] if len(argv) > 1 else str(out_dir / "music.wav") + output = argv[2] if len(argv) > 2 else str(out_dir / f"{CFG['id']}-mixed.mp4") + cmd = [ + "ffmpeg", "-y", "-i", video, "-i", music, + "-filter_complex", FILTER, + "-map", "0:v", "-map", "[a]", + "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", + output, + ] + print(" ".join(cmd)) + subprocess.run(cmd, check=True) + print(output) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tools/music.py b/tools/music.py new file mode 100644 index 0000000..190557e --- /dev/null +++ b/tools/music.py @@ -0,0 +1,100 @@ +"""Synthesize an underscore for the whole video → out/music.wav (no samples, numpy only). + +- a low drone pad, one section per scene: each scene gets its own chord progression (cycled from PROGRESSIONS) +- a soft hit (low thump + bell) at the start of each scene, i.e. on each chapter card +Scene boundaries come from src/timings.json + kit.config.json, so the score follows re-TTS automatically. +Then mix it under the narration with tools/mix.py. +""" +import wave + +import numpy as np + +from kit import ROOT, scene_spans, timings + +SR = 44100 +BAR = 9.0 # seconds per chord +# MIDI notes, four chords per progression; scene i uses PROGRESSIONS[i % len] +PROGRESSIONS = [ + [[38, 45, 50, 53], [34, 41, 50, 53], [36, 43, 48, 55], [33, 40, 49, 52]], # Dm Bb C A + [[41, 48, 53, 57], [36, 43, 52, 55], [38, 45, 50, 53], [34, 41, 50, 53]], # F C Dm Bb + [[43, 50, 55, 59], [40, 47, 52, 55], [36, 43, 48, 52], [38, 45, 50, 54]], # G Em C D +] +BRIGHT = [0.5, 0.6, 0.7] +BELL = [74, 69, 72, 67] + + +def hz(m): + return 440.0 * 2 ** ((m - 69) / 12) + + +def main(): + spans = scene_spans(timings()) + total = spans[-1][4] if spans else 0.0 + n = int(total * SR) + SR + t = np.arange(n) / SR + out = np.zeros(n) + rng = np.random.default_rng(11) + + def add(i0, sig): + i0 = max(0, i0) + i1 = min(n, i0 + len(sig)) + if i1 > i0: + out[i0:i1] += sig[: i1 - i0] + + # ── drone pad: one section per scene ── + for si, (sid, s0, _, _, s1) in enumerate(spans): + chords = PROGRESSIONS[si % len(PROGRESSIONS)] + bright = BRIGHT[si % len(BRIGHT)] + b = 0 + while s0 + b * BAR < s1: + t0 = s0 + b * BAR + t1 = min(s1, t0 + BAR) + chord = chords[b % len(chords)] + i0 = int(max(0, t0 - 3) * SR) + i1 = min(n, int((t1 + 3) * SR)) + tt = t[i0:i1] - t0 + span = t1 - t0 + env = (np.clip((tt + 3) / 3.5, 0, 1) * np.clip((span + 3 - tt) / 3.5, 0, 1)) ** 1.6 + seg = np.zeros(i1 - i0) + for k, m in enumerate(chord): + f = hz(m) + for det in (-0.003, 0.0, 0.0035): + ph = rng.random() * 2 * np.pi + seg += np.sin(2 * np.pi * f * (1 + det) * tt + ph) * (0.7 if k == 0 else 0.32) + seg += bright * 0.06 * np.sin(2 * np.pi * 2 * f * (1 + det) * tt + ph) + seg *= 1 + 0.1 * np.sin(2 * np.pi * 0.09 * tt + b) + out[i0:i1] += 0.55 * seg * env + b += 1 + + # ── soft hit at each chapter start: low thump + bell ── + for si, (sid, s0, _, _, _) in enumerate(spans): + at = s0 + 0.15 + k = int(2.5 * SR) + tt = np.arange(k) / SR + f = 60 * np.exp(-tt * 2.5) + 32 + thump = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-tt * 2.2) + add(int(at * SR), 0.5 * thump) + k = int(5 * SR) + tt = np.arange(k) / SR + m = BELL[si % len(BELL)] + bell = (np.sin(2 * np.pi * hz(m) * tt) + 0.3 * np.sin(2 * np.pi * hz(m) * 2.76 * tt) * np.exp(-tt * 3)) * np.exp(-tt * 1.1) + add(int(at * SR), 0.22 * bell) + + # fades + normalize + y = out / (np.max(np.abs(out)) + 1e-9) + fade = min(int(3 * SR), n // 2) + y[:fade] *= np.linspace(0, 1, fade) + y[-fade:] *= np.linspace(1, 0, fade) + y = (y * 0.9 * 32767).astype(np.int16) + path = ROOT / "out" / "music.wav" + path.parent.mkdir(parents=True, exist_ok=True) + with wave.open(str(path), "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(SR) + wf.writeframes(y.tobytes()) + print(f"{path} {total:.1f}s {len(spans)} sections") + + +if __name__ == "__main__": + main() diff --git a/tools/pick.mjs b/tools/pick.mjs new file mode 100644 index 0000000..26391ec --- /dev/null +++ b/tools/pick.mjs @@ -0,0 +1,23 @@ +// Render specific frames: node tools/pick.mjs s01:quote:40 s02:formula:0 s02:end:-10 +// Each argument is scene:cue:offsetFrames (cue "end" = end of narration) → out/pick/__.png +import fs from 'node:fs'; +import path from 'node:path'; +import {renderStill} from '@remotion/renderer'; +import {cueFrame, open, root, starts} from './timeline.mjs'; + +const args = process.argv.slice(2); +if (!args.length) { + console.error('usage: node tools/pick.mjs scene:cue[:offset] ...'); + process.exit(1); +} +const outDir = path.join(root, 'out/pick'); +fs.mkdirSync(outDir, {recursive: true}); +const {serveUrl, composition} = await open(); +for (const a of args) { + const [id, cue, off = '0'] = a.split(':'); + if (!(id in starts)) throw new Error(`unknown scene ${id}`); + const frame = starts[id] + cueFrame(id, cue) + Number(off); + const output = path.join(outDir, `${id}_${cue}_${off}.png`); + await renderStill({composition, serveUrl, output, frame, scale: 0.5}); + console.log(output); +} diff --git a/tools/sheet.py b/tools/sheet.py new file mode 100644 index 0000000..a5f304e --- /dev/null +++ b/tools/sheet.py @@ -0,0 +1,30 @@ +"""Tile review stills into 2x2 contact sheets: python tools/sheet.py s01 [s02 ...] → out/sheets/_.png + +Reads out/stills/_*.png (from tools/stills.mjs), in render order. Needs Pillow. +""" +import sys + +from PIL import Image + +from kit import CFG, ROOT + +W, H = CFG["width"], CFG["height"] +GAP = 4 +CW, CH = (W - GAP) // 2, (H - GAP) // 2 + +out = ROOT / "out" / "sheets" +out.mkdir(parents=True, exist_ok=True) +if len(sys.argv) < 2: + sys.exit("usage: python tools/sheet.py s01 [s02 ...]") +for sid in sys.argv[1:]: + files = sorted((ROOT / "out" / "stills").glob(f"{sid}_*.png"), key=lambda p: p.stat().st_mtime) + if not files: + print(f"{sid}: no stills in out/stills (run node tools/stills.mjs {sid} first)") + for k in range(0, len(files), 4): + sheet = Image.new("RGB", (W, H), (40, 40, 40)) + for j, fp in enumerate(files[k:k + 4]): + im = Image.open(fp).convert("RGB").resize((CW, CH)) + sheet.paste(im, ((j % 2) * (CW + GAP), (j // 2) * (CH + GAP))) + p = out / f"{sid}_{k // 4}.png" + sheet.save(p) + print(p, [f.stem for f in files[k:k + 4]]) diff --git a/tools/stills.mjs b/tools/stills.mjs new file mode 100644 index 0000000..cc799e2 --- /dev/null +++ b/tools/stills.mjs @@ -0,0 +1,24 @@ +// Review stills: bundle once, render one frame OFF frames after each cue (plus one near the scene end). +// usage: node tools/stills.mjs [s01 s02 ...] → out/stills/_.png +// OFF=30 node tools/stills.mjs s02 (default OFF=75, i.e. 2.5 s at 30 fps) +import fs from 'node:fs'; +import path from 'node:path'; +import {renderStill} from '@remotion/renderer'; +import {FPS, IDS, T, cueFrame, open, root, sceneLen, starts} from './timeline.mjs'; + +const OFF = Number(process.env.OFF ?? Math.round(2.5 * FPS)); +const only = process.argv.slice(2); +const outDir = path.join(root, 'out/stills'); +fs.mkdirSync(outDir, {recursive: true}); +const {serveUrl, composition} = await open(); +for (const id of IDS) { + if (only.length && !only.includes(id)) continue; + const len = sceneLen(id); + const frames = Object.keys(T[id].cues).map((k) => [k, Math.min(len - 15, cueFrame(id, k) + OFF)]); + frames.push(['end', len - 20]); + for (const [k, fr] of frames) { + const output = path.join(outDir, `${id}_${k}.png`); + await renderStill({composition, serveUrl, output, frame: starts[id] + fr, scale: 0.5}); + console.log(output); + } +} diff --git a/tools/timeline.mjs b/tools/timeline.mjs new file mode 100644 index 0000000..89a9727 --- /dev/null +++ b/tools/timeline.mjs @@ -0,0 +1,38 @@ +// Shared by the Node tools: config, timings and the same frame math as src/core/timeline.ts. +import fs from 'node:fs'; +import path from 'node:path'; + +export const root = path.resolve(import.meta.dirname, '..'); +export const CFG = JSON.parse(fs.readFileSync(path.join(root, 'kit.config.json'), 'utf8')); +export const T = JSON.parse(fs.readFileSync(path.join(root, 'src/timings.json'), 'utf8')); +export const FPS = CFG.fps; +export const IDS = Object.keys(T); + +export const leadOf = (id) => CFG.pace.leadOverride?.[id] ?? CFG.pace.lead; +export const tailOf = (id) => CFG.pace.tailOverride?.[id] ?? CFG.pace.tail; +export const narrEnd = (id) => leadOf(id) + Math.ceil(T[id].duration * FPS); +export const sceneLen = (id) => narrEnd(id) + tailOf(id); +export const starts = {}; +{ + let acc = 0; + for (const id of IDS) { + starts[id] = acc; + acc += sceneLen(id); + } +} +/** scene-local frame of a cue ('end' = end of narration) */ +export const cueFrame = (id, cue) => { + if (cue === 'end') return narrEnd(id); + const v = T[id].cues[cue]; + if (v === undefined) throw new Error(`cue "${cue}" missing in ${id}`); + return leadOf(id) + Math.round(v * FPS); +}; + +/** bundle once and select the composition named in kit.config.json */ +export const open = async () => { + const {bundle} = await import('@remotion/bundler'); + const {selectComposition} = await import('@remotion/renderer'); + const serveUrl = await bundle({entryPoint: path.join(root, 'src/index.ts')}); + const composition = await selectComposition({serveUrl, id: CFG.id}); + return {serveUrl, composition}; +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3b18c8e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "lib": ["DOM", "ES2022"], + "types": [], + "strict": true, + "noEmit": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true + }, + "include": ["src", "kit.config.json"] +} diff --git a/tts/gen.py b/tts/gen.py new file mode 100644 index 0000000..52898b6 --- /dev/null +++ b/tts/gen.py @@ -0,0 +1,296 @@ +"""Narration → audio + timing map for Remotion. + +For each scene in tts/script.json: strip the markers, synthesize each voice segment with edge-tts +(recording WordBoundary events), then map cues and subtitle lines onto audio timestamps. + +Output: public/audio/.mp3 (or .wav with --mock) and src/timings.json + + python tts/gen.py # all scenes, real edge-tts (needs network + ffmpeg) + python tts/gen.py s02 # only s02; other scenes keep their old timings + python tts/gen.py --mock # no network: word times estimated from character count, + # silent .wav written with the stdlib wave module + python tts/gen.py --mock --cps 5 # override chars/sec for the estimate + +Voices, rate, pause length and line length come from kit.config.json. +""" +from __future__ import annotations + +import asyncio +import json +import re +import subprocess +import sys +import wave +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SR = 24000 + +TOKEN_RE = re.compile(r"\[\[(\w+)\]\]|\|\||<<(\w+)\||>>") +BREAK_PUNCT = ",。!?;:,;:?!" +SENT_END = "。!??!" + + +def parse(text: str, pause: float = 0.75): + """Strip markers from narration. + + [[cue]] animation trigger: records the char offset where it sits in the clean text + || dramatic pause of `pause` seconds (also ends the current voice segment) + <> the enclosed text is read by the alternate voice `key` (a quote) + + Returns (clean_text, cues, segments). cues maps name → char offset in clean_text. + Each segment is {"s", "e", "v", "pause"}: a char range of clean_text, its voice key + (None = narrator) and the silence to insert after it. + """ + clean, cues, segs = "", {}, [] + cur = {"s": 0, "v": None, "pause": 0.0} + pos = 0 + + def close(newv): + nonlocal cur + cur["e"] = len(clean) + segs.append(cur) + cur = {"s": len(clean), "v": newv, "pause": 0.0} + + for m in TOKEN_RE.finditer(text): + clean += text[pos:m.start()] + pos = m.end() + tok = m.group(0) + if m.group(1): + cues[m.group(1)] = len(clean) + elif tok == "||": + cur["pause"] += pause + close(cur["v"]) + elif tok == ">>": + close(None) + else: + close(m.group(2)) + clean += text[pos:] + close(None) + # drop empty segments, carrying their pause over to the previous one + out = [] + for sg in segs: + if not clean[sg["s"]:sg["e"]].strip(): + if out: + out[-1]["pause"] += sg["pause"] + continue + out.append(sg) + return clean, cues, out + + +def split_lines(clean: str, max_line: int = 20): + """Subtitle lines as (start, end) char ranges. + + Split into clauses at punctuation, then greedily merge clauses while the line stays within + `max_line` characters and the previous clause did not end a sentence. + """ + clauses, start = [], 0 + for i, ch in enumerate(clean): + if ch in BREAK_PUNCT: + clauses.append((start, i + 1)) + start = i + 1 + if clean[start:].strip(): + clauses.append((start, len(clean))) + lines = [] + for s, e in clauses: + if lines and (e - lines[-1][0]) <= max_line and clean[lines[-1][1] - 1] not in SENT_END: + lines[-1] = (lines[-1][0], e) + else: + lines.append((s, e)) + out = [] + for s, e in lines: + while s < e and clean[s] == " ": + s += 1 + if s < e: + out.append((s, e)) + return out + + +def align(clean: str, words): + """Attach a char offset to each TTS word by scanning forward through the clean text.""" + ptr, aligned = 0, [] + for w in words: + idx = clean.find(w["w"], ptr) + if idx == -1 or idx - ptr > 12: + continue + aligned.append({**w, "s": idx, "e": idx + len(w["w"])}) + ptr = idx + len(w["w"]) + return aligned + + +def time_at(aligned, char_idx: int, fallback: float) -> float: + for w in aligned: + if w["e"] > char_idx: + return w["t"] + return fallback + + +MOCK_TOKEN_RE = re.compile(r"[A-Za-z0-9]+(?:[.'][A-Za-z0-9]+)*|\S") + + +def mock_synth(text: str, cps: float): + """Offline stand-in for edge-tts: every non-space character takes 1/cps seconds. + Punctuation takes time but is not reported as a word (like edge-tts). Returns (pcm, words).""" + step = 1.0 / cps + t, words = 0.0, [] + for m in MOCK_TOKEN_RE.finditer(text): + tok = m.group(0) + if tok[0].isalnum(): # CJK characters count as alnum; punctuation does not + words.append({"t": round(t, 3), "d": round(len(tok) * step, 3), "w": tok}) + t += len(tok) * step + return bytes(int(t * SR) * 2), words + + +def build_scene(sc, synth, pause: float, max_line: int): + """Synthesize one scene with `synth(text, voice_key) -> (pcm_s16le_mono, words)`. + Returns (pcm, partial timing dict without duration/audio).""" + clean, cues, segs = parse(sc["text"], pause) + pcm, aligned, nwords, offset, lines = bytearray(), [], 0, 0.0, [] + for sg in segs: + sub = clean[sg["s"]:sg["e"]] + raw, words = synth(sub, sg["v"]) + nwords += len(words) + seg_aligned = [{**w, "t": w["t"] + offset, "s": w["s"] + sg["s"], "e": w["e"] + sg["s"]} for w in align(sub, words)] + aligned += seg_aligned + seg_dur = len(raw) / 2 / SR + for s, e in split_lines(sub, max_line): + s, e = s + sg["s"], e + sg["s"] + lw = [{"t": round(w["t"], 3), "s": w["s"] - s, "e": w["e"] - s} for w in seg_aligned if s <= w["s"] < e] + st = lw[0]["t"] if lw else offset + ln = {"text": clean[s:e], "start": round(st, 3), "words": lw, "segEnd": round(offset + seg_dur, 3)} + if sg["v"]: + ln["q"] = sg["v"] + lines.append(ln) + pcm += raw + offset += seg_dur + if sg["pause"]: + pcm += bytes(int(sg["pause"] * SR) * 2) + offset += sg["pause"] + return pcm, {"clean": clean, "cues": cues, "aligned": aligned, "nwords": nwords, "lines": lines} + + +def finish_scene(sc, built, dur: float, audio: str): + lines = built["lines"] + for i, ln in enumerate(lines): + nxt = lines[i + 1]["start"] if i + 1 < len(lines) else round(dur, 3) + # don't hold a subtitle through a dramatic pause + ln["end"] = round(min(nxt, ln.pop("segEnd") + 0.25), 3) + return { + "chapter": sc["chapter"], + "duration": round(dur, 3), + "audio": audio, + "cues": {k: round(time_at(built["aligned"], v, dur), 3) for k, v in built["cues"].items()}, + "lines": lines, + "aligned": len(built["aligned"]), + "words": built["nwords"], + } + + +def write_wav(path: Path, pcm: bytes): + with wave.open(str(path), "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(SR) + wf.writeframes(bytes(pcm)) + + +# ───────────────────────── real edge-tts ───────────────────────── + +def edge_synth_factory(cfg): + import edge_tts # only needed for real synthesis + + def voice_of(key): + if key is None: + return cfg["voice"], cfg["rate"] + v = cfg["voices"][key] + return v["voice"], v["rate"] + + def decode(mp3_bytes): + return subprocess.run( + ["ffmpeg", "-loglevel", "error", "-i", "pipe:0", "-f", "s16le", "-ac", "1", "-ar", str(SR), "pipe:1"], + input=bytes(mp3_bytes), capture_output=True, check=True, + ).stdout + + async def synth_async(text, key): + voice, rate = voice_of(key) + for attempt in range(10): + try: + com = edge_tts.Communicate(text, voice, rate=rate, boundary="WordBoundary") + audio, words = bytearray(), [] + async for chunk in com.stream(): + if chunk["type"] == "audio": + audio.extend(chunk["data"]) + elif chunk["type"] == "WordBoundary": + words.append({"t": chunk["offset"] / 1e7, "d": chunk["duration"] / 1e7, "w": chunk["text"]}) + if not audio: + raise RuntimeError("empty audio") + return decode(audio), words + except Exception as e: # network flakiness + print(f" retry {attempt + 1}: {e}", file=sys.stderr) + await asyncio.sleep(3 + attempt * 3) + raise RuntimeError(f"TTS failed: {text[:20]}") + + return lambda text, key: asyncio.run(synth_async(text, key)) + + +def encode_mp3(path: Path, pcm: bytes): + subprocess.run( + ["ffmpeg", "-loglevel", "error", "-y", "-f", "s16le", "-ar", str(SR), "-ac", "1", "-i", "pipe:0", + "-c:a", "libmp3lame", "-b:a", "96k", str(path)], + input=bytes(pcm), check=True, + ) + out = subprocess.check_output(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", str(path)]) + return float(out.strip()) + + +# ───────────────────────── main ───────────────────────── + +def main(argv): + cfg = json.loads((ROOT / "kit.config.json").read_text(encoding="utf-8")) + script = json.loads((ROOT / "tts" / "script.json").read_text(encoding="utf-8")) + mock = "--mock" in argv + cps = float(cfg.get("mockCharsPerSec", 4.2)) + only = set() + it = iter(argv) + for a in it: + if a == "--cps": + cps = float(next(it)) + elif not a.startswith("--"): + only.add(a) + pause = float(cfg.get("pause", 0.75)) + max_line = int(cfg.get("maxLine", 20)) + + audio_dir = ROOT / "public" / "audio" + audio_dir.mkdir(parents=True, exist_ok=True) + tpath = ROOT / "src" / "timings.json" + old = json.loads(tpath.read_text(encoding="utf-8")) if tpath.exists() else {} + synth = (lambda text, key: mock_synth(text, cps)) if mock else edge_synth_factory(cfg) + + result = {} + for sc in script["scenes"]: + sid = sc["id"] + if only and sid not in only and sid in old: + result[sid] = old[sid] + continue + pcm, built = build_scene(sc, synth, pause, max_line) + if "end" in built["cues"]: + sys.exit(f"{sid}: cue name 'end' is reserved (tools use it for end of narration); rename it") + print(f"{sid}: {len(built['clean'])} chars{' (mock)' if mock else ''}", file=sys.stderr) + if mock: + path = audio_dir / f"{sid}.wav" + write_wav(path, pcm) + dur = len(pcm) / 2 / SR + else: + path = audio_dir / f"{sid}.mp3" + dur = encode_mp3(path, pcm) + result[sid] = finish_scene(sc, built, dur, f"audio/{path.name}") + + tpath.write_text(json.dumps(result, ensure_ascii=False, indent=1) + "\n", encoding="utf-8") + total = sum(v["duration"] for v in result.values()) + for sid, v in result.items(): + print(f"{sid} {v['duration']:6.1f}s aligned {v['aligned']}/{v['words']} cues {len(v['cues'])} {v['audio']}") + print(f"TOTAL narration {total:.1f}s ({total / 60:.1f} min) -> {tpath.relative_to(ROOT).as_posix()}") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tts/script.json b/tts/script.json new file mode 100644 index 0000000..4e7c806 --- /dev/null +++ b/tts/script.json @@ -0,0 +1,14 @@ +{ + "scenes": [ + { + "id": "s01", + "chapter": "第一场 · 旁白就是时间轴", + "text": "[[open]]做讲解视频,最磨人的往往不是画面,而是对时间。[[old]]旁白改一句,后面几十个动画就得重新对一遍。||[[idea]]这个模板换了个思路:让旁白自己当时间轴。[[mark]]在文稿里插一个方括号标记,合成语音时,每个字什么时候出口,都会被记下来。[[quote]]有位剪辑师说得好:<>||[[ask]]画面只需要问一句:这个词,第几帧出现?" + }, + { + "id": "s02", + "chapter": "第二场 · 三个函数", + "text": "[[api]]场景里只用三个函数。[[c]]c,取一个标记所在的帧。[[w]]w,取某个词出口的那一帧。[[rel]]rel,算从标记到这个词隔了多少帧。[[formula]]换算只有一行:开场留白,加上秒数乘以帧率。[[done]]改完文稿,重跑一次语音,动画自己跟上。" + } + ] +}