84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
|
|
"""Локальный whisper.cpp server для Whisper small (macOS / Windows / Linux).
|
||
|
|
|
||
|
|
Сначала скачайте GGML-вес через download_local_models.py. Исполняемый файл
|
||
|
|
задаётся WHISPER_SERVER_BIN или ищется в PATH / локальной сборке. Сеть не нужна.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
import urllib.error
|
||
|
|
import urllib.request
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[1]
|
||
|
|
MODEL = ROOT / "models" / "whisper-small" / "ggml-small-q5_1.bin"
|
||
|
|
PORT = 18082
|
||
|
|
|
||
|
|
|
||
|
|
def binary_path() -> str:
|
||
|
|
built = ROOT / "models" / "bin" / "whisper.cpp-1.9.4" / "build" / "bin" / "whisper-server"
|
||
|
|
binary = (os.environ.get("WHISPER_SERVER_BIN") or shutil.which("whisper-server")
|
||
|
|
or shutil.which("whisper-server.exe")
|
||
|
|
# The in-tree build is macOS arm64, not a portable executable.
|
||
|
|
or (str(built) if sys.platform == "darwin" and built.is_file() else None))
|
||
|
|
if not binary or not Path(binary).is_file():
|
||
|
|
raise RuntimeError("нет whisper-server; установите бинарник whisper.cpp и задайте WHISPER_SERVER_BIN")
|
||
|
|
return binary
|
||
|
|
|
||
|
|
|
||
|
|
def command(binary: str, threads: int = 4) -> list[str]:
|
||
|
|
if not MODEL.is_file():
|
||
|
|
raise RuntimeError(f"нет модели: {MODEL} — запустите make local-models")
|
||
|
|
return [binary, "-m", str(MODEL), "--host", "127.0.0.1", "--port", str(PORT),
|
||
|
|
"--language", "ru", "--threads", str(threads), "--no-fallback"]
|
||
|
|
|
||
|
|
|
||
|
|
def ready() -> bool:
|
||
|
|
try:
|
||
|
|
with urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=1) as response:
|
||
|
|
return response.status == 200
|
||
|
|
except (OSError, urllib.error.HTTPError):
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
try:
|
||
|
|
if ready():
|
||
|
|
raise RuntimeError(f"порт {PORT} уже занят сервером")
|
||
|
|
argv = command(binary_path())
|
||
|
|
except RuntimeError as exc:
|
||
|
|
print(exc, file=sys.stderr)
|
||
|
|
return 2
|
||
|
|
flags = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
||
|
|
process = subprocess.Popen(argv, cwd=ROOT, creationflags=flags, start_new_session=os.name != "nt")
|
||
|
|
try:
|
||
|
|
deadline = time.monotonic() + 120
|
||
|
|
while time.monotonic() < deadline:
|
||
|
|
if process.poll() is not None:
|
||
|
|
raise RuntimeError("whisper-server завершился до готовности")
|
||
|
|
if ready():
|
||
|
|
print(f"Whisper small готов на 127.0.0.1:{PORT}; Ctrl+C остановит сервер", flush=True)
|
||
|
|
while process.poll() is None:
|
||
|
|
time.sleep(0.5)
|
||
|
|
raise RuntimeError("whisper-server неожиданно завершился")
|
||
|
|
time.sleep(0.5)
|
||
|
|
raise RuntimeError("Whisper small не стал готов за 120 секунд")
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
return 0
|
||
|
|
except RuntimeError as exc:
|
||
|
|
print(exc, file=sys.stderr)
|
||
|
|
return 1
|
||
|
|
finally:
|
||
|
|
if process.poll() is None:
|
||
|
|
process.terminate()
|
||
|
|
try:
|
||
|
|
process.wait(timeout=5)
|
||
|
|
except subprocess.TimeoutExpired:
|
||
|
|
process.kill()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|