Spaces:
Runtime error
Runtime error
| """ | |
| app.py — HuggingFace Space bootstrapper + auto-updater | |
| ===================================================== | |
| Copy THIS file (alone) into a HuggingFace Space (Gradio SDK). On boot it: | |
| 1. clones / pulls the GPT-Protect GitHub repo into persistent storage, | |
| 2. hands off to the repo's copy of app.py (so the supervisor itself stays | |
| up to date), | |
| 3. installs requirements (only when they change), | |
| 4. launches the repo's `main.py` (Gradio UI + realtime training) as a child | |
| process, and supervises it. | |
| Auto-update loop (every UPDATE_INTERVAL_SEC, default 20 min): | |
| * `git fetch` and compare local HEAD vs remote HEAD; | |
| * if — and only if — they differ, send the child a graceful SIGTERM so it | |
| **pauses training and writes a checkpoint** to /data, `git pull`, then | |
| re-exec this supervisor so new code (app.py and/or main.py) takes effect. | |
| Because all state lives in /data, training resumes from the checkpoint. | |
| Space settings: | |
| * SDK: Gradio · app_file: app.py · Persistent storage enabled (/data) | |
| Optional Space variables: | |
| * REPO_URL (default Undertaker-afk/Gpt-Protect) · REPO_BRANCH | |
| * UPDATE_INTERVAL_SEC (default 1200) · UPDATE_GRACE_SEC (default 150) | |
| * MODEL_PRESET (default tiny) · DATA_DIR (default /data) | |
| """ | |
| import hashlib | |
| import os | |
| import signal | |
| import subprocess | |
| import sys | |
| import time | |
| REPO_URL = os.environ.get("REPO_URL", "https://github.com/Undertaker-afk/Gpt-Protect") | |
| REPO_BRANCH = os.environ.get("REPO_BRANCH", "").strip() | |
| _DATA = os.environ.get("DATA_DIR", "/data") | |
| _BASE = _DATA if os.path.isdir(_DATA) and os.access(_DATA, os.W_OK) else os.getcwd() | |
| REPO_DIR = os.environ.get("REPO_DIR", os.path.join(_BASE, "gpt-protect-repo")) | |
| UPDATE_INTERVAL = int(os.environ.get("UPDATE_INTERVAL_SEC", "1200")) # 20 min | |
| UPDATE_GRACE = int(os.environ.get("UPDATE_GRACE_SEC", "150")) | |
| SUPERVISE_TICK = int(os.environ.get("SUPERVISE_TICK_SEC", "30")) | |
| REQS_HASH_FILE = os.path.join(_BASE, ".gptp_reqs_hash") | |
| _STATUS_DIR = _DATA if os.path.isdir(_DATA) else _BASE | |
| STATUS_FILE = os.path.join(_STATUS_DIR, "updater.json") | |
| # the UI ("Check for update now" button) touches this file to force a check | |
| FORCE_FLAG = os.path.join(_STATUS_DIR, "force_update") | |
| def log(msg): | |
| print(f"[app] {time.strftime('%H:%M:%S')} {msg}", flush=True) | |
| def run(cmd): | |
| log(f"$ {cmd}") | |
| return subprocess.run(cmd, shell=True, check=False) | |
| def git(args): | |
| return subprocess.run(["git", "-C", REPO_DIR] + args, | |
| capture_output=True, text=True) | |
| def _write_status(**kw): | |
| try: | |
| import json | |
| kw["t"] = time.time() | |
| with open(STATUS_FILE, "w") as f: | |
| json.dump(kw, f) | |
| except Exception: | |
| pass | |
| # --------------------------------------------------------------------------- # | |
| # repo management | |
| # --------------------------------------------------------------------------- # | |
| def ensure_repo(): | |
| if os.path.isdir(os.path.join(REPO_DIR, ".git")): | |
| log(f"updating existing repo at {REPO_DIR}") | |
| git(["fetch", "--depth", "1", "origin"]) | |
| ref = f"origin/{REPO_BRANCH}" if REPO_BRANCH else "FETCH_HEAD" | |
| git(["reset", "--hard", ref]) | |
| else: | |
| log(f"cloning {REPO_URL} -> {REPO_DIR}") | |
| branch = ["-b", REPO_BRANCH] if REPO_BRANCH else [] | |
| r = subprocess.run(["git", "clone", "--depth", "1"] + branch + | |
| [REPO_URL, REPO_DIR], capture_output=True, text=True) | |
| if r.returncode != 0: | |
| log(f"clone with branch failed ({r.stderr.strip()[:120]}); retrying") | |
| subprocess.run(["git", "clone", "--depth", "1", REPO_URL, REPO_DIR]) | |
| def local_remote_diff(): | |
| """(local_sha, remote_sha, differs) — fetches remote first.""" | |
| git(["fetch", "--depth", "1", "origin"]) | |
| local = git(["rev-parse", "HEAD"]).stdout.strip() | |
| ref = f"origin/{REPO_BRANCH}" if REPO_BRANCH else "FETCH_HEAD" | |
| remote = git(["rev-parse", ref]).stdout.strip() | |
| differs = bool(local) and bool(remote) and local != remote | |
| return local, remote, differs | |
| def git_pull(): | |
| ref = f"origin/{REPO_BRANCH}" if REPO_BRANCH else "FETCH_HEAD" | |
| git(["fetch", "--depth", "1", "origin"]) | |
| git(["reset", "--hard", ref]) | |
| def install_requirements(force=False): | |
| req = os.path.join(REPO_DIR, "requirements.txt") | |
| if not os.path.exists(req): | |
| return | |
| h = hashlib.md5(open(req, "rb").read()).hexdigest() | |
| prev = "" | |
| if os.path.exists(REQS_HASH_FILE): | |
| prev = open(REQS_HASH_FILE).read().strip() | |
| if force or h != prev: | |
| log("installing requirements (changed)…") | |
| run(f"{sys.executable} -m pip install --no-input -r {req}") | |
| try: | |
| open(REQS_HASH_FILE, "w").write(h) | |
| except Exception: | |
| pass | |
| else: | |
| log("requirements unchanged; skipping install") | |
| # --------------------------------------------------------------------------- # | |
| # child process supervision | |
| # --------------------------------------------------------------------------- # | |
| def launch_main(): | |
| main_py = os.path.join(REPO_DIR, "main.py") | |
| if not os.path.exists(main_py): | |
| raise SystemExit(f"[app] main.py not found at {main_py}") | |
| log("launching main.py") | |
| env = dict(os.environ) | |
| return subprocess.Popen([sys.executable, "-u", "main.py"], | |
| cwd=REPO_DIR, env=env) | |
| def graceful_stop(proc): | |
| """Ask main.py to checkpoint + exit; escalate to kill if it overruns.""" | |
| if proc.poll() is not None: | |
| return | |
| log(f"SIGTERM -> main.py (grace {UPDATE_GRACE}s for checkpoint)") | |
| try: | |
| proc.send_signal(signal.SIGTERM) | |
| except Exception: | |
| pass | |
| t0 = time.time() | |
| while time.time() - t0 < UPDATE_GRACE: | |
| if proc.poll() is not None: | |
| log("main.py exited cleanly (checkpoint saved)") | |
| return | |
| time.sleep(1.0) | |
| log("grace expired; SIGKILL") | |
| try: | |
| proc.kill() | |
| except Exception: | |
| pass | |
| def supervise(): | |
| proc = launch_main() | |
| last_check = time.time() | |
| local0 = git(["rev-parse", "HEAD"]).stdout.strip() | |
| _write_status(local_sha=local0, remote_sha=local0, updating=False, | |
| last_check=time.time(), interval=UPDATE_INTERVAL) | |
| while True: | |
| time.sleep(SUPERVISE_TICK) | |
| # crash recovery | |
| if proc.poll() is not None: | |
| log(f"main.py exited rc={proc.returncode}; relaunching") | |
| proc = launch_main() | |
| # forced update check (UI button touches FORCE_FLAG) | |
| forced = False | |
| if os.path.exists(FORCE_FLAG): | |
| try: | |
| os.remove(FORCE_FLAG) | |
| except Exception: | |
| pass | |
| forced = True | |
| log("force-update requested via UI") | |
| # periodic or forced update check | |
| if forced or time.time() - last_check >= UPDATE_INTERVAL: | |
| last_check = time.time() | |
| try: | |
| local, remote, differs = local_remote_diff() | |
| except Exception as e: | |
| log(f"update check failed: {e}") | |
| continue | |
| _write_status(local_sha=local, remote_sha=remote, updating=differs, | |
| last_check=time.time(), interval=UPDATE_INTERVAL) | |
| if not differs: | |
| log(f"no update (HEAD {local[:7]})") | |
| continue | |
| log(f"update available {local[:7]} -> {remote[:7]}") | |
| graceful_stop(proc) | |
| git_pull() | |
| install_requirements() | |
| log("re-exec supervisor with updated code") | |
| _write_status(local_sha=remote, remote_sha=remote, updating=False, | |
| last_check=time.time(), last_update=time.time(), | |
| interval=UPDATE_INTERVAL) | |
| os.execv(sys.executable, | |
| [sys.executable, os.path.abspath(__file__)] + sys.argv[1:]) | |
| # --------------------------------------------------------------------------- # | |
| def main(): | |
| try: | |
| ensure_repo() | |
| except Exception as e: | |
| log(f"ensure_repo warning: {e}") | |
| # Hand off to the repo's own app.py so the supervisor is itself updatable. | |
| repo_app = os.path.join(REPO_DIR, "app.py") | |
| if (os.environ.get("GPTP_FROM_REPO") != "1" | |
| and os.path.exists(repo_app) | |
| and os.path.abspath(repo_app) != os.path.abspath(__file__)): | |
| os.environ["GPTP_FROM_REPO"] = "1" | |
| os.chdir(REPO_DIR) | |
| log("handoff -> repo app.py") | |
| os.execv(sys.executable, [sys.executable, repo_app] + sys.argv[1:]) | |
| install_requirements() | |
| # Fallback: if main.py somehow isn't in the repo, run a local one. | |
| if not os.path.exists(os.path.join(REPO_DIR, "main.py")) and \ | |
| os.path.exists(os.path.join(os.getcwd(), "main.py")): | |
| log("repo main.py missing; running local main.py") | |
| import runpy | |
| runpy.run_path("main.py", run_name="__main__") | |
| return | |
| supervise() | |
| if __name__ == "__main__": | |
| main() |