truscan / server /deploy_hf.py
Manar55's picture
Deploy c4f6e89
457871d verified
Raw History Blame Contribute Delete
5.7 kB
"""
Mirrors this repository (plus the models from the GitHub Release) into a
Hugging Face Docker Space. Run by .github/workflows/deploy-hf.yml.
Environment:
HF_TOKEN (required) Hugging Face WRITE token
APP_PASSWORD (required) password for the app login page, 8+ characters
HF_SPACE (optional) "user/name"; default "<your HF user>/truscan"
HF_HARDWARE (optional) "" (free CPU - needs HF PRO) | "cpu-upgrade" | "t4-small" | "l4x1"
"""
import os
import shutil
import sys
from huggingface_hub import HfApi
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STAGE = os.path.join(os.path.dirname(ROOT), "_hf_stage")
SKIP_DIRS = {".git", ".github", "python", "cache", ".venv", ".venv_nonescape", "wheels",
"nonescape_repo", "__pycache__", ".model_cache", ".plate_training_data"}
SKIP_EXT = {".bat", ".vbs", ".lnk", ".pyc"}
def stage(gpu):
if os.path.exists(STAGE):
shutil.rmtree(STAGE)
def ignore(d, names):
return [n for n in names if n in SKIP_DIRS or os.path.splitext(n)[1].lower() in SKIP_EXT
or n in (".env", "README.md") and os.path.abspath(d) == ROOT]
shutil.copytree(ROOT, STAGE, ignore=ignore)
shutil.copy(os.path.join(ROOT, "server", "Dockerfile.gpu" if gpu else "Dockerfile"),
os.path.join(STAGE, "Dockerfile"))
shutil.copy(os.path.join(ROOT, "server", "README.md"), os.path.join(STAGE, "README.md"))
_ALIASES = {"t4": "t4-small", "gpu": "t4-small", "t4small": "t4-small", "l4": "l4x1",
"cpu": "cpu-upgrade", "cpuupgrade": "cpu-upgrade", "a10g": "a10g-small", "free": "", "none": ""}
def _normalize_hw(raw):
"""Forgives typing slips in the HF_HARDWARE variable: capitals, spaces,
quotes, underscores, invisible characters pasted from a phone."""
v = "".join(ch for ch in (raw or "") if ch.isprintable()).strip().strip("'\"`").strip().lower()
v = v.replace("_", "-").replace(" ", "")
v = _ALIASES.get(v.replace("-", ""), _ALIASES.get(v, v))
if v and raw.strip() != v:
print(f"HF_HARDWARE '{raw!r}' -> '{v}'")
return v
def main():
token = os.environ.get("HF_TOKEN", "").strip()
password = os.environ.get("APP_PASSWORD", "").strip()
if not token:
sys.exit("HF_TOKEN secret is not set.")
# The app has its own login page (works inside the huggingface.co page
# too). On Hugging Face it refuses to run without APP_PASSWORD, so the
# Space stays protected whether it is public or private.
if len(password) < 8:
sys.exit("APP_PASSWORD secret is missing or shorter than 8 characters.\n"
"GitHub repo -> Settings -> Secrets and variables -> Actions -> New repository secret:\n"
" Name: APP_PASSWORD Value: a password of at least 8 characters.")
hw = _normalize_hw(os.environ.get("HF_HARDWARE", ""))
api = HfApi(token=token)
space = os.environ.get("HF_SPACE", "").strip() or f"{api.whoami()['name']}/truscan"
print(f"Space: {space} hardware: {hw or 'cpu-basic (free)'}")
# Hugging Face no longer allows Docker Spaces on the free CPU unless the
# account has PRO or billing. So the Space is created DIRECTLY on the
# chosen hardware (HF_HARDWARE) instead of free CPU first.
try:
api.create_repo(space, repo_type="space", space_sdk="docker", private=True, exist_ok=True,
space_hardware=hw or None, space_sleep_time=900 if hw else None)
except Exception as e:
msg = str(e)
if "PRO" in msg or "billing" in msg.lower() or "cpu-basic" in msg:
sys.exit("Hugging Face needs either:\n"
" - a PRO subscription (then the free CPU works), or\n"
" - a payment method (Settings -> Billing) AND the GitHub variable HF_HARDWARE,\n"
" e.g. cpu-upgrade (cheap CPU) or t4-small (GPU).\n"
f"Original error: {msg}")
raise
api.add_space_secret(space, "APP_PASSWORD", password)
print("App password: ON")
# Result feedback is mirrored to a PRIVATE dataset repo (the Space disk
# is wiped on every restart). Only results are stored, never files.
fb_repo = f"{space.split('/')[0]}/truscan-feedback"
try:
api.create_repo(fb_repo, repo_type="dataset", private=True, exist_ok=True)
api.add_space_secret(space, "FEEDBACK_TOKEN", token)
api.add_space_variable(space, "FEEDBACK_REPO", fb_repo)
print(f"Feedback log: private dataset {fb_repo}")
except Exception as e:
print(f"Feedback log mirror not configured: {e}")
rf_key = os.environ.get("ROBOFLOW_API_KEY", "").strip()
if rf_key:
api.add_space_secret(space, "ROBOFLOW_API_KEY", rf_key)
print("Roboflow plate model: ON")
stage(gpu=hw.startswith(("t4", "l4", "a10", "a100", "l40", "h1", "h2")))
api.upload_folder(folder_path=STAGE, repo_id=space, repo_type="space",
commit_message=f"Deploy {os.environ.get('GITHUB_SHA', '')[:7]}",
delete_patterns=["*.py", "**/*.py", "templates/**", "server/**", "Dockerfile"])
if hw:
try:
api.request_space_hardware(space, hw, sleep_time=900)
api.set_space_sleep_time(space, 900) # sleep after 15 idle min -> no charge
print(f"Requested {hw}; sleeps after 15 idle minutes.")
except Exception as e:
print(f"::warning::Could not switch to {hw}: {e}. Add a payment method on Hugging Face "
f"(Settings -> Billing) or pick the hardware in the Space settings.")
print(f"Done: https://huggingface.co/spaces/{space}")
if __name__ == "__main__":
main()