AIDetector / app.py
TNTFLO's picture
Update app.py
8d39df5 verified
Raw
History Blame Contribute Delete
8.98 kB
"""
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()