From 48568e52c61f2ccf6437d4567591e84083a4f775 Mon Sep 17 00:00:00 2001 From: Jianwu Huang <63531681+JefferyHcool@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:47:14 +0800 Subject: [PATCH 1/5] "Claude PR Assistant workflow" --- .github/workflows/claude.yml | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..6b15fac --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr *)' + From 9d424aef597e6e0f85bfedaaa8edda6151e7ad08 Mon Sep 17 00:00:00 2001 From: Jianwu Huang <63531681+JefferyHcool@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:47:16 +0800 Subject: [PATCH 2/5] "Claude Code Review workflow" --- .github/workflows/claude-code-review.yml | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/claude-code-review.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..b5e8cfd --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + From 3841719d5ae4279c60e1326eacd1077841f15d20 Mon Sep 17 00:00:00 2001 From: huangjianwu Date: Tue, 23 Jun 2026 10:01:00 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(transcriber):=20=E4=BF=AE=E5=A4=8D=20la?= =?UTF-8?q?rge-v3-turbo=20=E5=9B=A0=E4=BB=93=E5=BA=93=20404=20=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Systran/faster-whisper-large-v3-turbo 已从 HuggingFace 下架(API 返回 401/404,仓库不存在)。用户点击下载后,后台 snapshot_download 立即抛错 被吞掉,_downloading 置为 failed 但状态接口只回传 downloading/downloaded 两个布尔,于是表现为:无进度转圈、状态一直「未下载」、前端无错误提示。 改用社区维护的 CT2 转换版 deepdml/faster-whisper-large-v3-turbo-ct2: HF 直链可达(200,无重定向,保证缓存目录名与存在性检测一致),含 model.bin 等全部所需文件,与 faster-whisper 的 large-v3-turbo 等价。 附回归测试,断言 large-v3-turbo 解析到存活仓库而非已失效的 Systran 仓库。 Closes #402 Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/app/transcriber/whisper_models.py | 11 ++++++++--- backend/tests/test_whisper_models.py | 14 +++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/backend/app/transcriber/whisper_models.py b/backend/app/transcriber/whisper_models.py index 71dde1c..3a57b8f 100644 --- a/backend/app/transcriber/whisper_models.py +++ b/backend/app/transcriber/whisper_models.py @@ -6,7 +6,8 @@ 检测三处,用户想用命名不符合该约定的模型(比如社区微调版、或自己下到本地的模型)就接不上。 本模块把映射**显式化 + 可配置**(对齐 mlx_whisper_transcriber.MLX_MODEL_MAP 的模式): - - 内置:size → Systran/faster-whisper-{size} + - 内置:size → faster-whisper 兼容的 CT2 repo_id(多数为 Systran/faster-whisper-{size}; + turbo 用社区维护版,见 BUILTIN_WHISPER_MODELS) - 自定义:用户在 config/whisper_models.json 登记 {名称: ""} (JSON 持久化;Docker 下随 config 卷持久化) @@ -22,7 +23,8 @@ from app.utils.logger import get_logger logger = get_logger(__name__) -# 内置模型:size → faster-whisper 兼容的 HF repo_id(CTranslate2 转换版,Systran 官方维护)。 +# 内置模型:size → faster-whisper 兼容的 HF repo_id(CTranslate2 转换版)。 +# 多数档位用 Systran 官方维护的转换版;turbo 例外见下。 BUILTIN_WHISPER_MODELS: Dict[str, str] = { "tiny": "Systran/faster-whisper-tiny", "base": "Systran/faster-whisper-base", @@ -31,7 +33,10 @@ BUILTIN_WHISPER_MODELS: Dict[str, str] = { "large-v1": "Systran/faster-whisper-large-v1", "large-v2": "Systran/faster-whisper-large-v2", "large-v3": "Systran/faster-whisper-large-v3", - "large-v3-turbo": "Systran/faster-whisper-large-v3-turbo", + # issue #402:Systran 没有 turbo 的 CT2 转换版(Systran/faster-whisper-large-v3-turbo + # 在 HF 上 401/404),点下载会静默失败、状态一直「未下载」。改用社区维护的 CT2 转换版 + # (deepdml,直链可达、含 model.bin,与 faster-whisper 的 large-v3-turbo 等价)。 + "large-v3-turbo": "deepdml/faster-whisper-large-v3-turbo-ct2", } # 前端下拉默认展示的内置档位(保持与历史 WHISPER_MODEL_SIZES 一致,不把 8 个全列出来) diff --git a/backend/tests/test_whisper_models.py b/backend/tests/test_whisper_models.py index 0f323c1..9f57dba 100644 --- a/backend/tests/test_whisper_models.py +++ b/backend/tests/test_whisper_models.py @@ -50,7 +50,19 @@ class TestResolve(unittest.TestCase): def test_builtin_resolves_to_systran(self): self.assertEqual(self.reg.resolve("tiny"), "Systran/faster-whisper-tiny") - self.assertEqual(self.reg.resolve("large-v3-turbo"), "Systran/faster-whisper-large-v3-turbo") + + def test_large_v3_turbo_resolves_to_live_repo(self): + # 回归 issue #402:Systran 从未发布 turbo 的 CT2 转换版, + # 原映射 Systran/faster-whisper-large-v3-turbo 在 HF 上 401/404, + # 导致下载静默失败、状态一直「未下载」。改用社区维护的 CT2 转换版。 + self.assertEqual( + self.reg.resolve("large-v3-turbo"), + "deepdml/faster-whisper-large-v3-turbo-ct2", + ) + self.assertNotEqual( + self.reg.resolve("large-v3-turbo"), + "Systran/faster-whisper-large-v3-turbo", + ) def test_passthrough_repo_id(self): # 用户直接把 HF repo_id 当 model_size 传进来(含 "/") From 4a87c5b93b79a05572580bcde7a3a5bd68bb605a Mon Sep 17 00:00:00 2001 From: huangjianwu Date: Tue, 23 Jun 2026 10:19:38 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(transcriber):=20=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=97=B6=E9=80=8F=E4=BC=A0=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=88=B0=E5=89=8D=E7=AB=AF=E5=B9=B6=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit issue #402 衍生问题:whisper 模型后台下载失败时,/transcriber_models_status 只回传 downloading/downloaded 两个布尔,failed 态被直接丢弃,于是前端表现为 「点了下载没反应、状态一直未下载、且无任何错误提示」。 后端:新增轻量模块 model_download_state 统一维护下载状态(downloading/done/ failed)与失败原因,config.py 的下载触发与状态查询共享同一份内存态;状态接口 新增 failed 字段,失败时附带 error(仓库 404、网络中断、本地路径缺 model.bin 等)。 前端:模型管理列表新增「下载失败」红色徽标 + 错误详情,按钮在失败后变为「重试」; 自定义模型项同样展示失败图标与原因;并对「本次新出现的失败」弹一次 toast 主动提示。 测试:新增 test_model_download_state 覆盖状态流转(downloading/done/failed、 失败原因透传、downloaded 覆盖 failed、重下清错、mlx key 隔离)。 已用 docker compose 启动整套栈验证:触发本地路径缺失与 HF 仓库 404 两种失败, /transcriber_models_status 均正确回传 failed:true + error。 Refs #402 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/pages/SettingPage/transcriber.tsx | 88 ++++++++++---- BillNote_frontend/src/services/transcriber.ts | 4 + backend/app/routers/config.py | 55 ++++----- .../app/transcriber/model_download_state.py | 75 ++++++++++++ backend/tests/test_model_download_state.py | 113 ++++++++++++++++++ 5 files changed, 281 insertions(+), 54 deletions(-) create mode 100644 backend/app/transcriber/model_download_state.py create mode 100644 backend/tests/test_model_download_state.py diff --git a/BillNote_frontend/src/pages/SettingPage/transcriber.tsx b/BillNote_frontend/src/pages/SettingPage/transcriber.tsx index 74a5669..0779b1e 100644 --- a/BillNote_frontend/src/pages/SettingPage/transcriber.tsx +++ b/BillNote_frontend/src/pages/SettingPage/transcriber.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -40,6 +40,9 @@ export default function Transcriber() { const [newModelName, setNewModelName] = useState('') const [newModelTarget, setNewModelTarget] = useState('') const [addingModel, setAddingModel] = useState(false) + // 已提示过的下载失败 key(whisper 用 model_size,mlx 用 mlx-{size})。 + // null 表示尚未首次加载——首次加载只建立基线、不对历史失败弹窗。 + const prevFailedRef = useRef | null>(null) // 重新拉取配置(不重置用户当前的选择),用于增删自定义模型后刷新下拉与列表 const reloadConfig = useCallback(async () => { @@ -56,6 +59,23 @@ export default function Transcriber() { setModelStatuses(data.whisper) setMlxModelStatuses(data.mlx_whisper) setMlxAvailable(data.mlx_available) + + // 下载失败主动提示:只对「本次新出现的失败」弹一次,避免轮询期间反复弹窗 + const failedNow = new Map() + data.whisper.forEach(m => m.failed && failedNow.set(m.model_size, m)) + data.mlx_whisper.forEach(m => m.failed && failedNow.set(`mlx-${m.model_size}`, m)) + if (prevFailedRef.current === null) { + // 首次加载:建立基线,不对进入页面前就已失败的项弹窗(仍会在列表里红字展示) + prevFailedRef.current = new Set(failedNow.keys()) + } else { + failedNow.forEach((m, key) => { + if (!prevFailedRef.current!.has(key)) { + const detail = m.error ? `:${m.error.slice(0, 120)}` : '' + toast.error(`模型 ${m.model_size} 下载失败${detail}`, { duration: 6000 }) + } + }) + prevFailedRef.current = new Set(failedNow.keys()) + } } catch { // 静默失败,不阻塞主流程 } @@ -290,32 +310,44 @@ export default function Transcriber() { {currentModels.map(model => (
-
- {model.model_size} - {model.downloaded ? ( - - 已下载 - - ) : model.downloading ? ( - - - 下载中 - - ) : ( - 未下载 +
+
+ {model.model_size} + {model.downloaded ? ( + + 已下载 + + ) : model.downloading ? ( + + + 下载中 + + ) : model.failed ? ( + + + 下载失败 + + ) : ( + 未下载 + )} +
+ {!model.downloaded && !model.downloading && ( + )}
- {!model.downloaded && !model.downloading && ( - + {model.failed && model.error && ( +

+ {model.error} +

)}
))} @@ -368,10 +400,18 @@ export default function Transcriber() { {status?.downloading && ( )} + {status?.failed && ( + + )}
{target}
+ {status?.failed && status?.error && ( +
+ {status.error} +
+ )}