diff --git a/docs/design/v2-smart-subtitle-pipeline.md b/docs/design/v2-smart-subtitle-pipeline.md new file mode 100644 index 0000000..0e347b1 --- /dev/null +++ b/docs/design/v2-smart-subtitle-pipeline.md @@ -0,0 +1,183 @@ +# V2: 智慧字幕管線升級 + +## 背景與問題 + +BilingualSub 目前的管線是「一條路」:所有影片都經過 Whisper 轉錄 → LLM 翻譯 → 合併。這帶來三個痛點: + +1. **時間戳偏移**:Groq Whisper 對非 0 秒起始的音訊會把時間戳歸零(如音訊 03 秒開始 → 轉錄判定為 00 秒),這是 Whisper 架構的已知限制 +2. **專有名詞被亂翻**:技術詞彙如 Agent、Skills、Claude Code 被翻成「代理人」「技能」,翻譯 prompt 沒有術語保留機制 +3. **無語音影片無法處理**:螢幕錄製、操作教學等沒有旁白的影片完全無法產生字幕 + +另外前端 Vite 7 需要升級到 Vite 8。 + +## 使用者角色 + +使用者:需要將外語 YouTube 影片加上雙語字幕的個人使用者。痛點是翻譯品質不穩定、某些影片根本無法處理。 + +## 需求情境 + +- 使用者:When 影片有 YouTube 手動上傳字幕時,I want to 直接使用那些字幕來翻譯,so I can 避免 Whisper 的時間戳偏移問題且獲得更準確的原文 +- 使用者:When 翻譯結果把專有名詞翻錯時,I want to 建立一組術語表讓系統記住,so I can 不用每次都手動修正相同的錯誤 +- 使用者:When 影片沒有語音時,I want to 系統自動辨識畫面內容並產生描述性字幕,so I can 為操作教學等影片加上字幕 + +## 設計意圖 + +- **優先 YouTube 手動字幕、不用自動字幕** → 研究顯示 YouTube 自動字幕準確率僅 60-70%,Whisper 有 95%+。手動上傳字幕通常最準確,但自動生成的比 Whisper 差,所以只下載手動字幕 +- **Glossary 用 JSON 而非 SQLite** → 專案目前無資料庫(in-memory job store),JSON 和現有架構一致,百筆術語綽綽有餘 +- **Vision 模型不限定 provider** → 遵循現有 translator 用 Agno 抽象的模式,由環境變數切換 +- **靜音偵測自動分流** → 使用者不需要手動選擇管線模式,系統自動判斷 + +## User Journey + +### Journey 1:使用者 — 有手動字幕的影片 + +前置條件:影片在 YouTube 上有創作者上傳的字幕 + +1. 使用者貼上 YouTube URL → 系統開始下載影片 +2. 系統用 yt-dlp 檢查是否有手動上傳字幕 → 發現有英文手動字幕 +3. 進度顯示「使用 YouTube 字幕」→ 系統下載 SRT 字幕並解析 +4. 跳過 Whisper 轉錄 → 直接進入翻譯步驟 +5. 翻譯完成 → 使用者在編輯器中檢視雙語字幕 + → 時間戳來自 YouTube 原始字幕,精準度高 + +### Journey 2:使用者 — 無手動字幕的影片(有語音) + +前置條件:影片沒有手動字幕但有語音 + +1. 使用者貼上 YouTube URL → 系統下載影片 +2. 系統檢查手動字幕 → 無可用字幕 +3. 進度顯示「正在轉錄(Whisper)」→ 走既有 Whisper 管線 +4. 後續流程不變(翻譯 → 合併 → 燒錄) + +### Journey 3:使用者 — 管理術語表 + +前置條件:使用者翻譯過影片,發現某些專有名詞被翻錯 + +1. 使用者在字幕編輯器中看到 "Agent" 被翻成「代理人」 +2. 使用者點選該字幕條目的「加入術語表」按鈕 +3. 彈出輸入框:原文 "Agent"、目標 "Agent"(保留原文)→ 確認 +4. 術語被儲存到 glossary.json → 下次翻譯時 LLM prompt 會包含此術語 +5. 使用者也可以從工具列打開「術語表管理」面板,批次新增/編輯/刪除術語 + +### Journey 4:使用者 — 術語表生效 + +前置條件:術語表中已有 "Agent → Agent"、"Skills → Skills" + +1. 使用者提交新影片翻譯 +2. 系統載入 glossary.json,將術語表注入翻譯 prompt +3. LLM 翻譯時保留 "Agent"、"Skills" 不翻譯 +4. 使用者檢視結果 → 專有名詞正確保留 + +### Journey 5:使用者 — 無語音影片(視覺描述) + +前置條件:影片是螢幕錄製或操作教學,沒有旁白 + +1. 使用者貼上影片 URL → 系統下載影片 +2. 系統擷取音訊 → 偵測到幾乎全靜音 +3. 進度顯示「偵測到無語音影片,正在分析畫面」 +4. 系統每 5 秒擷取一個關鍵幀 → 送到 vision 模型分析 +5. Vision 模型產生描述(如「打開設定頁面,點擊帳號選項」) +6. 描述轉換為字幕格式 → 進入翻譯 → 合併為雙語字幕 + +## 替代流程 + +- **yt-dlp 字幕下載失敗**:記錄 warning,靜默 fallback 到 Whisper 轉錄 +- **靜音偵測誤判**(有語音但判為靜音):使用者可在前端手動觸發重新轉錄(未來可加 force_transcribe 參數) +- **Vision 模型不可用**:回傳錯誤提示「此影片無語音且未設定視覺模型,請設定 VISION_MODEL 環境變數」 +- **Glossary 檔案損壞/不存在**:系統啟動時建立空 glossary,損壞時重建並記錄 warning + +## 錯誤情境 + +### 系統錯誤 + +- yt-dlp 字幕 API 回傳格式異常 → 解析失敗後 fallback Whisper +- Vision 模型 API 超時/額度不足 → 回傳明確錯誤碼 +- glossary.json 寫入失敗(磁碟滿)→ 回傳 500 錯誤 + +### 使用者誤操作 + +- 在術語表中新增空白原文 → 前後端驗證,拒絕空值 +- 在術語表中新增重複原文 → 更新既有條目而非新增 + +### 惡意行為 + +- Glossary 注入(用極長文字或 prompt injection 內容作為術語)→ 限制單一術語長度 100 字元,glossary 總量上限 500 筆 + +## Out of Scope + +- 多使用者 glossary 隔離(目前是單一 glossary 全域共用) +- Glossary 分類標籤或群組 +- WhisperX 本地端對齊(需要 GPU,架構差異太大) +- YouTube 自動生成字幕的使用(研究顯示品質不如 Whisper) +- 即時串流字幕 +- Vite 8 升級細節(純基礎設施變更,不影響功能設計) + +## 整合點 + +| 系統 | 用途 | 備註 | +| ------------------- | ------------------------------------------- | ------------------------------- | +| yt-dlp | 影片下載 + 字幕下載 | 新增 `writesubtitles` 選項 | +| Groq Whisper API | 語音轉錄 | 既有,作為 fallback | +| Agno + LLM | 翻譯 | 既有,新增 glossary prompt 注入 | +| Agno + Vision Model | 畫面描述 | 新增,provider TBD | +| FFmpeg | 音訊擷取 + 靜音偵測 + 關鍵幀擷取 + 字幕燒錄 | 新增靜音偵測和幀擷取 | +| JSON 檔案系統 | Glossary 持久化 | 新增 | + +## Acceptance Criteria + +### 字幕來源 + +- Given 影片有 YouTube 手動上傳的英文字幕 + When 使用者提交該影片進行翻譯 + Then 系統下載手動字幕並跳過 Whisper 轉錄,進度顯示「使用 YouTube 字幕」 + +- Given 影片只有 YouTube 自動生成字幕(無手動字幕) + When 使用者提交該影片進行翻譯 + Then 系統使用 Whisper 轉錄,不使用自動生成字幕 + +- Given 影片既無手動也無自動字幕 + When 使用者提交該影片進行翻譯 + Then 系統使用 Whisper 轉錄 + +- Given yt-dlp 字幕下載過程中發生錯誤 + When 系統嘗試取得字幕 + Then 自動 fallback 到 Whisper 轉錄,不中斷流程 + +### 術語表 + +- Given 術語表中有 "Agent → Agent" + When 翻譯包含 "Agent" 的字幕 + Then 翻譯結果保留 "Agent" 不翻譯 + +- Given 使用者在字幕編輯器中選擇一個條目 + When 點擊「加入術語表」 + Then 彈出輸入框讓使用者確認原文和目標翻譯,確認後儲存到 glossary.json + +- Given 使用者打開術語表管理面板 + When 新增/編輯/刪除術語 + Then 變更即時生效,並持久化到 glossary.json + +- Given 術語表有 500 筆條目 + When 使用者嘗試新增第 501 筆 + Then 系統拒絕並提示「術語表已達上限」 + +### 視覺描述 + +- Given 影片音訊 90% 以上為靜音 + When 系統完成靜音偵測 + Then 自動切換到視覺描述管線,擷取關鍵幀並產生描述性字幕 + +- Given 視覺模型未設定(無 VISION_MODEL 環境變數) + When 系統偵測到靜音影片 + Then 回傳明確錯誤訊息提示使用者設定視覺模型 + +- Given 視覺描述管線完成 + When 描述性字幕產生後 + Then 字幕進入翻譯和合併流程,最終輸出雙語字幕 + +## 開放問題 + +1. Vision 模型選擇 — 等使用者 TBD 確認,目前架構先用 Agno 抽象支援多 provider +2. 靜音偵測的閾值 (-40dB) 和比例 (90%) 是否需要可設定?初版先寫死觀察 +3. 關鍵幀擷取間隔(5 秒)是否適合所有影片?操作快速的影片可能需要更短間隔 +4. Glossary 是否需要 import/export 功能?初版先不做 diff --git a/frontend/package.json b/frontend/package.json index c782d2f..9524415 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,7 @@ "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", + "@vitejs/plugin-react": "^6.0.1", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", @@ -39,7 +39,7 @@ "jsdom": "^28.0.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4", + "vite": "^8.0.8", "vitest": "^4.0.18" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 9471c48..e3d9b5a 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: dependencies: '@tailwindcss/vite': specifier: ^4.1.18 - version: 4.1.18(vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2)) + version: 4.1.18(vite@8.0.9(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -60,8 +60,8 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.13) '@vitejs/plugin-react': - specifier: ^5.1.1 - version: 5.1.3(vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2)) + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.9(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1)) eslint: specifier: ^9.39.1 version: 9.39.2(jiti@2.6.1) @@ -84,11 +84,11 @@ importers: specifier: ^8.46.4 version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: - specifier: ^7.2.4 - version: 7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2) + specifier: ^8.0.8 + version: 8.0.9(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@24.10.11)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2) + version: 4.0.18(@types/node@24.10.11)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.32.0) packages: '@acemir/cssom@0.9.31': @@ -179,13 +179,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.28.6': - resolution: - { - integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==, - } - engines: { node: '>=6.9.0' } - '@babel/helper-string-parser@7.27.1': resolution: { @@ -222,24 +215,6 @@ packages: engines: { node: '>=6.0.0' } hasBin: true - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: - { - integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==, - } - engines: { node: '>=6.9.0' } - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: - { - integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==, - } - engines: { node: '>=6.9.0' } - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/runtime@7.28.6': resolution: { @@ -317,6 +292,24 @@ packages: } engines: { node: '>=20.19.0' } + '@emnapi/core@1.9.2': + resolution: + { + integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==, + } + + '@emnapi/runtime@1.9.2': + resolution: + { + integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==, + } + + '@emnapi/wasi-threads@1.2.1': + resolution: + { + integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==, + } + '@esbuild/aix-ppc64@0.27.3': resolution: { @@ -687,10 +680,165 @@ packages: integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, } - '@rolldown/pluginutils@1.0.0-rc.2': + '@napi-rs/wasm-runtime@1.1.4': + resolution: + { + integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==, + } + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.126.0': + resolution: + { + integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==, + } + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + resolution: + { + integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + resolution: + { + integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + resolution: + { + integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + resolution: + { + integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + resolution: + { + integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + resolution: + { + integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': resolution: { - integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==, + integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + resolution: + { + integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + resolution: + { + integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + resolution: + { + integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + resolution: + { + integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + resolution: + { + integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + resolution: + { + integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + resolution: + { + integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + resolution: + { + integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.16': + resolution: + { + integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==, + } + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: + { + integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==, } '@rollup/rollup-android-arm-eabi@4.57.1': @@ -1075,34 +1223,16 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@types/aria-query@5.0.4': - resolution: - { - integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==, - } - - '@types/babel__core@7.20.5': + '@tybys/wasm-util@0.10.1': resolution: { - integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, + integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==, } - '@types/babel__generator@7.27.0': - resolution: - { - integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==, - } - - '@types/babel__template@7.4.4': - resolution: - { - integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, - } - - '@types/babel__traverse@7.28.0': + '@types/aria-query@5.0.4': resolution: { - integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==, + integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==, } '@types/chai@5.2.3': @@ -1238,14 +1368,21 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@vitejs/plugin-react@5.1.3': + '@vitejs/plugin-react@6.0.1': resolution: { - integrity: sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==, + integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==, } engines: { node: ^20.19.0 || >=22.12.0 } peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true '@vitest/expect@4.0.18': resolution: @@ -2019,6 +2156,15 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.32.0: + resolution: + { + integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.30.2: resolution: { @@ -2028,6 +2174,15 @@ packages: cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.32.0: + resolution: + { + integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.30.2: resolution: { @@ -2037,6 +2192,15 @@ packages: cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.32.0: + resolution: + { + integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.30.2: resolution: { @@ -2046,6 +2210,15 @@ packages: cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.32.0: + resolution: + { + integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.30.2: resolution: { @@ -2055,6 +2228,15 @@ packages: cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: + { + integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.30.2: resolution: { @@ -2064,6 +2246,15 @@ packages: cpu: [arm64] os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: + resolution: + { + integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [linux] + lightningcss-linux-arm64-musl@1.30.2: resolution: { @@ -2073,6 +2264,15 @@ packages: cpu: [arm64] os: [linux] + lightningcss-linux-arm64-musl@1.32.0: + resolution: + { + integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [linux] + lightningcss-linux-x64-gnu@1.30.2: resolution: { @@ -2082,6 +2282,15 @@ packages: cpu: [x64] os: [linux] + lightningcss-linux-x64-gnu@1.32.0: + resolution: + { + integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [linux] + lightningcss-linux-x64-musl@1.30.2: resolution: { @@ -2091,6 +2300,15 @@ packages: cpu: [x64] os: [linux] + lightningcss-linux-x64-musl@1.32.0: + resolution: + { + integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [linux] + lightningcss-win32-arm64-msvc@1.30.2: resolution: { @@ -2100,6 +2318,15 @@ packages: cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.32.0: + resolution: + { + integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, + } + engines: { node: '>= 12.0.0' } + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.30.2: resolution: { @@ -2109,6 +2336,15 @@ packages: cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.32.0: + resolution: + { + integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, + } + engines: { node: '>= 12.0.0' } + cpu: [x64] + os: [win32] + lightningcss@1.30.2: resolution: { @@ -2116,6 +2352,13 @@ packages: } engines: { node: '>= 12.0.0' } + lightningcss@1.32.0: + resolution: + { + integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, + } + engines: { node: '>= 12.0.0' } + locate-path@6.0.0: resolution: { @@ -2288,6 +2531,20 @@ packages: } engines: { node: '>=12' } + picomatch@4.0.4: + resolution: + { + integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==, + } + engines: { node: '>=12' } + + postcss@8.5.10: + resolution: + { + integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==, + } + engines: { node: ^10 || ^12 || >=14 } + postcss@8.5.6: resolution: { @@ -2349,13 +2606,6 @@ packages: integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==, } - react-refresh@0.18.0: - resolution: - { - integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==, - } - engines: { node: '>=0.10.0' } - react@19.2.4: resolution: { @@ -2384,6 +2634,14 @@ packages: } engines: { node: '>=4' } + rolldown@1.0.0-rc.16: + resolution: + { + integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + hasBin: true + rollup@4.57.1: resolution: { @@ -2525,6 +2783,13 @@ packages: } engines: { node: '>=12.0.0' } + tinyglobby@0.2.16: + resolution: + { + integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==, + } + engines: { node: '>=12.0.0' } + tinyrainbow@3.0.3: resolution: { @@ -2568,6 +2833,12 @@ packages: peerDependencies: typescript: '>=4.8.4' + tslib@2.8.1: + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } + type-check@0.4.0: resolution: { @@ -2672,6 +2943,52 @@ packages: yaml: optional: true + vite@8.0.9: + resolution: + { + integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitest@4.0.18: resolution: { @@ -2893,8 +3210,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -2910,16 +3225,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/runtime@7.28.6': {} '@babel/template@7.28.6': @@ -2967,6 +3272,22 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.27.3': optional: true @@ -3123,7 +3444,67 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@rolldown/pluginutils@1.0.0-rc.2': {} + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.126.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.16': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -3263,12 +3644,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2))': + '@tailwindcss/vite@4.1.18(vite@8.0.9(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 8.0.9(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1) '@testing-library/dom@10.4.1': dependencies: @@ -3304,28 +3685,12 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@types/aria-query@5.0.4': {} - - '@types/babel__core@7.20.5': + '@tybys/wasm-util@0.10.1': dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.29.0 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + tslib: 2.8.1 + optional: true - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.29.0 + '@types/aria-query@5.0.4': {} '@types/chai@5.2.3': dependencies: @@ -3441,17 +3806,10 @@ snapshots: '@typescript-eslint/types': 8.54.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@5.1.3(vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2))': + '@vitejs/plugin-react@6.0.1(vite@8.0.9(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1))': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@rolldown/pluginutils': 1.0.0-rc.2 - '@types/babel__core': 7.20.5 - react-refresh: 0.18.0 - vite: 7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2) - transitivePeerDependencies: - - supports-color + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.9(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1) '@vitest/expect@4.0.18': dependencies: @@ -3462,13 +3820,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0) '@vitest/pretty-format@4.0.18': dependencies: @@ -3765,6 +4123,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -3913,36 +4275,69 @@ snapshots: lightningcss-android-arm64@1.30.2: optional: true + lightningcss-android-arm64@1.32.0: + optional: true + lightningcss-darwin-arm64@1.30.2: optional: true + lightningcss-darwin-arm64@1.32.0: + optional: true + lightningcss-darwin-x64@1.30.2: optional: true + lightningcss-darwin-x64@1.32.0: + optional: true + lightningcss-freebsd-x64@1.30.2: optional: true + lightningcss-freebsd-x64@1.32.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + lightningcss-linux-arm64-gnu@1.30.2: optional: true + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + lightningcss-linux-arm64-musl@1.30.2: optional: true + lightningcss-linux-arm64-musl@1.32.0: + optional: true + lightningcss-linux-x64-gnu@1.30.2: optional: true + lightningcss-linux-x64-gnu@1.32.0: + optional: true + lightningcss-linux-x64-musl@1.30.2: optional: true + lightningcss-linux-x64-musl@1.32.0: + optional: true + lightningcss-win32-arm64-msvc@1.30.2: optional: true + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + lightningcss-win32-x64-msvc@1.30.2: optional: true + lightningcss-win32-x64-msvc@1.32.0: + optional: true + lightningcss@1.30.2: dependencies: detect-libc: 2.1.2 @@ -3959,6 +4354,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4038,6 +4449,14 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -4072,8 +4491,6 @@ snapshots: react-is@17.0.2: {} - react-refresh@0.18.0: {} - react@19.2.4: {} redent@3.0.0: @@ -4085,6 +4502,27 @@ snapshots: resolve-from@4.0.0: {} + rolldown@1.0.0-rc.16: + dependencies: + '@oxc-project/types': 0.126.0 + '@rolldown/pluginutils': 1.0.0-rc.16 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-x64': 1.0.0-rc.16 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.16 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.16 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.16 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16 + rollup@4.57.1: dependencies: '@types/estree': 1.0.8 @@ -4167,6 +4605,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinyrainbow@3.0.3: {} tldts-core@7.0.23: {} @@ -4187,6 +4630,9 @@ snapshots: dependencies: typescript: 5.9.3 + tslib@2.8.1: + optional: true + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -4222,7 +4668,7 @@ snapshots: dependencies: react: 19.2.4 - vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2): + vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -4234,12 +4680,25 @@ snapshots: '@types/node': 24.10.11 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.30.2 + lightningcss: 1.32.0 + + vite@8.0.9(@types/node@24.10.11)(esbuild@0.27.3)(jiti@2.6.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.16 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.10.11 + esbuild: 0.27.3 + fsevents: 2.3.3 + jiti: 2.6.1 - vitest@4.0.18(@types/node@24.10.11)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2): + vitest@4.0.18(@types/node@24.10.11)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.32.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -4256,7 +4715,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.3.1(@types/node@24.10.11)(jiti@2.6.1)(lightningcss@1.32.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.11 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cfc0780..81cc405 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -64,7 +64,13 @@ function App() { status={state.status} progress={state.progress} currentStep={state.currentStep} + subtitleSource={state.subtitleSource ?? undefined} /> + {state.sourceUrl && + !state.sourceUrl.includes('youtube.com') && + !state.sourceUrl.includes('youtu.be') && ( +
{t('progress.nonYoutubeHint')}
+ )} )} @@ -156,6 +162,7 @@ function App() { status={state.status} progress={state.progress} currentStep={state.currentStep} + subtitleSource={state.subtitleSource ?? undefined} steps={SUBTITLE_STEPS} /> @@ -225,6 +232,7 @@ function App() { status={state.status} progress={state.progress} currentStep={state.currentStep} + subtitleSource={state.subtitleSource ?? undefined} /> )} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index f3687ce..1fac7d1 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,5 +1,7 @@ import { FileType } from '../constants'; import type { + GlossaryEntry, + GlossaryListResponse, JobCreateRequest, JobCreateResponse, JobStatusResponse, @@ -144,6 +146,48 @@ class ApiClient { getDownloadUrl(jobId: string, fileType: FileType): string { return `${this.baseUrl}/api/jobs/${jobId}/download/${fileType}`; } + + async getGlossary(): Promise+ {t('glossary.title')} +
+ +{error}
+{t('glossary.empty')}
+ ) : ( + entries.map(entry => ( ++ {t('progress.subtitleSource')} + {': '} + {subtitleSource === SubtitleSource.YOUTUBE_MANUAL + ? t('progress.subtitleSourceYoutube') + : t('progress.subtitleSourceWhisper')} +
+ )} ); } diff --git a/frontend/src/components/SubtitleEditor.tsx b/frontend/src/components/SubtitleEditor.tsx index 69d1d91..3deb6f5 100644 --- a/frontend/src/components/SubtitleEditor.tsx +++ b/frontend/src/components/SubtitleEditor.tsx @@ -1,10 +1,12 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { BookOpen } from 'lucide-react'; import { apiClient } from '@/api/client'; import { FileType } from '@/constants'; import { parseSrt, serializeSrt, srtTimeToSeconds, isValidSrtTime } from '@/utils/srt'; import { triggerDownload } from '@/utils/download'; import type { SrtEntry } from '@/types'; +import { GlossaryPanel } from './GlossaryPanel'; interface SubtitleEditorProps { jobId: string; @@ -35,6 +37,7 @@ export function SubtitleEditor({ jobId, onBurn, isBurning }: SubtitleEditorProps const [retranslateChoices, setRetranslateChoices] = useState{t('editor.retranslateHint', { count: selectedCount })} @@ -567,9 +583,27 @@ export function SubtitleEditor({ jobId, onBurn, isBurning }: SubtitleEditorProps {/* Original text (read-only) */} {entry.original && ( -
- {entry.original} -
++ {entry.original} +
+ +