Cross-platform distribution, UI improvements, and performance optimizations
- PyInstaller frozen sidecar: spec file, build script, and ffmpeg path resolver for self-contained distribution without Python prerequisites - Dual-mode sidecar launcher: frozen binary (production) with dev mode fallback - Parallel transcription + diarization pipeline (~30-40% faster) - GPU auto-detection for diarization (CUDA when available) - Async run_pipeline command for real-time progress event delivery - Web Audio API backend for instant playback and seeking - OpenAI-compatible provider replacing LiteLLM client-side routing - Cross-platform RAM detection (Linux/macOS/Windows) - Settings: speaker count hint, token reveal toggles, dark dropdown styling - Loading splash screen, flexbox layout fix for viewport overflow - Gitea Actions CI/CD pipeline (Linux, Windows, macOS ARM) - Updated README and CLAUDE.md documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
215
python/build_sidecar.py
Normal file
215
python/build_sidecar.py
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the Voice to Notes sidecar as a standalone binary using PyInstaller.
|
||||
|
||||
Usage:
|
||||
python build_sidecar.py [--cpu-only]
|
||||
|
||||
Produces a directory `dist/voice-to-notes-sidecar/` containing the frozen
|
||||
sidecar binary and all dependencies. The main binary is renamed to include
|
||||
the Tauri target triple for externalBin resolution.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
DIST_DIR = SCRIPT_DIR / "dist"
|
||||
BUILD_DIR = SCRIPT_DIR / "build"
|
||||
SPEC_FILE = SCRIPT_DIR / "voice_to_notes.spec"
|
||||
|
||||
# Static ffmpeg download URLs (GPL-licensed builds)
|
||||
FFMPEG_URLS: dict[str, str] = {
|
||||
"linux-x86_64": "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz",
|
||||
"darwin-x86_64": "https://evermeet.cx/ffmpeg/getrelease/zip",
|
||||
"darwin-arm64": "https://evermeet.cx/ffmpeg/getrelease/zip",
|
||||
"win32-x86_64": "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip",
|
||||
}
|
||||
|
||||
|
||||
def get_target_triple() -> str:
|
||||
"""Determine the Tauri-compatible target triple for the current platform."""
|
||||
machine = platform.machine().lower()
|
||||
system = platform.system().lower()
|
||||
|
||||
arch_map = {
|
||||
"x86_64": "x86_64",
|
||||
"amd64": "x86_64",
|
||||
"aarch64": "aarch64",
|
||||
"arm64": "aarch64",
|
||||
}
|
||||
arch = arch_map.get(machine, machine)
|
||||
|
||||
if system == "linux":
|
||||
return f"{arch}-unknown-linux-gnu"
|
||||
elif system == "darwin":
|
||||
return f"{arch}-apple-darwin"
|
||||
elif system == "windows":
|
||||
return f"{arch}-pc-windows-msvc"
|
||||
else:
|
||||
return f"{arch}-unknown-{system}"
|
||||
|
||||
|
||||
def create_venv_and_install(cpu_only: bool) -> Path:
|
||||
"""Create a fresh venv and install dependencies."""
|
||||
venv_dir = BUILD_DIR / "sidecar-venv"
|
||||
if venv_dir.exists():
|
||||
shutil.rmtree(venv_dir)
|
||||
|
||||
print(f"[build] Creating venv at {venv_dir}")
|
||||
subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
|
||||
|
||||
# Determine pip and python paths inside venv
|
||||
if sys.platform == "win32":
|
||||
pip = str(venv_dir / "Scripts" / "pip")
|
||||
python = str(venv_dir / "Scripts" / "python")
|
||||
else:
|
||||
pip = str(venv_dir / "bin" / "pip")
|
||||
python = str(venv_dir / "bin" / "python")
|
||||
|
||||
# Upgrade pip
|
||||
subprocess.run([pip, "install", "--upgrade", "pip"], check=True)
|
||||
|
||||
# Install torch (CPU-only to avoid bundling ~2GB of CUDA libs)
|
||||
if cpu_only:
|
||||
print("[build] Installing PyTorch (CPU-only)")
|
||||
subprocess.run(
|
||||
[pip, "install", "torch", "torchaudio",
|
||||
"--index-url", "https://download.pytorch.org/whl/cpu"],
|
||||
check=True,
|
||||
)
|
||||
else:
|
||||
print("[build] Installing PyTorch (default, may include CUDA)")
|
||||
subprocess.run([pip, "install", "torch", "torchaudio"], check=True)
|
||||
|
||||
# Install project and dev deps (includes pyinstaller)
|
||||
print("[build] Installing project dependencies")
|
||||
subprocess.run([pip, "install", "-e", f"{SCRIPT_DIR}[dev]"], check=True)
|
||||
|
||||
return Path(python)
|
||||
|
||||
|
||||
def run_pyinstaller(python: Path) -> Path:
|
||||
"""Run PyInstaller using the spec file."""
|
||||
print("[build] Running PyInstaller")
|
||||
subprocess.run(
|
||||
[str(python), "-m", "PyInstaller", "--clean", "--noconfirm", str(SPEC_FILE)],
|
||||
cwd=str(SCRIPT_DIR),
|
||||
check=True,
|
||||
)
|
||||
output_dir = DIST_DIR / "voice-to-notes-sidecar"
|
||||
if not output_dir.exists():
|
||||
raise RuntimeError(f"PyInstaller output not found at {output_dir}")
|
||||
return output_dir
|
||||
|
||||
|
||||
def download_ffmpeg(output_dir: Path) -> None:
|
||||
"""Download a static ffmpeg/ffprobe binary for the current platform."""
|
||||
system = sys.platform
|
||||
machine = platform.machine().lower()
|
||||
if machine in ("amd64", "x86_64"):
|
||||
machine = "x86_64"
|
||||
elif machine in ("aarch64", "arm64"):
|
||||
machine = "arm64"
|
||||
|
||||
key = f"{system}-{machine}"
|
||||
if system == "win32":
|
||||
key = f"win32-{machine}"
|
||||
elif system == "linux":
|
||||
key = f"linux-{machine}"
|
||||
|
||||
url = FFMPEG_URLS.get(key)
|
||||
if not url:
|
||||
print(f"[build] Warning: No ffmpeg download URL for platform {key}, skipping")
|
||||
return
|
||||
|
||||
print(f"[build] Downloading ffmpeg for {key}")
|
||||
tmp_path = output_dir / "ffmpeg_download"
|
||||
try:
|
||||
urllib.request.urlretrieve(url, str(tmp_path))
|
||||
|
||||
if url.endswith(".tar.xz"):
|
||||
# Linux static build
|
||||
import tarfile
|
||||
with tarfile.open(str(tmp_path), "r:xz") as tar:
|
||||
for member in tar.getmembers():
|
||||
basename = os.path.basename(member.name)
|
||||
if basename in ("ffmpeg", "ffprobe"):
|
||||
member.name = basename
|
||||
tar.extract(member, path=str(output_dir))
|
||||
dest = output_dir / basename
|
||||
dest.chmod(dest.stat().st_mode | stat.S_IEXEC)
|
||||
elif url.endswith(".zip"):
|
||||
with zipfile.ZipFile(str(tmp_path), "r") as zf:
|
||||
for name in zf.namelist():
|
||||
basename = os.path.basename(name)
|
||||
if basename in ("ffmpeg", "ffprobe", "ffmpeg.exe", "ffprobe.exe"):
|
||||
data = zf.read(name)
|
||||
dest = output_dir / basename
|
||||
dest.write_bytes(data)
|
||||
if sys.platform != "win32":
|
||||
dest.chmod(dest.stat().st_mode | stat.S_IEXEC)
|
||||
print("[build] ffmpeg downloaded successfully")
|
||||
except Exception as e:
|
||||
print(f"[build] Warning: Failed to download ffmpeg: {e}")
|
||||
finally:
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
|
||||
|
||||
def rename_binary(output_dir: Path, target_triple: str) -> None:
|
||||
"""Rename the main binary to include the target triple for Tauri."""
|
||||
if sys.platform == "win32":
|
||||
src = output_dir / "voice-to-notes-sidecar.exe"
|
||||
dst = output_dir / f"voice-to-notes-sidecar-{target_triple}.exe"
|
||||
else:
|
||||
src = output_dir / "voice-to-notes-sidecar"
|
||||
dst = output_dir / f"voice-to-notes-sidecar-{target_triple}"
|
||||
|
||||
if src.exists():
|
||||
print(f"[build] Renaming {src.name} -> {dst.name}")
|
||||
src.rename(dst)
|
||||
else:
|
||||
print(f"[build] Warning: Expected binary not found at {src}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Build the Voice to Notes sidecar binary")
|
||||
parser.add_argument(
|
||||
"--cpu-only",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Install CPU-only PyTorch (default: True, avoids bundling CUDA)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--with-cuda",
|
||||
action="store_true",
|
||||
help="Install PyTorch with CUDA support",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
cpu_only = not args.with_cuda
|
||||
|
||||
target_triple = get_target_triple()
|
||||
print(f"[build] Target triple: {target_triple}")
|
||||
print(f"[build] CPU-only: {cpu_only}")
|
||||
|
||||
python = create_venv_and_install(cpu_only)
|
||||
output_dir = run_pyinstaller(python)
|
||||
download_ffmpeg(output_dir)
|
||||
rename_binary(output_dir, target_triple)
|
||||
|
||||
print(f"\n[build] Done! Sidecar built at: {output_dir}")
|
||||
print(f"[build] Copy contents to src-tauri/binaries/ for Tauri bundling")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user