diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..1f36c55 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,58 @@ +name: Quality Gate + +on: + push: + branches: [main, clean-main, "codex/**"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: windows-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Install Python dependencies + shell: pwsh + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + + - name: Verify backend and security contracts + shell: pwsh + run: | + python -m unittest discover -s tests -p "test_*.py" + node --test scripts/cf_worker_security.test.mjs + + - name: Verify frontend + working-directory: web + shell: pwsh + run: | + npm ci + npm audit --omit=dev --audit-level=high + npm exec tsc -- --noEmit + npm run build + + - name: Verify desktop shell + working-directory: web/src-tauri + shell: pwsh + run: | + # Tauri validates configured resource paths during cargo check. The + # release workflow replaces this empty directory with the built sidecar. + New-Item -ItemType Directory -Force sidecar | Out-Null + cargo check --locked diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml new file mode 100644 index 0000000..c784758 --- /dev/null +++ b/.github/workflows/release-windows.yml @@ -0,0 +1,98 @@ +name: Release Windows + +on: + workflow_dispatch: + push: + tags: ["v*"] + +permissions: + contents: write + id-token: write + attestations: write + +jobs: + build: + runs-on: windows-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Install and test + shell: pwsh + run: | + python -m pip install --upgrade pip uv + python -m pip install -r requirements.txt + python -m unittest discover -s tests -p "test_*.py" + node --test scripts/cf_worker_security.test.mjs + Push-Location web + npm ci + npm audit --omit=dev --audit-level=high + npm exec tsc -- --noEmit + npm run build + Pop-Location + + - name: Build Python sidecar + shell: pwsh + run: .\scripts\build_desktop_sidecar.ps1 -Clean + + - name: Build Windows installers + working-directory: web + shell: pwsh + run: npm run desktop:build + + - name: Collect release assets and hashes + shell: pwsh + run: | + New-Item -ItemType Directory -Force release | Out-Null + $installer = Get-ChildItem web\src-tauri\target\release\bundle\nsis -File -Filter *.exe | + Select-Object -First 1 + if (-not $installer) { throw "NSIS installer was not generated" } + Copy-Item $installer.FullName release\Wanxiang-Windows-x64-Setup.exe + Get-ChildItem release -File | Get-FileHash -Algorithm SHA256 | + ForEach-Object { "$($_.Hash.ToLowerInvariant()) $(Split-Path $_.Path -Leaf)" } | + Set-Content release\SHA256SUMS.txt -Encoding ascii + + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + path: . + format: spdx-json + output-file: release/sbom.spdx.json + upload-artifact: false + + - name: Attest installers + uses: actions/attest@v4 + with: + subject-path: | + release/*.exe + + - name: Attest SBOM + uses: actions/attest@v4 + with: + subject-path: | + release/*.exe + sbom-path: release/sbom.spdx.json + + - uses: actions/upload-artifact@v4 + with: + name: wanxiang-windows-${{ github.sha }} + path: release/* + if-no-files-found: error + + - name: Upload tagged release + if: startsWith(github.ref, 'refs/tags/') + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "${{ github.ref_name }}" release/* --verify-tag --generate-notes --title "万象成文 ${{ github.ref_name }}" diff --git a/.gitignore b/.gitignore index 29be6c1..196289e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,28 @@ web/node_modules/ frontend/dist/ audio2text.settings.json +.env +.env.* +!.env.example +*.pem +*.pfx +*.p12 +*.key +web/src-tauri/target/ +web/src-tauri/binaries/ +web/src-tauri/gen/ +web/src-tauri/icons/android/ +web/src-tauri/icons/ios/ +web/src-tauri/icons/64x64.png +web/src-tauri/icons/Square*.png +web/src-tauri/icons/StoreLogo.png +web/src-tauri/icons/icon.icns +web/src-tauri/icons/icon.png +web/src-tauri/sidecar/* +!web/src-tauri/sidecar/wanxiang-api/ +web/src-tauri/sidecar/wanxiang-api/* +!web/src-tauri/sidecar/wanxiang-api/.gitkeep +release/ tests/live_smoke_samples.local.json .playwright-mcp/ diff --git a/AGENTS.md b/AGENTS.md index fba2a14..d379644 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,20 +76,19 @@ HF Space 部署在海外(非中国 IP),无法直接访问小红书/抖音等中国平台。通过 Cloudflare Worker 实现双向代理解决 GFW 问题。 -### 部署账号 +### 部署身份 -- **Hugging Face**: 用户名 `liamgrant` -- **Cloudflare**: 邮箱 `liamgrant.ai@gmail.com`, Account ID `f375f33442fa409ea1a9f04a4da83745` -- **Worker 脚本**: `scripts/cf_worker_proxy.js`, 名称 `wanxiang-chengwen-proxy` +- Hugging Face 与 Cloudflare 身份只通过各自 CLI 登录态或 CI Secret 提供,不写入仓库。 +- Worker 脚本:`scripts/cf_worker_proxy.js`;默认名称:`wanxiang-chengwen-proxy`。 ### 服务 URL -| 用途 | URL | -|------|-----| -| 用户入口 (自定义域名) | `https://wanxiang.praxisai.online` | -| HF Space (后端源站) | `https://liamgrant-wanxiang-chengwen-preview.hf.space` | -| Worker 中继 (反向代理 + 转发) | `https://wanxiang.praxisai.online` | -| Worker 转发接口 | `https://wanxiang.praxisai.online/__proxy__?url=` | +| 用途 | URL | +| ----------------------------- | -------------------------------------------------------------- | +| 用户入口 (自定义域名) | `https://wanxiang.praxisai.online` | +| HF Space (后端源站) | `https://liamgrant-wanxiang-chengwen-preview.hf.space` | +| Worker 中继 (反向代理 + 转发) | `https://wanxiang.praxisai.online` | +| Worker 转发接口 | `https://wanxiang.praxisai.online/__proxy__?url=` | ### 工作原理 @@ -105,6 +104,7 @@ HF Space 部署在海外(非中国 IP),无法直接访问小红书/抖音 - Playwright 层: `_setup_playwright_relay_routing()` 拦截浏览器请求并路由到 Worker 3. **Worker 部署命令**: + ```bash npx wrangler deploy scripts/cf_worker_proxy.js --name wanxiang-chengwen-proxy --compatibility-date 2026-05-14 ``` @@ -120,45 +120,54 @@ HF Space 部署在海外(非中国 IP),无法直接访问小红书/抖音 三轮迭代,当前 commit `7492049`。所有改动已验证通过(122 tests OK, TS 无错误, 前端已构建)。 **前端体验** + - 移动端剪贴板优化:Clipboard API 失败时引导长按/系统粘贴(`App.tsx`) - 错误净化:20+ 技术术语过滤规则,前后端双重拦截(`api.ts`、`ProcessingStage.tsx`) - 移动端布局:textarea 对称 padding,清除按钮 absolute 不受影响(`styles.css`) **图片提取(`browser_provider.py`)** + - 尺寸阈值 160px → 120px,新增 `video[poster]` / `source[srcset]` 提取 - 小红书 Live Photo 优化:优先已知字段再递归搜索 **免费 Whisper(`cf_worker_proxy.js`、`wrangler.toml`、`transcription.py`)** + - CF Worker `/__transcribe__` 端点,Workers AI `@cf/openai/whisper` - `cloud_preview` 模式自动选择 `CloudflareWorkersAIWhisperProvider` **结果页信息架构** + - 布局 3 层:hero(pill + 标题 + 描述)→ 内容区(文字 + 图片/视频)→ meta footer(平台 | 类型 | 时长 | 查看来源) - 删除 `outcome-strip`(工程指标:正文已就绪 / N个下载项 / 质量较高) - 删除 `facts-strip`(信息与 hero/meta footer 重复) - 实现"整理正文 / 原文"标签切换(`views.primary` ↔ `views.notes`),无 notes 时原文禁用 **UI 细节(`styles.css`)** + - 完成 pill:灰色调,与品牌一致 - 图片卡片 hover:`translateY(-3px)` + 双层柔和阴影 - 文字卡片:双层阴影、微斜渐变、边框透明度 0.06 - 正文排版:行高 1.95、段落间距 18px、颜色 `#2c2c2c` **Bug 修复** + - `source_adapters.py` 补充遗漏导入 `DEPLOYMENT_MODE`、`CN_PLATFORM_RELAY_URL` **Apple HIG 合规(第 4 轮)** + - 删除死 CSS 9 组:`.header-note`、`.status-chip.is-ready/.is-danger`、`.hero-kicker`、`.hero-trust-grid`、`.processing-ticker`、`.processing-progress` 系列、`.image-gallery-body.is-previewing`、`.image-card-action.is-primary` - 触控目标 ≥ 44pt:`--action-height` 42→44、`.image-card-action` 36→44、`.image-viewer-close` 42→44、`.image-viewer-nav` 38→44 - 文档精简:三轮维护日志合并为一个条目,去除重复表述 **已知风险** + - CF Workers AI Whisper 免费版日配额限制,超大音频可能超时 - Live Photo 抓取成功率依赖平台接口稳定性 ### 2026-05-15 第 5 轮:8pt 网格对齐 + 文案微调(commit `0785777`) **8pt 网格间距对齐(`styles.css`,约 90 处修改)** + - gap 值:`5/6/10→8px`、`12/14→16px`、`18→24px`、`26→24px` - Section padding:`.main-stage` 18/26→24/24、`.processing-shell` 34/28/30→32/32/32、`.hero-form-card` 18/20→24/24、`.result-prose` 22/28→24/32 - 组件尺寸:`.brand-mark` 28→32、`.hero-submit` 52→56、`.image-card-badge` 58/30→56/32、`.image-viewer-toolbar-button` 34→32 @@ -166,41 +175,49 @@ HF Space 部署在海外(非中国 IP),无法直接访问小红书/抖音 - 微间距保留:2-4px 的视觉微调值(Apple 允许) **文案微调** + - placeholder `贴入`→`粘贴`(`InputStage.tsx`) - 视频不可用面板:`暂未拿到可预览视频`→`视频预览暂不可用`(`DeliverableStage.tsx`) **清理** + - 删除 15 个临时调试文件(截图、快照日志、测试 HTML),释放约 3MB ### 2026-05-15 第 6 轮:去标签化 + 测试全绿 + 上线准备 **移除整理正文/原文标签切换** + - 删除 `result-mode-tabs` 组件及相关 CSS(桌面+移动端,共 ~50 行) - 删除 `textViewMode` 状态、`hasNotesText`、`notesText` 逻辑、`TextViewMode` 类型 - 结果页直接展示 `primary_text`,不再有二级视图切换 - `DeliverableStage.tsx` 简化约 15 行 **Vite 开发配置** + - 新增 `server.proxy`:`/config` 和 `/v1` 代理到 `http://127.0.0.1:8000`(`vite.config.ts`) - dev server 可以直接调后端,无需额外配置 **测试验证** + - 122 后端测试全部通过(unittest discover),1 跳过(live smoke 需配置 URL) - TypeScript 零错误,Vite 构建通过(CSS 38.84KB, JS 322.35KB) - UI smoke `test_video_result_layout_stays_stable` `.facts-strip` 引用已修复为 `.deliverable-meta` - hover 测试使用 `force: true`(标准 hover 效果测试模式) **文档同步** + - README.md、docs/verification.md、docs/release-ready.md 中 "details row" / "facts row" 统一更新为 "meta footer" ### 2026-05-15 上线前最终闭环 **测试验证** + - 后端测试:122 passed / 1 skipped - TypeScript:零错误 - 前端构建:成功(CSS 38.93KB, JS 322.39KB) **功能冒烟** + - 首页:通过(品牌标识、输入框、按钮、最近记录均正常) - 处理中页面:通过(进度条、状态文案、处理中标签正常) - 结果页(视频):通过(hero区、双栏布局、视频播放器、meta footer齐全) @@ -210,31 +227,37 @@ HF Space 部署在海外(非中国 IP),无法直接访问小红书/抖音 - 图片查看器:未测试(本地无图文完整结果,需线上验证) **代码质量** + - 死代码清理:无(CSS/TSX/Python 均无未使用代码) - 文档一致性:无(README.md、docs/ 均无过时引用,CSS类名与TSX完全对齐) **结论** + - 是否可以上线:是 - 遗留风险:CF Workers AI Whisper 免费版日配额限制,超大音频可能超时;Live Photo 抓取成功率依赖平台接口稳定性;图片查看器未在本次冒烟中覆盖(需线上图文结果验证) ### 2026-05-15 部署记录 **已部署** + - HF Space: `https://liamgrant-wanxiang-chengwen-preview.hf.space`(海外可访问) - CF Worker: `wanxiang-chengwen-proxy`(已部署,功能正常) - 自定义域名: `wanxiang.praxisai.online`(已绑定 Worker,`.online` TLD 中国 DNS 可解析) - 旧域名: `praxisai.men`(已绑定 Worker,但 `.men` TLD 国内不可用) **国内访问** + - `wanxiang.praxisai.online` 已验证中国 DNS(阿里 223.5.5.5、腾讯 119.29.29.29)可正常解析 - `.online` 是主流 TLD,不会被 GFW 识别为代理域名 **当前可用地址** + - 国内/海外: `https://wanxiang.praxisai.online` - HF Space 直连: `https://liamgrant-wanxiang-chengwen-preview.hf.space` - Worker 默认域名: `https://wanxiang-chengwen-proxy.liamgrant-wanxiang.workers.dev` **额度限制(已实现)** + - 每日次数:12 次/IP/天(环境变量 `DAILY_CAPTURE_LIMIT`) - **小红书图文免限流**:无转写,不消耗计算资源,不计入每日次数 - 视频时长:≤ 30 分钟(环境变量 `MAX_VIDEO_DURATION_MINUTES`) @@ -245,17 +268,20 @@ HF Space 部署在海外(非中国 IP),无法直接访问小红书/抖音 ### 2026-05-16 架构简化 + Bug 修复 **根因修复(4 个)** + - 剪贴板粘贴:`execCommand('paste')` 移到 `await` 之前同步执行,保留用户手势(`App.tsx`) - Docker Chromium:加 `--no-sandbox --disable-gpu` 启动参数(`browser_provider.py`) - Worker Content-Type:保留原始 Content-Type,不再强制 `text/plain`,SPA 可正常渲染(`cf_worker_proxy.js`) - 小红书页面检测:补充 "你访问的页面不见了" 等 404 标题匹配(`source_adapters.py`) **架构剃刀** + - 小红书提取:跳过 HTTP 中继静态抓取(SPA 空壳永远无数据),直接 Playwright + `page.evaluate()` 从 `window.__INITIAL_STATE__` 提取笔记 JSON。不再传输 `page.content()` 全文(521KB → 2KB),总耗时减少 30-40%(`browser_provider.py`) - 前端错误处理:砍掉 55 行 `RAW_TECH_PATTERNS` 正则黑名单,后端已产出中文错误信息,前端直接透传(`api.ts`) - 小红书限流豁免:`_check_daily_rate_limit` 移至 URL 解析后,按平台判断(`main.py`) **已部署** + - GitHub: clean-main 分支 - HF Space: `liamgrant/wanxiang-chengwen-preview` - Cloudflare Worker: `wanxiang-chengwen-proxy`(Content-Type 修复) @@ -264,62 +290,74 @@ HF Space 部署在海外(非中国 IP),无法直接访问小红书/抖音 ### 2026-05-16 图片查看器工具栏 + 跨平台字体修复 **根因修复(2 个)** + - 工具栏膨胀:移动端 `.image-viewer-toolbar-group.is-navigation` 的 `flex: 1 1 100%` 强制占满整行,把图片工具组和下载按钮挤到额外行,工具栏从 2 行膨胀到 3 行(166px),遮挡图片(`styles.css`) - 跨平台字体差异:WeChat WebView 可能自动调整文本大小,导致字体渲染与 Safari 不一致(`styles.css`) **修复内容** + - 移动端工具栏重排:导航组 `flex: 0 1 auto`,图片工具组 `flex: 1 1 auto`,下载按钮 `order: 1` 独占末行;按钮尺寸 44→40px,间距 8→6px,圆角 26→20px - 工具栏高度从 166px(3 行)降至 100px(2 行),不再遮挡图片 - PC 端单行布局不受影响(桌面断点未修改) - 根元素添加 `-webkit-text-size-adjust: 100%` + `text-size-adjust: 100%`,阻止 WeChat WebView 自动放大文字 **验证** + - 后端测试:122 passed / 1 skipped - TypeScript:零错误 - 前端构建:CSS 37.93KB, JS 322.32KB - Playwright 截图验证:WeChat iPhone (375×812)、Safari iPhone (390×844)、Narrow Android (360×780)、Desktop (1440×900) 四端工具栏布局正确 **已部署** + - GitHub: clean-main 分支 - HF Space: `liamgrant/wanxiang-chengwen-preview` ### 2026-05-16 Apple HIG 合规 + 移动端布局全面优化 **触控目标修复(3 个,≥ 44pt)** + - `.hero-input-clear`:40→44px(清空输入按钮) - `.recent-inline-delete`:32→44px(删除记录按钮) - `.image-viewer-toolbar-button`:36→44px(图片查看器工具栏按钮) **工具栏布局统一** + - `image-tools` 组 `flex: 1 1 auto` → `flex: 0 1 auto`,不再膨胀挤占空间 - 普通图和 Live 图工具栏结构一致:导航+缩放在同一行,下载在第二行 - 按钮 44→36px(桌面)/ 44px(移动端),间距统一 4-6px **正文区间距消除** + - `.result-content-shell` 移动端 `flex: 1` → `flex: 0 0 auto`,不再撑满剩余空间 - `.result-prose` 移动端 `min-height: 100%` → `auto`,内容自适应高度 **跨平台字体一致性** + - 根元素 `-webkit-text-size-adjust: 100%` + `text-size-adjust: 100%`,阻止 WeChat WebView 自动放大 **验证** + - 后端测试:122 passed / 1 skipped - TypeScript:零错误 - 前端构建:CSS 37.98KB, JS 322.32KB - Playwright 四端截图:WeChat iPhone、Safari iPhone、Narrow Android、Desktop 全部布局正确 **已部署** + - GitHub: clean-main 分支(`e708c61`) - HF Space: `liamgrant/wanxiang-chengwen-preview` ### 2026-05-16 桌面端两栏布局修复 **根因** + - 提交 `d6c59f5` 移除了 `@media (min-width: 1025px)` 内的两栏网格布局,改为所有宽度单栏自然流 - 原因是桌面端 `height: 100vh` 高度锁定导致部分视口文案列被挤压至 ~25% 宽度 - 修复方案过于激进,未区分桌面/手机断点,导致桌面端 UI 崩坏为手机端样式 **修复内容** + - 恢复 `body.has-result-stage` 高度锁定链:body → #root → .app-shell → .main-stage → .stage-shell-ready - 恢复 `.stage-shell-ready` 高度计算 `calc(100vh - 96px)` + `overflow: hidden` 级联 - 恢复两栏网格:`.deliverable-workspace.has-images` → `grid-template-columns: minmax(0, 1.06fr) minmax(320px, 0.94fr)` @@ -328,35 +366,42 @@ HF Space 部署在海外(非中国 IP),无法直接访问小红书/抖音 - 手机端断点(≤960px / ≤640px)未触碰 **验证** + - 前端构建:CSS 39.22KB, JS 322.32KB - Playwright 截图验证:Desktop (1440×900) 两栏布局正确,Mobile (390×844) 单栏布局正确 **已部署** + - GitHub: clean-main 分支(`607226f`) - HF Space: `liamgrant/wanxiang-chengwen-preview` ### 2026-05-17 UI 回归修复 + 备份版本对齐 **根因** + - 多次增量修改导致 CSS 和组件代码与备份版本不一致 - 组件使用 `deliverable-meta` 类名,但 CSS 已切换为 `fact-chip` 类名 - `viewer-media-enter` 动画中的 `transform: scale()` 与图片查看器交互式 transform 冲突 **修复内容** + - 恢复备份版本的 CSS(`styles.css`)作为基础 - 恢复备份版本的组件(`DeliverableStage.tsx`)作为基础 - 仅叠加必要改动:hero 标签样式、`-webkit-text-size-adjust` - 移除冲突的动画 transform(只保留 opacity 淡入) **关键教训** + - 备份版本的类名体系(`fact-chip`、`facts-strip-side`)与之前版本不同 - 修改 CSS/组件时必须同时检查类名匹配 - 动画中的 `transform` 会覆盖元素的交互式 `transform`,导致布局异常 **验证** + - 前端构建:CSS 41.30KB, JS 323.20KB - Playwright 截图验证:Desktop 居中正确,两栏布局正常 **已部署** + - GitHub: clean-main 分支 - HF Space: `liamgrant/wanxiang-chengwen-preview` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..ab2f3c2 --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ +Wanxiang Chengwen 2.0 +Copyright 2026 Praxis AI + +This product includes open-source software maintained by their respective +copyright holders. Source code is licensed under Apache-2.0 unless a file or +component states otherwise. + +Runtime packages and speech-recognition model weights are separate works and +retain their own licenses. A release must include the matching third-party +notices and model license before those artifacts are redistributed. + +Praxis AI and Wanxiang Chengwen are product identifiers. This license does not +grant trademark rights. diff --git a/README.md b/README.md index d89ea0a..f6ff79a 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,24 @@ sdk: docker app_port: 8000 --- - # Praxis AI|无界笃行 / 万象成文 +# Praxis AI|无界笃行 / 万象成文 -`万象成文` 是一个单页交付型产品,用来把公开链接或本地音视频整理成可直接交付的正文、图片和视频素材。 +把公开视频、播客、图文链接或本地音视频,整理成可阅读、可复制、可下载的文字与媒体。 + +[在线体验](https://wanxiang.praxisai.online) · [下载 Windows 版](https://github.com/qinxujunai/Audio2Text/releases/latest/download/Wanxiang-Windows-x64-Setup.exe) · [观看 12 秒演示](docs/assets/wanxiang-demo.mp4) + +![万象成文桌面端首页](docs/assets/home-desktop.png) + +Windows 桌面端是完整主产品:安装包自带处理服务,不要求目标电脑安装 Python。首次使用下载约 222 MB 的 SenseVoice int8 与 FFmpeg 运行组件,不额外下载多套模型;浏览器提取复用 Windows 自带 Edge,文件在本机处理。云端版本用于限额体验。 当前只做三件事: + - 音频 / 视频转文字 - 图文链接提取正文与正文图片 -- 视频结果页预览并下载原视频 +- 预览并下载原视频、原音频、图片与文本交付件 当前不做这些: + - 账号体系 - 支付 / 套餐 / 积分 - 多工作台 / Agent 后台 @@ -45,7 +53,7 @@ app_port: 8000 ### 视频 - 页面预览优先 `preview_media`。 -- 下载始终使用 `source_media`。 +- 下载原视频使用 `source_media`;有音轨时同时提供 `source_audio`。 - 页面不额外叠自定义悬浮全屏 / 下载按钮,沿用浏览器原生控件。 - 预览件与下载原件不是同一个交付目标,不应混为一谈。 @@ -72,14 +80,24 @@ app_port: 8000 ## 当前支持平台 -| 平台 | 当前状态 | 主链路 | 说明 | -| --- | --- | --- | --- | -| YouTube | 条件可用 | 字幕优先,必要时回退媒体下载与转写 | 默认优先原语种字幕,不默认优先翻译轨 | -| 哔哩哔哩 | 基本可用 | `yt-dlp` + 字幕轨优先 | 命中字幕时跳过转写 | -| 小宇宙 | 条件可用 | 音频下载 + transcript | 页面 `shownotes / description` 不再当成交付正文 | -| 抖音 | 基本可用但高波动 | 公开链路优先,失败后回退浏览器会话与 `yt-dlp` | 视频主正文只认字幕或转写 | -| 小红书 | 条件可用 | 浏览器会话优先 | 图文优先正文与图片,视频主正文只认字幕或转写 | -| 微信公众号图文 | 基本可用 | 直连优先;命中验证页时回退浏览器会话;支持正文图片区与图片文章模板 | 过滤二维码、头像、装饰图、分享图等噪音 | +最近一次 live smoke:2026-08-11。平台页面会变化,表格记录的是可复核状态,不是永久承诺。 + +| 平台 | 当前状态 | 主链路 | 最近验证 | +| -------------- | -------------- | ------------------------------------------------------------- | -------- | +| 哔哩哔哩 | 通过 | 官方 view/player API → progressive/DASH → `yt-dlp` | 视频、文字、视频与音频交付件通过 | +| 小宇宙 | 通过 | 页面音频 → 本地转写 | `.m4a`、文字与音频交付件通过 | +| 抖音 | 通过但高波动 | 公开链路 → 浏览器会话 → `yt-dlp` | 公开视频、文字与媒体交付件通过 | +| 小红书 | 图文 / Live 通过 | 分享链接 → 结构化数据 → 浏览器会话 | 15 图、14 个 Live 片段通过;视频样本当前触发平台会话验证 | +| 微信公众号图文 | 通过 | 直连 → 浏览器会话 | 正文与图片样本通过 | +| YouTube | 网络条件可用 | 字幕 → EJS/`yt-dlp` → 媒体转写 | 当前中国网络连接 API 超时,错误归因通过 | + +## Windows 版 + +1. 下载 `Wanxiang-Windows-x64-Setup.exe`。 +2. 安装并打开万象成文。 +3. 首次使用按界面提示安装本地识别与媒体组件。 + +安装包、运行包与发布产物都提供 SHA-256;GitHub Actions 同时生成 SBOM 与 provenance attestation。桌面端只监听随机 loopback 端口,并用每次启动生成的短期令牌保护本地 API。 ## 项目级环境隔离 @@ -98,6 +116,7 @@ workspace/logs ``` 补充说明: + - 高波动平台建议保留 `workspace/runtime/browser-profile` 作为项目级持久会话目录。 - 若本地仍落到 `Assets/Models/FasterWhisper/...`,当前版本会把它视为 legacy fallback,并在 preflight 给出 warning。 @@ -115,6 +134,7 @@ tests_runtime/ 测试临时产物,可清理 ``` 目录说明: + - `web/` 是前端源码目录,应编辑这里。 - `frontend/dist/` 是构建输出目录,不应手动修改。 - `tests_runtime/` 是测试与截图临时产物目录,可清理。 @@ -124,41 +144,58 @@ tests_runtime/ 测试临时产物,可清理 ## 本地启动 -### 1. 安装 Python 依赖 +### 1. 一键启动产品并生成公网链接 ```powershell -.\.venv\Scripts\python.exe -m pip install -r requirements.txt +.\start_api.bat ``` -### 2. 安装项目级 Playwright Chromium +这个入口会先做启动自检,再启动本地服务和临时公网链接。自检会自动处理: + +- `.venv` 缺失、损坏或指向失效 Python 时,优先用 `uv` 重建 Python 3.11 环境并安装锁定依赖。 +- `frontend/dist` 缺失时,检测 `npm` 并自动构建前端产物。 +- 启动前检查模型、ffmpeg、Playwright Chromium、CUDA runtime、端口和运行时配置。 + +窗口里出现 `[PUBLIC URL]` 后,把那个链接发给别人即可。电脑和窗口都要保持开启。 + +只做本地开发或排障时,用本地模式: ```powershell -$env:PLAYWRIGHT_BROWSERS_PATH = "$PWD\workspace\runtime\playwright-browsers" -.\.venv\Scripts\python.exe -m playwright install chromium +$env:AUDIO2TEXT_LOCAL_ONLY = "1" +.\start_api.bat ``` -### 3. 准备 ffmpeg +默认端口是 `8000`;端口被占用时可临时换端口: -默认路径: +```powershell +$env:AUDIO2TEXT_API_PORT = "8001" +.\start_api.bat +``` + +### 2. 运行时资源 + +启动入口会检查这些项目级资源;缺失时会给出明确中文提示: ```text +workspace/runtime/models/faster-whisper/medium +workspace/runtime/playwright-browsers workspace/runtime/ffmpeg/ffmpeg.exe workspace/runtime/ffmpeg/ffprobe.exe ``` -### 4. 启动产品并生成公网链接 +高波动平台建议保留 `workspace/runtime/browser-profile` 作为项目级浏览器会话。若该目录为空,产品仍可启动,但抖音 / 小红书成功率会下降;可用下面命令刷新: ```powershell -.\start_api.bat +.\.venv\Scripts\python.exe -m scripts.refresh_browser_session ``` -这个入口会同时启动本地服务和临时公网链接。窗口里出现 `[PUBLIC URL]` 后,把那个链接发给别人即可。电脑和窗口都要保持开启。 +### 3. 排障用手工命令 -只做本地开发或排障时,用本地模式: +日常不需要手工安装依赖;只有自检失败需要定位时再用这些命令: ```powershell -$env:AUDIO2TEXT_LOCAL_ONLY = "1" -.\start_api.bat +uv venv --clear --seed --python 3.11 .venv +uv pip install --python .\.venv\Scripts\python.exe -r requirements.txt --link-mode copy --compile-bytecode ``` 底层 API 仍可直接运行: @@ -167,14 +204,7 @@ $env:AUDIO2TEXT_LOCAL_ONLY = "1" .\.venv\Scripts\python.exe -m scripts.start_api ``` -默认端口是 `8000`;端口被占用时可临时换端口: - -```powershell -$env:AUDIO2TEXT_API_PORT = "8001" -.\start_api.bat -``` - -### 5. 本地访问 +### 4. 本地访问 ```text http://127.0.0.1:8000 @@ -187,16 +217,18 @@ tests_runtime/public_preview/current_url.txt ``` 注意: + - 这是临时演示链接,不是正式云部署。 - 电脑不能关,启动窗口不能关。 - 如果出现 localtunnel 的 IP 确认页,按窗口提示输入页面上显示的 IP 后继续。 -- 用户可见启动入口只保留 `start_api.bat`;`scripts/start_cloudflare_tunnel.py` 和 `scripts/start_localtunnel.py` 只是内部兜底 helper。 +- 用户可见启动入口只保留 `start_api.bat`;`scripts/start_api_bootstrap.ps1` 负责自检和自愈,`scripts/start_cloudflare_tunnel.py` 和 `scripts/start_localtunnel.py` 只是内部兜底 helper。 ## 配置 示例配置见 [audio2text.settings.example.json](audio2text.settings.example.json)。 关键项: + - `workspace_dir` - `model_path` - `ffmpeg_path` @@ -217,6 +249,7 @@ tests_runtime/public_preview/current_url.txt 评测方法与当前基线记录见 [docs/transcription-benchmark.md](docs/transcription-benchmark.md)。 默认值说明: + - `language` 当前默认是 `auto` - `capture_history_limit` 当前默认是 `12` - `daily_capture_limit` 当前默认是 `12`(每 IP 每天最多提交次数) @@ -241,6 +274,7 @@ docker run --rm --name praxis-audio2text -p 8000:8000 ` ``` 前提: + - `workspace/runtime/models/faster-whisper/medium` 已有可用模型文件。 - 如需抖音 / 小红书浏览器链路,挂载后的 `workspace/runtime/playwright-browsers` 里也要有 Chromium runtime。 @@ -259,6 +293,7 @@ docker run --rm --name praxis-audio2text -p 8000:8000 ` ``` 当前约定: + - Docker 构建会在镜像内自行完成前端构建。 - 宿主机不需要预先提供 `frontend/dist`。 - 容器内默认使用镜像自带 `ffmpeg`。 @@ -311,6 +346,7 @@ AUDIO2TEXT_OPENAI_COMPATIBLE_MODEL= 脚本会自动写入前 6 个公开变量。`AUDIO2TEXT_OPENAI_COMPATIBLE_*` 三项涉及实际转写服务,其中 API key 必须放 Space Secret;没有配置时首页仍可访问,图文和字幕类链接可先体验,本地音视频文件会提示需要配置转写服务。 注意: + - API key 只能放在 Space Secret,不要写入仓库文件。 - 免费 Space 的 `workspace` 不应当成正式持久存储。 - 这条路径用于预览和演示,不等同正式生产上线。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d431085 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Supported Releases + +Security fixes are provided for the latest published Windows release and the +currently deployed cloud demo. + +## Reporting a Vulnerability + +Please use the repository's private **Security advisory** form. Do not open a +public issue for suspected credential exposure, SSRF, authentication bypass, +arbitrary file access, or remote code execution. + +Include the affected version, reproduction steps, impact, and a minimal proof +of concept. Do not include real user content, cookies, tokens, or credentials. + +We will acknowledge a valid report, investigate it privately, and coordinate a +release before public disclosure. There is no authorization to access data or +accounts that you do not own, bypass platform verification, or disrupt the +service while testing. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..a953a67 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,37 @@ +# Third-Party Notices + +This file records the direct dependencies shipped by the project. Release +automation must generate an SBOM and preserve each dependency's complete +license text in the installer artifact. + +| Component | Purpose | License | +| --- | --- | --- | +| FastAPI / Uvicorn | Local API service | MIT / BSD-3-Clause | +| Pydantic | API validation | MIT | +| HTTPX / Requests | HTTP clients | BSD-3-Clause / Apache-2.0 | +| yt-dlp | Public media extraction | Unlicense | +| Playwright | Controlled browser fallback | Apache-2.0 | +| FFmpeg 9.0 Essentials (Gyan.dev Windows build) | Media conversion | GPL-3.0-only; distributed as a separate runtime package | +| Faster-Whisper / CTranslate2 | Local transcription | MIT | +| ONNX Runtime | Optional native model runtime | MIT | +| PyAV | Media inspection | BSD-3-Clause | +| React / React DOM | Web interface | MIT | +| Motion | Interface motion | MIT | +| Lucide | Interface icons | ISC | +| Tauri | Windows desktop shell | Apache-2.0 / MIT | + +## Model Weights + +Model weights are not licensed by this repository's Apache-2.0 license. +The standard runtime package contains only the int8 SenseVoice model converted +by sherpa-onnx from `ASLP-lab/WSYue-ASR` (Apache-2.0): +`sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2025-09-09`. Faster-Whisper and +Qwen3-ASR remain optional benchmark candidates and are not shipped. + +## Runtime Sources + +- SenseVoice int8 model: https://github.com/k2-fsa/sherpa-onnx/releases/tag/asr-models +- SenseVoice conversion documentation: https://k2-fsa.github.io/sherpa/onnx/sense-voice/pretrained.html +- SenseVoice fine-tune source and license: https://huggingface.co/ASLP-lab/WSYue-ASR +- FFmpeg 9.0 Essentials binary and checksums: https://www.gyan.dev/ffmpeg/builds/ +- FFmpeg corresponding source: https://github.com/FFmpeg/FFmpeg/tree/n9.0 diff --git a/app/__init__.py b/app/__init__.py index 5420ab7..eea9bed 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1 +1 @@ -"""Application package for the v2 content capture product.""" +"""Wanxiang Chengwen application package.""" diff --git a/app/extractors.py b/app/extractors.py index d816a2e..daab96c 100644 --- a/app/extractors.py +++ b/app/extractors.py @@ -353,6 +353,8 @@ def _classify_external_error(message: str, *, platform: str = "", stage: str = " return "source_not_found", False if "http error 403" in cleaned or "forbidden" in cleaned: return "source_access_restricted", False + if "http error 412" in cleaned or "precondition failed" in cleaned: + return f"{platform or 'source'}_{stage}_precondition_failed", True if "timed out" in cleaned or "timeout" in cleaned: return f"{platform or 'source'}_{stage}_timeout", True if "connection" in cleaned or "temporarily unavailable" in cleaned: @@ -576,13 +578,22 @@ def _fetch_text(url: str, *, timeout_seconds: float | None = None, platform: str ) from exc -def _download_file(url: str, target_path: Path, *, platform: str | None = None) -> Path: +def _download_file( + url: str, + target_path: Path, + *, + platform: str | None = None, + headers: dict[str, str] | None = None, +) -> Path: target_path.parent.mkdir(parents=True, exist_ok=True) last_error: httpx.HTTPError | None = None for _ in range(3): try: target_path.unlink(missing_ok=True) - with _httpx_client(proxy=_proxy_for_platform(platform)) as client: + request_headers = _request_headers() + if headers: + request_headers.update(headers) + with _httpx_client(proxy=_proxy_for_platform(platform), headers=request_headers) as client: with client.stream("GET", url) as response: response.raise_for_status() with open(target_path, "wb") as handle: @@ -1150,6 +1161,14 @@ def _ytdlp_metadata(url: str, capture_dir: Path, cookie_text: str = "", platform "http_headers": {"User-Agent": DEFAULT_USER_AGENT}, "logger": SilentYtdlpLogger(), } + if platform == "youtube": + base_options.update( + { + "socket_timeout": 8, + "retries": 0, + "extractor_retries": 0, + } + ) last_exc: Exception | None = None for browser_cookie_source in _browser_cookie_sources(cookie_file, platform): options = dict(base_options) @@ -1287,10 +1306,10 @@ def _download_media_selection_with_ytdlp( if platform == "youtube": base_options.update( { - "socket_timeout": 20, - "retries": 3, - "fragment_retries": 3, - "extractor_retries": 3, + "socket_timeout": 8, + "retries": 0, + "fragment_retries": 1, + "extractor_retries": 0, } ) last_exc: Exception | None = None @@ -1412,7 +1431,11 @@ def extract_youtube_transcript( if not video_id: raise ExtractionError("resolve", "YouTube 链接可访问,但没有解析出视频 ID。") - final_url, html = _fetch_text(resolved.normalized_url, platform="youtube") + final_url, html = _fetch_text( + resolved.normalized_url, + timeout_seconds=10.0, + platform="youtube", + ) meta = _extract_meta_tags(html) title = meta.get("og:title") or meta.get("title") or f"YouTube {video_id}" description = meta.get("og:description") or meta.get("description") or "" @@ -1510,7 +1533,7 @@ def _youtube_via_invidious(video_id: str) -> tuple[str, str, str, str, str]: random.shuffle(instances) for instance in instances: try: - with _httpx_client(timeout=12.0) as client: + with _httpx_client(timeout=6.0) as client: resp = client.get(f"{instance}/api/v1/videos/{video_id}") resp.raise_for_status() data = resp.json() @@ -1591,7 +1614,7 @@ def _extract_youtube_captions_from_page(html: str) -> str: caption_url = "https:" + caption_url # Fetch caption XML via relay or direct - _, caption_xml = _fetch_text(caption_url, platform="youtube") + _, caption_xml = _fetch_text(caption_url, timeout_seconds=10.0, platform="youtube") try: root = ET.fromstring(caption_xml) @@ -1970,7 +1993,7 @@ def _extract_bilibili_video_id(url: str) -> str: return "" for segment in path.split("/"): if segment.lower().startswith("bv") and len(segment) >= 10: - return segment.upper() + return segment for segment in path.split("/"): if segment.lower().startswith("av") and segment[2:].isdigit(): return segment.lower() @@ -1978,6 +2001,143 @@ def _extract_bilibili_video_id(url: str) -> str: return match.group(1) if match else "" +def _bilibili_headers() -> dict[str, str]: + headers = _request_headers() + headers.update( + { + "Referer": "https://www.bilibili.com/", + "Origin": "https://www.bilibili.com", + } + ) + return headers + + +def _bilibili_api_json(client: httpx.Client, url: str, *, reason_code: str) -> dict: + try: + response = client.get(url) + response.raise_for_status() + payload = response.json() + except (httpx.HTTPError, ValueError) as exc: + code, retryable = _classify_external_error(str(exc), platform="bilibili", stage="extract") + raise ExtractionError( + "extract", + f"B站 API 请求失败:{_strip_ansi(str(exc))}", + reason_code=code if code != DEFAULT_REASON_CODES["extract"] else reason_code, + retryable=retryable, + ) from exc + + if not isinstance(payload, dict): + raise ExtractionError("extract", "B站 API 没有返回有效数据。", reason_code=reason_code, retryable=True) + if payload.get("code") not in (0, None): + message = str(payload.get("message") or payload.get("msg") or "B站 API 返回异常。") + raise ExtractionError("extract", message, reason_code=reason_code, retryable=True) + return payload + + +def _bilibili_subtitle_url(player_data: dict) -> str: + subtitle_info = player_data.get("subtitle", {}).get("subtitles") if isinstance(player_data.get("subtitle"), dict) else [] + if not isinstance(subtitle_info, list) or not subtitle_info: + return "" + for item in subtitle_info: + if not isinstance(item, dict): + continue + url = str(item.get("subtitle_url") or "").strip() + if not url: + continue + return url if url.startswith("http") else f"https:{url}" + return "" + + +def _bilibili_progressive_media_url(playurl_data: dict) -> str: + durls = playurl_data.get("durl") if isinstance(playurl_data.get("durl"), list) else [] + for item in durls: + if isinstance(item, dict) and item.get("url"): + return str(item["url"]) + return "" + + +def _bilibili_best_dash_pair(playurl_data: dict) -> tuple[str, str]: + dash = playurl_data.get("dash") if isinstance(playurl_data.get("dash"), dict) else {} + videos = dash.get("video") if isinstance(dash.get("video"), list) else [] + audios = dash.get("audio") if isinstance(dash.get("audio"), list) else [] + + def media_url(item: dict) -> str: + return str(item.get("baseUrl") or item.get("base_url") or "").strip() + + def video_rank(item: dict) -> tuple[int, int]: + try: + height = int(item.get("height") or 0) + except (TypeError, ValueError): + height = 0 + try: + bandwidth = int(item.get("bandwidth") or 0) + except (TypeError, ValueError): + bandwidth = 0 + height_score = height if height <= VIDEO_DELIVERY_TARGET_MAX_HEIGHT else 0 + return height_score, bandwidth + + def audio_rank(item: dict) -> int: + try: + return int(item.get("bandwidth") or 0) + except (TypeError, ValueError): + return 0 + + video = next((item for item in sorted(videos, key=video_rank, reverse=True) if isinstance(item, dict) and media_url(item)), None) + audio = next((item for item in sorted(audios, key=audio_rank, reverse=True) if isinstance(item, dict) and media_url(item)), None) + return (media_url(video), media_url(audio)) if video and audio else ("", "") + + +def _download_bilibili_media( + client: httpx.Client, + *, + aid: object, + cid: object, + bvid: str, + capture_dir: Path, + progress_callback: ProgressCallback | None = None, +) -> tuple[Path | None, list[str]]: + warnings: list[str] = [] + common_query = f"avid={aid}&cid={cid}&bvid={bvid}&fnver=0&fourk=1" + progressive_url = f"https://api.bilibili.com/x/player/playurl?{common_query}&qn=64&fnval=0" + dash_url = f"https://api.bilibili.com/x/player/playurl?{common_query}&qn=64&fnval=16" + + try: + payload = _bilibili_api_json(client, progressive_url, reason_code="bilibili_playurl_failed") + data = payload.get("data") if isinstance(payload.get("data"), dict) else {} + media_url = _bilibili_progressive_media_url(data) + if media_url: + media_path = capture_dir / "bilibili_source.mp4" + if progress_callback is not None: + progress_callback(36, "正在下载 B站原视频文件。") + _download_file(media_url, media_path, platform="bilibili", headers=_bilibili_headers()) + if media_path.exists(): + return media_path, warnings + except ExtractionError as exc: + warnings.append(f"渐进式视频下载失败:{exc.message}") + + try: + payload = _bilibili_api_json(client, dash_url, reason_code="bilibili_dash_failed") + data = payload.get("data") if isinstance(payload.get("data"), dict) else {} + video_url, audio_url = _bilibili_best_dash_pair(data) + if not video_url or not audio_url: + return None, warnings + ["B站 DASH 播放地址缺少视频或音频流。"] + if progress_callback is not None: + progress_callback(36, "正在下载 B站视频与音频流。") + video_path = capture_dir / "bilibili_video.m4s" + audio_path = capture_dir / "bilibili_audio.m4s" + output_path = capture_dir / "bilibili_source.mp4" + _download_file(video_url, video_path, platform="bilibili", headers=_bilibili_headers()) + _download_file(audio_url, audio_path, platform="bilibili", headers=_bilibili_headers()) + merged = _merge_media_streams(video_path, audio_path, output_path) + if merged is not None: + return merged, warnings + warnings.append("B站 DASH 视频与音频合并失败。") + except ExtractionError as exc: + warnings.append(f"DASH 视频下载失败:{exc.message}") + + return None, warnings + + def extract_bilibili_direct( resolved: ResolvedSource, capture_dir: Path, @@ -1991,85 +2151,61 @@ def extract_bilibili_direct( if progress_callback is not None: progress_callback(22, "正在请求 B站视频信息。") - headers = _request_headers() - headers["Referer"] = "https://www.bilibili.com/" + headers = _bilibili_headers() info_url = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}" with _httpx_client(headers=headers) as client: - try: - response = client.get(info_url) - response.raise_for_status() - payload = response.json() - except (httpx.HTTPError, ValueError) as exc: - raise ExtractionError("extract", f"B站 API 请求失败:{exc}", retryable=True) from exc - - data = payload.get("data") if isinstance(payload, dict) else None - if not isinstance(data, dict): - raise ExtractionError("extract", "B站 API 没有返回视频数据。", retryable=True) - - title = str(data.get("title") or resolved.normalized_url) - description = str(data.get("desc") or "") - duration_seconds = float(data["duration"]) if data.get("duration") else None - author = str(data.get("owner", {}).get("name") or "") - thumbnail_url = str(data.get("pic") or "") - cid_list = data.get("pages") if isinstance(data.get("pages"), list) else [] - cid = cid_list[0].get("cid") if cid_list else data.get("cid") - aid = data.get("aid") + payload = _bilibili_api_json(client, info_url, reason_code="bilibili_video_invalid") + data = payload.get("data") if isinstance(payload.get("data"), dict) else None + if not isinstance(data, dict): + raise ExtractionError("extract", "B站 API 没有返回视频数据。", reason_code="bilibili_video_invalid", retryable=True) + + title = str(data.get("title") or resolved.normalized_url) + description = str(data.get("desc") or "") + duration_seconds = float(data["duration"]) if data.get("duration") else None + author = str(data.get("owner", {}).get("name") or "") + thumbnail_url = str(data.get("pic") or "") + cid_list = data.get("pages") if isinstance(data.get("pages"), list) else [] + cid = cid_list[0].get("cid") if cid_list and isinstance(cid_list[0], dict) else data.get("cid") + aid = data.get("aid") - if progress_callback is not None: - progress_callback(28, "已拿到视频信息,正在获取播放地址。") + if progress_callback is not None: + progress_callback(28, "已拿到视频信息,正在获取字幕和播放地址。") - subtitle_text = "" - subtitle_timeline = "" - media_url = "" - warnings: list[str] = [] + subtitle_text = "" + subtitle_timeline = "" + media_path: Path | None = None + warnings: list[str] = [] - if cid and aid: - try: - player_url = ( - f"https://api.bilibili.com/x/player/wbi/v2" - f"?aid={aid}&cid={cid}&bvid={bvid}&fnval=4048&fnver=0&fourk=1" + if cid and aid: + try: + player_url = ( + "https://api.bilibili.com/x/player/wbi/v2" + f"?aid={aid}&cid={cid}&bvid={bvid}&fnval=4048&fnver=0&fourk=1" + ) + player_payload = _bilibili_api_json(client, player_url, reason_code="bilibili_player_failed") + player_data = player_payload.get("data") if isinstance(player_payload.get("data"), dict) else {} + sub_url = _bilibili_subtitle_url(player_data) + if sub_url: + try: + sub_path = capture_dir / "bilibili_subtitle.json" + _download_file(sub_url, sub_path, platform="bilibili", headers=headers) + raw = sub_path.read_text(encoding="utf-8", errors="ignore") + subtitle_text, subtitle_timeline = _parse_bilibili_subtitle(raw) + except Exception as exc: + warnings.append(f"字幕下载失败:{exc}") + except ExtractionError as exc: + warnings.append(f"播放信息获取失败:{exc.message}") + + media_path, media_warnings = _download_bilibili_media( + client, + aid=aid, + cid=cid, + bvid=bvid, + capture_dir=capture_dir, + progress_callback=progress_callback, ) - player_resp = client.get(player_url) - player_resp.raise_for_status() - player_data = player_resp.json() - - if isinstance(player_data.get("data"), dict): - pd = player_data["data"] - - subtitle_info = pd.get("subtitle", {}).get("subtitles") if isinstance(pd.get("subtitle"), dict) else [] - if subtitle_info: - best_sub = subtitle_info[0] - sub_url = best_sub.get("subtitle_url", "") - if sub_url and not sub_url.startswith("http"): - sub_url = f"https:{sub_url}" - if sub_url: - try: - sub_path = capture_dir / "bilibili_subtitle.json" - _download_file(sub_url, sub_path) - raw = sub_path.read_text(encoding="utf-8", errors="ignore") - subtitle_text, subtitle_timeline = _parse_bilibili_subtitle(raw) - except Exception as exc: - warnings.append(f"字幕下载失败:{exc}") - - durls = pd.get("durl") if isinstance(pd.get("durl"), list) else [] - if durls: - media_url = str(durls[0].get("url") or "") - except (httpx.HTTPError, ValueError) as exc: - warnings.append(f"播放地址获取失败:{exc}") - - if media_url: - media_path = capture_dir / "bilibili_source.mp4" - if progress_callback is not None: - progress_callback(36, "正在下载原视频文件。") - try: - _download_file(media_url, media_path, platform="bilibili", headers={"Referer": "https://www.bilibili.com/"}) - if not media_path.exists(): - media_url = "" - warnings.append("视频文件未能成功下载。") - except ExtractionError as exc: - media_url = "" - warnings.append(exc.message) + warnings.extend(media_warnings) if progress_callback is not None: progress_callback(50, "B站内容提取完成。") @@ -2087,7 +2223,7 @@ def extract_bilibili_direct( thumbnail_url=thumbnail_url, duration_seconds=duration_seconds, image_urls=[], - strategy=["direct_api"] + (["subtitle"] if subtitle_text else []) + (["media_direct"] if media_url else []), + strategy=["direct_api"] + (["subtitle"] if subtitle_text else []) + (["media_direct"] if media_path else []), warnings=warnings, notes_text=description[:280] if description else "", subtitle_text=subtitle_text, @@ -2096,8 +2232,8 @@ def extract_bilibili_direct( selected_language="zh" if subtitle_text else "", article_text="", primary_text=subtitle_text, - media_file_path=str(media_path) if media_path and Path(media_path).exists() else "", - needs_transcription=bool(media_url) and not bool(subtitle_text), + media_file_path=str(media_path) if media_path and media_path.exists() else "", + needs_transcription=bool(media_path) and not bool(subtitle_text), ) diff --git a/app/main.py b/app/main.py index ec842c0..f106590 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import hmac import json import shutil import sys @@ -16,6 +17,7 @@ from app.store_fs import CaptureStoreUnavailableError from app.pipeline import enqueue_capture, ensure_worker_started, recover_pending_captures from app.resolver import infer_source_item_id, resolve_url +from app.runtime_packs import RuntimePackError, RuntimePackManager from app.schemas import ( ArtifactPayloadModel, CaptureCreateUrlRequest, @@ -35,9 +37,12 @@ from app.settings import ( ADMIN_IPS, ALLOWED_ORIGINS, + APP_COMMIT, APP_DISPLAY_NAME, + APP_VERSION, CAPTURE_HISTORY_LIMIT, DAILY_CAPTURE_LIMIT, + DESKTOP_TOKEN, FREE_DURATION_MINUTES, FRONTEND_DIR, MAX_UPLOAD_SIZE_MB, @@ -47,8 +52,13 @@ PRODUCT_SLOGAN, PRODUCT_SUMMARY, DEPLOYMENT_MODE, + FFMPEG_PATH, + MODEL_PATH, + PLAYWRIGHT_BROWSERS_DIR, PUBLIC_PREVIEW_MODE, - RELAY_RUN_MODE, + RUNTIME_DIR, + RUNTIME_PACK_MANIFEST, + RUNTIME_TARGET, SUPPORTED_EXTENSIONS, TEMP_DIR, TRANSCRIPTION_AVAILABLE, @@ -61,6 +71,7 @@ logger = get_logger("capture_api", "capture_api.log") repository = get_capture_repository() +runtime_pack_manager = RuntimePackManager(RUNTIME_PACK_MANIFEST, RUNTIME_DIR) UPLOAD_CHUNK_SIZE = 1024 * 1024 app = FastAPI(title=APP_DISPLAY_NAME) @@ -72,6 +83,39 @@ ) PUBLIC_INTERNAL_ERROR_MESSAGE = "服务暂时繁忙,请稍后重试。" +DESKTOP_ALLOWED_ORIGINS = { + "http://tauri.localhost", + "https://tauri.localhost", + "tauri://localhost", +} + + +@app.middleware("http") +async def enforce_desktop_loopback_boundary(request: Request, call_next): + if RUNTIME_TARGET != "windows_desktop" or not DESKTOP_TOKEN: + return await call_next(request) + + client_host = request.client.host if request.client else "" + if client_host not in {"127.0.0.1", "::1", "testclient"}: + return JSONResponse(status_code=403, content={"detail": "本地服务仅允许本机访问。"}) + + origin = request.headers.get("origin", "") + if origin and origin not in DESKTOP_ALLOWED_ORIGINS: + return JSONResponse(status_code=403, content={"detail": "请求来源未获授权。"}) + + # Browsers send the CORS preflight before they are allowed to attach the + # desktop token header. The origin and loopback checks still apply here; + # every actual API request remains token-protected below. + if request.method == "OPTIONS" and request.headers.get("access-control-request-method"): + return await call_next(request) + + supplied_token = request.headers.get("x-wanxiang-desktop-token", "") + if not supplied_token: + supplied_token = request.query_params.get("desktop_token", "") + if not hmac.compare_digest(supplied_token, DESKTOP_TOKEN): + return JSONResponse(status_code=401, content={"detail": "本地服务授权已失效,请重新打开应用。"}) + + return await call_next(request) def _is_windows_connection_reset_noise(context: dict[str, Any]) -> bool: @@ -196,9 +240,25 @@ def _public_artifact(artifact) -> ArtifactPayloadModel: download_url=artifact.download_url, mime_type=artifact.mime_type, size_bytes=artifact.size_bytes, + status=getattr(artifact, "status", "ready"), + optional=bool(getattr(artifact, "optional", False)), ) +def _result_state(capture, result) -> str: + if capture.status == "failed": + return "failed" + has_text = bool(result and (result.views.primary or result.primary_text).strip()) + declared_state = str(getattr(capture, "result_state", "processing") or "processing") + if declared_state == "text_ready" and has_text: + return "text_ready" + if capture.status == "done" and bool(getattr(capture, "asset_preparation_pending", False)) and has_text: + return "text_ready" + if capture.status == "done": + return "complete" + return "processing" + + def _public_source_images(source) -> list[CaptureSourceImagePayloadModel]: image_urls = list(source.image_urls or []) live_urls = list(getattr(source, "live_photo_video_urls", []) or []) @@ -241,6 +301,7 @@ def _to_capture_envelope(capture) -> CaptureEnvelopeModel: capture=CaptureStatePayloadModel( id=capture.id, status=capture.status, + result_state=_result_state(capture, result), input_kind=capture.input_type, current_stage=capture.processing.current_stage, progress_percent=capture.processing.progress_percent, @@ -294,6 +355,36 @@ def _to_capture_envelope(capture) -> CaptureEnvelopeModel: ) +def _transition_event_names( + capture, + envelope: CaptureEnvelopeModel, + *, + previous_result_state: str | None, + ready_artifact_types: set[str], +) -> tuple[list[str], set[str]]: + events: list[str] = [] + current_state = envelope.capture.result_state + if ( + current_state in {"text_ready", "complete"} + and previous_result_state not in {"text_ready", "complete"} + and bool(envelope.result.primary_text.strip()) + ): + events.append("text_ready") + + current_ready = { + artifact.type + for artifact in envelope.artifacts + if artifact.status == "ready" + } + if current_ready - ready_artifact_types: + events.append("artifact_ready") + + reason_code = str(getattr(capture.processing, "failure_reason_code", "") or "") + if reason_code.startswith("runtime_"): + events.append("runtime_required") + return events, current_ready + + def _public_capture(capture) -> CaptureListItemModel: result = _display_result(capture, _normalized_result(capture.result)) platform = getattr(capture, "source_platform", "") or getattr(getattr(capture, "source", None), "platform", "") @@ -345,6 +436,27 @@ def _config_payload() -> dict[str, Any]: "public_preview_mode": PUBLIC_PREVIEW_MODE, "transcription_available": TRANSCRIPTION_AVAILABLE, "transcription_provider": TRANSCRIPTION_PROVIDER, + "runtime_target": RUNTIME_TARGET, + "capabilities": { + "url_capture": True, + "file_upload": True, + "source_audio": True, + "desktop_runtime_management": RUNTIME_TARGET == "windows_desktop", + "cloud_demo": DEPLOYMENT_MODE == "cloud_preview", + }, + } + + +def _component_status() -> dict[str, str]: + local_runtime = RUNTIME_TARGET in {"local_web", "windows_desktop"} + return { + "api": "ready", + "frontend": "ready" if _dist_available() else "missing", + "storage": "ready" if repository is not None else "unavailable", + "transcription": "ready" if TRANSCRIPTION_AVAILABLE else "unavailable", + "ffmpeg": "ready" if FFMPEG_PATH.exists() else ("missing" if local_runtime else "not_required"), + "browser": "ready" if PLAYWRIGHT_BROWSERS_DIR.exists() else ("missing" if local_runtime else "not_required"), + "model": "ready" if MODEL_PATH.exists() else ("missing" if local_runtime else "not_required"), } @@ -563,7 +675,11 @@ async def health() -> dict[str, Any]: "status": "ok", "product_name": PRODUCT_NAME, "feature_name": PRODUCT_FEATURE_NAME, - "run_mode": RELAY_RUN_MODE, + "deployment_mode": DEPLOYMENT_MODE, + "runtime_target": RUNTIME_TARGET, + "version": APP_VERSION, + "commit": APP_COMMIT, + "components": _component_status(), } @@ -572,6 +688,26 @@ async def config() -> dict[str, Any]: return _config_payload() +@app.get("/v1/runtime/packs") +async def list_runtime_packs() -> dict[str, Any]: + if RUNTIME_TARGET != "windows_desktop": + raise HTTPException(status_code=404, detail="该功能仅在 Windows 应用中可用。") + try: + return {"packs": runtime_pack_manager.status()} + except RuntimePackError as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + + +@app.post("/v1/runtime/packs/{pack_id}/install") +async def install_runtime_pack(pack_id: str) -> dict[str, Any]: + if RUNTIME_TARGET != "windows_desktop": + raise HTTPException(status_code=404, detail="该功能仅在 Windows 应用中可用。") + try: + return await asyncio.to_thread(runtime_pack_manager.install, pack_id) + except RuntimePackError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/v1/captures", response_model=CaptureListResponse) async def list_v1_captures(request: Request) -> CaptureListResponse: client_ip = _extract_client_ip(request) @@ -618,6 +754,8 @@ async def stream_v1_capture_events(capture_id: str) -> StreamingResponse: async def event_stream(): last_version = None + previous_result_state = None + ready_artifact_types: set[str] = set() while True: capture = repository.get(capture_id) if capture is None: @@ -630,9 +768,22 @@ async def event_stream(): envelope = _to_capture_envelope(capture) version = envelope.capture.updated_at if version != last_version: + transition_events, ready_artifact_types = _transition_event_names( + capture, + envelope, + previous_result_state=previous_result_state, + ready_artifact_types=ready_artifact_types, + ) + for event_name in transition_events: + transition_payload = CaptureEventEnvelopeModel( + event=event_name, + payload=envelope, + ).model_dump(mode="json") + yield f"event: {event_name}\ndata: {json.dumps(transition_payload, ensure_ascii=False)}\n\n" payload = CaptureEventEnvelopeModel(event="capture.updated", payload=envelope).model_dump(mode="json") yield f"event: capture.updated\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n" last_version = version + previous_result_state = envelope.capture.result_state if capture.status == "failed" or (capture.status == "done" and not capture.asset_preparation_pending): break diff --git a/app/pipeline.py b/app/pipeline.py index a51270d..2b24864 100644 --- a/app/pipeline.py +++ b/app/pipeline.py @@ -601,6 +601,60 @@ def _persist_source_media_artifact(capture, media_file_path: str | None) -> Arti return _artifact_entry(capture.id, "source_media", "原视频", target) +def _audio_codec_for_path(path: Path) -> str: + try: + with av.open(str(path)) as container: + for stream in container.streams: + if stream.type == "audio": + return str(getattr(getattr(stream, "codec_context", None), "name", "") or "").lower() + except Exception: + return "" + return "" + + +def _persist_source_audio_artifact(capture, artifact_dir: Path, media_file_path: str | None) -> ArtifactModel | None: + if capture.source.content_type not in {"audio", "video"} or not media_file_path: + return None + + source_path = Path(media_file_path) + if not source_path.exists() or not source_path.is_file(): + return None + + audio_codec = _audio_codec_for_path(source_path) + if not audio_codec: + return None + + if capture.source.content_type == "audio": + return _artifact_entry(capture.id, "source_audio", "原音频", source_path) + + if not FFMPEG_PATH.exists(): + return None + + target_path = artifact_dir / "source_audio.m4a" + target_path.unlink(missing_ok=True) + copy_friendly_codecs = {"aac", "mp4a"} + audio_args = ["-c:a", "copy"] if audio_codec in copy_friendly_codecs else ["-c:a", "aac", "-b:a", "192k"] + command = [ + str(FFMPEG_PATH), + "-y", + "-i", + str(source_path), + "-vn", + "-map", + "0:a:0", + *audio_args, + "-movflags", + "+faststart", + str(target_path), + ] + result = subprocess.run(command, capture_output=True, text=True, check=False) + if result.returncode != 0 or not target_path.exists(): + target_path.unlink(missing_ok=True) + logger.warning("source audio export failed for %s: %s", source_path, (result.stderr or result.stdout or "").strip()) + return None + return _artifact_entry(capture.id, "source_audio", "原音频", target_path) + + def _probe_video_stream_profile(path: Path) -> tuple[str, str, str]: try: with av.open(str(path)) as container: @@ -1125,6 +1179,9 @@ def _persist_result_artifacts( source_media_artifact = _persist_source_media_artifact(capture, media_file_path) if source_media_artifact is not None: artifacts.append(source_media_artifact) + source_audio_artifact = _persist_source_audio_artifact(capture, artifact_dir, media_file_path) + if source_audio_artifact is not None: + artifacts.append(source_audio_artifact) preview_media_artifact = _persist_preview_media_artifact(capture, artifact_dir, media_file_path) if preview_media_artifact is not None: artifacts.append(preview_media_artifact) @@ -1243,6 +1300,7 @@ def update_pending_progress(percent: int, detail: str) -> None: current = repository.get(capture_id) or capture persist_capture( asset_preparation_pending=False, + result_state="complete", result=persisted_result, processing=_update_processing( current, @@ -1259,6 +1317,7 @@ def update_pending_progress(percent: int, detail: str) -> None: if current is not None and current.asset_preparation_pending: persist_capture( asset_preparation_pending=False, + result_state="complete", processing=_update_processing( current, current_stage="completed", @@ -1415,6 +1474,7 @@ def recover_pending_captures() -> None: repository.update( capture_id, status="failed", + result_state="failed", finished_at=now_iso(), error_stage="input", error_message="排队中的本地文件已经不存在,请重新上传。", @@ -1430,6 +1490,7 @@ def recover_pending_captures() -> None: repository.update( capture_id, status="queued", + result_state="processing", error_stage=None, error_message=None, processing=_update_processing( @@ -1517,6 +1578,7 @@ def update_live_progress( diagnostics.start("resolve", input_type=capture.input_type) persist_capture( status="processing", + result_state="processing", started_at=capture.started_at or now_iso(), processing=_update_processing( capture, @@ -1713,6 +1775,7 @@ def transcription_progress(progress_ratio: float, detail: str) -> None: _ensure_final_result(source, result) persist_capture( source=source, + result_state="text_ready", processing=_update_processing( capture, current_stage="compose", @@ -1756,6 +1819,7 @@ def transcription_progress(progress_ratio: float, detail: str) -> None: ) persist_capture( status="done", + result_state="text_ready" if prepare_images_async else "complete", finished_at=now_iso(), source=source, asset_preparation_pending=prepare_images_async, @@ -1794,6 +1858,7 @@ def transcription_progress(progress_ratio: float, detail: str) -> None: repository.update( capture.id, status="failed", + result_state="failed", finished_at=now_iso(), error_stage=exc.stage, error_message=_public_error_message_with_reason(exc.stage, exc.reason_code, exc.message), @@ -1830,6 +1895,7 @@ def transcription_progress(progress_ratio: float, detail: str) -> None: repository.update( capture.id, status="failed", + result_state="failed", finished_at=now_iso(), error_stage="internal", error_message=PUBLIC_ERROR_MESSAGES["internal"], diff --git a/app/providers/transcription.py b/app/providers/transcription.py index 7c9c8af..2321c36 100644 --- a/app/providers/transcription.py +++ b/app/providers/transcription.py @@ -1,9 +1,14 @@ from __future__ import annotations +import hashlib +import hmac +import time +import uuid from dataclasses import dataclass from pathlib import Path from typing import Callable, Protocol +import av import httpx from app.settings import ( @@ -12,9 +17,13 @@ OPENAI_COMPATIBLE_API_KEY, OPENAI_COMPATIBLE_BASE_URL, OPENAI_COMPATIBLE_MODEL, + DEVICE, + MODEL_PATH, + SENSEVOICE_MODEL_DIR, TRANSCRIPTION_AVAILABLE, TRANSCRIPTION_PROVIDER, WORKER_TRANSCRIBE_URL, + WORKER_TRANSCRIBE_SHARED_SECRET, ) from scripts.run_transcribe import transcribe_file @@ -31,6 +40,50 @@ class TranscriptionResult: TranscriptionProgressCallback = Callable[[float, str], None] +def _sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _probe_audio_duration(path: Path) -> float: + with av.open(str(path)) as container: + if container.duration is not None: + return max(float(container.duration) / 1_000_000.0, 0.001) + durations = [ + float(stream.duration * stream.time_base) + for stream in container.streams + if stream.type == "audio" and stream.duration is not None and stream.time_base is not None + ] + if not durations: + raise RuntimeError("无法确认音频时长,云端转写已停止。") + return max(max(durations), 0.001) + + +def _build_worker_auth_headers(path: Path, *, secret: str, duration_seconds: float) -> dict[str, str]: + if not secret: + raise RuntimeError("云端转写服务尚未完成安全配置。") + timestamp = str(int(time.time())) + nonce = uuid.uuid4().hex + duration = f"{duration_seconds:.3f}" + body_hash = _sha256_file(path) + service = "wanxiang-backend" + payload = "\n".join([timestamp, nonce, duration, body_hash, service]) + signature = hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest() + return { + "Content-Type": "application/octet-stream", + "Content-Length": str(path.stat().st_size), + "X-Praxis-Timestamp": timestamp, + "X-Praxis-Nonce": nonce, + "X-Praxis-Audio-Duration": duration, + "X-Praxis-Content-SHA256": body_hash, + "X-Praxis-Service": service, + "X-Praxis-Signature": signature, + } + + class TranscriptionProvider(Protocol): name: str @@ -134,8 +187,9 @@ def transcribe( class CloudflareWorkersAIWhisperProvider: name = "cloudflare_workers_ai" - def __init__(self, *, transcribe_url: str) -> None: + def __init__(self, *, transcribe_url: str, shared_secret: str) -> None: self.transcribe_url = transcribe_url + self.shared_secret = shared_secret def transcribe( self, @@ -154,11 +208,18 @@ def transcribe( if progress_callback is not None: progress_callback(0.1, "正在通过云端免费 Whisper 进行转写。") + duration_seconds = _probe_audio_duration(file_path) + headers = _build_worker_auth_headers( + file_path, + secret=self.shared_secret, + duration_seconds=duration_seconds, + ) with file_path.open("rb") as handle: with httpx.Client(timeout=DEFAULT_TIMEOUT_SECONDS * 10, trust_env=False) as client: response = client.post( self.transcribe_url, - files={"file": (file_path.name, handle, "application/octet-stream")}, + headers=headers, + content=handle, ) response.raise_for_status() payload = response.json() @@ -179,6 +240,85 @@ def transcribe( ) +class LocalSenseVoiceProvider: + name = "local_sensevoice" + + def __init__(self, model_dir: str | Path | None = None, *, num_threads: int | None = None) -> None: + self.model_dir = Path(model_dir or SENSEVOICE_MODEL_DIR) + self.num_threads = max(1, num_threads or 2) + self._recognizer_instance = None + + def _recognizer(self): + if self._recognizer_instance is not None: + return self._recognizer_instance + try: + import sherpa_onnx + except ImportError as exc: + raise RuntimeError("轻量转写组件尚未安装完整。") from exc + model_path = self.model_dir / "model.int8.onnx" + tokens_path = self.model_dir / "tokens.txt" + if not model_path.is_file() or not tokens_path.is_file(): + raise RuntimeError("轻量转写模型尚未准备完成。") + self._recognizer_instance = sherpa_onnx.OfflineRecognizer.from_sense_voice( + model=str(model_path), + tokens=str(tokens_path), + num_threads=self.num_threads, + provider="cpu", + language="auto", + use_itn=True, + ) + return self._recognizer_instance + + @staticmethod + def _decode_audio(path: Path): + import numpy as np + + chunks = [] + with av.open(str(path)) as container: + if not container.streams.audio: + raise RuntimeError("媒体文件中没有可识别的音轨。") + resampler = av.AudioResampler(format="flt", layout="mono", rate=16000) + for frame in container.decode(audio=0): + for converted in resampler.resample(frame): + chunks.append(converted.to_ndarray().reshape(-1)) + for converted in resampler.resample(None): + chunks.append(converted.to_ndarray().reshape(-1)) + if not chunks: + raise RuntimeError("媒体文件中没有可识别的音频内容。") + return np.concatenate(chunks).astype("float32", copy=False) + + def transcribe( + self, + media_file_path: str, + *, + capture_id: str, + output_folder: str | Path, + output_stem: str = "raw_transcript", + source_url: str | None = None, + progress_callback: TranscriptionProgressCallback | None = None, + ) -> TranscriptionResult: + path = Path(media_file_path) + if not path.is_file(): + raise FileNotFoundError(path) + if progress_callback is not None: + progress_callback(0.1, "正在使用轻量引擎识别音频。") + samples = self._decode_audio(path) + recognizer = self._recognizer() + stream = recognizer.create_stream() + stream.accept_waveform(16000, samples) + recognizer.decode_stream(stream) + transcript = str(stream.result.text or "").strip() + if progress_callback is not None: + progress_callback(1.0, "音频识别完成,正在整理文字。") + return TranscriptionResult( + provider=self.name, + transcript_text=transcript, + timeline_text="", + language=None, + segment_count=1 if transcript else 0, + ) + + class UnavailableTranscriptionProvider: name = "unavailable" @@ -202,8 +342,24 @@ def get_transcription_provider() -> TranscriptionProvider: api_key=OPENAI_COMPATIBLE_API_KEY, model=OPENAI_COMPATIBLE_MODEL, ) - if DEPLOYMENT_MODE == "cloud_preview": - return CloudflareWorkersAIWhisperProvider(transcribe_url=WORKER_TRANSCRIBE_URL) + if DEPLOYMENT_MODE == "cloud_preview" and WORKER_TRANSCRIBE_SHARED_SECRET: + return CloudflareWorkersAIWhisperProvider( + transcribe_url=WORKER_TRANSCRIBE_URL, + shared_secret=WORKER_TRANSCRIBE_SHARED_SECRET, + ) + sensevoice_ready = (SENSEVOICE_MODEL_DIR / "model.int8.onnx").is_file() and ( + SENSEVOICE_MODEL_DIR / "tokens.txt" + ).is_file() + faster_whisper_ready = MODEL_PATH.is_dir() + if TRANSCRIPTION_PROVIDER == "local_sensevoice" and sensevoice_ready: + return LocalSenseVoiceProvider() + if TRANSCRIPTION_PROVIDER == "auto": + if DEVICE == "cuda" and faster_whisper_ready: + return LocalFasterWhisperProvider() + if sensevoice_ready: + return LocalSenseVoiceProvider() + if faster_whisper_ready: + return LocalFasterWhisperProvider() if TRANSCRIPTION_AVAILABLE and TRANSCRIPTION_PROVIDER == "openai_compatible": return UnavailableTranscriptionProvider() if TRANSCRIPTION_AVAILABLE: diff --git a/app/runtime_packs.py b/app/runtime_packs.py new file mode 100644 index 0000000..78895ab --- /dev/null +++ b/app/runtime_packs.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import hashlib +import hmac +import json +import os +import shutil +import tempfile +import zipfile +from dataclasses import dataclass +from pathlib import Path +from typing import Any +from urllib.parse import urljoin, urlparse + +import httpx + + +class RuntimePackError(RuntimeError): + pass + + +@dataclass(frozen=True) +class RuntimePack: + pack_id: str + version: str + url: str + sha256: str + size_bytes: int + install_subdir: str + archive_format: str = "zip" + required: bool = False + + +def _safe_subdir(value: str) -> str: + path = Path(value) + if not value or path.is_absolute() or ".." in path.parts: + raise RuntimePackError("运行组件安装目录无效。") + return value + + +def _parse_pack(item: dict[str, Any]) -> RuntimePack: + pack_id = str(item.get("id") or "").strip() + version = str(item.get("version") or "").strip() + url = str(item.get("url") or "").strip() + sha256 = str(item.get("sha256") or "").strip().lower() + archive_format = str(item.get("archive_format") or "zip").strip().lower() + try: + size_bytes = int(item.get("size_bytes") or 0) + except (TypeError, ValueError) as exc: + raise RuntimePackError("运行组件大小无效。") from exc + + if not pack_id or not version or not pack_id.replace("-", "").replace("_", "").isalnum(): + raise RuntimePackError("运行组件标识或版本无效。") + parsed_url = urlparse(url) + if parsed_url.scheme != "https" or not parsed_url.hostname or parsed_url.username or parsed_url.password: + raise RuntimePackError("运行组件必须使用可信的 HTTPS 下载地址。") + if len(sha256) != 64 or any(character not in "0123456789abcdef" for character in sha256): + raise RuntimePackError("运行组件缺少有效的 SHA-256。") + if size_bytes <= 0: + raise RuntimePackError("运行组件缺少有效的文件大小。") + if archive_format not in {"zip", "raw"}: + raise RuntimePackError("运行组件压缩格式不受支持。") + return RuntimePack( + pack_id=pack_id, + version=version, + url=url, + sha256=sha256, + size_bytes=size_bytes, + install_subdir=_safe_subdir(str(item.get("install_subdir") or pack_id).strip()), + archive_format=archive_format, + required=bool(item.get("required", False)), + ) + + +class RuntimePackManager: + def __init__(self, manifest_path: Path, runtime_dir: Path) -> None: + self.manifest_path = manifest_path + self.runtime_dir = runtime_dir + self.download_dir = runtime_dir / ".downloads" + self.staging_dir = runtime_dir / ".staging" + + def packs(self) -> list[RuntimePack]: + try: + payload = json.loads(self.manifest_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise RuntimePackError("运行组件清单不可用,请重新安装应用。") from exc + if payload.get("schema_version") != 1 or not isinstance(payload.get("packs"), list): + raise RuntimePackError("运行组件清单版本不受支持。") + packs = [_parse_pack(item) for item in payload["packs"] if isinstance(item, dict)] + if len({pack.pack_id for pack in packs}) != len(packs): + raise RuntimePackError("运行组件清单包含重复项目。") + return packs + + def get(self, pack_id: str) -> RuntimePack: + pack = next((item for item in self.packs() if item.pack_id == pack_id), None) + if pack is None: + raise RuntimePackError("未找到该运行组件。") + return pack + + def status(self) -> list[dict[str, Any]]: + records: list[dict[str, Any]] = [] + for pack in self.packs(): + marker = self.runtime_dir / pack.install_subdir / ".wanxiang-pack.json" + installed = False + if marker.is_file(): + try: + metadata = json.loads(marker.read_text(encoding="utf-8")) + installed = metadata.get("id") == pack.pack_id and metadata.get("version") == pack.version + except (OSError, json.JSONDecodeError): + installed = False + records.append( + { + "id": pack.pack_id, + "version": pack.version, + "size_bytes": pack.size_bytes, + "required": pack.required, + "installed": installed, + } + ) + return records + + def install(self, pack_id: str) -> dict[str, Any]: + pack = self.get(pack_id) + self.download_dir.mkdir(parents=True, exist_ok=True) + self.staging_dir.mkdir(parents=True, exist_ok=True) + archive_path = self.download_dir / f"{pack.pack_id}-{pack.version}.{pack.archive_format}.part" + self._download(pack, archive_path) + self._verify(pack, archive_path) + target = self.runtime_dir / pack.install_subdir + stage = Path(tempfile.mkdtemp(prefix=f"{pack.pack_id}-", dir=self.staging_dir)) + backup = target.with_name(f"{target.name}.previous") + try: + self._unpack(pack, archive_path, stage) + (stage / ".wanxiang-pack.json").write_text( + json.dumps({"id": pack.pack_id, "version": pack.version}, ensure_ascii=False), + encoding="utf-8", + ) + if backup.exists(): + shutil.rmtree(backup) + target.parent.mkdir(parents=True, exist_ok=True) + if target.exists(): + os.replace(target, backup) + try: + os.replace(stage, target) + except Exception: + if backup.exists() and not target.exists(): + os.replace(backup, target) + raise + if backup.exists(): + shutil.rmtree(backup) + except Exception: + shutil.rmtree(stage, ignore_errors=True) + raise + finally: + archive_path.unlink(missing_ok=True) + return {"id": pack.pack_id, "version": pack.version, "installed": True} + + def _download(self, pack: RuntimePack, destination: Path) -> None: + existing = destination.stat().st_size if destination.exists() else 0 + if existing == pack.size_bytes: + return + if existing > pack.size_bytes: + destination.unlink() + existing = 0 + headers = {"Range": f"bytes={existing}-"} if existing else {} + with httpx.Client(timeout=httpx.Timeout(30.0, read=120.0), follow_redirects=False, trust_env=False) as client: + current_url = pack.url + response = None + for _ in range(4): + request = client.build_request("GET", current_url, headers=headers) + response = client.send(request, stream=True) + if response.status_code not in {301, 302, 303, 307, 308}: + break + location = response.headers.get("location", "") + response.close() + current_url = urljoin(current_url, location) + parsed = urlparse(current_url) + if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password: + raise RuntimePackError("运行组件下载发生了不安全的重定向。") + else: + raise RuntimePackError("运行组件下载重定向次数过多。") + if response is None: + raise RuntimePackError("运行组件下载未返回有效响应。") + with response: + if existing and response.status_code == 200: + destination.unlink(missing_ok=True) + existing = 0 + elif existing and response.status_code != 206: + raise RuntimePackError("运行组件断点续传失败。") + response.raise_for_status() + mode = "ab" if existing else "wb" + with destination.open(mode) as handle: + for chunk in response.iter_bytes(1024 * 1024): + handle.write(chunk) + if handle.tell() > pack.size_bytes: + raise RuntimePackError("运行组件大小与清单不一致。") + + def _verify(self, pack: RuntimePack, path: Path) -> None: + if not path.is_file() or path.stat().st_size != pack.size_bytes: + raise RuntimePackError("运行组件下载不完整。") + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + if not hmac.compare_digest(digest.hexdigest(), pack.sha256): + raise RuntimePackError("运行组件校验失败,已拒绝安装。") + + def _unpack(self, pack: RuntimePack, archive_path: Path, stage: Path) -> None: + if pack.archive_format == "raw": + shutil.copy2(archive_path, stage / pack.pack_id) + return + try: + with zipfile.ZipFile(archive_path) as archive: + stage_root = stage.resolve() + for member in archive.infolist(): + destination = (stage / member.filename).resolve() + if destination != stage_root and stage_root not in destination.parents: + raise RuntimePackError("运行组件包含不安全的文件路径。") + archive.extractall(stage) + except zipfile.BadZipFile as exc: + raise RuntimePackError("运行组件压缩包已损坏。") from exc diff --git a/app/schemas.py b/app/schemas.py index 93fd2db..5685a9e 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -6,6 +6,8 @@ CaptureStatus = Literal["queued", "processing", "done", "failed"] +CaptureResultState = Literal["processing", "text_ready", "complete", "failed"] +ArtifactStatus = Literal["pending", "ready", "failed"] InputType = Literal["url", "file"] ContentType = Literal["video", "audio", "article", "image_article", "webpage", "unknown"] ResultOrigin = Literal["subtitle", "transcript", "article", "notes", "ocr"] @@ -24,6 +26,8 @@ class ArtifactModel(BaseModel): download_url: str mime_type: str = "application/octet-stream" size_bytes: int | None = None + status: ArtifactStatus = "ready" + optional: bool = False class ArtifactPayloadModel(BaseModel): @@ -32,6 +36,8 @@ class ArtifactPayloadModel(BaseModel): download_url: str mime_type: str = "application/octet-stream" size_bytes: int | None = None + status: ArtifactStatus = "ready" + optional: bool = False class CaptureSourceImagePayloadModel(BaseModel): @@ -147,6 +153,7 @@ class CaptureModel(BaseModel): id: str input_type: InputType status: CaptureStatus + result_state: CaptureResultState = "processing" title: str | None = None source_platform: str content_type: str @@ -191,6 +198,7 @@ class CaptureCreateUrlRequest(BaseModel): class CaptureStatePayloadModel(BaseModel): id: str status: CaptureStatus + result_state: CaptureResultState = "processing" input_kind: InputType current_stage: str = "queued" progress_percent: int = 0 diff --git a/app/settings.py b/app/settings.py index ff788f1..91e6f21 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +from pathlib import Path from scripts.config import ( ALLOWED_ORIGINS, @@ -30,12 +31,14 @@ PROJECT_ROOT, PUBLIC_PREVIEW_MODE, RUNTIME_DIR, + SENSEVOICE_MODEL_DIR, SUPPORTED_EXTENSIONS, TEMP_DIR, TRANSCRIPTION_PROVIDER, VAD_FILTER, WORKSPACE_DIR, WORKER_RELAY_BASE, + WORKER_TRANSCRIBE_SHARED_SECRET, WORKER_TRANSCRIBE_URL, ) @@ -49,6 +52,16 @@ APP_DISPLAY_NAME = f"{PRODUCT_NAME} API" CODE_NAME = os.environ.get("RELAY_ENGINE_NAME", "capture_text") +APP_VERSION = os.environ.get("AUDIO2TEXT_VERSION", "2.0.0-dev").strip() or "2.0.0-dev" +APP_COMMIT = os.environ.get("AUDIO2TEXT_COMMIT", "unknown").strip()[:40] or "unknown" +DESKTOP_TOKEN = os.environ.get("AUDIO2TEXT_DESKTOP_TOKEN", "").strip() +RUNTIME_TARGET = os.environ.get( + "AUDIO2TEXT_RUNTIME_TARGET", + "cloud_demo" if DEPLOYMENT_MODE == "cloud_preview" else "local_web", +).strip() or "local_web" +RUNTIME_PACK_MANIFEST = Path( + os.environ.get("AUDIO2TEXT_RUNTIME_PACK_MANIFEST", str(PROJECT_ROOT / "runtime-packs.json")) +).expanduser().resolve() FRONTEND_DIR = PROJECT_ROOT / "frontend" WEB_APP_DIR = PROJECT_ROOT / "web" @@ -94,7 +107,7 @@ ADMIN_IPS = {ip.strip() for ip in os.environ.get("ADMIN_IPS", "").split(",") if ip.strip()} FREE_DURATION_SECONDS = FREE_DURATION_MINUTES * 60 TRANSCRIPTION_AVAILABLE = ( - DEPLOYMENT_MODE == "cloud_preview" + (DEPLOYMENT_MODE == "cloud_preview" and bool(WORKER_TRANSCRIBE_SHARED_SECRET)) or ( TRANSCRIPTION_PROVIDER == "openai_compatible" and bool(OPENAI_COMPATIBLE_BASE_URL) @@ -102,10 +115,14 @@ and bool(OPENAI_COMPATIBLE_MODEL) ) or ( - TRANSCRIPTION_PROVIDER != "openai_compatible" - and MODEL_PATH.exists() + TRANSCRIPTION_PROVIDER in {"local_faster_whisper", "auto"} and MODEL_PATH.is_dir() ) + or ( + TRANSCRIPTION_PROVIDER in {"local_sensevoice", "auto"} + and (SENSEVOICE_MODEL_DIR / "model.int8.onnx").is_file() + and (SENSEVOICE_MODEL_DIR / "tokens.txt").is_file() + ) ) RELAY_RUN_MODE = os.environ.get("RELAY_RUN_MODE", "local").strip().lower() or "local" diff --git a/app/source_adapters.py b/app/source_adapters.py index 3a2eaec..118f78f 100644 --- a/app/source_adapters.py +++ b/app/source_adapters.py @@ -1058,6 +1058,8 @@ def extract( ) browser_error: str | None = None + browser_reason_code: str | None = None + browser_retryable = True browser_outcome: ExtractionOutcome | None = None provider_traces: list[ProviderTraceEntry] = [ ProviderTraceEntry(stage="extract", level="info", provider="browser_provider", message="开始尝试浏览器会话。") @@ -1079,6 +1081,8 @@ def extract( return browser_outcome except BrowserProviderError as exc: browser_error = exc.message + browser_reason_code = exc.reason_code + browser_retryable = exc.retryable provider_traces.append( ProviderTraceEntry( stage="extract", @@ -1090,6 +1094,8 @@ def extract( ) except ExtractionError as exc: browser_error = exc.message + browser_reason_code = exc.reason_code + browser_retryable = exc.retryable provider_traces.append( ProviderTraceEntry( stage="extract", @@ -1143,6 +1149,15 @@ def extract( browser_outcome.fallback_used = False browser_outcome.provider_traces = list(provider_traces) return _prepend_warnings(browser_outcome, [browser_error] if browser_error else []) + if browser_reason_code in {"browser_challenge_required", "browser_session_expired"} and browser_error: + raise ExtractionError( + "extract", + browser_error, + warnings=([browser_error] if browser_error else []) + list(exc.warnings), + reason_code=browser_reason_code, + retryable=browser_retryable, + provider_traces=list(provider_traces), + ) from exc exc.warnings = ([browser_error] if browser_error else []) + list(exc.warnings) exc.provider_traces = list(provider_traces) raise diff --git a/audio2text.settings.example.json b/audio2text.settings.example.json index 9c8d25a..39f9bf2 100644 --- a/audio2text.settings.example.json +++ b/audio2text.settings.example.json @@ -10,7 +10,8 @@ "beam_size": 1, "vad_filter": true, "max_upload_size_mb": 1024, - "transcription_provider": "local_faster_whisper", + "transcription_provider": "auto", + "sensevoice_model_path": "workspace/runtime/models/sensevoice/small-int8", "openai_compatible_base_url": "", "openai_compatible_api_key": "", "openai_compatible_model": "gpt-4o-mini-transcribe", diff --git a/docs/assets/home-desktop.png b/docs/assets/home-desktop.png new file mode 100644 index 0000000..0bc203a Binary files /dev/null and b/docs/assets/home-desktop.png differ diff --git a/docs/assets/home-mobile.png b/docs/assets/home-mobile.png new file mode 100644 index 0000000..70c42ad Binary files /dev/null and b/docs/assets/home-mobile.png differ diff --git a/docs/assets/result-images-mobile.png b/docs/assets/result-images-mobile.png new file mode 100644 index 0000000..9087983 Binary files /dev/null and b/docs/assets/result-images-mobile.png differ diff --git a/docs/assets/result-video-desktop.png b/docs/assets/result-video-desktop.png new file mode 100644 index 0000000..3d1e0b8 Binary files /dev/null and b/docs/assets/result-video-desktop.png differ diff --git a/docs/assets/wanxiang-demo.mp4 b/docs/assets/wanxiang-demo.mp4 new file mode 100644 index 0000000..b58b93e Binary files /dev/null and b/docs/assets/wanxiang-demo.mp4 differ diff --git a/docs/deployment.md b/docs/deployment.md index af22a4f..da81d53 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -5,6 +5,7 @@ 这是一个单机、单实例、文件存储、单页交付型产品。 部署目标固定为: + - 保持项目级环境隔离 - 保持结果可交付 - 保持失败可解释 @@ -30,9 +31,10 @@ workspace/ ``` 说明: + - `models`、`playwright-browsers`、`ffmpeg`、`browser-profile` 都属于项目级运行时目录。 - `captures`、`artifacts`、`temp`、`logs` 都属于项目级数据目录。 -- 视频预览件 `preview_media`、下载原件 `source_media`、图片、Live 片段、`.txt/.md` 都会落到 `workspace/artifacts//`。 +- 视频预览件 `preview_media`、下载原件 `source_media`、音频导出件 `source_audio`、图片、Live 片段、`.txt/.md` 都会落到 `workspace/artifacts//`。 ## 启动前 preflight @@ -166,6 +168,7 @@ docker run --rm --name praxis-audio2text -p 8000:8000 ` ``` 要求: + - `workspace/runtime/models/faster-whisper/medium` 已经存在可用模型文件。 - 如需浏览器提取链路,挂载后的 `workspace/runtime/playwright-browsers` 也要可用。 @@ -184,6 +187,7 @@ docker run --rm --name praxis-audio2text -p 8000:8000 ` ``` 当前约定: + - 镜像内会自行执行前端构建。 - Docker 不依赖宿主机已有 `frontend/dist`。 - 容器内默认使用镜像自带 `/usr/bin/ffmpeg`。 @@ -205,6 +209,7 @@ docker run --rm --name praxis-audio2text -p 8000:8000 ` ``` 默认会做四件事: + - 检查本地服务是否可用;不可用时自动启动 `scripts.start_api`。 - 优先创建 Cloudflare Quick Tunnel。 - 如果 Cloudflare 建连失败,自动回退 localtunnel。 @@ -218,13 +223,20 @@ $env:AUDIO2TEXT_LOCAL_ONLY = "1" ``` 注意: + - 电脑不能关,启动窗口不能关。 - localtunnel 首次访问可能出现 IP 确认页;按启动器提示输入页面显示的 IP 即可继续。 - 这仍是临时演示链接,不是正式云部署。 ### 内部 tunnel helper -日常启动只使用 `start_api.bat`。它会调用 `scripts.public_preview`,并在内部优先尝试 Cloudflare Quick Tunnel,失败时回退 localtunnel。 +日常启动只使用 `start_api.bat`。它会先调用 `scripts/start_api_bootstrap.ps1` 做环境自检和自愈,再调用 `scripts.public_preview`;公网链路优先尝试 Cloudflare Quick Tunnel,失败时回退 localtunnel。 + +启动自检负责: + +- `.venv` 缺失、损坏或 uv 托管 Python 路径漂移时,自动用 Python 3.11 重建虚拟环境。 +- 按 `requirements.txt` 的锁定版本安装依赖,减少换电脑或未来依赖漂移造成的故障。 +- 检查前端构建产物、模型、ffmpeg、Playwright Chromium、CUDA runtime 和端口。 `scripts/start_cloudflare_tunnel.py` 和 `scripts/start_localtunnel.py` 只保留为内部 helper,供公网预览编排器自动调用,或排障时临时使用。不要把它们作为用户文档里的同级启动入口。 @@ -235,6 +247,7 @@ $env:AUDIO2TEXT_LOCAL_ONLY = "1" 适合做免费云端预览。它能给 `https://-.hf.space` 链接,但免费磁盘不是长期持久存储,运行产物只适合演示。 前提: + - 已安装项目 `.venv` 内的 `hf` CLI;当前仓库可直接使用 `.\.venv\Scripts\hf.exe`。 - 已执行 `.\.venv\Scripts\hf.exe auth login`。 - 已准备 OpenAI-compatible 转写服务;不要在免费 Space 里默认依赖本地 Faster-Whisper 模型目录。 @@ -277,6 +290,7 @@ app_port: 8000 ``` 限制: + - 免费 Space 的 `workspace` 数据可能随重启、重建或休眠丢失,不要当正式资料库。 - OpenAI-compatible provider 的 API key 只能放在 Space Secret,不要写进仓库。 - 高波动平台仍可能因为浏览器会话、验证码、平台风控失败。 diff --git a/docs/pre-launch-prompt.md b/docs/pre-launch-prompt.md index 43aa4db..bba44c6 100644 --- a/docs/pre-launch-prompt.md +++ b/docs/pre-launch-prompt.md @@ -90,6 +90,7 @@ ### 第七步:最终构建确认 所有修改完成后,重新跑一遍完整验证: + 1. `python -m unittest discover -s tests -p "test_*.py"` 2. `cd web && npm exec tsc -- --noEmit` 3. `cd web && npm run build` diff --git a/docs/release-ready.md b/docs/release-ready.md index 57de815..e87dff2 100644 --- a/docs/release-ready.md +++ b/docs/release-ready.md @@ -5,9 +5,14 @@ 当前版本按“单页交付型产品”发布,不按工作台或多角色系统发布。 对外只承诺三类能力: + - 音频 / 视频转文字 - 图文正文与正文图片提取 -- 视频结果页预览并下载原视频 +- 视频结果页预览并下载原视频、原音频 + +Windows 安装包是完整主产品;云端用于限额演示。目标电脑不需要 +Python、uv、CUDA 或单独安装 Chromium。首次使用安装 FFmpeg 9.0 +Essentials 与 SenseVoice int8 两个校验运行包,总下载约 222 MB。 ## 当前真实交付形态 @@ -29,7 +34,7 @@ - 正文卡片内部滚动,视频卡片与正文卡片等高 - meta footer 左对齐展示平台、内容类型、时长与查看来源 - `.txt` 始终保留,`.md` 只在真实 Markdown 可用时出现 -- 视频预览优先 `preview_media`,下载始终使用 `source_media` +- 视频预览优先 `preview_media`,下载原视频使用 `source_media`,有音轨时提供 `source_audio` ## 当前不应误宣称的内容 @@ -41,11 +46,18 @@ ## 发布前硬门槛 - 后端自动化测试通过 +- Worker 安全测试与 npm 官方安全审计通过 - 前端类型检查通过 - 前端生产构建通过 +- Rust `fmt` / `clippy -D warnings` 通过 +- NSIS 在干净目录静默安装成功,安装后 WebView 能调用 Tauri 命令并访问 `/config` +- 强制结束桌面主进程后,sidecar 必须在 3 秒内退出 +- 运行包清单、下载大小和 SHA-256 与 GitHub Release 资产一致 +- `start_api.bat` 可在 `.venv` 缺失、损坏或 Python 路径漂移时自动重建项目虚拟环境 +- Python 依赖必须按 `requirements.txt` 的锁定版本安装,避免换电脑或隔一段时间后依赖漂移 - 默认端口未被旧服务占用;若被占用,`scripts.start_api` 必须清晰失败并提示占用进程 - 用户可见启动入口只保留一条:`start_api.bat`;默认启动本地服务和公网预览,`AUDIO2TEXT_LOCAL_ONLY=1` 仅用于本地排障 -- Docker 镜像可从当前仓库直接构建 +- Docker 镜像可从当前仓库直接构建,但不作为 Windows 主产品的前置条件 - Docker 运行口径必须明确选择“挂载本地 Faster-Whisper 模型”或“使用 openai_compatible 转写” - Hugging Face Spaces 预览必须使用 `openai_compatible`;不要把本地 Faster-Whisper 模型打进镜像或上传到 Space 仓库 - README / 部署 / 验收 / 发布文档与真实代码行为一致 @@ -56,6 +68,10 @@ - 云端未配置 OpenAI-compatible 转写服务时,首页仍应可用,本地音视频文件上传前必须给出明确提示 - 最终发布前必须执行 `.\.venv\Scripts\python.exe -m scripts.release_gate --final --with-live-smoke --live-smoke-base-url "http://127.0.0.1:8000"` +本轮 2026-08-11 证据:哔哩哔哩、小宇宙、抖音、微信图文通过; +小红书图文与 Live(15 图、14 个 Live 片段)通过。小红书视频样本触发 +平台会话验证,YouTube 在当前中国网络约 53 秒内明确失败,不得写成已通过。 + ## 建议发布前复验的平台样例 - YouTube:1 条字幕视频 @@ -68,7 +84,8 @@ ## 发布后仍应关注的边界 - 抖音、小红书仍属于高波动平台,成功率依赖公开链路与项目级浏览器会话 -- Faster-Whisper 本地模型仍需要显式部署到项目级 runtime 目录 +- Windows CPU 默认使用 SenseVoice int8;CUDA 机器可继续使用项目级 Faster-Whisper medium 精准档 +- 小红书视频发布样本需要新鲜分享链接与有效项目级浏览器会话 - `Assets/Models/...` 当前仍是 legacy fallback,不应在未迁移前直接删除 ## 仓库清理口径 diff --git a/docs/transcription-benchmark.md b/docs/transcription-benchmark.md index b74b3a0..36d85fc 100644 --- a/docs/transcription-benchmark.md +++ b/docs/transcription-benchmark.md @@ -2,6 +2,13 @@ Audio2Text should choose local ASR models by measured behavior, not by model size alone. +The Windows default is SenseVoice int8 on CPU because it runs on machines without +CUDA and avoids shipping multiple model families. The current one-sample hardware +probe measured approximately RTF 0.035 on CPU, versus Faster-Whisper medium at +approximately RTF 0.153 on RTX 3060 and 2.01 on CPU. This is a speed and packaging +decision, not a final accuracy verdict; the 60-sample / 3-hour benchmark gate still +applies before replacing the precision profile. + Use `scripts.benchmark_transcription` to compare Faster-Whisper model directories or Hugging Face model IDs with the same audio samples. ```powershell @@ -17,7 +24,15 @@ Use `scripts.benchmark_transcription` to compare Faster-Whisper model directorie Manifest format: ```jsonl -{"id":"zh_business_hours","audio":"E:/path/to/zh.wav","reference":"开放时间早上9点至下午5点","terms":["9点","5点"]} +{ + "id": "zh_business_hours", + "audio": "E:/path/to/zh.wav", + "reference": "开放时间早上9点至下午5点", + "terms": [ + "9点", + "5点" + ] +} ``` Selection rule: @@ -35,4 +50,7 @@ On the RTX 3060 Laptop GPU 6GB setup, the current production baseline is: - beam size: `1` - VAD: enabled -This baseline is intentionally fast and stable. Larger candidates such as CTranslate2 `large-v3` or `large-v3-turbo` should be downloaded and selected only after this benchmark reports a clear quality win on real samples. +This GPU baseline remains the precision profile. Larger candidates such as +CTranslate2 `large-v3-turbo` or Qwen3-ASR should be downloaded only in a benchmark +workspace and selected only after they satisfy the repository quality and speed gate; +they are not part of the standard Windows runtime. diff --git a/docs/verification.md b/docs/verification.md index 47cd09c..8508359 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -52,10 +52,14 @@ Docker 构建限时门槛: ## 本地 API 启动 ```powershell -.\.venv\Scripts\python.exe -m scripts.start_api +.\start_api.bat ``` 检查: + +- 首次启动或 `.venv` 损坏时,入口应自动重建 Python 3.11 虚拟环境并安装 `requirements.txt` 的锁定依赖 +- `frontend/dist` 缺失时,入口应在 `npm` 可用时自动构建前端产物;无法构建时必须给出中文错误 +- 启动前自检应覆盖 Python、依赖、模型、ffmpeg、Playwright Chromium、CUDA runtime 和端口 - 启动前如果 `8000` 已被旧服务占用,脚本应直接失败并提示占用 PID / 命令,不应继续进入 uvicorn - `http://127.0.0.1:8000/health` 返回 `status=ok` - `http://127.0.0.1:8000/config` 不暴露运行时绝对路径 @@ -74,7 +78,9 @@ workspace/runtime/browser-profile ``` 补充检查: + - `workspace/runtime/browser-profile` 为空时,系统仍可启动,但高波动平台会给出会话 warning +- 日常启动不应要求用户手动修 `.venv`;若 uv 托管 Python 路径变化,`start_api.bat` 应自动重建环境 - 如需刷新项目级浏览器会话,应运行: ```powershell @@ -84,6 +90,7 @@ workspace/runtime/browser-profile ## 平台契约检查 自动化测试重点覆盖: + - URL 路由识别 - adapter 选择 - provider 顺序 @@ -98,6 +105,7 @@ workspace/runtime/browser-profile - `progress_percent` 重点文件: + - [tests/test_platform_contracts.py](../tests/test_platform_contracts.py) - [tests/test_capture_api.py](../tests/test_capture_api.py) - [tests/test_pipeline_and_store.py](../tests/test_pipeline_and_store.py) @@ -119,10 +127,10 @@ workspace/runtime/browser-profile ### 视频 - 有 `preview_media` 时,页面优先播放 `preview_media` -- 下载始终使用 `source_media` +- 下载原视频使用 `source_media`;有音轨时显示 `source_audio` 音频下载 - 桌面端视频卡片与正文卡片等高 - 页面不额外叠自定义悬浮全屏 / 下载按钮 -- 下载视频按钮 hover / focus 时边框清晰可见 +- 下载音频 / 下载视频按钮 hover / focus 时边框清晰可见 - 没有视频 artifact 但文本成功时,页面要明确提示“文本已完成,原视频暂未成功下载” ### 图文与 Live 图 @@ -160,6 +168,7 @@ workspace/runtime/browser-profile ## 人工页面审计 至少完整走一轮以下界面: + - 首页输入区 - 处理中页面 - 文本结果页 @@ -168,12 +177,14 @@ workspace/runtime/browser-profile - 最近记录展开、单条删除、清空与撤销 重点人工检查: + - 白底按钮 hover / focus 时边框是否依然清晰 - 桌面端结果页是否保持“上方双栏 + 下方 meta footer” - meta footer 是否左对齐稳定,不与主卡片冲突 - 桌面和手机宽度下操作区不会随机塌成难看的纵向布局 当前最小 UI smoke 已自动覆盖: + - 首页 Hero 输入框与最近记录渲染 - 最近记录展开、删除、撤销链路 - 提交链接后首次读取若遇到瞬时 503,页面仍会进入处理中而不是误判失败 @@ -211,6 +222,7 @@ docker run --rm --name praxis-audio2text -p 8000:8000 ` ``` 检查: + - Docker 构建不依赖宿主机已有 `frontend/dist` - 镜像启动后首页可访问 - 未挂载本地模型目录且未配置 OpenAI-compatible provider 时,预览模式仍可启动首页,但音视频转写应给出明确失败提示 @@ -227,6 +239,7 @@ docker run --rm --name praxis-audio2text -p 8000:8000 ` ``` 检查: + - 输出中出现最终公网链接。 - 链接已写入 `tests_runtime/public_preview/current_url.txt`。 - 关闭启动窗口后链接失效;启动窗口保持打开时,公网首页可访问。 @@ -259,6 +272,7 @@ $env:AUDIO2TEXT_LOCAL_ONLY = "1" ``` 检查: + - Space build logs 没有 fatal error - Space 配置了 `AUDIO2TEXT_TRANSCRIPTION_PROVIDER=openai_compatible` - API key 只存在 Space Secret,不在仓库文件中 @@ -269,6 +283,7 @@ $env:AUDIO2TEXT_LOCAL_ONLY = "1" ## Live Smoke 默认样例文件: + - [tests/live_smoke_samples.json](../tests/live_smoke_samples.json) 运行: @@ -298,6 +313,7 @@ $env:AUDIO2TEXT_LIVE_SMOKE_SAMPLES = "tests/live_smoke_samples.local.json" - Desktop hero titles are clamped to two lines; mobile hero titles are clamped to three lines. - Long titles keep the full text in the element `title` attribute for inspection. - Text and video result panels should remain visually equal-height on desktop, with long body text scrolling inside `.result-content-shell`. + ## Result Source Link Checks - When `canonical_url` is present, the result page should render a `查看来源` action chip in the bottom meta footer. diff --git a/requirements.txt b/requirements.txt index 171d7f6..63cc42f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,14 @@ -faster-whisper -fastapi -uvicorn -python-multipart -ctranslate2 -onnxruntime -av -httpx -requests -yt-dlp -playwright -youtube-transcript-api -opencc-python-reimplemented +faster-whisper==1.2.1 +fastapi==0.138.2 +uvicorn==0.49.0 +python-multipart==0.0.32 +ctranslate2==4.8.0 +onnxruntime==1.27.0 +av==17.1.0 +httpx==0.28.1 +requests==2.34.2 +sherpa-onnx==1.13.4 +yt-dlp==2026.6.9 +playwright==1.61.0 +youtube-transcript-api==1.2.4 +opencc-python-reimplemented==0.1.7 diff --git a/runtime-packs.json b/runtime-packs.json new file mode 100644 index 0000000..e2350c4 --- /dev/null +++ b/runtime-packs.json @@ -0,0 +1,26 @@ +{ + "schema_version": 1, + "application_compatibility": ">=2.0.0 <3.0.0", + "packs": [ + { + "id": "ffmpeg-essentials-windows-x64", + "version": "9.0", + "url": "https://github.com/qinxujunai/Audio2Text/releases/download/runtime-v1/ffmpeg-essentials-9.0-windows-x64.zip", + "sha256": "92ce52515025d2a89ba6fee617081aeeab10fa6044586d79f591f977647b7e57", + "size_bytes": 77319028, + "install_subdir": "ffmpeg", + "archive_format": "zip", + "required": true + }, + { + "id": "sensevoice-small-int8", + "version": "2025.09.09", + "url": "https://github.com/qinxujunai/Audio2Text/releases/download/runtime-v1/sensevoice-small-int8-2025-09-09-v2.zip", + "sha256": "eb81ab14d94ac71f4d52820f7a8bf2e091d58c49ae06e08b3b330a60d5109886", + "size_bytes": 154731511, + "install_subdir": "models/sensevoice/small-int8", + "archive_format": "zip", + "required": true + } + ] +} diff --git a/scripts/__init__.py b/scripts/__init__.py index e69de29..7867c46 100644 --- a/scripts/__init__.py +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Wanxiang Chengwen operational scripts.""" diff --git a/scripts/benchmark_transcription.py b/scripts/benchmark_transcription.py index ccd7b21..5f56898 100644 --- a/scripts/benchmark_transcription.py +++ b/scripts/benchmark_transcription.py @@ -3,8 +3,11 @@ import argparse import glob import json +import os +import platform import re import statistics +import subprocess import time from dataclasses import dataclass from pathlib import Path @@ -13,6 +16,7 @@ import av from faster_whisper import WhisperModel +from app.providers.transcription import LocalSenseVoiceProvider from scripts.config import BEAM_SIZE, COMPUTE_TYPE, DEVICE, LANGUAGE, MODEL_PATH, VAD_FILTER @@ -22,6 +26,9 @@ class BenchmarkSample: audio_path: Path reference: str = "" terms: tuple[str, ...] = () + slice_name: str = "general" + weight: float = 1.0 + expected_silence: bool = False def _normalize_text(value: str) -> str: @@ -49,6 +56,14 @@ def _char_error_rate(reference: str, hypothesis: str) -> float | None: return previous[-1] / len(reference) +def _percentile(values: list[float], percentile: float) -> float | None: + if not values: + return None + ordered = sorted(values) + index = max(0, min(len(ordered) - 1, round((len(ordered) - 1) * percentile))) + return ordered[index] + + def _duration_seconds(path: Path) -> float: with av.open(str(path)) as container: if container.duration is not None: @@ -85,6 +100,9 @@ def _load_manifest(path: Path, *, limit: int | None = None) -> list[BenchmarkSam audio_path=audio_path, reference=str(item.get("reference") or ""), terms=terms, + slice_name=str(item.get("slice") or "general").strip() or "general", + weight=max(0.0, float(item.get("weight", 1.0))), + expected_silence=bool(item.get("expected_silence", False)), ) ) if limit and len(samples) >= limit: @@ -132,7 +150,12 @@ def _transcribe_sample( beam_size=beam_size, vad_filter=vad_filter, ) - segment_list = list(segments) + segment_list = [] + first_segment_seconds = None + for segment in segments: + if first_segment_seconds is None: + first_segment_seconds = time.perf_counter() - started + segment_list.append(segment) transcript = "".join(segment.text.strip() for segment in segment_list).strip() elapsed = time.perf_counter() - started cer = _char_error_rate(sample.reference, transcript) @@ -146,6 +169,11 @@ def _transcribe_sample( "language": getattr(info, "language", None), "language_probability": getattr(info, "language_probability", None), "segment_count": len(segment_list), + "first_segment_seconds": None if first_segment_seconds is None else round(first_segment_seconds, 3), + "slice": sample.slice_name, + "weight": sample.weight, + "expected_silence": sample.expected_silence, + "silence_hallucination": sample.expected_silence and bool(_normalize_text(transcript)), "reference": sample.reference, "transcript": transcript, "char_error_rate": None if cer is None else round(cer, 4), @@ -153,17 +181,153 @@ def _transcribe_sample( } +def _transcribe_sensevoice_sample( + provider: LocalSenseVoiceProvider, + sample: BenchmarkSample, +) -> dict[str, Any]: + duration = _duration_seconds(sample.audio_path) + started = time.perf_counter() + result = provider.transcribe( + str(sample.audio_path), + capture_id=f"benchmark-{sample.sample_id}", + output_folder=sample.audio_path.parent, + ) + elapsed = time.perf_counter() - started + transcript = result.transcript_text.strip() + cer = _char_error_rate(sample.reference, transcript) + return { + "id": sample.sample_id, + "audio": str(sample.audio_path), + "duration_seconds": round(duration, 3), + "transcribe_seconds": round(elapsed, 3), + "rtf": round(elapsed / duration, 4) if duration else None, + "language": result.language, + "segment_count": result.segment_count, + "first_segment_seconds": round(elapsed, 3), + "slice": sample.slice_name, + "weight": sample.weight, + "expected_silence": sample.expected_silence, + "silence_hallucination": sample.expected_silence and bool(_normalize_text(transcript)), + "reference": sample.reference, + "transcript": transcript, + "char_error_rate": None if cer is None else round(cer, 4), + **_term_stats(transcript, sample.terms), + } + + def _summarize(results: list[dict[str, Any]]) -> dict[str, Any]: rtfs = [item["rtf"] for item in results if isinstance(item.get("rtf"), float)] cers = [item["char_error_rate"] for item in results if isinstance(item.get("char_error_rate"), float)] + weighted_cers = [ + (item["char_error_rate"], float(item.get("weight", 1.0))) + for item in results + if isinstance(item.get("char_error_rate"), float) and float(item.get("weight", 1.0)) > 0 + ] + term_count = sum(int(item.get("term_count", 0)) for item in results) + term_hits = sum(len(item.get("term_hits", [])) for item in results) + first_segments = [item["first_segment_seconds"] for item in results if isinstance(item.get("first_segment_seconds"), float)] + slice_cers: dict[str, list[float]] = {} + for item in results: + if isinstance(item.get("char_error_rate"), float): + slice_cers.setdefault(str(item.get("slice") or "general"), []).append(item["char_error_rate"]) return { "sample_count": len(results), + "audio_hours": round(sum(float(item.get("duration_seconds") or 0) for item in results) / 3600, 3), "avg_rtf": round(statistics.mean(rtfs), 4) if rtfs else None, + "p95_rtf": None if not rtfs else round(_percentile(rtfs, 0.95) or 0, 4), "avg_char_error_rate": round(statistics.mean(cers), 4) if cers else None, + "weighted_char_error_rate": ( + round(sum(cer * weight for cer, weight in weighted_cers) / sum(weight for _, weight in weighted_cers), 4) + if weighted_cers + else None + ), + "term_recall": round(term_hits / term_count, 4) if term_count else None, + "p95_first_segment_seconds": ( + None if not first_segments else round(_percentile(first_segments, 0.95) or 0, 3) + ), + "silence_hallucination_count": sum(bool(item.get("silence_hallucination")) for item in results), + "slice_char_error_rate": { + name: round(statistics.mean(values), 4) for name, values in sorted(slice_cers.items()) + }, "has_reference": bool(cers), } +def _hardware_profile() -> dict[str, Any]: + gpu: list[str] = [] + try: + output = subprocess.run( + ["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"], + capture_output=True, + text=True, + timeout=5, + check=False, + ) + if output.returncode == 0: + gpu = [line.strip() for line in output.stdout.splitlines() if line.strip()] + except (OSError, subprocess.SubprocessError): + pass + return { + "os": platform.platform(), + "machine": platform.machine(), + "processor": platform.processor(), + "logical_cpu_count": os.cpu_count(), + "gpu": gpu, + } + + +def _release_dataset_ready(summary: dict[str, Any]) -> bool: + required_slices = {"mandarin", "dialect", "mixed_language", "multi_speaker", "noise", "music", "long_form"} + return ( + int(summary.get("sample_count") or 0) >= 60 + and float(summary.get("audio_hours") or 0) >= 3.0 + and required_slices.issubset(set((summary.get("slice_char_error_rate") or {}).keys())) + ) + + +def _candidate_decisions(runs: list[dict[str, Any]]) -> list[dict[str, Any]]: + if not runs: + return [] + baseline = runs[0]["summary"] + baseline_cer = baseline.get("weighted_char_error_rate") + baseline_rtf = baseline.get("p95_rtf") + decisions = [] + for run in runs[1:]: + summary = run["summary"] + candidate_cer = summary.get("weighted_char_error_rate") + candidate_rtf = summary.get("p95_rtf") + quality_gain = ( + (baseline_cer - candidate_cer) / baseline_cer + if isinstance(baseline_cer, float) and baseline_cer > 0 and isinstance(candidate_cer, float) + else None + ) + speed_gain = ( + (baseline_rtf - candidate_rtf) / baseline_rtf + if isinstance(baseline_rtf, float) and baseline_rtf > 0 and isinstance(candidate_rtf, float) + else None + ) + critical_regression = 0.0 + for name, base_value in (baseline.get("slice_char_error_rate") or {}).items(): + candidate_value = (summary.get("slice_char_error_rate") or {}).get(name) + if base_value and isinstance(candidate_value, float): + critical_regression = max(critical_regression, (candidate_value - base_value) / base_value) + quality_tied = quality_gain is not None and quality_gain >= -0.02 + eligible = critical_regression <= 0.05 and ( + (quality_gain is not None and quality_gain >= 0.10) + or (quality_tied and speed_gain is not None and speed_gain >= 0.30) + ) + decisions.append( + { + "model": run["model"], + "eligible_to_replace_default": eligible, + "quality_gain": None if quality_gain is None else round(quality_gain, 4), + "speed_gain": None if speed_gain is None else round(speed_gain, 4), + "max_slice_regression": round(critical_regression, 4), + } + ) + return decisions + + def run_benchmark(args: argparse.Namespace) -> dict[str, Any]: requested_language = str(args.language or "").strip().lower() language = None if requested_language in {"", "auto", "detect", "none"} else requested_language @@ -201,7 +365,30 @@ def run_benchmark(args: argparse.Namespace) -> dict[str, Any]: "samples": sample_results, } ) - return {"runs": runs} + for label, model_path in [_parse_model(item) for item in args.sensevoice_model]: + started = time.perf_counter() + provider = LocalSenseVoiceProvider(model_path, num_threads=args.sensevoice_threads) + provider._recognizer() + load_seconds = time.perf_counter() - started + sample_results = [_transcribe_sensevoice_sample(provider, sample) for sample in samples] + runs.append( + { + "model": label, + "model_path": model_path, + "engine": "sherpa_onnx_sensevoice", + "device": "cpu", + "compute_type": "int8", + "beam_size": 1, + "vad_filter": False, + "language": "auto", + "load_seconds": round(load_seconds, 3), + "summary": _summarize(sample_results), + "samples": sample_results, + } + ) + for run in runs: + run["summary"]["release_dataset_ready"] = _release_dataset_ready(run["summary"]) + return {"hardware": _hardware_profile(), "runs": runs, "candidate_decisions": _candidate_decisions(runs)} def main() -> int: @@ -209,6 +396,8 @@ def main() -> int: parser.add_argument("files", nargs="*", help="Audio/video files or glob patterns. Ignored when --manifest is set.") parser.add_argument("--manifest", help="JSONL manifest with audio, optional reference, and optional terms.") parser.add_argument("--model", action="append", default=None, help="Model spec as label=path_or_hf_id. Repeatable.") + parser.add_argument("--sensevoice-model", action="append", default=[], help="SenseVoice model spec as label=directory. Repeatable.") + parser.add_argument("--sensevoice-threads", type=int, default=max(1, min(4, os.cpu_count() or 1))) parser.add_argument("--device", default=DEVICE) parser.add_argument("--compute-type", default=COMPUTE_TYPE) parser.add_argument("--beam-size", type=int, default=BEAM_SIZE) @@ -217,8 +406,10 @@ def main() -> int: parser.add_argument("--limit", type=int) parser.add_argument("--json-out", help="Write the full benchmark result to this JSON file.") args = parser.parse_args() - if args.model is None: + if args.model is None and not args.sensevoice_model: args.model = [f"current={MODEL_PATH}"] + elif args.model is None: + args.model = [] if not args.manifest and not args.files: parser.error("provide files or --manifest") diff --git a/scripts/build_desktop_sidecar.ps1 b/scripts/build_desktop_sidecar.ps1 new file mode 100644 index 0000000..9580633 --- /dev/null +++ b/scripts/build_desktop_sidecar.ps1 @@ -0,0 +1,103 @@ +param( + [string]$OutputRoot = "web\src-tauri\sidecar", + [switch]$Clean +) + +$ErrorActionPreference = "Stop" +$projectRoot = Split-Path -Parent $PSScriptRoot +$outputRootPath = Join-Path $projectRoot $OutputRoot +$distPath = Join-Path $outputRootPath "wanxiang-api" +$workPath = Join-Path $projectRoot "tests_runtime\pyinstaller" +$specPath = Join-Path $projectRoot "tests_runtime\pyinstaller-spec" + +if ($Clean) { + Remove-Item -LiteralPath $distPath -Recurse -Force -ErrorAction SilentlyContinue +} + +New-Item -ItemType Directory -Force -Path $outputRootPath, $workPath, $specPath | Out-Null + +Push-Location $projectRoot +try { + uvx --from pyinstaller --with-requirements requirements.txt pyinstaller ` + --noconfirm ` + --clean ` + --onedir ` + --name wanxiang-api ` + --distpath $outputRootPath ` + --workpath $workPath ` + --specpath $specPath ` + --collect-submodules app ` + --collect-submodules scripts ` + --collect-all faster_whisper ` + --collect-all av ` + --collect-all sherpa_onnx ` + --collect-all playwright ` + --collect-all yt_dlp ` + scripts\desktop_sidecar.py + if ($LASTEXITCODE -ne 0) { throw "PyInstaller sidecar build failed." } +} finally { + Pop-Location +} + +$executable = Join-Path $distPath "wanxiang-api.exe" +if (-not (Test-Path -LiteralPath $executable)) { + throw "Sidecar executable was not produced: $executable" +} + +Write-Host "Running packaged sidecar smoke test..." +$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) +$listener.Start() +$smokePort = ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port +$listener.Stop() +$smokeToken = [Guid]::NewGuid().ToString("N") +$smokeWorkspace = Join-Path $projectRoot "tests_runtime\desktop-smoke-workspace" +$stdoutPath = Join-Path $projectRoot "tests_runtime\desktop-sidecar.stdout.log" +$stderrPath = Join-Path $projectRoot "tests_runtime\desktop-sidecar.stderr.log" +$savedEnvironment = @{ + AUDIO2TEXT_API_HOST = $env:AUDIO2TEXT_API_HOST + AUDIO2TEXT_API_PORT = $env:AUDIO2TEXT_API_PORT + AUDIO2TEXT_RUNTIME_TARGET = $env:AUDIO2TEXT_RUNTIME_TARGET + AUDIO2TEXT_DESKTOP_TOKEN = $env:AUDIO2TEXT_DESKTOP_TOKEN + AUDIO2TEXT_WORKSPACE_DIR = $env:AUDIO2TEXT_WORKSPACE_DIR + AUDIO2TEXT_ALLOW_DEGRADED_START = $env:AUDIO2TEXT_ALLOW_DEGRADED_START +} +$env:AUDIO2TEXT_API_HOST = "127.0.0.1" +$env:AUDIO2TEXT_API_PORT = "$smokePort" +$env:AUDIO2TEXT_RUNTIME_TARGET = "windows_desktop" +$env:AUDIO2TEXT_DESKTOP_TOKEN = $smokeToken +$env:AUDIO2TEXT_WORKSPACE_DIR = $smokeWorkspace +$env:AUDIO2TEXT_ALLOW_DEGRADED_START = "1" +$process = $null +try { + $process = Start-Process -FilePath $executable -WorkingDirectory $distPath -WindowStyle Hidden -PassThru ` + -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath + $ready = $false + for ($attempt = 0; $attempt -lt 40; $attempt++) { + Start-Sleep -Milliseconds 250 + if ($process.HasExited) { break } + try { + $response = Invoke-RestMethod -Uri "http://127.0.0.1:$smokePort/health" ` + -Headers @{ "X-Wanxiang-Desktop-Token" = $smokeToken } -TimeoutSec 2 + if ($response.status -eq "ok" -and $response.runtime_target -eq "windows_desktop") { + $ready = $true + break + } + } catch { + # The sidecar can need a few seconds for its first import. + } + } + if (-not $ready) { + $stderr = Get-Content -LiteralPath $stderrPath -Raw -ErrorAction SilentlyContinue + throw "Packaged sidecar did not become healthy. $stderr" + } +} finally { + if ($process -and -not $process.HasExited) { + Stop-Process -Id $process.Id -Force + $process.WaitForExit() + } + foreach ($name in $savedEnvironment.Keys) { + [Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name], "Process") + } +} + +Write-Host "Sidecar ready: $executable" diff --git a/scripts/cf_worker_proxy.js b/scripts/cf_worker_proxy.js index d6e6624..a2e1af8 100644 --- a/scripts/cf_worker_proxy.js +++ b/scripts/cf_worker_proxy.js @@ -1,151 +1,208 @@ -// Cloudflare Worker — reverse-proxy + CN forward-proxy + free Whisper transcription. -// -// Reverse proxy: all regular requests → HF Space (China → HF Space via workers.dev) -// Forward proxy: /__proxy__?url=ENCODED_URL → fetch CN platform (HF Space → CN via Worker) -// Transcription: /__transcribe__ (POST audio file) → Workers AI Whisper (free tier) -// -// Deploy: cd scripts && npx wrangler deploy +// Wanxiang Chengwen edge gateway: product reverse proxy, allowlisted platform +// relay, and authenticated Workers AI transcription. + +import { + isAllowedContentType, + isAllowedOrigin, + isFreshTimestamp, + readBodyWithLimit, + sha256Hex, + validateRelayUrl, + verifyServiceSignature, +} from "./cf_worker_security.mjs"; + +const TARGET_HOST = "liamgrant-wanxiang-chengwen-preview.hf.space"; +const MAX_RELAY_BYTES = 12 * 1024 * 1024; +const MAX_TRANSCRIBE_BYTES = 25 * 1024 * 1024; +const MAX_TRANSCRIBE_SECONDS = 30 * 60; +const MAX_REDIRECTS = 3; +const UPSTREAM_TIMEOUT_MS = 15_000; + +function jsonResponse(payload, status, headers = {}) { + return new Response(JSON.stringify(payload), { + status, + headers: { "Content-Type": "application/json; charset=utf-8", ...headers }, + }); +} -const TARGET_HOST = 'liamgrant-wanxiang-chengwen-preview.hf.space' +function corsHeaders(request) { + const origin = request.headers.get("Origin") || ""; + if (!isAllowedOrigin(origin)) return {}; + return { + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + Vary: "Origin", + }; +} async function handleReverseProxy(request) { - const url = new URL(request.url) - url.hostname = TARGET_HOST - - const headers = new Headers(request.headers) - headers.set('Host', TARGET_HOST) - const cfConnectingIp = request.headers.get('CF-Connecting-IP') + const url = new URL(request.url); + url.protocol = "https:"; + url.hostname = TARGET_HOST; + url.port = ""; + + const headers = new Headers(request.headers); + headers.set("Host", TARGET_HOST); + headers.delete("Authorization"); + headers.delete("Cookie"); + const cfConnectingIp = request.headers.get("CF-Connecting-IP"); if (cfConnectingIp) { - headers.set('X-Forwarded-For', cfConnectingIp) - headers.set('X-Real-IP', cfConnectingIp) + headers.set("X-Forwarded-For", cfConnectingIp); + headers.set("X-Real-IP", cfConnectingIp); } const upstream = await fetch(url.toString(), { method: request.method, headers, - body: request.body, - redirect: 'follow', - }) - - const responseHeaders = new Headers(upstream.headers) - responseHeaders.set('Access-Control-Allow-Origin', '*') - responseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') - responseHeaders.set('Access-Control-Allow-Headers', '*') - + body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body, + redirect: "manual", + }); + const responseHeaders = new Headers(upstream.headers); + for (const [name, value] of Object.entries(corsHeaders(request))) responseHeaders.set(name, value); + responseHeaders.set("X-Content-Type-Options", "nosniff"); + responseHeaders.set("Referrer-Policy", "strict-origin-when-cross-origin"); return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: responseHeaders, - }) + }); } -async function handleForwardProxy(request) { - const proxyUrl = new URL(request.url).searchParams.get('url') - if (!proxyUrl) { - return new Response('missing url parameter', { status: 400 }) +async function fetchRelayTarget(initialUrl) { + let target = validateRelayUrl(initialUrl); + for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount += 1) { + const upstream = await fetch(target.toString(), { + method: "GET", + headers: { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/137.0 Safari/537.36", + Accept: "text/html,application/json,image/*,audio/*,video/*,*/*;q=0.5", + }, + redirect: "manual", + signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), + }); + if (![301, 302, 303, 307, 308].includes(upstream.status)) return { upstream, target }; + const location = upstream.headers.get("Location"); + await upstream.body?.cancel(); + if (!location || redirectCount === MAX_REDIRECTS) throw new Error("Relay redirect limit exceeded"); + target = validateRelayUrl(new URL(location, target).toString()); } + throw new Error("Relay redirect limit exceeded"); +} + +async function handleForwardProxy(request) { + if (request.method !== "GET") return jsonResponse({ error: "method_not_allowed" }, 405); + const proxyUrl = new URL(request.url).searchParams.get("url"); + if (!proxyUrl) return jsonResponse({ error: "missing_url" }, 400); try { - const targetUrl = new URL(proxyUrl) - const headers = new Headers() - headers.set('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0 Safari/537.36') - headers.set('Accept', 'text/html,application/json,*/*') - - const upstream = await fetch(targetUrl.toString(), { - method: 'GET', - headers, - redirect: 'follow', - }) - - const body = await upstream.text() - const responseHeaders = new Headers() - const upstreamContentType = upstream.headers.get('Content-Type') || 'text/html; charset=utf-8' - responseHeaders.set('Content-Type', upstreamContentType) - responseHeaders.set('Access-Control-Allow-Origin', '*') - responseHeaders.set('X-Proxied-Url', upstream.url) - responseHeaders.set('X-Proxied-Status', String(upstream.status)) - - return new Response(body, { - status: 200, - headers: responseHeaders, - }) - } catch (err) { - return new Response(`proxy error: ${err.message}`, { status: 502 }) + const { upstream, target } = await fetchRelayTarget(proxyUrl); + const contentType = upstream.headers.get("Content-Type") || "application/octet-stream"; + if (!isAllowedContentType(contentType)) { + await upstream.body?.cancel(); + return jsonResponse({ error: "unsupported_upstream_content" }, 415, corsHeaders(request)); + } + const declaredLength = Number(upstream.headers.get("Content-Length") || 0); + if (declaredLength > MAX_RELAY_BYTES) { + await upstream.body?.cancel(); + return jsonResponse({ error: "upstream_response_too_large" }, 413, corsHeaders(request)); + } + const body = await readBodyWithLimit(upstream.body, MAX_RELAY_BYTES); + const responseHeaders = new Headers(corsHeaders(request)); + responseHeaders.set("Content-Type", contentType); + responseHeaders.set("X-Proxied-Url", target.toString()); + responseHeaders.set("X-Content-Type-Options", "nosniff"); + return new Response(body, { status: upstream.status, statusText: upstream.statusText, headers: responseHeaders }); + } catch (error) { + console.warn(JSON.stringify({ event: "relay_rejected", reason: String(error?.message || "relay_failed") })); + return jsonResponse({ error: "relay_request_rejected" }, 502, corsHeaders(request)); } } async function handleTranscribe(request, env) { - if (request.method !== 'POST') { - return new Response('Method not allowed', { - status: 405, - headers: { 'Access-Control-Allow-Origin': '*' }, - }) + if (request.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405); + if (!env.TRANSCRIBE_SHARED_SECRET) return jsonResponse({ error: "service_unavailable" }, 503); + + const declaredLength = Number(request.headers.get("Content-Length") || 0); + if (!Number.isFinite(declaredLength) || declaredLength <= 0 || declaredLength > MAX_TRANSCRIBE_BYTES) { + return jsonResponse({ error: "invalid_request_size" }, 413); + } + const contentType = (request.headers.get("Content-Type") || "").split(";", 1)[0].toLowerCase(); + if (contentType !== "application/octet-stream" && !contentType.startsWith("audio/")) { + return jsonResponse({ error: "unsupported_audio_type" }, 415); } + const timestamp = request.headers.get("X-Praxis-Timestamp") || ""; + const nonce = request.headers.get("X-Praxis-Nonce") || ""; + const duration = request.headers.get("X-Praxis-Audio-Duration") || ""; + const claimedHash = request.headers.get("X-Praxis-Content-SHA256") || ""; + const service = request.headers.get("X-Praxis-Service") || ""; + const signature = request.headers.get("X-Praxis-Signature") || ""; + const durationSeconds = Number(duration); + if ( + service !== "wanxiang-backend" || + !isFreshTimestamp(timestamp) || + !Number.isFinite(durationSeconds) || + durationSeconds <= 0 || + durationSeconds > MAX_TRANSCRIBE_SECONDS + ) { + return jsonResponse({ error: "unauthorized" }, 401); + } + + let audio; try { - const contentType = request.headers.get('Content-Type') || '' - let audioArray - - if (contentType.includes('multipart/form-data')) { - const formData = await request.formData() - const file = formData.get('file') - if (!file) { - return new Response('Missing file field', { - status: 400, - headers: { 'Access-Control-Allow-Origin': '*' }, - }) - } - const buffer = await file.arrayBuffer() - audioArray = [...new Uint8Array(buffer)] - } else { - const buffer = await request.arrayBuffer() - audioArray = [...new Uint8Array(buffer)] + audio = await readBodyWithLimit(request.body, MAX_TRANSCRIBE_BYTES); + } catch { + return jsonResponse({ error: "invalid_request_size" }, 413); + } + const bodyHash = await sha256Hex(audio); + if (bodyHash !== claimedHash) return jsonResponse({ error: "unauthorized" }, 401); + const authenticated = await verifyServiceSignature({ + secret: env.TRANSCRIBE_SHARED_SECRET, + timestamp, + nonce, + duration, + bodyHash, + service, + signature, + }); + if (!authenticated) return jsonResponse({ error: "unauthorized" }, 401); + + if (env.TRANSCRIBE_RATE_LIMITER) { + const { success } = await env.TRANSCRIBE_RATE_LIMITER.limit({ key: service }); + if (!success) { + console.warn(JSON.stringify({ event: "transcribe_rate_limited", service })); + return jsonResponse({ error: "rate_limit_exceeded" }, 429); } + } - const result = await env.AI.run('@cf/openai/whisper', { audio: audioArray }) - - return new Response(JSON.stringify({ - text: result.text || '', - }), { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - }) - } catch (err) { - return new Response(JSON.stringify({ error: err.message }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - }) + const requestId = crypto.randomUUID(); + console.log(JSON.stringify({ event: "transcribe_started", request_id: requestId, bytes: audio.byteLength, duration_seconds: durationSeconds })); + try { + const result = await env.AI.run("@cf/openai/whisper-large-v3-turbo", { + audio: Array.from(audio), + task: "transcribe", + vad_filter: true, + condition_on_previous_text: false, + }); + console.log(JSON.stringify({ event: "transcribe_completed", request_id: requestId })); + return jsonResponse({ text: result.text || "", vtt: result.vtt || "", segments: result.segments || [] }, 200); + } catch { + console.error(JSON.stringify({ event: "transcribe_failed", request_id: requestId })); + return jsonResponse({ error: "transcription_failed" }, 502); } } export default { async fetch(request, env) { - const url = new URL(request.url) - - if (request.method === 'OPTIONS') { - return new Response(null, { - status: 204, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': '*', - }, - }) - } - - if (url.pathname === '/__proxy__') { - return handleForwardProxy(request) + const url = new URL(request.url); + if (request.method === "OPTIONS") { + const headers = corsHeaders(request); + if (!headers["Access-Control-Allow-Origin"]) return new Response(null, { status: 403 }); + return new Response(null, { status: 204, headers }); } - - if (url.pathname === '/__transcribe__') { - return handleTranscribe(request, env) - } - - return handleReverseProxy(request) + if (url.pathname === "/__proxy__") return handleForwardProxy(request); + if (url.pathname === "/__transcribe__") return handleTranscribe(request, env); + return handleReverseProxy(request); }, -} +}; diff --git a/scripts/cf_worker_security.mjs b/scripts/cf_worker_security.mjs new file mode 100644 index 0000000..8f067b6 --- /dev/null +++ b/scripts/cf_worker_security.mjs @@ -0,0 +1,173 @@ +const ALLOWED_ORIGINS = new Set([ + "https://wanxiang.praxisai.online", + "https://liamgrant-wanxiang-chengwen-preview.hf.space", +]); + +const ALLOWED_HOST_SUFFIXES = [ + "bilibili.com", + "b23.tv", + "bilivideo.com", + "hdslb.com", + "xiaohongshu.com", + "xhslink.com", + "xhscdn.com", + "douyin.com", + "iesdouyin.com", + "douyinvod.com", + "byteimg.com", + "pstatp.com", + "amemv.com", + "xiaoyuzhoufm.com", + "xyzcdn.net", + "weixin.qq.com", + "mmbiz.qpic.cn", + "mmbiz.qlogo.cn", + "youtube.com", + "youtube-nocookie.com", + "youtu.be", + "googlevideo.com", + "ytimg.com", +]; + +const ALLOWED_CONTENT_TYPES = [ + "text/", + "image/", + "audio/", + "video/", + "application/json", + "application/javascript", + "application/xml", + "application/xhtml+xml", + "application/dash+xml", + "application/vnd.apple.mpegurl", + "application/octet-stream", +]; + +function isIpLiteral(hostname) { + const host = hostname.replace(/^\[|\]$/g, "").toLowerCase(); + if (host.includes(":")) return true; + if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return false; + return host.split(".").every((part) => Number(part) >= 0 && Number(part) <= 255); +} + +function hostMatches(hostname, suffix) { + return hostname === suffix || hostname.endsWith(`.${suffix}`); +} + +export function isAllowedOrigin(origin) { + return ALLOWED_ORIGINS.has((origin || "").trim().replace(/\/$/, "")); +} + +export function validateRelayUrl(rawUrl) { + let target; + try { + target = new URL(rawUrl); + } catch { + throw new Error("Invalid relay URL"); + } + if (target.protocol !== "https:") throw new Error("HTTPS is required"); + if (target.username || target.password) throw new Error("URL credentials are forbidden"); + + const hostname = target.hostname.toLowerCase().replace(/\.$/, ""); + if ( + !hostname || + hostname === "localhost" || + hostname.endsWith(".localhost") || + hostname.endsWith(".local") || + hostname === "metadata.google.internal" || + isIpLiteral(hostname) + ) { + throw new Error("Local and reserved targets are forbidden"); + } + if (!ALLOWED_HOST_SUFFIXES.some((suffix) => hostMatches(hostname, suffix))) { + throw new Error("Target host is not in the platform allowlist"); + } + target.hostname = hostname; + target.hash = ""; + return target; +} + +export function isAllowedContentType(contentType) { + const normalized = (contentType || "").split(";", 1)[0].trim().toLowerCase(); + return ALLOWED_CONTENT_TYPES.some((allowed) => + allowed.endsWith("/") ? normalized.startsWith(allowed) : normalized === allowed, + ); +} + +export function buildSignaturePayload({ timestamp, nonce, duration, bodyHash, service }) { + return [timestamp, nonce, duration, bodyHash, service].join("\n"); +} + +function hexToBytes(value) { + if (!/^[0-9a-f]{64}$/i.test(value || "")) return null; + const bytes = new Uint8Array(32); + for (let index = 0; index < 32; index += 1) { + bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16); + } + return bytes; +} + +export async function verifyServiceSignature({ + secret, + timestamp, + nonce, + duration, + bodyHash, + service, + signature, +}) { + if (!secret || !/^[0-9]{10}$/.test(timestamp || "") || !/^[0-9a-f]{32}$/i.test(nonce || "")) { + return false; + } + const signatureBytes = hexToBytes(signature); + if (!signatureBytes || !/^[0-9a-f]{64}$/i.test(bodyHash || "")) return false; + const key = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["verify"], + ); + const payload = buildSignaturePayload({ timestamp, nonce, duration, bodyHash, service }); + return crypto.subtle.verify( + "HMAC", + key, + signatureBytes, + new TextEncoder().encode(payload), + ); +} + +export function isFreshTimestamp(timestamp, nowSeconds = Math.floor(Date.now() / 1000), toleranceSeconds = 300) { + const value = Number(timestamp); + return Number.isInteger(value) && Math.abs(nowSeconds - value) <= toleranceSeconds; +} + +export async function sha256Hex(bytes) { + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); + return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join(""); +} + +export async function readBodyWithLimit(body, maximumBytes) { + if (!body) return new Uint8Array(); + const reader = body.getReader(); + const chunks = []; + let total = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > maximumBytes) throw new Error("Response body exceeds limit"); + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + const combined = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + combined.set(chunk, offset); + offset += chunk.byteLength; + } + return combined; +} diff --git a/scripts/cf_worker_security.test.mjs b/scripts/cf_worker_security.test.mjs new file mode 100644 index 0000000..bb0f6c2 --- /dev/null +++ b/scripts/cf_worker_security.test.mjs @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + buildSignaturePayload, + isAllowedContentType, + isAllowedOrigin, + validateRelayUrl, + verifyServiceSignature, +} from "./cf_worker_security.mjs"; + +test("relay accepts only HTTPS platform hosts", () => { + assert.equal(validateRelayUrl("https://www.bilibili.com/video/BV1xx").hostname, "www.bilibili.com"); + assert.equal(validateRelayUrl("https://api.xiaohongshu.com/api/sns/web/v1/feed").hostname, "api.xiaohongshu.com"); + assert.throws(() => validateRelayUrl("http://www.bilibili.com/video/BV1xx"), /HTTPS/); + assert.throws(() => validateRelayUrl("https://example.com/steal"), /allowlist/); +}); + +test("relay rejects local, metadata, credentialed and deceptive hosts", () => { + for (const candidate of [ + "https://127.0.0.1/", + "https://[::1]/", + "https://169.254.169.254/latest/meta-data/", + "https://localhost/", + "https://bilibili.com.example.org/", + "https://user:pass@www.bilibili.com/", + ]) { + assert.throws(() => validateRelayUrl(candidate)); + } +}); + +test("CORS and response content types use explicit allowlists", () => { + assert.equal(isAllowedOrigin("https://wanxiang.praxisai.online"), true); + assert.equal(isAllowedOrigin("https://liamgrant-wanxiang-chengwen-preview.hf.space"), true); + assert.equal(isAllowedOrigin("https://evil.example"), false); + assert.equal(isAllowedContentType("application/json; charset=utf-8"), true); + assert.equal(isAllowedContentType("text/html"), true); + assert.equal(isAllowedContentType("application/x-msdownload"), false); +}); + +test("service HMAC binds timestamp, nonce, duration and body digest", async () => { + const secret = "test-only-secret"; + const timestamp = String(Math.floor(Date.now() / 1000)); + const nonce = "0123456789abcdef0123456789abcdef"; + const duration = "42.5"; + const bodyHash = "a".repeat(64); + const service = "wanxiang-backend"; + const payload = buildSignaturePayload({ timestamp, nonce, duration, bodyHash, service }); + const key = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const signature = Buffer.from(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload))).toString("hex"); + + assert.equal( + await verifyServiceSignature({ secret, timestamp, nonce, duration, bodyHash, service, signature }), + true, + ); + assert.equal( + await verifyServiceSignature({ secret, timestamp, nonce, duration: "43", bodyHash, service, signature }), + false, + ); +}); diff --git a/scripts/config.py b/scripts/config.py index a3ef6b5..ecccec2 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -7,7 +7,11 @@ # ========================= # 项目基础路径 # ========================= -PROJECT_ROOT = Path(__file__).resolve().parents[1] +PROJECT_ROOT = ( + Path(sys.executable).resolve().parent + if getattr(sys, "frozen", False) + else Path(__file__).resolve().parents[1] +) SETTINGS_FILE = PROJECT_ROOT / "audio2text.settings.json" EXAMPLE_SETTINGS_FILE = PROJECT_ROOT / "audio2text.settings.example.json" @@ -94,6 +98,11 @@ def _csv_setting(key: str, env_name: str, default: list[str]) -> list[str]: RUNTIME_DIR = WORKSPACE_DIR / "runtime" MODELS_DIR = RUNTIME_DIR / "models" PLAYWRIGHT_BROWSERS_DIR = RUNTIME_DIR / "playwright-browsers" +SENSEVOICE_MODEL_DIR = _path_setting( + "sensevoice_model_path", + "AUDIO2TEXT_SENSEVOICE_MODEL_PATH", + MODELS_DIR / "sensevoice" / "small-int8", +) LEGACY_BUNDLED_MODEL_PATH = PROJECT_ROOT / "Assets" / "Models" / "FasterWhisper" / "medium" DEFAULT_MODEL_PATH = MODELS_DIR / "faster-whisper" / "medium" @@ -127,6 +136,13 @@ def _csv_setting(key: str, env_name: str, default: list[str]) -> list[str]: WORKER_RELAY_BASE = "https://wanxiang.praxisai.online/__proxy__" WORKER_TRANSCRIBE_URL = "https://wanxiang.praxisai.online/__transcribe__" +WORKER_TRANSCRIBE_SHARED_SECRET = str( + _setting( + "worker_transcribe_shared_secret", + "AUDIO2TEXT_WORKER_TRANSCRIBE_SHARED_SECRET", + "", + ) +).strip() _cn_proxy_raw = str( _setting( @@ -231,6 +247,9 @@ def _csv_setting(key: str, env_name: str, default: list[str]) -> list[str]: "http://localhost:8000", "http://127.0.0.1:5173", "http://localhost:5173", + "http://tauri.localhost", + "https://tauri.localhost", + "tauri://localhost", ], ) diff --git a/scripts/desktop_sidecar.py b/scripts/desktop_sidecar.py new file mode 100644 index 0000000..21ee99f --- /dev/null +++ b/scripts/desktop_sidecar.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import ctypes +import os +import threading +import time + +from scripts.start_api import main + + +def _windows_process_is_alive(pid: int) -> bool: + synchronize = 0x00100000 + wait_timeout = 0x00000102 + handle = ctypes.windll.kernel32.OpenProcess(synchronize, False, pid) + if not handle: + return False + try: + return ctypes.windll.kernel32.WaitForSingleObject(handle, 0) == wait_timeout + finally: + ctypes.windll.kernel32.CloseHandle(handle) + + +def _watch_desktop_parent() -> None: + raw_pid = os.getenv("AUDIO2TEXT_DESKTOP_PARENT_PID", "").strip() + if not raw_pid or os.name != "nt": + return + try: + parent_pid = int(raw_pid) + except ValueError: + return + + def watch() -> None: + while _windows_process_is_alive(parent_pid): + time.sleep(1) + os._exit(0) + + threading.Thread(target=watch, name="desktop-parent-watch", daemon=True).start() + + +if __name__ == "__main__": + _watch_desktop_parent() + raise SystemExit(main()) diff --git a/scripts/start_api.py b/scripts/start_api.py index da00f1a..58616fb 100644 --- a/scripts/start_api.py +++ b/scripts/start_api.py @@ -156,8 +156,13 @@ def main() -> int: return 1 ensure_runtime_ready() + application = "app.main:app" + if getattr(sys, "frozen", False): + from app.main import app as packaged_application + + application = packaged_application uvicorn.run( - "app.main:app", + application, host=API_HOST, port=API_PORT, reload=False, diff --git a/scripts/start_api_bootstrap.ps1 b/scripts/start_api_bootstrap.ps1 new file mode 100644 index 0000000..b6c7ebb --- /dev/null +++ b/scripts/start_api_bootstrap.ps1 @@ -0,0 +1,223 @@ +$ErrorActionPreference = "Stop" + +$ProjectRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$VenvPython = Join-Path $ProjectRoot ".venv\Scripts\python.exe" +$VenvActivate = Join-Path $ProjectRoot ".venv\Scripts\Activate.ps1" +$Requirements = Join-Path $ProjectRoot "requirements.txt" +$FrontendIndex = Join-Path $ProjectRoot "frontend\dist\index.html" +$PlaywrightBrowsers = Join-Path $ProjectRoot "workspace\runtime\playwright-browsers" +$DefaultCudaRuntime = "D:\Apps\NVIDIA\CUDA\v12-runtime" + +Set-Location $ProjectRoot + +function Write-Info([string] $Message) { + Write-Host "[INFO] $Message" +} + +function Write-Warn([string] $Message) { + Write-Host "[WARN] $Message" -ForegroundColor Yellow +} + +function Write-Fail([string] $Message) { + Write-Host "[ERROR] $Message" -ForegroundColor Red +} + +function Invoke-Checked([string] $Label, [scriptblock] $Command) { + Write-Info $Label + & $Command + if ($LASTEXITCODE -ne 0) { + throw "$Label failed with exit code $LASTEXITCODE." + } +} + +function Test-CommandExists([string] $Name) { + return $null -ne (Get-Command $Name -ErrorAction SilentlyContinue) +} + +function Test-VenvPython { + if (-not (Test-Path $VenvPython)) { + return $false + } + & $VenvPython -c "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 11) else 1)" *> $null + return $LASTEXITCODE -eq 0 +} + +function Test-RequiredImports { + if (-not (Test-VenvPython)) { + return $false + } + $code = "import fastapi, uvicorn, faster_whisper, ctranslate2, av, httpx, requests, yt_dlp, playwright, opencc" + & $VenvPython -c $code *> $null + return $LASTEXITCODE -eq 0 +} + +function Find-SystemPython311 { + if (Test-CommandExists "py") { + & py -3.11 -c "import sys; print(sys.executable)" 2>$null + if ($LASTEXITCODE -eq 0) { + return (& py -3.11 -c "import sys; print(sys.executable)" 2>$null | Select-Object -First 1) + } + } + + foreach ($candidate in @("python", "python3")) { + if (-not (Test-CommandExists $candidate)) { + continue + } + $path = & $candidate -c "import sys; print(sys.executable if sys.version_info[:2] == (3, 11) else '')" 2>$null + if ($LASTEXITCODE -eq 0 -and $path) { + return ($path | Select-Object -First 1) + } + } + + return "" +} + +function Ensure-Venv { + if (Test-RequiredImports) { + Write-Info "Python environment ready: $VenvPython" + return + } + + Write-Warn "Python environment is missing, broken, or incomplete. Rebuilding .venv." + if (Test-CommandExists "uv") { + Invoke-Checked "Create Python 3.11 virtual environment with uv" { + & uv venv --clear --seed --python 3.11 .venv + } + Invoke-Checked "Install pinned Python dependencies with uv" { + & uv pip install --python $VenvPython -r $Requirements --link-mode copy --compile-bytecode + } + return + } + + $python311 = Find-SystemPython311 + if ($python311) { + if (Test-Path (Join-Path $ProjectRoot ".venv")) { + Remove-Item -LiteralPath (Join-Path $ProjectRoot ".venv") -Recurse -Force + } + Invoke-Checked "Create virtual environment with system Python 3.11" { + & $python311 -m venv .venv + } + Invoke-Checked "Upgrade pip" { + & $VenvPython -m pip install --upgrade pip + } + Invoke-Checked "Install pinned Python dependencies" { + & $VenvPython -m pip install -r $Requirements + } + return + } + + throw "uv or Python 3.11 was not found. Install uv first, then run start_api.bat again." +} + +function Ensure-FrontendDist { + if (Test-Path $FrontendIndex) { + Write-Info "Frontend dist is ready." + return + } + + if (-not (Test-CommandExists "npm")) { + throw "frontend/dist is missing and npm was not found. Install Node.js, then run start_api.bat again." + } + + Push-Location (Join-Path $ProjectRoot "web") + try { + if (-not (Test-Path "node_modules")) { + Invoke-Checked "Install frontend dependencies" { + & npm ci + } + } + Invoke-Checked "Build frontend dist" { + & npm run build + } + } finally { + Pop-Location + } +} + +function Test-PlaywrightChromium { + if (-not (Test-Path $PlaywrightBrowsers)) { + return $false + } + + $code = @" +from playwright.sync_api import sync_playwright + +with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True) + browser.close() +"@ + & $VenvPython -c $code *> $null + return $LASTEXITCODE -eq 0 +} + +function Ensure-PlaywrightChromium { + if (Test-PlaywrightChromium) { + Write-Info "Playwright Chromium runtime ready." + return + } + + Write-Warn "Playwright Chromium is missing or version-mismatched. Installing project browser runtime." + Invoke-Checked "Install project Playwright Chromium" { + & $VenvPython -m playwright install chromium + } +} + +function Configure-RuntimeEnvironment { + $env:PLAYWRIGHT_BROWSERS_PATH = $PlaywrightBrowsers + + if (-not $env:CUDA_RUNTIME_PATH -and (Test-Path (Join-Path $DefaultCudaRuntime "bin"))) { + $env:CUDA_RUNTIME_PATH = $DefaultCudaRuntime + } + if (-not $env:CUDNN_PATH -and (Test-Path (Join-Path $DefaultCudaRuntime "bin"))) { + $env:CUDNN_PATH = $DefaultCudaRuntime + } + foreach ($runtimePath in @($env:CUDA_RUNTIME_PATH, $env:CUDNN_PATH)) { + if ($runtimePath -and (Test-Path (Join-Path $runtimePath "bin"))) { + $binPath = Join-Path $runtimePath "bin" + if (-not (($env:PATH -split ";") -contains $binPath)) { + $env:PATH = "$binPath;$env:PATH" + } + } + } +} + +function Run-Doctor { + Invoke-Checked "Run startup doctor" { + & $VenvPython -m scripts.doctor + } +} + +try { + Write-Info "Project directory: $ProjectRoot" + Configure-RuntimeEnvironment + Ensure-Venv + Ensure-FrontendDist + Ensure-PlaywrightChromium + Run-Doctor + + if ($env:AUDIO2TEXT_API_PORT) { + $displayPort = $env:AUDIO2TEXT_API_PORT + } else { + $displayPort = "8000" + } + Write-Info "Local URL: http://127.0.0.1:$displayPort" + + if ($env:AUDIO2TEXT_LOCAL_ONLY -eq "1") { + Write-Info "Local-only mode. No public tunnel will be started." + & $VenvPython -m scripts.start_api + } else { + Write-Info "Starting local API and public preview tunnel." + Write-Info "Keep this window open while other people use the link." + & $VenvPython -m scripts.public_preview + } + exit $LASTEXITCODE +} catch { + Write-Fail $_.Exception.Message + Write-Host "" + Write-Host "Suggested next steps:" + Write-Host "1. Make sure Python, PyPI, and npm sources are reachable." + Write-Host "2. If port 8000 is busy, run: set AUDIO2TEXT_API_PORT=8001" + Write-Host "3. For local-only diagnosis, run: set AUDIO2TEXT_LOCAL_ONLY=1" + Write-Host "4. Run start_api.bat again." + exit 1 +} diff --git a/scripts/wrangler.toml b/scripts/wrangler.toml index 0554b71..099ec58 100644 --- a/scripts/wrangler.toml +++ b/scripts/wrangler.toml @@ -4,3 +4,11 @@ compatibility_date = "2026-05-14" [ai] binding = "AI" + +[[ratelimits]] +name = "TRANSCRIBE_RATE_LIMITER" +namespace_id = "2001" + +[ratelimits.simple] +limit = 12 +period = 60 diff --git a/showcase/README.md b/showcase/README.md new file mode 100644 index 0000000..862cb86 --- /dev/null +++ b/showcase/README.md @@ -0,0 +1,11 @@ +# 万象成文演示片 + +这支演示片只使用项目真实截图,所有转场由 Remotion 代码生成。 + +```powershell +cd showcase +npm ci +npm run render +``` + +输出文件:`docs/assets/wanxiang-demo.mp4`。 diff --git a/showcase/package-lock.json b/showcase/package-lock.json new file mode 100644 index 0000000..02a0d1b --- /dev/null +++ b/showcase/package-lock.json @@ -0,0 +1,3740 @@ +{ + "name": "wanxiang-showcase", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wanxiang-showcase", + "version": "1.0.0", + "dependencies": { + "@remotion/cli": "4.0.507", + "react": "19.2.4", + "react-dom": "19.2.4", + "remotion": "4.0.507" + } + }, + "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.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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.50.8", + "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.50.8.tgz", + "integrity": "sha512-A5Se/LZd6RmYq/h36lBMSEsHvsyW8d0toR7FrAwpsFYbK+DVQYf90KiBT1Aw/mzLXx8/ypIOORJnd1sVZqOvJQ==", + "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.50.8", + "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.50.8.tgz", + "integrity": "sha512-4cfN03SbEoQaG+eBeYFUAb1R1ALaAHzf43dXFzQTr/oO5ueqzTgBoG3coKrIvBaAMU7Vv36mrBKLX8bvTppdAQ==", + "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.50.8", + "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.50.8.tgz", + "integrity": "sha512-eBT/H30tTu8AmZXqZ5RTpJ9VmwVlwednajuOrrWp0GS6cbGXsGMQ60Qvr3ZMIE0QDiadlEzb8/ozR+doP+MC1g==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@module-federation/error-codes": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", + "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", + "license": "MIT" + }, + "node_modules/@module-federation/runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", + "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/runtime-core": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-core": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", + "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-tools": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", + "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/webpack-bundler-runtime": "0.22.0" + } + }, + "node_modules/@module-federation/sdk": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", + "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", + "license": "MIT" + }, + "node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", + "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@remotion/bundler": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.507.tgz", + "integrity": "sha512-SECbwvydNg9XgHcKQkiiYQM92v4FHBADpTb5xlyGROWQ4qA1wySYdDfdMmd0WVQfCV1Zi9JPRDQAW8nDBW33VQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/media-parser": "4.0.507", + "@remotion/studio": "4.0.507", + "@remotion/studio-shared": "4.0.507", + "@remotion/timeline-utils": "4.0.507", + "@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.507", + "style-loader": "4.0.0", + "webpack": "5.105.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/captions": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/captions/-/captions-4.0.507.tgz", + "integrity": "sha512-mS2fe1nbofJQ5vEc6dV1zJpZ06QUmKj1zqRv1TLyACQmlr8O63IP3NTAqhBQSfLYX7Kad6+83Vt2huv+PgM/+w==", + "license": "MIT" + }, + "node_modules/@remotion/cli": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.507.tgz", + "integrity": "sha512-A53RU54y2wf7T00X0wcFOGoa4Dvjqc3VJFi+BgwWWtlSGsPvEmkXLsrly31GpZMXqXE1LU4IeDj1YoajY/GBOQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@babel/parser": "7.24.1", + "@remotion/bundler": "4.0.507", + "@remotion/media-utils": "4.0.507", + "@remotion/player": "4.0.507", + "@remotion/renderer": "4.0.507", + "@remotion/studio": "4.0.507", + "@remotion/studio-server": "4.0.507", + "@remotion/studio-shared": "4.0.507", + "dotenv": "17.3.1", + "minimist": "1.2.6", + "prompts": "2.4.2", + "remotion": "4.0.507", + "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/compositor-darwin-arm64": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.507.tgz", + "integrity": "sha512-MNvUNS33X3YCwalX+LbGcYXXxkWnOspX2vB7agzkHE/IKSFAG58sF9iXyp+rn6lJmCjBcaUxO7paJFxfAzjwEw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-darwin-x64": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.507.tgz", + "integrity": "sha512-oz8Wjq42oKZwhTc2N20tBMXx7Ts3K5T+cQH6oo5PEyFgrpx1j5Lm2AQTsunlgiMhOda20Vff0ZLeiXVUR+xtuA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-gnu": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.507.tgz", + "integrity": "sha512-K6PCQwPE2wJlV5/nat6UrRVx7XCk5LgCRV4cQsIXyYDCvo7995KMI4KgP4DtKXcvXaQUudz9r9JuFT+KGktPwA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-musl": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.507.tgz", + "integrity": "sha512-hKgdJm+dGcxUs/GNajUa2uH6RnphjKVvz+zLWWLdRhgPzxvPhm4ZJm9VPoyr1hmY3c84ws5qiXV30AkHtZVIVA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.507.tgz", + "integrity": "sha512-5w9iYYlfp0qLohFMAAyHPR00NxiQ3SuhJyx6ck+z9pkLZLgTAKC7EWB0e8HMaC6czxPAH1F+ZbXeA1ukfM7SOQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-musl": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.507.tgz", + "integrity": "sha512-tnieQCpBZ1uSL6z9Dk3noNwYG6wGIH3WZpMfRQZgrInJ4LhmRSzzIFx6Bb2l9QZGAkIiGKFadDFHlw0JVsJd1A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-win32-x64-msvc": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.507.tgz", + "integrity": "sha512-FCkZDLcPBCO2WO/MyrtMB5tpsIuqqkc7E1nY2lfY6WmRX2quGfykcsz4S9inYx/G+XybKVTgplqnyLtt9wyFnw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@remotion/licensing": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.507.tgz", + "integrity": "sha512-fdods4YD9c8Ww69wHdnmXpjx7W0rvct6B7gLb92cWsd+ITSUG3hKoh+NqSWXF0GK5bk8IFRmTtBAwJObxow9wA==", + "license": "MIT" + }, + "node_modules/@remotion/media-parser": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.507.tgz", + "integrity": "sha512-BONx6R/0LLUSKjSgRLewb9QjZ9ZulxCct/RGPAFN8TIdeFUWxQ81Yfq1hQmUyXNBUhbfcHBEjdGnjgHCAeJkNw==", + "license": "Remotion License https://remotion.dev/license" + }, + "node_modules/@remotion/media-utils": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.507.tgz", + "integrity": "sha512-Ncm9ATXVOJ7aFbGXycVAXo+ISp3PFZb2JYSnGBuzat+d03CLR3yyCzGPhovlZqhnP7UIaRJxo15I5DlUkiGxrQ==", + "license": "MIT", + "dependencies": { + "mediabunny": "1.50.8", + "remotion": "4.0.507" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/player": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.507.tgz", + "integrity": "sha512-ZAunyyCfOXSYh5sZrp+vFxZlSPatg5evmh7fHVCndAOWBUPIDVjdUWEM2D4i132MsN6GXasXDByi+6H0H+IkhQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "remotion": "4.0.507" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.507.tgz", + "integrity": "sha512-2qw3/2va97UNvkc1R/3L471mJwXEL33E5MI+YmBVUeS7TS/G6uoB4fNNrGMFs+DPp2u2dNGAIBEUqn1TWP9zGw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/licensing": "4.0.507", + "@remotion/streaming": "4.0.507", + "execa": "5.1.1", + "remotion": "4.0.507", + "source-map": "0.8.0", + "ws": "8.21.0" + }, + "optionalDependencies": { + "@remotion/compositor-darwin-arm64": "4.0.507", + "@remotion/compositor-darwin-x64": "4.0.507", + "@remotion/compositor-linux-arm64-gnu": "4.0.507", + "@remotion/compositor-linux-arm64-musl": "4.0.507", + "@remotion/compositor-linux-x64-gnu": "4.0.507", + "@remotion/compositor-linux-x64-musl": "4.0.507", + "@remotion/compositor-win32-x64-msvc": "4.0.507" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/streaming": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.507.tgz", + "integrity": "sha512-RyLYm7/d3w2I0+ZiK/xNUxnDKyIkmpRFfNiSaod2cXnTw2fsbQ7xt5exJZSylvsd581ciDLeOaIN1m/u3425NA==", + "license": "MIT" + }, + "node_modules/@remotion/studio": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.507.tgz", + "integrity": "sha512-6Za3v2uKItMqwiZm5Nk8la02KnPZIl+n3ctlVWmZe0kN8c9nsW3raTeYqGZyL9Bc2UXVq4J4IOWPu2etvnP/RQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@remotion/captions": "4.0.507", + "@remotion/media-utils": "4.0.507", + "@remotion/player": "4.0.507", + "@remotion/renderer": "4.0.507", + "@remotion/studio-protocol": "4.0.507", + "@remotion/studio-shared": "4.0.507", + "@remotion/timeline-utils": "4.0.507", + "@remotion/web-renderer": "4.0.507", + "@remotion/zod-types": "4.0.507", + "mediabunny": "1.50.8", + "memfs": "3.4.3", + "open": "8.4.2", + "remotion": "4.0.507", + "semver": "7.5.3", + "zod": "4.4.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/studio-codemods": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/studio-codemods/-/studio-codemods-4.0.507.tgz", + "integrity": "sha512-F5xSJ/UXoQqEX/63WLa94G4glUZDR5GwDoMANCjEUJmu+YlV9rVuqTGI99YbbtyL59/WBAJVMYAOHCmCMkSgpg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@babel/types": "7.24.0", + "@remotion/studio-shared": "4.0.507", + "ast-types": "0.16.1", + "recast": "0.23.11", + "remotion": "4.0.507" + } + }, + "node_modules/@remotion/studio-protocol": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/studio-protocol/-/studio-protocol-4.0.507.tgz", + "integrity": "sha512-udC5R7gIODKAmMCG808O+uetgsm6aPYXK+g8ztEAAFfPho1pMoUXYUhO20ceLuWXZGMsS3A+sdow0q1eI7BZMQ==", + "license": "Remotion License" + }, + "node_modules/@remotion/studio-server": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.507.tgz", + "integrity": "sha512-ZEsnEAJR3Y3jenbfO8oUMPkLfxrdI4oUuVmIZNs20xh1SWcsR3wMBPd44dK+u4CcwnJRiLMyVRhfGeK2Am9hjQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@babel/types": "7.24.0", + "@remotion/bundler": "4.0.507", + "@remotion/renderer": "4.0.507", + "@remotion/studio-codemods": "4.0.507", + "@remotion/studio-protocol": "4.0.507", + "@remotion/studio-shared": "4.0.507", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "kiwi-schema": "0.5.0", + "memfs": "3.4.3", + "open": "8.4.2", + "prettier": "3.8.1", + "recast": "0.23.11", + "remotion": "4.0.507", + "semver": "7.5.3", + "zod": "4.4.3" + } + }, + "node_modules/@remotion/studio-shared": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.507.tgz", + "integrity": "sha512-qFdI5XwIsVFFiKXx8snM+0SHi41/mfOR+tJcVHsPFzqqYTSd4Wq/4jyPfWzx89QHFkEJw1IBsl7ktpJ7pR7yhw==", + "license": "MIT", + "dependencies": { + "@remotion/studio-protocol": "4.0.507", + "remotion": "4.0.507" + } + }, + "node_modules/@remotion/timeline-utils": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/timeline-utils/-/timeline-utils-4.0.507.tgz", + "integrity": "sha512-xPn0EIQ3/QoIF1om7g76oW/ifpW9FxU0SuLvDn96ApIbZZ8RdMYJQlCXAUpbbrxd2x6nonj40BaQiB9x+eXPsQ==", + "license": "MIT", + "dependencies": { + "mediabunny": "1.50.8" + } + }, + "node_modules/@remotion/web-renderer": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.507.tgz", + "integrity": "sha512-fH8iZX1e0hmeJ+hPqn3RfqrDBNxMGsZZzfdyX2QpTDZ9F2kKqOkh0dQsRqaU8o+YoE8JNV/+FyYm4yyDAh9o6Q==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@mediabunny/aac-encoder": "1.50.8", + "@mediabunny/flac-encoder": "1.50.8", + "@mediabunny/mp3-encoder": "1.50.8", + "@remotion/licensing": "4.0.507", + "mediabunny": "1.50.8", + "remotion": "4.0.507" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@remotion/zod-types": { + "version": "4.0.507", + "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.507.tgz", + "integrity": "sha512-5Rezxss8eOaWq0n/P2U8GAMBQ1wav1HT+lXarqVkEmEGlv3IG7AFjrRSXnWnvm7h4eI5UhpiQ38tBS878QXSGg==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.507" + } + }, + "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/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/dom-mediacapture-transform": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.12.tgz", + "integrity": "sha512-d7/QsLRwF864A5mgIM/YrfiglHoYn7zgCcAoJgW404r+2DwnNr7EBbLnCWpmOMgH8y0te73L1AV6H1bmauaWFw==", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/commander": { + "version": "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.403", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz", + "integrity": "sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "license": "Unlicense" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "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/mediabunny": { + "version": "1.50.8", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.50.8.tgz", + "integrity": "sha512-LgykLyQzhdpo0V2yw3UXmOpj+b4JAGdpHBwsPE6kjSt8Za0d1VllD+FV7EGHBcdV4+oHUAo+yrqbVAWxNSDCPQ==", + "license": "MPL-2.0", + "workspaces": [ + ".", + "packages/*" + ], + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + } + }, + "node_modules/memfs": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "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/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "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.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "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.507", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.507.tgz", + "integrity": "sha512-g/NrnRZKJyh0isE+/0sIxKlgJl6BH5v/CJ35W4TGYR1obQZf60fHkf9CGxSBHJEw27dG1PJCgKT75YGfoCZ7tg==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/scheduler": { + "version": "0.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.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "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.49.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.2.tgz", + "integrity": "sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==", + "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/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/showcase/package.json b/showcase/package.json new file mode 100644 index 0000000..b7072a8 --- /dev/null +++ b/showcase/package.json @@ -0,0 +1,15 @@ +{ + "name": "wanxiang-showcase", + "private": true, + "version": "1.0.0", + "scripts": { + "studio": "remotion studio src/index.ts --no-open", + "render": "remotion render src/index.ts WanxiangDemo ../docs/assets/wanxiang-demo.mp4 --codec h264 --crf 20" + }, + "dependencies": { + "@remotion/cli": "4.0.507", + "react": "19.2.4", + "react-dom": "19.2.4", + "remotion": "4.0.507" + } +} diff --git a/showcase/public/home-desktop.png b/showcase/public/home-desktop.png new file mode 100644 index 0000000..0bc203a Binary files /dev/null and b/showcase/public/home-desktop.png differ diff --git a/showcase/public/result-video-desktop.png b/showcase/public/result-video-desktop.png new file mode 100644 index 0000000..3d1e0b8 Binary files /dev/null and b/showcase/public/result-video-desktop.png differ diff --git a/showcase/src/index.ts b/showcase/src/index.ts new file mode 100644 index 0000000..2ffde3d --- /dev/null +++ b/showcase/src/index.ts @@ -0,0 +1,5 @@ +import { registerRoot } from "remotion"; + +import { RemotionRoot } from "./root"; + +registerRoot(RemotionRoot); diff --git a/showcase/src/root.tsx b/showcase/src/root.tsx new file mode 100644 index 0000000..10cbe29 --- /dev/null +++ b/showcase/src/root.tsx @@ -0,0 +1,14 @@ +import { Composition } from "remotion"; + +import { WanxiangDemo } from "./wanxiang-demo"; + +export const RemotionRoot = () => ( + +); diff --git a/showcase/src/wanxiang-demo.tsx b/showcase/src/wanxiang-demo.tsx new file mode 100644 index 0000000..0bc14d9 --- /dev/null +++ b/showcase/src/wanxiang-demo.tsx @@ -0,0 +1,157 @@ +import { + AbsoluteFill, + Easing, + Img, + interpolate, + staticFile, + useCurrentFrame, +} from "remotion"; + +const ease = Easing.bezier(0.16, 1, 0.3, 1); + +function sceneOpacity(frame: number, start: number, end: number) { + return interpolate( + frame, + [start, start + 15, end - 15, end], + [0, 1, 1, 0], + { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: ease }, + ); +} + +const Brand = () => ( +
+
+
+
+
Praxis AI / 万象成文
+
+); + +const BrowserFrame = ({ src }: { src: string }) => ( +
+ +
+); + +export const WanxiangDemo = () => { + const frame = useCurrentFrame(); + + return ( + + + +
+ 把链接或文件,直接变成文字。 +
+
+ 视频、播客、图文,一处整理。 +
+
+ +
+
+ + +
+ +
+ 文字先到,媒体随后。 +
+
+ 字幕与正文优先交付,视频、音频和图片继续准备。 +
+
+
+ +
+
+ + + +
+
+ 本地优先,也能在线体验。 +
+
+ 哔哩哔哩 · 小宇宙 · 抖音 · 小红书 · YouTube · 微信公众号 +
+
+
+ 下载 Windows 版 +
+
+
+ ); +}; diff --git a/start_api.bat b/start_api.bat index fc6bfdc..03b6ae3 100644 --- a/start_api.bat +++ b/start_api.bat @@ -4,57 +4,7 @@ setlocal cd /d "%~dp0" -if not exist ".\.venv\Scripts\python.exe" ( - echo [ERROR] Python virtual environment not found: - echo .\.venv\Scripts\python.exe - pause - exit /b 1 -) - -call .\.venv\Scripts\activate.bat -set "PLAYWRIGHT_BROWSERS_PATH=%CD%\workspace\runtime\playwright-browsers" - -if "%CUDA_RUNTIME_PATH%"=="" ( - if exist "D:\Apps\NVIDIA\CUDA\v12-runtime\bin" set "CUDA_RUNTIME_PATH=D:\Apps\NVIDIA\CUDA\v12-runtime" -) - -if "%CUDNN_PATH%"=="" ( - if exist "D:\Apps\NVIDIA\CUDA\v12-runtime\bin" set "CUDNN_PATH=D:\Apps\NVIDIA\CUDA\v12-runtime" -) - -if not "%CUDA_RUNTIME_PATH%"=="" ( - if exist "%CUDA_RUNTIME_PATH%\bin" set "PATH=%CUDA_RUNTIME_PATH%\bin;%PATH%" -) - -if not "%CUDNN_PATH%"=="" ( - if exist "%CUDNN_PATH%\bin" set "PATH=%CUDNN_PATH%\bin;%PATH%" -) - -if "%AUDIO2TEXT_API_PORT%"=="" ( - set "AUDIO2TEXT_DISPLAY_PORT=8000" -) else ( - set "AUDIO2TEXT_DISPLAY_PORT=%AUDIO2TEXT_API_PORT%" -) - -echo [INFO] Project directory: -echo %CD% -echo. -echo [INFO] Local URL: -echo http://127.0.0.1:%AUDIO2TEXT_DISPLAY_PORT% -echo. - -if "%AUDIO2TEXT_LOCAL_ONLY%"=="1" ( - echo [INFO] Local-only mode. No public tunnel will be started. - echo [INFO] To share a public link, close this window and run start_api.bat without AUDIO2TEXT_LOCAL_ONLY=1. - echo. - python -m scripts.start_api -) else ( - echo [INFO] Starting local API and public preview tunnel. - echo [INFO] Keep this window open while other people use the link. - echo [INFO] To start local-only next time: set AUDIO2TEXT_LOCAL_ONLY=1 - echo. - python -m scripts.public_preview -) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\start_api_bootstrap.ps1" echo. echo [INFO] Service stopped or failed. diff --git a/tests/live_smoke_helpers.py b/tests/live_smoke_helpers.py index 1d05724..21a28e2 100644 --- a/tests/live_smoke_helpers.py +++ b/tests/live_smoke_helpers.py @@ -92,6 +92,14 @@ def validate_sample(bucket: str, index: int, sample: object) -> None: if missing_variants: errors.append("xiaohongshu success samples are missing variants: " + ", ".join(missing_variants)) + xiaohongshu_urls = [ + str(sample.get("url", "")).strip() + for sample in success_items + if isinstance(sample, dict) and str(sample.get("platform", "")).strip() == "xiaohongshu" + ] + if len(xiaohongshu_urls) != len(set(xiaohongshu_urls)): + errors.append("xiaohongshu success variants must use distinct sample URLs") + return errors diff --git a/tests/live_smoke_samples.json b/tests/live_smoke_samples.json index 4b59652..22bfe3d 100644 --- a/tests/live_smoke_samples.json +++ b/tests/live_smoke_samples.json @@ -10,18 +10,20 @@ }, { "platform": "bilibili", - "variant": "subtitle_video", - "url": "https://www.bilibili.com/video/BV1tuwezwEi7/", + "variant": "user_reported_video", + "url": "https://www.bilibili.com/video/BV1xBjW6pEym/", "expected_media_kind": "video", "require_text": true, - "require_source_media": true + "require_source_media": true, + "require_source_audio": true }, { "platform": "xiaoyuzhou", - "variant": "transcript_audio", - "url": "https://www.xiaoyuzhoufm.com/episode/69b6b4fbf8b8079bfa778b80", + "variant": "user_reported_audio", + "url": "https://www.xiaoyuzhoufm.com/episode/6a2fffb043a22a6955844245", "expected_media_kind": "audio", - "require_text": true + "require_text": true, + "require_source_audio": true }, { "platform": "douyin", @@ -34,18 +36,22 @@ { "platform": "xiaohongshu", "variant": "image", - "url": "https://www.xiaohongshu.com/discovery/item/69cc3a5c000000001a029643", + "url": "https://www.xiaohongshu.com/discovery/item/69e6cc20000000001d01becd", "require_image_urls": true }, { "platform": "xiaohongshu", "variant": "video", - "url": "https://www.xiaohongshu.com/discovery/item/69cc3a5c000000001a029643" + "url": "https://www.xiaohongshu.com/discovery/item/6956513d000000001e022eda", + "expected_media_kind": "video", + "require_source_media": true }, { "platform": "xiaohongshu", "variant": "live", - "url": "https://www.xiaohongshu.com/discovery/item/69cc3a5c000000001a029643" + "url": "https://www.xiaohongshu.com/discovery/item/6a2e9f630000000017009d6a", + "require_image_urls": true, + "require_live_artifact": true }, { "platform": "wechat_article", diff --git a/tests/test_benchmark_transcription.py b/tests/test_benchmark_transcription.py index 7890ff0..ef450ab 100644 --- a/tests/test_benchmark_transcription.py +++ b/tests/test_benchmark_transcription.py @@ -21,7 +21,7 @@ def test_manifest_loads_relative_audio_and_terms(self) -> None: audio.write_bytes(b"") manifest = root / "samples.jsonl" manifest.write_text( - '{"id":"sample","audio":"sample.wav","reference":"hello","terms":["CUDA","Whisper"]}\n', + '{"id":"sample","audio":"sample.wav","reference":"hello","terms":["CUDA","Whisper"],"slice":"noise","weight":2}\n', encoding="utf-8", ) @@ -32,6 +32,41 @@ def test_manifest_loads_relative_audio_and_terms(self) -> None: self.assertEqual(samples[0].audio_path, audio) self.assertEqual(samples[0].reference, "hello") self.assertEqual(samples[0].terms, ("CUDA", "Whisper")) + self.assertEqual(samples[0].slice_name, "noise") + self.assertEqual(samples[0].weight, 2) + + def test_summary_reports_release_metrics(self) -> None: + summary = benchmark_transcription._summarize( + [ + { + "rtf": 0.1, + "char_error_rate": 0.2, + "weight": 2, + "duration_seconds": 60, + "first_segment_seconds": 0.4, + "term_count": 2, + "term_hits": ["one"], + "slice": "noise", + "silence_hallucination": False, + }, + { + "rtf": 0.3, + "char_error_rate": 0.1, + "weight": 1, + "duration_seconds": 120, + "first_segment_seconds": 0.8, + "term_count": 1, + "term_hits": ["two"], + "slice": "mandarin", + "silence_hallucination": True, + }, + ] + ) + + self.assertEqual(summary["weighted_char_error_rate"], 0.1667) + self.assertEqual(summary["term_recall"], 0.6667) + self.assertEqual(summary["silence_hallucination_count"], 1) + self.assertFalse(benchmark_transcription._release_dataset_ready(summary)) if __name__ == "__main__": diff --git a/tests/test_capture_api.py b/tests/test_capture_api.py index 229c65c..a867cbd 100644 --- a/tests/test_capture_api.py +++ b/tests/test_capture_api.py @@ -53,6 +53,83 @@ def tearDown(self) -> None: child.rmdir() self.root.rmdir() + def test_desktop_runtime_requires_loopback_token_and_known_origin(self) -> None: + with ( + patch.object(capture_main, "RUNTIME_TARGET", "windows_desktop"), + patch.object(capture_main, "DESKTOP_TOKEN", "desktop-test-token"), + patch.object( + capture_main, + "DESKTOP_ALLOWED_ORIGINS", + capture_main.DESKTOP_ALLOWED_ORIGINS | {"http://localhost:5173"}, + ), + ): + unauthorized = self.client.get("/health") + wrong_origin = self.client.get( + "/health", + headers={ + "Origin": "https://example.com", + "X-Wanxiang-Desktop-Token": "desktop-test-token", + }, + ) + authorized = self.client.get( + "/health", + headers={ + "Origin": "http://tauri.localhost", + "X-Wanxiang-Desktop-Token": "desktop-test-token", + }, + ) + event_source_style = self.client.get( + "/health?desktop_token=desktop-test-token", + headers={"Origin": "http://tauri.localhost"}, + ) + preflight = self.client.options( + "/config", + headers={ + "Origin": "http://localhost:5173", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "x-wanxiang-desktop-token", + }, + ) + hostile_preflight = self.client.options( + "/config", + headers={ + "Origin": "https://example.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "x-wanxiang-desktop-token", + }, + ) + + self.assertEqual(unauthorized.status_code, 401) + self.assertEqual(wrong_origin.status_code, 403) + self.assertEqual(authorized.status_code, 200) + self.assertEqual(event_source_style.status_code, 200) + self.assertEqual(preflight.status_code, 200) + self.assertEqual( + preflight.headers.get("access-control-allow-origin"), + "http://localhost:5173", + ) + self.assertEqual(hostile_preflight.status_code, 403) + + def test_runtime_pack_api_is_desktop_only_and_sanitized(self) -> None: + web_response = self.client.get("/v1/runtime/packs") + with ( + patch.object(capture_main, "RUNTIME_TARGET", "windows_desktop"), + patch.object(capture_main, "DESKTOP_TOKEN", "desktop-test-token"), + patch.object( + capture_main.runtime_pack_manager, + "status", + return_value=[{"id": "asr", "version": "1", "installed": False}], + ), + ): + desktop_response = self.client.get( + "/v1/runtime/packs", + headers={"X-Wanxiang-Desktop-Token": "desktop-test-token"}, + ) + + self.assertEqual(web_response.status_code, 404) + self.assertEqual(desktop_response.status_code, 200) + self.assertEqual(desktop_response.json()["packs"][0]["id"], "asr") + def test_config_omits_runtime_details(self) -> None: response = self.client.get("/config") @@ -65,6 +142,26 @@ def test_config_omits_runtime_details(self) -> None: self.assertIn("public_preview_mode", payload) self.assertIn("transcription_available", payload) self.assertIn("transcription_provider", payload) + self.assertIn("runtime_target", payload) + self.assertIn("capabilities", payload) + self.assertNotIn("commit", payload) + + def test_health_exposes_consistent_sanitized_runtime_identity(self) -> None: + with patch.object(capture_main, "APP_VERSION", "2.0.0-test"), patch.object( + capture_main, "APP_COMMIT", "abc1234" + ), patch.object(capture_main, "DEPLOYMENT_MODE", "cloud_preview"), patch.object( + capture_main, "RUNTIME_TARGET", "cloud_demo" + ): + response = self.client.get("/health") + + self.assertEqual(response.status_code, 200) + payload = response.json() + self.assertEqual(payload["deployment_mode"], "cloud_preview") + self.assertEqual(payload["runtime_target"], "cloud_demo") + self.assertEqual(payload["version"], "2.0.0-test") + self.assertEqual(payload["commit"], "abc1234") + self.assertNotIn("run_mode", payload) + self.assertIsInstance(payload["components"], dict) def test_windows_connection_reset_filter_is_narrow(self) -> None: with patch.object(capture_main.sys, "platform", "win32"): @@ -132,6 +229,9 @@ def test_capture_envelope_exposes_canonical_url_but_omits_internal_fields(self) self.assertIn("progress_percent", payload["capture"]) self.assertIn("text_source", payload["quality"]) self.assertEqual(payload["artifacts"][0]["download_url"], f"/v1/captures/{capture.id}/artifacts/txt") + self.assertEqual(payload["capture"]["result_state"], "complete") + self.assertEqual(payload["artifacts"][0]["status"], "ready") + self.assertFalse(payload["artifacts"][0]["optional"]) def test_capture_envelope_exposes_source_images_and_asset_pending_flag(self) -> None: capture = capture_main.repository.create( @@ -159,12 +259,63 @@ def test_capture_envelope_exposes_source_images_and_asset_pending_flag(self) -> payload = self.client.get(f"/v1/captures/{capture.id}").json() self.assertTrue(payload["capture"]["asset_preparation_pending"]) + self.assertEqual(payload["capture"]["result_state"], "text_ready") self.assertEqual(len(payload["source"]["images"]), 2) self.assertEqual(payload["source"]["images"][0]["index"], 1) self.assertEqual(payload["source"]["images"][0]["image_url"], "https://example.com/image-1.jpg") self.assertEqual(payload["source"]["images"][0]["live_photo_video_url"], "https://example.com/live-1.mp4") self.assertIsNone(payload["source"]["images"][1]["live_photo_video_url"]) + def test_event_names_expose_text_artifact_and_runtime_transitions(self) -> None: + capture = capture_main.repository.create( + input_type="url", + source_platform="bilibili", + content_type="video", + url="https://www.bilibili.com/video/BV1xx", + ) + result = ResultDocumentModel( + primary_text="正文已经可以阅读", + primary_result_type="transcript", + artifacts=[ + ArtifactModel( + type="source_audio", + label="原音频", + path=str(self.root / "audio.m4a"), + download_url=f"/v1/captures/{capture.id}/artifacts/source_audio", + status="ready", + ) + ], + ) + capture = capture_main.repository.update( + capture.id, + status="done", + result_state="text_ready", + asset_preparation_pending=True, + result=result, + ) + envelope = capture_main._to_capture_envelope(capture) + + events, artifact_types = capture_main._transition_event_names( + capture, + envelope, + previous_result_state="processing", + ready_artifact_types=set(), + ) + + self.assertEqual(events, ["text_ready", "artifact_ready"]) + self.assertEqual(artifact_types, {"source_audio"}) + + capture.processing.failure_reason_code = "runtime_model_required" + capture.status = "failed" + failed_envelope = capture_main._to_capture_envelope(capture) + events, _ = capture_main._transition_event_names( + capture, + failed_envelope, + previous_result_state="text_ready", + ready_artifact_types=artifact_types, + ) + self.assertEqual(events, ["runtime_required"]) + def test_file_upload_uses_capture_workspace_storage(self) -> None: response = self.client.post( "/v1/captures", diff --git a/tests/test_desktop_contract.py b/tests/test_desktop_contract.py new file mode 100644 index 0000000..d73c53f --- /dev/null +++ b/tests/test_desktop_contract.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import re +import json +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class DesktopContractTests(unittest.TestCase): + def test_frontend_invokes_registered_tauri_commands(self) -> None: + frontend = (ROOT / "web" / "src" / "api.ts").read_text(encoding="utf-8") + rust = (ROOT / "web" / "src-tauri" / "src" / "lib.rs").read_text( + encoding="utf-8" + ) + + invoked = set(re.findall(r'invoke(?:<[^>]+>)?\("([a-z_]+)"', frontend)) + handler_match = re.search( + r"generate_handler!\[(.*?)\]", rust, flags=re.DOTALL + ) + self.assertIsNotNone(handler_match) + registered = set(re.findall(r"\b([a-z][a-z0-9_]+)\b", handler_match.group(1))) + + self.assertTrue(invoked) + self.assertEqual(set(), invoked - registered) + + def test_runtime_manifest_has_a_stable_bundle_path(self) -> None: + config = json.loads( + (ROOT / "web" / "src-tauri" / "tauri.conf.json").read_text( + encoding="utf-8" + ) + ) + resources = config["bundle"]["resources"] + self.assertEqual(resources["../../runtime-packs.json"], "runtime-packs.json") + self.assertEqual(resources["sidecar/"], "sidecar/") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_live_smoke_samples_contract.py b/tests/test_live_smoke_samples_contract.py index 521f31e..fe137ab 100644 --- a/tests/test_live_smoke_samples_contract.py +++ b/tests/test_live_smoke_samples_contract.py @@ -21,6 +21,21 @@ def test_default_live_smoke_samples_do_not_commit_share_tokens(self) -> None: self.assertEqual(find_committed_sample_url_hygiene_issues(payload), []) + def test_rejects_reused_xiaohongshu_variant_urls(self) -> None: + sample_path = Path("tests/live_smoke_samples.json") + payload = load_live_smoke_samples(sample_path) + xiaohongshu = [ + item for item in payload["success"] if item["platform"] == "xiaohongshu" + ] + xiaohongshu[1]["url"] = xiaohongshu[0]["url"] + + from tests.live_smoke_helpers import validate_live_smoke_samples + + self.assertIn( + "xiaohongshu success variants must use distinct sample URLs", + validate_live_smoke_samples(payload), + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_pipeline_and_store.py b/tests/test_pipeline_and_store.py index 5b92610..b34a3de 100644 --- a/tests/test_pipeline_and_store.py +++ b/tests/test_pipeline_and_store.py @@ -408,6 +408,84 @@ def fake_preview(source_path: Path, target_path: Path) -> Path: child.rmdir() root.rmdir() + def test_persist_result_artifacts_exports_source_audio_for_video(self) -> None: + root = Path.cwd() / "tests_runtime" / "artifact_runs" / uuid4().hex + artifacts_dir = root / "artifacts" + media_dir = root / "media" + artifacts_dir.mkdir(parents=True, exist_ok=True) + media_dir.mkdir(parents=True, exist_ok=True) + media_path = media_dir / "sample.mp4" + media_path.write_bytes(b"video-with-audio") + + capture = SimpleNamespace( + id="cap-video-audio", + title="sample", + url="https://example.com/video", + source=SourceMetaModel(platform="bilibili", content_type="video", canonical_url="https://example.com/video"), + ) + result = ResultDocumentModel(primary_text="hello world") + captured_command: list[str] = [] + + def fake_run(command, capture_output, text, check): + captured_command.extend(command) + (artifacts_dir / capture.id / "source_audio.m4a").write_bytes(b"audio-bytes") + return SimpleNamespace(returncode=0, stderr="", stdout="") + + with patch.object(pipeline, "ARTIFACTS_DIR", artifacts_dir), patch.object( + pipeline, "_audio_codec_for_path", return_value="opus" + ), patch.object(pipeline.subprocess, "run", side_effect=fake_run), patch.object( + pipeline, "_source_media_needs_preview", return_value=False + ): + persisted = pipeline._persist_result_artifacts(capture, result, media_file_path=str(media_path)) + + source_audio = next(artifact for artifact in persisted.artifacts if artifact.type == "source_audio") + self.assertEqual(source_audio.mime_type, "audio/mp4") + self.assertEqual(Path(source_audio.path), artifacts_dir / capture.id / "source_audio.m4a") + self.assertIn("-vn", captured_command) + self.assertIn("192k", captured_command) + + for child in sorted(root.rglob("*"), key=lambda item: len(item.parts), reverse=True): + if child.is_file(): + child.unlink(missing_ok=True) + elif child.is_dir(): + child.rmdir() + root.rmdir() + + def test_persist_result_artifacts_preserves_source_audio_for_audio_capture(self) -> None: + root = Path.cwd() / "tests_runtime" / "artifact_runs" / uuid4().hex + artifacts_dir = root / "artifacts" + media_dir = root / "media" + artifacts_dir.mkdir(parents=True, exist_ok=True) + media_dir.mkdir(parents=True, exist_ok=True) + media_path = media_dir / "episode.m4a" + media_path.write_bytes(b"audio") + + capture = SimpleNamespace( + id="cap-audio", + title="episode", + url="https://example.com/episode", + source=SourceMetaModel(platform="xiaoyuzhou", content_type="audio", canonical_url="https://example.com/episode"), + ) + result = ResultDocumentModel(primary_text="hello world") + + with patch.object(pipeline, "ARTIFACTS_DIR", artifacts_dir), patch.object( + pipeline, "_audio_codec_for_path", return_value="aac" + ): + persisted = pipeline._persist_result_artifacts(capture, result, media_file_path=str(media_path)) + + artifact_types = [artifact.type for artifact in persisted.artifacts] + self.assertIn("source_audio", artifact_types) + self.assertNotIn("source_media", artifact_types) + source_audio = next(artifact for artifact in persisted.artifacts if artifact.type == "source_audio") + self.assertEqual(Path(source_audio.path), media_path) + + for child in sorted(root.rglob("*"), key=lambda item: len(item.parts), reverse=True): + if child.is_file(): + child.unlink(missing_ok=True) + elif child.is_dir(): + child.rmdir() + root.rmdir() + def test_persist_result_artifacts_skips_preview_media_for_browser_friendly_video(self) -> None: root = Path.cwd() / "tests_runtime" / "artifact_runs" / uuid4().hex artifacts_dir = root / "artifacts" diff --git a/tests/test_platform_contracts.py b/tests/test_platform_contracts.py index 9a8e253..8c00e38 100644 --- a/tests/test_platform_contracts.py +++ b/tests/test_platform_contracts.py @@ -18,7 +18,11 @@ ) from app.extractors import ( ExtractionOutcome, + _bilibili_best_dash_pair, + _bilibili_progressive_media_url, + _bilibili_subtitle_url, _classify_external_error, + _extract_bilibili_video_id, _parse_web_subtitle, _select_subtitle_track, _select_youtube_transcript, @@ -131,6 +135,59 @@ def test_resolve_url_routes_all_supported_platforms(self) -> None: resolved = resolve_url(url) self.assertEqual(resolved.platform, expected) + def test_bilibili_direct_contract_selects_subtitles_and_media(self) -> None: + self.assertEqual( + _extract_bilibili_video_id("https://www.bilibili.com/video/BV1xBjW6pEym/"), + "BV1xBjW6pEym", + ) + + subtitle_url = _bilibili_subtitle_url( + { + "subtitle": { + "subtitles": [ + {"subtitle_url": "//example.com/subtitle.json"}, + ] + } + } + ) + self.assertEqual(subtitle_url, "https://example.com/subtitle.json") + + progressive_url = _bilibili_progressive_media_url( + {"durl": [{"url": "https://media.example.com/progressive.mp4"}]} + ) + self.assertEqual(progressive_url, "https://media.example.com/progressive.mp4") + + video_url, audio_url = _bilibili_best_dash_pair( + { + "dash": { + "video": [ + { + "baseUrl": "https://media.example.com/4k.m4s", + "height": 2160, + "bandwidth": 3000000, + }, + { + "baseUrl": "https://media.example.com/720p.m4s", + "height": 720, + "bandwidth": 1500000, + }, + ], + "audio": [ + { + "baseUrl": "https://media.example.com/audio-low.m4s", + "bandwidth": 64000, + }, + { + "baseUrl": "https://media.example.com/audio-high.m4s", + "bandwidth": 192000, + }, + ], + } + } + ) + self.assertEqual(video_url, "https://media.example.com/720p.m4s") + self.assertEqual(audio_url, "https://media.example.com/audio-high.m4s") + def test_douyin_provider_order_prefers_browser_before_open_source(self) -> None: resolved = SimpleNamespace(normalized_url="https://www.douyin.com/video/1234567890") providers = source_adapters.DouyinAdapter().build_providers(resolved, Path(tempfile.gettempdir())) @@ -147,6 +204,34 @@ def test_xiaohongshu_provider_order_excludes_generic_web_fallback(self) -> None: ["browser_provider", "open_source_provider"], ) + def test_xiaohongshu_preserves_browser_challenge_error_priority(self) -> None: + adapter = source_adapters.XiaohongshuAdapter() + resolved = SimpleNamespace(normalized_url="https://www.xiaohongshu.com/explore/demo") + + with patch.object( + adapter, + "_browser_extract", + side_effect=BrowserProviderError( + "当前页面触发了平台验证,请刷新项目级浏览器会话后再试。", + reason_code="browser_challenge_required", + retryable=True, + ), + ), patch( + "app.source_adapters.extract_with_ytdlp", + side_effect=extractors.ExtractionError( + "extract", + "ERROR: [XiaoHongShu] No video formats found!", + reason_code="extract_failed", + retryable=False, + ), + ): + with self.assertRaises(extractors.ExtractionError) as caught: + adapter.extract(resolved, Path(tempfile.gettempdir())) + + self.assertEqual(caught.exception.reason_code, "browser_challenge_required") + self.assertTrue(caught.exception.retryable) + self.assertIn("平台验证", caught.exception.message) + def test_adapter_success_records_extractor_and_fallback_metadata(self) -> None: adapter = _FallbackAdapter() resolved = SimpleNamespace(normalized_url="https://example.com/watch") @@ -773,6 +858,10 @@ def test_external_error_classification_is_platform_aware(self) -> None: _classify_external_error("ERROR: 1111111111: KeyError('bvid')", platform="bilibili", stage="extract"), ("bilibili_video_invalid", False), ) + self.assertEqual( + _classify_external_error("HTTP Error 412: Precondition Failed", platform="bilibili", stage="extract"), + ("bilibili_extract_precondition_failed", True), + ) def test_public_error_message_prefers_reason_code(self) -> None: self.assertEqual( diff --git a/tests/test_platform_live_smoke.py b/tests/test_platform_live_smoke.py index 58daa0f..4e00bd0 100644 --- a/tests/test_platform_live_smoke.py +++ b/tests/test_platform_live_smoke.py @@ -73,6 +73,12 @@ def test_success_samples(self) -> None: f"expected source_media artifact for {sample['platform']}", ) + if sample.get("require_source_audio", False): + self.assertTrue( + any(item["type"] == "source_audio" for item in payload["artifacts"]), + f"expected source_audio artifact for {sample['platform']}", + ) + if sample.get("require_image_urls", False): self.assertTrue(payload["source"]["image_urls"]) diff --git a/tests/test_runtime_packs.py b/tests/test_runtime_packs.py new file mode 100644 index 0000000..18e1f72 --- /dev/null +++ b/tests/test_runtime_packs.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import hashlib +import json +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import patch + +from app.runtime_packs import RuntimePackError, RuntimePackManager + + +class RuntimePackManagerTestCase(unittest.TestCase): + def _manager(self, root: Path, pack: dict) -> RuntimePackManager: + manifest = root / "runtime-packs.json" + manifest.write_text(json.dumps({"schema_version": 1, "packs": [pack]}), encoding="utf-8") + return RuntimePackManager(manifest, root / "runtime") + + def test_manifest_rejects_insecure_url_and_traversal(self) -> None: + with tempfile.TemporaryDirectory() as raw_root: + manager = self._manager( + Path(raw_root), + { + "id": "model", + "version": "1", + "url": "http://example.com/model.zip", + "sha256": "0" * 64, + "size_bytes": 1, + "install_subdir": "../outside", + }, + ) + with self.assertRaises(RuntimePackError): + manager.packs() + + def test_install_is_verified_and_atomic(self) -> None: + with tempfile.TemporaryDirectory() as raw_root: + root = Path(raw_root) + archive = root / "pack.zip" + with zipfile.ZipFile(archive, "w") as handle: + handle.writestr("model.bin", b"verified") + payload = archive.read_bytes() + manager = self._manager( + root, + { + "id": "sensevoice", + "version": "1.0.0", + "url": "https://downloads.example.com/model.zip", + "sha256": hashlib.sha256(payload).hexdigest(), + "size_bytes": len(payload), + "install_subdir": "models/sensevoice", + }, + ) + + def fake_download(pack, destination): + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(payload) + + with patch.object(manager, "_download", fake_download): + result = manager.install("sensevoice") + + self.assertTrue(result["installed"]) + self.assertEqual((root / "runtime/models/sensevoice/model.bin").read_bytes(), b"verified") + self.assertTrue(manager.status()[0]["installed"]) + + def test_zip_path_traversal_is_rejected(self) -> None: + with tempfile.TemporaryDirectory() as raw_root: + root = Path(raw_root) + archive = root / "pack.zip" + with zipfile.ZipFile(archive, "w") as handle: + handle.writestr("../escape.txt", b"no") + payload = archive.read_bytes() + manager = self._manager( + root, + { + "id": "bad-pack", + "version": "1", + "url": "https://downloads.example.com/model.zip", + "sha256": hashlib.sha256(payload).hexdigest(), + "size_bytes": len(payload), + "install_subdir": "models/bad", + }, + ) + with self.assertRaises(RuntimePackError): + manager._unpack(manager.get("bad-pack"), archive, root / "stage") + self.assertFalse((root / "escape.txt").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_transcription_sensevoice.py b/tests/test_transcription_sensevoice.py new file mode 100644 index 0000000..ed8cf4a --- /dev/null +++ b/tests/test_transcription_sensevoice.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + +from app.providers import transcription + + +class _FakeStream: + def __init__(self) -> None: + self.result = SimpleNamespace(text="轻量模型转写成功") + self.accepted = None + + def accept_waveform(self, sample_rate, samples) -> None: + self.accepted = (sample_rate, samples) + + +class _FakeRecognizer: + def __init__(self) -> None: + self.stream = _FakeStream() + + def create_stream(self): + return self.stream + + def decode_stream(self, stream) -> None: + self.decoded = stream + + +class SenseVoiceProviderTestCase(unittest.TestCase): + def test_provider_returns_normalized_result(self) -> None: + with tempfile.TemporaryDirectory() as raw_root: + media = Path(raw_root) / "sample.wav" + media.write_bytes(b"fixture") + provider = transcription.LocalSenseVoiceProvider(raw_root) + recognizer = _FakeRecognizer() + progress = [] + with ( + patch.object(provider, "_decode_audio", return_value=[0.0, 0.1]), + patch.object(provider, "_recognizer", return_value=recognizer), + ): + result = provider.transcribe( + str(media), + capture_id="capture", + output_folder=raw_root, + progress_callback=lambda ratio, detail: progress.append((ratio, detail)), + ) + + self.assertEqual(result.provider, "local_sensevoice") + self.assertEqual(result.transcript_text, "轻量模型转写成功") + self.assertEqual(recognizer.stream.accepted[0], 16000) + self.assertEqual([item[0] for item in progress], [0.1, 1.0]) + + def test_auto_prefers_sensevoice_on_cpu_without_whisper_model(self) -> None: + with tempfile.TemporaryDirectory() as raw_root: + model_dir = Path(raw_root) + (model_dir / "model.int8.onnx").write_bytes(b"model") + (model_dir / "tokens.txt").write_text("tokens", encoding="utf-8") + with ( + patch.object(transcription, "TRANSCRIPTION_PROVIDER", "auto"), + patch.object(transcription, "DEVICE", "cpu"), + patch.object(transcription, "MODEL_PATH", model_dir / "missing"), + patch.object(transcription, "SENSEVOICE_MODEL_DIR", model_dir), + ): + provider = transcription.get_transcription_provider() + + self.assertIsInstance(provider, transcription.LocalSenseVoiceProvider) + self.assertEqual(provider.model_dir, model_dir) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_transcription_worker_auth.py b/tests/test_transcription_worker_auth.py new file mode 100644 index 0000000..6e55ca6 --- /dev/null +++ b/tests/test_transcription_worker_auth.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import hashlib +import hmac +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from app.providers.transcription import _build_worker_auth_headers + + +class TranscriptionWorkerAuthTestCase(unittest.TestCase): + def test_auth_headers_bind_file_hash_and_duration(self) -> None: + with tempfile.TemporaryDirectory() as folder: + media_path = Path(folder) / "sample.bin" + media_path.write_bytes(b"signed audio payload") + + with patch("app.providers.transcription.time.time", return_value=1_700_000_000), patch( + "app.providers.transcription.uuid.uuid4" + ) as uuid4: + uuid4.return_value.hex = "0123456789abcdef0123456789abcdef" + headers = _build_worker_auth_headers( + media_path, + secret="test-secret", + duration_seconds=42.5, + ) + + body_hash = hashlib.sha256(b"signed audio payload").hexdigest() + expected_payload = "\n".join( + [ + "1700000000", + "0123456789abcdef0123456789abcdef", + "42.500", + body_hash, + "wanxiang-backend", + ] + ) + expected_signature = hmac.new( + b"test-secret", + expected_payload.encode("utf-8"), + hashlib.sha256, + ).hexdigest() + + self.assertEqual(headers["X-Praxis-Timestamp"], "1700000000") + self.assertEqual(headers["X-Praxis-Nonce"], "0123456789abcdef0123456789abcdef") + self.assertEqual(headers["X-Praxis-Audio-Duration"], "42.500") + self.assertEqual(headers["X-Praxis-Content-SHA256"], body_hash) + self.assertEqual(headers["X-Praxis-Signature"], expected_signature) + self.assertEqual(headers["X-Praxis-Service"], "wanxiang-backend") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_worker_security.py b/tests/test_worker_security.py new file mode 100644 index 0000000..1fbb5e4 --- /dev/null +++ b/tests/test_worker_security.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import subprocess +import unittest +from pathlib import Path + + +class WorkerSecurityTestCase(unittest.TestCase): + def test_cloudflare_worker_security_contract(self) -> None: + project_root = Path(__file__).resolve().parents[1] + completed = subprocess.run( + ["node", "--test", "scripts/cf_worker_security.test.mjs"], + cwd=project_root, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=30, + check=False, + ) + self.assertEqual(completed.returncode, 0, (completed.stdout or "") + (completed.stderr or "")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ui_smoke_suite.py b/tests/ui_smoke_suite.py index 4f17543..c36b395 100644 --- a/tests/ui_smoke_suite.py +++ b/tests/ui_smoke_suite.py @@ -60,6 +60,24 @@ def _wait_for_http(url: str, *, timeout: float = 45.0) -> None: raise RuntimeError(f"Timed out waiting for {url}: {last_error}") +def _terminate_process_tree(process: subprocess.Popen[str] | None, *, timeout: float = 10.0) -> None: + if process is None or process.poll() is not None: + return + if os.name == "nt": + subprocess.run( + ["taskkill", "/PID", str(process.pid), "/T", "/F"], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + else: + process.terminate() + try: + process.wait(timeout=timeout) + except subprocess.TimeoutExpired: + process.kill() + + def _seed_capture(workspace_dir: Path, capture: CaptureModel) -> None: capture_path = workspace_dir / "captures" / capture.id / "capture.json" capture_path.parent.mkdir(parents=True, exist_ok=True) @@ -138,6 +156,14 @@ def _video_capture(workspace_dir: Path, capture_id: str, *, created_at: str, tit mime_type="video/mp4", content=b"fake-source-video", ), + _artifact( + workspace_dir, + capture_id, + "source_audio", + "source_audio.m4a", + mime_type="audio/mp4", + content=b"fake-source-audio", + ), ] return CaptureModel( id=capture_id, @@ -188,6 +214,72 @@ def _video_capture(workspace_dir: Path, capture_id: str, *, created_at: str, tit ) +def _audio_capture(workspace_dir: Path, capture_id: str, *, created_at: str, title: str) -> CaptureModel: + primary_text = "小宇宙音频样例已经转写完成。\n\n这里保留两段正文,用于验证音频下载入口不会和视频下载混在一起。" + artifacts = [ + _artifact( + workspace_dir, + capture_id, + "txt", + "capture.txt", + mime_type="text/plain", + content=primary_text.encode("utf-8"), + ), + _artifact( + workspace_dir, + capture_id, + "source_audio", + "episode.m4a", + mime_type="audio/mp4", + content=b"fake-podcast-audio", + ), + ] + return CaptureModel( + id=capture_id, + input_type="url", + status="done", + title=title, + source_platform="xiaoyuzhou", + content_type="audio", + url=f"https://example.com/{capture_id}", + created_at=created_at, + updated_at=created_at, + started_at=created_at, + finished_at=created_at, + source=SourceMetaModel( + platform="xiaoyuzhou", + content_type="audio", + canonical_url=f"https://example.com/{capture_id}", + author="Praxis AI", + duration_seconds=128, + description="音频交付样例。", + extractor_used="fixture", + ), + processing=ProcessingStateModel( + current_stage="completed", + progress_percent=100, + progress_detail="整理完成", + completed_stages=["resolve", "extract", "transcribe", "compose"], + ), + result=ResultDocumentModel( + primary_text=primary_text, + primary_result_type="transcript", + confidence="high", + completeness="full", + transcript_status="transcribed", + text_source="transcript", + views=ResultViewsModel(primary=primary_text), + content_facts=[ + {"key": "title", "label": "标题", "value": title}, + {"key": "platform", "label": "平台", "value": "小宇宙"}, + {"key": "content_type", "label": "内容类型", "value": "音频"}, + {"key": "duration", "label": "时长", "value": "2 分钟"}, + ], + artifacts=artifacts, + ), + ) + + def _image_capture(workspace_dir: Path, capture_id: str, *, created_at: str, title: str) -> CaptureModel: artifacts = [ _artifact( @@ -357,6 +449,7 @@ def _seed_workspace(workspace_dir: Path) -> None: captures = [ _video_capture(workspace_dir, "demovideo001", created_at="2026-04-11 10:00:00", title="你只能靠自己得到自己想要的情感共鸣 语录"), + _audio_capture(workspace_dir, "demoaudio001", created_at="2026-04-11 09:59:30", title="蚂蚁测试 AI 版支付宝,市场监管总局约谈山姆"), _image_capture(workspace_dir, "demoimage001", created_at="2026-04-11 09:59:00", title="图文整理结果样例"), _simple_capture( workspace_dir, @@ -411,6 +504,8 @@ def setUpClass(cls) -> None: cls.tempdir = tempfile.TemporaryDirectory() cls.workspace_dir = Path(cls.tempdir.name) / "workspace" _seed_workspace(cls.workspace_dir) + cls.server_log_path = Path(cls.tempdir.name) / "ui-smoke-server.log" + cls.server_log = cls.server_log_path.open("w", encoding="utf-8") cls.port = _free_port() cls.base_url = f"http://127.0.0.1:{cls.port}" @@ -433,7 +528,7 @@ def setUpClass(cls) -> None: [str(_python_executable()), "-m", "scripts.start_api"], cwd=str(cls.project_root), env=env, - stdout=subprocess.PIPE, + stdout=cls.server_log, stderr=subprocess.STDOUT, text=True, ) @@ -443,17 +538,14 @@ def setUpClass(cls) -> None: cls.playwright = sync_playwright().start() cls.browser = cls.playwright.chromium.launch(headless=True) except Exception as exc: + cls.server_log.flush() logs = "" - if cls.server_process.stdout is not None: - try: - logs = cls.server_process.stdout.read() - except Exception: # pragma: no cover - best effort on startup failure - logs = "" - cls.server_process.terminate() try: - cls.server_process.wait(timeout=10) - except subprocess.TimeoutExpired: # pragma: no cover - shutdown fallback - cls.server_process.kill() + logs = cls.server_log_path.read_text(encoding="utf-8") + except Exception: # pragma: no cover - best effort on startup failure + logs = "" + _terminate_process_tree(cls.server_process) + cls.server_log.close() cls.tempdir.cleanup() raise RuntimeError(f"UI smoke server failed to start: {exc}\n{logs}") from exc @@ -464,16 +556,18 @@ def tearDownClass(cls) -> None: if hasattr(cls, "playwright"): cls.playwright.stop() if hasattr(cls, "server_process"): - cls.server_process.terminate() - try: - cls.server_process.wait(timeout=10) - except subprocess.TimeoutExpired: # pragma: no cover - shutdown fallback - cls.server_process.kill() + _terminate_process_tree(cls.server_process) + if hasattr(cls, "server_log"): + cls.server_log.close() if hasattr(cls, "tempdir"): cls.tempdir.cleanup() def setUp(self) -> None: - self.page = self.browser.new_page(viewport={"width": 1512, "height": 940}) + _seed_workspace(self.workspace_dir) + self.context = self.browser.new_context(viewport={"width": 1512, "height": 940}) + self.page = self.context.new_page() + self.page.set_default_timeout(15000) + self.page.set_default_navigation_timeout(30000) def tearDown(self) -> None: try: @@ -490,11 +584,16 @@ def tearDown(self) -> None: except Exception: pass self.page.close() + self.context.close() def test_homepage_recent_history_is_stable(self) -> None: - self.page.goto(self.base_url, wait_until="networkidle") + self.page.goto(self.base_url, wait_until="domcontentloaded", timeout=30000) + self.page.locator("textarea").wait_for() self.assertTrue(self.page.locator("textarea").is_visible()) + self.page.locator(".recent-inline-card").first.wait_for() self.assertEqual(self.page.locator(".recent-inline-card").count(), 4) + self.assertGreaterEqual(self.page.locator(".recent-inline-preview").count(), 1) + self.assertGreaterEqual(self.page.locator(".recent-inline-foot").count(), 4) heights = [] for index in range(self.page.locator(".recent-inline-card").count()): @@ -519,6 +618,21 @@ def test_homepage_recent_history_is_stable(self) -> None: self.page.wait_for_timeout(200) self.assertTrue(self.page.locator(".recent-inline-card").filter(has_text=first_title).first.is_visible()) + def test_mobile_deep_link_uses_full_result_width(self) -> None: + self.context.close() + self.context = self.browser.new_context(viewport={"width": 390, "height": 844}) + self.page = self.context.new_page() + self.page.set_default_timeout(15000) + self.page.goto(f"{self.base_url}/c/demoimage001", wait_until="domcontentloaded") + self.page.locator(".deliverable-workspace").wait_for() + self.page.wait_for_timeout(700) + + self.assertEqual(self.page.locator(".main-stage > .stage-shell").count(), 1) + stage_box = self.page.locator(".stage-shell-ready").bounding_box() + self.assertIsNotNone(stage_box) + self.assertGreaterEqual(stage_box["width"], 360) + self.assertLessEqual(self.page.evaluate("document.documentElement.scrollWidth"), 390) + def test_submit_flow_recovers_from_transient_capture_read(self) -> None: intercepted = {"count": 0} @@ -548,7 +662,8 @@ def flaky_capture_read(route) -> None: self.page.route("**/v1/captures", stable_capture_create) self.page.route("**/v1/captures/*", flaky_capture_read) - self.page.goto(self.base_url, wait_until="networkidle") + self.page.goto(self.base_url, wait_until="domcontentloaded", timeout=30000) + self.page.locator("textarea").wait_for() self.page.locator("textarea").fill( "https://www.xiaohongshu.com/discovery/item/66f61f96000000001d03f12d" ) @@ -563,7 +678,7 @@ def flaky_capture_read(route) -> None: self.page.unroute("**/v1/captures", stable_capture_create) def test_video_result_layout_stays_stable(self) -> None: - self.page.goto(f"{self.base_url}/c/demovideo001", wait_until="commit", timeout=60000) + self.page.goto(f"{self.base_url}/c/demovideo001", wait_until="domcontentloaded", timeout=60000) self.page.locator(".result-content-shell").wait_for() hero_metrics = self.page.locator(".deliverable-hero h1").evaluate( @@ -613,6 +728,8 @@ def test_video_result_layout_stays_stable(self) -> None: source_link = self.page.get_by_role("link", name="查看来源") self.assertTrue(source_link.is_visible()) self.assertEqual(source_link.get_attribute("target"), "_blank") + self.assertTrue(self.page.get_by_role("link", name="下载音频").is_visible()) + self.assertTrue(self.page.get_by_role("link", name="下载视频").is_visible()) button = self.page.locator(".result-actions .copy-action").first before_hover = button.evaluate( @@ -622,6 +739,15 @@ def test_video_result_layout_stays_stable(self) -> None: self.assertNotIn("0, 0, 0, 0", before_hover["borderColor"]) self.assertNotEqual(before_hover["color"], "rgba(0, 0, 0, 0)") + def test_audio_result_download_action_renders(self) -> None: + self.page.goto(f"{self.base_url}/c/demoaudio001", wait_until="domcontentloaded", timeout=30000) + self.page.locator(".result-content-shell").wait_for() + + audio_action = self.page.get_by_role("link", name="下载音频") + self.assertTrue(audio_action.is_visible()) + self.assertIn("/artifacts/source_audio", audio_action.get_attribute("href") or "") + self.assertEqual(self.page.get_by_role("link", name="下载视频").count(), 0) + def test_image_result_actions_render(self) -> None: self.page.goto(f"{self.base_url}/c/demoimage001", wait_until="domcontentloaded") self.page.locator(".image-card").first.wait_for() @@ -687,6 +813,8 @@ def test_image_result_actions_render(self) -> None: self.page.wait_for_timeout(420) self.assertIn("2/2", self.page.locator(".image-viewer-toolbar-count").inner_text()) self.page.locator(".image-viewer-nav.is-prev").click(force=True) + self.page.locator(".image-viewer-transition-underlay").wait_for(state="attached") + self.assertGreaterEqual(self.page.locator(".image-viewer-transition-underlay").count(), 1) self.page.wait_for_timeout(180) self.assertIn("1/2", self.page.locator(".image-viewer-toolbar-count").inner_text()) self.page.get_by_role("button", name="图片", exact=True).click() @@ -719,7 +847,7 @@ def test_image_result_actions_render(self) -> None: self.page.locator(".image-card .image-card-media-button").first.click() self.page.locator(".image-viewer-overlay").wait_for() - self.page.get_by_role("button", name="Live").click() + self.page.get_by_role("button", name="Live", exact=True).click() self.page.wait_for_timeout(150) self.assertTrue(self.page.locator(".image-viewer-stage video").is_visible()) @@ -729,12 +857,15 @@ def test_image_result_actions_render(self) -> None: self.assertGreater(self.page.locator(".image-card a[href]").count(), 0) def test_mobile_result_actions_and_viewer_are_tappable(self) -> None: - page = self.browser.new_page( + context = self.browser.new_context( viewport={"width": 390, "height": 844}, is_mobile=True, has_touch=True, device_scale_factor=3, ) + page = context.new_page() + page.set_default_timeout(15000) + page.set_default_navigation_timeout(30000) try: page.goto(f"{self.base_url}/c/demoimage001", wait_until="domcontentloaded") page.locator(".image-card").first.wait_for() @@ -808,13 +939,14 @@ def test_mobile_result_actions_and_viewer_are_tappable(self) -> None: """() => ({ width: window.innerWidth, docWidth: document.documentElement.scrollWidth, - resultActionMaxRight: Math.max(...[...document.querySelectorAll(".result-actions > *")].map((el) => el.getBoundingClientRect().right)), - resultActionMinHeight: Math.min(...[...document.querySelectorAll(".result-actions > *")].map((el) => el.getBoundingClientRect().height)), + actionMaxRight: Math.max(...[...document.querySelectorAll(".result-actions > *, .media-panel-actions > *")].map((el) => el.getBoundingClientRect().right)), + actionMinHeight: Math.min(...[...document.querySelectorAll(".result-actions > *, .media-panel-actions > *")].map((el) => el.getBoundingClientRect().height)), })""" ) self.assertLessEqual(video_layout["docWidth"], video_layout["width"] + 2) - self.assertLessEqual(video_layout["resultActionMaxRight"], video_layout["width"] + 2) - self.assertGreaterEqual(video_layout["resultActionMinHeight"], 43.5) + self.assertLessEqual(video_layout["actionMaxRight"], video_layout["width"] + 2) + self.assertGreaterEqual(video_layout["actionMinHeight"], 43.5) + self.assertTrue(page.get_by_role("link", name="下载音频").is_visible()) finally: try: page.evaluate( @@ -830,6 +962,7 @@ def test_mobile_result_actions_and_viewer_are_tappable(self) -> None: except Exception: pass page.close() + context.close() if __name__ == "__main__": diff --git a/web/package-lock.json b/web/package-lock.json index 7624155..efec66c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,27 +8,29 @@ "name": "praxis-capture-web", "version": "0.1.0", "dependencies": { + "@tauri-apps/api": "^2.11.1", "lucide-react": "^1.7.0", "motion": "^12.38.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { + "@tauri-apps/cli": "^2.11.4", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.2.1", + "@vitejs/plugin-react": "5.2.0", "typescript": "^5.6.3", - "vite": "^4.5.5" + "vite": "7.3.6" } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -37,9 +39,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -47,21 +49,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@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", @@ -78,14 +80,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@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" @@ -95,14 +97,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@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" @@ -112,9 +114,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -122,29 +124,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -164,9 +166,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -174,9 +176,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "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==", "dev": true, "license": "MIT", "engines": { @@ -184,9 +186,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "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==", "dev": true, "license": "MIT", "engines": { @@ -194,27 +196,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -256,33 +258,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -290,23 +292,40 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -317,13 +336,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -334,13 +353,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -351,13 +370,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -368,13 +387,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -385,13 +404,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -402,13 +421,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -419,13 +438,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -436,13 +455,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -453,13 +472,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -470,13 +489,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -487,13 +506,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -504,13 +523,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -521,13 +540,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -538,13 +557,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -555,13 +574,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -572,13 +591,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -589,13 +625,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -606,13 +659,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -623,13 +693,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -640,13 +710,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -657,13 +727,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -674,7 +744,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@jridgewell/gen-mapping": { @@ -727,13 +797,664 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "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/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "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.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -779,6 +1500,13 @@ "@babel/types": "^7.28.2" } }, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -808,30 +1536,30 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "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.28.0", + "@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-beta.27", + "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "react-refresh": "^0.18.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", - "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -842,9 +1570,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -862,11 +1590,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -876,9 +1604,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001784", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", - "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", "dev": true, "funding": [ { @@ -929,16 +1657,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.330", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.330.tgz", - "integrity": "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==", + "version": "1.5.403", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz", + "integrity": "sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==", "dev": true, "license": "ISC" }, "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -946,31 +1674,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escalade": { @@ -983,6 +1715,24 @@ "node": ">=6" } }, + "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/framer-motion": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", @@ -1147,9 +1897,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -1166,11 +1916,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/picocolors": { "version": "1.1.1", @@ -1179,10 +1932,23 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -1200,7 +1966,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1234,9 +2000,9 @@ } }, "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", "engines": { @@ -1244,19 +2010,48 @@ } }, "node_modules/rollup": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", - "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", "dev": true, "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" } }, @@ -1289,6 +2084,23 @@ "node": ">=0.10.0" } }, + "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/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1310,9 +2122,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "dev": true, "funding": [ { @@ -1341,41 +2153,51 @@ } }, "node_modules/vite": { - "version": "4.5.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", - "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "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": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.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 }, @@ -1385,6 +2207,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -1393,6 +2218,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, diff --git a/web/package.json b/web/package.json index 3c55496..e97a22b 100644 --- a/web/package.json +++ b/web/package.json @@ -6,19 +6,24 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "tauri": "tauri", + "desktop:dev": "tauri dev", + "desktop:build": "tauri build" }, "dependencies": { + "@tauri-apps/api": "^2.11.1", "lucide-react": "^1.7.0", "motion": "^12.38.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { + "@tauri-apps/cli": "^2.11.4", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.2.1", + "@vitejs/plugin-react": "5.2.0", "typescript": "^5.6.3", - "vite": "^4.5.5" + "vite": "7.3.6" } } diff --git a/web/src-tauri/Cargo.lock b/web/src-tauri/Cargo.lock new file mode 100644 index 0000000..90b85e0 --- /dev/null +++ b/web/src-tauri/Cargo.lock @@ -0,0 +1,4495 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.20", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.20", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.4+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wanxiang-chengwen" +version = "2.0.0" +dependencies = [ + "serde", + "tauri", + "tauri-build", + "uuid", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.20", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/web/src-tauri/Cargo.toml b/web/src-tauri/Cargo.toml new file mode 100644 index 0000000..cf1ef74 --- /dev/null +++ b/web/src-tauri/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "wanxiang-chengwen" +version = "2.0.0" +description = "Wanxiang Chengwen Windows desktop application" +authors = ["Praxis AI"] +license = "Apache-2.0" +repository = "https://github.com/qinxujunai/Audio2Text" +edition = "2021" +rust-version = "1.84" + +[lib] +name = "wanxiang_chengwen_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.5.3", features = [] } + +[dependencies] +serde = { version = "1", features = ["derive"] } +tauri = { version = "2.11.5", features = ["tray-icon"] } +uuid = { version = "1", features = ["v4"] } + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.61", features = ["Win32_System_Threading"] } diff --git a/web/src-tauri/app-icon.png b/web/src-tauri/app-icon.png new file mode 100644 index 0000000..944bf51 Binary files /dev/null and b/web/src-tauri/app-icon.png differ diff --git a/web/src-tauri/build.rs b/web/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/web/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/web/src-tauri/capabilities/default.json b/web/src-tauri/capabilities/default.json new file mode 100644 index 0000000..016ac8d --- /dev/null +++ b/web/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main-capability", + "description": "Minimum permissions for the Wanxiang Chengwen main window", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/web/src-tauri/icons/128x128.png b/web/src-tauri/icons/128x128.png new file mode 100644 index 0000000..5ae9641 Binary files /dev/null and b/web/src-tauri/icons/128x128.png differ diff --git a/web/src-tauri/icons/128x128@2x.png b/web/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..fa946bb Binary files /dev/null and b/web/src-tauri/icons/128x128@2x.png differ diff --git a/web/src-tauri/icons/32x32.png b/web/src-tauri/icons/32x32.png new file mode 100644 index 0000000..2a23bc5 Binary files /dev/null and b/web/src-tauri/icons/32x32.png differ diff --git a/web/src-tauri/icons/icon.ico b/web/src-tauri/icons/icon.ico new file mode 100644 index 0000000..5ef9d8b Binary files /dev/null and b/web/src-tauri/icons/icon.ico differ diff --git a/web/src-tauri/src/lib.rs b/web/src-tauri/src/lib.rs new file mode 100644 index 0000000..bd150d9 --- /dev/null +++ b/web/src-tauri/src/lib.rs @@ -0,0 +1,268 @@ +use serde::Serialize; +use std::{ + net::{SocketAddr, TcpListener, TcpStream}, + path::{Path, PathBuf}, + process::{Child, Command, Stdio}, + sync::Mutex, + thread, + time::{Duration, Instant}, +}; +use tauri::{ + menu::{Menu, MenuItem}, + tray::TrayIconBuilder, + AppHandle, Emitter, Manager, State, +}; + +#[cfg(windows)] +use std::os::windows::process::CommandExt; + +const CREATE_NO_WINDOW: u32 = 0x0800_0000; + +#[derive(Clone, Serialize)] +struct DesktopRuntimeInfo { + api_base: String, + token: String, +} + +#[derive(Default)] +struct DesktopState { + runtime: Mutex>, + child: Mutex>, + background_task_active: Mutex, +} + +fn choose_loopback_port() -> Result { + TcpListener::bind(("127.0.0.1", 0)) + .and_then(|listener| listener.local_addr()) + .map(|address| address.port()) + .map_err(|error| format!("无法分配本地服务端口:{error}")) +} + +fn project_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("..") +} + +fn sidecar_command(app: &AppHandle) -> Result<(Command, PathBuf), String> { + if cfg!(debug_assertions) { + let root = project_root(); + let python = root.join(".venv").join("Scripts").join("python.exe"); + if !python.is_file() { + return Err("开发环境缺少可用的项目 Python。".into()); + } + let mut command = Command::new(python); + command.args(["-m", "scripts.start_api"]); + return Ok((command, root)); + } + + let resource_dir = app + .path() + .resource_dir() + .map_err(|error| format!("无法定位应用资源目录:{error}"))?; + let sidecar_dir = resource_dir.join("sidecar").join("wanxiang-api"); + let executable = sidecar_dir.join("wanxiang-api.exe"); + if !executable.is_file() { + return Err("应用运行组件不完整,请重新安装万象成文。".into()); + } + Ok((Command::new(executable), sidecar_dir)) +} + +fn spawn_backend(app: &AppHandle) -> Result<(Child, DesktopRuntimeInfo), String> { + let port = choose_loopback_port()?; + let token = uuid::Uuid::new_v4().simple().to_string(); + let (mut command, working_dir) = sidecar_command(app)?; + let resource_dir = app + .path() + .resource_dir() + .map_err(|error| format!("无法定位应用资源目录:{error}"))?; + let workspace_dir = app + .path() + .app_local_data_dir() + .map_err(|error| format!("无法定位应用数据目录:{error}"))? + .join("workspace"); + command + .current_dir(working_dir) + .env("AUDIO2TEXT_API_HOST", "127.0.0.1") + .env("AUDIO2TEXT_API_PORT", port.to_string()) + .env("AUDIO2TEXT_RUNTIME_TARGET", "windows_desktop") + .env( + "AUDIO2TEXT_DESKTOP_PARENT_PID", + std::process::id().to_string(), + ) + .env("AUDIO2TEXT_TRANSCRIPTION_PROVIDER", "auto") + .env("AUDIO2TEXT_DESKTOP_TOKEN", &token) + .env("AUDIO2TEXT_WORKSPACE_DIR", workspace_dir) + .env( + "AUDIO2TEXT_RUNTIME_PACK_MANIFEST", + resource_dir.join("runtime-packs.json"), + ) + .env( + "AUDIO2TEXT_ALLOWED_ORIGINS", + "http://tauri.localhost,https://tauri.localhost,tauri://localhost", + ) + .env("AUDIO2TEXT_ALLOW_DEGRADED_START", "1") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + #[cfg(windows)] + command.creation_flags(CREATE_NO_WINDOW); + + let child = command + .spawn() + .map_err(|error| format!("无法启动本地处理服务:{error}"))?; + let runtime = DesktopRuntimeInfo { + api_base: format!("http://127.0.0.1:{port}"), + token, + }; + Ok((child, runtime)) +} + +fn wait_for_backend(runtime: &DesktopRuntimeInfo, timeout: Duration) -> bool { + let address: SocketAddr = match runtime.api_base.trim_start_matches("http://").parse() { + Ok(address) => address, + Err(_) => return false, + }; + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if TcpStream::connect_timeout(&address, Duration::from_millis(200)).is_ok() { + return true; + } + thread::sleep(Duration::from_millis(100)); + } + false +} + +#[tauri::command] +fn desktop_runtime_info(state: State<'_, DesktopState>) -> Result { + state + .runtime + .lock() + .map_err(|_| "桌面运行状态暂时不可用。".to_string())? + .clone() + .ok_or_else(|| "本地处理服务尚未准备完成。".to_string()) +} + +#[tauri::command] +fn diagnostics_folder(app: AppHandle) -> Result { + app.path() + .app_local_data_dir() + .map(|path| path.join("workspace").join("logs").display().to_string()) + .map_err(|error| format!("无法定位诊断目录:{error}")) +} + +fn stop_backend(state: &DesktopState) { + if let Ok(mut guard) = state.child.lock() { + if let Some(child) = guard.as_mut() { + let _ = child.kill(); + let _ = child.wait(); + } + *guard = None; + } +} + +#[tauri::command] +fn restart_backend(app: AppHandle, state: State<'_, DesktopState>) -> Result<(), String> { + stop_backend(&state); + let (mut child, runtime) = spawn_backend(&app)?; + if !wait_for_backend(&runtime, Duration::from_secs(8)) { + let _ = child.kill(); + let _ = child.wait(); + return Err("本地处理服务重新启动超时。".into()); + } + *state.child.lock().map_err(|_| "本地服务状态不可用。")? = Some(child); + *state.runtime.lock().map_err(|_| "本地运行状态不可用。")? = Some(runtime); + Ok(()) +} + +#[tauri::command] +fn set_background_task_active(active: bool, state: State<'_, DesktopState>) -> Result<(), String> { + *state + .background_task_active + .lock() + .map_err(|_| "任务状态不可用。")? = active; + Ok(()) +} + +pub fn run() { + tauri::Builder::default() + .manage(DesktopState::default()) + .invoke_handler(tauri::generate_handler![ + desktop_runtime_info, + diagnostics_folder, + restart_backend, + set_background_task_active + ]) + .setup(|app| { + let open_item = MenuItem::with_id(app, "open", "打开万象成文", true, None::<&str>)?; + let diagnostics_item = + MenuItem::with_id(app, "diagnostics", "导出诊断", true, None::<&str>)?; + let quit_item = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&open_item, &diagnostics_item, &quit_item])?; + TrayIconBuilder::new() + .icon(app.default_window_icon().expect("application icon").clone()) + .tooltip("万象成文") + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(|app, event| match event.id.as_ref() { + "open" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + "diagnostics" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.emit("desktop:diagnostics-requested", ()); + } + } + "quit" => app.exit(0), + _ => {} + }) + .build(app)?; + + let (child, runtime) = spawn_backend(app.handle()).map_err(std::io::Error::other)?; + if !wait_for_backend(&runtime, Duration::from_secs(5)) { + return Err( + std::io::Error::other("本地处理服务启动超时,请导出诊断后重新启动。").into(), + ); + } + let state = app.state::(); + *state + .child + .lock() + .map_err(|_| std::io::Error::other("backend child lock"))? = Some(child); + *state + .runtime + .lock() + .map_err(|_| std::io::Error::other("runtime lock"))? = Some(runtime); + if let Some(window) = app.get_webview_window("main") { + window.show()?; + window.set_focus()?; + } + Ok(()) + }) + .on_window_event(|window, event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + let state = window.state::(); + let has_active_task = state + .background_task_active + .lock() + .map(|guard| *guard) + .unwrap_or(false); + if has_active_task { + api.prevent_close(); + let _ = window.hide(); + } + } + }) + .build(tauri::generate_context!()) + .expect("failed to build Wanxiang Chengwen") + .run(|app, event| { + if matches!( + event, + tauri::RunEvent::Exit | tauri::RunEvent::ExitRequested { .. } + ) { + let state = app.state::(); + stop_backend(&state); + } + }); +} diff --git a/web/src-tauri/src/main.rs b/web/src-tauri/src/main.rs new file mode 100644 index 0000000..8e0803a --- /dev/null +++ b/web/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + wanxiang_chengwen_lib::run(); +} diff --git a/web/src-tauri/tauri.conf.json b/web/src-tauri/tauri.conf.json new file mode 100644 index 0000000..97e5ac3 --- /dev/null +++ b/web/src-tauri/tauri.conf.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "万象成文", + "version": "2.0.0", + "identifier": "online.praxisai.wanxiang", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:5173", + "beforeBuildCommand": "npm run build", + "frontendDist": "../../frontend/dist" + }, + "app": { + "security": { + "csp": "default-src 'self'; connect-src 'self' http://127.0.0.1:*; img-src 'self' asset: https: data: blob:; media-src 'self' http://127.0.0.1:* https: blob:; style-src 'self' 'unsafe-inline'; script-src 'self'" + }, + "windows": [ + { + "label": "main", + "title": "万象成文", + "width": 1280, + "height": 820, + "minWidth": 980, + "minHeight": 680, + "center": true, + "visible": false, + "resizable": true + } + ] + }, + "bundle": { + "active": true, + "targets": ["nsis"], + "publisher": "Praxis AI", + "copyright": "Copyright 2026 Praxis AI", + "category": "Productivity", + "shortDescription": "把公开链接或本地音视频整理成可阅读、可下载的文字。", + "longDescription": "万象成文是一款本地优先的内容整理工具,支持公开视频、播客、图文和本地音视频文件。", + "resources": { + "sidecar/": "sidecar/", + "../../runtime-packs.json": "runtime-packs.json", + "../../NOTICE": "NOTICE", + "../../THIRD_PARTY_NOTICES.md": "THIRD_PARTY_NOTICES.md" + }, + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.ico"] + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx index 3d86ebe..93b23ca 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -10,13 +10,17 @@ import { getCapture, getCaptureWithRetry, getConfig, + installRuntimePack, listCaptures, + listRuntimePacks, + restartDesktopRuntime, } from "./api"; import { DeliverableStage } from "./components/DeliverableStage"; import { Header } from "./components/Header"; import { InputStage } from "./components/InputStage"; import { ProcessingStage } from "./components/ProcessingStage"; -import type { CaptureEnvelope, CaptureListItem, ConfigResponse } from "./types"; +import { RuntimeSetup } from "./components/RuntimeSetup"; +import type { CaptureEnvelope, CaptureListItem, ConfigResponse, RuntimePack } from "./types"; type AppPhase = "idle" | "processing" | "done" | "failed"; type PendingRemoval = { @@ -52,14 +56,21 @@ function stageLabel(stage?: string | null) { function processingDescription(capture: CaptureEnvelope | null) { const stage = capture?.capture.current_stage || capture?.capture.status || ""; - if (stage === "resolve") return "\u6B63\u5728\u8BC6\u522B\u6765\u6E90\u548C\u5185\u5BB9\u7C7B\u578B\u3002"; - if (stage === "extract") return "\u6B63\u5728\u63D0\u53D6\u53EF\u4EA4\u4ED8\u7684\u6B63\u6587\u3001\u5B57\u5E55\u3001\u5A92\u4F53\u6216\u56FE\u7247\u3002"; - if (stage === "transcribe") return "\u6B63\u5728\u8F6C\u5199\u97F3\u9891\u5185\u5BB9\uFF0C\u8BF7\u4FDD\u6301\u5F53\u524D\u9875\u9762\u3002"; - if (stage === "compose") return "\u6B63\u5728\u6574\u7406\u6700\u7EC8\u7ED3\u679C\u3002"; + if (stage === "resolve") + return "\u6B63\u5728\u8BC6\u522B\u6765\u6E90\u548C\u5185\u5BB9\u7C7B\u578B\u3002"; + if (stage === "extract") + return "\u6B63\u5728\u63D0\u53D6\u53EF\u4EA4\u4ED8\u7684\u6B63\u6587\u3001\u5B57\u5E55\u3001\u5A92\u4F53\u6216\u56FE\u7247\u3002"; + if (stage === "transcribe") + return "\u6B63\u5728\u8F6C\u5199\u97F3\u9891\u5185\u5BB9\uFF0C\u8BF7\u4FDD\u6301\u5F53\u524D\u9875\u9762\u3002"; + if (stage === "compose") + return "\u6B63\u5728\u6574\u7406\u6700\u7EC8\u7ED3\u679C\u3002"; return "\u8BF7\u4FDD\u6301\u5F53\u524D\u9875\u9762\uFF0C\u6211\u4EEC\u4F1A\u81EA\u52A8\u5B8C\u6210\u6574\u7406\u3002"; } -function errorMessageForCapture(capture: CaptureEnvelope | null, workspaceError: string | null) { +function errorMessageForCapture( + capture: CaptureEnvelope | null, + workspaceError: string | null, +) { if (workspaceError) return workspaceError; const backendMessage = capture?.capture.error_message?.trim() || ""; @@ -89,7 +100,9 @@ function errorMessageForCapture(capture: CaptureEnvelope | null, workspaceError: export default function App() { const [config, setConfig] = useState(null); const [captures, setCaptures] = useState([]); - const [currentCapture, setCurrentCapture] = useState(null); + const [currentCapture, setCurrentCapture] = useState( + null, + ); const [input, setInput] = useState(""); const [selectedFile, setSelectedFile] = useState(null); const [submitting, setSubmitting] = useState(false); @@ -98,8 +111,12 @@ export default function App() { const [loadingDeepLink, setLoadingDeepLink] = useState(false); const [toast, setToast] = useState(""); const [workspaceError, setWorkspaceError] = useState(null); + const [runtimePacks, setRuntimePacks] = useState([]); + const [runtimeBusy, setRuntimeBusy] = useState(false); + const [runtimeError, setRuntimeError] = useState(""); const [pendingRemovals, setPendingRemovals] = useState([]); - const [pendingHistoryClear, setPendingHistoryClear] = useState(null); + const [pendingHistoryClear, setPendingHistoryClear] = + useState(null); const initialPathRef = useRef(window.location.pathname); const pendingRemovalTimersRef = useRef>({}); const pendingHistoryClearTimerRef = useRef(null); @@ -108,7 +125,8 @@ export default function App() { if (workspaceError) return "failed"; if (submitting && !currentCapture) return "processing"; if (!currentCapture) return "idle"; - if (["queued", "processing"].includes(currentCapture.capture.status)) return "processing"; + if (["queued", "processing"].includes(currentCapture.capture.status)) + return "processing"; if (currentCapture.capture.status === "failed") return "failed"; if (currentCapture.capture.status === "done") return "done"; return "idle"; @@ -124,13 +142,31 @@ export default function App() { useEffect(() => { const match = initialPathRef.current.match(/^\/c\/([^/]+)$/); const bootstrap = async () => { - // Load config and history in background \u2014 don't block the UI + const configRequest = getConfig(); + const historyRequest = listCaptures(); + try { - const [runtimeConfig, history] = await Promise.all([getConfig(), listCaptures()]); + const runtimeConfig = await configRequest; setConfig(runtimeConfig); + if (runtimeConfig.runtime_target === "windows_desktop") { + try { + const runtime = await listRuntimePacks(); + setRuntimePacks( + runtime.packs.filter((item) => item.required && !item.installed), + ); + } catch { + setRuntimeError("本地识别组件暂时无法读取,请重新打开应用后再试。"); + } + } + } catch { + setWorkspaceError("本地处理服务没有准备好,请重新打开应用后再试。"); + } + + try { + const history = await historyRequest; setCaptures(history.items || []); } catch { - // Config/history load failed \u2014 UI still works with defaults + // History is optional during startup; capture submission remains available. } if (match?.[1]) { @@ -146,6 +182,21 @@ export default function App() { void bootstrap(); }, []); + async function prepareDesktopRuntime() { + if (!runtimePacks.length || runtimeBusy) return; + setRuntimeBusy(true); + setRuntimeError(""); + try { + for (const pack of runtimePacks) { + await installRuntimePack(pack.id); + } + await restartDesktopRuntime(); + } catch (error) { + setRuntimeError(error instanceof Error ? error.message : "安装没有完成,请检查网络后重试。"); + setRuntimeBusy(false); + } + } + useEffect(() => { if (bootstrapping) return; if (!currentCapture) { @@ -163,7 +214,9 @@ export default function App() { useEffect( () => () => { - Object.values(pendingRemovalTimersRef.current).forEach((timerId) => window.clearTimeout(timerId)); + Object.values(pendingRemovalTimersRef.current).forEach((timerId) => + window.clearTimeout(timerId), + ); pendingRemovalTimersRef.current = {}; if (pendingHistoryClearTimerRef.current) { window.clearTimeout(pendingHistoryClearTimerRef.current); @@ -188,11 +241,18 @@ export default function App() { return () => document.body.classList.remove(className); }, [phase]); + useEffect(() => { + void import("@tauri-apps/api/core") + .then(({ invoke }) => invoke("set_background_task_active", { active: phase === "processing" })) + .catch(() => undefined); + }, [phase]); + useEffect(() => { const shouldTrackCapture = !!currentCapture && (["queued", "processing"].includes(currentCapture.capture.status) || - (currentCapture.capture.status === "done" && currentCapture.capture.asset_preparation_pending)); + (currentCapture.capture.status === "done" && + currentCapture.capture.asset_preparation_pending)); if (!currentCapture || !shouldTrackCapture) return undefined; let active = true; @@ -208,7 +268,8 @@ export default function App() { setCurrentCapture(nextCapture); if ( nextCapture.capture.status === "failed" || - (nextCapture.capture.status === "done" && !nextCapture.capture.asset_preparation_pending) + (nextCapture.capture.status === "done" && + !nextCapture.capture.asset_preparation_pending) ) { source.close(); void refreshHistory(); @@ -221,12 +282,15 @@ export default function App() { }; source.addEventListener("capture.updated", (event) => { - const payload = JSON.parse((event as MessageEvent).data) as { payload: CaptureEnvelope }; + const payload = JSON.parse((event as MessageEvent).data) as { + payload: CaptureEnvelope; + }; const nextCapture = payload.payload; setCurrentCapture(nextCapture); if ( nextCapture.capture.status === "failed" || - (nextCapture.capture.status === "done" && !nextCapture.capture.asset_preparation_pending) + (nextCapture.capture.status === "done" && + !nextCapture.capture.asset_preparation_pending) ) { source.close(); void refreshHistory(); @@ -246,7 +310,11 @@ export default function App() { source.close(); window.clearInterval(pollTimer); }; - }, [currentCapture?.capture.id, currentCapture?.capture.status, currentCapture?.capture.asset_preparation_pending]); + }, [ + currentCapture?.capture.id, + currentCapture?.capture.status, + currentCapture?.capture.asset_preparation_pending, + ]); function requestStatus(error: unknown) { if (!(error instanceof Error)) return undefined; @@ -262,10 +330,14 @@ export default function App() { async function finalizePendingRemoval(removal: PendingRemoval) { delete pendingRemovalTimersRef.current[removal.token]; - setPendingRemovals((items) => items.filter((item) => item.token !== removal.token)); + setPendingRemovals((items) => + items.filter((item) => item.token !== removal.token), + ); try { await deleteCapture(removal.item.id); - setCaptures((items) => items.filter((item) => item.id !== removal.item.id)); + setCaptures((items) => + items.filter((item) => item.id !== removal.item.id), + ); await refreshHistory(); } catch (error) { setToast(error instanceof Error ? error.message : "删除记录失败。"); @@ -291,14 +363,21 @@ export default function App() { try { const result = await clearCompletedCaptures(); await refreshHistory(); - if (pending.currentCapture && ["done", "failed"].includes(pending.currentCapture.capture.status)) { + if ( + pending.currentCapture && + ["done", "failed"].includes(pending.currentCapture.capture.status) + ) { resetWorkspace(); } setToast(`\u5DF2\u6E05\u7A7A ${result.cleared} \u6761\u8BB0\u5F55\u3002`); } catch (error) { setCaptures(pending.captures); setCurrentCapture(pending.currentCapture); - setToast(error instanceof Error ? error.message : "\u6E05\u7A7A\u8BB0\u5F55\u5931\u8D25\u3002"); + setToast( + error instanceof Error + ? error.message + : "\u6E05\u7A7A\u8BB0\u5F55\u5931\u8D25\u3002", + ); } finally { setClearBusy(false); } @@ -368,8 +447,6 @@ export default function App() { setToast("请长按输入框粘贴链接。"); } - - async function submitText() { if (!input.trim()) { setToast("\u5148\u8D34\u5165\u4E00\u4E2A\u516C\u5F00\u94FE\u63A5\u3002"); @@ -401,7 +478,9 @@ export default function App() { async function submitFile() { if (!selectedFile) { - setToast("\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u672C\u5730\u6587\u4EF6\u3002"); + setToast( + "\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u672C\u5730\u6587\u4EF6\u3002", + ); return; } @@ -413,7 +492,11 @@ export default function App() { await refreshHistory(); setSelectedFile(null); } catch (error) { - setToast(error instanceof Error ? error.message : "\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25\u3002"); + setToast( + error instanceof Error + ? error.message + : "\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25\u3002", + ); } finally { setSubmitting(false); } @@ -421,13 +504,19 @@ export default function App() { async function clearHistory() { if (pendingHistoryClear) return; - const removableItems = captures.filter((item) => ["done", "failed"].includes(item.status)); + const removableItems = captures.filter((item) => + ["done", "failed"].includes(item.status), + ); if (!removableItems.length) { - setToast("\u6682\u65F6\u6CA1\u6709\u53EF\u6E05\u7A7A\u7684\u8BB0\u5F55\u3002"); + setToast( + "\u6682\u65F6\u6CA1\u6709\u53EF\u6E05\u7A7A\u7684\u8BB0\u5F55\u3002", + ); return; } - Object.values(pendingRemovalTimersRef.current).forEach((timerId) => window.clearTimeout(timerId)); + Object.values(pendingRemovalTimersRef.current).forEach((timerId) => + window.clearTimeout(timerId), + ); pendingRemovalTimersRef.current = {}; setPendingRemovals([]); @@ -437,7 +526,9 @@ export default function App() { currentCapture, removedCount: removableItems.length, }; - setCaptures((items) => items.filter((item) => !["done", "failed"].includes(item.status))); + setCaptures((items) => + items.filter((item) => !["done", "failed"].includes(item.status)), + ); setPendingHistoryClear(pending); pendingHistoryClearTimerRef.current = window.setTimeout(() => { void finalizePendingHistoryClear(pending); @@ -448,7 +539,10 @@ export default function App() { if (item.status === "failed") return false; const title = (item.title || "").trim().toLowerCase(); const preview = (item.preview_text || "").trim().toLowerCase(); - if (item.source_platform === "xiaohongshu" && (/xhslink\.com/.test(title) || /xhslink\.com/.test(preview))) { + if ( + item.source_platform === "xiaohongshu" && + (/xhslink\.com/.test(title) || /xhslink\.com/.test(preview)) + ) { return false; } return true; @@ -482,37 +576,78 @@ export default function App() { } const headerStatus = loadingDeepLink ? "processing" : phase; - const pendingRemovalIds = useMemo(() => new Set(pendingRemovals.map((item) => item.item.id)), [pendingRemovals]); + const pendingRemovalIds = useMemo( + () => new Set(pendingRemovals.map((item) => item.item.id)), + [pendingRemovals], + ); const recentCaptures = useMemo( () => captures - .filter((item) => shouldShowInRecent(item) && !pendingRemovalIds.has(item.id)) + .filter( + (item) => shouldShowInRecent(item) && !pendingRemovalIds.has(item.id), + ) .slice(0, config?.capture_history_limit || 12), [captures, config?.capture_history_limit, pendingRemovalIds], ); const supportedExtensions = useMemo( - () => (config?.supported_extensions || []).map((item) => item.replace(/^\./, "").toUpperCase()), + () => + (config?.supported_extensions || []).map((item) => + item.replace(/^\./, "").toUpperCase(), + ), [config?.supported_extensions], ); const processingCopy = { - eyebrow: phase === "failed" ? "\u5904\u7406\u5931\u8D25" : "\u6B63\u5728\u63D0\u53D6\u5185\u5BB9", + eyebrow: + phase === "failed" + ? "\u5904\u7406\u5931\u8D25" + : "\u6B63\u5728\u63D0\u53D6\u5185\u5BB9", title: phase === "failed" ? "\u8FD9\u6761\u5185\u5BB9\u6682\u65F6\u6CA1\u80FD\u5904\u7406\u6210\u529F" - : stageLabel(currentCapture?.capture.current_stage || currentCapture?.capture.status), + : stageLabel( + currentCapture?.capture.current_stage || + currentCapture?.capture.status, + ), description: phase === "failed" ? errorMessageForCapture(currentCapture, workspaceError) : processingDescription(currentCapture), }; + const runtimeSetupRequired = + !loadingDeepLink && + phase === "idle" && + config?.runtime_target === "windows_desktop" && + runtimePacks.length > 0; return (
-
+
- {phase === "idle" ? ( + {runtimeSetupRequired ? ( + + void prepareDesktopRuntime()} + /> + + ) : phase === "idle" && !loadingDeepLink ? ( ) : null} - {(loadingDeepLink || phase === "processing" || phase === "failed") && ( + {(loadingDeepLink || + phase === "processing" || + phase === "failed") && ( )} - {phase === "done" && currentCapture ? ( + {!loadingDeepLink && phase === "done" && currentCapture ? ( {`\u5DF2\u6E05\u7A7A ${pendingHistoryClear.removedCount} \u6761\u8BB0\u5F55`} - @@ -614,7 +788,11 @@ export default function App() { transition={{ duration: 0.2 }} > 已删除这条记录 - diff --git a/web/src/api.ts b/web/src/api.ts index e595385..b60208d 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -3,15 +3,59 @@ import type { CaptureListResponse, ConfigResponse, CreateCaptureResponse, + RuntimePack, } from "./types"; -const API_BASE = window.location.origin.replace(/\/$/, ""); +type DesktopRuntime = { api_base: string; token: string }; + +const WEB_API_BASE = window.location.origin.replace(/\/$/, ""); +let desktopRuntime: DesktopRuntime | null = null; +let desktopRuntimePromise: Promise | null = null; const TEMPORARY_BUSY_MESSAGE = "服务暂时繁忙,请稍后重试。"; type RequestError = Error & { status?: number; }; +async function getRuntime(): Promise { + if (desktopRuntime) return desktopRuntime; + if (!("__TAURI_INTERNALS__" in window)) return null; + if (!desktopRuntimePromise) { + desktopRuntimePromise = import("@tauri-apps/api/core") + .then(async ({ invoke }) => { + let lastError: unknown = null; + for (let attempt = 0; attempt < 30; attempt++) { + try { + return await invoke("desktop_runtime_info"); + } catch (error) { + lastError = error; + await wait(100); + } + } + throw lastError; + }) + .then((runtime) => { + desktopRuntime = runtime; + return runtime; + }) + .catch((error) => { + desktopRuntimePromise = null; + throw error; + }); + } + return desktopRuntimePromise; +} + +export function resolveApiUrl(path: string, includeDesktopToken = false) { + const runtime = desktopRuntime; + const base = runtime?.api_base.replace(/\/$/, "") || WEB_API_BASE; + const url = new URL(path, `${base}/`); + if (includeDesktopToken && runtime?.token) { + url.searchParams.set("desktop_token", runtime.token); + } + return url.toString(); +} + function _sanitizeErrorMessage(raw: string): string { const trimmed = raw.trim(); if (!trimmed) return TEMPORARY_BUSY_MESSAGE; @@ -23,17 +67,31 @@ function _sanitizeErrorMessage(raw: string): string { return trimmed; } -async function request(path: string, options?: RequestInit, retries = 3): Promise { +async function request( + path: string, + options?: RequestInit, + retries = 3, +): Promise { let lastError: Error | null = null; for (let attempt = 0; attempt <= retries; attempt++) { try { - const response = await fetch(`${API_BASE}${path}`, options); + const runtime = await getRuntime(); + const headers = new Headers(options?.headers); + if (runtime?.token) { + headers.set("X-Wanxiang-Desktop-Token", runtime.token); + } + const response = await fetch(resolveApiUrl(path), { ...options, headers }); const contentType = response.headers.get("content-type") || ""; - const payload = contentType.includes("application/json") ? await response.json() : await response.text(); + const payload = contentType.includes("application/json") + ? await response.json() + : await response.text(); if (!response.ok) { // Retry on server errors (502/503/504) and rate limits (429) - if (attempt < retries && (response.status >= 502 || response.status === 429)) { + if ( + attempt < retries && + (response.status >= 502 || response.status === 429) + ) { await wait(500 * (attempt + 1)); continue; } @@ -44,7 +102,9 @@ async function request(path: string, options?: RequestInit, retries = 3): Pro ? payload.detail : JSON.stringify(payload); const safeMessage = _sanitizeErrorMessage(rawDetail || ""); - const error = new Error(safeMessage || `HTTP ${response.status}`) as RequestError; + const error = new Error( + safeMessage || `HTTP ${response.status}`, + ) as RequestError; error.status = response.status; throw error; } @@ -53,7 +113,12 @@ async function request(path: string, options?: RequestInit, retries = 3): Pro } catch (error: any) { lastError = error; // Retry on network errors - if (attempt < retries && (error.name === 'TypeError' || error.message?.includes('fetch') || error.message?.includes('NetworkError'))) { + if ( + attempt < retries && + (error.name === "TypeError" || + error.message?.includes("fetch") || + error.message?.includes("NetworkError")) + ) { await wait(500 * (attempt + 1)); continue; } @@ -134,17 +199,41 @@ export function clearCompletedCaptures() { } export function deleteCapture(captureId: string) { - return request<{ deleted: boolean; capture_id: string }>(`/v1/captures/${captureId}`, { - method: "DELETE", - }); + return request<{ deleted: boolean; capture_id: string }>( + `/v1/captures/${captureId}`, + { + method: "DELETE", + }, + ); } export function retryCapture(captureId: string) { - return request<{ capture_id: string; status: string }>(`/v1/captures/${captureId}/retry`, { - method: "POST", - }); + return request<{ capture_id: string; status: string }>( + `/v1/captures/${captureId}/retry`, + { + method: "POST", + }, + ); } export function captureEventsUrl(captureId: string) { - return `${API_BASE}/v1/captures/${captureId}/events`; + return resolveApiUrl(`/v1/captures/${captureId}/events`, true); +} + +export function listRuntimePacks() { + return request<{ packs: RuntimePack[] }>("/v1/runtime/packs"); +} + +export function installRuntimePack(packId: string) { + return request<{ id: string; version: string; installed: boolean }>( + `/v1/runtime/packs/${encodeURIComponent(packId)}/install`, + { method: "POST" }, + 0, + ); +} + +export async function restartDesktopRuntime() { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("restart_backend"); + window.location.reload(); } diff --git a/web/src/components/DeliverableStage.tsx b/web/src/components/DeliverableStage.tsx index 4315673..af10ab4 100644 --- a/web/src/components/DeliverableStage.tsx +++ b/web/src/components/DeliverableStage.tsx @@ -5,6 +5,7 @@ import { Download, Expand, ExternalLink, + Headphones, FileText, Film, Images, @@ -25,6 +26,7 @@ import { useState, } from "react"; +import { resolveApiUrl } from "../api"; import type { Artifact, CaptureEnvelope, CaptureSourceImage } from "../types"; type DeliverableStageProps = { @@ -42,6 +44,10 @@ type ImageGalleryItem = { liveArtifact: Artifact | null; }; type ViewerPan = { x: number; y: number }; +type ViewerVisual = { + key: string; + imageSrc: string; +}; function clampNumber(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); @@ -65,7 +71,7 @@ function bytesLabel(size?: number | null) { } function artifactUrl(artifact: Artifact) { - return `${window.location.origin}${artifact.download_url}`; + return resolveApiUrl(artifact.download_url, true); } function isStaticImageArtifact(artifact: Artifact) { @@ -128,7 +134,10 @@ function factLabel(key: string, fallback: string) { } function heroDescription(capture: CaptureEnvelope, hasVideoArtifact: boolean) { - if (capture.source.media_kind === "image_article" || capture.source.platform === "wechat_article") { + if ( + capture.source.media_kind === "image_article" || + capture.source.platform === "wechat_article" + ) { return capture.capture.asset_preparation_pending ? "正文和图片已可查看,下载文件仍在后台准备。" : "正文和图片已整理完成,可直接查看、复制或下载。"; @@ -158,7 +167,10 @@ function heroTitle(capture: CaptureEnvelope) { if (sourceTitle && !looksLikeUrl(sourceTitle)) { return sourceTitle; } - if (capture.source.media_kind === "image_article" || capture.source.platform === "wechat_article") { + if ( + capture.source.media_kind === "image_article" || + capture.source.platform === "wechat_article" + ) { return "图文整理结果"; } if (capture.source.media_kind === "video") { @@ -171,7 +183,10 @@ function heroTitle(capture: CaptureEnvelope) { } function shouldShowImages(capture: CaptureEnvelope) { - return capture.source.media_kind === "image_article" || capture.source.platform === "wechat_article"; + return ( + capture.source.media_kind === "image_article" || + capture.source.platform === "wechat_article" + ); } function buildGalleryItems(capture: CaptureEnvelope): ImageGalleryItem[] { @@ -197,7 +212,9 @@ function buildGalleryItems(capture: CaptureEnvelope): ImageGalleryItem[] { image_url: imageUrl, live_photo_video_url: null, })); - const sourceImagesByIndex = new Map(sourceImages.map((item) => [item.index, item])); + const sourceImagesByIndex = new Map( + sourceImages.map((item) => [item.index, item]), + ); const allIndices = Array.from( new Set([ @@ -232,11 +249,13 @@ function buildGalleryItems(capture: CaptureEnvelope): ImageGalleryItem[] { } function downloadHref(artifact: Artifact | null, fallbackUrl: string | null) { - return artifact ? artifactUrl(artifact) : (fallbackUrl || ""); + return artifact ? artifactUrl(artifact) : fallbackUrl || ""; } function preferredImageUrl(item: ImageGalleryItem) { - return item.imageArtifact ? artifactUrl(item.imageArtifact) : item.imageSourceUrl; + return item.imageArtifact + ? artifactUrl(item.imageArtifact) + : item.imageSourceUrl; } function imageFallbackUrl(item: ImageGalleryItem) { @@ -244,7 +263,9 @@ function imageFallbackUrl(item: ImageGalleryItem) { } function preferredLiveUrl(item: ImageGalleryItem) { - return item.liveArtifact ? artifactUrl(item.liveArtifact) : item.liveSourceUrl || ""; + return item.liveArtifact + ? artifactUrl(item.liveArtifact) + : item.liveSourceUrl || ""; } function liveFallbackUrl(item: ImageGalleryItem) { @@ -276,7 +297,11 @@ function MediaImage({ if (!resolvedPrimary) { return ( -
+
图片准备中
); @@ -412,32 +437,72 @@ function ImageViewerOverlay({ onNavigate: (nextIndex: number) => void; }) { const activeItem = items[activeIndex]; - const liveAvailable = Boolean(activeItem?.liveSourceUrl || activeItem?.liveArtifact); + const liveAvailable = Boolean( + activeItem?.liveSourceUrl || activeItem?.liveArtifact, + ); const effectiveMode = liveAvailable ? previewMode : "image"; - const imageDownload = activeItem ? downloadHref(activeItem.imageArtifact, activeItem.imageSourceUrl) : ""; - const liveDownload = activeItem ? downloadHref(activeItem.liveArtifact, activeItem.liveSourceUrl) : ""; + const imageDownload = activeItem + ? downloadHref(activeItem.imageArtifact, activeItem.imageSourceUrl) + : ""; + const liveDownload = activeItem + ? downloadHref(activeItem.liveArtifact, activeItem.liveSourceUrl) + : ""; const livePrimary = activeItem ? preferredLiveUrl(activeItem) : ""; const imagePrimary = activeItem ? preferredImageUrl(activeItem) : ""; + const activeVisual = useMemo( + () => + activeItem + ? { + key: `${activeItem.index}:${effectiveMode}`, + imageSrc: imagePrimary, + } + : null, + [activeItem, effectiveMode, imagePrimary], + ); const [liveSrc, setLiveSrc] = useState(livePrimary); const [livePlaybackError, setLivePlaybackError] = useState(false); + const [transitionVisual, setTransitionVisual] = + useState(null); + const activeVisualRef = useRef(activeVisual); const overlayRef = useRef(null); const viewportRef = useRef(null); const imageSurfaceRef = useRef(null); const liveVideoRef = useRef(null); - const pointerStateRef = useRef<{ pointerId: number; x: number; y: number } | null>(null); - const touchNavigationRef = useRef<{ x: number; y: number; at: number } | null>(null); + const pointerStateRef = useRef<{ + pointerId: number; + x: number; + y: number; + } | null>(null); + const touchNavigationRef = useRef<{ + x: number; + y: number; + at: number; + } | null>(null); const wheelNavigationRef = useRef(0); const chromeHideTimerRef = useRef(null); const [viewportSize, setViewportSize] = useState({ width: 0, height: 0 }); - const [imageNaturalSize, setImageNaturalSize] = useState({ width: 0, height: 0 }); - const [liveNaturalSize, setLiveNaturalSize] = useState({ width: 0, height: 0 }); + const [imageNaturalSize, setImageNaturalSize] = useState({ + width: 0, + height: 0, + }); + const [liveNaturalSize, setLiveNaturalSize] = useState({ + width: 0, + height: 0, + }); const [imageScale, setImageScale] = useState(1); const [imageRotation, setImageRotation] = useState(0); const [imagePan, setImagePan] = useState({ x: 0, y: 0 }); const [isDraggingImage, setIsDraggingImage] = useState(false); const [isChromeVisible, setIsChromeVisible] = useState(false); - const [navOffsets, setNavOffsets] = useState<{ left: number; right: number } | null>(null); - const [mediaChrome, setMediaChrome] = useState<{ top: number; left: number; right: number } | null>(null); + const [navOffsets, setNavOffsets] = useState<{ + left: number; + right: number; + } | null>(null); + const [mediaChrome, setMediaChrome] = useState<{ + top: number; + left: number; + right: number; + } | null>(null); useEffect(() => { setLiveSrc(livePrimary); @@ -445,6 +510,19 @@ function ImageViewerOverlay({ setLiveNaturalSize({ width: 0, height: 0 }); }, [livePrimary, activeItem?.index]); + useEffect(() => { + if (!activeVisual) return; + const previous = activeVisualRef.current; + if (previous && previous.key !== activeVisual.key && previous.imageSrc) { + setTransitionVisual(previous); + const timer = window.setTimeout(() => setTransitionVisual(null), 240); + activeVisualRef.current = activeVisual; + return () => window.clearTimeout(timer); + } + activeVisualRef.current = activeVisual; + return undefined; + }, [activeVisual]); + useEffect(() => { if (!viewportRef.current) return; const element = viewportRef.current; @@ -484,12 +562,23 @@ function ImageViewerOverlay({ const isImageRotatedSideways = imageRotation % 180 !== 0; const imageDisplaySize = { - width: isImageRotatedSideways ? imageNaturalSize.height : imageNaturalSize.width, - height: isImageRotatedSideways ? imageNaturalSize.width : imageNaturalSize.height, + width: isImageRotatedSideways + ? imageNaturalSize.height + : imageNaturalSize.width, + height: isImageRotatedSideways + ? imageNaturalSize.width + : imageNaturalSize.height, }; const fitScale = - imageDisplaySize.width > 0 && imageDisplaySize.height > 0 && viewportSize.width > 0 && viewportSize.height > 0 - ? Math.min(viewportSize.width / imageDisplaySize.width, viewportSize.height / imageDisplaySize.height, 1) + imageDisplaySize.width > 0 && + imageDisplaySize.height > 0 && + viewportSize.width > 0 && + viewportSize.height > 0 + ? Math.min( + viewportSize.width / imageDisplaySize.width, + viewportSize.height / imageDisplaySize.height, + 1, + ) : 1; const isOriginalView = Math.abs(imageScale - 1) < 0.02; const scaledImageSize = { @@ -500,7 +589,8 @@ function ImageViewerOverlay({ effectiveMode === "image" && viewportSize.width > 0 && viewportSize.height > 0 && - (scaledImageSize.width > viewportSize.width + 2 || scaledImageSize.height > viewportSize.height + 2); + (scaledImageSize.width > viewportSize.width + 2 || + scaledImageSize.height > viewportSize.height + 2); const liveFrameClass = liveNaturalSize.width > 0 && liveNaturalSize.height > 0 ? liveNaturalSize.width >= liveNaturalSize.height @@ -519,14 +609,33 @@ function ImageViewerOverlay({ if (!imageNaturalSize.width || !viewportSize.width) return; setImageScale(fitScale); setImagePan({ x: 0, y: 0 }); - }, [activeItem?.index, effectiveMode, fitScale, imageNaturalSize.height, imageNaturalSize.width, imageRotation, viewportSize.width]); + }, [ + activeItem?.index, + effectiveMode, + fitScale, + imageNaturalSize.height, + imageNaturalSize.width, + imageRotation, + viewportSize.width, + ]); function clampImagePanValue(nextPan: ViewerPan, scale = imageScale) { - if (!viewportSize.width || !viewportSize.height || !imageDisplaySize.width || !imageDisplaySize.height) { + if ( + !viewportSize.width || + !viewportSize.height || + !imageDisplaySize.width || + !imageDisplaySize.height + ) { return { x: 0, y: 0 }; } - const maxX = Math.max(0, (imageDisplaySize.width * scale - viewportSize.width) / 2); - const maxY = Math.max(0, (imageDisplaySize.height * scale - viewportSize.height) / 2); + const maxX = Math.max( + 0, + (imageDisplaySize.width * scale - viewportSize.width) / 2, + ); + const maxY = Math.max( + 0, + (imageDisplaySize.height * scale - viewportSize.height) / 2, + ); return { x: clampNumber(nextPan.x, -maxX, maxX), y: clampNumber(nextPan.y, -maxY, maxY), @@ -583,7 +692,8 @@ function ImageViewerOverlay({ function updateNavOffsets() { const overlay = overlayRef.current; - const mediaElement = effectiveMode === "live" ? liveVideoRef.current : imageSurfaceRef.current; + const mediaElement = + effectiveMode === "live" ? liveVideoRef.current : imageSurfaceRef.current; if (!overlay || !mediaElement) { setNavOffsets(null); setMediaChrome(null); @@ -601,12 +711,30 @@ function ImageViewerOverlay({ const inset = 14; const minEdge = 18; const maxLeft = Math.max(minEdge, overlayRect.width - 64); - const left = clampNumber(mediaRect.left - overlayRect.left + inset, minEdge, maxLeft); - const right = clampNumber(overlayRect.right - mediaRect.right + inset, minEdge, maxLeft); - const top = clampNumber(mediaRect.top - overlayRect.top + inset, minEdge, Math.max(minEdge, overlayRect.height - 64)); - setNavOffsets((current) => (current?.left === left && current?.right === right ? current : { left, right })); + const left = clampNumber( + mediaRect.left - overlayRect.left + inset, + minEdge, + maxLeft, + ); + const right = clampNumber( + overlayRect.right - mediaRect.right + inset, + minEdge, + maxLeft, + ); + const top = clampNumber( + mediaRect.top - overlayRect.top + inset, + minEdge, + Math.max(minEdge, overlayRect.height - 64), + ); + setNavOffsets((current) => + current?.left === left && current?.right === right + ? current + : { left, right }, + ); setMediaChrome((current) => - current?.top === top && current?.left === left && current?.right === right ? current : { top, left, right }, + current?.top === top && current?.left === left && current?.right === right + ? current + : { top, left, right }, ); } @@ -632,21 +760,39 @@ function ImageViewerOverlay({ function handleImagePointerDown(event: ReactPointerEvent) { if (effectiveMode !== "image") return; if (!imageCanPan) return; - pointerStateRef.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY }; + pointerStateRef.current = { + pointerId: event.pointerId, + x: event.clientX, + y: event.clientY, + }; event.currentTarget.setPointerCapture(event.pointerId); setIsDraggingImage(true); } function handleImagePointerMove(event: ReactPointerEvent) { - if (!pointerStateRef.current || pointerStateRef.current.pointerId !== event.pointerId) return; + if ( + !pointerStateRef.current || + pointerStateRef.current.pointerId !== event.pointerId + ) + return; const deltaX = event.clientX - pointerStateRef.current.x; const deltaY = event.clientY - pointerStateRef.current.y; - pointerStateRef.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY }; - setImagePan((current) => clampImagePanValue({ x: current.x + deltaX, y: current.y + deltaY })); + pointerStateRef.current = { + pointerId: event.pointerId, + x: event.clientX, + y: event.clientY, + }; + setImagePan((current) => + clampImagePanValue({ x: current.x + deltaX, y: current.y + deltaY }), + ); } function clearImagePointer(event?: ReactPointerEvent) { - if (event && pointerStateRef.current && pointerStateRef.current.pointerId === event.pointerId) { + if ( + event && + pointerStateRef.current && + pointerStateRef.current.pointerId === event.pointerId + ) { event.currentTarget.releasePointerCapture(event.pointerId); } pointerStateRef.current = null; @@ -655,14 +801,21 @@ function ImageViewerOverlay({ function handleViewerWheel(event: React.WheelEvent) { if (items.length <= 1) return; - const mediaElement = effectiveMode === "live" ? liveVideoRef.current : imageSurfaceRef.current; + const mediaElement = + effectiveMode === "live" ? liveVideoRef.current : imageSurfaceRef.current; if (mediaElement) { const rect = mediaElement.getBoundingClientRect(); const withinMedia = - event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom; + event.clientX >= rect.left && + event.clientX <= rect.right && + event.clientY >= rect.top && + event.clientY <= rect.bottom; if (!withinMedia) return; } - const dominantDelta = Math.abs(event.deltaY) >= Math.abs(event.deltaX) ? event.deltaY : event.deltaX; + const dominantDelta = + Math.abs(event.deltaY) >= Math.abs(event.deltaX) + ? event.deltaY + : event.deltaX; if (Math.abs(dominantDelta) < 2) return; event.preventDefault(); const now = window.performance.now(); @@ -677,7 +830,11 @@ function ImageViewerOverlay({ return; } const touch = event.touches[0]; - touchNavigationRef.current = { x: touch.clientX, y: touch.clientY, at: window.performance.now() }; + touchNavigationRef.current = { + x: touch.clientX, + y: touch.clientY, + at: window.performance.now(), + }; } function handleViewerTouchEnd(event: ReactTouchEvent) { @@ -688,7 +845,8 @@ function ImageViewerOverlay({ if (!touch) return; const deltaX = touch.clientX - start.x; const deltaY = touch.clientY - start.y; - if (Math.abs(deltaX) < 52 || Math.abs(deltaX) < Math.abs(deltaY) * 1.35) return; + if (Math.abs(deltaX) < 52 || Math.abs(deltaX) < Math.abs(deltaY) * 1.35) + return; navigateBy(deltaX < 0 ? 1 : -1); } @@ -697,7 +855,11 @@ function ImageViewerOverlay({ return (
LIVE @@ -760,6 +926,12 @@ function ImageViewerOverlay({ ) : null}
+ {transitionVisual ? ( + + ) : null} +
- {livePlaybackError ?

当前浏览器无法直接播放 Live,仍可下载原片段。

: null} + {livePlaybackError ? ( +

+ 当前浏览器无法直接播放 Live,仍可下载原片段。 +

+ ) : null}
) : (
@@ -859,14 +1035,22 @@ function ImageViewerOverlay({ {liveAvailable ? (
) : null} {`${activeIndex + 1}/${items.length}`} {items.length > 1 ? ( - ) : null} @@ -897,7 +1091,13 @@ function ImageViewerOverlay({ aria-label="缩小图片" className="image-viewer-toolbar-button" onClick={() => - setImageScale((current) => clampNumber(Number((current - 0.14).toFixed(2)), Math.min(fitScale, 0.35), 3.5)) + setImageScale((current) => + clampNumber( + Number((current - 0.14).toFixed(2)), + Math.min(fitScale, 0.35), + 3.5, + ), + ) } type="button" > @@ -907,7 +1107,15 @@ function ImageViewerOverlay({ - @@ -932,8 +1145,10 @@ function ImageViewerOverlay({ className="image-viewer-toolbar-button is-label is-download" download={ effectiveMode === "live" - ? activeItem.liveArtifact?.label || `image_${activeItem.index}_live.mp4` - : activeItem.imageArtifact?.label || `image_${activeItem.index}.jpg` + ? activeItem.liveArtifact?.label || + `image_${activeItem.index}_live.mp4` + : activeItem.imageArtifact?.label || + `image_${activeItem.index}.jpg` } href={effectiveMode === "live" ? liveDownload : imageDownload} > @@ -949,17 +1164,22 @@ function ImageViewerOverlay({ function VideoPanel({ previewArtifact, sourceArtifact, + audioArtifact, posterUrl, }: { previewArtifact: Artifact | null; sourceArtifact: Artifact | null; + audioArtifact: Artifact | null; posterUrl?: string | null; }) { const [orientation, setOrientation] = useState("unknown"); const [playbackError, setPlaybackError] = useState(false); const [aspectRatio, setAspectRatio] = useState(""); const [preferPreview, setPreferPreview] = useState(Boolean(previewArtifact)); - const activeArtifact = (preferPreview ? previewArtifact : sourceArtifact) || sourceArtifact || previewArtifact; + const activeArtifact = + (preferPreview ? previewArtifact : sourceArtifact) || + sourceArtifact || + previewArtifact; const downloadArtifact = sourceArtifact || activeArtifact; const fileSize = bytesLabel(downloadArtifact?.size_bytes); @@ -988,7 +1208,9 @@ function VideoPanel({ } const panelStyle = aspectRatio - ? ({ ["--video-aspect-ratio" as "--video-aspect-ratio"]: aspectRatio } as CSSProperties) + ? ({ + ["--video-aspect-ratio" as "--video-aspect-ratio"]: aspectRatio, + } as CSSProperties) : undefined; if (!activeArtifact || !downloadArtifact) { @@ -996,7 +1218,11 @@ function VideoPanel({ } return ( -
+
视频 @@ -1004,11 +1230,27 @@ function VideoPanel({
- + {audioArtifact ? ( + + + 下载音频 + + ) : null} + 下载视频 - {fileSize ? {fileSize} : null} + {fileSize ? ( + {fileSize} + ) : null}
@@ -1022,7 +1264,12 @@ function VideoPanel({ src={artifactUrl(activeArtifact)} onCanPlay={() => setPlaybackError(false)} onError={() => { - if (preferPreview && previewArtifact && sourceArtifact && previewArtifact.download_url !== sourceArtifact.download_url) { + if ( + preferPreview && + previewArtifact && + sourceArtifact && + previewArtifact.download_url !== sourceArtifact.download_url + ) { setPreferPreview(false); return; } @@ -1035,7 +1282,11 @@ function VideoPanel({
- {playbackError ?

当前浏览器无法直接内联预览,仍可使用上方按钮下载原视频文件。

: null} + {playbackError ? ( +

+ 当前浏览器无法直接内联预览,仍可使用上方按钮下载原视频文件。 +

+ ) : null} ); } @@ -1062,41 +1313,77 @@ function VideoUnavailablePanel({ notice }: { notice: string }) { } export function DeliverableStage({ capture }: DeliverableStageProps) { - const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">("idle"); + const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">( + "idle", + ); const [activeImageIndex, setActiveImageIndex] = useState(null); const [previewMode, setPreviewMode] = useState("image"); - const primaryText = capture.result.views.primary || capture.result.primary_text || ""; + const primaryText = + capture.result.views.primary || capture.result.primary_text || ""; const markdownText = capture.result.views.markdown || ""; const copyPayload = markdownText.trim() || primaryText; - const isLongTextResult = primaryText.trim().length >= 900 || primaryText.split("\n").filter((item) => item.trim()).length >= 8; + const isLongTextResult = + primaryText.trim().length >= 900 || + primaryText.split("\n").filter((item) => item.trim()).length >= 8; const showImages = shouldShowImages(capture); const imageItems = useMemo(() => buildGalleryItems(capture), [capture]); const imagesZip = useMemo( - () => (showImages ? capture.artifacts.find((item) => item.type === "images_zip") || null : null), + () => + showImages + ? capture.artifacts.find((item) => item.type === "images_zip") || null + : null, [capture.artifacts, showImages], ); - const textArtifact = useMemo(() => capture.artifacts.find((item) => item.type === "txt") || null, [capture.artifacts]); - const markdownArtifact = useMemo(() => capture.artifacts.find((item) => item.type === "md") || null, [capture.artifacts]); + const textArtifact = useMemo( + () => capture.artifacts.find((item) => item.type === "txt") || null, + [capture.artifacts], + ); + const markdownArtifact = useMemo( + () => capture.artifacts.find((item) => item.type === "md") || null, + [capture.artifacts], + ); + const sourceAudioArtifact = useMemo( + () => + capture.artifacts.find((item) => item.type === "source_audio") || null, + [capture.artifacts], + ); const previewVideoArtifact = useMemo( - () => (capture.source.media_kind === "video" ? capture.artifacts.find((item) => item.type === "preview_media") || null : null), + () => + capture.source.media_kind === "video" + ? capture.artifacts.find((item) => item.type === "preview_media") || + null + : null, [capture.artifacts, capture.source.media_kind], ); const sourceVideoArtifact = useMemo( - () => (capture.source.media_kind === "video" ? capture.artifacts.find((item) => item.type === "source_media") || null : null), + () => + capture.source.media_kind === "video" + ? capture.artifacts.find((item) => item.type === "source_media") || null + : null, [capture.artifacts, capture.source.media_kind], ); const fallbackVideoArtifact = useMemo( () => capture.source.media_kind === "video" - ? capture.artifacts.find((item) => item.mime_type.startsWith("video/")) || null + ? capture.artifacts.find((item) => + item.mime_type.startsWith("video/"), + ) || null : null, [capture.artifacts, capture.source.media_kind], ); - const videoArtifact = previewVideoArtifact || sourceVideoArtifact || fallbackVideoArtifact; + const videoArtifact = + previewVideoArtifact || sourceVideoArtifact || fallbackVideoArtifact; const visibleFacts = useMemo( () => capture.result.content_facts.filter((fact) => - ["title", "platform", "content_type", "author", "published_at", "duration"].includes(fact.key), + [ + "title", + "platform", + "content_type", + "author", + "published_at", + "duration", + ].includes(fact.key), ), [capture.result.content_facts], ); @@ -1123,36 +1410,42 @@ export function DeliverableStage({ capture }: DeliverableStageProps) { const titleFact = visibleFacts.find((fact) => fact.key === "title"); const secondaryFacts = visibleFacts.filter((fact) => fact.key !== "title"); - const factsSection = titleFact || secondaryFacts.length || sourceUrl ? ( -
- {titleFact ? ( -
-
- {factLabel(titleFact.key, titleFact.label)} - {titleFact.value} + const factsSection = + titleFact || secondaryFacts.length || sourceUrl ? ( +
+ {titleFact ? ( +
+
+ {factLabel(titleFact.key, titleFact.label)} + {titleFact.value} +
-
- ) : null} + ) : null} - {secondaryFacts.length || sourceUrl ? ( -
- {secondaryFacts.map((fact) => ( -
- {factLabel(fact.key, fact.label)} - {fact.value} -
- ))} - {sourceUrl ? ( - - 来源 - 查看来源 - - - ) : null} -
- ) : null} -
- ) : null; + {secondaryFacts.length || sourceUrl ? ( +
+ {secondaryFacts.map((fact) => ( +
+ {factLabel(fact.key, fact.label)} + {fact.value} +
+ ))} + {sourceUrl ? ( + + 来源 + 查看来源 + + + ) : null} +
+ ) : null} + + ) : null; useEffect(() => { if (activeImageIndex == null) return; @@ -1164,7 +1457,9 @@ export function DeliverableStage({ capture }: DeliverableStageProps) { useEffect(() => { if (activeImageIndex == null) return; const activeItem = imageItems[activeImageIndex]; - const hasLive = Boolean(activeItem?.liveSourceUrl || activeItem?.liveArtifact); + const hasLive = Boolean( + activeItem?.liveSourceUrl || activeItem?.liveArtifact, + ); setPreviewMode(hasLive ? "live" : "image"); }, [activeImageIndex, imageItems]); @@ -1188,11 +1483,17 @@ export function DeliverableStage({ capture }: DeliverableStageProps) { if (imageItems.length <= 1) return; if (event.key === "ArrowLeft") { event.preventDefault(); - setActiveImageIndex((current) => (current == null ? current : (current - 1 + imageItems.length) % imageItems.length)); + setActiveImageIndex((current) => + current == null + ? current + : (current - 1 + imageItems.length) % imageItems.length, + ); } if (event.key === "ArrowRight") { event.preventDefault(); - setActiveImageIndex((current) => (current == null ? current : (current + 1) % imageItems.length)); + setActiveImageIndex((current) => + current == null ? current : (current + 1) % imageItems.length, + ); } } @@ -1215,7 +1516,9 @@ export function DeliverableStage({ capture }: DeliverableStageProps) {
{platformLabel(capture.source.platform)} {contentTypeLabel(capture.source.media_kind)} - {capture.source.duration_seconds ? {durationLabel(capture.source.duration_seconds)} : null} + {capture.source.duration_seconds ? ( + {durationLabel(capture.source.duration_seconds)} + ) : null}
@@ -1230,42 +1533,96 @@ export function DeliverableStage({ capture }: DeliverableStageProps) {
+ {sourceAudioArtifact && !hasVideoPanel ? ( + + + 下载音频 + + ) : null} + {textArtifact ? ( - + 下载 .txt ) : null} {markdownArtifact ? ( - + 下载 .md ) : null} {showCopyAction ? ( - ) : null}
-
+
{primaryText.trim() ? ( -
- {primaryText.split("\n").map((paragraph, index) => - paragraph.trim() ?

{paragraph}

:
, - )} +
+ {primaryText + .split("\n") + .map((paragraph, index) => + paragraph.trim() ? ( +

{paragraph}

+ ) : ( +
+ ), + )}
) : ( -
+
{isImageOnlyResult ? ( <> 这条图文已整理完成。 -

右侧可直接查看正文图片,打包下载会在准备完成后自动可用。

+

+ 右侧可直接查看正文图片,打包下载会在准备完成后自动可用。 +

{`本次共保留 ${imageItems.length} 张正文图片。`}

) : isVideoOnlyResult ? ( @@ -1286,12 +1643,15 @@ export function DeliverableStage({ capture }: DeliverableStageProps) { ) : null} - {hasVideoPanel && !hasPlayableVideo ? : null} + {hasVideoPanel && !hasPlayableVideo ? ( + + ) : null} {!hasVideoPanel && showImages ? (
@@ -1302,7 +1662,11 @@ export function DeliverableStage({ capture }: DeliverableStageProps) {
{imagesZip ? ( - + 全部下载 @@ -1318,7 +1682,11 @@ export function DeliverableStage({ capture }: DeliverableStageProps) { {imageItems.length ? (
{imageItems.map((item, index) => ( - setActiveImageIndex(index)} /> + setActiveImageIndex(index)} + /> ))}
) : ( diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index b7600e9..f49f0c4 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,14 +1,23 @@ -import { Activity } from "lucide-react"; +import { Activity, Download } from "lucide-react"; type HeaderStatus = "idle" | "processing" | "done" | "failed"; type HeaderProps = { status: HeaderStatus; featureName: string; + showWindowsDownload: boolean; onReset: () => void; }; -export function Header({ status, featureName, onReset }: HeaderProps) { +const WINDOWS_DOWNLOAD_URL = + "https://github.com/qinxujunai/Audio2Text/releases/latest/download/Wanxiang-Windows-x64-Setup.exe"; + +export function Header({ + status, + featureName, + showWindowsDownload, + onReset, +}: HeaderProps) { return (
+ {showWindowsDownload ? ( + + + ) : null} {status === "processing" ? ( diff --git a/web/src/components/InputStage.tsx b/web/src/components/InputStage.tsx index 89f4c23..08bbbea 100644 --- a/web/src/components/InputStage.tsx +++ b/web/src/components/InputStage.tsx @@ -55,7 +55,10 @@ function contentTypeLabel(type?: string | null) { function isLikelyUrl(value?: string | null) { if (!value) return false; const text = value.trim(); - return /^https?:\/\//i.test(text) || /^(xhslink\.com|b23\.tv|v\.douyin\.com|youtu\.be)\//i.test(text); + return ( + /^https?:\/\//i.test(text) || + /^(xhslink\.com|b23\.tv|v\.douyin\.com|youtu\.be)\//i.test(text) + ); } function sanitizePreview(value?: string | null) { @@ -76,10 +79,19 @@ function friendlyFallbackTitle(item: CaptureListItem) { } function displayTitle(item: CaptureListItem) { - const preferred = [item.title, item.preview_text].map((value) => sanitizePreview(value)).find(Boolean); + const preferred = [item.title, item.preview_text] + .map((value) => sanitizePreview(value)) + .find(Boolean); return preferred || friendlyFallbackTitle(item); } +function displayPreview(item: CaptureListItem) { + const title = sanitizePreview(item.title); + const preview = sanitizePreview(item.preview_text); + if (!preview || preview === title) return ""; + return preview; +} + function clampText(value: string, limit = 34) { return value.length > limit ? `${value.slice(0, limit)}...` : value; } @@ -117,14 +129,18 @@ export function InputStage({ }: InputStageProps) { const [showAllHistory, setShowAllHistory] = useState(false); const textareaRef = useRef(null); - const extensionLabel = useMemo(() => extensionSummary(supportedExtensions), [supportedExtensions]); + const extensionLabel = useMemo( + () => extensionSummary(supportedExtensions), + [supportedExtensions], + ); useEffect(() => { setShowAllHistory(false); }, [captures.length]); const visibleCaptures = useMemo( - () => (showAllHistory ? captures : captures.slice(0, COLLAPSED_HISTORY_COUNT)), + () => + showAllHistory ? captures : captures.slice(0, COLLAPSED_HISTORY_COUNT), [captures, showAllHistory], ); @@ -178,7 +194,12 @@ export function InputStage({ } }} onKeyDown={(event) => { - if ((event.metaKey || event.ctrlKey) && event.key === "Enter" && input.trim() && !submitting) { + if ( + (event.metaKey || event.ctrlKey) && + event.key === "Enter" && + input.trim() && + !submitting + ) { event.preventDefault(); void onSubmitText(); return; @@ -210,12 +231,19 @@ export function InputStage({ type="file" hidden accept=".mp3,.m4a,.wav,.flac,.mp4,.mov,.mkv" - onChange={(event) => onSelectFile(event.target.files?.[0] || null)} + onChange={(event) => + onSelectFile(event.target.files?.[0] || null) + } />
-
@@ -232,14 +260,28 @@ export function InputStage({
{selectedFile.name} - {extensionLabel ? `${fileSizeLabel(selectedFile.size)} · ${extensionLabel}` : fileSizeLabel(selectedFile.size)} + + {extensionLabel + ? `${fileSizeLabel(selectedFile.size)} · ${extensionLabel}` + : fileSizeLabel(selectedFile.size)} + {`${maxUploadSizeMb} MB 以内可直接处理,音视频建议控制在 ${freeDurationMinutes} 分钟内。`}
- -
@@ -255,53 +297,86 @@ export function InputStage({
{captures.length > COLLAPSED_HISTORY_COUNT ? ( - ) : null} -
{visibleCaptures.length ? ( -
- {visibleCaptures.map((item) => ( -
- + - -
- ))} + +
+ ); + })}
) : ( -
还没有历史记录。先贴一个链接或上传文件试试。
+
+ 还没有历史记录。先贴一个链接或上传文件试试。 +
)}
diff --git a/web/src/components/ProcessingStage.tsx b/web/src/components/ProcessingStage.tsx index bae0b9f..8e70bd3 100644 --- a/web/src/components/ProcessingStage.tsx +++ b/web/src/components/ProcessingStage.tsx @@ -32,11 +32,18 @@ export function ProcessingStage({ progressDetail, onReset, }: ProcessingStageProps) { - const normalizedProgress = Math.min(100, Math.max(phase === "bootstrapping" ? 18 : progressPercent || 0, 0)); + const normalizedProgress = Math.min( + 100, + Math.max(phase === "bootstrapping" ? 18 : progressPercent || 0, 0), + ); const safeDetail = safeProgressDetail(progressDetail); return ( -
+
{phase !== "failed" ? (