fffiloni commited on
Commit
721e47f
·
verified ·
1 Parent(s): 0900234

Upload 10 files

Browse files
Files changed (3) hide show
  1. src/jobs.py +17 -2
  2. src/model_scan.py +351 -0
  3. src/worker_payload.py +14 -4
src/jobs.py CHANGED
@@ -16,6 +16,21 @@ from .worker_payload import (
16
 
17
  SPACE_SLUG_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{1,95}$")
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  def _base_env(*, run_id: str, username: str, bucket_source: str, worker_script_b64: str) -> dict[str, str]:
21
  return {
@@ -141,8 +156,8 @@ def launch_universal_model_card_job(
141
  env["TARGET_SPACE_ID"] = target_space_id
142
  env["MODEL_ID"] = clean_model_id
143
  env["PI_MODEL"] = (pi_model or "Qwen/Qwen3-Coder-Next").strip()
144
- env["PREFERRED_SPACE_HARDWARE"] = (preferred_space_hardware or "zero-a10g").strip()
145
- env["FALLBACK_SPACE_HARDWARE"] = (fallback_space_hardware or "l40sx1").strip()
146
  env["ALLOW_FIXED_GPU_FALLBACK"] = "true" if allow_fixed_gpu_fallback else "false"
147
  env["IMPLEMENTATION_MODE"] = (implementation_mode or "full-inference-gated").strip()
148
  env["EXPECTED_OUTPUT_TYPE"] = (expected_output_type or "any").strip()
 
16
 
17
  SPACE_SLUG_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{1,95}$")
18
 
19
+ AUTO_SPACE_HARDWARE_CHOICES = {"zero-a10g", "cpu-basic", "t4-small", "t4-medium", "a10g-small", "a10g-large", "l4x1", "l40sx1"}
20
+ DEFAULT_PREFERRED_SPACE_HARDWARE = "zero-a10g"
21
+ DEFAULT_FALLBACK_SPACE_HARDWARE = "a10g-large"
22
+
23
+ def normalize_auto_space_hardware(value: str | None, *, default: str) -> str:
24
+ """Return a hardware flavor safe for automatic Space fallback.
25
+
26
+ Expensive or restricted tiers such as A100/H200 are intentionally not used
27
+ by the automatic fallback path. Users can still select them manually in the
28
+ generated Space settings if their account or organization is allowed to.
29
+ """
30
+ candidate = (value or default).strip()
31
+ return candidate if candidate in AUTO_SPACE_HARDWARE_CHOICES else default
32
+
33
+
34
 
35
  def _base_env(*, run_id: str, username: str, bucket_source: str, worker_script_b64: str) -> dict[str, str]:
36
  return {
 
156
  env["TARGET_SPACE_ID"] = target_space_id
157
  env["MODEL_ID"] = clean_model_id
158
  env["PI_MODEL"] = (pi_model or "Qwen/Qwen3-Coder-Next").strip()
159
+ env["PREFERRED_SPACE_HARDWARE"] = normalize_auto_space_hardware(preferred_space_hardware, default=DEFAULT_PREFERRED_SPACE_HARDWARE)
160
+ env["FALLBACK_SPACE_HARDWARE"] = normalize_auto_space_hardware(fallback_space_hardware, default=DEFAULT_FALLBACK_SPACE_HARDWARE)
161
  env["ALLOW_FIXED_GPU_FALLBACK"] = "true" if allow_fixed_gpu_fallback else "false"
162
  env["IMPLEMENTATION_MODE"] = (implementation_mode or "full-inference-gated").strip()
163
  env["EXPECTED_OUTPUT_TYPE"] = (expected_output_type or "any").strip()
src/model_scan.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from huggingface_hub import HfApi, hf_hub_download
10
+
11
+ MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$")
12
+ SMALL_CONFIG_MAX_BYTES = 2_000_000
13
+ WEIGHT_EXTENSIONS = (".safetensors", ".bin", ".pt", ".pth", ".ckpt", ".onnx", ".gguf")
14
+ RISKY_SERIALIZATION_EXTENSIONS = (".bin", ".pt", ".pth", ".ckpt", ".pkl", ".pickle", ".joblib")
15
+ PYTHON_EXTENSIONS = (".py",)
16
+ DIFFUSERS_EXAMPLE_RE = re.compile(r"(from\s+diffusers\s+import|DiffusionPipeline\.from_pretrained|AutoPipelineFor(?:Text2Image|Image2Image|Inpainting)\.from_pretrained|StableDiffusion(?:XL)?Pipeline\.from_pretrained|FluxPipeline\.from_pretrained|ZImagePipeline)", re.IGNORECASE)
17
+ GENERIC_INFERENCE_EXAMPLE_RE = re.compile(r"(from_pretrained\s*\(|gradio_client|pipeline\s*\(|pipe\s*\(|predict\s*\()", re.IGNORECASE)
18
+ DIFFUSERS_PIPELINE_CLASSES = {
19
+ "DiffusionPipeline",
20
+ "StableDiffusionPipeline",
21
+ "StableDiffusionXLPipeline",
22
+ "StableDiffusionImg2ImgPipeline",
23
+ "StableDiffusionInpaintPipeline",
24
+ "AutoPipelineForText2Image",
25
+ "AutoPipelineForImage2Image",
26
+ "AutoPipelineForInpainting",
27
+ "FluxPipeline",
28
+ "ZImagePipeline",
29
+ }
30
+
31
+
32
+ def normalize_model_id(value: str | None) -> str:
33
+ cleaned = (value or "").strip()
34
+ cleaned = cleaned.replace("https://huggingface.co/", "")
35
+ cleaned = cleaned.split("?", 1)[0].split("#", 1)[0].strip("/")
36
+ if not MODEL_ID_RE.match(cleaned):
37
+ raise ValueError("Model ID must look like owner/name or a Hugging Face model URL.")
38
+ return cleaned
39
+
40
+
41
+ def _get(obj: Any, name: str, default: Any = None) -> Any:
42
+ if isinstance(obj, dict):
43
+ return obj.get(name, default)
44
+ return getattr(obj, name, default)
45
+
46
+
47
+ def _sibling_name(sibling: Any) -> str:
48
+ return str(_get(sibling, "rfilename", _get(sibling, "path", _get(sibling, "name", ""))) or "")
49
+
50
+
51
+ def _sibling_size(sibling: Any) -> int | None:
52
+ value = _get(sibling, "size", None)
53
+ try:
54
+ return int(value) if value is not None else None
55
+ except Exception:
56
+ return None
57
+
58
+
59
+ def _read_small_json(repo_id: str, filename: str, *, token: str | None, max_bytes: int = SMALL_CONFIG_MAX_BYTES) -> dict[str, Any]:
60
+ try:
61
+ with tempfile.TemporaryDirectory(prefix="asf-model-scan-") as tmp:
62
+ path = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model", token=token, local_dir=tmp)
63
+ p = Path(path)
64
+ if p.stat().st_size > max_bytes:
65
+ return {}
66
+ return json.loads(p.read_text(encoding="utf-8"))
67
+ except Exception:
68
+ return {}
69
+
70
+
71
+ def _read_small_text(repo_id: str, filename: str, *, token: str | None, max_bytes: int = SMALL_CONFIG_MAX_BYTES) -> str:
72
+ try:
73
+ with tempfile.TemporaryDirectory(prefix="asf-model-scan-") as tmp:
74
+ path = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model", token=token, local_dir=tmp)
75
+ p = Path(path)
76
+ if p.stat().st_size > max_bytes:
77
+ return ""
78
+ return p.read_text(encoding="utf-8", errors="replace")
79
+ except Exception:
80
+ return ""
81
+
82
+
83
+ def _model_index_pipeline_class(model_index: dict[str, Any]) -> str:
84
+ value = model_index.get("_class_name") or model_index.get("pipeline_class") or ""
85
+ return str(value or "")
86
+
87
+
88
+ def _extract_model_card_signals(readme: str, model_index: dict[str, Any]) -> dict[str, Any]:
89
+ text = readme or ""
90
+ diffusers_example = bool(DIFFUSERS_EXAMPLE_RE.search(text))
91
+ inference_example = diffusers_example or bool(GENERIC_INFERENCE_EXAMPLE_RE.search(text))
92
+ pipeline_class = _model_index_pipeline_class(model_index)
93
+ if not pipeline_class and text:
94
+ for name in sorted(DIFFUSERS_PIPELINE_CLASSES, key=len, reverse=True):
95
+ if name in text:
96
+ pipeline_class = name
97
+ break
98
+ runtime_hints = []
99
+ lower = text.lower()
100
+ for label, patterns in [
101
+ ("bfloat16", ("bfloat16", "bf16")),
102
+ ("float16", ("float16", "fp16")),
103
+ ("device_map", ("device_map",)),
104
+ ("cuda", ("cuda", ".to(\"cuda\")", ".to('cuda')")),
105
+ ("offload", ("offload", "enable_model_cpu_offload")),
106
+ ("num_inference_steps", ("num_inference_steps", "inference steps")),
107
+ ]:
108
+ if any(p in lower for p in patterns):
109
+ runtime_hints.append(label)
110
+ return {
111
+ "has_inference_example": inference_example,
112
+ "has_diffusers_example": diffusers_example,
113
+ "pipeline_class": pipeline_class,
114
+ "runtime_hints": runtime_hints[:8],
115
+ }
116
+
117
+
118
+ def _classify(score: int, *, blocking: bool = False) -> str:
119
+ if blocking:
120
+ return "unsupported"
121
+ if score >= 75:
122
+ return "safe"
123
+ if score >= 45:
124
+ return "caution"
125
+ return "risky"
126
+
127
+
128
+ def analyze_model_metadata(
129
+ *,
130
+ model_id: str,
131
+ pipeline_tag: str | None = None,
132
+ library_name: str | None = None,
133
+ tags: list[str] | None = None,
134
+ siblings: list[Any] | None = None,
135
+ gated: Any = None,
136
+ private: Any = None,
137
+ card_data: dict[str, Any] | None = None,
138
+ config: dict[str, Any] | None = None,
139
+ model_index: dict[str, Any] | None = None,
140
+ readme: str | None = None,
141
+ ) -> dict[str, Any]:
142
+ """Pure heuristic model-card scan used by the API and tests.
143
+
144
+ It intentionally does not claim to be a security scanner. It summarizes
145
+ common signals that predict whether an autonomous Space build is likely to
146
+ be safe, supported, and worth spending compute on.
147
+ """
148
+ tags = [str(t) for t in (tags or []) if t]
149
+ files = [_sibling_name(s) for s in (siblings or []) if _sibling_name(s)]
150
+ lower_files = [f.lower() for f in files]
151
+ config = config or {}
152
+ model_index = model_index or {}
153
+ readme = readme or ""
154
+ card_signals = _extract_model_card_signals(readme, model_index)
155
+ has_inference_example = bool(card_signals["has_inference_example"])
156
+ has_diffusers_example = bool(card_signals["has_diffusers_example"])
157
+ pipeline_class = str(card_signals["pipeline_class"] or "")
158
+ runtime_hints = list(card_signals["runtime_hints"] or [])
159
+
160
+ score = 50
161
+ good: list[str] = []
162
+ risk: list[str] = []
163
+ recommendations: list[str] = []
164
+
165
+ has_safetensors = any(f.endswith(".safetensors") for f in lower_files)
166
+ has_risky_serialization = any(f.endswith(RISKY_SERIALIZATION_EXTENSIONS) for f in lower_files)
167
+ has_weight_file = any(f.endswith(WEIGHT_EXTENSIONS) for f in lower_files)
168
+ has_python = any(f.endswith(PYTHON_EXTENSIONS) for f in lower_files)
169
+ has_config = "config.json" in lower_files
170
+ has_model_index = "model_index.json" in lower_files
171
+ has_readme = bool(readme.strip()) or any(f in {"readme.md", "README.md".lower()} for f in lower_files)
172
+ custom_code = bool(config.get("auto_map")) or "custom_code" in tags or "trust_remote_code" in tags or has_python
173
+
174
+ if pipeline_tag:
175
+ score += 12
176
+ good.append(f"Pipeline tag detected: {pipeline_tag}.")
177
+ else:
178
+ score -= 12
179
+ risk.append("No pipeline tag detected; task type may be ambiguous for an autonomous builder.")
180
+
181
+ if library_name:
182
+ score += 10
183
+ good.append(f"Library detected: {library_name}.")
184
+ elif model_index:
185
+ score += 6
186
+ good.append("Diffusers-style model_index.json detected.")
187
+ else:
188
+ score -= 8
189
+ risk.append("No library metadata detected.")
190
+
191
+ if has_safetensors:
192
+ score += 18
193
+ good.append("Safetensors weights are available.")
194
+ elif has_weight_file:
195
+ score -= 10
196
+ risk.append("No safetensors file detected; weights may use less safe or less portable formats.")
197
+ else:
198
+ score -= 18
199
+ risk.append("No obvious model weight file detected in the repository listing.")
200
+
201
+ if has_risky_serialization:
202
+ score -= 14
203
+ risk.append("Repository includes pickle-like or PyTorch pickle serialization files (.bin/.pt/.pth/.ckpt/.pkl).")
204
+
205
+ if custom_code:
206
+ score -= 22
207
+ risk.append("Custom code or trust_remote_code is likely required.")
208
+ recommendations.append("Review code manually before using Strict inference or paid hardware.")
209
+
210
+ if gated in {True, "auto", "manual"} or str(gated).lower() in {"true", "auto", "manual"}:
211
+ score -= 18
212
+ risk.append("Model appears gated; the Job may fail unless the signed-in account has accepted access terms.")
213
+ recommendations.append("Confirm model access with the same Hugging Face account before launching.")
214
+
215
+ if private:
216
+ score -= 8
217
+ risk.append("Model is private; ensure the OAuth token has access.")
218
+
219
+ if has_config or has_model_index:
220
+ score += 8
221
+ good.append("Standard config metadata is present.")
222
+ else:
223
+ score -= 8
224
+ risk.append("No config.json or model_index.json detected.")
225
+
226
+ is_diffusers = (library_name or "").lower() == "diffusers" or "diffusers" in {t.lower() for t in tags} or bool(model_index)
227
+ if is_diffusers and has_model_index:
228
+ score += 8
229
+ good.append("Diffusers repository structure detected.")
230
+ if pipeline_class:
231
+ score += 5
232
+ good.append(f"Pipeline class detected: {pipeline_class}.")
233
+ if has_diffusers_example:
234
+ score += 12
235
+ good.append("Model card includes a runnable Diffusers example.")
236
+ elif has_inference_example:
237
+ score += 7
238
+ good.append("Model card includes an inference example.")
239
+ elif has_readme:
240
+ score -= 5
241
+ risk.append("No clear runnable inference example found in the model card.")
242
+ if runtime_hints:
243
+ score += 4
244
+ good.append("Model card provides runtime hints: " + ", ".join(runtime_hints) + ".")
245
+
246
+ if has_readme and len(readme.strip()) > 400:
247
+ score += 6
248
+ good.append("Model card documentation is present.")
249
+ elif not has_readme:
250
+ score -= 8
251
+ risk.append("README/model card appears missing.")
252
+ else:
253
+ score -= 4
254
+ risk.append("Model card documentation appears very short.")
255
+
256
+ task = (pipeline_tag or "").lower()
257
+ library = (library_name or "").lower()
258
+ unsupported = False
259
+ if task in {"reinforcement-learning", "robotics"}:
260
+ unsupported = True
261
+ risk.append(f"Task '{pipeline_tag}' is not a good fit for automatic Gradio Space generation.")
262
+ if "gguf" in tags or any(f.endswith(".gguf") for f in lower_files):
263
+ score -= 10
264
+ risk.append("GGUF assets may need a custom llama.cpp runtime rather than the standard Transformers/Diffusers path.")
265
+ if library in {"adapter-transformers"}:
266
+ score -= 8
267
+ risk.append(f"Library '{library_name}' may require custom integration.")
268
+
269
+ # A clear Diffusers model card with safetensors and a runnable example is a strong
270
+ # positive signal. Do not downgrade it just because the model may be large; size
271
+ # affects hardware planning, not whether the card is healthy for autonomous build.
272
+ if is_diffusers and has_safetensors and has_diffusers_example and has_model_index and not custom_code and not unsupported:
273
+ score = max(score, 82)
274
+
275
+ score = max(0, min(100, score))
276
+ verdict = _classify(score, blocking=unsupported)
277
+ if not recommendations:
278
+ if verdict == "safe":
279
+ recommendations.append("Good candidate for Strict inference.")
280
+ elif verdict == "caution":
281
+ recommendations.append("Use Best effort inference if the model card lacks a clear runnable example.")
282
+ elif verdict == "risky":
283
+ recommendations.append("Prefer Best effort or Demo scaffold until the risky signals are reviewed.")
284
+ else:
285
+ recommendations.append("Do not launch automatically; inspect manually first.")
286
+
287
+ return {
288
+ "ok": True,
289
+ "model_id": model_id,
290
+ "verdict": verdict,
291
+ "score": score,
292
+ "summary": {
293
+ "safe": "Looks like a good candidate for an autonomous build.",
294
+ "caution": "Usable, but review the caution signals before spending compute.",
295
+ "risky": "Risky for an autonomous paid build; review before launching.",
296
+ "unsupported": "Not a good fit for the automated builder.",
297
+ }[verdict],
298
+ "good_signals": good[:8],
299
+ "risk_signals": risk[:10],
300
+ "recommendations": recommendations[:6],
301
+ "metadata": {
302
+ "pipeline_tag": pipeline_tag or "",
303
+ "library_name": library_name or "",
304
+ "tags": tags[:30],
305
+ "gated": gated,
306
+ "private": bool(private),
307
+ "file_count": len(files),
308
+ "has_safetensors": has_safetensors,
309
+ "has_risky_serialization": has_risky_serialization,
310
+ "has_custom_code_signal": custom_code,
311
+ "has_inference_example": has_inference_example,
312
+ "has_diffusers_example": has_diffusers_example,
313
+ "pipeline_class": pipeline_class,
314
+ "runtime_hints": runtime_hints,
315
+ "diffusers_standard": bool(is_diffusers and has_model_index and has_safetensors),
316
+ },
317
+ }
318
+
319
+
320
+ def scan_model_card(model_id_or_url: str, *, token: str | None = None) -> dict[str, Any]:
321
+ model_id = normalize_model_id(model_id_or_url)
322
+ api = HfApi(token=token)
323
+ try:
324
+ info = api.model_info(model_id, files_metadata=True, token=token)
325
+ except TypeError:
326
+ info = api.model_info(model_id, token=token)
327
+
328
+ siblings = list(_get(info, "siblings", []) or [])
329
+ files = {_sibling_name(s).lower(): s for s in siblings if _sibling_name(s)}
330
+ config = _read_small_json(model_id, "config.json", token=token) if "config.json" in files else {}
331
+ model_index = _read_small_json(model_id, "model_index.json", token=token) if "model_index.json" in files else {}
332
+ readme = _read_small_text(model_id, "README.md", token=token) if "readme.md" in files else ""
333
+ card_data = _get(info, "card_data", _get(info, "cardData", {})) or {}
334
+ if not isinstance(card_data, dict):
335
+ try:
336
+ card_data = dict(card_data)
337
+ except Exception:
338
+ card_data = {}
339
+ return analyze_model_metadata(
340
+ model_id=model_id,
341
+ pipeline_tag=_get(info, "pipeline_tag", None),
342
+ library_name=_get(info, "library_name", None),
343
+ tags=list(_get(info, "tags", []) or []),
344
+ siblings=siblings,
345
+ gated=_get(info, "gated", None),
346
+ private=_get(info, "private", False),
347
+ card_data=card_data,
348
+ config=config,
349
+ model_index=model_index,
350
+ readme=readme,
351
+ )
src/worker_payload.py CHANGED
@@ -23,6 +23,9 @@ from textwrap import dedent
23
 
24
  TARGET_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{1,95}/[A-Za-z0-9][A-Za-z0-9._-]{1,95}$")
25
  GIST_URL = "https://gist.github.com/gary149/2aba2962375fa9ca56bb9ef53f00b73d"
 
 
 
26
  DEFAULT_MODEL_ID = "sshleifer/tiny-gpt2"
27
 
28
 
@@ -829,8 +832,15 @@ def request_hardware(api, target_space_id: str, hardware: str, token: str, event
829
  return {"phase": "post_create_request", "requested": True, "hardware": hardware, "ok": False, "attempts": retries, "error": last_error, "manual_action_required": False}
830
 
831
 
 
 
 
 
 
832
  def build_hardware_sequence(preferred_hardware: str, fallback_hardware: str, allow_fixed_gpu_fallback: bool) -> list[str]:
833
  sequence = []
 
 
834
  for hw in ["zero-a10g", preferred_hardware, fallback_hardware if allow_fixed_gpu_fallback else None]:
835
  value = (hw or "").strip()
836
  if value and value not in sequence:
@@ -971,7 +981,7 @@ Non-negotiable safety and product constraints:
971
  - Do not delete any user resources.
972
  - Do not print secrets or tokens.
973
  - Work only inside the current workspace.
974
- - The wrapper will create the private Space, request hardware best-effort, upload files, and validate the live app. Do not create/delete repos yourself in this builder worker.
975
  - Preserve a cheap health endpoint named `health` with `api_name="health"`. It must not load weights, run GPU work, or download large files.
976
  - Do not pin huggingface_hub below 1.0. Use huggingface_hub>=0.34.0,<2.0.0 unless the model card requires a narrower compatible range. If transformers>=5 is used, keep huggingface_hub compatible with it, for example huggingface_hub>=1.5.0,<2.0.0.
977
  - README.md frontmatter must remain valid; if it uses short_description, it must be 60 characters or fewer.
@@ -981,7 +991,7 @@ Implementation contract:
981
  - Try to implement the closest real inference path for the model card using evidence from README, model metadata, config files, and repo files.
982
  - You may choose an appropriate Gradio UI for the task: text, image, audio, video, multimodal, embeddings, classification, etc.
983
  - If the model is standard and feasible, implement a real generate/predict function and expose it as a Gradio endpoint.
984
- - If the model requires GPU, add ZeroGPU-compatible `@spaces.GPU(...)` only around the inference function. Do not decorate health.
985
  - If the model requires special dependencies, include them only when needed and document risks.
986
  - Investigate compatibility fallbacks before declaring a blocker: PyTorch SDPA, xformers, HF Kernels where relevant, CPU/offload/lazy loading, smaller resolution/steps, safe smoke-test inputs.
987
  - If real inference is impossible or unsafe in a Space, write TECHNICAL_BLOCKERS.json with concrete evidence for every blocker.
@@ -1264,8 +1274,8 @@ def main():
1264
  target_space_id = os.environ.get("TARGET_SPACE_ID", "")
1265
  model_id = sanitize_model_id(os.environ.get("MODEL_ID", DEFAULT_MODEL_ID))
1266
  pi_model = os.environ.get("PI_MODEL", "Qwen/Qwen3-Coder-Next")
1267
- preferred_hardware = os.environ.get("PREFERRED_SPACE_HARDWARE", "zero-a10g")
1268
- fallback_hardware = os.environ.get("FALLBACK_SPACE_HARDWARE", "l40sx1")
1269
  allow_fixed_gpu_fallback = os.environ.get("ALLOW_FIXED_GPU_FALLBACK", "true").lower() in {"1", "true", "yes", "on"}
1270
  implementation_mode = os.environ.get("IMPLEMENTATION_MODE", "full-inference-attempt")
1271
  expected_output_type = os.environ.get("EXPECTED_OUTPUT_TYPE", "any")
 
23
 
24
  TARGET_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{1,95}/[A-Za-z0-9][A-Za-z0-9._-]{1,95}$")
25
  GIST_URL = "https://gist.github.com/gary149/2aba2962375fa9ca56bb9ef53f00b73d"
26
+ AUTO_SPACE_HARDWARE_CHOICES = {"zero-a10g", "cpu-basic", "t4-small", "t4-medium", "a10g-small", "a10g-large", "l4x1", "l40sx1"}
27
+ DEFAULT_PREFERRED_SPACE_HARDWARE = "zero-a10g"
28
+ DEFAULT_FALLBACK_SPACE_HARDWARE = "a10g-large"
29
  DEFAULT_MODEL_ID = "sshleifer/tiny-gpt2"
30
 
31
 
 
832
  return {"phase": "post_create_request", "requested": True, "hardware": hardware, "ok": False, "attempts": retries, "error": last_error, "manual_action_required": False}
833
 
834
 
835
+ def normalize_auto_space_hardware(value: str | None, default: str) -> str:
836
+ candidate = (value or default).strip()
837
+ return candidate if candidate in AUTO_SPACE_HARDWARE_CHOICES else default
838
+
839
+
840
  def build_hardware_sequence(preferred_hardware: str, fallback_hardware: str, allow_fixed_gpu_fallback: bool) -> list[str]:
841
  sequence = []
842
+ preferred_hardware = normalize_auto_space_hardware(preferred_hardware, DEFAULT_PREFERRED_SPACE_HARDWARE)
843
+ fallback_hardware = normalize_auto_space_hardware(fallback_hardware, DEFAULT_FALLBACK_SPACE_HARDWARE)
844
  for hw in ["zero-a10g", preferred_hardware, fallback_hardware if allow_fixed_gpu_fallback else None]:
845
  value = (hw or "").strip()
846
  if value and value not in sequence:
 
981
  - Do not delete any user resources.
982
  - Do not print secrets or tokens.
983
  - Work only inside the current workspace.
984
+ - The wrapper will create the private Space, request allowed hardware best-effort, upload files, and validate the live app. Do not create/delete repos yourself in this builder worker.
985
  - Preserve a cheap health endpoint named `health` with `api_name="health"`. It must not load weights, run GPU work, or download large files.
986
  - Do not pin huggingface_hub below 1.0. Use huggingface_hub>=0.34.0,<2.0.0 unless the model card requires a narrower compatible range. If transformers>=5 is used, keep huggingface_hub compatible with it, for example huggingface_hub>=1.5.0,<2.0.0.
987
  - README.md frontmatter must remain valid; if it uses short_description, it must be 60 characters or fewer.
 
991
  - Try to implement the closest real inference path for the model card using evidence from README, model metadata, config files, and repo files.
992
  - You may choose an appropriate Gradio UI for the task: text, image, audio, video, multimodal, embeddings, classification, etc.
993
  - If the model is standard and feasible, implement a real generate/predict function and expose it as a Gradio endpoint.
994
+ - If the model requires GPU, add ZeroGPU-compatible `@spaces.GPU(...)` only around the inference function. Do not decorate health. Do not assume dedicated A100/H200 hardware is available automatically; if such hardware is needed, document it as a manual requirement.
995
  - If the model requires special dependencies, include them only when needed and document risks.
996
  - Investigate compatibility fallbacks before declaring a blocker: PyTorch SDPA, xformers, HF Kernels where relevant, CPU/offload/lazy loading, smaller resolution/steps, safe smoke-test inputs.
997
  - If real inference is impossible or unsafe in a Space, write TECHNICAL_BLOCKERS.json with concrete evidence for every blocker.
 
1274
  target_space_id = os.environ.get("TARGET_SPACE_ID", "")
1275
  model_id = sanitize_model_id(os.environ.get("MODEL_ID", DEFAULT_MODEL_ID))
1276
  pi_model = os.environ.get("PI_MODEL", "Qwen/Qwen3-Coder-Next")
1277
+ preferred_hardware = normalize_auto_space_hardware(os.environ.get("PREFERRED_SPACE_HARDWARE"), DEFAULT_PREFERRED_SPACE_HARDWARE)
1278
+ fallback_hardware = normalize_auto_space_hardware(os.environ.get("FALLBACK_SPACE_HARDWARE"), DEFAULT_FALLBACK_SPACE_HARDWARE)
1279
  allow_fixed_gpu_fallback = os.environ.get("ALLOW_FIXED_GPU_FALLBACK", "true").lower() in {"1", "true", "yes", "on"}
1280
  implementation_mode = os.environ.get("IMPLEMENTATION_MODE", "full-inference-attempt")
1281
  expected_output_type = os.environ.get("EXPECTED_OUTPUT_TYPE", "any")