跳转至

Desktop release plan — VoiceStudio

{#desktop-release-plan-voicestudio}

历史性/仅手工操作的 Tauri 计划。 以下所有内容仅为审计或手工重建最终 v0.5.3 Tauri 版本而保留。其构建、签名和发布说明已不再维护, 不得用于当前版本发布。Electron 是唯一受维护的桌面应用;请遵循 RELEASING.md 完成所有当前的构建和发布工作。 其中提到的文件(frontend/src-tauri/、.github/workflows/release.yml、 scripts/desktop-prod.sh)已随 Tauri 外壳一起移除;请从 git 历史的 v0.5.3 tag 中读取。

一个可交付的 macOS(最终将跨平台)桌面版本:用户将 .app 拖入 Applications, 双击一次,然后所有其他操作都在 UI 中完成——依赖运行时、模型权重、首次运行同意, 全部在应用内完成。

技术栈:Tauri v2 + FastAPI sidecar + PyInstaller。目标:约 500 MB、已签名并 公证的 arm64 DMG,后续再提供对应的 Windows .msi/.nsis 包。 大型可选载荷(CUDA 库、额外模型包)以单独的 lazy-download tarball 形式提供, 不包含在基础 DMG 中。


Target architecture

{#target-architecture}

层级 内容 是否随 DMG 发布?
Tauri v2 shell(Rust) 原生窗口、进程生命周期、文件系统路径 是
前端 bundle React/Vite 构建产物位于 .app/Contents/Resources/dist/ 是
FastAPI sidecar 二进制 PyInstaller 冻结的 omnivoice-backend,包含 Python + torch + mlx + soundfile + demucs + yt_dlp + omnivoice TTS 是(约 400–500 MB bundle)
ffmpeg .app/Contents/Resources/bin/ 中的 arm64 二进制 是(约 20 MB)
模型权重(VoiceStudio TTS、MLX Whisper) ~/Library/Application Support/OmniVoice/models/ 否 — 首次运行时下载
可选引擎包(VoxCPM2 CUDA、pyannote、MOSS-TTS) 通过 GitHub Releases 清单单独提供的 .tar.gz 否 — 用户选择加入后首次运行时下载

目标 DMG 大小:约 500 MB。 首次运行模型下载:约 2.4 GB,一次性(TTS 模型是唯一必需的下载; ASR/转录模型是可选的,按平台精选,由向导或 Settings 按需安装)。


Four key techniques

{#four-key-techniques}

1. Sidecar port-reuse dance(开发体验 + 崩溃恢复)

{#1-sidecar-port-reuse-dance-dev-ergonomics-crash-recovery}

Tauri 的启动流程:

  1. 检查 127.0.0.1:17493/health 是否响应。
  2. 如果是,则验证 JSON 结构:status == "healthy"、model_loaded: bool、gpu_available: bool。如果有效,则附加到现有进程,而不是新启动一个。
  3. 如果旧端口(8000)上有孤儿进程,通过 lsof -ti :8000 | xargs kill -9 将其结束。
  4. 否则,通过 Tauri 的 externalBin 启动冻结后的后端 sidecar。
  5. 应用关闭时:发送 SIGTERM,等待 2 秒,如果仍存活则发送 SIGKILL。

为什么这很重要: 你在开发时仍然可以运行 uv run uvicorn …,Tauri 会配合工作。 重启崩溃的后端只需一次端口探测,而不是进程强杀的舞蹈。

我们需要改动的文件: frontend/src-tauri/src/lib.rs(将当前的 find_project_root / uv run 逻辑替换为端口探测 + externalBin 启动)。

2. tqdm → SSE progress,用于 HuggingFace 下载

{#2-tqdm-sse-progress-for-huggingface-downloads}

我们的 backend/utils/hf_progress.py 大约 80 行:

from huggingface_hub.utils import _tqdm as hf_tqdm_module
from tqdm.auto import tqdm as base_tqdm

class TrackedTqdm(base_tqdm):
    def update(self, n=1):
        super().update(n)
        callback(self.desc or "download", self.n, self.total)

# Monkey-patch once at startup — every hf_hub_download() across every
# library (transformers, diffusers, accelerate, mlx_whisper) now reports.
hf_tqdm_module._original_tqdm_class = hf_tqdm_module.tqdm_class
hf_tqdm_module.tqdm_class = TrackedTqdm

将回调接入新的 /setup/download/stream SSE 端点。React 通过 EventSource 订阅,为每个文件渲染进度条,并在模型就绪前锁定 UI 的其余部分。

调用方代码零改动。 每个 mlx_whisper.load_model(...) 现在都会自动报告进度。

我们需要新增的文件: backend/utils/hf_progress.py + backend/api/routers/setup.py, 其中包含 /setup/status 和 /setup/download/stream 端点。前端 src/pages/SetupWizard.jsx 会在 /setup/status 显示模型尚未就绪时渲染。

3. 两级二进制:小体积基础 DMG + 按需可选载荷

{#3-two-tier-binary-small-base-dmg-lazy-optional-payloads}

我们从 Apple Silicon 构建中排除所有 nvidia.* wheel(节省约 2 GB),并将 CUDA 库 放入单独的 cuda-libs-cu128-v1.tar.gz(约 2 GB)中,由发布版上的 cuda-libs.json 引用。

对我们而言: - 基础 DMG 仅提供 MPS + MLX 路径。 排除 nvidia.*、triton、flash-attn, 以及 spec 中任何 CUDA 特定内容。 - 可选包:VoxCPM2(需要 CUDA)。默认不安装。Model Catalogue → 选择 “Install VoxCPM2” 会触发从我们的 voxcpm2-cu128-v1.tar.gz 发布资产下载。 - 可选包:pyannote(受 HF token 门控)。默认关闭。Settings → Speaker diarisation → “Enable” 会提示输入 HF token,然后下载并安装。 - 可选包:MOSS-TTS-Nano。 同样的模式。

cuda-libs.json 的清单格式:

{
  "url": "https://github.com/.../releases/download/v0.1.0/voxcpm2-cu128-v1.tar.gz",
  "sha256": "…",
  "size_bytes": 2100000000,
  "extract_to": "packs/voxcpm2"
}

4. PyInstaller spec + 运行时钩子

{#4-pyinstaller-spec-runtime-hooks}

起点:我们现有的 backend.spec(本次会话中已重写)。

我们需要两个运行时钩子:

  • pyi_rth_numpy_compat.py — 修复 PyInstaller 遗漏的 numpy 兼容性 shim。
  • pyi_rth_torch_compiler_disable.py — 禁用 torch.compile 中在冻结导入下会出错的代码路径。

排除列表(在 Apple Silicon 构建中节省空间):

excludes = [
    'nvidia.cublas', 'nvidia.cudnn', 'nvidia.cuda_runtime',
    'nvidia.nccl', 'nvidia.nvtx',
    'triton', 'flash_attn',
    'tkinter', 'matplotlib.backends._tkagg',
]

需要添加的 hidden-imports(迭代进行 — 随着 PyInstaller 报错逐步修复): - mlx.core、mlx.nn - omnivoice、omnivoice.models.omnivoice - soundfile._soundfile - demucs.separate、demucs.pretrained - huggingface_hub.repocard_data


Phased execution plan

{#phased-execution-plan}

每个阶段都会产出一个可测试的工件。在当前阶段完成端到端验证之前,不要进入下一阶段。

Phase A — 冻结后端可运行(3–5 小时,风险最高)

交付物:dist/omnivoice-backend/omnivoice-backend 可独立运行,并提供完整 API。

  1. 在 backend/hooks/ 中添加两个运行时钩子。
  2. 更新 backend.spec,加入排除列表和运行时钩子路径。
  3. 运行 uv run pyinstaller backend.spec --noconfirm --clean。
  4. 反复调整 hidden-imports,直到 ./dist/omnivoice-backend/omnivoice-backend 能干净启动,且 /system/info 返回 200。
  5. 端到端冒烟测试:转写 Fireship fixture → 生成西班牙语配音 → 验证输出音频存在。

验证:对冻结后的二进制文件运行 curl -sf http://127.0.0.1:17493/system/info,应在 <2 秒内返回 JSON。

失败路径:如果 5 小时后 PyInstaller 仍无法完成打包,则转向“在 .app/Contents/Resources/ 内携带一个可移植的 .venv”方案——不太美观,但稳定可靠。会增加约 300 MB,但能绕开 PyInstaller 的麻烦。

Phase B — Tauri 启动冻结的 sidecar(2 小时)

交付物:bun run desktop 启动一个使用冻结后端的开发版 .app,而不是 uv run。

  1. 重写 frontend/src-tauri/src/lib.rs 的 setup 钩子:
  2. 先检查端口 17493(端口复用舞蹈)。
  3. 如果空闲,通过 Tauri 的 shell_plugin::Command 启动打包进来的 Contents/Resources/backend/omnivoice-backend。
  4. 杀掉 8000 端口上的遗留孤儿进程。
  5. 配置 tauri.conf.json 的 bundle.resources,使其包含 ../../dist/omnivoice-backend/** 和 binaries/ffmpeg。
  6. 将后端默认端口从 8000 改为 17493(新命名空间,与其他开发工具的冲突更少)。

验证:用 bun run desktop 启动开发版应用——窗口打开、分段表格加载、测试 ingest-url 可用。

Phase C — 首次运行模型下载 UI(4–6 小时)

交付物:在没有缓存 HF 模型的机器上,全新应用能引导用户完成下载并显示实时进度。

  1. 实现 hf_progress.py——约 80 行代码。
  2. 新增 backend/api/routers/setup.py:
  3. GET /setup/status → { models_ready: bool, missing: [...], disk_free_gb: number }。
  4. GET /setup/download/stream → SSE:先发送 { type: "progress", file, bytes, total, pct },然后发送 { type: "done" }。
  5. 前端 src/pages/SetupWizard.jsx:
  6. 当 /setup/status 显示模型缺失时展示。
  7. 由 SSE 流驱动的逐文件进度条。
  8. 磁盘空间检查;剩余空间 <2 GB 时显示错误状态,低于 10 GB 时显示警告。
  9. 网络失败时支持重试。
  10. 应用级路由守卫:如果 setupWizardNeeded 为真,渲染 <SetupWizard> 而不是 <Launchpad>。

验证:移动/重命名 ~/Library/Application Support/OmniVoice/models/——启动应用——向导出现——进度条前进——模型下载完成——界面解锁。

Phase D — DMG 构建 + 干净机器测试(2–3 小时)

交付物:一个已签名但未公证的 DMG,在全新 Mac 上右键 → 打开后即可使用。

  1. bun run tauri build(通过我们将新增的 scripts/build_desktop.sh)。
  2. 产物:frontend/src-tauri/target/release/bundle/dmg/VoiceStudio_0.1.0_aarch64.dmg。
  3. 复制到一个全新的 macOS 用户账户(或另一台 Mac)。
  4. 右键 → 打开一次,完成向导,为 Fireship fixture 生成配音。
  5. 修复所有出现的问题。

验证:未安装任何开发工具的目标 Mac 能端到端地把 YouTube URL 配音为目标语言。

Phase E —(可选)签名 + 公证

交付物:无需 Gatekeeper 覆盖即可打开的 DMG。

要求: - Apple Developer ID(约 $99/年)。 - 代码签名证书、App Store Connect API 密钥。 - GitHub Actions 工作流(.github/workflows/release.yml): - apple-actions/import-codesign-certs@v3 - tauri-apps/tauri-action@v0.6,使用 APPLE_SIGNING_IDENTITY + APPLE_API_KEY + APPLE_API_ISSUER + APPLE_PROVIDER_SHORT_NAME - 明确增加 DMG 重新公证步骤——macOS 15 Sequoia 会拒绝那些包裹了已签名 .app、但自身未经过公证的 DMG。需要对 DMG 运行 xcrun notarytool submit --wait + xcrun stapler staple,然后作为发布资产重新上传。


跨平台扩展(未来)

目标平台:macOS arm64 + macOS x64 + Windows x64(.msi、.nsis、.exe)。Linux 为尽力而为。

一旦 Phase A–D 全部通过,我们可以通过扩展 CI 矩阵来覆盖这些平台:

目标 运行器 PyInstaller 变体 备注
macOS Apple Silicon macos-14 (ARM) backend.spec (MPS/MLX) 我们的主要目标
macOS Intel macos-13 backend.spec (MPS/x64 torch) 无 MLX——回退到 CPU Whisper
Windows x64 (NVIDIA) windows-2022 backend.spec + --bootloader + CUDA 需要第二个 CUDA tarball(cuda-libs-cu128-v1.tar.gz)
Linux x64 ubuntu-22.04 backend.spec 尽力而为——未经测试

每个平台的首次构建耗时最长(PyInstaller 的 hidden-import 调优是随操作系统而异的)。后续构建会复用该 spec。

实话实说: Windows 完全是另一套麻烦——mlx_whisper 在 Windows 上不存在,pyannote + soundfile 的 wheel 来源不同,签名需要单独的 Windows 代码签名证书。每增加一个平台,需要 增加 1 个完整的开发时段。

对于我们当前的目标(朋友同款 M2 Air),做到 Phase D 即可。等 macOS 版本稳定后,再讨论跨平台。


风险与应对措施

风险 可能性 应对措施
PyInstaller 无法打包 torch 的 Metal 库 中 对 torch + MLX 使用 collect_all(...) 调用。备用方案:便携式 .venv 方案
torch.compile 在冻结导入下崩溃 必然 pyi_rth_torch_compiler_disable.py 运行时钩子
DMG 体积 >800 MB 中 将 nvidia/triton/matplotlib 排除在 spec 之外;将可选包延迟到按需下载
首次下载中途失败 中 SSE 重试 + 可断点续传的 hf_hub_download(原生支持)。提前显示磁盘空间检查
Gatekeeper 阻止未签名应用 必然 将右键 → 打开记录为一次性步骤。长期方案:购买 Apple Developer ID
用户朋友的磁盘剩余空间 <2 GB 低 在 /setup/status 中预先检查。空间不足时拒绝开始下载。引导用户清理空间
Apple Silicon 构建在 Intel Mac 上运行 可能 在安装程序和应用顶部栏中发出警告。在没有实际 Intel 构建之前,不要承诺跨架构支持

成功标准(针对本计划,按阶段)

  • A ✅ 当 ./dist/omnivoice-backend/omnivoice-backend 在干净的 shell 中于 3 秒内启动并提供 /system/info 服务时。
  • B ✅ 当 bun run desktop 启动的窗口使用冻结的二进制文件(而非 uv run),且所有核心 API 正常工作时。
  • C ✅ 当 删除模型目录后启动,显示一个无需任何终端交互即可完成并达到可用状态的向导时。
  • D ✅ 当 一台无关的 M 系列 Mac 在未安装任何开发者工具的情况下,端到端运行 DMG(Fireship 片段 → 西班牙语配音),只需右键单击 → 打开一次时。

关键实现文件

  • backend.spec — PyInstaller spec 文件
  • backend/utils/hf_progress.py — 用于下载进度的 tqdm monkey-patch
  • backend/hooks/pyi_rth_numpy_compat.py — numpy 运行时钩子
  • backend/hooks/pyi_rth_torch_compiler_disable.py — 禁用 torch.compile
  • frontend/src-tauri/src/lib.rs — sidecar 启动 + 端口复用
  • frontend/src-tauri/tauri.conf.json — 打包 + 更新器配置
  • scripts/build_desktop.sh — 构建入口
  • .github/workflows/release.yml — 完整发布流水线(签名、公证、DMG 重新公证)

v1 范围之外

  • 自动更新(Tauri 有 tauri-plugin-updater,但需要签名 + 托管 latest.json)。
  • 自动崩溃报告(需要类似 Sentry 的端点)。
  • 任何形式的用户遥测。
  • 应用内反馈表单。
  • 公证安装程序 — 推迟至 E 阶段。
  • 跨平台构建 — 参见“跨平台扩展”一节。
  • Homebrew cask — 以后可能支持,不构成阻塞。

时间线(诚实评估,单人开发)

阶段 小时 置信度
A — 冻结后端 3–5 高
B — Tauri sidecar + 端口复用 2 高
C — 首次运行向导 + hf 进度 3–4 高(80 行代码 + UI)
D — DMG + 干净机器测试 2–3 中(Gatekeeper 右键操作)
总计(仅 macOS arm64) 10–14 可分 2–3 个时段推进
E — 签名 + 公证 +3 受阻于 Apple Developer ID
跨平台(每个 OS) +8 各 OS 的工作量

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 om/VoiceStudio