#!/usr/bin/env python3 # start_voicevox_engine.py from __future__ import annotations import argparse import os import platform import subprocess import re import sys import time from pathlib import Path from urllib.error import HTTPError, URLError from urllib.parse import urlparse from urllib.request import Request, urlopen DEFAULT_ENDPOINT = "http://localhost:50021" DEFAULT_WINDOWS_ENGINE = r"D:/App/App/Multimedia/VOICEVOX/vv-engine/run.exe" LINUX_ENGINE_CANDIDATES = [ "./run", "./vv-engine/run", "/opt/VOICEVOX/vv-engine/run", "/opt/voicevox_engine/run", "/usr/local/bin/voicevox_engine", ] def normalize_endpoint(endpoint: str) -> str: endpoint = endpoint.strip() if "://" not in endpoint: endpoint = "http://" + endpoint return endpoint.rstrip("/") def make_url(endpoint: str, path: str) -> str: return f"{normalize_endpoint(endpoint)}/{path.lstrip('/')}" def is_voicevox_running(endpoint: str, timeout: float = 2.0) -> bool: """ VOICEVOX Engine の /version にアクセスして応答を確認する。 """ url = make_url(endpoint, "/version") req = Request(url, headers={"User-Agent": "voicevox-engine-starter"}) try: with urlopen(req, timeout=timeout) as res: return 200 <= res.status < 300 except HTTPError: # サーバはいるが /version が正常応答しない場合は、 # VOICEVOX Engine としては未起動扱いにする。 return False except URLError: return False except TimeoutError: return False def parse_host_port(endpoint: str) -> tuple[str, int]: endpoint = normalize_endpoint(endpoint) parsed = urlparse(endpoint) host = parsed.hostname or "localhost" if parsed.port is not None: port = parsed.port elif parsed.scheme == "https": port = 443 else: port = 80 return host, port def find_engine_path(given_path: str | None) -> Path: if given_path: return Path(given_path).expanduser().resolve() env_path = os.environ.get("VOICEVOX_ENGINE_PATH") if env_path: return Path(env_path).expanduser().resolve() system = platform.system() if system == "Windows": return Path(DEFAULT_WINDOWS_ENGINE) if system == "Linux": for candidate in LINUX_ENGINE_CANDIDATES: path = Path(candidate).expanduser().resolve() if path.exists(): return path raise FileNotFoundError( "VOICEVOX Engine の実行ファイルが見つかりません。" " --engine-path で run.exe / run の場所を指定してください。" ) def build_engine_args(endpoint: str, pass_endpoint_args: bool) -> list[str]: if not pass_endpoint_args: return [] host, port = parse_host_port(endpoint) return ["--host", host, "--port", str(port)] def launch_engine(engine_path: Path, engine_args: list[str]) -> subprocess.Popen: system = platform.system() if not engine_path.exists(): raise FileNotFoundError(f"実行ファイルが見つかりません: {engine_path}") if system == "Windows": # start cmd /K ... 相当 cmdline = subprocess.list2cmdline([str(engine_path), *engine_args]) return subprocess.Popen( ["cmd", "/K", cmdline], creationflags=subprocess.CREATE_NEW_CONSOLE, ) if system == "Linux": # Linux では現在の端末からバックグラウンド起動する。 # 必要なら別ターミナルからこのスクリプトを実行してください。 return subprocess.Popen( [str(engine_path), *engine_args], start_new_session=True, ) # macOS 等は Linux と同じ扱い return subprocess.Popen( [str(engine_path), *engine_args], start_new_session=True, ) def wait_until_ready(endpoint: str, timeout: float, interval: float = 0.5) -> bool: deadline = time.time() + timeout while time.time() < deadline: if is_voicevox_running(endpoint, timeout=1.0): return True time.sleep(interval) return False def stop_voicevox_engine(port=50021, force=False): result = subprocess.run( ["netstat", "-ano"], capture_output=True, text=True, encoding="cp932", errors="ignore", ) pids = set() pattern = re.compile(rf"^\s*TCP\s+\S+:{port}\s+\S+\s+LISTENING\s+(\d+)", re.I) for line in result.stdout.splitlines(): m = pattern.search(line) if m: pids.add(m.group(1)) if not pids: print(f"VOICEVOX Engine on port {port} is not running.") return False for pid in pids: cmd = ["taskkill", "/PID", pid, "/T"] if force: cmd.append("/F") print(" ".join(cmd)) subprocess.run(cmd) return True def main() -> int: parser = argparse.ArgumentParser( description="VOICEVOX Engine が未起動の場合だけ起動します。" ) parser.add_argument( "--mode", default="start", help=f"動作モード [start|kill]", ) parser.add_argument( "--force-kill", type=int, default=0, help=f"mode=killで強制的に停止させるか [0|1]", ) parser.add_argument( "--endpoint", default=DEFAULT_ENDPOINT, help=f"VOICEVOX Engine の接続先。既定値: {DEFAULT_ENDPOINT}", ) parser.add_argument( "--engine-path", default=None, help="VOICEVOX Engine の run.exe または run のパス。", ) parser.add_argument( "--timeout", type=float, default=2.0, help="起動済み確認時のタイムアウト秒数。既定値: 2.0", ) parser.add_argument( "--wait", type=float, default=30.0, help="起動後に応答を待つ秒数。0なら待たない。既定値: 30.0", ) parser.add_argument( "--no-endpoint-args", action="store_true", help="起動時に --host / --port を VOICEVOX Engine に渡さない。", ) parser.add_argument( "--pause", type=int, default=0, help="プログラム終了時にinput()でENTERキー入力を待つ", ) args = parser.parse_args() if args.mode == 'kill': host, port = parse_host_port(args.endpoint) stop_voicevox_engine(port=port, force=args.force_kill) if args.pause: input("\n>>Press ENTER to terminate>>\n") exit(0) endpoint = normalize_endpoint(args.endpoint) if is_voicevox_running(endpoint, timeout=args.timeout): print(f"VOICEVOX Engine は既に起動しています: {endpoint}") return 0, args try: engine_path = find_engine_path(args.engine_path) engine_args = build_engine_args( endpoint, pass_endpoint_args=not args.no_endpoint_args, ) print("VOICEVOX Engine を起動します。") print(f"engine : {engine_path}") print(f"endpoint: {endpoint}") if engine_args: print(f"args : {' '.join(engine_args)}") proc = launch_engine(engine_path, engine_args) print(f"process started: pid={proc.pid}") if args.wait > 0: if wait_until_ready(endpoint, timeout=args.wait): print(f"VOICEVOX Engine が応答しました: {endpoint}") return 0, args print( "VOICEVOX Engine を起動しましたが、指定時間内に応答を確認できませんでした。", file=sys.stderr, ) return 2, args return 0, args except Exception as e: print(f"ERROR: {e}", file=sys.stderr) return 1, args if __name__ == "__main__": ret, args = main() if args.pause: input("\nPress ENTER to terminate>>\n") raise SystemExit(ret)