From a237a7c60455ba883e78f08609ef330b16b7d86f Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Thu, 5 Nov 2026 11:38:25 +0800 Subject: [PATCH] Add one-line score visualizer with 3D orb and MP4 export - Verovio single-system engraving, MuseScore audio/MIDI from the same source - React + Remotion player with live camera, colour and orb controls - Express pipeline, render queue and local H.264/AAC export - MIT license plus third-party notices for the reference skill --- .gitignore | 23 + LICENSE | 21 + README.md | 133 + THIRD_PARTY_NOTICES.md | 38 + index.html | 13 + package-lock.json | 6404 ++++++++++++++++++++++++++++++++++ package.json | 52 + public/demo/demo.musicxml | 267 ++ scripts/check-demo.ts | 71 + scripts/make-demo.mjs | 292 ++ scripts/props-from-job.ts | 44 + src/client/App.tsx | 1116 ++++++ src/client/ViewGizmo.tsx | 174 + src/client/api.ts | 75 + src/client/main.tsx | 12 + src/client/styles.css | 1501 ++++++++ src/remotion/MusicScroll.tsx | 716 ++++ src/remotion/Root.tsx | 58 + src/remotion/animation.ts | 148 + src/remotion/index.ts | 4 + src/server/audio.ts | 37 + src/server/config.ts | 57 + src/server/engrave.ts | 77 + src/server/events.ts | 285 ++ src/server/geometry.ts | 281 ++ src/server/index.ts | 301 ++ src/server/jobs.ts | 130 + src/server/midi.ts | 47 + src/server/musescore.ts | 159 + src/server/musicxml.ts | 665 ++++ src/server/pipeline.ts | 216 ++ src/server/queue.ts | 27 + src/server/render.ts | 161 + src/shared/scroll.ts | 115 + src/shared/types.ts | 174 + src/types/vendor.d.ts | 24 + tsconfig.json | 21 + vite.config.ts | 19 + 38 files changed, 13958 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/demo/demo.musicxml create mode 100644 scripts/check-demo.ts create mode 100644 scripts/make-demo.mjs create mode 100644 scripts/props-from-job.ts create mode 100644 src/client/App.tsx create mode 100644 src/client/ViewGizmo.tsx create mode 100644 src/client/api.ts create mode 100644 src/client/main.tsx create mode 100644 src/client/styles.css create mode 100644 src/remotion/MusicScroll.tsx create mode 100644 src/remotion/Root.tsx create mode 100644 src/remotion/animation.ts create mode 100644 src/remotion/index.ts create mode 100644 src/server/audio.ts create mode 100644 src/server/config.ts create mode 100644 src/server/engrave.ts create mode 100644 src/server/events.ts create mode 100644 src/server/geometry.ts create mode 100644 src/server/index.ts create mode 100644 src/server/jobs.ts create mode 100644 src/server/midi.ts create mode 100644 src/server/musescore.ts create mode 100644 src/server/musicxml.ts create mode 100644 src/server/pipeline.ts create mode 100644 src/server/queue.ts create mode 100644 src/server/render.ts create mode 100644 src/shared/scroll.ts create mode 100644 src/shared/types.ts create mode 100644 src/types/vendor.d.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbc38fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +*.tsbuildinfo + +# Runtime data: uploaded scores, MuseScore exports, rendered MP4s, check frames +data/ + +# Logs +*.log + +# Local environment +.env +.env.* +!.env.example + +# Editor / OS noise +.DS_Store +Thumbs.db +.idea/ +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d71024e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Music Visualizer contributors + +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..9f9b1a7 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# Music Visualizer + +把一份 MusicXML / MXL / MIDI 谱面变成横向滚动、单行不换页的音乐视频:谱面始终只有一行,发光小球沿真实符头跳跃,支持实时调参并用 Remotion 在本地导出 MP4。 + +视觉与运动参数参考了 [Dancing Notes](https://github.com/SyanChen7/dancing-notes)(MIT),本项目重写了排版、音频、对齐、滚动和 Web 端流水线。 + +## 功能 + +- **单行谱面**:Verovio `breaks: none` 生成一条超宽 system,按播放进度横向滚动到结尾,不换行、不翻页。 +- **音画同源**:默认调用本机 MuseScore 4,从同一份 MusicXML 生成 WAV 和 MIDI;动画时间来自 MIDI,谱面符头坐标来自 Verovio SVG。 +- **发光小球**:每个声部一颗球,沿符头连续跳跃;包含起跳、抛物线、落点光环、拖尾和纸面光晕。 +- **反复段**:按演奏顺序回到前面的谱面位置,滚动也会短暂回退。 +- **实时预览**:生成一次后,画面高度、滚动提前量、3D 视角、颜色、光晕、小球大小、标题显示等参数会立即反映在 Remotion Player 中,不需要重新跑流水线。 +- **3D 倾斜视角**:可调俯仰、滚转、偏航、透视距离和缩放,内置平视 / 轻斜 / 电影 / 强透视预设。 +- **导出**:使用 `@remotion/renderer` 在本地渲染 1080p H.264 + AAC MP4。导出前会把当前实时参数写回任务,保证视频与预览一致。 +- **输入**:MusicXML、MXL(压缩包)、MIDI。MIDI 会先由 MuseScore 识别成 MusicXML。 +- **可选音频**:可以上传自己的 WAV / MP3 / M4A / OGG / FLAC 替代自动合成音频;动画仍使用 MIDI/谱面时间轴。 + +## 环境要求 + +- Node.js 22+(推荐 24) +- MuseScore 4(自动合成音频、MIDI 转谱需要) +- Chrome 或 Edge(Remotion 渲染会优先使用本机浏览器,避免下载 Headless Shell) +- Windows PowerShell 下开发即可;其他平台同样适用 + +MuseScore 默认从常见安装路径查找,也可以显式指定: + +```powershell +$env:MUSESCORE_PATH = "C:\Program Files\MuseScore 4\bin\MuseScore4.exe" +``` + +## 开发 + +```powershell +npm install +npm run dev +``` + +- 前端: +- API: +- Vite 会把 `/api`、`/jobs`、`/demo` 代理到服务端。 + +## 生产运行 + +```powershell +npm run build +npm start +``` + +生产模式由 Express 同时提供 API 和 `dist/client` 静态文件,默认端口 `3001`。 + +可配置环境变量: + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| `PORT` | `3001` | 服务端口 | +| `PUBLIC_BASE_URL` | `http://127.0.0.1:${PORT}` | Remotion 渲染时访问 SVG / 音频的公开地址 | +| `DATA_DIR` | `data` | 任务与产物目录 | +| `MUSESCORE_PATH` | 自动查找 | MuseScore 4 可执行文件 | +| `REMOTION_BROWSER_EXECUTABLE` / `BROWSER_EXECUTABLE` | 自动查找 | 指定 Chrome / Edge 可执行文件 | + +## 使用流程 + +1. 拖入 MusicXML / MXL / MIDI;或者点“加载示例曲”。 +2. 可选填写标题、副标题、作曲者;留空时读取谱面信息。 +3. 选择画幅、帧率、谱面高度、3D 视角和颜色。 +4. 点“生成预览”,等待解析、MuseScore 合成、Verovio 排版和 MIDI 对齐完成。 +5. 在右侧 Remotion Player 中播放、拖动时间轴、实时调整左侧画面参数。 +6. 点“导出 MP4”,服务端会先保存当前参数,再在本地渲染视频并给出下载链接。 + +> 首次生成需要先跑完一次流水线;之后调整视觉参数都是即时预览。修改谱面/音频/标题文字本身需要重新生成,标题、副标题、作曲者的非空修改也会在导出前写入。 + +### 开发时改渲染代码 + +服务端第一次导出时会把 Remotion 项目打包一次,并缓存在内存里。缓存带源码指纹:改动 `src/remotion/`、`src/shared/` 或 `public/` 后再次导出会自动重新打包(日志会打印 `[render] sources changed, rebuilding the Remotion bundle`),不需要重启服务,也不会出现“预览是新的、导出还是旧画面”。 + +## 3D 视角 + +“3D 视角”把纸面、谱面、纸面光和 Canvas 小球放进同一个 CSS 3D 变换平面,因此小球和符头会一起倾斜、透视,不会错位。 + +- **俯仰角**:数值越大,谱面上方越向后倒。 +- **滚转角**:负值让谱面右侧抬高,形成参考视频里的斜线。 +- **偏航角**:左右转向;过大时透视会明显压缩。 +- **透视距离**:越小透视越强,越大越接近正交投影。 +- **视角缩放**:倾斜后用于补足画面;谱面被裁切或留白时优先调它。 + +## API + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `GET` | `/api/health` | MuseScore / 浏览器 / FFmpeg 状态 | +| `GET` | `/api/jobs` | 最近任务 | +| `POST` | `/api/jobs` | 创建任务,`multipart/form-data`:`score`、可选 `audio`、`settings`(JSON)、`title`、`subtitle`、`composer` | +| `GET` | `/api/jobs/:id` | 任务状态与预览数据 | +| `PATCH` | `/api/jobs/:id/settings` | 保存实时预览参数,并重新计算滚动关键帧 | +| `POST` | `/api/jobs/:id/render` | 加入渲染队列 | +| `GET` | `/api/jobs/:id/video` | 下载 MP4 | + +## 验证脚本 + +```powershell +npx tsx scripts/check-demo.ts +``` + +会对内置示例跑解析、Verovio 单行排版、MuseScore 导出、MIDI 映射,并输出 `data/check/score.json`。也可以直接用 Remotion CLI 验证单帧: + +```powershell +# 用某个已完成任务的完整设置生成单帧 props(含按当前画幅重算的滚动关键帧) +npx tsx scripts/props-from-job.ts data/check/p-job.json + +npx remotion still src/remotion/index.ts MusicScroll data/check/frame.png ` + --frame=300 --props=data/check/p-job.json ` + --browser-executable="C:\Program Files\Google\Chrome\Application\chrome.exe" +``` + +`` 是 `data/jobs/` 下的目录名,`props-from-job.ts` 会读取该任务的 `score.json` 与 `job.json`,因此导出的单帧和网页里看到的一致。 + +## 已知限制 + +- 反复、ending、D.C./D.S. 等跳转按谱面结构做近似展开;复杂跳转建议先在 MuseScore 中展开后导出 MusicXML。 +- 多声部/多谱表会渲染成同一条 system 上的多组五线谱,不是多行总谱。 +- 歌词、和弦符号、指法、tab 等会随 Verovio SVG 显示,但不参与小球坐标。 +- 上传音频时,动画时间仍来自 MIDI/谱面;录音速度与谱面差异过大时会渐进漂移,可用“音频偏移”做整体校准。 +- 视频最长 20 分钟,上传文件最大 30MB。 +- 渲染是 CPU 本地渲染,长曲会比较慢;可降低 fps 或谱面分辨率来加速。 + +## 许可与归属 + +本项目代码以 MIT 许可发布。 + +视觉与运动设计参考 **Dancing Notes**,Copyright (c) 2026 言说新事 (SyanChen7),MIT License。原版权声明与许可全文见 [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md)。 + +使用的第三方组件包括 Remotion、Verovio、MuseScore、React、Vite、zod 等,请分别遵守其许可证。Remotion 的商用许可要求请以官方说明为准。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..e51e680 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,38 @@ +# Third-party notices + +## Dancing Notes + +This project's visual language, ball motion and paper/light treatment are adapted from +[Dancing Notes](https://github.com/SyanChen7/dancing-notes). The original project is +licensed under the MIT License: + +```text +MIT License + +Copyright (c) 2026 言说新事 (SyanChen7) + +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. +``` + +## Other components + +- [Remotion](https://www.remotion.dev/) — video rendering. See its license and commercial terms. +- [Verovio](https://www.verovio.org/) — MusicXML engraving. +- [MuseScore](https://musescore.org/) — local audio/MIDI export. +- [React](https://react.dev/), [Vite](https://vite.dev/), [zod](https://zod.dev/), [Express](https://expressjs.com/) and other npm dependencies — see their respective licenses. diff --git a/index.html b/index.html new file mode 100644 index 0000000..1bdeec7 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Music Visualizer · 单行乐谱 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c9eb71f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6404 @@ +{ + "name": "music-visualizer", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "music-visualizer", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@remotion/bundler": "4.0.538", + "@remotion/cli": "4.0.538", + "@remotion/player": "4.0.538", + "@remotion/renderer": "4.0.538", + "@tonejs/midi": "^2.0.28", + "@xmldom/xmldom": "^0.8.11", + "cors": "^2.8.5", + "express": "^4.21.2", + "fflate": "^0.8.2", + "music-metadata": "^11.9.0", + "nanoid": "^5.1.6", + "react": "19.2.3", + "react-dom": "19.2.3", + "remotion": "4.0.538", + "svg-path-bounds": "^1.0.2", + "verovio": "6.3.0", + "zod": "4.5.4" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/multer": "^2.0.0", + "@types/node": "^24.7.2", + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@types/web": "0.0.166", + "@vitejs/plugin-react": "^5.0.4", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "multer": "^2.0.2", + "tsx": "^4.20.6", + "typescript": "5.9.3", + "vite": "^7.1.9" + } + }, + "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.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@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.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@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.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@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-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "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.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@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/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@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.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@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/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "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/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "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.62.0", + "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.62.0.tgz", + "integrity": "sha512-BtGaX3GVGgfQzbqeuqeKAOvHF2lhV70FO3T2orAr4Lr6YklRBOaggfRsKEp93cd6Y7DQab9GFYAuURa1DksCYw==", + "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.62.0", + "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.62.0.tgz", + "integrity": "sha512-ZxyElacW8o61mydIXVzYClv6E0ULkXcq9Aup+GQGpuaHKaZPF1y8S5gGrs4rFkr1s0bZ7iYGYPxHeGrg7J1yjg==", + "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.62.0", + "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.62.0.tgz", + "integrity": "sha512-72yrJXFO+qkFurWzVpkTu7e5yVF6qFB0mmyaMcg7Ztwz5a+bSvuitkaIY82FBk5vM7euCKzFuqVwOzH0YUCcLg==", + "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/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "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.538", + "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.538.tgz", + "integrity": "sha512-8chqxvsKXhh3Wo0UQufKr54lVp5NsJRm0PhFHcKv5CzLSyWASQD1wG4w9jWOMDCT1A7YPulFGqoDGlFWYaFR0A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/studio": "4.0.538", + "@remotion/studio-shared": "4.0.538", + "@remotion/timeline-utils": "4.0.538", + "@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.538", + "style-loader": "4.0.0", + "webpack": "5.105.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/bundler/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/@remotion/bundler/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/@remotion/captions": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/captions/-/captions-4.0.538.tgz", + "integrity": "sha512-ADDnP8QxU7hDtjaE2PuS6UeyHRtfvkomXBKmK8M9LlC98+iuNEAeMGw02T4p15wDzEER44QDPG09K9kKBnWqUw==", + "license": "MIT" + }, + "node_modules/@remotion/cli": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.538.tgz", + "integrity": "sha512-4JN4ywRT2gj+HrpFrFvl7SdyUoRBZgN61jnvxvaLUdWZClchIvx5/Fwk9/dXahxf//yMd5d6vEdJcOFsyZw90w==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@babel/parser": "7.24.1", + "@remotion/bundler": "4.0.538", + "@remotion/codemods": "4.0.538", + "@remotion/media-utils": "4.0.538", + "@remotion/player": "4.0.538", + "@remotion/renderer": "4.0.538", + "@remotion/studio": "4.0.538", + "@remotion/studio-server": "4.0.538", + "@remotion/studio-shared": "4.0.538", + "dotenv": "17.3.1", + "mediabunny": "1.62.0", + "minimist": "1.2.6", + "prompts": "2.4.2", + "remotion": "4.0.538", + "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.538", + "resolved": "https://registry.npmjs.org/@remotion/codemods/-/codemods-4.0.538.tgz", + "integrity": "sha512-m+oASCDQa1+22cANXp97ZwZ+ZvnuWZaSFqEFPpVF6RajssJRcn+npr9UCFleTJrx7Fm2bOVVdmFtPTuXdT8+Bw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@babel/types": "7.24.0", + "@remotion/studio-protocol": "4.0.538", + "@remotion/studio-shared": "4.0.538", + "ast-types": "0.16.1", + "recast": "0.23.21", + "remotion": "4.0.538" + } + }, + "node_modules/@remotion/compositor-darwin-arm64": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.538.tgz", + "integrity": "sha512-VV4f68T9UGLjuumo1BycjKwgPFr5b6yLkznFp5NufScK8wIe+w34rRQ6ZMO2SEEwr+q97FkgMXHu0fgB8UTQmg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-darwin-x64": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.538.tgz", + "integrity": "sha512-PSKUGDfZuvkpUsxlrUAeXz49gwJ8DIlArIIxldO35h5aPF10s8IY92vVz4EXuT/ETfGDsb9X9lgmlsy51bbb6g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-gnu": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.538.tgz", + "integrity": "sha512-QSGe7eoL6r4ksOYYWrt6iypGLwtT25fuMMuZtcdl5ij/IgQQuYkeD6TE8/Nvy3qhCJc/tNazcuC8OonBtLiXvQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-musl": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.538.tgz", + "integrity": "sha512-ALEWbRQeKYXBYCA4d8mjuyahRBziTgZFnkWjINfat2N0YxH1zOOxKqdHevcYFgBYAS4KgVd5PZ/+zCUGSyRb3A==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.538.tgz", + "integrity": "sha512-i+u3ea1MN1BkBSp5BK1YGqVBEJPS9AxxszHeUSJ4zYtldlu1lEAnATLPWO0zs5jA6P0Vtap+2rDcFuKKAlDxyA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-musl": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.538.tgz", + "integrity": "sha512-ickPY224j3K3f2WiooleExbMtE/G/upt3c4tA5xqVu09STUE3fEBMqZly7PoFa/byfVk8ifVUVs0BuyGaG1oVA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-win32-x64-msvc": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.538.tgz", + "integrity": "sha512-eQdKMPAPBodquLi/QbHP7irvIFstHkksh6in3jaJ0w397qeIHiRUCB3DWA8foRqyGPykibt+B2kQg9l3xxIcog==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@remotion/elevenlabs": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/elevenlabs/-/elevenlabs-4.0.538.tgz", + "integrity": "sha512-K06HrD+dKz25i31sRbxniNzIjGgzPEeiLLwmCyuVZuat+PQ9stb5EmIuWue/n++YnisT4bgFt6QNyoSdQEbByw==", + "license": "MIT", + "dependencies": { + "@remotion/captions": "4.0.538", + "zod": "4.5.4" + } + }, + "node_modules/@remotion/licensing": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.538.tgz", + "integrity": "sha512-vm8mpeZCowrn0OI/Y7KRmeAVuXiK6ylj0xk0utMbwlsLNB3ywLg3nyO0H4pD+a7fDn9FId3y283xFkJ6XymtfQ==", + "license": "MIT" + }, + "node_modules/@remotion/media": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/media/-/media-4.0.538.tgz", + "integrity": "sha512-+TViwM5SWbj0nPLHgCNcuvatnlezOZHHV8BcTG+uFKwMHTxxmEcj40Ow0vM0H5/MAP54B7i/qlmV74YZqLXfDw==", + "dependencies": { + "mediabunny": "1.62.0", + "remotion": "4.0.538", + "zod": "4.5.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/media-utils": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.538.tgz", + "integrity": "sha512-Rfjg5P+lkioXz4ip+0SQlzzaOspu4/jP8NWOwVi7ICeJPJ3m0ZaU7TKjLTVA1pzX9bWV4vwoigALNWn+cMiHHg==", + "license": "MIT", + "dependencies": { + "mediabunny": "1.62.0", + "remotion": "4.0.538" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/openai-whisper": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/openai-whisper/-/openai-whisper-4.0.538.tgz", + "integrity": "sha512-+AVrT5WwkRhN1er2fd4cR52EZDfGx+riiw65mnOf8oQKnbAT+1OhmoH6WIwPKX1qOZxhTwvGY5m2xthlAOSW1w==", + "license": "MIT", + "dependencies": { + "@remotion/captions": "4.0.538", + "zod": "4.5.4" + } + }, + "node_modules/@remotion/paths": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/paths/-/paths-4.0.538.tgz", + "integrity": "sha512-GN/6cpxQn6ssvhb4Ph1RRQLrhQTtZYmJysZ+PjHi1Zn4WzC07akkgNj+BTqtdGWsUKFj1cyGvzzcZswbPLLc6w==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.538" + } + }, + "node_modules/@remotion/player": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.538.tgz", + "integrity": "sha512-HYvGtDD2cG8/EICy8kleIqZJIHqRXNsJjETv0BS4n+Yrlo95oZvrzbfhGn0GbP8b3ucO4EkehNkw+qpVqG9M7Q==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "remotion": "4.0.538" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.538.tgz", + "integrity": "sha512-d4xzVMHqw/ajV0WEjyuFNt3UY6Tg5Aj0iABSxcwBiVI7YwsJQ9RGoB80VS95Sa9choYQfSSRJGFSQT53X/qjRg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/licensing": "4.0.538", + "@remotion/streaming": "4.0.538", + "execa": "5.1.1", + "remotion": "4.0.538", + "source-map": "0.8.0", + "ws": "8.21.0" + }, + "optionalDependencies": { + "@remotion/compositor-darwin-arm64": "4.0.538", + "@remotion/compositor-darwin-x64": "4.0.538", + "@remotion/compositor-linux-arm64-gnu": "4.0.538", + "@remotion/compositor-linux-arm64-musl": "4.0.538", + "@remotion/compositor-linux-x64-gnu": "4.0.538", + "@remotion/compositor-linux-x64-musl": "4.0.538", + "@remotion/compositor-win32-x64-msvc": "4.0.538" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/sdk": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/sdk/-/sdk-4.0.538.tgz", + "integrity": "sha512-ypTWi4pwWFlb/ZETuSMn6YYFpAfUmbJa6bUmqZlj2iQ3V5W2Pfi1lYl/El4xOl3GEYVErJutIf0JJYI3AE7McQ==", + "license": "Remotion License", + "dependencies": { + "@remotion/player": "4.0.538", + "@remotion/studio-shared": "4.0.538", + "remotion": "4.0.538" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/streaming": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.538.tgz", + "integrity": "sha512-9zoJ59eBMRwufh9eXp4R1i8X+J6BZ4xIfjGFUmiDKPBYh6N1GMRlOwFPZAKyW/QLJZMVtUkz0jTqyYrrAGrWhg==", + "license": "MIT" + }, + "node_modules/@remotion/studio": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.538.tgz", + "integrity": "sha512-lO+rEYRuS+0yKW65pqy8szDTjmy23x4RImXR3JAiHOfv7KLDY8NoB3XY+lLiDHAVseL2bZHAG2ZIjTotCQvyQQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@remotion/captions": "4.0.538", + "@remotion/codemods": "4.0.538", + "@remotion/elevenlabs": "4.0.538", + "@remotion/media": "4.0.538", + "@remotion/media-utils": "4.0.538", + "@remotion/openai-whisper": "4.0.538", + "@remotion/paths": "4.0.538", + "@remotion/player": "4.0.538", + "@remotion/renderer": "4.0.538", + "@remotion/sdk": "4.0.538", + "@remotion/studio-protocol": "4.0.538", + "@remotion/studio-shared": "4.0.538", + "@remotion/timeline-utils": "4.0.538", + "@remotion/web-renderer": "4.0.538", + "@remotion/zod-types": "4.0.538", + "@tanstack/react-virtual": "3.14.9", + "mediabunny": "1.62.0", + "memfs": "3.4.3", + "open": "8.4.2", + "prismjs": "1.30.0", + "remotion": "4.0.538", + "semver": "7.5.3", + "zod": "4.5.4" + }, + "peerDependencies": { + "@remotion/video-matting": "4.0.538", + "@remotion/whisper-webgpu": "4.0.538", + "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.538", + "resolved": "https://registry.npmjs.org/@remotion/studio-protocol/-/studio-protocol-4.0.538.tgz", + "integrity": "sha512-QycNTOjcnUD6MPFlkJ0N6ZAw6kq450aAKSqKSHlsLjuMEGDF3yZEzIaDnF2QTuJc02WnNEBV2o8mx4XRNda9Zw==", + "license": "Remotion License", + "dependencies": { + "zod": "4.5.4" + } + }, + "node_modules/@remotion/studio-server": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.538.tgz", + "integrity": "sha512-skvVMWsGJZNXFFagAY+jq4/7QNN/UlcfSMdaF1wDQSyZ7Bb6KL/R6udApikrPkHLFeExVmQiB8dHkSjaSwp+1g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@babel/types": "7.24.0", + "@remotion/bundler": "4.0.538", + "@remotion/codemods": "4.0.538", + "@remotion/renderer": "4.0.538", + "@remotion/studio-protocol": "4.0.538", + "@remotion/studio-shared": "4.0.538", + "@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.538", + "semver": "7.5.3", + "zod": "4.5.4" + } + }, + "node_modules/@remotion/studio-shared": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.538.tgz", + "integrity": "sha512-FpKJ/43J86ay/4ZhEFa2SAu8bmrzN8IMDjAD0qFPgf3SNkgd9RX2vhzth6RH+RyOggOFGpHrdcPlI6EYpv6ByQ==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.538" + } + }, + "node_modules/@remotion/timeline-utils": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/timeline-utils/-/timeline-utils-4.0.538.tgz", + "integrity": "sha512-sGRUxrXSdxkd78C8ChQNApoEiYKgOPFO6+cezOLB+gqk2updtreR3sjau9RzmsPO/2C5r7x8x1vDyTSb3U/geQ==", + "license": "MIT", + "dependencies": { + "mediabunny": "1.62.0", + "remotion": "4.0.538" + } + }, + "node_modules/@remotion/web-renderer": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.538.tgz", + "integrity": "sha512-dIpIHPrVCiy9Nz5FvUWOejn3ZYgcAVdQzkdVL64R0aCEqx8BnGxTNYmKz+xm3H3fQE+q2HKCJoR6dYMgTayS2Q==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@mediabunny/aac-encoder": "1.62.0", + "@mediabunny/flac-encoder": "1.62.0", + "@mediabunny/mp3-encoder": "1.62.0", + "@remotion/licensing": "4.0.538", + "mediabunny": "1.62.0", + "remotion": "4.0.538" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@remotion/zod-types": { + "version": "4.0.538", + "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.538.tgz", + "integrity": "sha512-/2+zn7/QGGMFhr30ccEwhc1GYpyMa+TsVOMdjS9+7SylfXm2ULAxIHHhJDJYrxTES/ROx5EI4UdD+ugoEHQXEg==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.538" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.65.0.tgz", + "integrity": "sha512-t2AnVba25baFdq7gczzS13JyVcdubLVbFX1X04mxrggDrTQPUXNv3XM0k9H1hr5tqvg7AVvtQ720QHAj0iYqhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.65.0.tgz", + "integrity": "sha512-dNONUaEwPGhl/nuO2KV8VABWlcjYyU6KtZGN8dQv5OS5JU7JSCxPijVNKXbRJd0iuTzTiJ4TjPt/4bbvDRNiMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.65.0.tgz", + "integrity": "sha512-zWQqUH+9zavr2Bxuzfpuv2SIF3X31cr58TvSQC9o1GhpOvkqRwr60GkZdQcudyT/BEgdpdSafjgd2I13MbTL7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.65.0.tgz", + "integrity": "sha512-RQYreBFWtO4//hIuOaZkYUnF/lPU4lz35dveDimDshWCJvVisVtJ/shmT9bkXK8wFJY+FJF89X6F3Cq1qI3LCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.65.0.tgz", + "integrity": "sha512-A0NfMj6Cuh/9BE5k3awJKv6HoxFVXIcLAb6SZqAMoxUHPA1ZNNK4RsGuUPD/vL7nEAm9LG+ZzXWWTrnC0XtkpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.65.0.tgz", + "integrity": "sha512-BI6/H7q3aP0I7S7GOrjyxTjgEJlSnovXE38+bdxBpuKN6z+d//uF+T+VQ3Q7ltJL/71p17ufartARX9Ja5gOGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.65.0.tgz", + "integrity": "sha512-W1vUprf09RWaVWnsFP1vlUkEWJzTZlpObOEAYZXt9Fq0UVAOYEkeGr7wBX7McKqqIQskXdBnpiOMgwPujordgA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.65.0.tgz", + "integrity": "sha512-s8QTcZWgrl9haFzEGIqyE5BWyo89pFDGLMBFZ752WYxk8DJwNU/O1X6pZgPw829d9/sm6CCHyrMP3aurt76p1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.65.0.tgz", + "integrity": "sha512-ke1f6dLRzkqveyjjF2fkEwQYZvxHtNNwjegY8nJrwcO0Bdqr8CzFO/pKE0Bns13cx0qz2Ba2Y0Hs3dSwMdnHxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.65.0.tgz", + "integrity": "sha512-926SMeUcy4y18Rz/w2aJiUKOnyAnm6czg72bXkQEaeemsll+Wr0hnsjb2BGRpHvWgwAMYy5ACJCotbvfLCfj0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.65.0.tgz", + "integrity": "sha512-6T7JNRP1hc4hqF2dUsEoVLOt9K8tmODoCLldtQRYnrddGGQY8Gqe2pM/qMMUo7EMowpP7oa2N95k0/8sYK/ipw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.65.0.tgz", + "integrity": "sha512-SImGFl1Je5kpyAgOCPONVT7h6ASV8mAUwGRjY7eZvVjPg//PfHOXgI+P241RF/+HqsA4PP3qD527ysWFKNQyhQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.65.0.tgz", + "integrity": "sha512-XeDTbmPF1Yu5asfXGPrMkWoxTDhgryQbmw5Q1+R582O51VNCk23J6f2pDrsAp5YkneBVBwtExx6ZGOb9D2aQ6A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.65.0.tgz", + "integrity": "sha512-VS55JUI/1nsljmV/jfqNkLZXFw8uMO8nNlfoHmSevNf6fO3neuhyDvx9dv3c/Ytt8r/431SL8Qax1dpYvOMZTg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.65.0.tgz", + "integrity": "sha512-Vblw84TbX7RS4la1Y4+yQ7aore/oSyaNtFbppuBr5Vs/4LfBeXKq9dIfoExw6zOvRERqd2rRAjnZRaMhH/0x5w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.65.0.tgz", + "integrity": "sha512-7+gHjtONvKgwR9InTKFaLq53f6YzBJkgeMRBENdWTsQr+spahGqQm8HF5aXpq8i4JaRrOqYyv6Zn1WltDTxaZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.65.0.tgz", + "integrity": "sha512-KvNDC0J6s4vVHhtJ6KrDXU0Er0au/hmRIBq/+1maCV8TseGupeKizq0bWcrbp2HE1T8xoknvN0Q/KZ/YF0qX3w==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.65.0.tgz", + "integrity": "sha512-GExdD15WPvu/x/drELjVam7vX8EsPgvTMOa/iJL/p/4m/oOqzbb7JXZg8fXR4hlXtg/sF2zyTZ3bR0AEtEMlhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.65.0.tgz", + "integrity": "sha512-8sT9PemIZfIMED4Vd4KGoOXVDj5CMSWNcGdbheRUx9TYS03yOWdT2Q0lXPyEyNBB9YTilwewIQKNIBk6Nxi0Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.65.0.tgz", + "integrity": "sha512-pO6Ehi0jo5yE0R6PltKhJhv9mlIiyzBJk5N5NUF2OF6xtVKI+1bdXxj/rb0jGXy3hck1ELVT0QPY3tzkAm360w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.65.0.tgz", + "integrity": "sha512-vo7N3/+cjjPDYr8vtxC+qSNgWa9sDvJ7g8N8R5SWqFY4WiNH1ULcAU70SKVyHeiIZOYYwNP9pTipmhN5sl1NPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.65.0.tgz", + "integrity": "sha512-ArwvgFAbjKcDM7qqotZ4OVYf+UMT9xH2YzkCzK4+AuZSDihMlz1Ekoe4INwBVXzn2mnJzuIYOcSx07iTBijGRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.65.0.tgz", + "integrity": "sha512-QGMmK4/cxKE/36FoLMSHQBCyymv/eFTnsCvvsBF0rgmOvVbvX78VWZRerL3dHa77f4pM6Xcc2iGDhJ10w/svKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.65.0.tgz", + "integrity": "sha512-XfKYFWHIPs4HPODIMsGF1IKlS3H1e3WokUZIgpmn4+uNnZBlnRDDHEylNyazbQXwhMKZre68OjtRYbKqb5+j8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.65.0.tgz", + "integrity": "sha512-wPWi++K4Bs1VR4XihOhfGi4IGX6Cub7FlerTf8X2v+2BtDHddgSqWCXwGD+h2yAe4/niIhNDwAltioOC+wEogQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "musl" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "musl" + ], + "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/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tonejs/midi": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@tonejs/midi/-/midi-2.0.28.tgz", + "integrity": "sha512-RII6YpInPsOZ5t3Si/20QKpNqB1lZ2OCFJSOzJxz38YdY/3zqDr3uaml4JuCWkdixuPqP1/TBnXzhQ39csyoVg==", + "license": "MIT", + "dependencies": { + "array-flatten": "^3.0.0", + "midi-file": "^1.2.2" + } + }, + "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/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/babel__traverse/node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "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/multer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.3.0.tgz", + "integrity": "sha512-i7STIm9V4K2MPH4ZYMtrZAwNxs3kglk2LgleaTuB9pUXgADBcQYQuYMd7+6xgTIxfoggIkFA/DkkvycdZpIARA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/web": { + "version": "0.0.166", + "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.166.tgz", + "integrity": "sha512-qvY/TzK1WuxfeACL3Zzw+gMivGiIynRKH98nLET7ACzTRTX8CWMA6LQJ9WayIHvTBU1JeFCBRIBjsxhGz4TfHQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.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/@xmldom/xmldom": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "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/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "dev": true, + "license": "MIT" + }, + "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/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "license": "MIT" + }, + "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.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.33.tgz", + "integrity": "sha512-yiVKDX+qCMRKBE084lXvq9EcwFUhKZ2nU8TK1EQQuOgPNR/lmbGxXOeDdlGUqC2Zz11Cs9rk1d/8NfgBmlkt0w==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.9", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.9.tgz", + "integrity": "sha512-FISIQxV5u91Y9gvuFq3q/tA/qTqLJn3AhRcEvyozyhJTh5OBFFtv827AdHU7Rbyct4r+Ba1/99oFa1Y3yuFsdw==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "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.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" + }, + "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/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "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/concurrently": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.5.tgz", + "integrity": "sha512-YMZlvWrTPQEhjKp60XSmMGt60+pffbv6DyRnOMvVO98+J7aF8w8Ui9FhdcOwvSixc/kyR6SoVvmejcn/2lZBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.12.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.464", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", + "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/express": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz", + "integrity": "sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.13", + "proxy-addr": "~2.0.7", + "qs": "~6.16.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-svg-path": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-svg-path/-/is-svg-path-1.0.2.tgz", + "integrity": "sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==", + "license": "MIT" + }, + "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/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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-2.0.0.tgz", + "integrity": "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mediabunny": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.62.0.tgz", + "integrity": "sha512-D8E8BDxjCyqyrGj074jLRQNgilDW5izJu8o9pg7THMYMIZBL1Fao5hOtdDPDfA+nSnk68tMa8IaCJmz8hSpJpw==", + "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-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/midi-file": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/midi-file/-/midi-file-1.2.4.tgz", + "integrity": "sha512-B5SnBC6i2bwJIXTY9MElIydJwAmnKx+r5eJ1jknTLetzLflEl0GWveuBB6ACrQpecSRkOB6fhTx1PwXk2BVxnA==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/minimizer-webpack-plugin": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.13.1.tgz", + "integrity": "sha512-1mhAfvachWYYOm8tpmany5NLuvP+F2wxZWBSX8DC0uFS2wJpM9q3rWgN9wBooAH9d5V12wX0BjyJe4fMjHMarQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.6.0", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@napi-rs/image": { + "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 + }, + "imagemin": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "sharp": { + "optional": true + }, + "svgo": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "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/multer": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.5.0.tgz", + "integrity": "sha512-BsqOzYGVq49JcW2IzoSAxeKYRevb+dGGPPWleOR8Lrlk4igwxu1/9pwSXlByhCb7MVZDrNJFC0IBP5/O5nLYRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/music-metadata": { + "version": "11.16.1", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.16.1.tgz", + "integrity": "sha512-uR/mHK6eyfl8h3jVCobF5b38Mfg1IDJMiBxIVbmm5F+G1OXEFcUpQHveO84tJBAqvY93GCRY2R4kDyx7s06Rug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.2", + "@tokenizer/token": "^0.3.0", + "content-type": "^2.1.0", + "debug": "^4.4.3", + "file-type": "^21.3.4", + "media-typer": "^2.0.0", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/music-metadata/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz", + "integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "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/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "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.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "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.7", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.7.tgz", + "integrity": "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==", + "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/postcss/node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "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/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/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/qs": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "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.538", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.538.tgz", + "integrity": "sha512-HA9/+3GOfNxBtT+iFsqmzGtegtE1M3qra3N/iBiXv5oRX+1q0HsFTjcFxT9Ljmmodd5huVZLKLK0ymvYsecCaw==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.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/rollup": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.65.0.tgz", + "integrity": "sha512-QVCvQUTGDN6JRwi3jVGCY3H4aTHvi9rX0LyoBMkGUvdh/Z6O51CzcLz85BWZaTCDGhl6ySGqYZs+xI1jmPqGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.65.0", + "@rollup/rollup-android-arm64": "4.65.0", + "@rollup/rollup-darwin-arm64": "4.65.0", + "@rollup/rollup-darwin-x64": "4.65.0", + "@rollup/rollup-freebsd-arm64": "4.65.0", + "@rollup/rollup-freebsd-x64": "4.65.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.65.0", + "@rollup/rollup-linux-arm-musleabihf": "4.65.0", + "@rollup/rollup-linux-arm64-gnu": "4.65.0", + "@rollup/rollup-linux-arm64-musl": "4.65.0", + "@rollup/rollup-linux-loong64-gnu": "4.65.0", + "@rollup/rollup-linux-loong64-musl": "4.65.0", + "@rollup/rollup-linux-ppc64-gnu": "4.65.0", + "@rollup/rollup-linux-ppc64-musl": "4.65.0", + "@rollup/rollup-linux-riscv64-gnu": "4.65.0", + "@rollup/rollup-linux-riscv64-musl": "4.65.0", + "@rollup/rollup-linux-s390x-gnu": "4.65.0", + "@rollup/rollup-linux-x64-gnu": "4.65.0", + "@rollup/rollup-linux-x64-musl": "4.65.0", + "@rollup/rollup-openbsd-x64": "4.65.0", + "@rollup/rollup-openharmony-arm64": "4.65.0", + "@rollup/rollup-win32-arm64-msvc": "4.65.0", + "@rollup/rollup-win32-ia32-msvc": "4.65.0", + "@rollup/rollup-win32-x64-gnu": "4.65.0", + "@rollup/rollup-win32-x64-msvc": "4.65.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.8.1.tgz", + "integrity": "sha512-x39pYlJPyH7Uge5QO64B9XGP9zLDc9WNmiQZc+CE3IzZPvMu3lHOuAMGpYNoPixEL7MSWlGhYTg5UlLSL8G6iw==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "ajv-keywords": "^5.1.0" + }, + "bin": { + "schema-utils": "bin/schema-utils.js" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.112.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "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/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "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/shell-quote": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/strtok3": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.6.2.tgz", + "integrity": "sha512-BkdixFFUt7r7LsX6aGNPVyYMLjW+M1USLlKPNbZsrZEmLFh5tGqhdfckKo6xgsZIJqdDD0CZGkeCJJwNWQIKQA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "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-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, + "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/svg-path-bounds": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz", + "integrity": "sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==", + "license": "MIT", + "dependencies": { + "abs-svg-path": "^0.1.1", + "is-svg-path": "^1.0.1", + "normalize-svg-path": "^1.0.0", + "parse-svg-path": "^0.1.2" + } + }, + "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/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/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "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/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verovio": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/verovio/-/verovio-6.3.0.tgz", + "integrity": "sha512-1p6WhP/w3WrAhPVDTNUDH9UVPAZOnHnZVTWxclE8haHccVI/q8c+99vLH6rRgL9MuYaFR9HF4UJD6MyW2CfMwg==", + "license": "LGPL-3.0-or-later", + "engines": { + "node": ">= 14.18.0" + } + }, + "node_modules/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.6.3.tgz", + "integrity": "sha512-s3NZy0QpTY9qGqzc0wtz4qI28z3+5qKbbAec4MV4G9N3CfgZ9wgvQRr0cl2jh/tqkWG1NWHkmVMHZpCamev5Wg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.112.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.2.tgz", + "integrity": "sha512-B2tp1LI9D8GVpPDqprdt12ukTkXXahj9MzqSWJq4faSPgOUM7CdZaGAB6HrL/8E6UwbBpdj4vzWDbKoFnVoU6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@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", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.26.0", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.13.0", + "schema-utils": "^4.8.1", + "tapable": "^2.3.0", + "watchpack": "^2.6.3", + "webpack-sources": "^3.6.0" + }, + "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/webpack/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", + "peer": true + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "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/win-guid": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", + "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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..3cad529 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "music-visualizer", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "One-line scrolling music score visualizer with ball animation and MP4 export.", + "license": "MIT", + "scripts": { + "dev": "concurrently -k -n server,client -c blue,magenta \"npm:dev:server\" \"npm:dev:client\"", + "dev:server": "tsx watch src/server/index.ts", + "dev:client": "vite", + "build": "tsc --noEmit && vite build", + "start": "cross-env NODE_ENV=production tsx src/server/index.ts", + "typecheck": "tsc --noEmit", + "remotion:studio": "remotion studio src/remotion/index.ts" + }, + "dependencies": { + "@remotion/bundler": "4.0.538", + "@remotion/cli": "4.0.538", + "@remotion/player": "4.0.538", + "@remotion/renderer": "4.0.538", + "@tonejs/midi": "^2.0.28", + "@xmldom/xmldom": "^0.8.11", + "cors": "^2.8.5", + "express": "^4.21.2", + "fflate": "^0.8.2", + "music-metadata": "^11.9.0", + "nanoid": "^5.1.6", + "react": "19.2.3", + "react-dom": "19.2.3", + "remotion": "4.0.538", + "svg-path-bounds": "^1.0.2", + "verovio": "6.3.0", + "zod": "4.5.4" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/multer": "^2.0.0", + "@types/node": "^24.7.2", + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@types/web": "0.0.166", + "@vitejs/plugin-react": "^5.0.4", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "multer": "^2.0.2", + "tsx": "^4.20.6", + "typescript": "5.9.3", + "vite": "^7.1.9" + } +} diff --git a/public/demo/demo.musicxml b/public/demo/demo.musicxml new file mode 100644 index 0000000..a61a880 --- /dev/null +++ b/public/demo/demo.musicxml @@ -0,0 +1,267 @@ + + + + Morning Window + + Demo · Original + music-visualizer demo generator + + Piano + + + + 4 + 0 + + 2 + G2 + F4 + + + quarter96 + + + + C541quarter1 + E521eighth1 + G521eighth1 + A541quarter1 + G541quarter1 + 16 + C325eighth2 + G325eighth2 + E425eighth2 + G325eighth2 + C325eighth2 + G325eighth2 + E425eighth2 + G325eighth2 + + + F541quarter1 + E521eighth1 + D521eighth1 + C581half1 + E581half1 + G581half1 + 16 + F325eighth2 + C425eighth2 + A325eighth2 + C425eighth2 + F325eighth2 + C425eighth2 + A325eighth2 + C425eighth2 + + + 41quarter1 + G521eighth1 + A521eighth1 + G541quarter1 + E541quarter1 + 16 + G325eighth2 + D425eighth2 + B325eighth2 + D425eighth2 + G325eighth2 + D425eighth2 + B325eighth2 + D425eighth2 + + + D541quarter1 + E541quarter1 + C581half1 + 16 + C325eighth2 + G325eighth2 + E425eighth2 + G325eighth2 + C325eighth2 + G325eighth2 + E425eighth2 + G325eighth2 + + + + E541quarter1 + G521eighth1 + A521eighth1 + C641quarter1 + B541quarter1 + 16 + A325eighth2 + E425eighth2 + C425eighth2 + E425eighth2 + A325eighth2 + E425eighth2 + C425eighth2 + E425eighth2 + + + A541quarter1 + G521eighth1 + E521eighth1 + D541quarter1 + E541quarter1 + 16 + F325eighth2 + C425eighth2 + A325eighth2 + C425eighth2 + F325eighth2 + C425eighth2 + A325eighth2 + C425eighth2 + + + G541quarter1 + E521eighth1 + D521eighth1 + C581half1 + 16 + G325eighth2 + D425eighth2 + B325eighth2 + D425eighth2 + G325eighth2 + D425eighth2 + B325eighth2 + D425eighth2 + + + D541quarter1 + E521eighth1 + F521eighth1 + G581half1 + 16 + C325eighth2 + G325eighth2 + E425eighth2 + G325eighth2 + C325eighth2 + G325eighth2 + E425eighth2 + G325eighth2 + + + + quarter104 + + + G581half1 + A541quarter1 + G541quarter1 + 16 + A325eighth2 + E425eighth2 + C425eighth2 + E425eighth2 + A325eighth2 + E425eighth2 + C425eighth2 + E425eighth2 + + + A51eighth1 + G541quarter1 + E541quarter1 + D581half1 + 16 + F325eighth2 + C425eighth2 + A325eighth2 + C425eighth2 + F325eighth2 + C425eighth2 + A325eighth2 + C425eighth2 + + + E541quarter1 + G521eighth1 + A521eighth1 + C681half1 + 16 + G325eighth2 + D425eighth2 + B325eighth2 + D425eighth2 + G325eighth2 + D425eighth2 + B325eighth2 + D425eighth2 + + + B541quarter1 + A541quarter1 + G581half1 + 16 + C325eighth2 + G325eighth2 + E425eighth2 + G325eighth2 + C325eighth2 + G325eighth2 + E425eighth2 + G325eighth2 + + + C641quarter1 + B521eighth1 + A521eighth1 + G541quarter1 + E541quarter1 + 16 + A325eighth2 + E425eighth2 + C425eighth2 + E425eighth2 + A325eighth2 + E425eighth2 + C425eighth2 + E425eighth2 + + + F541quarter1 + E521eighth1 + D521eighth1 + C581half1 + 16 + F325eighth2 + C425eighth2 + A325eighth2 + C425eighth2 + F325eighth2 + C425eighth2 + A325eighth2 + C425eighth2 + + + D541quarter1 + E541quarter1 + G581half1 + 16 + G325eighth2 + D425eighth2 + B325eighth2 + D425eighth2 + G325eighth2 + D425eighth2 + B325eighth2 + D425eighth2 + + + C5161whole1 + E5161whole1 + G5161whole1 + 16 + C3165whole2 + E3165whole2 + G3165whole2 + + + diff --git a/scripts/check-demo.ts b/scripts/check-demo.ts new file mode 100644 index 0000000..21425d3 --- /dev/null +++ b/scripts/check-demo.ts @@ -0,0 +1,71 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import {parseMusicXml, buildPerformanceOrder} from '../src/server/musicxml.js'; +import {engraveOneLine} from '../src/server/engrave.js'; +import {exportAudioAndMidi} from '../src/server/musescore.js'; +import {parseMidiFile} from '../src/server/midi.js'; +import {buildEvents} from '../src/server/events.js'; +import {readAudioDuration} from '../src/server/audio.js'; +import {DEFAULT_SETTINGS} from '../src/shared/types.js'; + +const demo = path.resolve('public/demo/demo.musicxml'); +const outDir = path.resolve('data/check'); +fs.mkdirSync(outDir, {recursive: true}); + +const parsed = parseMusicXml(fs.readFileSync(demo)); +console.log('title', parsed.title, '| composer', parsed.composer); +console.log('parts', parsed.parts.length, 'measures', parsed.measures.length, 'notes', parsed.allNotes.length); +console.log('unsupported', parsed.unsupported); +console.log('warnings', parsed.warnings.slice(0, 5)); + +const performance = buildPerformanceOrder(parsed.measures, []); +console.log('performance order length', performance.order.length); +console.log('order', performance.order.map((i) => i + 1).join(', ')); + +const engraved = await engraveOneLine(parsed.annotatedXml); +fs.writeFileSync(path.join(outDir, 'score.svg'), engraved.svg); +console.log('svg', engraved.geometry.width, 'x', engraved.geometry.height, 'geometry notes', engraved.geometry.noteCount); +console.log('measure times', engraved.measureStartMs.size, 'note times', engraved.noteTimes.size); + +console.time('musescore'); +const {wav, midi} = await exportAudioAndMidi(demo, outDir); +console.timeEnd('musescore'); +console.log('wav', fs.statSync(wav).size, 'midi', fs.statSync(midi).size); + +const audioDuration = await readAudioDuration(wav); +const parsedMidi = parseMidiFile(midi); +console.log('audioDuration', audioDuration, 'midi notes', parsedMidi.notes.length, 'midi duration', parsedMidi.duration); + +const built = buildEvents({ + parsed, + engrave: engraved, + midi: parsedMidi, + performanceOrder: performance.order, + audioDuration, + settings: DEFAULT_SETTINGS, +}); +console.log('events', built.events.length, 'scroll keys', built.scroll.length); +console.log('meta', JSON.stringify({...built.meta, warnings: built.meta.warnings.slice(0, 8)}, null, 2)); +console.log('first events', built.events.slice(0, 8)); +console.log('last events', built.events.slice(-5)); +console.log('scroll head', built.scroll.slice(0, 6)); +console.log('scroll tail', built.scroll.slice(-4)); + +fs.writeFileSync( + path.join(outDir, 'score.json'), + JSON.stringify( + { + version: 1, + meta: built.meta, + events: built.events, + activations: built.activations, + scroll: built.scroll, + svgUrl: '/jobs/check/score.svg', + audioUrl: '/jobs/check/audio.wav', + settings: DEFAULT_SETTINGS, + }, + null, + 2, + ), +); +console.log('wrote', path.join(outDir, 'score.json')); diff --git a/scripts/make-demo.mjs b/scripts/make-demo.mjs new file mode 100644 index 0000000..eeb5180 --- /dev/null +++ b/scripts/make-demo.mjs @@ -0,0 +1,292 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const divisions = 4; +const typeFor = (d) => + ({16: 'whole', 8: 'half', 4: 'quarter', 2: 'eighth', 1: '16th'})[d] ?? 'quarter'; + +let xml = ` + + + Morning Window + + Demo · Original + music-visualizer demo generator + + Piano + +`; + +function note(step, octave, duration, opts = {}) { + const {chord = false, staff = 1, voice = '1', tie, grace = false} = opts; + const parts = []; + if (grace) parts.push(''); + if (chord) parts.push(''); + parts.push(`${step}${octave}`); + if (!grace) parts.push(`${duration}`); + if (tie === 'start') parts.push(''); + if (tie === 'stop') parts.push(''); + parts.push(`${voice}`); + if (!grace) parts.push(`${typeFor(duration)}`); + else parts.push('eighth'); + if (tie) { + parts.push(``); + } + parts.push(`${staff}`); + return ` ${parts.join('')}\n`; +} + +function rest(duration, opts = {}) { + const {staff = 1, voice = '1'} = opts; + return ` ${duration}${voice}${typeFor(duration)}${staff}\n`; +} + +function leftArpeggio(notes) { + // 8 eighth notes = 16 divisions + let out = ''; + for (const [step, octave] of notes) { + out += note(step, octave, 2, {staff: 2, voice: '5'}); + } + return out; +} + +const measures = [ + // 1 + { + right: + note('C', 5, 4) + + note('E', 5, 2) + + note('G', 5, 2) + + note('A', 5, 4) + + note('G', 5, 4), + left: leftArpeggio([ + ['C', 3], ['G', 3], ['E', 4], ['G', 3], + ['C', 3], ['G', 3], ['E', 4], ['G', 3], + ]), + }, + // 2 + { + right: + note('F', 5, 4) + + note('E', 5, 2) + + note('D', 5, 2) + + note('C', 5, 8) + + note('E', 5, 8, {chord: true}) + + note('G', 5, 8, {chord: true}), + left: leftArpeggio([ + ['F', 3], ['C', 4], ['A', 3], ['C', 4], + ['F', 3], ['C', 4], ['A', 3], ['C', 4], + ]), + }, + // 3 + { + right: + rest(4) + + note('G', 5, 2) + + note('A', 5, 2) + + note('G', 5, 4) + + note('E', 5, 4), + left: leftArpeggio([ + ['G', 3], ['D', 4], ['B', 3], ['D', 4], + ['G', 3], ['D', 4], ['B', 3], ['D', 4], + ]), + }, + // 4 + { + right: + note('D', 5, 4) + + note('E', 5, 4) + + note('C', 5, 8), + left: leftArpeggio([ + ['C', 3], ['G', 3], ['E', 4], ['G', 3], + ['C', 3], ['G', 3], ['E', 4], ['G', 3], + ]), + }, + // 5 + { + right: + note('E', 5, 4) + + note('G', 5, 2) + + note('A', 5, 2) + + note('C', 6, 4) + + note('B', 5, 4), + left: leftArpeggio([ + ['A', 3], ['E', 4], ['C', 4], ['E', 4], + ['A', 3], ['E', 4], ['C', 4], ['E', 4], + ]), + }, + // 6 + { + right: + note('A', 5, 4) + + note('G', 5, 2) + + note('E', 5, 2) + + note('D', 5, 4) + + note('E', 5, 4), + left: leftArpeggio([ + ['F', 3], ['C', 4], ['A', 3], ['C', 4], + ['F', 3], ['C', 4], ['A', 3], ['C', 4], + ]), + }, + // 7 + { + right: + note('G', 5, 4) + + note('E', 5, 2) + + note('D', 5, 2) + + note('C', 5, 8), + left: leftArpeggio([ + ['G', 3], ['D', 4], ['B', 3], ['D', 4], + ['G', 3], ['D', 4], ['B', 3], ['D', 4], + ]), + }, + // 8 + { + right: + note('D', 5, 4) + + note('E', 5, 2) + + note('F', 5, 2) + + note('G', 5, 8, {tie: 'start'}), + left: leftArpeggio([ + ['C', 3], ['G', 3], ['E', 4], ['G', 3], + ['C', 3], ['G', 3], ['E', 4], ['G', 3], + ]), + }, + // 9 + { + right: + note('G', 5, 8, {tie: 'stop'}) + + note('A', 5, 4) + + note('G', 5, 4), + left: leftArpeggio([ + ['A', 3], ['E', 4], ['C', 4], ['E', 4], + ['A', 3], ['E', 4], ['C', 4], ['E', 4], + ]), + }, + // 10 + { + right: + note('A', 5, 2, {grace: true}) + + note('G', 5, 4) + + note('E', 5, 4) + + note('D', 5, 8), + left: leftArpeggio([ + ['F', 3], ['C', 4], ['A', 3], ['C', 4], + ['F', 3], ['C', 4], ['A', 3], ['C', 4], + ]), + }, + // 11 + { + right: + note('E', 5, 4) + + note('G', 5, 2) + + note('A', 5, 2) + + note('C', 6, 8), + left: leftArpeggio([ + ['G', 3], ['D', 4], ['B', 3], ['D', 4], + ['G', 3], ['D', 4], ['B', 3], ['D', 4], + ]), + }, + // 12 + { + right: + note('B', 5, 4) + + note('A', 5, 4) + + note('G', 5, 8), + left: leftArpeggio([ + ['C', 3], ['G', 3], ['E', 4], ['G', 3], + ['C', 3], ['G', 3], ['E', 4], ['G', 3], + ]), + }, + // 13 + { + right: + note('C', 6, 4) + + note('B', 5, 2) + + note('A', 5, 2) + + note('G', 5, 4) + + note('E', 5, 4), + left: leftArpeggio([ + ['A', 3], ['E', 4], ['C', 4], ['E', 4], + ['A', 3], ['E', 4], ['C', 4], ['E', 4], + ]), + }, + // 14 + { + right: + note('F', 5, 4) + + note('E', 5, 2) + + note('D', 5, 2) + + note('C', 5, 8), + left: leftArpeggio([ + ['F', 3], ['C', 4], ['A', 3], ['C', 4], + ['F', 3], ['C', 4], ['A', 3], ['C', 4], + ]), + }, + // 15 + { + right: + note('D', 5, 4) + + note('E', 5, 4) + + note('G', 5, 8), + left: leftArpeggio([ + ['G', 3], ['D', 4], ['B', 3], ['D', 4], + ['G', 3], ['D', 4], ['B', 3], ['D', 4], + ]), + }, + // 16 + { + right: + note('C', 5, 16) + + note('E', 5, 16, {chord: true}) + + note('G', 5, 16, {chord: true}), + left: + note('C', 3, 16, {staff: 2, voice: '5'}) + + note('E', 3, 16, {staff: 2, voice: '5', chord: true}) + + note('G', 3, 16, {staff: 2, voice: '5', chord: true}), + }, +]; + +measures.forEach((measure, index) => { + const number = index + 1; + xml += ` \n`; + if (number === 1) { + xml += ` + ${divisions} + 0 + + 2 + G2 + F4 + + + quarter96 + + + +`; + } + if (number === 9) { + xml += ` + quarter104 + + +`; + } + xml += measure.right; + xml += ` 16\n`; + xml += measure.left; + if (number === 4) { + xml += ` \n`; + } + xml += ` \n`; +}); + +xml += ` + +`; + +const out = path.resolve('public/demo/demo.musicxml'); +fs.mkdirSync(path.dirname(out), {recursive: true}); +fs.writeFileSync(out, xml, 'utf8'); +console.log(`Wrote ${out} (${xml.length} bytes, ${measures.length} measures)`); diff --git a/scripts/props-from-job.ts b/scripts/props-from-job.ts new file mode 100644 index 0000000..f001b73 --- /dev/null +++ b/scripts/props-from-job.ts @@ -0,0 +1,44 @@ +import fs from 'node:fs'; +import {buildScrollKeys} from '../src/shared/scroll.js'; +import type {RenderData, VisualSettings} from '../src/shared/types.js'; + +/** Builds Remotion still props for a finished job, using exactly its saved settings. */ +const id = process.argv[2]; +if (!id) throw new Error('usage: tsx scripts/props-from-job.ts [outFile]'); +const out = process.argv[3] ?? 'data/check/p-job.json'; + +const data = JSON.parse(fs.readFileSync(`data/jobs/${id}/score.json`, 'utf8')) as RenderData; +const job = JSON.parse(fs.readFileSync(`data/jobs/${id}/job.json`, 'utf8')) as { + settings?: Partial; +}; +const settings: VisualSettings = {...data.settings, ...(job.settings ?? {})}; + +const displayHeight = settings.height * settings.scoreHeightRatio; +const displayScale = displayHeight / Math.max(1, data.meta.svgHeight); +const viewWidthScore = settings.width / displayScale; +const maxLeft = Math.max(0, data.meta.svgWidth - viewWidthScore); +const scroll = buildScrollKeys( + data.events, + settings.scrollLead, + viewWidthScore, + maxLeft, + data.meta.musicDuration, +); + +fs.writeFileSync( + out, + JSON.stringify( + { + data: { + ...data, + settings, + scroll, + svgUrl: `http://127.0.0.1:3001/jobs/${id}/score.svg`, + audioUrl: `http://127.0.0.1:3001/jobs/${id}/audio.wav`, + }, + }, + null, + 2, + ), +); +console.log(`${out}: ${settings.width}x${settings.height} tiltX=${settings.tiltX} tiltZ=${settings.tiltZ}`); diff --git a/src/client/App.tsx b/src/client/App.tsx new file mode 100644 index 0000000..fbf13a1 --- /dev/null +++ b/src/client/App.tsx @@ -0,0 +1,1116 @@ +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {Player} from '@remotion/player'; +import { + DEFAULT_SETTINGS, + RESOLUTION_PRESETS, + type HealthResponse, + type JobPublic, + type VisualSettings, +} from '../shared/types'; +import {MusicScroll} from '../remotion/MusicScroll'; +import {buildScrollKeys} from '../shared/scroll'; +import {ViewGizmo} from './ViewGizmo'; +import { + createJob, + fetchDemoFile, + fetchHealth, + getJob, + listJobs, + startRender, + updateJobSettings, +} from './api'; + +const STAGES = [ + {id: 'reading', label: '解析谱面'}, + {id: 'synthesizing', label: '合成音频'}, + {id: 'engraving', label: '单行排版'}, + {id: 'mapping', label: '对齐音符'}, + {id: 'building', label: '生成预览'}, + {id: 'rendering', label: '渲染视频'}, +] as const; + +const STATUS_LABEL: Record = { + queued: '排队中', + running: '生成中', + ready: '可预览', + rendering: '渲染中', + done: '已完成', + error: '失败', +}; + +const CAMERA_PRESETS = [ + { + id: 'flat', + label: '平视', + values: { + tiltX: 0, + tiltY: 0, + tiltZ: 0, + perspective: 1800, + tiltZoom: 1, + }, + }, + { + id: 'soft', + label: '轻斜', + values: { + tiltX: 16, + tiltY: -2, + tiltZ: -6, + perspective: 1600, + tiltZoom: 1.18, + }, + }, + { + id: 'cinema', + label: '电影', + values: { + tiltX: 32, + tiltY: -6, + tiltZ: -10, + perspective: 1200, + tiltZoom: 1.45, + }, + }, + { + id: 'reference', + label: '参考图', + values: { + tiltX: 50, + tiltY: -10, + tiltZ: -14, + perspective: 900, + tiltZoom: 1.75, + paperAlpha: 0, + paperColor: '#4b4c4e', + scoreColor: '#0e0e0f', + background: '#3a3b3d', + }, + }, +] as const; + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +function formatTime(seconds: number): string { + if (!Number.isFinite(seconds) || seconds <= 0) return '0:00'; + const total = Math.round(seconds); + const m = Math.floor(total / 60); + const s = total % 60; + return `${m}:${String(s).padStart(2, '0')}`; +} + +const Field: React.FC<{label: string; hint?: string; children: React.ReactNode}> = ({label, hint, children}) => ( + +); + +const Section: React.FC<{title: string; aside?: React.ReactNode; children: React.ReactNode}> = ({ + title, + aside, + children, +}) => ( +
+
+

{title}

+ {aside} +
+ {children} +
+); + +const Slider: React.FC<{ + label: string; + value: number; + min: number; + max: number; + step: number; + display: string; + onChange: (value: number) => void; +}> = ({label, value, min, max, step, display, onChange}) => ( + +); + +export const App: React.FC = () => { + const [health, setHealth] = useState(null); + const [healthError, setHealthError] = useState(null); + const [scoreFile, setScoreFile] = useState(null); + const [audioFile, setAudioFile] = useState(null); + const [settings, setSettings] = useState(DEFAULT_SETTINGS); + const [title, setTitle] = useState(''); + const [subtitle, setSubtitle] = useState(''); + const [composer, setComposer] = useState(''); + const [jobId, setJobId] = useState(null); + const [job, setJob] = useState(null); + const [jobs, setJobs] = useState([]); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const [renderBusy, setRenderBusy] = useState(false); + const [dragActive, setDragActive] = useState(false); + const [pollKey, setPollKey] = useState(0); + const scoreInput = useRef(null); + const audioInput = useRef(null); + const loadedJobRef = useRef(null); + + const refreshJobs = useCallback(async () => { + try { + setJobs(await listJobs()); + } catch { + // History is non-critical. + } + }, []); + + useEffect(() => { + fetchHealth() + .then((next) => { + setHealth(next); + setHealthError(null); + }) + .catch((cause: unknown) => setHealthError(cause instanceof Error ? cause.message : String(cause))); + void refreshJobs(); + }, [refreshJobs]); + + useEffect(() => { + if (!jobId) return; + let alive = true; + let timer: number | undefined; + const tick = async () => { + try { + const next = await getJob(jobId); + if (!alive) return; + setJob(next); + if (next.status === 'queued' || next.status === 'running' || next.status === 'rendering') { + timer = window.setTimeout(tick, 900); + } else { + void refreshJobs(); + } + } catch (cause) { + if (alive) setError(cause instanceof Error ? cause.message : String(cause)); + } + }; + void tick(); + return () => { + alive = false; + if (timer) window.clearTimeout(timer); + }; + }, [jobId, refreshJobs, pollKey]); + + // When a job's preview first arrives, adopt its stored settings so the form + // and the live player start from the same values. + useEffect(() => { + if (!job?.data || loadedJobRef.current === job.id) return; + loadedJobRef.current = job.id; + setSettings({...DEFAULT_SETTINGS, ...job.data.settings}); + }, [job?.id, job?.data]); + + const submitJob = useCallback( + async ( + score: File, + audio: File | null, + metadata?: {title?: string; subtitle?: string; composer?: string}, + ) => { + setBusy(true); + setError(null); + try { + const meta = metadata ?? { + title: title.trim() || undefined, + subtitle: subtitle.trim() || undefined, + composer: composer.trim() || undefined, + }; + const {id} = await createJob({ + score, + audio, + settings, + ...meta, + }); + setJobId(id); + setJob(null); + } catch (cause) { + setError(cause instanceof Error ? cause.message : String(cause)); + } finally { + setBusy(false); + } + }, + [settings, title, subtitle, composer], + ); + + const handleGenerate = () => { + if (!scoreFile) { + setError('请先选择一个 MusicXML / MXL / MIDI 谱面文件。'); + return; + } + void submitJob(scoreFile, audioFile); + }; + + const handleDemo = async () => { + setBusy(true); + setError(null); + try { + const demo = await fetchDemoFile(); + setScoreFile(demo); + setTitle(''); + setSubtitle(''); + setComposer(''); + await submitJob(demo, null, {}); + } catch (cause) { + setError(cause instanceof Error ? cause.message : String(cause)); + setBusy(false); + } + }; + + const updateSetting = (key: K, value: VisualSettings[K]) => { + setSettings((current) => ({...current, [key]: value})); + }; + + const applyCameraPreset = (preset: Partial) => { + setSettings((current) => ({...current, ...preset})); + }; + + const handleFiles = (files: FileList | null) => { + if (!files?.length) return; + for (const file of Array.from(files)) { + const lower = file.name.toLowerCase(); + if (/\.(wav|mp3|m4a|aac|ogg|oga|flac|opus)$/.test(lower)) setAudioFile(file); + else setScoreFile(file); + } + }; + + const data = job?.data; + + // Everything the player needs is derived on the client, so visual changes + // (including camera tilt) apply instantly without re-running the pipeline. + const liveData = useMemo(() => { + if (!data) return null; + const liveSettings: VisualSettings = {...DEFAULT_SETTINGS, ...data.settings, ...settings}; + const displayHeight = liveSettings.height * liveSettings.scoreHeightRatio; + const displayScale = displayHeight / Math.max(1, data.meta.svgHeight); + const viewWidthScore = liveSettings.width / displayScale; + const maxLeft = Math.max(0, data.meta.svgWidth - viewWidthScore); + const meta = {...data.meta}; + if (title.trim()) meta.title = title.trim(); + if (subtitle.trim()) meta.subtitle = subtitle.trim(); + if (composer.trim()) meta.composer = composer.trim(); + return { + ...data, + meta, + settings: liveSettings, + scroll: buildScrollKeys( + data.events, + liveSettings.scrollLead, + viewWidthScore, + maxLeft, + data.meta.musicDuration, + ), + }; + }, [data, settings, title, subtitle, composer]); + + const handleRender = async () => { + if (!job || !liveData) return; + setRenderBusy(true); + setError(null); + try { + // Persist what the player is showing right now, so the MP4 matches the + // live preview even if parameters were changed after the job was made. + const updated = await updateJobSettings(job.id, liveData.settings, { + title: title.trim() || undefined, + subtitle: subtitle.trim() || undefined, + composer: composer.trim() || undefined, + }); + setJob(updated); + await startRender(job.id); + setPollKey((value) => value + 1); + } catch (cause) { + setError(cause instanceof Error ? cause.message : String(cause)); + } finally { + setRenderBusy(false); + } + }; + + const previewSettings = liveData?.settings ?? settings; + const isPortrait = previewSettings.height > previewSettings.width; + const playerDuration = liveData + ? Math.max( + 1, + Math.ceil( + (previewSettings.introSeconds + + Math.max(liveData.meta.musicDuration, liveData.meta.audioDuration) + + previewSettings.outroSeconds) * + previewSettings.fps, + ), + ) + : 1; + const warnings = data?.meta.warnings ?? job?.warnings ?? []; + const unsupported = data?.meta.unsupported ?? job?.unsupported ?? []; + const stageIndex = job ? STAGES.findIndex((stage) => stage.id === job.stage) : -1; + const resolutionId = + RESOLUTION_PRESETS.find((preset) => preset.width === settings.width && preset.height === settings.height)?.id ?? + 'landscape'; + const isWorking = job?.status === 'queued' || job?.status === 'running' || job?.status === 'rendering'; + + const recentJobs = useMemo(() => jobs.slice(0, 6), [jobs]); + + return ( +
+
+
+
+ + + +
+
+
Music Visualizer
+
单行乐谱 · 发光小球 · 视频导出
+
+
+
+ {health ? ( + + + {health.musescore.available ? 'MuseScore 已就绪' : 'MuseScore 未找到'} + + ) : ( + {healthError ? '服务未连接' : '检查环境…'} + )} + 本地渲染 +
+
+ +
+ + +
+ {job?.error ? ( +
+
+ 任务失败 +

{job.error}

+
+
+ ) : null} + + {!job && !busy ? ( +
+
+
+ {Array.from({length: 5}).map((_, index) => ( + + ))} + ♪ + ♫ + ♪ + + +
+
+
+ ONE LINE · ENDLESS SCROLL +

把一份谱面变成横向滚动的音乐视频

+

+ 上传 MusicXML(推荐)或 MIDI,自动生成单行谱面、合成音频、计算符头坐标与滚动路径, + 再导出 1080p MP4。小球会沿着真实符头连续跳跃,谱面始终只有一行。 +

+
+ + +
+
+ MusicXML / MXL / MIDI + MuseScore 自动音频 + 16:9 / 9:16 / 1:1 + Remotion 本地导出 +
+
+
+ ) : null} + + {busy && !job ? ( +
+
+ 正在创建任务… + 解析谱面、合成音频、排版单行谱面 +
+ ) : null} + + {job && !data && !job.error ? ( +
+
+
+ {STATUS_LABEL[job.status] ?? job.status} +

{job.message}

+
+ {job.progress}% +
+
+
+
+
    + {STAGES.map((stage, index) => { + const state = stageIndex < 0 ? 'pending' : index < stageIndex ? 'done' : index === stageIndex ? 'active' : 'pending'; + return ( +
  1. + + {stage.label} +
  2. + ); + })} +
+

+ 音频合成和排版在服务端完成;长曲可能需要几十秒到几分钟。这个页面会自动刷新。 +

+
+ ) : null} + + {job && liveData ? ( + <> +
+
+
+ + PREVIEW 实时 + +

{liveData.meta.title || 'Untitled'}

+

+ {liveData.meta.composer ? `${liveData.meta.composer} · ` : ''} + {formatTime(liveData.meta.musicDuration)} 音乐 · {previewSettings.width}×{previewSettings.height} ·{' '} + {previewSettings.fps}fps +

+
+
+ {job.status === 'done' && job.videoUrl ? ( + + 下载 MP4 + + ) : ( + + )} +
+
+
+
+ +
+
+ setSettings((current) => ({...current, ...patch}))} + /> + +
+
+ {job.status === 'rendering' ? ( +
+
+ {job.message} + {job.progress}% +
+
+
+
+
+ ) : null} + {job.status === 'done' ? ( +
+ 视频已生成。左侧参数可实时调整,改完重新导出即可匹配新画面。 + {job.videoUrl ? ( + + 再次下载 + + ) : null} +
+ ) : null} +
+ +
+
+ 小节 + {liveData.meta.measureCount} +
+
+ 谱面音符 + {liveData.meta.scoreNoteCount} +
+
+ MIDI 对齐 + + {liveData.meta.mappedNoteCount} + / {liveData.meta.scoreNoteCount} + +
+
+ 音乐时长 + {formatTime(liveData.meta.musicDuration)} +
+
+ 音频时长 + {formatTime(liveData.meta.audioDuration)} +
+
+ 谱面宽度 + {Math.round(liveData.meta.svgWidth)} + SVG 单位 +
+
+ + {warnings.length || unsupported.length ? ( +
0}> + + 生成说明 + {unsupported.length ? {unsupported.length} 项限制 : null} + {warnings.length ? {warnings.length} 条提示 : null} + + {unsupported.length ? ( +
+

当前版本未完整支持

+
    + {unsupported.map((item) => ( +
  • {item}
  • + ))} +
+
+ ) : null} + {warnings.length ? ( +
+

提示

+
    + {warnings.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ ) : null} +
+ ) : null} + + ) : null} +
+
+
+ ); +}; diff --git a/src/client/ViewGizmo.tsx b/src/client/ViewGizmo.tsx new file mode 100644 index 0000000..633e884 --- /dev/null +++ b/src/client/ViewGizmo.tsx @@ -0,0 +1,174 @@ +import React, {useMemo, useRef, useState} from 'react'; + +type Camera = { + tiltX: number; + tiltY: number; + tiltZ: number; +}; + +type Props = Camera & { + onChange: (patch: Partial) => void; +}; + +const AXES = [ + {key: 'x', label: 'X', color: '#ff7b8a', vector: [1, 0, 0] as const}, + {key: 'y', label: 'Y', color: '#8fe3c8', vector: [0, 1, 0] as const}, + {key: 'z', label: 'Z', color: '#7aa2ff', vector: [0, 0, 1] as const}, +]; + +const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); + +function project( + vector: readonly [number, number, number], + camera: Camera, + radius: number, +): {x: number; y: number; z: number} { + const rx = (camera.tiltX * Math.PI) / 180; + const ry = (camera.tiltY * Math.PI) / 180; + const rz = (camera.tiltZ * Math.PI) / 180; + const [x, y, z] = vector; + + // CSS applies rotateX * rotateY * rotateZ, so match that order. + const x1 = x * Math.cos(rz) - y * Math.sin(rz); + const y1 = x * Math.sin(rz) + y * Math.cos(rz); + const z1 = z; + + const x2 = x1 * Math.cos(ry) + z1 * Math.sin(ry); + const z2 = -x1 * Math.sin(ry) + z1 * Math.cos(ry); + const y2 = y1; + + const y3 = y2 * Math.cos(rx) - z2 * Math.sin(rx); + const z3 = y2 * Math.sin(rx) + z2 * Math.cos(rx); + const x3 = x2; + + const scale = 1 / Math.max(0.55, 1 - z3 * 0.28); + return {x: x3 * radius * scale, y: y3 * radius * scale, z: z3}; +} + +export const ViewGizmo: React.FC = ({tiltX, tiltY, tiltZ, onChange}) => { + const [dragging, setDragging] = useState(false); + const drag = useRef<{ + mode: 'orbit' | 'roll'; + startX: number; + startY: number; + baseX: number; + baseY: number; + baseZ: number; + angle: number; + } | null>(null); + + const axes = useMemo( + () => + AXES.map((axis) => ({ + ...axis, + ...project(axis.vector, {tiltX, tiltY, tiltZ}, 38), + })).sort((a, b) => a.z - b.z), + [tiltX, tiltY, tiltZ], + ); + + const pointerPosition = (event: React.PointerEvent) => { + const rect = event.currentTarget.getBoundingClientRect(); + return { + x: ((event.clientX - rect.left) / rect.width) * 140 - 70, + y: ((event.clientY - rect.top) / rect.height) * 140 - 70, + }; + }; + + const handlePointerDown = (event: React.PointerEvent) => { + if (event.pointerType === 'mouse' && event.button !== 0) return; + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.setPointerCapture(event.pointerId); + const {x, y} = pointerPosition(event); + const distance = Math.hypot(x, y); + drag.current = { + mode: distance > 46 ? 'roll' : 'orbit', + startX: event.clientX, + startY: event.clientY, + baseX: tiltX, + baseY: tiltY, + baseZ: tiltZ, + angle: (Math.atan2(y, x) * 180) / Math.PI, + }; + setDragging(true); + }; + + const handlePointerMove = (event: React.PointerEvent) => { + const current = drag.current; + if (!current) return; + event.preventDefault(); + event.stopPropagation(); + if (current.mode === 'roll') { + const {x, y} = pointerPosition(event); + const angle = (Math.atan2(y, x) * 180) / Math.PI; + let delta = angle - current.angle; + if (delta > 180) delta -= 360; + if (delta < -180) delta += 360; + onChange({tiltZ: clamp(current.baseZ + delta, -60, 60)}); + return; + } + const dx = event.clientX - current.startX; + const dy = event.clientY - current.startY; + onChange({ + tiltY: clamp(current.baseY + dx * 0.36, -60, 60), + tiltX: clamp(current.baseX - dy * 0.36, -85, 85), + }); + }; + + const handlePointerUp = (event: React.PointerEvent) => { + event.stopPropagation(); + drag.current = null; + setDragging(false); + }; + + return ( + event.stopPropagation()} + onClick={(event) => event.stopPropagation()} + > + + + + {axes.map((axis) => ( + + 0 ? 2.4 : 1.8} + strokeOpacity={axis.z > 0 ? 0.95 : 0.55} + /> + 0 ? 0.95 : 0.55} + /> + 0 ? 1 : 0.72} + > + {axis.label} + + + ))} + + ); +}; diff --git a/src/client/api.ts b/src/client/api.ts new file mode 100644 index 0000000..55823d1 --- /dev/null +++ b/src/client/api.ts @@ -0,0 +1,75 @@ +import type {HealthResponse, JobPublic, VisualSettings} from '../shared/types.js'; + +async function parseError(response: Response): Promise { + try { + const body = (await response.json()) as {error?: string}; + return body.error ?? `请求失败(HTTP ${response.status})`; + } catch { + return `请求失败(HTTP ${response.status})`; + } +} + +export async function fetchHealth(): Promise { + const response = await fetch('/api/health'); + if (!response.ok) throw new Error(await parseError(response)); + return (await response.json()) as HealthResponse; +} + +export async function listJobs(): Promise { + const response = await fetch('/api/jobs'); + if (!response.ok) throw new Error(await parseError(response)); + const body = (await response.json()) as {jobs: JobPublic[]}; + return body.jobs ?? []; +} + +export async function getJob(id: string): Promise { + const response = await fetch(`/api/jobs/${encodeURIComponent(id)}`); + if (!response.ok) throw new Error(await parseError(response)); + return (await response.json()) as JobPublic; +} + +export async function createJob(input: { + score: File; + audio?: File | null; + settings: VisualSettings; + title?: string; + subtitle?: string; + composer?: string; +}): Promise<{id: string}> { + const form = new FormData(); + form.append('score', input.score, input.score.name); + if (input.audio) form.append('audio', input.audio, input.audio.name); + form.append('settings', JSON.stringify(input.settings)); + if (input.title !== undefined) form.append('title', input.title); + if (input.subtitle !== undefined) form.append('subtitle', input.subtitle); + if (input.composer !== undefined) form.append('composer', input.composer); + const response = await fetch('/api/jobs', {method: 'POST', body: form}); + if (!response.ok) throw new Error(await parseError(response)); + return (await response.json()) as {id: string}; +} + +export async function startRender(id: string): Promise { + const response = await fetch(`/api/jobs/${encodeURIComponent(id)}/render`, {method: 'POST'}); + if (!response.ok) throw new Error(await parseError(response)); +} + +export async function updateJobSettings( + id: string, + settings: VisualSettings, + metadata?: {title?: string; subtitle?: string; composer?: string}, +): Promise { + const response = await fetch(`/api/jobs/${encodeURIComponent(id)}/settings`, { + method: 'PATCH', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({settings, metadata}), + }); + if (!response.ok) throw new Error(await parseError(response)); + return (await response.json()) as JobPublic; +} + +export async function fetchDemoFile(): Promise { + const response = await fetch('/demo/demo.musicxml'); + if (!response.ok) throw new Error('示例曲目加载失败。'); + const text = await response.text(); + return new File([text], 'demo.musicxml', {type: 'application/vnd.recordare.musicxml+xml'}); +} diff --git a/src/client/main.tsx b/src/client/main.tsx new file mode 100644 index 0000000..4ed3b79 --- /dev/null +++ b/src/client/main.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import {createRoot} from 'react-dom/client'; +import {App} from './App'; +import './styles.css'; + +const container = document.getElementById('root'); +if (!container) throw new Error('Missing #root'); +createRoot(container).render( + + + , +); diff --git a/src/client/styles.css b/src/client/styles.css new file mode 100644 index 0000000..c2e47b1 --- /dev/null +++ b/src/client/styles.css @@ -0,0 +1,1501 @@ +:root { + color-scheme: dark; + --bg: #0b0a0e; + --bg-2: #111015; + --panel: rgba(255, 255, 255, 0.035); + --panel-strong: rgba(255, 255, 255, 0.055); + --border: rgba(255, 255, 255, 0.09); + --border-strong: rgba(255, 255, 255, 0.16); + --text: #ece6dc; + --muted: #9d9489; + --muted-2: #756d64; + --violet: #bda0ff; + --violet-soft: rgba(189, 160, 255, 0.16); + --amber: #ffbc6d; + --amber-soft: rgba(255, 188, 109, 0.15); + --danger: #ff7f8f; + --ok: #8fe3c8; + --radius: 16px; + --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; + --font-serif: Georgia, "Times New Roman", "Noto Serif SC", "Songti SC", serif; +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + min-height: 100%; +} + +body { + margin: 0; + background: + radial-gradient(ellipse 70% 45% at 75% -10%, rgba(189, 160, 255, 0.1), transparent 60%), + radial-gradient(ellipse 55% 40% at 0% 100%, rgba(255, 188, 109, 0.06), transparent 65%), + var(--bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 14px; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +button, +input, +select { + font: inherit; +} + +a { + color: var(--violet); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.app { + min-height: 100vh; +} + +.topbar { + position: sticky; + top: 0; + z-index: 40; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + min-height: 74px; + padding: 14px 28px; + border-bottom: 1px solid var(--border); + background: rgba(11, 10, 14, 0.82); + backdrop-filter: blur(18px); +} + +.brand { + display: flex; + align-items: center; + gap: 14px; + min-width: 0; +} + +.brand-mark { + position: relative; + display: flex; + align-items: flex-end; + gap: 3px; + width: 38px; + height: 38px; + padding: 8px 7px; + border: 1px solid var(--border-strong); + border-radius: 12px; + background: linear-gradient(150deg, rgba(189, 160, 255, 0.2), rgba(255, 188, 109, 0.08)); + box-shadow: 0 0 24px rgba(189, 160, 255, 0.12); +} + +.brand-mark span { + display: block; + width: 5px; + border-radius: 4px; + background: var(--violet); +} + +.brand-mark span:nth-child(1) { + height: 10px; + opacity: 0.55; +} + +.brand-mark span:nth-child(2) { + height: 20px; + background: var(--amber); +} + +.brand-mark span:nth-child(3) { + height: 15px; + opacity: 0.8; +} + +.brand-title { + font-family: var(--font-serif); + font-size: 21px; + line-height: 1.1; + letter-spacing: 0.02em; +} + +.brand-sub { + margin-top: 3px; + color: var(--muted); + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 7px 11px; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--muted); + font-size: 12px; + white-space: nowrap; + background: rgba(255, 255, 255, 0.025); +} + +.chip i { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} + +.chip-ok { + color: var(--ok); + border-color: rgba(143, 227, 200, 0.25); + background: rgba(143, 227, 200, 0.07); +} + +.chip-warn { + color: var(--amber); + border-color: rgba(255, 188, 109, 0.28); + background: var(--amber-soft); +} + +.chip-muted { + color: var(--muted-2); +} + +.shell { + display: grid; + grid-template-columns: 390px minmax(0, 1fr); + gap: 22px; + width: 100%; + max-width: 1800px; + margin: 0 auto; + padding: 22px 28px 48px; + align-items: start; +} + +.sidebar { + position: sticky; + top: 96px; + display: flex; + flex-direction: column; + gap: 14px; + max-height: calc(100vh - 118px); + overflow-y: auto; + padding-right: 6px; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.18) transparent; +} + +.sidebar::-webkit-scrollbar { + width: 6px; +} + +.sidebar::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.18); + border-radius: 999px; +} + +.panel { + padding: 17px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: linear-gradient(160deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.018)); + box-shadow: 0 18px 50px rgba(0, 0, 0, 0.18); +} + +.panel-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + margin-bottom: 13px; +} + +.panel h2 { + margin: 0; + font-family: var(--font-serif); + font-size: 16px; + font-weight: 600; + letter-spacing: 0.02em; +} + +.muted { + color: var(--muted); +} + +.subhead { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + margin: 0 0 9px; + color: var(--text); + font-family: var(--font-serif); + font-size: 13.5px; + letter-spacing: 0.02em; +} + +.subhead .muted { + font-family: var(--font-sans); + font-size: 10.5px; +} + +.preset-row { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 6px; + margin-bottom: 4px; +} + +.preset-row-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.preset-button { + padding: 7px 4px; + border: 1px solid var(--border); + border-radius: 9px; + background: rgba(255, 255, 255, 0.025); + color: var(--muted); + cursor: pointer; + font-size: 11px; + transition: border-color 0.15s, color 0.15s, background 0.15s; +} + +.preset-button:hover { + border-color: rgba(189, 160, 255, 0.45); + color: var(--text); +} + +.preset-button.active { + border-color: rgba(189, 160, 255, 0.6); + background: var(--violet-soft); + color: var(--violet); +} + +.live-badge { + display: inline-flex; + align-items: center; + gap: 4px; + margin-left: 6px; + padding: 2px 6px; + border: 1px solid rgba(143, 227, 200, 0.25); + border-radius: 999px; + background: rgba(143, 227, 200, 0.07); + color: var(--ok); + font-size: 9px; + letter-spacing: 0.08em; + vertical-align: middle; +} + +.live-badge::before { + content: ''; + width: 4px; + height: 4px; + border-radius: 50%; + background: currentColor; + box-shadow: 0 0 6px currentColor; +} + +.hint { + margin: 9px 0 0; + color: var(--muted-2); + font-size: 11.5px; + line-height: 1.55; +} + +.hint.warn { + color: #d9a76b; +} + +.field { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 11px; +} + +.field:last-child { + margin-bottom: 0; +} + +.field-label { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; + color: var(--muted); + font-size: 11.5px; + letter-spacing: 0.03em; +} + +.field-hint { + color: var(--muted-2); + font-size: 10.5px; +} + +.input { + width: 100%; + min-height: 38px; + padding: 9px 11px; + border: 1px solid var(--border); + border-radius: 10px; + outline: none; + background: rgba(0, 0, 0, 0.24); + color: var(--text); + font-size: 13px; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.input:focus { + border-color: rgba(189, 160, 255, 0.6); + box-shadow: 0 0 0 3px rgba(189, 160, 255, 0.1); +} + +select.input { + appearance: none; + background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), + linear-gradient(135deg, var(--muted) 50%, transparent 50%); + background-position: calc(100% - 16px) 16px, calc(100% - 11px) 16px; + background-size: 5px 5px, 5px 5px; + background-repeat: no-repeat; + padding-right: 30px; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 40px; + padding: 10px 16px; + border: 1px solid transparent; + border-radius: 11px; + cursor: pointer; + font-weight: 600; + font-size: 13px; + letter-spacing: 0.01em; + transition: transform 0.15s, filter 0.15s, background 0.15s, border-color 0.15s; +} + +.button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.button.primary { + background: linear-gradient(135deg, #c9b1ff, var(--violet) 45%, #a986f0); + color: #17131f; + box-shadow: 0 8px 30px rgba(189, 160, 255, 0.18); +} + +.button.primary:hover:not(:disabled) { + filter: brightness(1.06); + transform: translateY(-1px); +} + +.button.outline { + border-color: var(--border-strong); + background: rgba(255, 255, 255, 0.03); + color: var(--text); +} + +.button.outline:hover:not(:disabled) { + border-color: rgba(189, 160, 255, 0.5); + background: var(--violet-soft); +} + +.button.wide { + width: 100%; + margin-top: 3px; +} + +.ghost-button { + padding: 7px 11px; + border: 1px solid var(--border); + border-radius: 9px; + background: rgba(255, 255, 255, 0.025); + color: var(--text); + cursor: pointer; + font-size: 12px; +} + +.ghost-button:hover { + border-color: var(--border-strong); + background: rgba(255, 255, 255, 0.05); +} + +.link-button { + padding: 0; + border: none; + background: none; + color: var(--muted); + cursor: pointer; + font-size: 12px; + text-decoration: underline; + text-decoration-color: rgba(255, 255, 255, 0.18); + text-underline-offset: 3px; +} + +.link-button:hover { + color: var(--text); +} + +.dropzone { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + min-height: 138px; + padding: 18px; + border: 1px dashed rgba(255, 255, 255, 0.18); + border-radius: 14px; + background: rgba(0, 0, 0, 0.14); + text-align: center; + cursor: pointer; + transition: border-color 0.15s, background 0.15s, transform 0.15s; +} + +.dropzone:hover, +.dropzone.dragging { + border-color: rgba(189, 160, 255, 0.6); + background: rgba(189, 160, 255, 0.06); +} + +.dropzone.dragging { + transform: scale(1.01); +} + +.dropzone.has-file { + border-style: solid; + border-color: rgba(189, 160, 255, 0.32); + background: rgba(189, 160, 255, 0.055); +} + +.dropzone strong { + font-size: 13px; + font-weight: 600; + word-break: break-all; +} + +.dropzone .muted { + font-size: 11.5px; + line-height: 1.45; +} + +.drop-icon { + display: grid; + place-items: center; + width: 38px; + height: 38px; + margin-bottom: 2px; + border: 1px solid rgba(189, 160, 255, 0.28); + border-radius: 50%; + color: var(--violet); + font-family: var(--font-serif); + font-size: 19px; + background: rgba(189, 160, 255, 0.08); +} + +.file-row { + display: flex; + align-items: center; + gap: 12px; + margin-top: 9px; + flex-wrap: wrap; +} + +.divider { + height: 1px; + margin: 14px 0; + background: var(--border); +} + +.drop-small { + display: flex; + align-items: center; + gap: 11px; + padding: 11px 12px; + border: 1px dashed rgba(255, 255, 255, 0.16); + border-radius: 12px; + background: rgba(0, 0, 0, 0.12); + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} + +.drop-small:hover { + border-color: rgba(255, 188, 109, 0.4); + background: rgba(255, 188, 109, 0.04); +} + +.drop-small.has-file { + border-style: solid; + border-color: rgba(255, 188, 109, 0.3); + background: rgba(255, 188, 109, 0.05); +} + +.drop-small-icon { + display: grid; + place-items: center; + flex: 0 0 30px; + width: 30px; + height: 30px; + border-radius: 9px; + color: var(--amber); + background: var(--amber-soft); + font-size: 15px; +} + +.drop-small span:last-child { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.drop-small strong { + font-size: 12px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.drop-small em { + color: var(--muted-2); + font-size: 10.5px; + font-style: normal; + line-height: 1.4; +} + +.slider-row { + display: flex; + flex-direction: column; + gap: 6px; + margin: 11px 0; +} + +.slider-label { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + color: var(--muted); + font-size: 11.5px; +} + +.slider-label b { + color: var(--text); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +input[type='range'] { + width: 100%; + height: 4px; + margin: 4px 0; + border-radius: 999px; + appearance: none; + background: linear-gradient(90deg, rgba(189, 160, 255, 0.6), rgba(255, 188, 109, 0.5)); + outline: none; + cursor: pointer; +} + +input[type='range']::-webkit-slider-thumb { + appearance: none; + width: 14px; + height: 14px; + border: 2px solid #17131f; + border-radius: 50%; + background: var(--violet); + box-shadow: 0 0 0 3px rgba(189, 160, 255, 0.18); +} + +input[type='range']::-moz-range-thumb { + width: 14px; + height: 14px; + border: 2px solid #17131f; + border-radius: 50%; + background: var(--violet); +} + +.color-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin: 12px 0; +} + +.color-input { + width: 100%; + height: 38px; + padding: 3px; + border: 1px solid var(--border); + border-radius: 9px; + background: rgba(0, 0, 0, 0.2); + cursor: pointer; +} + +.toggle-row { + display: flex; + gap: 18px; + flex-wrap: wrap; + margin-top: 10px; +} + +.toggle { + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--muted); + font-size: 12px; + cursor: pointer; +} + +.toggle input { + width: 15px; + height: 15px; + accent-color: var(--violet); + cursor: pointer; +} + +.inline-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.job-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.job-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + width: 100%; + padding: 9px 10px; + border: 1px solid transparent; + border-radius: 10px; + background: rgba(255, 255, 255, 0.02); + color: var(--text); + text-align: left; + cursor: pointer; +} + +.job-row:hover { + border-color: var(--border); + background: rgba(255, 255, 255, 0.04); +} + +.job-row.active { + border-color: rgba(189, 160, 255, 0.35); + background: var(--violet-soft); +} + +.job-row span:first-child { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.job-row strong { + overflow: hidden; + font-size: 12px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.job-row em { + color: var(--muted-2); + font-size: 10px; + font-style: normal; +} + +.status-dot { + flex: 0 0 auto; + padding: 3px 7px; + border-radius: 999px; + font-size: 10px; + white-space: nowrap; + background: rgba(255, 255, 255, 0.06); + color: var(--muted); +} + +.status-ready, +.status-done { + background: rgba(143, 227, 200, 0.1); + color: var(--ok); +} + +.status-rendering, +.status-running, +.status-queued { + background: var(--violet-soft); + color: var(--violet); +} + +.status-error { + background: rgba(255, 127, 143, 0.12); + color: var(--danger); +} + +.footnote { + margin: 4px 2px 0; + color: var(--muted-2); + font-size: 10.5px; + line-height: 1.6; +} + +.stage { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 0; +} + +.alert { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + border: 1px solid rgba(255, 127, 143, 0.25); + border-radius: 12px; + background: rgba(255, 127, 143, 0.08); + color: #ffc4cb; + font-size: 12.5px; + line-height: 1.5; +} + +.alert strong { + display: block; + margin-bottom: 3px; + color: #ffdfe3; +} + +.alert p { + margin: 0; +} + +.alert button { + border: none; + background: none; + color: inherit; + cursor: pointer; + font-size: 18px; + line-height: 1; +} + +.stage-alert { + margin-bottom: 0; +} + +.empty-card { + display: grid; + grid-template-columns: 1.08fr 0.92fr; + gap: 34px; + align-items: stretch; + min-height: 540px; + padding: 34px; + border: 1px solid var(--border); + border-radius: 22px; + background: + radial-gradient(ellipse 80% 90% at 18% 0%, rgba(189, 160, 255, 0.1), transparent 55%), + radial-gradient(ellipse 60% 80% at 100% 100%, rgba(255, 188, 109, 0.06), transparent 60%), + linear-gradient(160deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)); + overflow: hidden; +} + +.empty-preview { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 380px; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 18px; + background: + radial-gradient(ellipse at 50% 45%, rgba(255, 255, 255, 0.06), transparent 55%), + linear-gradient(160deg, #17151b, #0d0c10); + overflow: hidden; +} + +.empty-paper { + position: absolute; + left: -6%; + right: -6%; + top: 26%; + height: 48%; + border-radius: 8px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.04) 20%, rgba(0, 0, 0, 0.04) 80%, rgba(0, 0, 0, 0.18)), + #c9c2b6; + box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5); + transform: rotate(-1.2deg); + overflow: hidden; +} + +.empty-strip { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 130%; + animation: scroll-strip 7s ease-in-out infinite alternate; +} + +.empty-staff { + position: absolute; + inset: 0; +} + +.empty-staff i { + position: absolute; + left: 0; + right: 0; + height: 1px; + background: rgba(20, 16, 14, 0.38); +} + +.empty-note { + position: absolute; + color: #121018; + font-family: var(--font-serif); + font-size: 30px; + line-height: 1; + opacity: 0.9; +} + +.empty-note.n1 { + left: 16%; + top: 20%; +} + +.empty-note.n2 { + left: 47%; + top: 48%; + font-size: 34px; +} + +.empty-note.n3 { + left: 76%; + top: 27%; +} + +.empty-ball { + position: absolute; + width: 13px; + height: 13px; + border-radius: 50%; + animation: bob 1.9s ease-in-out infinite; +} + +.empty-ball.b1 { + left: 15.4%; + top: 10%; + background: radial-gradient(circle at 34% 30%, #fff, var(--violet) 40%, #5a3d9e); + box-shadow: 0 0 22px rgba(189, 160, 255, 0.85), 0 0 60px rgba(189, 160, 255, 0.3); +} + +.empty-ball.b2 { + left: 46.5%; + top: 58%; + background: radial-gradient(circle at 34% 30%, #fff, var(--amber) 40%, #93521b); + box-shadow: 0 0 22px rgba(255, 188, 109, 0.8), 0 0 60px rgba(255, 188, 109, 0.28); + animation-delay: -0.7s; +} + +@keyframes scroll-strip { + from { + transform: translateX(0); + } + to { + transform: translateX(-9%); + } +} + +@keyframes bob { + 0%, + 100% { + transform: translateY(0) scale(1); + } + 45% { + transform: translateY(-8px) scale(1.03); + } +} + +.empty-copy { + display: flex; + flex-direction: column; + justify-content: center; + min-width: 0; +} + +.eyebrow { + display: block; + color: var(--muted-2); + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.2em; + text-transform: uppercase; +} + +.empty-copy h1 { + max-width: 620px; + margin: 14px 0 14px; + font-family: var(--font-serif); + font-size: clamp(28px, 3vw, 44px); + font-weight: 500; + line-height: 1.16; + letter-spacing: -0.01em; +} + +.empty-copy p { + max-width: 560px; + margin: 0; + color: var(--muted); + font-size: 13.5px; + line-height: 1.75; +} + +.empty-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 24px; +} + +.empty-points { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 26px; +} + +.empty-points span { + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--muted-2); + font-size: 10.5px; + letter-spacing: 0.02em; + background: rgba(255, 255, 255, 0.02); +} + +.working-card { + display: flex; + flex-direction: column; + gap: 18px; + min-height: 360px; + padding: 34px; + border: 1px solid var(--border); + border-radius: 20px; + background: linear-gradient(160deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.012)); +} + +.busy-card { + align-items: center; + justify-content: center; + text-align: center; +} + +.working-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; +} + +.working-head h2 { + margin: 8px 0 0; + font-family: var(--font-serif); + font-size: 22px; + font-weight: 500; +} + +.percent { + color: var(--violet); + font-family: var(--font-serif); + font-size: 34px; + font-variant-numeric: tabular-nums; +} + +.progress-track { + height: 5px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.07); + overflow: hidden; +} + +.progress-fill { + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, var(--violet), var(--amber)); + transition: width 0.4s ease; +} + +.stage-list { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 8px; + margin: 4px 0 0; + padding: 0; + list-style: none; +} + +.stage-item { + display: flex; + align-items: center; + gap: 7px; + min-width: 0; + color: var(--muted-2); + font-size: 11px; +} + +.stage-item i { + flex: 0 0 auto; + width: 7px; + height: 7px; + border: 1px solid currentColor; + border-radius: 50%; +} + +.stage-item.active { + color: var(--violet); +} + +.stage-item.active i { + background: var(--violet); + box-shadow: 0 0 10px rgba(189, 160, 255, 0.8); +} + +.stage-item.done { + color: var(--ok); +} + +.stage-item.done i { + background: var(--ok); +} + +.spinner { + width: 34px; + height: 34px; + border: 2px solid rgba(255, 255, 255, 0.12); + border-top-color: var(--violet); + border-radius: 50%; + animation: spin 0.9s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.working-card.busy-card strong { + font-family: var(--font-serif); + font-size: 19px; + font-weight: 500; +} + +.preview-card { + padding: 18px; + border: 1px solid var(--border); + border-radius: 20px; + background: linear-gradient(160deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.012)); + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.22); +} + +.preview-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + margin-bottom: 14px; +} + +.preview-head h2 { + margin: 7px 0 3px; + font-family: var(--font-serif); + font-size: 26px; + font-weight: 500; + line-height: 1.15; +} + +.preview-head p { + margin: 0; + color: var(--muted-2); + font-size: 11.5px; +} + +.preview-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.player-shell { + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 15px; + background: + radial-gradient(ellipse at 50% 42%, rgba(255, 255, 255, 0.035), transparent 65%), + #050407; + overflow: hidden; +} + +.view-gizmo-dock { + position: absolute; + top: 16px; + right: 16px; + z-index: 5; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + pointer-events: none; +} + +.view-gizmo { + width: 120px; + height: 120px; + pointer-events: auto; + touch-action: none; + cursor: grab; + user-select: none; + filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.45)); +} + +.view-gizmo.dragging { + cursor: grabbing; +} + +.gizmo-glass { + fill: rgba(10, 9, 13, 0.62); + stroke: rgba(255, 255, 255, 0.16); + stroke-width: 1; +} + +.gizmo-ring { + fill: none; + stroke: rgba(255, 255, 255, 0.22); + stroke-width: 1; + stroke-dasharray: 2 5; +} + +.gizmo-center { + fill: rgba(255, 255, 255, 0.6); +} + +.gizmo-axis line { + stroke-linecap: round; +} + +.gizmo-axis text { + font-family: var(--font-sans); + font-size: 11.5px; + font-weight: 700; + pointer-events: none; +} + +.gizmo-reset { + pointer-events: auto; + padding: 3px 9px; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 999px; + background: rgba(10, 9, 13, 0.6); + color: var(--muted); + cursor: pointer; + font-size: 10px; + backdrop-filter: blur(8px); +} + +.gizmo-reset:hover { + color: var(--text); + border-color: rgba(189, 160, 255, 0.45); +} + +.color-grid-spacer { + min-height: 1px; +} + +.player-frame { + max-width: 100%; + overflow: hidden; + border-radius: 9px; + background: #000; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); +} + +.player-shell.portrait .player-frame { + max-width: 100%; +} + +.render-progress { + margin-top: 13px; + padding: 12px 13px; + border: 1px solid rgba(189, 160, 255, 0.2); + border-radius: 11px; + background: var(--violet-soft); +} + +.render-progress-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; + color: var(--violet); + font-size: 11.5px; +} + +.render-progress-head b { + font-variant-numeric: tabular-nums; +} + +.done-banner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + margin-top: 13px; + padding: 12px 14px; + border: 1px solid rgba(143, 227, 200, 0.22); + border-radius: 11px; + background: rgba(143, 227, 200, 0.06); + color: #b8e9dc; + font-size: 12px; + line-height: 1.5; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 10px; +} + +.stat { + padding: 13px 14px; + border: 1px solid var(--border); + border-radius: 12px; + background: rgba(255, 255, 255, 0.025); +} + +.stat span { + display: block; + color: var(--muted-2); + font-size: 10.5px; + letter-spacing: 0.04em; +} + +.stat b { + display: block; + margin-top: 5px; + font-family: var(--font-serif); + font-size: 22px; + font-weight: 500; + font-variant-numeric: tabular-nums; +} + +.stat small { + color: var(--muted-2); + font-family: var(--font-sans); + font-size: 10px; + font-weight: 400; +} + +.notes-card { + padding: 0; + border: 1px solid var(--border); + border-radius: 14px; + background: rgba(255, 255, 255, 0.02); + overflow: hidden; +} + +.notes-card summary { + display: flex; + align-items: center; + gap: 8px; + padding: 13px 15px; + color: var(--muted); + font-size: 12px; + cursor: pointer; + list-style: none; +} + +.notes-card summary::-webkit-details-marker { + display: none; +} + +.notes-card summary::before { + content: '›'; + color: var(--muted-2); + font-size: 16px; + transform: rotate(90deg); + transition: transform 0.15s; +} + +.notes-card[open] summary::before { + transform: rotate(-90deg); +} + +.badge { + padding: 3px 7px; + border-radius: 999px; + font-size: 10px; +} + +.badge.warn { + background: var(--amber-soft); + color: var(--amber); +} + +.badge.danger { + background: rgba(255, 127, 143, 0.12); + color: var(--danger); +} + +.note-group { + padding: 0 15px 14px 15px; +} + +.note-group h3 { + margin: 4px 0 7px; + color: var(--muted); + font-size: 11.5px; + font-weight: 600; + letter-spacing: 0.04em; +} + +.note-group ul { + margin: 0; + padding-left: 17px; + color: var(--muted); + font-size: 11.5px; + line-height: 1.65; +} + +.note-group li + li { + margin-top: 3px; +} + +@media (max-width: 1240px) { + .shell { + grid-template-columns: 1fr; + max-width: 1100px; + } + + .sidebar { + position: static; + max-height: none; + overflow: visible; + order: 2; + } + + .stage { + order: 1; + } + + .stats-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .empty-card { + grid-template-columns: 1fr; + min-height: 0; + gap: 24px; + padding: 24px; + } + + .empty-preview { + min-height: 300px; + order: 2; + } +} + +@media (max-width: 720px) { + .topbar { + flex-wrap: wrap; + padding: 12px 16px; + } + + .topbar-right { + width: 100%; + justify-content: flex-start; + } + + .shell { + padding: 14px 14px 40px; + gap: 14px; + } + + .panel { + padding: 15px; + } + + .stage-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .preview-head { + flex-direction: column; + } + + .preview-actions { + justify-content: flex-start; + } + + .stats-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .empty-copy h1 { + font-size: 28px; + } + + .empty-actions .button { + flex: 1; + } + + .done-banner { + flex-direction: column; + align-items: flex-start; + } +} + +@media (prefers-reduced-motion: reduce) { + .empty-strip, + .empty-ball, + .spinner { + animation: none !important; + } +} diff --git a/src/remotion/MusicScroll.tsx b/src/remotion/MusicScroll.tsx new file mode 100644 index 0000000..e2f4f16 --- /dev/null +++ b/src/remotion/MusicScroll.tsx @@ -0,0 +1,716 @@ +import React, {useLayoutEffect, useMemo, useRef} from 'react'; +import { + AbsoluteFill, + Audio, + Img, + Sequence, + useCurrentFrame, + useVideoConfig, +} from 'remotion'; +import type {RenderData} from '../shared/types'; +import {scrollTrack} from '../shared/scroll'; +import { + activeRingsAt, + arcPoint, + groupLandings, + liftDepth, + performanceAt, + REST_DEPTH, + type Motion, +} from './animation'; + +const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); + +/** + * Accepts hex and rgb()/rgba() colours. Canvas silently keeps its previous + * strokeStyle for invalid colours (default black), so this must cover whatever + * `mix()` hands back. + */ +function parseColor(color: string): [number, number, number] { + const value = color.trim(); + const rgb = value.match(/^rgba?\(([^)]+)\)$/i); + if (rgb) { + const parts = rgb[1]!.split(',').map((part) => Number.parseFloat(part.trim())); + if (parts.length >= 3 && parts.slice(0, 3).every(Number.isFinite)) { + return [parts[0]!, parts[1]!, parts[2]!]; + } + } + const hex = value.startsWith('#') ? value.slice(1) : value; + if (/^[0-9a-f]{3}$/i.test(hex)) { + return [ + Number.parseInt(hex[0]! + hex[0]!, 16), + Number.parseInt(hex[1]! + hex[1]!, 16), + Number.parseInt(hex[2]! + hex[2]!, 16), + ]; + } + if (/^[0-9a-f]{6}$/i.test(hex)) { + return [ + Number.parseInt(hex.slice(0, 2), 16), + Number.parseInt(hex.slice(2, 4), 16), + Number.parseInt(hex.slice(4, 6), 16), + ]; + } + return [255, 255, 255]; +} + +function withAlpha(hex: string, alpha: number): string { + const [r, g, b] = parseColor(hex); + return `rgba(${r},${g},${b},${clamp(alpha, 0, 1)})`; +} + +function mix(hexA: string, hexB: string, amount: number): string { + const a = parseColor(hexA); + const b = parseColor(hexB); + const t = clamp(amount, 0, 1); + return `rgb(${Math.round(a[0] + (b[0] - a[0]) * t)},${Math.round( + a[1] + (b[1] - a[1]) * t, + )},${Math.round(a[2] + (b[2] - a[2]) * t)})`; +} + +function makePaperTexture(): string | undefined { + if (typeof document === 'undefined') return undefined; + const size = 96; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) return undefined; + const image = ctx.createImageData(size, size); + let seed = 0x1a2b3c4d; + for (let i = 0; i < image.data.length; i += 4) { + seed = (seed * 1664525 + 1013904223) >>> 0; + const grain = seed % 18; + image.data[i] = 0; + image.data[i + 1] = 0; + image.data[i + 2] = 0; + image.data[i + 3] = grain; + } + ctx.putImageData(image, 0, 0); + return canvas.toDataURL('image/png'); +} + +type BallState = { + staff: number; + color: string; + motion: Motion; +}; + +export const MusicScroll: React.FC<{data: RenderData}> = ({data}) => { + const frame = useCurrentFrame(); + const {width, height, fps} = useVideoConfig(); + const canvasRef = useRef(null); + const paperTexture = useMemo(makePaperTexture, []); + const settings = data.settings; + const meta = data.meta; + + const uniqueLanes = useMemo( + () => [...new Set(data.activations.map((note) => note.staff))].sort((a, b) => a - b), + [data.activations], + ); + const landings = useMemo( + () => new Map(uniqueLanes.map((staff) => [staff, groupLandings(data.activations, staff)])), + [data.activations, uniqueLanes], + ); + const colorForStaff = useMemo(() => { + const map = new Map(); + uniqueLanes.forEach((staff, index) => { + map.set(staff, settings.staffColors[index % settings.staffColors.length] ?? '#bda0ff'); + }); + return map; + }, [uniqueLanes, settings.staffColors]); + + const introSeconds = settings.introSeconds; + const t = frame / fps - introSeconds; + const paperOpacity = clamp((t + introSeconds) / Math.max(0.2, Math.min(0.55, introSeconds)), 0, 1); + const contentOpacity = clamp((t + 0.2) / 0.28, 0, 1); + const titleOpacity = clamp((t + introSeconds) / Math.max(0.3, Math.min(0.7, introSeconds)), 0, 1); + + const displayHeight = Math.max(120, height * settings.scoreHeightRatio); + const displayScale = displayHeight / Math.max(1, meta.svgHeight); + const totalWidth = meta.svgWidth * displayScale; + const centerOffset = totalWidth < width ? (width - totalWidth) / 2 : 0; + const scrollX = scrollTrack(data.scroll, t); + const scoreLeft = centerOffset - scrollX * displayScale; + const scoreTop = (height - displayHeight) / 2 + height * 0.018; + const paperPad = Math.max(16, displayHeight * 0.07); + /** + * The sheet is much wider than the frame: once the camera tilts, a + * viewport-wide band would show its left/right edges inside the picture. + */ + const paperOverhang = width * 2; + + const balls: BallState[] = []; + for (const staff of uniqueLanes) { + const motion = performanceAt(landings.get(staff) ?? [], t); + if (!motion) continue; + if (motion.opacity <= 0.001) continue; + balls.push({staff, color: colorForStaff.get(staff) ?? '#bda0ff', motion}); + } + + const rings = activeRingsAt(data.activations, t); + const noteAlpha = paperOpacity; + const isPortrait = height > width; + const headerTop = height * (isPortrait ? 0.05 : 0.055); + const headerPadding = width * (isPortrait ? 0.065 : 0.055); + const titleSize = Math.round(height * (isPortrait ? 0.026 : 0.04)); + const subtitleSize = Math.round(height * (isPortrait ? 0.014 : 0.019)); + const composerSize = Math.round(height * (isPortrait ? 0.013 : 0.017)); + const progressBottom = height * (isPortrait ? 0.045 : 0.042); + const progress = clamp(t / Math.max(0.1, meta.musicDuration), 0, 1); + const progressOpacity = titleOpacity * (settings.showProgress ? 0.6 : 0); + + const tiltX = settings.tiltX ?? 0; + const tiltY = settings.tiltY ?? 0; + const tiltZ = settings.tiltZ ?? 0; + const perspective = Math.max(500, settings.perspective ?? 1800); + const tiltZoom = Math.max(0.5, settings.tiltZoom ?? 1); + const paperAlpha = settings.paperAlpha ?? 1; + const scoreColor = settings.scoreColor ?? '#14110d'; + const planeTransform = `rotateX(${tiltX}deg) rotateY(${tiltY}deg) rotateZ(${tiltZ}deg) scale(${tiltZoom})`; + + const introFrames = Math.round(settings.introSeconds * fps); + const offsetMs = settings.audioOffsetMs; + const delayFrames = offsetMs > 0 ? Math.round((offsetMs / 1000) * fps) : 0; + const trimFrames = offsetMs < 0 ? Math.round((-offsetMs / 1000) * fps) : 0; + + useLayoutEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + if (canvas.width !== width) canvas.width = width; + if (canvas.height !== height) canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.clearRect(0, 0, width, height); + + const unit = displayScale; + /** Score coordinates -> the plane's own pixel canvas (before the 3D transform). */ + const toPaper = (x: number, y: number): [number, number] => [ + (x - scrollX) * displayScale + centerOffset, + scoreTop + y * displayScale, + ]; + + // Mirror of the CSS transform on the score plane, evaluated here so the orbs + // can be drawn in screen space: they keep a round silhouette at any camera + // angle instead of being squashed into ellipses by the paper's perspective. + const rad = (deg: number) => (deg * Math.PI) / 180; + const cosX = Math.cos(rad(tiltX)); + const sinX = Math.sin(rad(tiltX)); + const cosY = Math.cos(rad(tiltY)); + const sinY = Math.sin(rad(tiltY)); + const cosZ = Math.cos(rad(tiltZ)); + const sinZ = Math.sin(rad(tiltZ)); + const midX = width / 2; + const midY = height / 2; + /** Score units above the paper -> plane-local pixels, before the 3D transform. */ + const lift = Math.max(0, settings.ballLift ?? 1); + const depthToPixels = (depth: number) => liftDepth(depth, lift) * displayScale; + const orbRadius = (depth: number) => 2.5 * unit * settings.ballScale * depth; + + const project = (x: number, y: number, z = 0) => { + const lx = (x - midX) * tiltZoom; + const ly = (y - midY) * tiltZoom; + // CSS applies rotateX * rotateY * rotateZ * scale to the local point. + const rzX = lx * cosZ - ly * sinZ; + const rzY = lx * sinZ + ly * cosZ; + const ryX = rzX * cosY + z * sinY; + const ryZ = -rzX * sinY + z * cosY; + const rxY = rzY * cosX - ryZ * sinX; + const rxZ = rzY * sinX + ryZ * cosX; + const w = perspective / Math.max(1, perspective - rxZ); + return {x: midX + ryX * w, y: midY + rxY * w, w, depth: rxZ}; + }; + + /** Draw in paper-plane units, so shadows and rings lie flat on the sheet. */ + const onPaper = (x: number, y: number, draw: () => void) => { + const origin = project(x, y, 0); + const alongX = project(x + 1, y, 0); + const alongY = project(x, y + 1, 0); + ctx.save(); + ctx.setTransform( + alongX.x - origin.x, + alongX.y - origin.y, + alongY.x - origin.x, + alongY.y - origin.y, + origin.x, + origin.y, + ); + draw(); + ctx.restore(); + }; + + // Contact shadows, flattened onto the sheet so each hop reads as height. + for (const ball of balls) { + const motion = ball.motion; + const [px, py] = toPaper(motion.position[0], motion.position[1]); + const r = 2.5 * unit * settings.ballScale; + const height = Math.max(0, motion.depth - REST_DEPTH) * lift; + const spread = r * (1.6 + Math.min(2.2, height * 0.08)); + const strength = 0.36 / (1 + height / 9); + onPaper(px, py, () => { + const shadow = ctx.createRadialGradient(0, 0, r * 0.1, 0, 0, spread); + shadow.addColorStop(0, `rgba(0,0,0,${strength})`); + shadow.addColorStop(0.5, `rgba(0,0,0,${strength * 0.4})`); + shadow.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.globalAlpha = motion.opacity * 0.9 * contentOpacity; + ctx.fillStyle = shadow; + ctx.beginPath(); + ctx.arc(0, 0, spread, 0, Math.PI * 2); + ctx.fill(); + }); + } + + // Hit markers stay on the sheet and carry the orb's own lane colour, so a + // played note reads as a trace of that ball instead of a dark ring. + for (const note of rings) { + const age = t - note.time; + if (age < 0 || age > 3.6) continue; + const sustain = note.kind === 'sustain'; + const flash = Math.exp(-age / 0.14) * (sustain ? 0.55 : 1); + const fade = Math.max(0, 1 - age / 3.6) ** 1.15; + const expansion = 1 + 0.23 * (1 - Math.exp(-age / 0.055)); + const color = colorForStaff.get(note.staff) ?? '#bda0ff'; + const [nx, ny] = toPaper(note.x, note.y); + const ringR = 4.2 * unit * expansion; + onPaper(nx, ny, () => { + ctx.globalAlpha = noteAlpha; + + const halo = ctx.createRadialGradient(0, 0, ringR * 0.45, 0, 0, ringR * 2.3); + halo.addColorStop(0, withAlpha(color, (0.3 + flash * 0.25) * fade)); + halo.addColorStop(1, withAlpha(color, 0)); + ctx.fillStyle = halo; + ctx.beginPath(); + ctx.arc(0, 0, ringR * 2.3, 0, Math.PI * 2); + ctx.fill(); + + ctx.strokeStyle = withAlpha(color, (sustain ? 0.6 : 0.82) * fade); + ctx.lineWidth = Math.max(1.2, 0.55 * unit); + ctx.beginPath(); + ctx.arc(0, 0, ringR, 0, Math.PI * 2); + ctx.stroke(); + + ctx.strokeStyle = withAlpha(mix(color, '#ffffff', 0.4 + flash * 0.4), 0.6 * fade); + ctx.lineWidth = Math.max(0.9, 0.28 * unit); + ctx.beginPath(); + ctx.arc(0, 0, ringR * 0.84, 0, Math.PI * 2); + ctx.stroke(); + }); + } + + // Trails: a 3D arc in the same space as the orb, drawn only along the part + // of the hop already travelled so it reads as a tail behind the ball. + for (const ball of balls) { + const motion = ball.motion; + if (motion.trailOpacity <= 0.001) continue; + const steps = 30; + const trailEnd = motion.departing ? Math.max(0.04, motion.u) : 1; + const samples = Array.from({length: steps}, (_, i) => { + const [x, y, z] = arcPoint(motion.from, motion.to, (i / (steps - 1)) * trailEnd); + const [px, py] = toPaper(x, y); + const point = project(px, py, depthToPixels(z)); + return {x: point.x, y: point.y, w: point.w}; + }); + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + const stroke = (widthScale: number, alphaScale: number) => { + for (let i = 1; i < samples.length; i += 1) { + const a = samples[i - 1]!; + const b = samples[i]!; + const f = i / (samples.length - 1); + const head = f * f; + ctx.globalAlpha = noteAlpha * motion.trailOpacity * (0.04 + head * 0.72) * alphaScale; + ctx.strokeStyle = withAlpha(ball.color, 0.85); + ctx.lineWidth = Math.max( + 0.7, + (0.3 + 0.62 * head) * unit * b.w * widthScale, + ); + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.stroke(); + } + }; + stroke(3.6, 0.16); + stroke(1, 1); + ctx.restore(); + } + + // Orbs: real spheres in screen space, so they never collapse into ellipses. + for (const ball of balls) { + const motion = ball.motion; + const opacity = motion.opacity * contentOpacity; + if (opacity <= 0.001) continue; + const [px, py] = toPaper(motion.position[0], motion.position[1]); + const head = project(px, py, depthToPixels(motion.depth)); + const r = orbRadius(head.w); + const pulse = motion.pulse; + const radius = r * (1 + pulse * 0.1); + const sx = head.x; + const sy = head.y; + + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + ctx.globalAlpha = opacity; + const glowRadius = radius * 9.5; + const glow = ctx.createRadialGradient(sx, sy, radius * 0.2, sx, sy, glowRadius); + glow.addColorStop(0, withAlpha(ball.color, 0.5 * settings.glow)); + glow.addColorStop(0.22, withAlpha(ball.color, 0.16 * settings.glow)); + glow.addColorStop(0.6, withAlpha(ball.color, 0.04 * settings.glow)); + glow.addColorStop(1, withAlpha(ball.color, 0)); + ctx.fillStyle = glow; + ctx.beginPath(); + ctx.arc(sx, sy, glowRadius, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + + ctx.save(); + ctx.globalAlpha = opacity; + const body = ctx.createRadialGradient( + sx - radius * 0.34, + sy - radius * 0.38, + radius * 0.04, + sx + radius * 0.1, + sy + radius * 0.14, + radius * 1.1, + ); + body.addColorStop(0, 'rgba(255,255,255,1)'); + body.addColorStop(0.16, mix(ball.color, '#ffffff', 0.9)); + body.addColorStop(0.42, mix(ball.color, '#ffffff', 0.52)); + body.addColorStop(0.7, mix(ball.color, '#ffffff', 0.18)); + body.addColorStop(0.9, ball.color); + body.addColorStop(1, mix(ball.color, '#000000', 0.12)); + ctx.fillStyle = body; + ctx.beginPath(); + ctx.arc(sx, sy, radius, 0, Math.PI * 2); + ctx.fill(); + + const specular = ctx.createRadialGradient( + sx - radius * 0.36, + sy - radius * 0.4, + 0, + sx - radius * 0.36, + sy - radius * 0.4, + radius * 0.46, + ); + specular.addColorStop(0, 'rgba(255,255,255,0.92)'); + specular.addColorStop(1, 'rgba(255,255,255,0)'); + ctx.fillStyle = specular; + ctx.beginPath(); + ctx.arc(sx, sy, radius, 0, Math.PI * 2); + ctx.fill(); + + ctx.globalCompositeOperation = 'lighter'; + ctx.strokeStyle = withAlpha(mix(ball.color, '#ffffff', 0.85), 0.2 * opacity); + ctx.lineWidth = Math.max(0.8, radius * 0.07); + ctx.beginPath(); + ctx.arc(sx, sy, radius * 0.94, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + } + }, [ + width, + height, + t, + scrollX, + displayScale, + displayHeight, + centerOffset, + scoreTop, + balls, + rings, + noteAlpha, + contentOpacity, + settings.ballScale, + settings.ballLift, + settings.glow, + colorForStaff, + tiltX, + tiltY, + tiltZ, + perspective, + tiltZoom, + ]); + + if (!data.events.length || !data.svgUrl) { + return ( + + 请先在网页中上传 MusicXML 并生成预览 + + ); + } + + return ( + + + + {/* Tilted score plane: paper, notation and its light spill share one 3D transform */} +
+
+ {/* Paper band, wider than the frame so tilted edges stay off-screen */} +
+ + {/* Score: the opaque SVG becomes a mask so its ink colour is live-editable */} + +
+ + {/* Paper lights */} + {balls.map((ball) => { + const [x, y] = [ + (ball.motion.position[0] - scrollX) * displayScale + centerOffset, + scoreTop + ball.motion.position[1] * displayScale, + ]; + const radius = Math.max(150, displayHeight * 0.48) * (0.85 + 0.2 * settings.glow); + return ( +
+ ); + })} + +
+
+ + {/* Orbs live above the tilted plane and are drawn in screen space */} + + + {/* Vignette keeps the eye on the score */} + + + {/* Header scrim keeps the title readable over a tilted sheet */} +
+ + {settings.showTitle && ( +
+
+
+ {meta.title || 'Untitled'} +
+ {meta.subtitle ? ( +
+ {meta.subtitle} +
+ ) : null} +
+ {meta.composer ? ( +
+ {meta.composer} +
+ ) : null} +
+ )} + + {/* Thin rule under header */} + {settings.showTitle && ( +
+ )} + + {/* Progress */} + {settings.showProgress && ( +
+
+
+ )} + + {data.audioUrl ? ( + + + ) : null} + + ); +}; diff --git a/src/remotion/Root.tsx b/src/remotion/Root.tsx new file mode 100644 index 0000000..86b615c --- /dev/null +++ b/src/remotion/Root.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {Composition} from 'remotion'; +import type {RenderData} from '../shared/types'; +import {DEFAULT_SETTINGS} from '../shared/types'; +import {MusicScroll} from './MusicScroll'; + +export const EMPTY_DATA: RenderData = { + version: 1, + meta: { + title: 'Music Visualizer', + subtitle: '', + composer: '', + svgWidth: 1120, + svgHeight: 800, + musicDuration: 3, + audioDuration: 3, + measureCount: 0, + scoreNoteCount: 0, + midiNoteCount: 0, + mappedNoteCount: 0, + warnings: [], + unsupported: [], + }, + events: [], + activations: [], + scroll: [ + {time: 0, x: 0}, + {time: 5, x: 0}, + ], + svgUrl: '', + audioUrl: '', + settings: DEFAULT_SETTINGS, +}; + +export const Root: React.FC = () => ( + { + const data = (props as {data?: RenderData}).data ?? EMPTY_DATA; + const settings = data.settings; + const music = Math.max(0.5, data.meta.musicDuration || 0.5); + const audio = Math.max(0, data.meta.audioDuration || 0); + const total = settings.introSeconds + Math.max(music, audio) + settings.outroSeconds; + return { + width: settings.width, + height: settings.height, + fps: settings.fps, + durationInFrames: Math.max(1, Math.ceil(total * settings.fps)), + }; + }} + /> +); diff --git a/src/remotion/animation.ts b/src/remotion/animation.ts new file mode 100644 index 0000000..aed56a9 --- /dev/null +++ b/src/remotion/animation.ts @@ -0,0 +1,148 @@ +import type {NoteEvent} from '../shared/types'; + +export const BALL_ENTRY_SECONDS = 0.18; +/** Resting height above the paper, in score units: just enough to clear the ink. */ +export const REST_DEPTH = 1.6; + +/** + * Amplifies only the part of the height that comes from a hop, so the ball + * still rests just above the ink and 0 flattens every jump onto the sheet. + */ +export function liftDepth(depth: number, lift: number): number { + return REST_DEPTH + (depth - REST_DEPTH) * Math.max(0, lift); +} + +export type Motion = { + position: [number, number]; + /** Height above the paper along its normal, in score units. */ + depth: number; + pulse: number; + landed: boolean; + opacity: number; + from: NoteEvent; + to: NoteEvent; + u: number; + trailOpacity: number; + departing: boolean; +}; + +const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); + +export function groupLandings(notes: NoteEvent[], staff: number): NoteEvent[] { + const groups: NoteEvent[][] = []; + for (const note of notes + .filter((n) => n.staff === staff) + .sort((a, b) => a.time - b.time)) { + const last = groups[groups.length - 1]; + if (last && Math.abs(last[0]!.time - note.time) < 1e-6) last.push(note); + else groups.push([note]); + } + const preferHigh = staff % 2 === 1; + return groups.map( + (group) => + [...group].sort( + (a, b) => + (a.kind === 'attack' ? 0 : 1) - (b.kind === 'attack' ? 0 : 1) || + (preferHigh ? b.pitch - a.pitch : a.pitch - b.pitch), + )[0]!, + ); +} + +/** + * Hop from note `a` to note `b`. X/Y travel across the sheet in a straight line + * while Z arcs above it, so the ball lands back on the paper at every note. + */ +export function arcPoint( + a: NoteEvent, + b: NoteEvent, + u: number, +): [number, number, number] { + const phase = clamp(u, 0, 1); + const distance = Math.hypot(b.x - a.x, b.y - a.y); + const interval = Math.max(0, b.time - a.time); + // Big enough that a hop still reads on a nearly face-on sheet, where a + // movement along the paper normal mostly points at the camera. + const hop = + (22 + Math.min(22, distance * 0.15)) * Math.min(1, Math.max(0.25, interval / 0.24)); + return [ + a.x + (b.x - a.x) * phase, + a.y + (b.y - a.y) * phase, + REST_DEPTH + 4 * phase * (1 - phase) * hop, + ]; +} + +export function performanceAt(targets: NoteEvent[], t: number): Motion | null { + if (!targets.length) return null; + let i = 0; + while (i + 1 < targets.length && targets[i + 1]!.time <= t) i++; + const current = targets[i]!; + const next = targets[i + 1]; + const age = t - current.time; + const landed = age >= 0; + const resting = Boolean(next && next.time - current.time - current.duration > 0.65); + const enteringNext = Boolean(resting && next && t >= next.time - BALL_ENTRY_SECONDS); + const entryTarget = enteringNext ? next! : current; + const entryPhase = clamp((t - entryTarget.time + BALL_ENTRY_SECONDS) / BALL_ENTRY_SECONDS, 0, 1); + const departing = Boolean(next && landed && !resting); + const u = departing ? clamp(age / Math.max(1e-6, next!.time - current.time), 0, 1) : 1; + const [ballX, ballY, ballZ] = departing + ? arcPoint(current, next!, u) + : arcPoint(entryTarget, entryTarget, 1); + const position: [number, number] = [ballX, ballY]; + + let opacity = 1; + if (!landed || enteringNext) { + opacity = Math.min(1, entryPhase * 2.5); + } else if (resting || !next) { + opacity = clamp((current.time + current.duration + 0.18 - t) / 0.18, 0, 1); + } + + const pulse = landed && !enteringNext ? Math.exp(-Math.max(0, age) / 0.1) : 0; + const from = departing ? current : targets[Math.max(0, i - 1)] ?? current; + const to = departing ? next! : current; + const trailOpacity = departing + ? 0.68 + : from.scoreId !== to.scoreId && i > 0 + ? 0.48 * Math.max(0, 1 - age / 0.3) + : 0; + + return { + position, + depth: ballZ, + pulse, + landed, + opacity, + from, + to, + u, + trailOpacity, + departing, + }; +} + +function lowerBound(notes: NoteEvent[], time: number): number { + let low = 0; + let high = notes.length; + while (low < high) { + const mid = (low + high) >> 1; + if (notes[mid]!.time < time) low = mid + 1; + else high = mid; + } + return low; +} + +/** Latest ring per scoreId within the 3.6s window, matching the original behavior. */ +export function activeRingsAt(notes: NoteEvent[], t: number): NoteEvent[] { + const start = lowerBound(notes, t - 3.6); + const latest = new Map(); + for (let i = start; i < notes.length; i++) { + const note = notes[i]!; + if (note.time > t) break; + latest.set(note.scoreId, note); + } + return [...latest.values()]; +} + +export function staffColorIndex(staff: number): number { + return Math.max(0, staff - 1); +} diff --git a/src/remotion/index.ts b/src/remotion/index.ts new file mode 100644 index 0000000..69bbf50 --- /dev/null +++ b/src/remotion/index.ts @@ -0,0 +1,4 @@ +import {registerRoot} from 'remotion'; +import {Root} from './Root'; + +registerRoot(Root); diff --git a/src/server/audio.ts b/src/server/audio.ts new file mode 100644 index 0000000..1e0cb41 --- /dev/null +++ b/src/server/audio.ts @@ -0,0 +1,37 @@ +import path from 'node:path'; +import {parseFile} from 'music-metadata'; + +const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.m4a', '.aac', '.ogg', '.oga', '.flac', '.opus']); + +export function isSupportedAudio(filename: string): boolean { + return AUDIO_EXTENSIONS.has(path.extname(filename).toLowerCase()); +} + +export async function readAudioDuration(filePath: string): Promise { + const metadata = await parseFile(filePath, {duration: true}); + const duration = metadata.format.duration; + if (!duration || !Number.isFinite(duration) || duration <= 0) { + throw new Error('无法读取音频时长。请换一个标准的 WAV/MP3/M4A 文件。'); + } + return duration; +} + +export function audioMimeType(filename: string): string { + switch (path.extname(filename).toLowerCase()) { + case '.wav': + return 'audio/wav'; + case '.mp3': + return 'audio/mpeg'; + case '.m4a': + case '.aac': + return 'audio/mp4'; + case '.ogg': + case '.oga': + case '.opus': + return 'audio/ogg'; + case '.flac': + return 'audio/flac'; + default: + return 'application/octet-stream'; + } +} diff --git a/src/server/config.ts b/src/server/config.ts new file mode 100644 index 0000000..9fc2b64 --- /dev/null +++ b/src/server/config.ts @@ -0,0 +1,57 @@ +import fs from 'node:fs'; +import {spawnSync} from 'node:child_process'; + +export const PORT = Number(process.env.PORT ?? 3001); +export const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL ?? `http://127.0.0.1:${PORT}`; +export const IS_PRODUCTION = process.env.NODE_ENV === 'production'; +export const REMOTION_ENTRY = 'src/remotion/index.ts'; +export const REMOTION_COMPOSITION_ID = 'MusicScroll'; + +function exists(file: string | undefined): file is string { + if (!file) return false; + try { + return fs.statSync(file).isFile(); + } catch { + return false; + } +} + +/** + * Prefer an already installed Chrome/Edge so Remotion does not have to download + * a 120 MB headless shell (which is slow or blocked on some networks). + */ +export function findBrowserExecutable(): string | undefined { + const explicit = process.env.REMOTION_BROWSER_EXECUTABLE ?? process.env.BROWSER_EXECUTABLE; + if (exists(explicit)) return explicit; + const candidates: string[] = []; + if (process.platform === 'win32') { + candidates.push( + 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', + 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', + 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', + 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', + ); + } else if (process.platform === 'darwin') { + candidates.push( + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', + ); + } + for (const candidate of candidates) { + if (exists(candidate)) return candidate; + } + for (const name of ['google-chrome', 'chromium', 'chromium-browser', 'microsoft-edge', 'chrome']) { + const result = spawnSync(process.platform === 'win32' ? 'where.exe' : 'which', [name], { + encoding: 'utf8', + windowsHide: true, + }); + if (result.status === 0) { + const first = result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean); + if (exists(first)) return first; + } + } + return undefined; +} + +export const BROWSER_EXECUTABLE = findBrowserExecutable(); diff --git a/src/server/engrave.ts b/src/server/engrave.ts new file mode 100644 index 0000000..a3af2d1 --- /dev/null +++ b/src/server/engrave.ts @@ -0,0 +1,77 @@ +import createModule from 'verovio/wasm'; +import {VerovioToolkit} from 'verovio/esm'; +import {extractNoteGeometry, type SvgGeometry} from './geometry.js'; + +export type NoteTime = { + timeMs: number; + durationMs: number; + pitch: number; +}; + +export type EngraveResult = { + svg: string; + geometry: SvgGeometry; + measureStartMs: Map; + noteTimes: Map; + pageCount: number; +}; + +type VerovioExtra = { + getTimeForElement(id: string): number; + getMIDIValuesForElement(id: string): {duration: number; pitch: number; time: number} | null; + getPageWithElement(id: string): number; +}; + +export async function engraveOneLine(xml: string): Promise { + const tk = new VerovioToolkit(await createModule()); + tk.resetXmlIdSeed(1); + tk.setOptions({ + scale: 40, + pageWidth: 50000, + pageHeight: 3600, + breaks: 'none', + adjustPageHeight: true, + header: 'none', + footer: 'none', + svgViewBox: true, + spacingStaff: 12, + spacingSystem: 12, + }); + if (!tk.loadData(xml)) { + throw new Error(`Verovio 无法读取 MusicXML:${tk.getLog()}`); + } + const pageCount = tk.getPageCount(); + if (pageCount > 1) { + // breaks:none should always yield one page. If not, render all pages side by side later. + throw new Error(`单行排版意外生成了 ${pageCount} 页,请报告这个谱面。`); + } + const svg = tk.renderToSVG(1); + const geometry = extractNoteGeometry(svg); + const extra = tk as unknown as VerovioExtra; + + const measureStartMs = new Map(); + const noteTimes = new Map(); + const allElements = svg.match(/]*\sid="([^"]+)"[^>]*>/g) ?? []; + for (const tag of allElements) { + const id = tag.match(/\sid="([^"]+)"/)?.[1]; + if (!id) continue; + if (id.startsWith('meas-')) { + const time = extra.getTimeForElement(id); + if (Number.isFinite(time) && time >= 0) measureStartMs.set(id, time); + } else if (id.startsWith('note-')) { + const midi = extra.getMIDIValuesForElement(id); + const time = extra.getTimeForElement(id); + if (midi && Number.isFinite(midi.time) && midi.time >= 0) { + noteTimes.set(id, { + timeMs: midi.time, + durationMs: Math.max(0, midi.duration ?? 0), + pitch: midi.pitch ?? 0, + }); + } else if (Number.isFinite(time) && time >= 0) { + noteTimes.set(id, {timeMs: time, durationMs: 0, pitch: 0}); + } + } + } + + return {svg, geometry, measureStartMs, noteTimes, pageCount}; +} diff --git a/src/server/events.ts b/src/server/events.ts new file mode 100644 index 0000000..fd5a0ad --- /dev/null +++ b/src/server/events.ts @@ -0,0 +1,285 @@ +import type {NoteEvent, ScoreMeta, ScrollKey, VisualSettings} from '../shared/types.js'; +import {buildScrollKeys} from '../shared/scroll.js'; +import {measureDurationSeconds, type ParsedScore, type ParsedNote} from './musicxml.js'; +import type {EngraveResult} from './engrave.js'; +import type {ParsedMidi, MidiNote} from './midi.js'; + +export type BuildEventsInput = { + parsed: ParsedScore; + engrave: EngraveResult; + midi: ParsedMidi | null; + performanceOrder: number[]; + audioDuration: number; + settings: VisualSettings; +}; + +export type BuildEventsResult = { + events: NoteEvent[]; + activations: NoteEvent[]; + scroll: ScrollKey[]; + meta: ScoreMeta; +}; + +type PerformedNote = { + eventId: string; + scoreId: string; + measureIndex: number; + occurrence: number; + partIndex: number; + staff: number; + pitch: number; + nominalTimeMs: number; + nominalDurationMs: number; + x: number; + y: number; + isGrace: boolean; + isTieContinuation: boolean; + isCue: boolean; + midiTime: number | null; + midiDuration: number | null; + midiVelocity: number | null; +}; + +const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); + +function buildMeasureTimings(parsed: ParsedScore, engrave: EngraveResult) { + const timeline = parsed.measures; + let accumulatedMs = 0; + return timeline.map((measure, index) => { + const durationQuarters = measure.durationQuarters > 0 ? measure.durationQuarters : 4; + const durationMs = measureDurationSeconds(measure, parsed.tempoEvents) * 1000; + const verovioStart = engrave.measureStartMs.get(measure.id); + const timing = { + index, + id: measure.id, + startMs: accumulatedMs, + endMs: accumulatedMs + durationMs, + verovioStartMs: Number.isFinite(verovioStart) ? (verovioStart as number) : accumulatedMs, + durationQuarters, + }; + accumulatedMs += durationMs; + return timing; + }); +} + +function matchMidi( + performed: PerformedNote[], + midiNotes: MidiNote[], + warnings: string[], +): {matchedAttacks: number; unmatchedAttacks: number; unmatchedMidi: number} { + const matched = new Array(performed.length).fill(false); + let minIndex = 0; + let unmatchedMidi = 0; + const window = 192; + + for (const midi of midiNotes) { + while (minIndex < performed.length && matched[minIndex]) minIndex++; + if (minIndex >= performed.length) { + unmatchedMidi++; + continue; + } + const start = Math.max(0, minIndex - 32); + const end = Math.min(performed.length, minIndex + window); + let best = -1; + let bestScore = Number.POSITIVE_INFINITY; + for (let i = start; i < end; i++) { + if (matched[i]) continue; + const note = performed[i]!; + if (note.pitch !== midi.midi) continue; + const dt = Math.abs(note.nominalTimeMs - midi.time * 1000); + const distancePenalty = i >= minIndex ? (i - minIndex) * 1.5 : (minIndex - i) * 4; + const tiePenalty = note.isTieContinuation ? 30 : 0; + const score = dt + distancePenalty + tiePenalty; + if (score < bestScore) { + bestScore = score; + best = i; + } + } + if (best < 0) { + unmatchedMidi++; + continue; + } + matched[best] = true; + performed[best]!.midiTime = midi.time; + performed[best]!.midiDuration = midi.duration; + performed[best]!.midiVelocity = midi.velocity; + if (bestScore > 8000) { + warnings.push( + `MIDI 与谱面在 ${midi.time.toFixed(2)}s 附近的匹配偏差较大(${(bestScore / 1000).toFixed(2)}s)。`, + ); + } + } + + let unmatchedAttacks = 0; + let matchedAttacks = 0; + for (let i = 0; i < performed.length; i++) { + const note = performed[i]!; + if (note.isTieContinuation) continue; + if (matched[i]) matchedAttacks++; + else unmatchedAttacks++; + } + return {matchedAttacks, unmatchedAttacks, unmatchedMidi}; +} + +export function buildEvents(input: BuildEventsInput): BuildEventsResult { + const {parsed, engrave, midi, performanceOrder, audioDuration, settings} = input; + const warnings = [...parsed.warnings]; + const timings = buildMeasureTimings(parsed, engrave); + const performed: PerformedNote[] = []; + let accumulatedMs = 0; + let missingGeometry = 0; + let missingTime = 0; + let cueCount = 0; + + performanceOrder.forEach((measureIndex, occurrence) => { + const timing = timings[measureIndex]; + if (!timing) return; + const occurrenceStartMs = accumulatedMs; + accumulatedMs += timing.endMs - timing.startMs; + const notes = parsed.notesByMeasure[measureIndex] ?? []; + for (const note of notes) { + const scoreNote = note as ParsedNote; + if (scoreNote.isRest) continue; + const geometry = engrave.geometry.notes[scoreNote.id]; + if (!geometry) { + missingGeometry++; + continue; + } + if (scoreNote.cue) { + cueCount++; + continue; + } + const noteTiming = engrave.noteTimes.get(scoreNote.id); + const quarters = Math.max(1e-6, timing.durationQuarters); + const msPerQuarter = (timing.endMs - timing.startMs) / quarters; + let offsetMs: number; + let durationMs: number; + if (noteTiming) { + offsetMs = noteTiming.timeMs - timing.verovioStartMs; + durationMs = noteTiming.durationMs; + } else { + missingTime++; + offsetMs = scoreNote.onset * msPerQuarter; + durationMs = scoreNote.duration * msPerQuarter; + } + if (!Number.isFinite(durationMs) || durationMs <= 0) durationMs = 120; + const pitch = scoreNote.pitch ?? noteTiming?.pitch ?? null; + if (pitch === null) continue; + performed.push({ + eventId: `evt-${occurrence}-${scoreNote.id}`, + scoreId: scoreNote.id, + measureIndex, + occurrence, + partIndex: scoreNote.partIndex, + staff: scoreNote.lane, + pitch, + nominalTimeMs: occurrenceStartMs + offsetMs, + nominalDurationMs: durationMs, + x: geometry.x, + y: geometry.y, + isGrace: scoreNote.isGrace, + isTieContinuation: scoreNote.tieStop, + isCue: scoreNote.cue, + midiTime: null, + midiDuration: null, + midiVelocity: null, + }); + } + }); + + performed.sort( + (a, b) => + a.nominalTimeMs - b.nominalTimeMs || + a.partIndex - b.partIndex || + a.pitch - b.pitch || + a.eventId.localeCompare(b.eventId), + ); + + let mapping: {matchedAttacks: number; unmatchedAttacks: number; unmatchedMidi: number}; + if (midi && midi.notes.length) { + mapping = matchMidi(performed, midi.notes, warnings); + } else { + mapping = {matchedAttacks: 0, unmatchedAttacks: performed.filter((n) => !n.isTieContinuation).length, unmatchedMidi: 0}; + warnings.push('没有使用 MIDI 时间;动画使用谱面标称时间,上传音频时可能需要手动对齐。'); + } + + const events: NoteEvent[] = []; + for (const note of performed) { + if (note.isTieContinuation || note.isCue) continue; + const matched = note.midiTime !== null; + let time = matched ? note.midiTime! : note.nominalTimeMs / 1000; + if (!matched && note.isGrace) time = Math.max(0, time - 0.07); + const duration = matched ? note.midiDuration ?? 0 : note.nominalDurationMs / 1000; + const velocity = matched + ? clamp(Math.round((note.midiVelocity ?? 0.63) * 127), 1, 127) + : 80; + events.push({ + eventId: note.eventId, + scoreId: note.scoreId, + time: Math.max(0, time), + duration: Math.max(0.02, duration), + pitch: note.pitch, + velocity, + staff: note.staff, + x: note.x, + y: note.y, + kind: 'attack', + grace: note.isGrace || undefined, + }); + } + events.sort((a, b) => a.time - b.time || a.staff - b.staff || a.pitch - b.pitch); + + if (!events.length) { + throw new Error('没有生成任何可播放的音符事件。请检查谱面是否为有效音高。'); + } + + const maxEventEnd = events.reduce((max, event) => Math.max(max, event.time + event.duration), 0); + const musicDuration = Math.max(maxEventEnd, accumulatedMs / 1000, 0.5); + const displayHeight = settings.height * settings.scoreHeightRatio; + const displayScale = displayHeight / engrave.geometry.height; + const viewWidthScore = settings.width / displayScale; + const maxLeft = Math.max(0, engrave.geometry.width - viewWidthScore); + const scroll = buildScrollKeys(events, settings.scrollLead, viewWidthScore, maxLeft, musicDuration); + + if (mapping.unmatchedMidi > 0) { + warnings.push(`${mapping.unmatchedMidi} 个 MIDI 起音没有对应谱面音符,已忽略(通常来自装饰音或额外声部)。`); + } + if (mapping.unmatchedAttacks > 0) { + warnings.push(`${mapping.unmatchedAttacks} 个谱面音符没有匹配到 MIDI 起音,使用标称时间与默认力度。`); + } + if (missingGeometry > 0) { + warnings.push(`${missingGeometry} 个音符没有取到符头坐标,已跳过。`); + } + if (missingTime > 0) { + warnings.push(`${missingTime} 个音符没有 Verovio 时间,使用拍位估算。`); + } + if (cueCount > 0) { + warnings.push(`${cueCount} 个提示音符(cue)已跳过动画。`); + } + if (mapping.matchedAttacks > 0 && mapping.matchedAttacks / Math.max(1, events.length) < 0.85) { + warnings.push('MIDI 与谱面的匹配率低于 85%,建议检查反复/装饰音或改用同源 MIDI。'); + } + + const meta: ScoreMeta = { + title: parsed.title || 'Untitled', + subtitle: parsed.subtitle, + composer: parsed.composer, + svgWidth: engrave.geometry.width, + svgHeight: engrave.geometry.height, + musicDuration, + audioDuration, + measureCount: parsed.measures.length, + scoreNoteCount: performed.filter((n) => !n.isTieContinuation && !n.isCue).length, + midiNoteCount: midi?.notes.length ?? 0, + mappedNoteCount: mapping.matchedAttacks, + warnings, + unsupported: parsed.unsupported, + }; + + return { + events, + activations: events, + scroll, + meta, + }; +} diff --git a/src/server/geometry.ts b/src/server/geometry.ts new file mode 100644 index 0000000..2011c24 --- /dev/null +++ b/src/server/geometry.ts @@ -0,0 +1,281 @@ +import {DOMParser} from '@xmldom/xmldom'; +import getBounds from 'svg-path-bounds'; + +export type NoteGeometry = {x: number; y: number}; +export type SvgGeometry = { + width: number; + height: number; + notes: Record; + noteCount: number; +}; + +type Matrix = [number, number, number, number, number, number]; + +const IDENTITY: Matrix = [1, 0, 0, 1, 0, 0]; + +function multiply(a: Matrix, b: Matrix): Matrix { + return [ + a[0] * b[0] + a[2] * b[1], + a[1] * b[0] + a[3] * b[1], + a[0] * b[2] + a[2] * b[3], + a[1] * b[2] + a[3] * b[3], + a[0] * b[4] + a[2] * b[5] + a[4], + a[1] * b[4] + a[3] * b[5] + a[5], + ]; +} + +function parseTransform(value: string | null): Matrix { + if (!value) return IDENTITY; + let matrix: Matrix = IDENTITY; + const re = /(matrix|translate|scale|rotate)\s*\(([^)]*)\)/g; + let match: RegExpExecArray | null; + while ((match = re.exec(value))) { + const args = (match[2] ?? '') + .split(/[\s,]+/) + .filter(Boolean) + .map(Number); + let next: Matrix = IDENTITY; + switch (match[1]) { + case 'matrix': + if (args.length >= 6) { + next = [args[0]!, args[1]!, args[2]!, args[3]!, args[4]!, args[5]!]; + } + break; + case 'translate': + next = [1, 0, 0, 1, args[0] ?? 0, args[1] ?? 0]; + break; + case 'scale': { + const sx = args[0] ?? 1; + const sy = args[1] ?? sx; + next = [sx, 0, 0, sy, 0, 0]; + break; + } + case 'rotate': { + const angle = ((args[0] ?? 0) * Math.PI) / 180; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + const cx = args[1] ?? 0; + const cy = args[2] ?? 0; + // translate(cx,cy) rotate(a) translate(-cx,-cy) + next = [ + cos, + sin, + -sin, + cos, + cx - cx * cos + cy * sin, + cy - cx * sin - cy * cos, + ]; + break; + } + default: + break; + } + matrix = multiply(matrix, next); + } + return matrix; +} + +function applyMatrix(m: Matrix, x: number, y: number): [number, number] { + return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]]; +} + +function classList(el: Element): string[] { + return (el.getAttribute('class') ?? '').split(/\s+/).filter(Boolean); +} + +function localName(el: Element): string { + return (el.localName || el.nodeName || '').toLowerCase(); +} + +function childElements(el: Element): Element[] { + const out: Element[] = []; + for (let node = el.firstChild; node; node = node.nextSibling) { + if (node.nodeType === 1) out.push(node as Element); + } + return out; +} + +function findDescendantByClass(el: Element, className: string): Element | null { + const stack: Element[] = [...childElements(el)]; + while (stack.length) { + const current = stack.shift()!; + if (classList(current).includes(className)) return current; + stack.push(...childElements(current)); + } + return null; +} + +function findFirstUse(el: Element): Element | null { + const stack: Element[] = [...childElements(el)]; + while (stack.length) { + const current = stack.shift()!; + if (localName(current) === 'use') return current; + stack.push(...childElements(current)); + } + return null; +} + +function elementBox(el: Element): [number, number, number, number] | null { + const name = localName(el); + try { + if (name === 'path') { + const d = el.getAttribute('d'); + if (!d) return null; + return getBounds(d) as [number, number, number, number]; + } + if (name === 'rect') { + const x = Number(el.getAttribute('x') ?? 0); + const y = Number(el.getAttribute('y') ?? 0); + const w = Number(el.getAttribute('width') ?? 0); + const h = Number(el.getAttribute('height') ?? 0); + return [x, y, x + w, y + h]; + } + if (name === 'ellipse') { + const cx = Number(el.getAttribute('cx') ?? 0); + const cy = Number(el.getAttribute('cy') ?? 0); + const rx = Number(el.getAttribute('rx') ?? 0); + const ry = Number(el.getAttribute('ry') ?? 0); + return [cx - rx, cy - ry, cx + rx, cy + ry]; + } + if (name === 'circle') { + const cx = Number(el.getAttribute('cx') ?? 0); + const cy = Number(el.getAttribute('cy') ?? 0); + const r = Number(el.getAttribute('r') ?? 0); + return [cx - r, cy - r, cx + r, cy + r]; + } + } catch { + return null; + } + return null; +} + +function glyphBounds(glyph: Element): [number, number, number, number] | null { + const stack: Element[] = [...childElements(glyph)]; + let box: [number, number, number, number] | null = null; + while (stack.length) { + const current = stack.shift()!; + const name = localName(current); + if (name === 'g' || name === 'symbol') { + stack.push(...childElements(current)); + continue; + } + const raw = elementBox(current); + if (!raw) continue; + const matrix = parseTransform(current.getAttribute('transform')); + const corners = [ + applyMatrix(matrix, raw[0], raw[1]), + applyMatrix(matrix, raw[2], raw[1]), + applyMatrix(matrix, raw[0], raw[3]), + applyMatrix(matrix, raw[2], raw[3]), + ]; + for (const [x, y] of corners) { + if (!box) box = [x, y, x, y]; + else { + box = [Math.min(box[0], x), Math.min(box[1], y), Math.max(box[2], x), Math.max(box[3], y)]; + } + } + } + return box; +} + +function collectIds(root: Element): Map { + const map = new Map(); + const stack: Element[] = [root]; + while (stack.length) { + const current = stack.pop()!; + const id = current.getAttribute('id'); + if (id) map.set(id, current); + stack.push(...childElements(current)); + } + return map; +} + +export function extractNoteGeometry(svg: string): SvgGeometry { + const doc = new DOMParser().parseFromString(svg, 'image/svg+xml'); + const root = doc.documentElement; + const viewBoxRaw = root.getAttribute('viewBox') ?? ''; + const viewBox = viewBoxRaw.split(/[\s,]+/).filter(Boolean).map(Number); + const width = Number(root.getAttribute('width')) || viewBox[2] || 0; + const height = Number(root.getAttribute('height')) || viewBox[3] || 0; + if (!width || !height) throw new Error('谱面 SVG 缺少 viewBox/宽高。'); + + const allSvgs = doc.getElementsByTagName('svg'); + let innerSvg: Element | null = null; + for (let i = 0; i < allSvgs.length; i++) { + const candidate = allSvgs.item(i) as Element | null; + if (candidate && candidate !== root && classList(candidate).includes('definition-scale')) { + innerSvg = candidate; + break; + } + } + if (!innerSvg) { + for (let i = 0; i < allSvgs.length; i++) { + const candidate = allSvgs.item(i) as Element | null; + if (candidate && candidate !== root) { + innerSvg = candidate; + break; + } + } + } + const innerBoxRaw = innerSvg?.getAttribute('viewBox') ?? viewBoxRaw; + const innerBox = innerBoxRaw.split(/[\s,]+/).filter(Boolean).map(Number); + const outerX = viewBox[0] ?? 0; + const outerY = viewBox[1] ?? 0; + const innerX = innerBox[0] ?? 0; + const innerY = innerBox[1] ?? 0; + const scaleX = (viewBox[2] ?? width) / (innerBox[2] || viewBox[2] || width); + const scaleY = (viewBox[3] ?? height) / (innerBox[3] || viewBox[3] || height); + + const byId = collectIds(root); + const notes: Record = {}; + const allGroups = root.getElementsByTagName('g'); + for (let i = 0; i < allGroups.length; i++) { + const group = allGroups.item(i) as Element | null; + if (!group || !classList(group).includes('note')) continue; + const id = group.getAttribute('id'); + if (!id) continue; + const head = findDescendantByClass(group, 'notehead'); + if (!head) continue; + + let point: [number, number] | null = null; + + const use = findFirstUse(head); + if (use) { + const href = use.getAttribute('xlink:href') || use.getAttribute('href') || ''; + const glyphId = href.startsWith('#') ? href.slice(1) : href; + const glyph = byId.get(glyphId); + const box = glyph ? glyphBounds(glyph) : null; + if (box) { + let p: [number, number] = [(box[0] + box[2]) / 2, (box[1] + box[3]) / 2]; + const useMatrix = multiply( + parseTransform(use.getAttribute('transform')), + parseTransform( + `translate(${Number(use.getAttribute('x') ?? 0)},${Number(use.getAttribute('y') ?? 0)})`, + ), + ); + p = applyMatrix(useMatrix, p[0], p[1]); + point = p; + } + } + + if (!point) { + const box = elementBox(head); + if (!box) continue; + point = [(box[0] + box[2]) / 2, (box[1] + box[3]) / 2]; + } + + // Apply ancestor transforms from the notehead up to (but not including) the inner SVG. + let node: Element | null = head; + while (node && node !== innerSvg && node.nodeType === 1) { + const matrix = parseTransform(node.getAttribute('transform')); + point = applyMatrix(matrix, point[0], point[1]); + node = node.parentNode as Element | null; + } + + const x = outerX + (point[0] - innerX) * scaleX; + const y = outerY + (point[1] - innerY) * scaleY; + notes[id] = {x: Number(x.toFixed(4)), y: Number(y.toFixed(4))}; + } + + return {width, height, notes, noteCount: Object.keys(notes).length}; +} diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 0000000..b690341 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,301 @@ +import path from 'node:path'; +import fs from 'node:fs'; +import express from 'express'; +import cors from 'cors'; +import multer from 'multer'; +import {z} from 'zod'; +import { + DEFAULT_SETTINGS, + MAX_RENDER_SECONDS, + MAX_UPLOAD_BYTES, + type HealthResponse, + type VisualSettings, +} from '../shared/types.js'; +import { + createJob, + getJob, + JOBS_DIR, + jobDirectory, + listJobs, + loadJobsFromDisk, + publicJob, + updateJob, +} from './jobs.js'; +import {buildScrollKeys} from '../shared/scroll.js'; +import {runPipeline} from './pipeline.js'; +import {enqueueRender} from './queue.js'; +import {findMuseScore, museScoreVersion} from './musescore.js'; +import {IS_PRODUCTION, PORT, PUBLIC_BASE_URL} from './config.js'; + +loadJobsFromDisk(); + +const app = express(); +app.use(cors()); +app.use(express.json({limit: '2mb'})); + +const upload = multer({ + storage: multer.memoryStorage(), + limits: {fileSize: MAX_UPLOAD_BYTES, files: 2}, +}); + +const colorSchema = z.string().regex(/^#[0-9a-fA-F]{3,8}$/, '颜色必须是 #RRGGBB'); +const settingsSchema = z + .object({ + width: z.number().int().min(320).max(3840), + height: z.number().int().min(320).max(3840), + fps: z.number().int().min(15).max(60), + introSeconds: z.number().min(0).max(5), + outroSeconds: z.number().min(0).max(15), + scoreHeightRatio: z.number().min(0.18).max(0.82), + scrollLead: z.number().min(0.1).max(0.7), + paperColor: colorSchema, + paperAlpha: z.number().min(0).max(1), + scoreColor: colorSchema, + background: colorSchema, + staffColors: z.array(colorSchema).min(2).max(8), + ballScale: z.number().min(0.4).max(3), + ballLift: z.number().min(0).max(3), + glow: z.number().min(0).max(3), + showTitle: z.boolean(), + showProgress: z.boolean(), + titleFont: z.string().min(1).max(300), + audioOffsetMs: z.number().min(-5000).max(5000), + tiltX: z.number().min(-85).max(85), + tiltY: z.number().min(-60).max(60), + tiltZ: z.number().min(-60).max(60), + perspective: z.number().min(400).max(6000), + tiltZoom: z.number().min(0.5).max(3), + }) + .partial(); + +function normalizeSettings(input: Partial | undefined): VisualSettings { + return {...DEFAULT_SETTINGS, ...(input ?? {})}; +} + +/** Scroll keys depend on the viewport and strip height, so recompute them when those change. */ +function scrollForSettings( + data: {events: import('../shared/types.js').NoteEvent[]; meta: import('../shared/types.js').ScoreMeta}, + settings: VisualSettings, +) { + const displayHeight = settings.height * settings.scoreHeightRatio; + const displayScale = displayHeight / Math.max(1, data.meta.svgHeight); + const viewWidthScore = settings.width / displayScale; + const maxLeft = Math.max(0, data.meta.svgWidth - viewWidthScore); + return buildScrollKeys( + data.events, + settings.scrollLead, + viewWidthScore, + maxLeft, + data.meta.musicDuration, + ); +} + +app.get('/api/health', (_req, res) => { + const musescorePath = findMuseScore(); + const body: HealthResponse = { + ok: true, + musescore: { + available: Boolean(musescorePath), + path: musescorePath ?? undefined, + version: museScoreVersion(), + }, + chrome: { + available: true, + note: 'Remotion 首次渲染会使用或下载 Chrome Headless Shell。', + }, + ffmpeg: { + available: true, + note: 'Remotion 渲染使用内置 FFmpeg。', + }, + version: process.env.npm_package_version ?? '0.1.0', + }; + res.json(body); +}); + +app.get('/api/jobs', (_req, res) => { + res.json({jobs: listJobs().slice(0, 30).map((job) => publicJob(job))}); +}); + +app.post('/api/jobs', upload.fields([{name: 'score', maxCount: 1}, {name: 'audio', maxCount: 1}]), (req, res) => { + const files = req.files as Record | undefined; + const score = files?.score?.[0]; + if (!score) { + res.status(400).json({error: '请选择 MusicXML / MXL / MIDI 谱面文件。'}); + return; + } + if (score.size > MAX_UPLOAD_BYTES) { + res.status(413).json({error: '文件超过 30MB 限制。'}); + return; + } + let settings: VisualSettings = DEFAULT_SETTINGS; + const rawSettings = typeof req.body?.settings === 'string' ? req.body.settings : null; + if (rawSettings) { + try { + const parsed = settingsSchema.parse(JSON.parse(rawSettings)); + settings = normalizeSettings(parsed); + } catch (error) { + res.status(400).json({error: `设置无效:${error instanceof Error ? error.message : String(error)}`}); + return; + } + } + const metadata = { + title: typeof req.body?.title === 'string' ? req.body.title : undefined, + subtitle: typeof req.body?.subtitle === 'string' ? req.body.subtitle : undefined, + composer: typeof req.body?.composer === 'string' ? req.body.composer : undefined, + }; + const audio = files?.audio?.[0]; + const job = createJob({ + settings, + inputName: score.originalname, + inputFormat: path.extname(score.originalname).toLowerCase().replace('.', ''), + }); + void runPipeline({ + jobId: job.id, + file: {buffer: score.buffer, filename: score.originalname}, + audio: audio ? {buffer: audio.buffer, filename: audio.originalname} : undefined, + settings, + metadata, + }); + res.status(202).json({id: job.id}); +}); + +app.get('/api/jobs/:id', (req, res) => { + const job = getJob(req.params.id); + if (!job) { + res.status(404).json({error: '任务不存在。'}); + return; + } + res.json(publicJob(job)); +}); + +app.patch('/api/jobs/:id/settings', (req, res) => { + const job = getJob(req.params.id); + if (!job) { + res.status(404).json({error: '任务不存在。'}); + return; + } + if (!job.data) { + res.status(409).json({error: '任务还没有生成预览数据。'}); + return; + } + if (job.status === 'rendering') { + res.status(409).json({error: '正在渲染,暂时不能修改参数。'}); + return; + } + + let parsedSettings: Partial; + try { + parsedSettings = settingsSchema.parse(req.body?.settings ?? {}); + } catch (error) { + res.status(400).json({error: `设置无效:${error instanceof Error ? error.message : String(error)}`}); + return; + } + + const settings = normalizeSettings({ + ...job.data.settings, + ...job.settings, + ...parsedSettings, + }); + + const meta = {...job.data.meta}; + const metadata = req.body?.metadata as + | {title?: unknown; subtitle?: unknown; composer?: unknown} + | undefined; + if (typeof metadata?.title === 'string' && metadata.title.trim()) meta.title = metadata.title.trim(); + if (typeof metadata?.subtitle === 'string' && metadata.subtitle.trim()) { + meta.subtitle = metadata.subtitle.trim(); + } + if (typeof metadata?.composer === 'string' && metadata.composer.trim()) { + meta.composer = metadata.composer.trim(); + } + + const data = { + ...job.data, + meta, + settings, + scroll: scrollForSettings(job.data, settings), + }; + + fs.writeFileSync(path.join(jobDirectory(job.id), 'score.json'), JSON.stringify(data, null, 2), 'utf8'); + const updated = updateJob(job.id, { + settings, + data, + ...(job.status === 'done' + ? { + status: 'ready' as const, + stage: 'done' as const, + progress: 100, + message: '参数已更新,可以重新导出。', + videoFile: undefined, + } + : {}), + }); + res.json(updated ? publicJob(updated) : {error: '任务不存在。'}); +}); + +app.post('/api/jobs/:id/render', (req, res) => { + const job = getJob(req.params.id); + if (!job) { + res.status(404).json({error: '任务不存在。'}); + return; + } + if (!job.data) { + res.status(409).json({error: '任务还没有生成预览数据。'}); + return; + } + if (job.data.meta.audioDuration > MAX_RENDER_SECONDS || job.data.meta.musicDuration > MAX_RENDER_SECONDS) { + res.status(400).json({ + error: `这段音乐超过 ${Math.round(MAX_RENDER_SECONDS / 60)} 分钟,请先截取片段再渲染。`, + }); + return; + } + if (job.status === 'rendering') { + res.json({ok: true, alreadyRunning: true}); + return; + } + updateJob(job.id, {message: '已加入渲染队列…'}); + enqueueRender(job.id); + res.json({ok: true}); +}); + +app.get('/api/jobs/:id/video', (req, res) => { + const job = getJob(req.params.id); + if (!job || !job.videoFile) { + res.status(404).json({error: '视频还没有生成。'}); + return; + } + const file = path.join(JOBS_DIR, job.id, job.videoFile); + if (!fs.existsSync(file)) { + res.status(404).json({error: '视频文件不存在。'}); + return; + } + const safeName = (job.data?.meta.title || 'music-visualizer').replace(/[\\/:*?"<>|]/g, '_').slice(0, 80); + res.download(file, `${safeName}.mp4`); +}); + +app.use('/jobs', express.static(JOBS_DIR, {maxAge: '1h'})); +app.use('/demo', express.static(path.resolve('public/demo'), {maxAge: '1h'})); + +if (IS_PRODUCTION) { + const clientDir = path.resolve('dist/client'); + app.use(express.static(clientDir)); + app.get('*', (_req, res) => { + res.sendFile(path.join(clientDir, 'index.html')); + }); +} + +app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => { + if (error instanceof multer.MulterError) { + res.status(error.code === 'LIMIT_FILE_SIZE' ? 413 : 400).json({error: error.message}); + return; + } + console.error(error); + res.status(500).json({error: error instanceof Error ? error.message : '服务器错误。'}); +}); + +app.listen(PORT, () => { + console.log(`Music Visualizer API: http://127.0.0.1:${PORT}`); + console.log(`Public base URL for render: ${PUBLIC_BASE_URL}`); + console.log(`MuseScore: ${findMuseScore() ?? 'not found'}`); + if (!IS_PRODUCTION) console.log('Client dev server: http://127.0.0.1:5173'); +}); diff --git a/src/server/jobs.ts b/src/server/jobs.ts new file mode 100644 index 0000000..9c5d566 --- /dev/null +++ b/src/server/jobs.ts @@ -0,0 +1,130 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import {nanoid} from 'nanoid'; +import type {JobPublic, JobStage, JobStatus, RenderData, VisualSettings} from '../shared/types.js'; + +export const DATA_DIR = path.resolve(process.env.DATA_DIR ?? 'data'); +export const JOBS_DIR = path.join(DATA_DIR, 'jobs'); + +export type JobRecord = { + id: string; + status: JobStatus; + stage: JobStage; + progress: number; + message: string; + error?: string; + createdAt: string; + updatedAt: string; + inputName?: string; + inputFormat?: string; + settings: VisualSettings; + data?: RenderData; + warnings: string[]; + unsupported: string[]; + videoFile?: string; + audioFile?: string; + audioMime?: string; +}; + +const jobs = new Map(); + +function jobFile(id: string): string { + return path.join(JOBS_DIR, id, 'job.json'); +} + +function ensureDirs(): void { + fs.mkdirSync(JOBS_DIR, {recursive: true}); +} + +function persist(job: JobRecord): void { + ensureDirs(); + const dir = path.join(JOBS_DIR, job.id); + fs.mkdirSync(dir, {recursive: true}); + fs.writeFileSync(jobFile(job.id), JSON.stringify(job, null, 2), 'utf8'); +} + +export function loadJobsFromDisk(): void { + ensureDirs(); + for (const entry of fs.readdirSync(JOBS_DIR, {withFileTypes: true})) { + if (!entry.isDirectory()) continue; + const file = jobFile(entry.name); + if (!fs.existsSync(file)) continue; + try { + const job = JSON.parse(fs.readFileSync(file, 'utf8')) as JobRecord; + if (job.status === 'running' || job.status === 'rendering' || job.status === 'queued') { + job.status = 'error'; + job.stage = 'done'; + job.message = '服务重启,任务已中断。'; + job.error = '服务在任务完成前重启。请重新生成。'; + } + jobs.set(job.id, job); + } catch { + // Ignore broken job metadata. + } + } +} + +export function createJob(input: { + settings: VisualSettings; + inputName: string; + inputFormat: string; +}): JobRecord { + const id = nanoid(10); + const now = new Date().toISOString(); + const job: JobRecord = { + id, + status: 'queued', + stage: 'queued', + progress: 0, + message: '等待处理…', + createdAt: now, + updatedAt: now, + inputName: input.inputName, + inputFormat: input.inputFormat, + settings: input.settings, + warnings: [], + unsupported: [], + }; + jobs.set(id, job); + fs.mkdirSync(path.join(JOBS_DIR, id), {recursive: true}); + persist(job); + return job; +} + +export function getJob(id: string): JobRecord | null { + return jobs.get(id) ?? null; +} + +export function updateJob(id: string, patch: Partial): JobRecord | null { + const job = jobs.get(id); + if (!job) return null; + Object.assign(job, patch, {updatedAt: new Date().toISOString()}); + persist(job); + return job; +} + +export function listJobs(): JobRecord[] { + return [...jobs.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); +} + +export function jobDirectory(id: string): string { + return path.join(JOBS_DIR, id); +} + +export function publicJob(job: JobRecord): JobPublic { + return { + id: job.id, + status: job.status, + stage: job.stage, + progress: job.progress, + message: job.message, + error: job.error, + warnings: job.warnings.length ? job.warnings : undefined, + unsupported: job.unsupported.length ? job.unsupported : undefined, + data: job.status === 'ready' || job.status === 'rendering' || job.status === 'done' ? job.data : undefined, + videoUrl: job.videoFile ? `/jobs/${job.id}/${job.videoFile}` : undefined, + createdAt: job.createdAt, + inputName: job.inputName, + inputFormat: job.inputFormat, + }; +} diff --git a/src/server/midi.ts b/src/server/midi.ts new file mode 100644 index 0000000..9b6e68b --- /dev/null +++ b/src/server/midi.ts @@ -0,0 +1,47 @@ +import fs from 'node:fs'; +import pkg from '@tonejs/midi'; + +const {Midi} = pkg; + +export type MidiNote = { + time: number; + duration: number; + midi: number; + velocity: number; + channel: number; + track: number; +}; + +export type ParsedMidi = { + notes: MidiNote[]; + duration: number; + trackCount: number; + tempos: {time: number; bpm: number}[]; +}; + +export function parseMidiFile(filePath: string): ParsedMidi { + const buffer = fs.readFileSync(filePath); + const midi = new Midi(buffer); + const notes: MidiNote[] = []; + midi.tracks.forEach((track, trackIndex) => { + const channel = track.channel ?? 0; + if (channel === 9) return; // percussion + for (const note of track.notes) { + notes.push({ + time: note.time, + duration: note.duration, + midi: note.midi, + velocity: note.velocity, + channel, + track: trackIndex, + }); + } + }); + notes.sort((a, b) => a.time - b.time || a.midi - b.midi || a.duration - b.duration); + return { + notes, + duration: midi.duration, + trackCount: midi.tracks.length, + tempos: midi.header.tempos.map((tempo) => ({time: tempo.time ?? 0, bpm: tempo.bpm})), + }; +} diff --git a/src/server/musescore.ts b/src/server/musescore.ts new file mode 100644 index 0000000..2205542 --- /dev/null +++ b/src/server/musescore.ts @@ -0,0 +1,159 @@ +import {spawn, spawnSync} from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +let cachedPath: string | null | undefined; + +function exists(file: string | undefined): file is string { + if (!file) return false; + try { + return fs.statSync(file).isFile(); + } catch { + return false; + } +} + +function fromPath(names: string[]): string | null { + for (const name of names) { + const command = process.platform === 'win32' ? 'where.exe' : 'which'; + const result = spawnSync(command, [name], {encoding: 'utf8', windowsHide: true}); + if (result.status === 0) { + const first = result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean); + if (exists(first)) return first; + } + } + return null; +} + +export function findMuseScore(): string | null { + if (cachedPath !== undefined) return cachedPath; + const candidates: string[] = []; + if (process.env.MUSESCORE_PATH) candidates.push(process.env.MUSESCORE_PATH); + if (process.platform === 'win32') { + candidates.push( + 'C:\\Program Files\\MuseScore 4\\bin\\MuseScore4.exe', + 'C:\\Program Files\\MuseScore 4\\bin\\mscore.exe', + 'C:\\Program Files\\MuseScore 4\\bin\\musescore.exe', + 'C:\\Program Files (x86)\\MuseScore 4\\bin\\MuseScore4.exe', + 'C:\\Program Files\\MuseScore 3\\bin\\mscore.exe', + ); + } else if (process.platform === 'darwin') { + candidates.push( + '/Applications/MuseScore 4.app/Contents/MacOS/mscore', + '/Applications/MuseScore 4.app/Contents/MacOS/MuseScore 4', + '/Applications/MuseScore 3.app/Contents/MacOS/mscore', + ); + } + for (const candidate of candidates) { + if (exists(candidate)) { + cachedPath = candidate; + return candidate; + } + } + cachedPath = fromPath(['MuseScore4', 'musescore4', 'mscore', 'musescore']); + return cachedPath; +} + +export function hasMuseScore(): boolean { + return Boolean(findMuseScore()); +} + +export class MuseScoreError extends Error { + constructor(message: string) { + super(message); + this.name = 'MuseScoreError'; + } +} + +function run(command: string, args: string[], timeoutMs: number): Promise<{stdout: string; stderr: string}> { + return new Promise((resolve, reject) => { + const child = spawn(command, args, {windowsHide: true}); + let stdout = ''; + let stderr = ''; + let settled = false; + const timer = setTimeout(() => { + if (settled) return; + settled = true; + child.kill(); + reject(new MuseScoreError(`MuseScore 处理超时(${Math.round(timeoutMs / 1000)} 秒)。`)); + }, timeoutMs); + child.stdout?.on('data', (chunk) => { + stdout += String(chunk); + }); + child.stderr?.on('data', (chunk) => { + stderr += String(chunk); + }); + child.on('error', (error) => { + if (settled) return; + settled = true; + clearTimeout(timer); + reject(new MuseScoreError(`无法启动 MuseScore:${error.message}`)); + }); + child.on('close', (code) => { + if (settled) return; + settled = true; + clearTimeout(timer); + if (code === 0) resolve({stdout, stderr}); + else { + const detail = (stderr || stdout).trim().slice(-1200); + reject(new MuseScoreError(`MuseScore 退出码 ${code}${detail ? `:${detail}` : ''}`)); + } + }); + }); +} + +async function exportOne(input: string, output: string, timeoutMs: number): Promise { + const executable = findMuseScore(); + if (!executable) throw new MuseScoreError('没有找到 MuseScore 4。请安装 MuseScore,或设置 MUSESCORE_PATH。'); + fs.mkdirSync(path.dirname(output), {recursive: true}); + if (fs.existsSync(output)) fs.rmSync(output, {force: true}); + await run(executable, ['-o', output, input], timeoutMs); + if (!fs.existsSync(output) || fs.statSync(output).size === 0) { + throw new MuseScoreError(`MuseScore 没有生成 ${path.basename(output)}。请检查输入谱面。`); + } +} + +export async function exportWav(inputMusicXml: string, outputDir: string): Promise { + fs.mkdirSync(outputDir, {recursive: true}); + const wav = path.join(outputDir, 'audio.wav'); + await exportOne(inputMusicXml, wav, 5 * 60 * 1000); + return wav; +} + +export async function exportMidi(inputMusicXml: string, outputDir: string): Promise { + fs.mkdirSync(outputDir, {recursive: true}); + const midi = path.join(outputDir, 'performance.mid'); + await exportOne(inputMusicXml, midi, 5 * 60 * 1000); + return midi; +} + +export async function exportAudioAndMidi(inputMusicXml: string, outputDir: string): Promise<{wav: string; midi: string}> { + const wav = await exportWav(inputMusicXml, outputDir); + const midi = await exportMidi(inputMusicXml, outputDir); + return {wav, midi}; +} + +export async function convertMidiToMusicXml(inputMidi: string, outputMusicXml: string): Promise { + await exportOne(inputMidi, outputMusicXml, 5 * 60 * 1000); +} + +/** + * Round-trip a score through MuseScore so it regains the engraving information + * (beams, tuplet grouping) that MIDI-derived MusicXML files often lack. + */ +export async function normalizeMusicXml(inputMusicXml: string, outputMusicXml: string): Promise { + await exportOne(inputMusicXml, outputMusicXml, 5 * 60 * 1000); +} + +export function museScoreVersion(): string | undefined { + const executable = findMuseScore(); + if (!executable) return undefined; + try { + const result = spawnSync(executable, ['--version'], {encoding: 'utf8', windowsHide: true, timeout: 10000}); + const text = `${result.stdout ?? ''} ${result.stderr ?? ''}`.trim(); + const match = text.match(/(?:MuseScore|mscore)\s*([0-9][^\s]*)/i); + return match?.[1] ?? text.split(/\r?\n/)[0]?.slice(0, 80); + } catch { + return undefined; + } +} diff --git a/src/server/musicxml.ts b/src/server/musicxml.ts new file mode 100644 index 0000000..9b88fed --- /dev/null +++ b/src/server/musicxml.ts @@ -0,0 +1,665 @@ +import {DOMParser, XMLSerializer} from '@xmldom/xmldom'; +import {unzipSync} from 'fflate'; + +export type ParsedNote = { + id: string; + partIndex: number; + measureIndex: number; + noteIndex: number; + staff: number; + /** Stable visual lane: partIndex * 10 + staff. */ + lane: number; + voice: string; + pitch: number | null; + isRest: boolean; + isGrace: boolean; + isChord: boolean; + /** Quarter-note units within the measure. */ + onset: number; + duration: number; + tieStart: boolean; + tieStop: boolean; + type: string; + dots: number; + cue: boolean; +}; + +export type EndingGroup = { + start: number; + end: number; + numbers: number[]; +}; + +export type TempoEvent = { + measureIndex: number; + /** Quarter-note offset inside the measure. */ + beat: number; + bpm: number; +}; + +export type ParsedMeasure = { + index: number; + id: string; + number: string; + partIndex: number; + repeatForward: boolean; + repeatBackward: boolean; + repeatTimes: number | null; + forwardTimes: number | null; + endingStart: boolean; + endingStop: boolean; + endingNumbers: number[] | null; + endingGroup: EndingGroup | null; + dacapo: boolean; + dalsegno: boolean; + tocoda: boolean; + fine: boolean; + segno: boolean; + coda: boolean; + tempoMarks: number; + fermataCount: number; + cueCount: number; + /** Notated length in quarter notes, including trailing rests. */ + durationQuarters: number; +}; + +export type ParsedPart = { + index: number; + id: string; + name: string; + staves: number; + measures: ParsedMeasure[]; +}; + +export type ParsedScore = { + title: string; + subtitle: string; + composer: string; + parts: ParsedPart[]; + /** First part's measure timeline. */ + measures: ParsedMeasure[]; + /** All notes grouped by measure index across parts. */ + notesByMeasure: ParsedNote[][]; + allNotes: ParsedNote[]; + /** Tempo marks from the first part, sorted by measure and beat. */ + tempoEvents: TempoEvent[]; + annotatedXml: string; + originalXml: string; + warnings: string[]; + unsupported: string[]; +}; + +const STEP_SEMITONES: Record = { + C: 0, + D: 2, + E: 4, + F: 5, + G: 7, + A: 9, + B: 11, +}; + +function childElements(el: Element, localName?: string): Element[] { + const out: Element[] = []; + for (let node = el.firstChild; node; node = node.nextSibling) { + if (node.nodeType !== 1) continue; + const element = node as Element; + const name = element.localName || element.nodeName; + if (!localName || name === localName) out.push(element); + } + return out; +} + +function firstChild(el: Element, localName: string): Element | null { + for (let node = el.firstChild; node; node = node.nextSibling) { + if (node.nodeType !== 1) continue; + const element = node as Element; + if ((element.localName || element.nodeName) === localName) return element; + } + return null; +} + +function childText(el: Element, localName: string): string | null { + return firstChild(el, localName)?.textContent?.trim() ?? null; +} + +function descendants(el: Element, localName: string): Element[] { + const nodes = el.getElementsByTagName(localName); + return Array.from({length: nodes.length}, (_, i) => nodes.item(i) as Element | null).filter( + (node): node is Element => Boolean(node), + ); +} + +function textOfDescendant(el: Element, localName: string): string | null { + return descendants(el, localName)[0]?.textContent?.trim() ?? null; +} + +function parsePitch(note: Element): number | null { + const pitch = firstChild(note, 'pitch'); + if (!pitch) return null; + const step = childText(pitch, 'step'); + const octaveRaw = childText(pitch, 'octave'); + if (!step || octaveRaw === null) return null; + const octave = Number(octaveRaw); + const alter = Number(childText(pitch, 'alter') ?? '0'); + const semitone = STEP_SEMITONES[step.toUpperCase()]; + if (!Number.isFinite(octave) || semitone === undefined || !Number.isFinite(alter)) return null; + return 12 * (octave + 1) + semitone + alter; +} + +function hasTie(note: Element, type: 'start' | 'stop'): boolean { + const direct = descendants(note, 'tie').some((tie) => tie.getAttribute('type') === type); + if (direct) return true; + const tied = descendants(note, 'tied').some((tie) => tie.getAttribute('type') === type); + return tied; +} + +function readSoundFlags(el: Element | null): { + dacapo: boolean; + dalsegno: boolean; + tocoda: boolean; + fine: boolean; +} { + const result = {dacapo: false, dalsegno: false, tocoda: false, fine: false}; + if (!el) return result; + for (const sound of descendants(el, 'sound')) { + if (sound.getAttribute('dacapo') === 'yes') result.dacapo = true; + if (sound.getAttribute('dalsegno')) result.dalsegno = true; + if (sound.getAttribute('tocoda')) result.tocoda = true; + if (sound.getAttribute('fine') === 'yes') result.fine = true; + } + return result; +} + +function parseMeasure( + measure: Element, + partIndex: number, + measureIndex: number, + measureId: string, + noteIndexStart: number, + divisionsRef: {value: number}, + stavesRef: {value: number}, + notes: ParsedNote[], + warnings: string[], + tempoEvents: TempoEvent[], +): {measure: ParsedMeasure; noteIndexEnd: number} { + let cursor = 0; + let maxCursor = 0; + let lastNonChordOnset = 0; + let noteIndex = noteIndexStart; + let repeatForward = false; + let repeatBackward = false; + let repeatTimes: number | null = null; + let forwardTimes: number | null = null; + let endingNumbers: number[] | null = null; + let endingStart = false; + let endingStop = false; + let soundFlags = {dacapo: false, dalsegno: false, tocoda: false, fine: false}; + let segno = false; + let coda = false; + let tempoMarks = 0; + let fermataCount = 0; + let cueCount = 0; + + for (const child of childElements(measure)) { + const name = child.localName || child.nodeName; + if (name === 'attributes') { + const div = Number(childText(child, 'divisions') ?? ''); + if (Number.isFinite(div) && div > 0) divisionsRef.value = div; + const staves = Number(childText(child, 'staves') ?? ''); + if (Number.isFinite(staves) && staves > 0) stavesRef.value = Math.max(stavesRef.value, staves); + continue; + } + if (name === 'backup' || name === 'forward') { + const raw = Number(childText(child, 'duration') ?? '0'); + if (Number.isFinite(raw)) { + const quarter = raw / divisionsRef.value; + cursor += name === 'backup' ? -quarter : quarter; + maxCursor = Math.max(maxCursor, cursor); + } + continue; + } + if (name === 'barline') { + for (const repeat of descendants(child, 'repeat')) { + const direction = repeat.getAttribute('direction'); + const times = repeat.getAttribute('times'); + if (direction === 'forward') { + repeatForward = true; + if (times) forwardTimes = Math.max(2, Number(times) || 2); + } else if (direction === 'backward') { + repeatBackward = true; + if (times) repeatTimes = Math.max(2, Number(times) || 2); + } + } + for (const ending of descendants(child, 'ending')) { + const type = ending.getAttribute('type'); + if (type === 'start') { + endingStart = true; + const raw = ending.getAttribute('number') ?? ''; + endingNumbers = raw + .split(/[,\s]+/) + .map((n) => Number(n)) + .filter((n) => Number.isFinite(n) && n > 0); + } else if (type === 'stop') { + endingStop = true; + } else if (type === 'discontinue') { + endingStop = true; + } + } + soundFlags = {...soundFlags, ...readSoundFlags(child)}; + continue; + } + if (name === 'direction') { + const soundTempo = descendants(child, 'sound').find((sound) => sound.getAttribute('tempo')); + const metronome = descendants(child, 'metronome')[0]; + if (soundTempo || metronome) { + tempoMarks++; + let bpm: number | null = null; + if (soundTempo) { + const value = Number(soundTempo.getAttribute('tempo')); + if (Number.isFinite(value) && value > 0) bpm = value; + } + if (bpm === null && metronome) bpm = metronomeToQpm(metronome); + if (bpm !== null && partIndex === 0) { + tempoEvents.push({measureIndex, beat: Math.max(0, cursor), bpm}); + } + } + if (descendants(child, 'segno').length) segno = true; + if (descendants(child, 'coda').length) coda = true; + const words = descendants(child, 'words') + .map((w) => w.textContent?.trim() ?? '') + .join(' '); + if (/\bD\.?\s*C\.?\b/i.test(words) || /da\s*capo/i.test(words)) soundFlags.dacapo = true; + if (/\bD\.?\s*S\.?\b/i.test(words) || /dal\s*segno/i.test(words)) soundFlags.dalsegno = true; + soundFlags = {...soundFlags, ...readSoundFlags(child)}; + continue; + } + if (name !== 'note') continue; + + const noteId = `note-${partIndex}-${measureIndex}-${noteIndex}`; + child.setAttribute('id', noteId); + noteIndex++; + + const isRest = Boolean(firstChild(child, 'rest')); + const isGrace = Boolean(firstChild(child, 'grace')); + const isChord = Boolean(firstChild(child, 'chord')); + const isCue = Boolean(firstChild(child, 'cue')); + const durationRaw = Number(childText(child, 'duration') ?? '0'); + const duration = Number.isFinite(durationRaw) && durationRaw > 0 ? durationRaw / divisionsRef.value : 0; + const staff = Number(childText(child, 'staff') ?? '1') || 1; + const voice = childText(child, 'voice') ?? '1'; + const pitch = parsePitch(child); + const onset = isChord ? lastNonChordOnset : cursor; + + if (isChord && lastNonChordOnset === 0 && cursor === 0) { + warnings.push(`第 ${measureIndex + 1} 小节出现无前导音的 chord,按当前拍位处理。`); + } + if (!isChord && !isGrace) { + lastNonChordOnset = cursor; + cursor += duration; + maxCursor = Math.max(maxCursor, cursor); + } + if (isCue) cueCount++; + if (descendants(child, 'fermata').length) fermataCount++; + + notes.push({ + id: noteId, + partIndex, + measureIndex, + noteIndex: noteIndex - 1, + staff, + lane: partIndex * 10 + staff, + voice, + pitch, + isRest, + isGrace, + isChord, + onset, + duration, + tieStart: hasTie(child, 'start'), + tieStop: hasTie(child, 'stop'), + type: childText(child, 'type') ?? '', + dots: descendants(child, 'dot').length, + cue: isCue, + }); + } + + const parsedMeasure: ParsedMeasure = { + index: measureIndex, + id: measureId, + number: measure.getAttribute('number') ?? String(measureIndex + 1), + partIndex, + repeatForward, + repeatBackward, + repeatTimes, + forwardTimes, + endingStart, + endingStop, + endingNumbers, + endingGroup: null, + ...soundFlags, + segno, + coda, + tempoMarks, + fermataCount, + cueCount, + durationQuarters: Math.max(maxCursor, 0), + }; + return {measure: parsedMeasure, noteIndexEnd: noteIndex}; +} + +const NOTE_VALUE: Record = { + whole: 1, + half: 2, + quarter: 4, + eighth: 8, + '16th': 16, + '32nd': 32, + '64th': 64, +}; + +function metronomeToQpm(metronome: Element): number | null { + const unit = childText(metronome, 'beat-unit'); + const perMinute = Number(childText(metronome, 'per-minute') ?? ''); + if (!unit || !Number.isFinite(perMinute) || perMinute <= 0) return null; + const value = NOTE_VALUE[unit]; + if (!value) return null; + const dots = descendants(metronome, 'beat-unit-dot').length; + const quarterLength = (4 / value) * (2 - 1 / Math.pow(2, dots)); + return perMinute * quarterLength; +} + +function tempoAt(measureIndex: number, beat: number, tempoEvents: TempoEvent[]): number { + let bpm = 120; + for (const event of tempoEvents) { + if (event.measureIndex < measureIndex || (event.measureIndex === measureIndex && event.beat <= beat + 1e-9)) { + bpm = event.bpm; + } else { + break; + } + } + return bpm > 0 ? bpm : 120; +} + +/** Notated duration in seconds from the first part's tempo map. */ +export function measureDurationSeconds(measure: ParsedMeasure, tempoEvents: TempoEvent[]): number { + const quarters = measure.durationQuarters > 0 ? measure.durationQuarters : 4; + const changes = tempoEvents + .filter((event) => event.measureIndex === measure.index && event.beat > 0 && event.beat < quarters) + .sort((a, b) => a.beat - b.beat); + let bpm = tempoAt(measure.index, 0, tempoEvents); + let cursor = 0; + let seconds = 0; + for (const change of changes) { + const beat = Math.max(cursor, Math.min(quarters, change.beat)); + if (beat > cursor) { + seconds += ((beat - cursor) * 60) / bpm; + cursor = beat; + } + bpm = change.bpm; + } + seconds += ((quarters - cursor) * 60) / bpm; + return Math.max(0.01, seconds); +} + +function attachEndingGroups(measures: ParsedMeasure[]): void { + let current: EndingGroup | null = null; + for (const measure of measures) { + if (measure.endingStart) { + if (current) current.end = measure.index - 1; + current = { + start: measure.index, + end: measure.index, + numbers: measure.endingNumbers ?? [], + }; + } + if (current) { + measure.endingGroup = current; + current.end = Math.max(current.end, measure.index); + } + if (measure.endingStop) { + if (current) { + current.end = measure.index; + current = null; + } + } + } +} + +/** + * Build the performed measure order from repeat and ending marks. + * Supports sequential and nested repeats, and the common 1st/2nd ending form. + */ +export function buildPerformanceOrder( + measures: ParsedMeasure[], + warnings: string[], +): {order: number[]; passes: number[]} { + const order: number[] = []; + const passes: number[] = []; + type Frame = {start: number; times: number; plays: number; pass: number}; + const stack: Frame[] = []; + let i = 0; + let steps = 0; + + while (i < measures.length && steps < 20000) { + steps++; + const m = measures[i]; + if (!m) break; + + // If we're inside an ending group that doesn't apply to the current repeat pass, + // skip the whole group. This is what makes 1st/2nd endings work. + if (m.endingGroup && stack.length) { + const frame = stack[stack.length - 1]!; + if (!m.endingGroup.numbers.includes(frame.pass)) { + i = m.endingGroup.end + 1; + continue; + } + } + + if (m.repeatForward) { + const top = stack[stack.length - 1]; + if (!top || top.start !== i) { + const times = Math.max(2, m.forwardTimes ?? m.repeatTimes ?? 2); + stack.push({start: i, times, plays: 1, pass: 1}); + } + } + + order.push(i); + passes.push(stack.length ? stack[stack.length - 1]!.pass : 1); + + if (m.repeatBackward) { + const frame = stack[stack.length - 1]; + if (frame && frame.start <= i && frame.times === 2 && m.repeatTimes && m.repeatTimes > 2) { + frame.times = m.repeatTimes; + } + if (!frame || frame.start > i) { + warnings.push(`第 ${i + 1} 小节有反复结束标记但找不到对应开始,已忽略。`); + } else if (frame.plays < frame.times) { + frame.plays++; + frame.pass = frame.plays; + i = frame.start; + continue; + } else { + stack.pop(); + } + } + i++; + } + + if (stack.length) { + warnings.push('存在未闭合的反复开始标记;已按顺序播放到结尾。'); + } + if (steps >= 20000) { + warnings.push('反复展开超过安全上限,可能包含异常嵌套;已截断。'); + } + return {order, passes}; +} + +export function needsRepeatUnroll(measures: ParsedMeasure[]): boolean { + return measures.some((m) => m.repeatForward || m.repeatBackward); +} + +export function detectUnsupported(parts: ParsedPart[], measures: ParsedMeasure[]): string[] { + const unsupported = new Set(); + for (const part of parts) { + for (const measure of part.measures) { + if (measure.dacapo) unsupported.add('D.C.(da capo)跳转'); + if (measure.dalsegno) unsupported.add('D.S.(dal segno)跳转'); + if (measure.tocoda) unsupported.add('To Coda 跳转'); + if (measure.fine) unsupported.add('Fine 终止标记'); + } + } + if (measures.some((m) => m.fermataCount > 0)) { + unsupported.add('延长记号(fermata)的额外时值'); + } + return [...unsupported]; +} + +export function parseMusicXml(input: Buffer | string): ParsedScore { + const originalXml = typeof input === 'string' ? input : input.toString('utf8'); + const xml = originalXml.replace(/^\uFEFF/, ''); + const doc = new DOMParser().parseFromString(xml, 'text/xml'); + + const root = doc.documentElement; + if (!root) throw new Error('MusicXML 为空或无法解析。'); + if (root.localName === 'parsererror' || root.nodeName === 'parsererror') { + throw new Error('MusicXML 格式不正确(parsererror)。'); + } + + const warnings: string[] = []; + const title = + textOfDescendant(root, 'work-title') ?? + textOfDescendant(root, 'movement-title') ?? + ''; + const subtitle = textOfDescendant(root, 'movement-title') ?? ''; + const composer = + descendants(root, 'creator') + .find((creator) => creator.getAttribute('type') === 'composer') + ?.textContent?.trim() ?? ''; + + const partList = descendants(root, 'score-part'); + const partNames = new Map(); + for (const part of partList) { + const id = part.getAttribute('id'); + if (id) partNames.set(id, childText(part, 'part-name') ?? id); + } + + const partElements = descendants(root, 'part'); + if (!partElements.length) throw new Error('MusicXML 中没有找到 。'); + if (partElements.length > 4) { + warnings.push(`检测到 ${partElements.length} 个 part,单行谱面会很高;建议只保留需要的声部。`); + } + + const parts: ParsedPart[] = []; + const notesByMeasure: ParsedNote[][] = []; + const allNotes: ParsedNote[] = []; + const tempoEvents: TempoEvent[] = []; + let maxMeasureCount = 0; + let timeline: ParsedMeasure[] = []; + + partElements.forEach((partElement, partIndex) => { + const partId = partElement.getAttribute('id') ?? `P${partIndex + 1}`; + const measures: ParsedMeasure[] = []; + const divisionsRef = {value: 1}; + const stavesRef = {value: 1}; + let noteIndex = 0; + const measureElements = childElements(partElement, 'measure'); + measureElements.forEach((measureElement, measureIndex) => { + const measureId = `meas-${partIndex}-${measureIndex}`; + measureElement.setAttribute('id', measureId); + const result = parseMeasure( + measureElement, + partIndex, + measureIndex, + measureId, + noteIndex, + divisionsRef, + stavesRef, + allNotes, + warnings, + tempoEvents, + ); + noteIndex = result.noteIndexEnd; + measures.push(result.measure); + }); + attachEndingGroups(measures); + parts.push({ + index: partIndex, + id: partId, + name: partNames.get(partId) ?? partId, + staves: stavesRef.value, + measures, + }); + if (measures.length > maxMeasureCount) { + maxMeasureCount = measures.length; + timeline = measures; + } + }); + + for (const note of allNotes) { + const bucket = (notesByMeasure[note.measureIndex] ??= []); + bucket.push(note); + } + + // Parts may disagree on measure count. Pad the timeline so note measure indices remain valid. + if (parts.some((p) => p.measures.length !== timeline.length)) { + warnings.push('各 part 的小节数不一致;以最长 part 的时间线为准。'); + } + + const annotatedXml = new XMLSerializer().serializeToString(doc); + const unsupported = detectUnsupported(parts, timeline); + tempoEvents.sort((a, b) => a.measureIndex - b.measureIndex || a.beat - b.beat); + const dedupedTempo: TempoEvent[] = []; + for (const event of tempoEvents) { + const prev = dedupedTempo[dedupedTempo.length - 1]; + if (prev && prev.measureIndex === event.measureIndex && Math.abs(prev.beat - event.beat) < 1e-6) { + prev.bpm = event.bpm; + } else { + dedupedTempo.push({...event}); + } + } + + return { + title, + subtitle, + composer, + parts, + measures: timeline, + notesByMeasure, + allNotes, + tempoEvents: dedupedTempo, + annotatedXml, + originalXml: xml, + warnings, + unsupported, + }; +} + +export function readMusicXmlFile(filename: string, buffer: Buffer): {xml: string; format: string} { + const lower = filename.toLowerCase(); + if (lower.endsWith('.mxl')) { + const files = unzipSync(new Uint8Array(buffer)); + const containerBytes = files['META-INF/container.xml']; + let rootPath: string | null = null; + if (containerBytes) { + const container = new DOMParser().parseFromString( + new TextDecoder('utf-8').decode(containerBytes), + 'text/xml', + ); + const rootfile = descendants(container.documentElement, 'rootfile')[0]; + rootPath = rootfile?.getAttribute('full-path') ?? null; + } + if (!rootPath) { + rootPath = Object.keys(files).find((name) => /\.(musicxml|xml)$/i.test(name)) ?? null; + } + if (!rootPath || !files[rootPath]) { + throw new Error('MXL 中没有找到 MusicXML 主文件。'); + } + const xml = new TextDecoder('utf-8').decode(files[rootPath]).replace(/^\uFEFF/, ''); + return {xml, format: 'mxl'}; + } + if (lower.endsWith('.mid') || lower.endsWith('.midi')) { + throw new Error('MIDI 需要先通过 MuseScore 转换为 MusicXML。'); + } + return {xml: buffer.toString('utf8').replace(/^\uFEFF/, ''), format: lower.endsWith('.musicxml') ? 'musicxml' : 'xml'}; +} diff --git a/src/server/pipeline.ts b/src/server/pipeline.ts new file mode 100644 index 0000000..418559f --- /dev/null +++ b/src/server/pipeline.ts @@ -0,0 +1,216 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type {JobStage, RenderData, VisualSettings} from '../shared/types.js'; +import {readMusicXmlFile, parseMusicXml, buildPerformanceOrder} from './musicxml.js'; +import {engraveOneLine} from './engrave.js'; +import {parseMidiFile} from './midi.js'; +import {buildEvents} from './events.js'; +import { + exportAudioAndMidi, + exportMidi, + convertMidiToMusicXml, + normalizeMusicXml, + findMuseScore, + MuseScoreError, +} from './musescore.js'; +import {isSupportedAudio, readAudioDuration, audioMimeType} from './audio.js'; +import {jobDirectory, updateJob} from './jobs.js'; + +export type PipelineInput = { + jobId: string; + file: {buffer: Buffer; filename: string}; + audio?: {buffer: Buffer; filename: string}; + settings: VisualSettings; + metadata?: {title?: string; subtitle?: string; composer?: string}; +}; + +function setProgress(jobId: string, stage: JobStage, progress: number, message: string) { + updateJob(jobId, {status: 'running', stage, progress, message}); +} + +/** MusicXML written by MIDI converters carries no beaming, so every eighth note gets its own flag. */ +function needsBeamFix(xml: string): boolean { + if (/(eighth|16th|32nd|64th)<\/type>/.test(xml); +} + +export async function runPipeline(input: PipelineInput): Promise { + const {jobId, file, audio, settings, metadata} = input; + const dir = jobDirectory(jobId); + fs.mkdirSync(dir, {recursive: true}); + + try { + setProgress(jobId, 'reading', 3, '读取上传文件…'); + const ext = path.extname(file.filename).toLowerCase(); + const isMidiInput = ext === '.mid' || ext === '.midi'; + if (!['.musicxml', '.xml', '.mxl', '.mid', '.midi'].includes(ext)) { + throw new Error('谱面输入只支持 .musicxml / .xml / .mxl / .mid / .midi;音频请放在单独的音频输入框。'); + } + + fs.writeFileSync(path.join(dir, `original${ext}`), file.buffer); + const inputMusicXml = path.join(dir, 'input.musicxml'); + + if (isMidiInput) { + if (!findMuseScore()) { + throw new Error( + '收到 MIDI 文件,但没有找到 MuseScore。MIDI 需要先转换为 MusicXML。请安装 MuseScore 4,或直接上传 MusicXML/MXL。', + ); + } + setProgress(jobId, 'reading', 8, '用 MuseScore 将 MIDI 识别为谱面…'); + await convertMidiToMusicXml(path.join(dir, `original${ext}`), inputMusicXml); + } else { + const {xml} = readMusicXmlFile(file.filename, file.buffer); + fs.writeFileSync(inputMusicXml, xml, 'utf8'); + } + + setProgress(jobId, 'reading', 12, '解析 MusicXML…'); + const warnings: string[] = []; + const musescore = findMuseScore(); + + // Re-engrave angle: files without make Verovio flag every eighth note. + let scorePath = inputMusicXml; + if (musescore && needsBeamFix(fs.readFileSync(inputMusicXml, 'utf8'))) { + setProgress(jobId, 'reading', 10, '用 MuseScore 规整谱面(符杠/连音)…'); + const normalized = path.join(dir, 'score.musicxml'); + try { + await normalizeMusicXml(inputMusicXml, normalized); + scorePath = normalized; + } catch (error) { + warnings.push( + `MuseScore 规整谱面失败,按原始文件排版,符杠可能缺失:${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + } + + const parsed = parseMusicXml(fs.readFileSync(scorePath)); + if (!parsed.measures.length) throw new Error('谱面里没有小节。'); + warnings.push(...parsed.warnings); + + if (parsed.unsupported.length) { + warnings.push( + `谱面包含当前版本不会展开的结构:${parsed.unsupported.join('、')}。自动合成音频会按 MuseScore 的播放跳转,但动画时间轴按谱面顺序;如出现不同步,请先在 MuseScore 中展开反复/跳转后导出 MusicXML。`, + ); + } + if (parsed.parts.length > 1) { + warnings.push(`检测到 ${parsed.parts.length} 个 part,单行谱面会包含多组五线谱。`); + } + + const hasRepeats = parsed.measures.some((m) => m.repeatForward || m.repeatBackward); + if (hasRepeats) { + warnings.push('谱面包含反复段:动画会按演奏顺序回到前面的符头,谱面滚动也会短暂回退。'); + } + + // Audio + MIDI. MuseScore is the source of truth for both when it is available. + let audioPath: string | null = null; + let audioFile: string | null = null; + let audioMime: string | undefined; + let midiPath: string | null = null; + + if (audio) { + if (!isSupportedAudio(audio.filename)) throw new Error('音频格式不支持。请使用 WAV / MP3 / M4A / OGG / FLAC。'); + setProgress(jobId, 'synthesizing', 20, '保存上传音频…'); + const audioExt = path.extname(audio.filename).toLowerCase() || '.wav'; + audioFile = `audio${audioExt}`; + audioPath = path.join(dir, audioFile); + fs.writeFileSync(audioPath, audio.buffer); + audioMime = audioMimeType(audio.filename); + if (musescore) { + setProgress(jobId, 'synthesizing', 24, '用 MuseScore 生成 MIDI 时间轴…'); + try { + midiPath = await exportMidi(scorePath, dir); + } catch (error) { + warnings.push( + `MuseScore 生成 MIDI 失败,将使用谱面标称时间:${error instanceof Error ? error.message : String(error)}`, + ); + } + } else { + warnings.push('没有找到 MuseScore,使用谱面标称时间;上传音频可能需要手动对齐。'); + } + warnings.push('使用上传音频:动画时间来自谱面/MIDI,若录音速度与谱面不同,可能出现渐进漂移。'); + } else { + if (!musescore) { + throw new Error( + '没有找到 MuseScore 4。自动合成音频需要 MuseScore;也可以上传自己的音频文件。安装后可设置 MUSESCORE_PATH 指向可执行文件。', + ); + } + setProgress(jobId, 'synthesizing', 20, 'MuseScore 正在合成音频(长曲可能需要几十秒)…'); + const exported = await exportAudioAndMidi(scorePath, dir); + audioPath = exported.wav; + midiPath = exported.midi; + audioFile = path.basename(exported.wav); + audioMime = 'audio/wav'; + } + + setProgress(jobId, 'engraving', 55, 'Verovio 正在排版单行谱面…'); + const engraved = await engraveOneLine(parsed.annotatedXml); + fs.writeFileSync(path.join(dir, 'score.svg'), engraved.svg, 'utf8'); + + setProgress(jobId, 'mapping', 70, '展开演奏顺序并对齐音符…'); + const performance = buildPerformanceOrder(parsed.measures, warnings); + const midi = midiPath ? parseMidiFile(midiPath) : null; + const audioDuration = audioPath ? await readAudioDuration(audioPath) : 0; + + const built = buildEvents({ + parsed, + engrave: engraved, + midi, + performanceOrder: performance.order, + audioDuration, + settings, + }); + + if (audioDuration > 0 && built.meta.musicDuration > audioDuration + 0.5) { + warnings.push( + `音频时长(${audioDuration.toFixed(1)}s)短于谱面时长(${built.meta.musicDuration.toFixed(1)}s),结尾可能没有声音。`, + ); + } + if (metadata?.title) built.meta.title = metadata.title.trim(); + if (metadata?.subtitle !== undefined) built.meta.subtitle = metadata.subtitle.trim(); + if (metadata?.composer !== undefined) built.meta.composer = metadata.composer.trim(); + + built.meta.warnings = [...new Set([...warnings, ...built.meta.warnings])]; + built.meta.unsupported = parsed.unsupported; + + if (!audioFile) throw new Error('没有生成音频文件。'); + + const renderData: RenderData = { + version: 1, + meta: built.meta, + events: built.events, + activations: built.activations, + scroll: built.scroll, + svgUrl: `/jobs/${jobId}/score.svg`, + audioUrl: `/jobs/${jobId}/${audioFile}`, + settings, + }; + fs.writeFileSync(path.join(dir, 'score.json'), JSON.stringify(renderData, null, 2), 'utf8'); + + setProgress(jobId, 'building', 92, '生成预览数据…'); + updateJob(jobId, { + status: 'ready', + stage: 'done', + progress: 100, + message: '预览已生成,可以播放或导出视频。', + data: renderData, + warnings: built.meta.warnings, + unsupported: built.meta.unsupported, + audioFile, + audioMime, + error: undefined, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + updateJob(jobId, { + status: 'error', + stage: 'done', + progress: 100, + message: '生成失败。', + error: message, + }); + if (!(error instanceof MuseScoreError)) { + console.error(`[job ${jobId}]`, error); + } + } +} diff --git a/src/server/queue.ts b/src/server/queue.ts new file mode 100644 index 0000000..d52fbf6 --- /dev/null +++ b/src/server/queue.ts @@ -0,0 +1,27 @@ +import {getJob} from './jobs.js'; +import {renderJob} from './render.js'; + +const pending: string[] = []; +let running = false; + +export function enqueueRender(jobId: string): void { + const job = getJob(jobId); + if (!job || !job.data) return; + if (job.status === 'rendering') return; + if (!pending.includes(jobId)) pending.push(jobId); + void drain(); +} + +async function drain(): Promise { + if (running) return; + running = true; + try { + while (pending.length) { + const id = pending.shift(); + if (!id) continue; + await renderJob(id); + } + } finally { + running = false; + } +} diff --git a/src/server/render.ts b/src/server/render.ts new file mode 100644 index 0000000..11815c7 --- /dev/null +++ b/src/server/render.ts @@ -0,0 +1,161 @@ +import path from 'node:path'; +import fs from 'node:fs'; +import net from 'node:net'; +import crypto from 'node:crypto'; +import {bundle} from '@remotion/bundler'; +import {renderMedia, selectComposition} from '@remotion/renderer'; +import type {RenderData} from '../shared/types.js'; +import {jobDirectory, updateJob, getJob} from './jobs.js'; +import {BROWSER_EXECUTABLE, PUBLIC_BASE_URL, REMOTION_COMPOSITION_ID, REMOTION_ENTRY} from './config.js'; + +const PROJECT_ROOT = path.resolve('.'); +const BUNDLE_IGNORED = new Set(['node_modules', '.git', 'data', 'dist', 'out', '.vite']); + +/** + * Every file the bundler can inline, fingerprinted by size and mtime. The bundle + * is reused only while this stays the same, so an edit never leaks an old + * composition into a render while the Vite preview already shows the new one. + */ +export function bundleFingerprint(): string { + const hash = crypto.createHash('sha1'); + const walk = (dir: string) => { + for (const entry of fs.readdirSync(dir, {withFileTypes: true}).sort((a, b) => a.name.localeCompare(b.name))) { + if (BUNDLE_IGNORED.has(entry.name)) continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + continue; + } + const stat = fs.statSync(full); + hash.update(`${path.relative(PROJECT_ROOT, full)}:${stat.size}:${stat.mtimeMs}\n`); + } + }; + walk(PROJECT_ROOT); + return hash.digest('hex'); +} + +let bundleCache: {fingerprint: string; serveUrl: Promise} | null = null; +let servePortPromise: Promise | null = null; + +function getServeUrl(): Promise { + const fingerprint = bundleFingerprint(); + if (bundleCache?.fingerprint === fingerprint) return bundleCache.serveUrl; + if (bundleCache) console.log('[render] sources changed, rebuilding the Remotion bundle'); + const serveUrl = bundle({ + entryPoint: path.resolve(REMOTION_ENTRY), + publicDir: path.resolve('public'), + onProgress: () => {}, + }); + bundleCache = {fingerprint, serveUrl}; + serveUrl.catch(() => { + if (bundleCache?.serveUrl === serveUrl) bundleCache = null; + }); + return serveUrl; +} + +/** + * Remotion serves the bundle from its own HTTP server. Left to itself it walks + * ports 3000-3100 and can collide with this API (or a lingering dev server), + * so hand it a port that is free right now. + */ +function getServePort(): Promise { + if (!servePortPromise) { + servePortPromise = new Promise((resolve) => { + const probe = net.createServer(); + probe.once('error', () => resolve(null)); + probe.listen(0, '127.0.0.1', () => { + const address = probe.address(); + const port = typeof address === 'object' && address ? address.port : 0; + probe.close(() => resolve(port > 0 ? port : null)); + }); + }); + } + return servePortPromise; +} + +function absoluteUrl(url: string): string { + try { + return new URL(url, PUBLIC_BASE_URL).toString(); + } catch { + return url; + } +} + +export async function renderJob(jobId: string): Promise { + const job = getJob(jobId); + if (!job) return; + if (!job.data) { + updateJob(jobId, {status: 'error', stage: 'done', progress: 100, error: '没有可渲染的数据。'}); + return; + } + const output = path.join(jobDirectory(jobId), 'video.mp4'); + if (fs.existsSync(output)) fs.rmSync(output, {force: true}); + updateJob(jobId, { + status: 'rendering', + stage: 'rendering', + progress: 1, + message: '打包 Remotion 项目…', + error: undefined, + }); + + try { + const data: RenderData = { + ...job.data, + svgUrl: absoluteUrl(job.data.svgUrl), + audioUrl: absoluteUrl(job.data.audioUrl), + }; + const serveUrl = await getServeUrl(); + const port = await getServePort(); + updateJob(jobId, {progress: 4, message: '准备 Composition…'}); + const composition = await selectComposition({ + serveUrl, + id: REMOTION_COMPOSITION_ID, + inputProps: {data}, + browserExecutable: BROWSER_EXECUTABLE, + timeoutInMilliseconds: 120000, + port, + }); + updateJob(jobId, {progress: 6, message: '渲染视频帧…'}); + await renderMedia({ + composition, + serveUrl, + inputProps: {data}, + port, + codec: 'h264', + audioCodec: 'aac', + outputLocation: output, + overwrite: true, + imageFormat: 'jpeg', + jpegQuality: 92, + crf: 18, + concurrency: 2, + browserExecutable: BROWSER_EXECUTABLE, + timeoutInMilliseconds: 120000, + onProgress: ({progress}) => { + const value = Math.min(99, Math.round(6 + progress * 93)); + updateJob(jobId, { + progress: value, + message: `渲染视频帧… ${Math.round(progress * 100)}%`, + }); + }, + }); + updateJob(jobId, { + status: 'done', + stage: 'done', + progress: 100, + message: '视频已生成,可以下载。', + videoFile: 'video.mp4', + error: undefined, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`[render ${jobId}]`, error); + updateJob(jobId, { + status: 'error', + stage: 'done', + progress: 100, + message: '视频渲染失败。', + error: message, + }); + } +} diff --git a/src/shared/scroll.ts b/src/shared/scroll.ts new file mode 100644 index 0000000..a1fcbd7 --- /dev/null +++ b/src/shared/scroll.ts @@ -0,0 +1,115 @@ +import type {NoteEvent, ScrollKey} from './types'; + +const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); + +/** + * Build the scrolling keyframes from note geometry. + * Shared by the server pipeline and the client's live preview so both agree. + */ +export function buildScrollKeys( + events: NoteEvent[], + lead: number, + viewWidthScore: number, + maxLeft: number, + musicDuration: number, +): ScrollKey[] { + const keys: ScrollKey[] = [{time: 0, x: 0}]; + if (maxLeft <= 0.001) { + return [ + {time: 0, x: 0}, + {time: Math.max(0.001, musicDuration + 2), x: 0}, + ]; + } + + const groups: {time: number; x: number; count: number}[] = []; + for (const event of events) { + const last = groups[groups.length - 1]; + if (last && Math.abs(last.time - event.time) <= 0.003) { + last.x += event.x; + last.count++; + } else { + groups.push({time: event.time, x: event.x, count: 1}); + } + } + + for (const group of groups) { + const avgX = group.x / group.count; + const target = clamp(avgX - lead * viewWidthScore, 0, maxLeft); + if (group.time <= 0.05) { + keys[0]!.x = target; + continue; + } + const last = keys[keys.length - 1]!; + if (group.time <= last.time + 0.001) { + last.x = target; + } else { + keys.push({time: group.time, x: target}); + } + } + + const lastNoteTime = groups[groups.length - 1]?.time ?? 0; + const endTime = Math.max(musicDuration, lastNoteTime + 0.4); + const lastKey = keys[keys.length - 1]!; + if (lastKey.x < maxLeft - 0.001) { + keys.push({time: endTime, x: maxLeft}); + } + keys.push({time: Math.max(endTime + 1, musicDuration + 2), x: maxLeft}); + // Strictly increasing times are required by the interpolator. + const sorted = keys + .filter((key) => Number.isFinite(key.time) && Number.isFinite(key.x)) + .sort((a, b) => a.time - b.time); + const deduped: ScrollKey[] = []; + for (const key of sorted) { + const prev = deduped[deduped.length - 1]; + if (prev && Math.abs(prev.time - key.time) < 0.0005) prev.x = key.x; + else deduped.push({...key}); + } + return deduped; +} + +/** + * Cubic Hermite interpolation through explicit scroll keys. + * Slopes are harmonic means, zeroed at direction changes, so repeats and + * rests ease instead of overshooting. All values are SVG user units. + */ +export function scrollTrack(keys: ScrollKey[], t: number): number { + if (!keys.length) return 0; + const first = keys[0]!; + const last = keys[keys.length - 1]!; + if (keys.length === 1) return first.x; + if (t <= first.time) return first.x; + if (t >= last.time) return last.x; + + let i = 0; + while (i + 1 < keys.length && keys[i + 1]!.time <= t) i++; + i = Math.min(i, keys.length - 2); + const a = keys[i]!; + const b = keys[i + 1]!; + const h = b.time - a.time; + if (h <= 0) return a.x; + const p = clamp((t - a.time) / h, 0, 1); + + const slope = (j: number) => { + const from = keys[j]!; + const to = keys[j + 1]!; + const dt = to.time - from.time; + return dt > 0 ? (to.x - from.x) / dt : 0; + }; + const tangent = (j: number) => { + if (j === 0) return slope(0); + if (j === keys.length - 1) return slope(j - 1); + const left = slope(j - 1); + const right = slope(j); + if (left * right <= 0) return 0; + return (2 * left * right) / (left + right); + }; + + const p2 = p * p; + const p3 = p2 * p; + return ( + (2 * p3 - 3 * p2 + 1) * a.x + + (p3 - 2 * p2 + p) * h * tangent(i) + + (-2 * p3 + 3 * p2) * b.x + + (p3 - p2) * h * tangent(i + 1) + ); +} diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 0000000..56d03db --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,174 @@ +export const APP_NAME = 'Music Visualizer'; + +export type NoteKind = 'attack' | 'sustain'; + +export type NoteEvent = { + /** Unique per performance occurrence. */ + eventId: string; + /** Verovio SVG g.note id, shared by repeated performances of the same notehead. */ + scoreId: string; + time: number; + duration: number; + pitch: number; + velocity: number; + staff: number; + /** SVG user coordinates (outer viewBox). */ + x: number; + y: number; + kind: NoteKind; + grace?: boolean; +}; + +export type ScrollKey = { + /** Music seconds. */ + time: number; + /** Left edge of the visible score in SVG user units. */ + x: number; +}; + +export type ScoreMeta = { + title: string; + subtitle: string; + composer: string; + svgWidth: number; + svgHeight: number; + /** Last sounding note end, in music seconds. */ + musicDuration: number; + /** Audio duration, in seconds. */ + audioDuration: number; + measureCount: number; + scoreNoteCount: number; + midiNoteCount: number; + mappedNoteCount: number; + warnings: string[]; + unsupported: string[]; +}; + +export type VisualSettings = { + width: number; + height: number; + fps: number; + introSeconds: number; + outroSeconds: number; + /** Score strip height as a fraction of the video height. 0.3 - 0.75. */ + scoreHeightRatio: number; + /** Active note target position as a fraction of viewport width. 0.25 - 0.6. */ + scrollLead: number; + paperColor: string; + /** Paper opacity; 0 makes the score float on the background with no paper at all. */ + paperAlpha: number; + /** Ink colour for the engraved notation. */ + scoreColor: string; + background: string; + staffColors: string[]; + /** Ball radius multiplier around the default. */ + ballScale: number; + /** + * Amplifies the orb's jump along the sheet normal. Resting height above the + * ink stays put; 0 slides every hop flat onto the paper. + */ + ballLift: number; + /** Glow intensity multiplier. */ + glow: number; + showTitle: boolean; + showProgress: boolean; + titleFont: string; + audioOffsetMs: number; + /** Camera pitch in degrees. Positive values tip the top of the score away. */ + tiltX: number; + /** Camera yaw in degrees. Positive values turn the right side away. */ + tiltY: number; + /** Camera roll in degrees. Negative values lift the right side of the staff. */ + tiltZ: number; + /** CSS perspective distance in pixels. Smaller values give stronger convergence. */ + perspective: number; + /** Extra zoom applied to the tilted score plane so it still fills the frame. */ + tiltZoom: number; +}; + +export type RenderData = { + version: 1; + meta: ScoreMeta; + events: NoteEvent[]; + activations: NoteEvent[]; + scroll: ScrollKey[]; + svgUrl: string; + audioUrl: string; + settings: VisualSettings; +}; + +export type JobStatus = 'queued' | 'running' | 'ready' | 'rendering' | 'done' | 'error'; + +export type JobStage = + | 'queued' + | 'reading' + | 'synthesizing' + | 'engraving' + | 'mapping' + | 'building' + | 'rendering' + | 'done'; + +export type JobPublic = { + id: string; + status: JobStatus; + stage: JobStage; + progress: number; + message: string; + error?: string; + warnings?: string[]; + unsupported?: string[]; + data?: RenderData; + videoUrl?: string; + createdAt: string; + inputName?: string; + inputFormat?: string; +}; + +export type CreateJobResponse = { + id: string; +}; + +export type HealthResponse = { + ok: boolean; + musescore: { available: boolean; path?: string; version?: string }; + chrome: { available: boolean; note: string }; + ffmpeg: { available: boolean; note: string }; + version: string; +}; + +export const DEFAULT_SETTINGS: VisualSettings = { + width: 1920, + height: 1080, + fps: 30, + introSeconds: 0.65, + outroSeconds: 1.8, + scoreHeightRatio: 0.5, + scrollLead: 0.38, + paperColor: '#c9c2b6', + paperAlpha: 1, + scoreColor: '#14110d', + background: '#111015', + staffColors: ['#bda0ff', '#ffbc6d', '#8fe3c8', '#ff8fa3'], + ballScale: 1, + ballLift: 1, + glow: 1, + showTitle: true, + showProgress: true, + titleFont: "Georgia, 'Times New Roman', 'Noto Serif SC', 'Songti SC', serif", + audioOffsetMs: 0, + tiltX: 12, + tiltY: 0, + tiltZ: -5, + perspective: 1800, + tiltZoom: 1.12, +}; + +export const RESOLUTION_PRESETS = [ + {id: 'landscape', label: '横屏 · 16:9', width: 1920, height: 1080}, + {id: 'vertical', label: '竖屏 · 9:16', width: 1080, height: 1920}, + {id: 'square', label: '方形 · 1:1', width: 1080, height: 1080}, +] as const; + +export const MAX_UPLOAD_BYTES = 30 * 1024 * 1024; +export const MAX_RENDER_SECONDS = 20 * 60; diff --git a/src/types/vendor.d.ts b/src/types/vendor.d.ts new file mode 100644 index 0000000..1ceeb65 --- /dev/null +++ b/src/types/vendor.d.ts @@ -0,0 +1,24 @@ +declare module 'verovio/wasm' { + const createModule: () => Promise; + export default createModule; +} + +declare module 'verovio/esm' { + export class VerovioToolkit { + constructor(module: unknown); + resetXmlIdSeed(seed: number): void; + setOptions(options: Record): void; + loadData(data: string): boolean; + getLog(): string; + getPageCount(): number; + renderToSVG(page: number): string; + getTimeForElement(id: string): number; + getMIDIValuesForElement(id: string): {duration: number; pitch: number; time: number} | null; + getPageWithElement(id: string): number; + } +} + +declare module 'svg-path-bounds' { + const getBounds: (path: string) => [number, number, number, number]; + export default getBounds; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..eb60364 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node", "vite/client"] + }, + "include": ["src", "scripts", "vite.config.ts"], + "exclude": ["node_modules", "dist", "data"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..e9c49c2 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,19 @@ +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + publicDir: 'public', + build: { + outDir: 'dist/client', + emptyOutDir: true, + }, + server: { + port: 5173, + proxy: { + '/api': 'http://127.0.0.1:3001', + '/jobs': 'http://127.0.0.1:3001', + '/demo': 'http://127.0.0.1:3001', + }, + }, +});