dream-customs / scripts /local_space_mirror.py
ADJCJH's picture
Route mic recording through MiMo ASR (#42)
b3177bf
Raw
History Blame Contribute Delete
6.04 kB
#!/usr/bin/env python3
"""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())