File size: 32,715 Bytes
fbb9ae2
 
8100150
 
a762763
8100150
 
 
 
 
 
57b14d4
a762763
57b14d4
8100150
a762763
c6ba1cc
a762763
8100150
57b14d4
ad8ab1f
d987121
ad8ab1f
a762763
ad8ab1f
57b14d4
ad8ab1f
 
 
 
 
57b14d4
6b94010
ad8ab1f
fbb9ae2
 
8100150
ad8ab1f
 
c6ba1cc
ad8ab1f
8100150
 
fbb9ae2
8100150
 
 
 
c6ba1cc
 
 
8100150
 
 
ad8ab1f
 
 
 
 
 
 
 
 
 
 
 
86c2419
 
 
5e8eee6
86c2419
 
 
 
c6ba1cc
 
86c2419
 
 
c6ba1cc
86c2419
 
 
 
 
 
 
 
 
 
 
 
 
ad8ab1f
 
86c2419
 
 
 
 
ad8ab1f
 
86c2419
ad8ab1f
 
 
d991244
a762763
86c2419
ad8ab1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6ba1cc
ad8ab1f
c6ba1cc
ad8ab1f
 
 
c6ba1cc
ad8ab1f
86c2419
 
 
 
 
 
 
 
 
 
 
 
 
 
ad8ab1f
 
c6ba1cc
ad8ab1f
86c2419
48db666
 
 
 
 
 
 
 
 
ad8ab1f
fbb9ae2
 
c6ba1cc
fbb9ae2
8100150
 
ad8ab1f
f9f7c36
ad8ab1f
f9f7c36
d987121
f9f7c36
ad8ab1f
 
 
d987121
ad8ab1f
f9f7c36
ad8ab1f
8100150
 
fbb9ae2
c6ba1cc
fbb9ae2
ad8ab1f
fbb9ae2
ad8ab1f
 
 
 
 
 
 
 
 
fbb9ae2
 
 
 
ad8ab1f
 
fbb9ae2
 
 
ad8ab1f
 
fbb9ae2
 
c6ba1cc
 
 
 
 
fbb9ae2
 
c6ba1cc
 
fbb9ae2
c6ba1cc
ad8ab1f
fbb9ae2
c6ba1cc
fbb9ae2
 
 
c6ba1cc
ad8ab1f
 
 
 
c6ba1cc
 
 
8100150
ad8ab1f
 
fbb9ae2
c6ba1cc
 
 
fbb9ae2
ad8ab1f
 
8100150
c6ba1cc
ad8ab1f
c6ba1cc
 
0dc2b95
 
 
 
 
c6ba1cc
ad8ab1f
c6ba1cc
ad8ab1f
8100150
c6ba1cc
 
ad8ab1f
8100150
c6ba1cc
 
8100150
ad8ab1f
 
 
fbb9ae2
 
ad8ab1f
fbb9ae2
 
ad8ab1f
fbb9ae2
 
c6ba1cc
8100150
 
fbb9ae2
8100150
ad8ab1f
 
 
 
c6ba1cc
6b94010
401cc86
 
 
 
 
ad8ab1f
c6ba1cc
8100150
0dc2b95
ad8ab1f
fbb9ae2
 
c6ba1cc
8100150
ad8ab1f
c6ba1cc
8100150
 
a762763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d22d4b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a762763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8100150
fbb9ae2
 
ad8ab1f
8100150
 
 
ad8ab1f
a762763
43b03fd
ad8ab1f
c6ba1cc
8100150
 
ad8ab1f
57b14d4
ad8ab1f
c6ba1cc
ad8ab1f
8100150
ad8ab1f
 
 
 
 
 
 
 
 
48db666
ad8ab1f
 
8100150
fbb9ae2
c6ba1cc
 
 
 
ad8ab1f
 
c6ba1cc
 
8100150
ad8ab1f
8100150
ad8ab1f
 
 
fbb9ae2
ad8ab1f
8100150
 
ad8ab1f
48db666
ad8ab1f
 
 
8100150
fbb9ae2
c6ba1cc
 
 
 
ad8ab1f
 
c6ba1cc
8100150
a762763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d22d4b1
 
 
 
 
 
 
 
 
 
 
 
a762763
 
 
 
 
 
 
 
 
 
 
 
ad8ab1f
c6ba1cc
a762763
6b94010
0dc2b95
 
6b94010
 
 
 
 
 
 
a762763
ad8ab1f
6b94010
48db666
6b94010
fbb9ae2
6b94010
c6ba1cc
fbb9ae2
8100150
c6ba1cc
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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
from __future__ import annotations

import json
import random
import uuid
import shutil
import tempfile
import time
import zipfile
from pathlib import Path

import gradio as gr
from huggingface_hub import HfApi, fetch_job_logs, hf_hub_download, inspect_job, run_job, snapshot_download, whoami

APP_NAME = "Companion Forge"
VERSION = "7.0-lab-v6.4-prod"
APP_ROOT = Path(__file__).resolve().parent
TMP_ROOT = Path(tempfile.gettempdir()) / "companion-forge-v7"
TMP_ROOT.mkdir(parents=True, exist_ok=True)

L4_FLAVOR = "l4x1"
FLUX2_IMAGE = "hf.co/spaces/black-forest-labs/FLUX.2-klein-4B"
ANIGEN_IMAGE = "hf.co/spaces/VAST-AI/AniGen"
V7_RUNTIME_REPO = "patdev/Companion-Forge-L4-ONNX"

PRESETS = {
    "Desktop Companion": "friendly virtual desktop companion, expressive face, appealing mascot proportions, clean readable silhouette, full body, neutral A-pose, centered, isolated subject, no environment, no text",
    "Chibi Mascot": "cute chibi desktop mascot, large expressive head, compact proportions, full body, neutral A-pose, centered, isolated subject, clean silhouette, no environment, no text",
    "Robot": "small friendly desktop robot companion, articulated limbs, clean hard-surface design, full body, neutral A-pose, centered, isolated subject, no environment, no text",
    "Creature": "stylized fantasy creature companion, friendly and expressive, full body, standing neutral pose, centered, isolated subject, clean silhouette, no environment, no text",
    "Stylized Human": "stylized humanoid virtual companion, full body, neutral A-pose, centered, isolated subject, clean silhouette, practical clothing, no environment, no text",
}
BACKENDS = ["AniGen ONNX/TensorRT L4 — Full Engine Runtime (recommended)", "AniGen Native PyTorch — L4 Job"]
QUALITY = ["Fast", "Balanced", "Quality"]
BEHAVIOR_PROFILES = ["Code Pet", "Calm Assistant", "Energetic Mascot"]


def _require_token(oauth_token: gr.OAuthToken | None) -> str:
    if not oauth_token:
        raise gr.Error("Sign in with Hugging Face first. Companion Forge uses the OAuth `jobs` scope to launch an on-demand L4 under your account.")
    return oauth_token.token


def _expanded_prompt(prompt: str, preset: str) -> str:
    base = (prompt or "").strip()
    if not base:
        raise gr.Error("Enter a prompt first.")
    return (
        f"{base}. {PRESETS.get(preset, PRESETS['Desktop Companion'])}. high quality stylized 3D asset reference, coherent materials, "
        "entire character visible from head to feet, arms clearly separated from torso, legs clearly separated, symmetric neutral A-pose, "
        "simple light studio background, no floating objects, no props hiding limbs, animation-friendly character design"
    )


def _quality_slug(value: str) -> str:
    return str(value or "Balanced").strip().lower()


def _tail_job_logs(job_id: str, token: str, limit: int = 30) -> str:
    try:
        lines = [str(line) for line in fetch_job_logs(job_id=job_id, token=token)]
        return "\n".join(lines[-limit:])
    except Exception as exc:
        return f"Could not fetch Job logs: {exc}"


def _artifact_repo(token: str) -> tuple[HfApi, str]:
    api = HfApi(token=token)
    username = whoami(token=token)["name"]
    repo_id = f"{username}/companion-forge-jobs-artifacts"
    api.create_repo(repo_id=repo_id, repo_type="dataset", private=True, exist_ok=True)
    return api, repo_id


def _run_l4_worker(*, work: Path, worker: str, image: str, model_repo: str, model_mount: str, token: str, name: str, timeout: str, progress: gr.Progress):
    worker_src = APP_ROOT / "jobs" / worker
    runner_src = APP_ROOT / "jobs" / "repo_runner.py"
    if not worker_src.exists() or not runner_src.exists():
        raise gr.Error("Missing HF Jobs worker sources in the Space image")
    shutil.copy2(worker_src, work / "worker.py")
    shutil.copy2(runner_src, work / "runner.py")

    api, repo_id = _artifact_repo(token)
    run_id = f"{int(time.time())}-{random.randint(100000,999999)}"
    prefix = f"runs/{run_id}"
    progress(0.05, desc="Uploading private Job artifacts")
    api.upload_folder(
        repo_id=repo_id,
        repo_type="dataset",
        folder_path=work,
        path_in_repo=prefix,
        commit_message=f"Companion Forge request {run_id}",
    )

    progress(0.10, desc="Scheduling NVIDIA L4 Job")
    bootstrap = (
        "from huggingface_hub import hf_hub_download; import os,runpy; "
        "p=hf_hub_download(os.environ['CF_REPO_ID'],f\"runs/{os.environ['CF_RUN_ID']}/runner.py\","
        "repo_type='dataset',token=os.environ['HF_TOKEN']); runpy.run_path(p,run_name='__main__')"
    )
    job = run_job(
        image=image,
        command=["python", "-c", bootstrap],
        flavor=L4_FLAVOR,
        timeout=timeout,
        name=name,
        labels={"app": "companion-forge", "worker": worker.replace(".", "-").replace("/", "-"), "version": VERSION.replace(".", "-")},
        env={"CF_REPO_ID": repo_id, "CF_RUN_ID": run_id, "HF_XET_HIGH_PERFORMANCE": "1", "CF_RUNTIME_FIX": "flux2-trt" if worker == "flux2_trt_l4.py" else ("anigen-hybrid" if (worker == "anigen_hybrid_l4.py" or worker.startswith("v7_")) else ("anigen" if worker == "anigen_l4.py" else ("trellis" if worker == "trellis_l4.py" else "")))},
        secrets={"HF_TOKEN": token},
        token=token,
    )

    started = time.time()
    last_stage = None
    while True:
        info = inspect_job(job_id=job.id, token=token)
        stage = str(info.status.stage)
        if stage != last_stage:
            print(f"[Companion Forge] Job {job.id}: {stage}", flush=True)
            last_stage = stage
        if stage in {"COMPLETED", "CANCELED", "ERROR", "DELETED"}:
            break
        elapsed = time.time() - started
        if stage == "SCHEDULING":
            progress(min(0.25, 0.12 + elapsed / 600.0), desc=f"L4 scheduling · {int(elapsed)}s")
        else:
            progress(min(0.82, 0.30 + elapsed / 900.0), desc=f"L4 running · {int(elapsed)}s")
        time.sleep(2.5)

    if stage != "COMPLETED":
        raise gr.Error(f"HF Job ended with {stage}.\n\n{_tail_job_logs(job.id, token)}")

    progress(0.88, desc="Downloading private Job outputs")
    downloaded = Path(tempfile.mkdtemp(prefix="job-result-", dir=TMP_ROOT))
    snapshot_download(
        repo_id=repo_id,
        repo_type="dataset",
        token=token,
        allow_patterns=[f"{prefix}/**"],
        local_dir=downloaded,
    )
    remote_run = downloaded / prefix
    for item in remote_run.iterdir():
        if item.is_file():
            shutil.copy2(item, work / item.name)

    result_file = work / "result.json"
    if not result_file.exists():
        raise gr.Error(f"Job completed but result.json is missing.\n\n{_tail_job_logs(job.id, token)}")
    result = json.loads(result_file.read_text(encoding="utf-8"))
    result.update({"job_id": job.id, "job_url": job.url, "hardware": L4_FLAVOR, "artifact_repo": repo_id, "run_id": run_id})
    try:
        api.delete_folder(
            path_in_repo=prefix, repo_id=repo_id, repo_type="dataset",
            commit_message=f"Clean Companion Forge run {run_id}",
        )
        result["artifact_cleanup"] = True
    except Exception as exc:
        print(f"[Companion Forge] artifact cleanup skipped: {exc}", flush=True)
        result["artifact_cleanup"] = False
    return result


def generate_reference(prompt, preset, seed, randomize_seed, progress=gr.Progress(), oauth_token: gr.OAuthToken | None = None):
    token = _require_token(oauth_token)
    full_prompt = _expanded_prompt(prompt, preset)
    seed = random.randint(0, 2_147_483_647) if randomize_seed else int(seed)
    work = Path(tempfile.mkdtemp(prefix="reference-", dir=TMP_ROOT))
    (work / "request.json").write_text(json.dumps({"prompt": full_prompt, "seed": seed, "width": 512, "height": 512, "steps": 4}, indent=2), encoding="utf-8")
    result = _run_l4_worker(
        work=work, worker="flux2_trt_l4.py", image=FLUX2_IMAGE,
        model_repo="black-forest-labs/FLUX.2-klein-4B", model_mount="/models/flux2",
        token=token, name="companion-forge-flux2-trt512-reference", timeout="25m", progress=progress,
    )
    image_path = work / result.get("file", "reference.png")
    if not image_path.exists():
        raise gr.Error("FLUX.2 Klein Job did not return reference.png")
    progress(1.0, desc="Reference ready")
    job_md = f"**FLUX.2 Reference Job:** [{result['job_id']}]({result['job_url']}) · {result.get('seconds', '?')}s · runtime `{result.get('runtime','?')}` · device peak {result.get('peak_device_vram_gib', result.get('peak_vram_gib','?'))} GiB"
    return str(image_path), seed, full_prompt, job_md


def _animation_pack(profile: str, rigged: bool) -> dict:
    energetic, calm = profile == "Energetic Mascot", profile == "Calm Assistant"
    speed = 0.72 if calm else (1.25 if energetic else 1.0)
    driver = "skeletal-procedural" if rigged else "model-transform"
    clips = {
        "idle": {"loop": True, "duration": 3.4 if calm else (1.8 if energetic else 2.6), "driver": driver},
        "walk": {"loop": True, "duration": 0.72 / speed, "driver": driver},
        "wave": {"loop": False, "duration": 1.35 / speed, "driver": driver},
        "happy": {"loop": False, "duration": 1.0 / speed, "driver": driver},
        "thinking": {"loop": True, "duration": 2.2 / speed, "driver": driver},
        "typing": {"loop": True, "duration": 0.55 / speed, "driver": driver},
        "sleep": {"loop": True, "duration": 3.5, "driver": driver},
        "error": {"loop": False, "duration": 0.65, "driver": driver},
        "celebrate": {"loop": False, "duration": 1.45 / speed, "driver": driver},
    }
    states = {
        "idle": {"clip": "idle", "loop": True, "blend_ms": 180},
        "walking": {"clip": "walk", "loop": True, "blend_ms": 120},
        "waving": {"clip": "wave", "return": "idle", "blend_ms": 120},
        "happy": {"clip": "happy", "return": "idle", "blend_ms": 100},
        "thinking": {"clip": "thinking", "loop": True, "blend_ms": 180},
        "typing": {"clip": "typing", "loop": True, "blend_ms": 100},
        "sleeping": {"clip": "sleep", "loop": True, "blend_ms": 400},
        "error": {"clip": "error", "return": "idle", "blend_ms": 60},
        "celebrating": {"clip": "celebrate", "return": "idle", "blend_ms": 90},
    }
    events = {
        "agent.idle": "idle", "agent.move": "walking", "agent.hello": "waving",
        "agent.message.received": "happy", "agent.thinking": "thinking",
        "agent.tool.start": "typing", "agent.tool.end": "happy",
        "agent.task.complete": "celebrating", "agent.error": "error",
        "agent.sleep": "sleeping", "agent.wake": "idle",
    }
    return {
        "format": "companion-motion/v2", "profile": profile, "rigged": rigged,
        "default_state": "idle", "blend_mode": "crossfade",
        "semantic_bones": {
            "strategy": "geometry-inference", "source_pattern": "joint_*" if rigged else None,
            "targets": ["root", "body", "head", "arm_l", "forearm_l", "hand_l", "arm_r", "forearm_r", "hand_r", "leg_l", "shin_l", "foot_l", "leg_r", "shin_r", "foot_r", "antenna"] if rigged else [],
        },
        "clips": clips, "states": states, "events": events,
    }


def _build_bundle(work: Path, *, image_path: str, worker_result: dict, backend: str, behavior_profile: str, prompt: str = "", expanded_prompt: str = "", preset: str = ""):
    output_dir = Path(tempfile.mkdtemp(prefix="output-", dir=TMP_ROOT))
    source_model = work / worker_result.get("file", "companion.glb")
    if not source_model.exists():
        raise gr.Error("The L4 Job completed but companion.glb is missing")
    reference_out, model_out = output_dir / "reference.png", output_dir / "companion.glb"
    skeleton_out, manifest_out = output_dir / "skeleton.glb", output_dir / "companion.json"
    animations_out, bundle_out = output_dir / "animations.json", output_dir / "companion-bundle.zip"
    shutil.copy2(image_path, reference_out)
    shutil.copy2(source_model, model_out)
    rigged = worker_result.get("kind") == "rigged"
    skeleton_file = None
    src_skeleton = worker_result.get("skeleton_file")
    if src_skeleton and (work / src_skeleton).exists():
        shutil.copy2(work / src_skeleton, skeleton_out)
        skeleton_file = str(skeleton_out)
    animations = _animation_pack(behavior_profile, rigged)
    animations_out.write_text(json.dumps(animations, indent=2), encoding="utf-8")
    manifest = {
        "format": "companion-forge/v5.1",
        "execution": {
            "platform": "Hugging Face Jobs", "hardware": L4_FLAVOR,
            "job_id": worker_result.get("job_id"), "job_url": worker_result.get("job_url"),
            "seconds": worker_result.get("seconds"),
            "runtime": worker_result.get("runtime"),
            "peak_vram_gib": worker_result.get("peak_vram_gib"),
            "peak_device_vram_gib": worker_result.get("peak_device_vram_gib"),
            "baseline_device_vram_gib": worker_result.get("baseline_device_vram_gib"),
            "gpu": worker_result.get("gpu"), "stages": worker_result.get("stages"),
        },
        "prompt": prompt or None, "expanded_prompt": expanded_prompt or None, "preset": preset or None,
        "backend": backend,
        "mesh": {
            "file": "companion.glb", "vertices": worker_result.get("vertices"), "faces": worker_result.get("faces"),
            "resolution": worker_result.get("resolution"), "texture_size": worker_result.get("texture_size"),
            "texture_mode": worker_result.get("texture_mode", "PBR"),
        },
        "rig": {"rigged": rigged, "skeleton_file": "skeleton.glb" if skeleton_file else None, "joints": worker_result.get("joints"), "skinning": rigged},
        "behavior": {"profile": behavior_profile, "animation_file": "animations.json", "states": list(animations["states"])},
    }
    manifest_out.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
    with zipfile.ZipFile(bundle_out, "w", zipfile.ZIP_DEFLATED) as archive:
        archive.write(model_out, "companion.glb")
        archive.write(reference_out, "reference.png")
        archive.write(manifest_out, "companion.json")
        archive.write(animations_out, "animations.json")
        if skeleton_file:
            archive.write(skeleton_out, "skeleton.glb")
    return str(reference_out), str(model_out), skeleton_file, str(bundle_out), manifest, animations


def generate_3d_from_image(image_path, backend, behavior_profile, quality, seed, randomize_seed, texture_size, prompt="", expanded_prompt="", preset="", progress=gr.Progress(), oauth_token: gr.OAuthToken | None = None):
    if not image_path:
        raise gr.Error("Provide an image first.")
    token = _require_token(oauth_token)
    seed = random.randint(0, 2_147_483_647) if randomize_seed else int(seed)
    work = Path(tempfile.mkdtemp(prefix="forge-", dir=TMP_ROOT))
    suffix = Path(image_path).suffix.lower() or ".png"
    local_input = work / f"input{suffix}"
    shutil.copy2(image_path, local_input)
    (work / "request.json").write_text(json.dumps({"input_file": local_input.name, "seed": seed, "quality": _quality_slug(quality), "texture_size": int(texture_size)}, indent=2), encoding="utf-8")
    if backend.startswith("AniGen ONNX/TensorRT") or backend.startswith("AniGen Hybrid"):
        result = _run_l4_worker(work=work, worker="anigen_hybrid_l4.py", image=ANIGEN_IMAGE, model_repo="VAST-AI/AniGen", model_mount="/models/anigen", token=token, name="companion-forge-anigen-hybrid", timeout="35m", progress=progress)
    elif backend.startswith("AniGen Native"):
        result = _run_l4_worker(work=work, worker="anigen_l4.py", image=ANIGEN_IMAGE, model_repo="VAST-AI/AniGen", model_mount="/models/anigen", token=token, name="companion-forge-anigen-native", timeout="35m", progress=progress)
    else:
        raise gr.Error("Unknown L4 backend")
    progress(0.93, desc="Building companion bundle")
    outputs = _build_bundle(work, image_path=image_path, worker_result=result, backend=backend, behavior_profile=behavior_profile, prompt=prompt, expanded_prompt=expanded_prompt, preset=preset)
    progress(1.0, desc="Companion ready")
    job_md = f"**3D Job:** [{result['job_id']}]({result['job_url']}) · {result.get('seconds', '?')}s · runtime `{result.get('runtime','?')}` · device peak {result.get('peak_device_vram_gib', result.get('peak_vram_gib','?'))} GiB"
    return (*outputs, seed, job_md)


def text_to_3d_generate(reference_path, prompt, expanded_prompt, preset, backend, behavior_profile, quality, seed, randomize_seed, texture_size, progress=gr.Progress(), oauth_token: gr.OAuthToken | None = None):
    if not reference_path:
        raise gr.Error("Generate a reference first.")
    return generate_3d_from_image(reference_path, backend, behavior_profile, quality, seed, randomize_seed, texture_size, prompt=prompt, expanded_prompt=expanded_prompt, preset=preset, progress=progress, oauth_token=oauth_token)


def _v7_user_repos(token: str) -> tuple[str, str, str]:
    api = HfApi(token=token)
    username = whoami(token=token)["name"]
    cache_repo = f"{username}/companion-forge-v7-teacher-cache"
    students_repo = f"{username}/companion-forge-v7-students"
    api.create_repo(repo_id=cache_repo, repo_type="dataset", private=True, exist_ok=True)
    api.create_repo(repo_id=students_repo, repo_type="model", private=True, exist_ok=True)
    return username, cache_repo, students_repo


def v7_refresh_status(oauth_token: gr.OAuthToken | None = None):
    token = oauth_token.token if oauth_token else None
    try:
        local = hf_hub_download(V7_RUNTIME_REPO, "v7/bench/validation_status.json", repo_type="model", token=token, force_download=True)
        status = json.loads(Path(local).read_text(encoding="utf-8"))
        if token:
            username, cache_repo, students_repo = _v7_user_repos(token)
            status["user"] = {"name": username, "cache_repo": cache_repo, "students_repo": students_repo}
        else:
            status["user"] = {"signed_in": False}
        return status
    except Exception as exc:
        return {"error": str(exc)}


def v7_add_to_cache(image, seed, progress=gr.Progress(), oauth_token: gr.OAuthToken | None = None):
    token = _require_token(oauth_token)
    if not image:
        raise gr.Error("Choose a reference image first.")
    _, cache_repo, _ = _v7_user_repos(token)
    work = Path(tempfile.mkdtemp(prefix="v7-cache-", dir=TMP_ROOT))
    src = work / "reference.png"
    shutil.copy2(Path(image), src)
    key = f"ref-{int(seed)}-{uuid.uuid4().hex[:10]}"
    request = {"input_file": src.name, "seed": int(seed), "ss_steps": 10, "cache_repo": cache_repo, "key": key}
    (work / "request.json").write_text(json.dumps(request, indent=2), encoding="utf-8")
    result = _run_l4_worker(
        work=work, worker="v7_cache_l4.py", image=ANIGEN_IMAGE,
        model_repo="", model_mount="", token=token,
        name="companion-forge-v7-cache", timeout="35m", progress=progress,
    )
    progress(1.0, desc="v7 cache ready")
    return (
        f"**v7 cache:** `{cache_repo}:{result.get('path')}` · "
        f"{result.get('bytes',0)/1e6:.1f} MB · geo coords {result.get('coords')} · skeleton coords {result.get('coords_skl')}",
        result,
    )



def v7_add_multiview_cache(front, left, back, right, seed, progress=gr.Progress(), oauth_token: gr.OAuthToken | None = None):
    token = _require_token(oauth_token)
    views = {"front": front, "left": left, "back": back, "right": right}
    missing = [name for name, value in views.items() if not value]
    if missing:
        raise gr.Error("Missing multi-view images: " + ", ".join(missing))
    _, cache_repo, _ = _v7_user_repos(token)
    work = Path(tempfile.mkdtemp(prefix="v7-multiview-", dir=TMP_ROOT))
    request = {"seed": int(seed), "ss_steps": 10, "cache_repo": cache_repo, "key": f"multiview-{int(seed)}-{uuid.uuid4().hex[:10]}"}
    for name, value in views.items():
        dst = work / f"{name}.png"
        shutil.copy2(Path(value), dst)
        request[f"{name}_file"] = dst.name
    (work / "request.json").write_text(json.dumps(request, indent=2), encoding="utf-8")
    result = _run_l4_worker(
        work=work, worker="v7_multiview_cache_l4.py", image=ANIGEN_IMAGE,
        model_repo="", model_mount="", token=token,
        name="companion-forge-v7-multiview", timeout="45m", progress=progress,
    )
    progress(1.0, desc="v7 multi-view cache ready")
    return (
        f"**v7 multi-view cache:** `{cache_repo}:{result.get('path')}` · fusion `{result.get('fusion')}` · "
        f"geo coords {result.get('coords')} · skeleton coords {result.get('coords_skl')}",
        result,
    )

def v7_train_stage(component, stage_label, run_length, progress=gr.Progress(), oauth_token: gr.OAuthToken | None = None):
    token = _require_token(oauth_token)
    _, cache_repo, students_repo = _v7_user_repos(token)
    stage_map = {"10 → 4": 0, "4 → 2": 1, "2 → 1": 2}
    step_map = {"Smoke · 1 step": 1, "Short · 100 steps": 100, "Configured stage": 0}
    stage = stage_map[stage_label]
    steps = step_map[run_length]
    chain = {
        0: [],
        1: [f"{component}/10to4/adapter.pt"],
        2: [f"{component}/10to4/adapter.pt", f"{component}/4to2/adapter.pt"],
    }[stage]
    api = HfApi(token=token)
    missing = [path for path in chain if not api.file_exists(repo_id=students_repo, filename=path, repo_type="model")]
    if missing:
        raise gr.Error("Previous v7 stage is missing: " + ", ".join(missing))
    request = {
        "component": component, "stage": stage, "steps": steps,
        "cache_repo": cache_repo, "cache_prefix": "cache",
        "students_repo": students_repo, "resume_paths": chain,
    }
    work = Path(tempfile.mkdtemp(prefix="v7-train-", dir=TMP_ROOT))
    (work / "request.json").write_text(json.dumps(request, indent=2), encoding="utf-8")
    result = _run_l4_worker(
        work=work, worker="v7_train_l4.py", image=ANIGEN_IMAGE,
        model_repo="", model_mount="", token=token,
        name=f"companion-forge-v7-{component}-{stage}", timeout="90m", progress=progress,
    )
    metrics = result.get("job_metrics", {})
    progress(1.0, desc="v7 training stage complete")
    return (
        f"**v7 {component} {stage_label}:** `{students_repo}:{result.get('adapter_path')}` · "
        f"{metrics.get('elapsed_s','?')} s · peak {metrics.get('peak_vram_gib','?')} GiB",
        result,
    )


CSS = """
.gradio-container { max-width: 1500px !important; }
#hero { text-align:center; margin: 4px 0 14px; }
#hero h1 { font-size: 2.25rem; margin-bottom: .15rem; }
#hero p { opacity: .72; margin-top: 0; }
"""

with gr.Blocks(title=APP_NAME, delete_cache=(3600, 3600)) as demo:
    gr.HTML("<div id='hero'><h1>🤖 Companion Forge v7 Lab</h1><p>Validated v6.4 production runtime + v7 few-step distillation, FP8, 2:4 sparsity, multi-view, symmetry and MoE research on on-demand NVIDIA L4.</p></div>")
    with gr.Row():
        gr.LoginButton(value="Sign in with Hugging Face", logout_value="Logout ({})", variant="huggingface")
        gr.Markdown("**Compute:** `l4x1` · 8 vCPU · 30 GB RAM · 1× NVIDIA L4 (24 GB class). Jobs stop automatically after generation.")

    with gr.Tabs():
        with gr.Tab("Text → Code Pet"):
            with gr.Row():
                with gr.Column(scale=5, min_width=390):
                    prompt = gr.Textbox(label="Prompt", placeholder="A cute white chibi desktop robot, blue screen face, small antenna, articulated arms and legs...", lines=4)
                    preset = gr.Dropdown(list(PRESETS), value="Robot", label="Character preset")
                    with gr.Row():
                        seed = gr.Number(value=42, precision=0, label="Seed")
                        randomize = gr.Checkbox(value=True, label="Randomize")
                    gen_ref = gr.Button("1. Generate Reference on L4", variant="primary", size="lg")
                    reference = gr.Image(label="Reference — edit/replace before 3D", type="filepath", height=380)
                    expanded = gr.Textbox(visible=False)
                    ref_job = gr.Markdown()
                    backend = gr.Dropdown(BACKENDS, value=BACKENDS[0], label="3D backend")
                    behavior = gr.Dropdown(BEHAVIOR_PROFILES, value="Code Pet", label="Behavior profile")
                    quality = gr.Radio(QUALITY, value="Balanced", label="L4 optimization profile")
                    texture = gr.State(0)
                    forge = gr.Button("2. Forge Companion on L4", variant="primary", size="lg")
                    forge_job = gr.Markdown()
                with gr.Column(scale=7):
                    with gr.Tabs():
                        with gr.Tab("Companion"): model = gr.Model3D(label="companion.glb", height=590, display_mode="solid")
                        with gr.Tab("Skeleton"): skeleton = gr.Model3D(label="skeleton.glb", height=590, display_mode="solid")
                        with gr.Tab("Behavior"): animation = gr.JSON(label="State machine")
                        with gr.Tab("Manifest"): manifest = gr.JSON(label="Generation metadata / benchmark")
                    bundle = gr.File(label="Download Code Pet bundle")
                    final_ref = gr.Image(visible=False)
            gen_ref.click(generate_reference, inputs=[prompt, preset, seed, randomize], outputs=[reference, seed, expanded, ref_job], concurrency_limit=1)
            forge.click(text_to_3d_generate, inputs=[reference, prompt, expanded, preset, backend, behavior, quality, seed, randomize, texture], outputs=[final_ref, model, skeleton, bundle, manifest, animation, seed, forge_job], concurrency_limit=1)

        with gr.Tab("Image → Code Pet"):
            with gr.Row():
                with gr.Column(scale=5, min_width=390):
                    image = gr.Image(label="Input character", type="filepath", image_mode="RGBA", height=470)
                    img_backend = gr.Dropdown(BACKENDS, value=BACKENDS[0], label="3D backend")
                    img_behavior = gr.Dropdown(BEHAVIOR_PROFILES, value="Code Pet", label="Behavior profile")
                    img_quality = gr.Radio(QUALITY, value="Balanced", label="L4 optimization profile")
                    with gr.Row():
                        img_seed = gr.Number(value=42, precision=0, label="Seed")
                        img_random = gr.Checkbox(value=True, label="Randomize")
                    img_texture = gr.State(0)
                    img_forge = gr.Button("Forge on L4", variant="primary", size="lg")
                    img_job = gr.Markdown()
                    gr.Markdown("Best rigging: one full-body character, clean background, separated limbs, neutral A-pose.")
                with gr.Column(scale=7):
                    with gr.Tabs():
                        with gr.Tab("Companion"): img_model = gr.Model3D(label="companion.glb", height=590, display_mode="solid")
                        with gr.Tab("Skeleton"): img_skeleton = gr.Model3D(label="skeleton.glb", height=590, display_mode="solid")
                        with gr.Tab("Behavior"): img_animation = gr.JSON(label="State machine")
                        with gr.Tab("Manifest"): img_manifest = gr.JSON(label="Generation metadata / benchmark")
                    img_bundle = gr.File(label="Download Code Pet bundle")
                    img_final_ref = gr.Image(visible=False)
            img_forge.click(generate_3d_from_image, inputs=[image, img_backend, img_behavior, img_quality, img_seed, img_random, img_texture], outputs=[img_final_ref, img_model, img_skeleton, img_bundle, img_manifest, img_animation, img_seed, img_job], concurrency_limit=1)

        with gr.Tab("V7 Lab"):
            gr.Markdown(
                "### 🧪 Companion Forge v7 — Distillation & Optimization Lab\n"
                "Generation stays on the validated v6.4 TensorRT runtime until a v7 student passes quality, finite-tensor and rig gates. "
                "References enter the training cache **only when you explicitly click the cache button**; the cache and student repos are private to your HF account."
            )
            v7_refresh_btn = gr.Button("Refresh v7 status")
            v7_status = gr.JSON(value={"status": "Click Refresh v7 status"}, label="Technology validation status")
            gr.Markdown("#### 1. Add a training reference")
            with gr.Row():
                v7_cache_image = gr.Image(type="filepath", label="Reference image", sources=["upload", "clipboard"])
                with gr.Column():
                    v7_cache_seed = gr.Number(value=42, precision=0, label="Cache seed")
                    v7_cache_btn = gr.Button("Add to private v7 cache", variant="primary")
                    v7_cache_msg = gr.Markdown()
            v7_cache_result = gr.JSON(label="Cache Job result")
            with gr.Accordion("Optional 4-view cache · front / left / back / right", open=False):
                with gr.Row():
                    v7_mv_front = gr.Image(type="filepath", label="Front", sources=["upload", "clipboard"])
                    v7_mv_left = gr.Image(type="filepath", label="Left", sources=["upload", "clipboard"])
                    v7_mv_back = gr.Image(type="filepath", label="Back", sources=["upload", "clipboard"])
                    v7_mv_right = gr.Image(type="filepath", label="Right", sources=["upload", "clipboard"])
                with gr.Row():
                    v7_mv_seed = gr.Number(value=42, precision=0, label="Multi-view seed")
                    v7_mv_btn = gr.Button("Add 4-view set to private v7 cache")
                v7_mv_msg = gr.Markdown()
                v7_mv_result = gr.JSON(label="Multi-view cache Job result")
                v7_mv_btn.click(v7_add_multiview_cache, [v7_mv_front, v7_mv_left, v7_mv_back, v7_mv_right, v7_mv_seed], [v7_mv_msg, v7_mv_result], concurrency_limit=1)
            gr.Markdown("#### 2. Train a few-step flow student")
            with gr.Row():
                v7_component = gr.Dropdown(["ss_flow", "slat_flow"], value="ss_flow", label="Component")
                v7_stage = gr.Dropdown(["10 → 4", "4 → 2", "2 → 1"], value="10 → 4", label="Progressive stage")
                v7_length = gr.Radio(["Smoke · 1 step", "Short · 100 steps", "Configured stage"], value="Smoke · 1 step", label="Run length")
            v7_train_btn = gr.Button("Launch v7 L4 training", variant="primary")
            v7_train_msg = gr.Markdown()
            v7_train_result = gr.JSON(label="Training Job result")
            v7_refresh_btn.click(v7_refresh_status, inputs=[], outputs=[v7_status])
            v7_cache_btn.click(v7_add_to_cache, [v7_cache_image, v7_cache_seed], [v7_cache_msg, v7_cache_result], concurrency_limit=1)
            v7_train_btn.click(v7_train_stage, [v7_component, v7_stage, v7_length], [v7_train_msg, v7_train_result], concurrency_limit=1)

        with gr.Tab("L4 Architecture"):
            gr.Markdown("""
### Production v6.4 + v7 training architecture
`CPU Space` → private request repo → `HF Job l4x1` → **staged ONNX/TensorRT engines** → minimal sparse topology shell → private output sync → GPU terminates.

**Production runtime order**
- FLUX.2 Klein transformer: **ONNX opset 23 + TensorRT FP16 / SM89**, static 512 profile; BF16 PyTorch fallback.
- DINOv2: **ONNX + TensorRT** → ORT CUDA → PyTorch.
- DSINE: **ONNX + TensorRT** → staged native/ORT fallback.
- AniGen SS Flow: **full ONNX opset 23 + TensorRT**.
- AniGen SS decoder: **full ONNX opset 23 + TensorRT**.
- AniGen SLat Flow: **24-block transformer core ONNX/TensorRT** + only a ~188 MB native sparse IO shell; the old ~2.46 GB PyTorch flow checkpoint is no longer downloaded normally.
- AniGen skin decoder: **dynamic ONNX/TensorRT**, profile up to 300k vertices / 64 joints.
- SLat topology/decoder is now backed by the validated custom ONNX/TensorRT plugin stack (`SparseConv3D`, `SparseWindowAttention`, `SparseDownsample`, `SparseUpsample`, `SparseSubdivide`, `SparseMeshTopologyExtract`); native decoder is lazy fallback only.

**Fast**: 10/10 SS + SLat steps. **Balanced**: 14/14. **Quality**: 20/20.

Validated Fast L4 runtime: `dino-trt + dsine-trt + slat-shell-trt + skin-trt + ss-trt`, GLB + skeleton exported with no PyTorch flow-model fallback.

The rigged path exports **vertex-color GLB** to avoid nvdiffrast CUDA JIT. TensorRT plans are precompiled for NVIDIA L4 / SM89; dense conditionners are freed before 3D generation to limit VRAM overlap.
            """)

if __name__ == "__main__":
    demo.queue(default_concurrency_limit=2).launch(css=CSS, ssr_mode=False, show_error=True, mcp_server=True)