File size: 5,700 Bytes
82d7501
 
 
 
 
 
9437092
82d7501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db488c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82d7501
 
 
 
 
9437092
 
 
 
 
 
 
db488c7
82d7501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9437092
 
82d7501
457871d
 
 
 
 
 
 
 
 
 
 
65ecad6
 
 
 
 
82d7501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""
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()