File size: 11,212 Bytes
6e53100
 
 
 
 
542fcf0
6e53100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592d705
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e53100
592d705
 
 
 
6e53100
 
 
592d705
 
 
6e53100
 
592d705
01a3754
 
6e53100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
945f24f
 
 
542fcf0
 
 
 
 
 
945f24f
 
542fcf0
 
 
 
945f24f
 
 
 
 
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
from __future__ import annotations

import re
from typing import Any

from huggingface_hub import Volume, fetch_job_logs, inspect_job, run_job

from .config import bucket_uri_from_source, user_bucket_source, settings
from .bucket import assert_user_bucket_ready
from .runs import make_run_id, utc_now_iso, validate_run_id
from .worker_payload import (
    encoded_universal_model_card_worker_script,
    encoded_validate_existing_space_worker_script,
    python_decode_and_run_command,
)

SPACE_SLUG_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{1,95}$")


def _base_env(*, run_id: str, username: str, bucket_source: str, worker_script_b64: str) -> dict[str, str]:
    return {
        "RUN_ID": run_id,
        "HF_USERNAME": username or "unknown",
        "BUCKET_SOURCE": bucket_source,
        "OUTPUT_ROOT": settings.bucket_mount,
        "WORKER_SCRIPT_B64": worker_script_b64,
        "LAUNCHED_AT": utc_now_iso(),
    }


def _launch_job(*, token: str, env: dict[str, str], bucket_source: str, flavor: str | None = None, timeout: str | None = None) -> Any:
    return run_job(
        image=settings.job_image,
        command=python_decode_and_run_command(),
        flavor=flavor or settings.job_flavor,
        timeout=timeout or settings.job_timeout,
        env=env,
        secrets={"HF_TOKEN": token},
        volumes=[Volume(type="bucket", source=bucket_source, mount_path=settings.bucket_mount)],
        token=token,
    )


def _job_field(job: Any, name: str, default: Any = None) -> Any:
    if isinstance(job, dict):
        return job.get(name, default)
    return getattr(job, name, default)


def _job_status_stage(job: Any) -> Any:
    status = _job_field(job, "status")
    if isinstance(status, dict):
        return status.get("stage") or status.get("status") or status.get("state")
    return getattr(status, "stage", None) or getattr(status, "status", None) or getattr(status, "state", None)


def _job_url_from_id(*, job_id: str | None, bucket_source: str) -> str:
    if not job_id:
        return ""
    owner = str(bucket_source or "").split("/", 1)[0].strip()
    return f"https://huggingface.co/jobs/{owner}/{job_id}" if owner else ""


def _job_result(job: Any, *, run_id: str, kind: str, bucket_source: str, extra: dict[str, Any] | None = None) -> dict[str, Any]:
    job_id = str(_job_field(job, "id") or _job_field(job, "job_id") or _job_field(job, "jobId") or "").strip()
    job_url = str(_job_field(job, "url") or _job_field(job, "job_url") or "").strip()
    if not job_url:
        job_url = _job_url_from_id(job_id=job_id, bucket_source=bucket_source)
    payload: dict[str, Any] = {
        "run_id": run_id,
        "kind": kind,
        "job_id": job_id,
        "job_url": job_url,
        "status": _job_status_stage(job),
        "bucket_source": bucket_source,
        "bucket_uri": bucket_uri_from_source(bucket_source),
        "created_by": str(bucket_source or "").split("/", 1)[0].strip(),
        "created_at": str(_job_field(job, "created_at") or utc_now_iso()),
        "started_at": str(_job_field(job, "started_at") or _job_field(job, "created_at") or utc_now_iso()),
    }
    if extra:
        payload.update(extra)
    return payload


def normalize_target_space(*, username: str, target_slug: str | None, run_id: str) -> str:
    """Return `username/slug`, constrained to the signed-in user's namespace."""
    slug = (target_slug or "").strip()
    if not slug:
        slug = f"space-factory-{run_id}".lower()[:80]
    if "/" in slug:
        namespace, repo = slug.split("/", 1)
        if namespace != username:
            raise ValueError("The target Space must be created in your own namespace.")
        slug = repo
    if not SPACE_SLUG_RE.match(slug):
        raise ValueError("Invalid target Space name. Use letters, numbers, dots, underscores, or dashes.")
    return f"{username}/{slug}"


def _clean_repo_id(value: str | None, *, repo_kind: str) -> str:
    cleaned = (value or "").strip()
    cleaned = cleaned.replace("https://huggingface.co/spaces/", "")
    cleaned = cleaned.replace("https://huggingface.co/", "")
    cleaned = cleaned.strip("/")
    if "/" not in cleaned:
        raise ValueError(f"{repo_kind} must look like owner/name or a Hugging Face URL.")
    return cleaned


def launch_universal_model_card_job(
    *,
    token: str,
    username: str,
    target_slug: str | None = None,
    model_id: str | None = None,
    pi_model: str | None = None,
    preferred_space_hardware: str | None = None,
    fallback_space_hardware: str | None = None,
    allow_fixed_gpu_fallback: bool = True,
    implementation_mode: str | None = None,
    run_id: str | None = None,
    bucket_name: str | None = None,
) -> dict[str, Any]:
    """Launch the public product builder: model card → private Space attempt."""
    if not token:
        raise ValueError("Missing OAuth token. Please sign in with Hugging Face first.")
    safe_run_id = validate_run_id(run_id) if run_id else make_run_id("universal")
    target_space_id = normalize_target_space(username=username, target_slug=target_slug, run_id=safe_run_id)
    clean_model_id = _clean_repo_id(model_id, repo_kind="Model ID")
    bucket_source = user_bucket_source(username=username, bucket_name=bucket_name)
    assert_user_bucket_ready(username=username, bucket_name=bucket_name, token=token)

    env = _base_env(
        run_id=safe_run_id,
        username=username,
        bucket_source=bucket_source,
        worker_script_b64=encoded_universal_model_card_worker_script(),
    )
    env["TARGET_SPACE_ID"] = target_space_id
    env["MODEL_ID"] = clean_model_id
    env["PI_MODEL"] = (pi_model or "Qwen/Qwen3-Coder-Next").strip()
    env["PREFERRED_SPACE_HARDWARE"] = (preferred_space_hardware or "zero-a10g").strip()
    env["FALLBACK_SPACE_HARDWARE"] = (fallback_space_hardware or "l40sx1").strip()
    env["ALLOW_FIXED_GPU_FALLBACK"] = "true" if allow_fixed_gpu_fallback else "false"
    env["IMPLEMENTATION_MODE"] = (implementation_mode or "full-inference-gated").strip()
    job = _launch_job(token=token, env=env, bucket_source=bucket_source, timeout="60m")
    return _job_result(
        job,
        run_id=safe_run_id,
        kind="universal_model_card_builder",
        bucket_source=bucket_source,
        extra={
            "target_space": target_space_id,
            "target_space_url": f"https://huggingface.co/spaces/{target_space_id}",
            "model_id": clean_model_id,
            "pi_model": env["PI_MODEL"],
            "preferred_space_hardware": env["PREFERRED_SPACE_HARDWARE"],
            "fallback_space_hardware": env["FALLBACK_SPACE_HARDWARE"],
            "allow_fixed_gpu_fallback": allow_fixed_gpu_fallback,
            "implementation_mode": env["IMPLEMENTATION_MODE"],
        },
    )


def launch_validate_existing_space_job(
    *,
    token: str,
    username: str,
    target_space_id: str,
    api_name: str | None = None,
    test_args_json: str | None = None,
    test_kwargs_json: str | None = None,
    expected_output_type: str | None = None,
    live_timeout_seconds: int = 1800,
    run_id: str | None = None,
    bucket_name: str | None = None,
) -> dict[str, Any]:
    """Launch the public product validator for an existing generated Space."""
    if not token:
        raise ValueError("Missing OAuth token. Please sign in with Hugging Face first.")
    safe_run_id = validate_run_id(run_id) if run_id else make_run_id("validate")
    target = _clean_repo_id(target_space_id, repo_kind="Target Space")
    namespace, _ = target.split("/", 1)
    if namespace != username:
        raise ValueError("For this version, target Space validation is limited to your own namespace.")
    bucket_source = user_bucket_source(username=username, bucket_name=bucket_name)
    assert_user_bucket_ready(username=username, bucket_name=bucket_name, token=token)
    env = _base_env(
        run_id=safe_run_id,
        username=username,
        bucket_source=bucket_source,
        worker_script_b64=encoded_validate_existing_space_worker_script(),
    )
    env["TARGET_SPACE_ID"] = target
    env["API_NAME"] = (api_name or "/generate").strip()
    env["TEST_ARGS_JSON"] = (test_args_json or '["a cinematic robot cat astronaut, detailed, studio lighting"]').strip()
    env["TEST_KWARGS_JSON"] = (test_kwargs_json or "{}").strip()
    env["EXPECTED_OUTPUT_TYPE"] = (expected_output_type or "image").strip()
    env["LIVE_TIMEOUT_SECONDS"] = str(int(live_timeout_seconds or 1800))
    job = _launch_job(token=token, env=env, bucket_source=bucket_source, timeout="60m")
    return _job_result(
        job,
        run_id=safe_run_id,
        kind="validate_existing_space",
        bucket_source=bucket_source,
        extra={
            "target_space": target,
            "target_space_url": f"https://huggingface.co/spaces/{target}",
            "api_name": env["API_NAME"],
            "expected_output_type": env["EXPECTED_OUTPUT_TYPE"],
            "test_args_json": env["TEST_ARGS_JSON"],
            "test_kwargs_json": env["TEST_KWARGS_JSON"],
        },
    )


def inspect_job_safe(job_id: str, token: str | None = None) -> dict[str, Any]:
    if not job_id:
        return {"error": "Missing job_id"}
    try:
        info = inspect_job(job_id=job_id, token=token)
        status = getattr(info, "status", None)
        return {
            "id": info.id,
            "url": getattr(info, "url", None),
            "stage": getattr(status, "stage", None),
            "message": getattr(status, "message", None),
            "flavor": getattr(info, "flavor", None),
            "created_at": str(getattr(info, "created_at", "")),
            "started_at": str(getattr(info, "started_at", "")),
            "finished_at": str(getattr(info, "finished_at", "")),
        }
    except Exception as exc:  # noqa: BLE001
        return {"error": str(exc)}


def fetch_recent_logs_safe(job_id: str, token: str | None = None, max_lines: int = 120) -> str:
    if not job_id:
        return ""
    try:
        logs = list(fetch_job_logs(job_id=job_id, token=token))
        return "\n".join(str(line).rstrip("\n") for line in logs[-max_lines:])
    except Exception as exc:  # noqa: BLE001
        return f"Could not fetch job logs: {exc}"


def cancel_job_safe(job_id: str, *, namespace: str | None = None, token: str | None = None) -> dict[str, Any]:
    """Cancel a running HF Job. Returns a JSON-safe status payload.

    Import cancel_job lazily so older cached Space environments do not crash the
    whole app at import time. The endpoint reports a clear error if the installed
    huggingface_hub build does not expose Jobs cancellation.
    """
    if not job_id:
        return {"ok": False, "error": "Missing job_id"}
    try:
        from huggingface_hub import cancel_job  # type: ignore
    except Exception as exc:  # noqa: BLE001
        return {"ok": False, "job_id": job_id, "error": f"huggingface_hub.cancel_job is unavailable: {exc}"}
    try:
        cancel_job(job_id=job_id, namespace=namespace, token=token)
        return {"ok": True, "job_id": job_id, "status": "cancelled"}
    except Exception as exc:  # noqa: BLE001
        return {"ok": False, "job_id": job_id, "error": str(exc)}