| |
| """Run the Dream QA Hugging Face Space app locally for pre-merge review.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import os |
| import signal |
| import subprocess |
| import sys |
| import time |
| from pathlib import Path |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| root = str(ROOT) |
| if root not in sys.path: |
| sys.path.insert(0, root) |
|
|
| from dream_customs.runtime_env import ( |
| DEFAULT_RUNTIME_ENV_JSON, |
| SPACE_ID, |
| configured_env, |
| load_runtime_env_json, |
| ) |
|
|
|
|
| DEFAULT_LOCAL_HOST = "127.0.0.1" |
| DEFAULT_LOCAL_PORT = 7862 |
|
|
|
|
| def _repo_root_on_path() -> None: |
| root = str(ROOT) |
| if root not in sys.path: |
| sys.path.insert(0, root) |
|
|
|
|
| def mirror_manifest(host: str, port: int) -> dict: |
| _repo_root_on_path() |
| from dream_customs.defaults import DEFAULT_ASR_BACKEND, DEFAULT_TEXT_BACKEND, DEFAULT_VISION_BACKEND |
|
|
| return { |
| "mode": "local-space-mirror", |
| "space_id": SPACE_ID, |
| "url": f"http://{host}:{port}", |
| "app_file": "app.py", |
| "default_backends": { |
| "text": DEFAULT_TEXT_BACKEND, |
| "vision": DEFAULT_VISION_BACKEND, |
| "asr": DEFAULT_ASR_BACKEND, |
| }, |
| "env": configured_env(SPACE_ID), |
| "notes": [ |
| "Uses the same app.py/build_demo path as the Hugging Face Space.", |
| "Missing hosted endpoint secrets fall back to deterministic demo behavior.", |
| "Do not print or store endpoint URLs or tokens in logs.", |
| ], |
| } |
|
|
|
|
| def parse_args(argv: list[str] | None = None) -> argparse.Namespace: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--host", default=os.getenv("GRADIO_SERVER_NAME", DEFAULT_LOCAL_HOST)) |
| parser.add_argument( |
| "--port", |
| type=int, |
| default=int(os.getenv("GRADIO_SERVER_PORT", str(DEFAULT_LOCAL_PORT))), |
| ) |
| parser.add_argument("--share", action="store_true", help="Create a public Gradio share link.") |
| parser.add_argument( |
| "--manifest-only", |
| action="store_true", |
| help="Print the local mirror manifest without starting Gradio.", |
| ) |
| parser.add_argument( |
| "--runtime-env-json", |
| type=Path, |
| default=Path(os.getenv("DREAM_CUSTOMS_RUNTIME_ENV_JSON", DEFAULT_RUNTIME_ENV_JSON)), |
| help=( |
| "Load hosted endpoint/token secrets from a local JSON file before launching. " |
| "Only configuration booleans are printed." |
| ), |
| ) |
| parser.add_argument( |
| "--no-runtime-env-json", |
| action="store_true", |
| help="Do not auto-load the default local runtime env JSON.", |
| ) |
| parser.add_argument( |
| "--no-replace", |
| action="store_true", |
| help="Do not stop an older Dream QA local app that is already listening on the target port.", |
| ) |
| return parser.parse_args(argv) |
|
|
|
|
| def _run_text(argv: list[str]) -> str: |
| try: |
| result = subprocess.run(argv, check=False, capture_output=True, text=True) |
| except FileNotFoundError: |
| return "" |
| if result.returncode != 0: |
| return "" |
| return result.stdout.strip() |
|
|
|
|
| def _listener_pids(port: int) -> list[int]: |
| output = _run_text(["lsof", "-nP", f"-iTCP:{port}", "-sTCP:LISTEN", "-t"]) |
| pids: list[int] = [] |
| for line in output.splitlines(): |
| try: |
| pids.append(int(line.strip())) |
| except ValueError: |
| continue |
| return pids |
|
|
|
|
| def _process_command(pid: int) -> str: |
| return _run_text(["ps", "-p", str(pid), "-o", "command="]) |
|
|
|
|
| def _process_cwd(pid: int) -> Path | None: |
| output = _run_text(["lsof", "-a", "-p", str(pid), "-d", "cwd", "-Fn"]) |
| for line in output.splitlines(): |
| if line.startswith("n"): |
| return Path(line[1:]).resolve() |
| return None |
|
|
|
|
| def _is_repo_gradio_command(command: str) -> bool: |
| return "scripts/local_space_mirror.py" in command or command.endswith(" app.py") |
|
|
|
|
| def _is_repo_local_gradio_process(pid: int) -> bool: |
| cwd = _process_cwd(pid) |
| command = _process_command(pid) |
| return cwd == ROOT.resolve() and _is_repo_gradio_command(command) |
|
|
|
|
| def _wait_until_port_is_free(port: int, stopped_pids: list[int]) -> None: |
| deadline = time.time() + 4 |
| while time.time() < deadline: |
| remaining = [pid for pid in _listener_pids(port) if pid in stopped_pids] |
| if not remaining: |
| return |
| time.sleep(0.2) |
| raise RuntimeError( |
| f"Stopped local Gradio process(es) {stopped_pids}, but port {port} is still busy." |
| ) |
|
|
|
|
| def stop_previous_local_app(port: int) -> list[int]: |
| stopped_pids: list[int] = [] |
| for pid in _listener_pids(port): |
| if pid == os.getpid() or not _is_repo_local_gradio_process(pid): |
| continue |
| os.kill(pid, signal.SIGTERM) |
| stopped_pids.append(pid) |
| if stopped_pids: |
| _wait_until_port_is_free(port, stopped_pids) |
| return stopped_pids |
|
|
|
|
| def main(argv: list[str] | None = None) -> int: |
| args = parse_args(argv) |
| os.environ["GRADIO_SERVER_NAME"] = args.host |
| os.environ["GRADIO_SERVER_PORT"] = str(args.port) |
| os.environ.setdefault("SPACE_ID", SPACE_ID) |
| os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False") |
|
|
| runtime_env = {"loaded": False, "reason": "disabled"} |
| if not args.no_runtime_env_json: |
| runtime_env = load_runtime_env_json(args.runtime_env_json) |
|
|
| manifest = mirror_manifest(args.host, args.port) |
| manifest["runtime_env_json"] = runtime_env |
| if args.manifest_only or args.no_replace: |
| manifest["replaced_local_pids"] = [] |
| else: |
| manifest["replaced_local_pids"] = stop_previous_local_app(args.port) |
| print(json.dumps(manifest, ensure_ascii=False, indent=2), flush=True) |
| if args.manifest_only: |
| return 0 |
|
|
| _repo_root_on_path() |
| from app import demo |
|
|
| demo.launch( |
| server_name=args.host, |
| server_port=args.port, |
| show_api=False, |
| show_error=True, |
| share=args.share, |
| ) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|