diff --git a/CHANGELOG.md b/CHANGELOG.md index f4134ad..c7cc405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ 本项目所有重要变更记录于此。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [2.2.0] - 2026-05-09 + +主线:浏览器插件功能与 web 端 NoteForm 完整对齐;桌面客户端 UX 与错误恢复一波重炼。 + +### Added — 浏览器插件 + +- 笔记选项与 web 端 NoteForm 完整对齐: + - `style` 由自由文本改成 9 个预设下拉(minimal / detailed / academic / tutorial / xiaohongshu / life_journal / task_oriented / business / meeting_minutes),与 backend `prompt_builder.note_styles` 严格匹配(之前自由文本不命中 enum 等于没传——隐性 bug) + - `format` 完整 4 个 checkbox(toc / link / screenshot / summary,原来只有 screenshot/link) + - `extras` 文本框:拼接到 prompt 末尾的 ad-hoc 提示 +- 多模态视频理解:`video_understanding` 开关 + `video_interval`(1-30 秒)+ `grid_size`([r,c],1-10),抽帧拼图喂视觉模型,提示需选视觉模型才生效 + +### Added — 桌面客户端 + +- **首启 4 步引导**(`/onboarding`):后端连通性自检 → LLM 供应商 + 模型 → 转写引擎选择(默认推荐 Groq)→ Cookie 同步说明。完成后 `localStorage('bilinote-onboarded')` 标记,纯 web 端不打扰 +- **Sidecar 健康度面板**:右下角浮动状态点(绿/黄/红,5s 轮询 `/sys_health`),点开抽屉看最近 200 行后端日志、一键重启后端(新增 Tauri command `restart_backend_sidecar`)、复制日志 +- **启动期路径诊断**:Tauri `setup` 中检测安装路径含非 ASCII / 含空格 / 父目录不可写时,emit `backend-warning` 让前端顶端横幅显式告警,主动暴露 README 长期文字警告但无防御的"中文路径"等坑 + +### Changed + +- Whisper 默认模型 size 从 `medium`(~1.5GB)改为 `tiny`(~75MB):新装用户没主动设置时不再卡在首次大模型下载;高精度可在「音频转写配置」页主动切 +- 切到 `fast-whisper` / `mlx-whisper` 且当前 size 未下载时,「音频转写配置」页保存前 confirm 体积提示,并推荐改用在线引擎 +- Tauri sidecar 启动逻辑抽出 `spawn_backend_sidecar()`;child handle 存进 `SidecarHandle` state 以支持后续 restart +- sidecar stdout/stderr emit 时不再用 `format!("'{}'", ...)` 包引号,原文直传(前端 hook 兼容旧格式兜底剥引号) + +### Fixed + +- WhisperTranscriber 在半成品模型目录上死循环报 `Unable to open file 'model.bin'`:判定从「目录存在」改为「`model.bin` 落盘」,半成品目录会被识别并重新下载(PR `fix/backend-deploy-resilience`) +- `/api/deploy_status` 在没装 torch 的部署上 `ModuleNotFoundError: No module named 'torch'` 500:torch 改 try/except,未装时返回 `{available: false, torch_installed: false}`;transcriber 配置 + ffmpeg 也都裹 try,单项失败不再打死整个监控页(同上 PR) +- `routers/config._check_whisper_model_exists` 同步改用 `model.bin` 判定,避免「已下载」状态在监控页误报 + ## [2.1.4] - 2026-05-07 CI 工程化修复,无运行时行为变化。 diff --git a/README.md b/README.md index eb98c75..d4a8252 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

BiliNote Banner

-

BiliNote v2.1.4

+

BiliNote v2.2.0

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

@@ -53,6 +53,16 @@ BiliNote 是一个开源的 AI 视频笔记助手,支持通过哔哩哔哩、Y - 笔记顶部视频封面 Banner 展示 - 工作区和生成历史面板支持折叠/展开 +### v2.2.0 新增 + +- **浏览器插件**笔记选项与 web 端完整对齐:style 9 个预设下拉、format 4 个 checkbox、extras 文本框、多模态视频理解开关 +- **桌面客户端**首启 4 步引导(连通自检 → 供应商/模型 → 转写引擎 → Cookie 提示) +- **桌面客户端**右下角后端运行状态指示,点开看日志、一键重启 +- **桌面客户端**启动期主动检测中文 / 空格 / 不可写安装路径,弹横幅告警 +- Whisper 默认 size 从 medium(~1.5GB)改为 tiny(~75MB);切大模型时显式 confirm +- 修:whisper 半成品模型目录死循环;`/deploy_status` 在没装 torch 的部署 500 +- 详见 [CHANGELOG.md](./CHANGELOG.md) + ### v2.1.4 修订 - CI:桌面端 Tauri 构建去掉 Linux(17m+ 慢线退役;Linux 用户继续走 Docker 镜像) diff --git a/backend/app/routers/config.py b/backend/app/routers/config.py index 0fecc48..2dcf094 100644 --- a/backend/app/routers/config.py +++ b/backend/app/routers/config.py @@ -87,10 +87,13 @@ _downloading: dict[str, str] = {} # model_size -> status ("downloading" | "done def _check_whisper_model_exists(model_size: str, subdir: str = "whisper") -> bool: - """检查指定 whisper 模型是否已下载到本地。""" + """检查指定 whisper 模型是否已完整下载到本地。 + + 只看目录会把"上次下载中断剩下的空目录"误判为已下载;以 model.bin 落盘为准。 + """ model_dir = get_model_dir(subdir) - model_path = os.path.join(model_dir, f"whisper-{model_size}") - return Path(model_path).exists() + model_path = Path(os.path.join(model_dir, f"whisper-{model_size}")) + return (model_path / "model.bin").exists() @router.get("/transcriber_models_status") @@ -227,30 +230,49 @@ async def sys_check(): @router.get("/deploy_status") async def deploy_status(): - """返回部署监控所需的所有状态信息""" - import torch + """返回部署监控所需的所有状态信息。 + + 每一项都做容错:torch 没装 / Whisper 配置读取失败 / FFmpeg 不可用 都不应让整个 + endpoint 500 把监控页打死。 + """ import os - - # CUDA 状态 - cuda_available = torch.cuda.is_available() - cuda_info = { - "available": cuda_available, - "version": torch.version.cuda if cuda_available else None, - "gpu_name": torch.cuda.get_device_name(0) if cuda_available else None, - } - - # Whisper 模型状态(从配置文件读取,与前端设置同步) - transcriber_cfg = transcriber_config_manager.get_config() - model_size = transcriber_cfg["whisper_model_size"] - transcriber_type = transcriber_cfg["transcriber_type"] - + + # CUDA 状态:torch 是 fast-whisper 路径才需要的依赖;轻量部署可能没装 + cuda_info = {"available": False, "version": None, "gpu_name": None, "torch_installed": False} + try: + import torch + cuda_info["torch_installed"] = True + cuda_available = torch.cuda.is_available() + cuda_info["available"] = cuda_available + if cuda_available: + cuda_info["version"] = torch.version.cuda + try: + cuda_info["gpu_name"] = torch.cuda.get_device_name(0) + except Exception as e: + logger.warning(f"读取 GPU 名称失败: {e}") + except ImportError: + # torch 未安装:保持 available=False;插件 / web 监控页能识别 + pass + except Exception as e: + logger.warning(f"CUDA 状态检测失败: {e}") + + # Whisper 配置(任何失败回落到空,监控页不应被这个打死) + model_size = None + transcriber_type = None + try: + transcriber_cfg = transcriber_config_manager.get_config() + model_size = transcriber_cfg.get("whisper_model_size") + transcriber_type = transcriber_cfg.get("transcriber_type") + except Exception as e: + logger.warning(f"读取转写器配置失败: {e}") + # FFmpeg 状态 try: ensure_ffmpeg_or_raise() ffmpeg_ok = True - except: + except Exception: ffmpeg_ok = False - + return R.success(data={ "backend": {"status": "running", "port": int(os.getenv("BACKEND_PORT", 8483))}, "cuda": cuda_info, diff --git a/backend/app/transcriber/whisper.py b/backend/app/transcriber/whisper.py index fca49f6..217394f 100644 --- a/backend/app/transcriber/whisper.py +++ b/backend/app/transcriber/whisper.py @@ -50,12 +50,20 @@ class WhisperTranscriber(Transcriber): model_dir = get_model_dir("whisper") model_path = os.path.join(model_dir, f"whisper-{model_size}") - if not Path(model_path).exists(): - logger.info(f"模型 whisper-{model_size} 不存在,开始下载...") + # 仅看目录存在不够:一次失败的 / 中断的下载会留下空 / 半成品目录, + # 后面 WhisperModel 加载时会以 'Unable to open file model.bin' 抛错。 + # 必须以 model.bin 这个核心权重文件落盘为准。 + 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} 不存在,开始下载...") repo_id = MODEL_MAP[model_size] model_path = snapshot_download( repo_id, - local_dir=model_path, ) logger.info("模型下载完成")