Download server/deploy_hf.py from Manar55/truscan: direct link, hf CLI and curl.
- Browser
- Download file 5.7 kB
-
https://huggingface.co/spaces/Manar55/truscan/resolve/main/server/deploy_hf.py
- Command line
-
hf download hf://spaces/Manar55/truscan/server/deploy_hf.py
-
curl -L -o deploy_hf.py https://huggingface.co/spaces/Manar55/truscan/resolve/main/server/deploy_hf.py
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() | |