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.3tag 中读取。
一个可交付的 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 的启动流程:
- 检查
127.0.0.1:17493/health是否响应。 - 如果是,则验证 JSON 结构:
status == "healthy"、model_loaded: bool、gpu_available: bool。如果有效,则附加到现有进程,而不是新启动一个。 - 如果旧端口(8000)上有孤儿进程,通过
lsof -ti :8000 | xargs kill -9将其结束。 - 否则,通过 Tauri 的
externalBin启动冻结后的后端 sidecar。 - 应用关闭时:发送 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。
- 在
backend/hooks/中添加两个运行时钩子。 - 更新
backend.spec,加入排除列表和运行时钩子路径。 - 运行
uv run pyinstaller backend.spec --noconfirm --clean。 - 反复调整 hidden-imports,直到
./dist/omnivoice-backend/omnivoice-backend能干净启动,且/system/info返回 200。 - 端到端冒烟测试:转写 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。
- 重写
frontend/src-tauri/src/lib.rs的setup钩子: - 先检查端口 17493(端口复用舞蹈)。
- 如果空闲,通过 Tauri 的
shell_plugin::Command启动打包进来的Contents/Resources/backend/omnivoice-backend。 - 杀掉 8000 端口上的遗留孤儿进程。
- 配置
tauri.conf.json的bundle.resources,使其包含../../dist/omnivoice-backend/**和binaries/ffmpeg。 - 将后端默认端口从 8000 改为 17493(新命名空间,与其他开发工具的冲突更少)。
验证:用 bun run desktop 启动开发版应用——窗口打开、分段表格加载、测试 ingest-url 可用。
Phase C — 首次运行模型下载 UI(4–6 小时)¶
交付物:在没有缓存 HF 模型的机器上,全新应用能引导用户完成下载并显示实时进度。
- 实现
hf_progress.py——约 80 行代码。 - 新增
backend/api/routers/setup.py: GET /setup/status→{ models_ready: bool, missing: [...], disk_free_gb: number }。GET /setup/download/stream→ SSE:先发送{ type: "progress", file, bytes, total, pct },然后发送{ type: "done" }。- 前端
src/pages/SetupWizard.jsx: - 当
/setup/status显示模型缺失时展示。 - 由 SSE 流驱动的逐文件进度条。
- 磁盘空间检查;剩余空间 <2 GB 时显示错误状态,低于 10 GB 时显示警告。
- 网络失败时支持重试。
- 应用级路由守卫:如果
setupWizardNeeded为真,渲染<SetupWizard>而不是<Launchpad>。
验证:移动/重命名 ~/Library/Application Support/OmniVoice/models/——启动应用——向导出现——进度条前进——模型下载完成——界面解锁。
Phase D — DMG 构建 + 干净机器测试(2–3 小时)¶
交付物:一个已签名但未公证的 DMG,在全新 Mac 上右键 → 打开后即可使用。
bun run tauri build(通过我们将新增的scripts/build_desktop.sh)。- 产物:
frontend/src-tauri/target/release/bundle/dmg/VoiceStudio_0.1.0_aarch64.dmg。 - 复制到一个全新的 macOS 用户账户(或另一台 Mac)。
- 右键 → 打开一次,完成向导,为 Fireship fixture 生成配音。
- 修复所有出现的问题。
验证:未安装任何开发工具的目标 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-patchbackend/hooks/pyi_rth_numpy_compat.py— numpy 运行时钩子backend/hooks/pyi_rth_torch_compiler_disable.py— 禁用 torch.compilefrontend/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