From 1cc7f38e14d3f9f976ccecdaab79c0a5da1f5a2c Mon Sep 17 00:00:00 2001 From: huangjianwu Date: Fri, 22 May 2026 11:26:49 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(backend):=20=E5=8D=87=E7=BA=A7=20ctrans?= =?UTF-8?q?late2=204.5.0=E2=86=924.6.0=20=E4=BF=AE=E5=A4=8D=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=90=AF=E5=8A=A8=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docker 容器反复重启,启动即报: ImportError: libctranslate2-*.so.4.5.0: cannot enable executable stack as shared object requires: Invalid argument 根因:ctranslate2 4.5.0 预编译 wheel 把共享库标记为「需要可执行栈」, 新内核 / glibc 2.41+ 加载时拒绝并返回 EINVAL。faster-whisper 在 whisper.py 顶层 import,import 失败直接拖垮整个后端启动 → 重启死循环。 ctranslate2 4.6.0 加入 noexecstack 链接标志(OpenNMT/CTranslate2 #1852、 #1861)从 wheel 层根治。faster-whisper 1.1.1 依赖 ctranslate2<5,>=4.0, 4.6.0 兼容;同时覆盖 web / GPU / 桌面 三条构建链。 Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index b29b0ab..b0d2326 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -24,7 +24,7 @@ click-repl==0.3.0 colorama==0.4.6 coloredlogs==15.0.1 cssselect2==0.8.0 -ctranslate2==4.5.0 +ctranslate2==4.6.0 distro==1.9.0 dnspython==2.7.0 email_validator==2.2.0 From 261c95cf12f26c26f58b562ac9fd6f832eb018a8 Mon Sep 17 00:00:00 2001 From: huangjianwu Date: Fri, 22 May 2026 11:27:03 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(transcriber):=20whisper=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E4=B8=8B=E8=BD=BD/=E5=8A=A0=E8=BD=BD=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=B5=B0=20HF=20cache=20=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前用 modelscope 下到自定义目录 whisper-{size}/ 再把该路径传给 WhisperModel。但 faster-whisper 1.1.1 只要 path 含 '/' 就当成 HF repo_id 处理,没有「本地目录直接返回」分支 → 在线请求失败后 fallback local_files_only,又因 modelscope 布局命不中 HF cache → LocalEntryNotFound, 误导用户以为是「离线模式」。 改为下载与加载路径对齐: - 下载:huggingface_hub.snapshot_download(cache_dir=model_dir),落到 HF cache 布局 models--Systran--faster-whisper-{size}/snapshots// - 加载:WhisperModel(model_size_or_path=size, download_root=model_dir), 让 faster-whisper 自己映射到 Systran/faster-whisper-* 并命中同一 cache - 完整性检测 / 损坏自愈(_purge_cache) 同步按 HF cache 布局,并兼容老 modelscope 目录(向后兼容已下载的老用户) HF_ENDPOINT 已在 Dockerfile 指向 hf-mirror.com,国内可用。 Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/routers/config.py | 56 +++++++++++++------ backend/app/transcriber/whisper.py | 87 ++++++++++++++---------------- 2 files changed, 78 insertions(+), 65 deletions(-) diff --git a/backend/app/routers/config.py b/backend/app/routers/config.py index 97687f6..c816fc5 100644 --- a/backend/app/routers/config.py +++ b/backend/app/routers/config.py @@ -119,12 +119,21 @@ _downloading: dict[str, str] = {} # model_size -> status ("downloading" | "done def _check_whisper_model_exists(model_size: str, subdir: str = "whisper") -> bool: """检查指定 whisper 模型是否已下载完整到本地。 - 必须 model.bin 落盘才算完成,仅有空目录或半成品不能算「已下载」—— - 否则监控页会显示绿勾但加载时报「Unable to open file 'model.bin'」。 + faster-whisper 把模型缓存在 HF cache 布局下: + /models--Systran--faster-whisper-{size}/snapshots//model.bin + 必须能在某个 snapshot 目录里找到 model.bin 才算完成。 + (历史 modelscope 布局 /whisper-{size}/model.bin 也兼容识别。) """ - model_dir = get_model_dir(subdir) - model_path = os.path.join(model_dir, f"whisper-{model_size}") - return (Path(model_path) / "model.bin").exists() + model_dir = Path(get_model_dir(subdir)) + # HF cache 布局 + hf_repo_dir = model_dir / f"models--Systran--faster-whisper-{model_size}" / "snapshots" + if hf_repo_dir.exists(): + for snapshot in hf_repo_dir.iterdir(): + if (snapshot / "model.bin").exists(): + return True + # 历史 modelscope 布局(向后兼容老用户) + legacy = model_dir / f"whisper-{model_size}" / "model.bin" + return legacy.exists() def _check_mlx_whisper_model_exists(model_size: str) -> bool: @@ -189,24 +198,37 @@ class ModelDownloadRequest(BaseModel): def _do_download_whisper(model_size: str): - """后台下载 faster-whisper 模型。""" - from app.transcriber.whisper import MODEL_MAP - from modelscope import snapshot_download + """后台下载 faster-whisper 模型。 + + 直接走 huggingface_hub.snapshot_download,把模型放到 HF cache 布局里—— + 这样 faster-whisper 加载时(WhisperModel(model_size_or_path=size_name, + download_root=model_dir))能直接命中缓存,跟加载路径完全对齐。 + """ + from huggingface_hub import snapshot_download try: _downloading[model_size] = "downloading" model_dir = get_model_dir("whisper") - model_path = os.path.join(model_dir, f"whisper-{model_size}") - # 用 model.bin 判定而非目录存在:半成品目录不能算「已下载」 - if (Path(model_path) / "model.bin").exists(): + + # 已经下好就不重复下 + if _check_whisper_model_exists(model_size, "whisper"): _downloading[model_size] = "done" return - repo_id = MODEL_MAP.get(model_size) - if not repo_id: - _downloading[model_size] = "failed" - return - logger.info(f"开始下载 whisper 模型: {model_size}") - snapshot_download(repo_id, local_dir=model_path) + repo_id = f"Systran/faster-whisper-{model_size}" + logger.info(f"开始下载 whisper 模型: {repo_id}") + # 跟 faster-whisper utils.py 用同样的 allow_patterns,避免多下无关文件; + # 不传 local_dir 让它走 HF 默认 cache 布局(与加载逻辑对齐) + snapshot_download( + repo_id, + cache_dir=model_dir, + allow_patterns=[ + "config.json", + "preprocessor_config.json", + "model.bin", + "tokenizer.json", + "vocabulary.*", + ], + ) logger.info(f"whisper 模型下载完成: {model_size}") _downloading[model_size] = "done" except Exception as e: diff --git a/backend/app/transcriber/whisper.py b/backend/app/transcriber/whisper.py index e212861..5308579 100644 --- a/backend/app/transcriber/whisper.py +++ b/backend/app/transcriber/whisper.py @@ -11,8 +11,6 @@ from events import transcription_finished from pathlib import Path import os import shutil -from tqdm import tqdm -from modelscope import snapshot_download ''' @@ -20,19 +18,16 @@ from modelscope import snapshot_download ''' logger=get_logger(__name__) -MODEL_MAP={ - "tiny": "pengzhendong/faster-whisper-tiny", - 'base':'pengzhendong/faster-whisper-base', - 'small':'pengzhendong/faster-whisper-small', - 'medium':'pengzhendong/faster-whisper-medium', - 'large-v1':'pengzhendong/faster-whisper-large-v1', - 'large-v2':'pengzhendong/faster-whisper-large-v2', - 'large-v3':'pengzhendong/faster-whisper-large-v3', - 'large-v3-turbo':'pengzhendong/faster-whisper-large-v3-turbo', -} - +# 历史遗留:之前用 modelscope 下载到自定义目录然后把路径传给 WhisperModel。 +# 但 faster-whisper 1.1.1 的 download_model(utils.py:76)逻辑是: +# 只要 size_or_id 里含 "/" 就当 HF repo_id 处理,没有「本地目录直接返回」分支。 +# 我们传 /app/models/whisper/whisper-tiny 进去 → 被当成不存在的 HF repo → +# 在线请求失败 → fallback local_files_only=True → HF cache 找不到(因为是 +# modelscope 目录布局不是 HF)→ LocalEntryNotFoundError,误导说"离线模式"。 +# 解法:彻底让 faster-whisper 自己处理下载——传 size name,配 download_root +# 作为 HF cache 根目录,HF_ENDPOINT 已经在 Dockerfile 里指到 hf-mirror.com, +# 国内能用。删掉 modelscope 那一套,避免布局不匹配。 class WhisperTranscriber(Transcriber): - # TODO:修改为可配置 def __init__( self, model_size: str = "base", @@ -48,44 +43,40 @@ class WhisperTranscriber(Transcriber): print('没有 cuda 使用 cpu进行计算') self.compute_type = compute_type or ("float16" if self.device == "cuda" else "int8") + self.model_size = model_size model_dir = get_model_dir("whisper") - model_path = os.path.join(model_dir, f"whisper-{model_size}") - repo_id = MODEL_MAP[model_size] - - # 第一步:目录 / model.bin 不在 → 下载。 - # 关键判据用 model.bin 而不是目录存在:首次下载若被打断(网络中断 / 磁盘满 / - # 容器被 kill)会留下半成品目录,只看目录存在会跳过下载。 - model_bin = Path(model_path) / "model.bin" - if not model_bin.exists(): - if Path(model_path).exists(): - logger.warning(f"模型目录 {model_path} 存在但 model.bin 缺失(上次下载未完成),重新下载") - else: - logger.info(f"模型 whisper-{model_size} 不存在,开始下载...") - model_path = snapshot_download(repo_id, local_dir=model_path) - logger.info("模型下载完成") - - # 第二步:加载。model.bin 可能存在但【内容截断】(下载到一半被 kill), - # 此时 WhisperModel() 会抛 "File model.bin is incomplete: failed to read a buffer..."。 - # 捕获后删掉损坏目录、重新下载、再试一次——自愈,避免 500 死循环。 try: - self.model = WhisperModel( - model_size_or_path=model_path, - device=self.device, - compute_type=self.compute_type, - download_root=model_dir, - ) + self.model = self._build_model(model_size, model_dir) except Exception as e: - logger.warning(f"加载 whisper-{model_size} 失败(疑似模型文件损坏 / 截断):{e};删除后重新下载") - shutil.rmtree(model_path, ignore_errors=True) - model_path = snapshot_download(repo_id, local_dir=model_path) - logger.info("模型重新下载完成,重试加载") - self.model = WhisperModel( - model_size_or_path=model_path, - device=self.device, - compute_type=self.compute_type, - download_root=model_dir, - ) + # 自愈:损坏 / 截断 / 半成品 cache → 删掉对应 HF cache 重下一次 + logger.warning(f"加载 whisper-{model_size} 失败:{e};清理 cache 后重新下载") + self._purge_cache(model_dir, model_size) + self.model = self._build_model(model_size, model_dir) + + def _build_model(self, model_size: str, model_dir: str) -> WhisperModel: + return WhisperModel( + model_size_or_path=model_size, # 传 size name,让 faster-whisper 自己映射到 Systran/faster-whisper-* + device=self.device, + compute_type=self.compute_type, + download_root=model_dir, + ) + + @staticmethod + def _purge_cache(model_dir: str, model_size: str) -> None: + """删掉 HF cache 里这个 size 对应的 snapshot 目录,强制下次重新下载。 + + HF cache 布局:/models--Systran--faster-whisper-{size}/ + 没找到也不报错——可能用户改了 endpoint 或者 cache 布局变了。 + """ + candidates = [ + Path(model_dir) / f"models--Systran--faster-whisper-{model_size}", + Path(model_dir) / f"whisper-{model_size}", # 历史 modelscope 目录,顺手清掉 + ] + for path in candidates: + if path.exists(): + logger.info(f"清理损坏 cache: {path}") + shutil.rmtree(path, ignore_errors=True) @staticmethod def is_torch_installed() -> bool: try: From b740e70068354873581af1510b0a86d464dcc438 Mon Sep 17 00:00:00 2001 From: huangjianwu Date: Fri, 22 May 2026 11:40:04 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(desktop):=20=E6=9E=84=E5=BB=BA=E6=97=B6?= =?UTF-8?q?=E4=BB=8E=20tag=20=E6=B3=A8=E5=85=A5=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E4=BA=A7=E7=89=A9=E7=89=88=E6=9C=AC=E6=81=92?= =?UTF-8?q?=E4=B8=BA=202.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面端构建产物(.dmg/.msi 文件名 + app 内部版本)一直是 2.0.0: Tauri 取 tauri.conf.json 的静态 version 字段作为产物版本,而 Release 工作流只把 tag 名用作 Release 标题,没同步到 conf → 产物版本与 Release 版本错位。 在 pnpm tauri build 前新增 Sync version 步骤,从 github.ref_name(形如 v2.3.2,去掉前缀 v)注入版本到 tauri.conf.json。以后每次 tag 发版自动 对齐;workflow_dispatch 手动构建无 tag 时跳过,保留静态值不破坏。 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/main.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe45181..909b5b2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -79,6 +79,19 @@ jobs: key: ${{ runner.os }}-cargo-${{ hashFiles('BillNote_frontend/src-tauri/Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo- + # 从 tag 注入版本号到 tauri.conf.json:Tauri 取该文件的静态 version 作为 + # 产物版本,不同步的话构建产物会恒为 conf 里写死的值(此前的 2.0.0)。 + # github.ref_name 形如 v2.3.2,去掉前缀 v。workflow_dispatch(无 tag)时跳过,保留静态值。 + - name: Sync version from tag + if: startsWith(github.ref, 'refs/tags/v') + working-directory: BillNote_frontend + shell: bash + run: | + VERSION="${GITHUB_REF_NAME#v}" + echo "Injecting version $VERSION into tauri.conf.json" + node -e "const f='src-tauri/tauri.conf.json'; const fs=require('fs'); const j=JSON.parse(fs.readFileSync(f,'utf8')); j.version=process.argv[1]; fs.writeFileSync(f, JSON.stringify(j,null,2)+'\n');" "$VERSION" + node -e "console.log('tauri.conf.json version =', require('./src-tauri/tauri.conf.json').version)" + # 打包 Tauri 应用 - name: Build Tauri App working-directory: BillNote_frontend From 3e28f1fe38e2675920eed7e83886be3a97a5bdb5 Mon Sep 17 00:00:00 2001 From: huangjianwu Date: Fri, 22 May 2026 11:40:55 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20v2.3.2=20CHANGELOG=20+=20README=20+?= =?UTF-8?q?=20tauri=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker 启动崩溃 / whisper 模型路径 / 桌面端版本号 修复版本。 Co-Authored-By: Claude Opus 4.7 (1M context) --- BillNote_frontend/src-tauri/tauri.conf.json | 2 +- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/BillNote_frontend/src-tauri/tauri.conf.json b/BillNote_frontend/src-tauri/tauri.conf.json index 79bf8e2..65f876e 100644 --- a/BillNote_frontend/src-tauri/tauri.conf.json +++ b/BillNote_frontend/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "BiliNote", - "version": "2.0.0", + "version": "2.3.2", "identifier": "com.jefferyhuang.bilinote", "build": { "frontendDist": "../dist", diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca0ac1..9747dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 本项目所有重要变更记录于此。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [2.3.2] - 2026-05-22 + +### Fixed + +- **后端启动崩溃(Docker)**:`python:3.11-slim` 基础镜像升级到 Debian 13 / glibc 2.41 后,`ctranslate2` 4.5.0 预编译库带「可执行栈」标记被 glibc 拒绝加载(`cannot enable executable stack ... Invalid argument`)。由于 `from faster_whisper import WhisperModel` 在顶层 import,import 失败直接拖垮整个后端启动 → 容器反复重启。升级 `ctranslate2` 4.5.0→4.6.0(wheel 加入 `noexecstack` 链接标志,从二进制层根治) +- **whisper 模型误报「离线模式找不到模型」**:下载(modelscope 自定义目录)与加载(faster-whisper HF cache)布局不一致导致命不中缓存。统一为下载 / 加载 / 完整性检测 / 损坏自愈都走 HF cache 布局,并向后兼容老 modelscope 目录 +- **桌面端构建产物版本恒为 2.0.0**:Release 工作流在 `pnpm tauri build` 前从 git tag 注入版本号到 `tauri.conf.json`,使产物版本与 Release 版本对齐 + ## [2.3.1] - 2026-05-22 ### Changed diff --git a/README.md b/README.md index 18b4af1..44d9c40 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

BiliNote Banner

-

BiliNote v2.3.1

+

BiliNote v2.3.2

AI 视频笔记生成工具 让 AI 为你的视频做笔记