- setup_whisper.py: descarcă build CPU din release-urile oficiale, sare peste Vulkan/CUDA/OpenBLAS - run.bat: elimină env var GGML_VK_PREFER_HOST_MEMORY și check-ul Vulkan SDK - transcribe.py: --no-gpu era deja setat Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
301 lines
9.5 KiB
Python
301 lines
9.5 KiB
Python
"""
|
|
Auto-download and setup whisper.cpp (CPU build) + model for Windows.
|
|
Called by run.bat when prerequisites are missing.
|
|
"""
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
from urllib.request import urlopen, Request
|
|
|
|
MODELS_DIR = Path("models")
|
|
MODEL_NAME = "ggml-medium-q5_0.bin"
|
|
MODEL_URL = f"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/{MODEL_NAME}"
|
|
|
|
GITHUB_API = "https://api.github.com/repos/ggml-org/whisper.cpp/releases/latest"
|
|
# Community Vulkan builds (for AMD GPUs)
|
|
VULKAN_BUILDS_API = "https://api.github.com/repos/jerryshell/whisper.cpp-windows-vulkan-bin/releases/latest"
|
|
WHISPER_DIR = Path("whisper-bin")
|
|
|
|
|
|
def progress_bar(current: int, total: int, width: int = 40):
|
|
if total <= 0:
|
|
return
|
|
pct = current / total
|
|
filled = int(width * pct)
|
|
bar = "=" * filled + "-" * (width - filled)
|
|
mb_done = current / 1_048_576
|
|
mb_total = total / 1_048_576
|
|
print(f"\r [{bar}] {pct:.0%} {mb_done:.0f}/{mb_total:.0f} MB", end="", flush=True)
|
|
|
|
|
|
def download_file(url: str, dest: Path, desc: str):
|
|
"""Download a file with progress bar."""
|
|
print(f"\n Downloading {desc}...")
|
|
print(f" URL: {url}")
|
|
|
|
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
resp = urlopen(req, timeout=60)
|
|
|
|
total = int(resp.headers.get("Content-Length", 0))
|
|
downloaded = 0
|
|
tmp = dest.with_suffix(".tmp")
|
|
|
|
with open(tmp, "wb") as f:
|
|
while True:
|
|
chunk = resp.read(1024 * 1024)
|
|
if not chunk:
|
|
break
|
|
f.write(chunk)
|
|
downloaded += len(chunk)
|
|
progress_bar(downloaded, total)
|
|
|
|
print() # newline after progress bar
|
|
tmp.rename(dest)
|
|
print(f" Saved: {dest} ({downloaded / 1_048_576:.0f} MB)")
|
|
|
|
|
|
def fetch_release(api_url: str) -> dict | None:
|
|
"""Fetch a GitHub release JSON."""
|
|
req = Request(api_url, headers={"User-Agent": "Mozilla/5.0"})
|
|
try:
|
|
resp = urlopen(req, timeout=30)
|
|
return json.loads(resp.read())
|
|
except Exception as e:
|
|
print(f" Could not fetch from {api_url}: {e}")
|
|
return None
|
|
|
|
|
|
def extract_zip(zip_path: Path):
|
|
"""Extract zip contents into WHISPER_DIR, flattened."""
|
|
print(f"\n Extracting to {WHISPER_DIR}/...")
|
|
WHISPER_DIR.mkdir(exist_ok=True)
|
|
with zipfile.ZipFile(zip_path) as zf:
|
|
for member in zf.namelist():
|
|
filename = Path(member).name
|
|
if not filename:
|
|
continue
|
|
target = WHISPER_DIR / filename
|
|
with zf.open(member) as src, open(target, "wb") as dst:
|
|
dst.write(src.read())
|
|
print(f" {filename}")
|
|
zip_path.unlink()
|
|
|
|
|
|
def find_whisper_exe() -> str | None:
|
|
"""Find whisper-cli.exe (or similar) in WHISPER_DIR."""
|
|
whisper_exe = WHISPER_DIR / "whisper-cli.exe"
|
|
if whisper_exe.exists():
|
|
return str(whisper_exe)
|
|
|
|
# Try main.exe (older naming)
|
|
main_exe = WHISPER_DIR / "main.exe"
|
|
if main_exe.exists():
|
|
return str(main_exe)
|
|
|
|
exes = list(WHISPER_DIR.glob("*.exe"))
|
|
for exe in exes:
|
|
if "whisper" in exe.name.lower() and "cli" in exe.name.lower():
|
|
return str(exe)
|
|
for exe in exes:
|
|
if "whisper" in exe.name.lower():
|
|
return str(exe)
|
|
if exes:
|
|
return str(exes[0])
|
|
return None
|
|
|
|
|
|
def try_community_vulkan_build() -> str | None:
|
|
"""Try downloading Vulkan build from jerryshell's community repo."""
|
|
print("\n Trying community Vulkan build (jerryshell/whisper.cpp-windows-vulkan-bin)...")
|
|
release = fetch_release(VULKAN_BUILDS_API)
|
|
if not release:
|
|
return None
|
|
|
|
tag = release.get("tag_name", "unknown")
|
|
print(f" Community release: {tag}")
|
|
|
|
# Find a zip asset
|
|
for asset in release.get("assets", []):
|
|
name = asset["name"].lower()
|
|
if name.endswith(".zip"):
|
|
print(f" Found: {asset['name']}")
|
|
zip_path = Path(asset["name"])
|
|
download_file(asset["browser_download_url"], zip_path, asset["name"])
|
|
extract_zip(zip_path)
|
|
return find_whisper_exe()
|
|
|
|
print(" No zip asset found in community release")
|
|
return None
|
|
|
|
|
|
def try_official_vulkan_build() -> str | None:
|
|
"""Try downloading Vulkan build from official ggml-org releases."""
|
|
print("\n Fetching latest whisper.cpp release from ggml-org...")
|
|
release = fetch_release(GITHUB_API)
|
|
if not release:
|
|
return None
|
|
|
|
tag = release.get("tag_name", "unknown")
|
|
print(f" Official release: {tag}")
|
|
|
|
# Priority: CPU build (no GPU deps needed)
|
|
cpu_asset = None
|
|
for asset in release.get("assets", []):
|
|
name = asset["name"].lower()
|
|
if not name.endswith(".zip"):
|
|
continue
|
|
# Must be Windows
|
|
if "win" not in name and "x64" not in name:
|
|
continue
|
|
# Skip GPU builds entirely
|
|
if "cuda" in name or "vulkan" in name or "openblas" in name:
|
|
continue
|
|
if "noavx" not in name:
|
|
cpu_asset = asset
|
|
break
|
|
|
|
if not cpu_asset:
|
|
print(" No CPU build found in official releases")
|
|
print(" Available assets:")
|
|
for asset in release.get("assets", []):
|
|
print(f" - {asset['name']}")
|
|
return None
|
|
|
|
print(f" Found CPU build: {cpu_asset['name']}")
|
|
chosen = cpu_asset
|
|
|
|
zip_path = Path(chosen["name"])
|
|
download_file(chosen["browser_download_url"], zip_path, chosen["name"])
|
|
extract_zip(zip_path)
|
|
return find_whisper_exe()
|
|
|
|
|
|
def setup_whisper_bin() -> str | None:
|
|
"""Download whisper.cpp CPU release. Returns path to whisper-cli.exe."""
|
|
whisper_exe = WHISPER_DIR / "whisper-cli.exe"
|
|
if whisper_exe.exists():
|
|
print(f" whisper-cli.exe already exists at {whisper_exe}")
|
|
return str(whisper_exe)
|
|
|
|
exe_path = try_official_vulkan_build()
|
|
if exe_path:
|
|
print(f"\n whisper-cli.exe ready at: {exe_path}")
|
|
return exe_path
|
|
|
|
print("\n ERROR: Could not download whisper.cpp")
|
|
print(" Manual install: https://github.com/ggml-org/whisper.cpp/releases")
|
|
return None
|
|
|
|
|
|
FFMPEG_DIR = Path("ffmpeg-bin")
|
|
FFMPEG_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
|
|
|
|
|
|
def setup_ffmpeg() -> str | None:
|
|
"""Download ffmpeg if not found. Returns path to ffmpeg.exe."""
|
|
import shutil
|
|
|
|
# Already in PATH?
|
|
if shutil.which("ffmpeg"):
|
|
path = shutil.which("ffmpeg")
|
|
print(f" ffmpeg already in PATH: {path}")
|
|
return path
|
|
|
|
# Already downloaded locally?
|
|
local_exe = FFMPEG_DIR / "ffmpeg.exe"
|
|
if local_exe.exists():
|
|
print(f" ffmpeg already exists at {local_exe}")
|
|
return str(local_exe)
|
|
|
|
print("\n Downloading ffmpeg (essentials build)...")
|
|
zip_path = Path("ffmpeg-essentials.zip")
|
|
download_file(FFMPEG_URL, zip_path, "ffmpeg")
|
|
|
|
print(f"\n Extracting ffmpeg...")
|
|
FFMPEG_DIR.mkdir(exist_ok=True)
|
|
with zipfile.ZipFile(zip_path) as zf:
|
|
for member in zf.namelist():
|
|
# Only extract the bin/*.exe files
|
|
if member.endswith(".exe"):
|
|
filename = Path(member).name
|
|
target = FFMPEG_DIR / filename
|
|
with zf.open(member) as src, open(target, "wb") as dst:
|
|
dst.write(src.read())
|
|
print(f" {filename}")
|
|
|
|
zip_path.unlink()
|
|
|
|
if local_exe.exists():
|
|
print(f"\n ffmpeg ready at: {local_exe}")
|
|
return str(local_exe)
|
|
|
|
print(" ERROR: ffmpeg.exe not found after extraction")
|
|
return None
|
|
|
|
|
|
def setup_model() -> bool:
|
|
"""Download whisper model. Returns True on success."""
|
|
MODELS_DIR.mkdir(exist_ok=True)
|
|
model_path = MODELS_DIR / MODEL_NAME
|
|
|
|
if model_path.exists() and model_path.stat().st_size > 100_000_000:
|
|
print(f" Model already exists: {model_path} ({model_path.stat().st_size / 1_048_576:.0f} MB)")
|
|
return True
|
|
|
|
download_file(MODEL_URL, model_path, f"Whisper model ({MODEL_NAME})")
|
|
|
|
if model_path.exists() and model_path.stat().st_size > 100_000_000:
|
|
return True
|
|
|
|
print(" ERROR: Model file too small or missing after download")
|
|
return False
|
|
|
|
|
|
def main():
|
|
what = sys.argv[1] if len(sys.argv) > 1 else "all"
|
|
|
|
if what in ("all", "ffmpeg"):
|
|
print("=" * 60)
|
|
print(" Setting up ffmpeg")
|
|
print("=" * 60)
|
|
ffmpeg_path = setup_ffmpeg()
|
|
if ffmpeg_path:
|
|
Path(".ffmpeg_bin_path").write_text(ffmpeg_path)
|
|
else:
|
|
print("\nFAILED to set up ffmpeg")
|
|
if what == "ffmpeg":
|
|
sys.exit(1)
|
|
|
|
if what in ("all", "whisper"):
|
|
print("=" * 60)
|
|
print(" Setting up whisper.cpp")
|
|
print("=" * 60)
|
|
exe_path = setup_whisper_bin()
|
|
if exe_path:
|
|
# Write path to temp file so run.bat can read it
|
|
Path(".whisper_bin_path").write_text(exe_path)
|
|
else:
|
|
print("\nFAILED to set up whisper.cpp")
|
|
if what == "whisper":
|
|
sys.exit(1)
|
|
|
|
if what in ("all", "model"):
|
|
print()
|
|
print("=" * 60)
|
|
print(f" Downloading Whisper model: {MODEL_NAME}")
|
|
print("=" * 60)
|
|
if not setup_model():
|
|
print("\nFAILED to download model")
|
|
sys.exit(1)
|
|
|
|
print()
|
|
print("Setup complete!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|