Yang2001 commited on
Commit
a8fdda0
·
verified ·
1 Parent(s): 5b10b87

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +78 -0
  2. .gitignore +20 -0
  3. README.md +9 -9
  4. app.py +379 -0
  5. app_bak.py +493 -0
  6. app_local.py +597 -0
  7. app_proxy.py +379 -0
  8. assets/app/basecolor.png +0 -0
  9. assets/app/clay.png +0 -0
  10. assets/app/hdri_city.png +0 -0
  11. assets/app/hdri_courtyard.png +0 -0
  12. assets/app/hdri_forest.png +0 -0
  13. assets/app/hdri_interior.png +0 -0
  14. assets/app/hdri_night.png +0 -0
  15. assets/app/hdri_studio.png +0 -0
  16. assets/app/hdri_sunrise.png +0 -0
  17. assets/app/hdri_sunset.png +0 -0
  18. assets/app/normal.png +0 -0
  19. assets/hdri/city.exr +3 -0
  20. assets/hdri/courtyard.exr +3 -0
  21. assets/hdri/forest.exr +3 -0
  22. assets/hdri/interior.exr +3 -0
  23. assets/hdri/license.txt +15 -0
  24. assets/hdri/night.exr +3 -0
  25. assets/hdri/studio.exr +0 -0
  26. assets/hdri/sunrise.exr +3 -0
  27. assets/hdri/sunset.exr +3 -0
  28. assets/images/0_img.png +3 -0
  29. assets/images/10_img.webp +3 -0
  30. assets/images/11_img.png +3 -0
  31. assets/images/12_img.png +3 -0
  32. assets/images/17_img.png +3 -0
  33. assets/images/1_img.png +3 -0
  34. assets/images/21_img.png +3 -0
  35. assets/images/3_img.webp +3 -0
  36. assets/images/4_img.png +3 -0
  37. assets/images/5_img.webp +3 -0
  38. assets/images/5c80e5e03a3b60b6f03eaf555ba1dafc0e4230c472d7e8c8e2c5ca0a0dfcef10.webp +3 -0
  39. assets/images/6_img.png +3 -0
  40. assets/images/7_img.png +3 -0
  41. assets/images/9_img.png +3 -0
  42. assets/images/c9340e744541f310bf89838f652602961d3e5950b31cd349bcbfc7e59e15cd2e.webp +3 -0
  43. assets/images/f94e2b76494ce2cf1874611273e5fb3d76b395793bb5647492fa85c2ce0a248b.webp +0 -0
  44. assets/images/musicman.png +3 -0
  45. assets/images/pizza.png +3 -0
  46. assets/images/s_13_img.jpg +3 -0
  47. assets/images/s_14_img.jpg +3 -0
  48. assets/images/s_15_img.png +3 -0
  49. assets/images/s_16_img.png +3 -0
  50. assets/images/s_18_img.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,81 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.webp filter=xet diff=xet merge=xet -text
37
+ *.exr filter=xet diff=xet merge=xet -text
38
+ *.png filter=xet diff=xet merge=xet -text
39
+ *.ply filter=xet diff=xet merge=xet -text
40
+ assets/example_image/0a34fae7ba57cb8870df5325b9c30ea474def1b0913c19c596655b85a79fdee4.webp filter=lfs diff=lfs merge=lfs -text
41
+ assets/example_image/0e4984a9b3765ce80e9853443f9319ecedf90885c74b56cccfebc09402740f8a.webp filter=lfs diff=lfs merge=lfs -text
42
+ assets/example_image/0f168a4b1b6e96c72e9627c97a212c27a4572250ff58e25703b9d0c2bc74191a.webp filter=lfs diff=lfs merge=lfs -text
43
+ assets/example_image/130c2b18f1651a70f8aa15b2c99f8dba29bb943044d92871f9223bd3e989e8b1.webp filter=lfs diff=lfs merge=lfs -text
44
+ assets/example_image/22a868bac8e62511fccd2bc82ed31ae77ed31ae2a8a149be7150957f11b30c9b.webp filter=lfs diff=lfs merge=lfs -text
45
+ assets/example_image/25d412fe36aab9f33913bc9f5e2fb1ff6458bdb286bf14397162c672c95d3697.webp filter=lfs diff=lfs merge=lfs -text
46
+ assets/example_image/26717a7dad644a5cf7554e8e6d06cf82d3dd9bbae31620b36cc7eb38b8de7ac9.webp filter=lfs diff=lfs merge=lfs -text
47
+ assets/example_image/290af2dd390c95db88a35b8062fdd2ac1a9c28edc6533bc6a26ab2c83c523c61.webp filter=lfs diff=lfs merge=lfs -text
48
+ assets/example_image/2bb0932314bae71eec94d0d01a20d3f761ade9664e013b9a9a43c00a2f44163a.webp filter=lfs diff=lfs merge=lfs -text
49
+ assets/example_image/3723615e3766742ae35b09517152a58c36d62b707bc60d7f76f8a6c922add2c0.webp filter=lfs diff=lfs merge=lfs -text
50
+ assets/example_image/454e7d8a30486c0635369936e7bec5677b78ae5f436d0e46af0d533738be859f.webp filter=lfs diff=lfs merge=lfs -text
51
+ assets/example_image/4bc7abe209c8673dd3766ee4fad14d40acbed02d118e7629f645c60fd77313f1.webp filter=lfs diff=lfs merge=lfs -text
52
+ assets/example_image/4dae7ef0224e9305533c4801ce8144d5b3a89d883ca5d35bdb0aebb860ff705f.webp filter=lfs diff=lfs merge=lfs -text
53
+ assets/example_image/50b70c5f88a5961d2c786158655d2fce5c3b214b2717956500a66a4e5b5fbe37.webp filter=lfs diff=lfs merge=lfs -text
54
+ assets/example_image/51b1b31d40476b123db70a51ae0b5f8b8d0db695b616bc2ec4e6324eb178fc14.webp filter=lfs diff=lfs merge=lfs -text
55
+ assets/example_image/52284bf45134c59a94be150a5b18b9cc3619ada4b30ded8d8d0288383b8c016f.webp filter=lfs diff=lfs merge=lfs -text
56
+ assets/example_image/5a020584b95cf3db3b6420e9b09fb93e7c0f4046e61076e5b4c65c63dc1f5837.webp filter=lfs diff=lfs merge=lfs -text
57
+ assets/example_image/5a6c81d3b2afca4323e4b8b379e2cf06d18371a57fc8c5dc24b57e60e3216690.webp filter=lfs diff=lfs merge=lfs -text
58
+ assets/example_image/5c80e5e03a3b60b6f03eaf555ba1dafc0e4230c472d7e8c8e2c5ca0a0dfcef10.webp filter=lfs diff=lfs merge=lfs -text
59
+ assets/example_image/61fea9d08e0bd9a067c9f696621dc89165afb5aab318d0701bc025d7863dabf0.webp filter=lfs diff=lfs merge=lfs -text
60
+ assets/example_image/65433d02fc56dae164719ec29cb9646c0383aa1d0e24f0bb592899f08428d68e.webp filter=lfs diff=lfs merge=lfs -text
61
+ assets/example_image/799ab13a23fe319a6876b8bf48007d0374d514f5e7aa31210e9b2cecfbace082.webp filter=lfs diff=lfs merge=lfs -text
62
+ assets/example_image/7baa867b4790b8596ee120f9b171b727fd9428c41980577a518505507c99d8a0.webp filter=lfs diff=lfs merge=lfs -text
63
+ assets/example_image/7bd0521d20ee4805d1462a0ffb7d9aacc15180c2b741c9ac42a0d82ad3d340cb.webp filter=lfs diff=lfs merge=lfs -text
64
+ assets/example_image/7d585a8475db078593486367d98b5efa9368a60a3528c555b96026a1a674aa54.webp filter=lfs diff=lfs merge=lfs -text
65
+ assets/example_image/7d6f4da4eafcc60243daf6ed210853df394a8bad7e701cadf551e21abcc77869.webp filter=lfs diff=lfs merge=lfs -text
66
+ assets/example_image/7d7659d5943e85a73a4ffe33c6dd48f5d79601e9bf11b103516f419ce9fbf713.webp filter=lfs diff=lfs merge=lfs -text
67
+ assets/example_image/80ad7988fc2ce62fc655b21a8950865566ec3f5a8b4398f2502db6414a3e6834.webp filter=lfs diff=lfs merge=lfs -text
68
+ assets/example_image/8aa698c59aab48d4ce69a558d9159107890e3d64e522af404d9635ad0be21f88.webp filter=lfs diff=lfs merge=lfs -text
69
+ assets/example_image/8ce83f6a28910e755902de10918672e77dd23476f43f0f1521c48667de6cea84.webp filter=lfs diff=lfs merge=lfs -text
70
+ assets/example_image/8e12cf0977c0476396e7112f04b73d4d73569421173fcb553213d45030bddec3.webp filter=lfs diff=lfs merge=lfs -text
71
+ assets/example_image/901d8de4c2011a8502a0decd0adec0fc7418f26165cd52ced64fd44f720353ef.webp filter=lfs diff=lfs merge=lfs -text
72
+ assets/example_image/95db3c13622788ec311ae4dfa24dd88732c66ca5e340a0bf3465d2a528204037.webp filter=lfs diff=lfs merge=lfs -text
73
+ assets/example_image/9c306c7bd0e857285f536fb500c0828e5fad4e23c3ceeab92c888c568fa19101.webp filter=lfs diff=lfs merge=lfs -text
74
+ assets/example_image/T.png filter=lfs diff=lfs merge=lfs -text
75
+ assets/example_image/a13d176cd7a7d457b42d1b32223bcff1a45dafbbb42c6a272b97d65ac2f2eb52.webp filter=lfs diff=lfs merge=lfs -text
76
+ assets/example_image/a306e2ee5cbc3da45e7db48d75a0cade0bb7eee263a74bc6820c617afaba1302.webp filter=lfs diff=lfs merge=lfs -text
77
+ assets/example_image/a3d0c28c7d9c6f23adb941c4def2523572c903a94469abcaa7dd1398d28af8f1.webp filter=lfs diff=lfs merge=lfs -text
78
+ assets/example_image/a63d2595e10229067b19cb167fe2bdc152dabfd8b62ae45fc1655a4cf66509bc.webp filter=lfs diff=lfs merge=lfs -text
79
+ assets/example_image/ab3bb3e183991253ae66c06d44dc6105f3c113a1a1f819ab57a93c6f60b0d32b.webp filter=lfs diff=lfs merge=lfs -text
80
+ assets/example_image/b205f4483c47bd1fec8e229163361e4fdff9f77923c5e968343b8f1dd76b61dc.webp filter=lfs diff=lfs merge=lfs -text
81
+ assets/example_image/b358d0eb96a68ac4ba1f2fb6d44ea2225f95fdfbf9cf4e0da08650c3704f1d23.webp filter=lfs diff=lfs merge=lfs -text
82
+ assets/example_image/bb3190891dd8341c9d6d3d4faa6525c6ecdac19945526904928f6bcd2f3f45f1.webp filter=lfs diff=lfs merge=lfs -text
83
+ assets/example_image/be7deb26f4fdd2080d4288668af4c39e526564282c579559ff8a4126ca4ed6c1.webp filter=lfs diff=lfs merge=lfs -text
84
+ assets/example_image/c2125d086c2529638841f38918ae1defbf33e6796d827253885b4c51e601034f.webp filter=lfs diff=lfs merge=lfs -text
85
+ assets/example_image/c3d714bc125f06ce1187799d5ca10736b4064a24c141e627089aad2bdedf7aa5.webp filter=lfs diff=lfs merge=lfs -text
86
+ assets/example_image/c9340e744541f310bf89838f652602961d3e5950b31cd349bcbfc7e59e15cd2e.webp filter=lfs diff=lfs merge=lfs -text
87
+ assets/example_image/cd3c309f17eee5ad6afe4e001765893ade20b653f611365c93d158286b4cee96.webp filter=lfs diff=lfs merge=lfs -text
88
+ assets/example_image/cdf996a6cc218918eeb90209891ce306a230e6d9cca2a3d9bbb37c6d7b6bd318.webp filter=lfs diff=lfs merge=lfs -text
89
+ assets/example_image/d39c2bd426456bd686de33f924524d18eb47343a5f080826aa3cb8e77de5147b.webp filter=lfs diff=lfs merge=lfs -text
90
+ assets/example_image/d64c94dffdadf82d46004d11412b5a3b2a17f1b4ddb428477a7ba38652adf973.webp filter=lfs diff=lfs merge=lfs -text
91
+ assets/example_image/e134444178eae855cfdefb9e5259d076df5e34f780ee44d4ad604483ff69cc74.webp filter=lfs diff=lfs merge=lfs -text
92
+ assets/example_image/e3c57169ce3d5ce10b3c10acef20b81ca774b54a17aabe74e8aca320c7b07b55.webp filter=lfs diff=lfs merge=lfs -text
93
+ assets/example_image/e4d6b2f3a18c3e0f5146a5b40cda6c95d7f69372b2e741c023e5ec9661deda2b.webp filter=lfs diff=lfs merge=lfs -text
94
+ assets/example_image/ebd09565cf0b6593aced573dffdfff34915aa359c60ec5dd0b30cd91a7f153c8.webp filter=lfs diff=lfs merge=lfs -text
95
+ assets/example_image/ee8ecf658fde9c58830c021b2e30d0d5e7e492ef52febe7192a6c74fbf1b0472.webp filter=lfs diff=lfs merge=lfs -text
96
+ assets/example_image/f351569ddc61116da4a7b929bccdab144d011f56b9603e6e72abea05236160f4.webp filter=lfs diff=lfs merge=lfs -text
97
+ assets/example_image/f5332118a0cda9cd13fe13d4be2b00437e702d1f9af51ebb6b75219a572a6ce9.webp filter=lfs diff=lfs merge=lfs -text
98
+ assets/example_image/f8920788b704531f7a7e875afd7c5c423d62e0a987e9495c63893c2cb4d2b5dc.webp filter=lfs diff=lfs merge=lfs -text
99
+ assets/example_image/f8a7eafe26a4f3ebd26a9e7d0289e4a40b5a93e9234e94ec3e1071c352acc65a.webp filter=lfs diff=lfs merge=lfs -text
100
+ assets/example_texturing/the_forgotten_knight.ply filter=lfs diff=lfs merge=lfs -text
101
+ assets/hdri/city.exr filter=lfs diff=lfs merge=lfs -text
102
+ assets/hdri/courtyard.exr filter=lfs diff=lfs merge=lfs -text
103
+ assets/hdri/forest.exr filter=lfs diff=lfs merge=lfs -text
104
+ assets/hdri/interior.exr filter=lfs diff=lfs merge=lfs -text
105
+ assets/hdri/night.exr filter=lfs diff=lfs merge=lfs -text
106
+ assets/hdri/sunrise.exr filter=lfs diff=lfs merge=lfs -text
107
+ assets/hdri/sunset.exr filter=lfs diff=lfs merge=lfs -text
108
+ assets/teaser.webp filter=lfs diff=lfs merge=lfs -text
109
+ assets/images/0_img.png filter=lfs diff=lfs merge=lfs -text
110
+ assets/images/7_img.png filter=lfs diff=lfs merge=lfs -text
111
+ assets/images/*.png filter=lfs diff=lfs merge=lfs -text
112
+ assets/images/*.jpg filter=lfs diff=lfs merge=lfs -text
113
+ assets/images/*.webp filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ venv/
5
+ *.ckpt
6
+ *.pt
7
+ *.bin
8
+ *.safetensors
9
+ wandb/
10
+ .wandb/
11
+ node_modules/
12
+ *.egg-info/
13
+ .gradio/
14
+ example.py
15
+
16
+ outputs*/
17
+ results*/
18
+ ckpts*/
19
+ tmp/example.py
20
+ tmp/
README.md CHANGED
@@ -1,15 +1,15 @@
1
  ---
2
- title: Pixal3D Server
3
- emoji: 👁
4
- colorFrom: blue
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
- license: apache-2.0
12
- short_description: Pixal3D Server
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Pixal3D
3
+ emoji: 🏆
4
+ colorFrom: indigo
5
+ colorTo: gray
6
  sdk: gradio
7
+ sdk_version: 6.13.0
8
+ python_version: "3.10"
9
  app_file: app.py
10
  pinned: false
11
+ license: mit
12
+ short_description: "High-fidelity pixel-aligned image-to-3D generation."
13
  ---
14
 
15
+
app.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pixal3D HF Space Proxy
3
+ ======================
4
+ This is a lightweight proxy app for HF Space that redirects users to a
5
+ locally deployed Gradio share link.
6
+
7
+ Setup:
8
+ 1. Deploy this as your HF Space app.py
9
+ 2. Set HF Space Secret: REMOTE_URL = your local share link (e.g. https://xxxxx.gradio.live)
10
+ 3. Users visiting the HF Space will be seamlessly redirected to your local instance.
11
+
12
+ To update the share link:
13
+ - Go to HF Space Settings -> Variables and secrets -> Update REMOTE_URL
14
+ """
15
+
16
+ import os
17
+ import gradio as gr
18
+
19
+ REMOTE_URL = os.environ.get("REMOTE_URL", "")
20
+ GPU_NAME = os.environ.get("GPU_NAME", "")
21
+
22
+ # Multi-instance support: REMOTE_URL as #0, REMOTE_URL_1, REMOTE_URL_2, REMOTE_URL_3
23
+ REMOTE_URLS = []
24
+ if REMOTE_URL:
25
+ name0 = os.environ.get("REMOTE_NAME", "Instance 0")
26
+ REMOTE_URLS.append({"url": REMOTE_URL, "name": name0})
27
+ for i in range(1, 4):
28
+ url = os.environ.get(f"REMOTE_URL_{i}", "")
29
+ name = os.environ.get(f"REMOTE_NAME_{i}", f"Instance {i}")
30
+ if url:
31
+ REMOTE_URLS.append({"url": url, "name": name})
32
+
33
+ PROXY_HTML = """
34
+ <!DOCTYPE html>
35
+ <html lang="en">
36
+ <head>
37
+ <meta charset="UTF-8">
38
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
39
+ <title>Pixal3D | AI Image-to-3D</title>
40
+ <style>
41
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
42
+ html, body {{
43
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44
+ background: #0b0f1a;
45
+ color: #f1f5f9;
46
+ height: 100%;
47
+ overflow: hidden;
48
+ display: flex;
49
+ flex-direction: column;
50
+ }}
51
+ .header {{
52
+ padding: 8px 24px;
53
+ background: rgba(22, 28, 45, 0.9);
54
+ border-bottom: 1px solid rgba(255,255,255,0.08);
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 16px;
58
+ backdrop-filter: blur(12px);
59
+ }}
60
+ .header h1 {{
61
+ font-size: 16px;
62
+ font-weight: 700;
63
+ background: linear-gradient(135deg, #818cf8, #10b981);
64
+ -webkit-background-clip: text;
65
+ -webkit-text-fill-color: transparent;
66
+ white-space: nowrap;
67
+ }}
68
+ .header .notice {{
69
+ flex: 1;
70
+ font-size: 12px;
71
+ color: #fbbf24;
72
+ text-align: center;
73
+ }}
74
+ .status {{
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 6px;
78
+ font-size: 12px;
79
+ color: #94a3b8;
80
+ white-space: nowrap;
81
+ }}
82
+ .status-dot {{
83
+ width: 7px;
84
+ height: 7px;
85
+ border-radius: 50%;
86
+ background: {status_color};
87
+ animation: {status_anim};
88
+ }}
89
+ @keyframes pulse {{
90
+ 0%, 100% {{ opacity: 1; }}
91
+ 50% {{ opacity: 0.4; }}
92
+ }}
93
+ .iframe-container {{
94
+ flex: 1;
95
+ position: relative;
96
+ }}
97
+ .iframe-container iframe {{
98
+ width: 100%;
99
+ height: 100%;
100
+ border: none;
101
+ position: absolute;
102
+ top: 0;
103
+ left: 0;
104
+ }}
105
+ .no-url {{
106
+ flex: 1;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ padding: 40px;
111
+ }}
112
+ .no-url-card {{
113
+ max-width: 560px;
114
+ background: rgba(22, 28, 45, 0.8);
115
+ border: 1px solid rgba(255,255,255,0.08);
116
+ border-radius: 16px;
117
+ padding: 48px;
118
+ text-align: center;
119
+ }}
120
+ .no-url-card h2 {{
121
+ font-size: 24px;
122
+ margin-bottom: 16px;
123
+ }}
124
+ .no-url-card p {{
125
+ color: #94a3b8;
126
+ line-height: 1.7;
127
+ margin-bottom: 12px;
128
+ }}
129
+ .no-url-card code {{
130
+ background: rgba(129, 140, 248, 0.15);
131
+ color: #818cf8;
132
+ padding: 2px 8px;
133
+ border-radius: 4px;
134
+ font-size: 13px;
135
+ }}
136
+ .cards-container {{
137
+ flex: 1;
138
+ display: flex;
139
+ flex-direction: column;
140
+ align-items: center;
141
+ justify-content: center;
142
+ padding: 40px;
143
+ overflow-y: auto;
144
+ }}
145
+ .cards-grid {{
146
+ display: grid;
147
+ grid-template-columns: repeat(2, 1fr);
148
+ gap: 28px;
149
+ max-width: 1000px;
150
+ width: 100%;
151
+ }}
152
+ .instance-card {{
153
+ width: 100%;
154
+ background: rgba(22, 28, 45, 0.8);
155
+ border: 1px solid rgba(255,255,255,0.08);
156
+ border-radius: 24px;
157
+ padding: 60px 48px;
158
+ text-align: center;
159
+ transition: transform 0.2s, border-color 0.2s;
160
+ }}
161
+ .instance-card:hover {{
162
+ transform: translateY(-4px);
163
+ border-color: rgba(129, 140, 248, 0.4);
164
+ }}
165
+ .instance-card h3 {{
166
+ font-size: 26px;
167
+ margin-bottom: 16px;
168
+ color: #f1f5f9;
169
+ }}
170
+ .queue-status {{
171
+ display: inline-flex;
172
+ align-items: center;
173
+ gap: 8px;
174
+ padding: 8px 16px;
175
+ border-radius: 20px;
176
+ font-size: 15px;
177
+ font-weight: 600;
178
+ margin-bottom: 8px;
179
+ background: rgba(148, 163, 184, 0.1);
180
+ color: #94a3b8;
181
+ }}
182
+ .queue-status.idle {{
183
+ background: rgba(16, 185, 129, 0.15);
184
+ color: #10b981;
185
+ }}
186
+ .queue-status.busy {{
187
+ background: rgba(251, 146, 60, 0.15);
188
+ color: #fb923c;
189
+ }}
190
+ .queue-status.offline {{
191
+ background: rgba(239, 68, 68, 0.15);
192
+ color: #ef4444;
193
+ }}
194
+ .queue-dot {{
195
+ width: 8px;
196
+ height: 8px;
197
+ border-radius: 50%;
198
+ background: currentColor;
199
+ animation: pulse 2s infinite;
200
+ }}
201
+ .instance-card .url-hint {{
202
+ font-size: 13px;
203
+ color: #64748b;
204
+ margin-top: 18px;
205
+ word-break: break-all;
206
+ }}
207
+ .instance-card .btn-go {{
208
+ display: inline-block;
209
+ margin-top: 24px;
210
+ padding: 16px 44px;
211
+ background: linear-gradient(135deg, #818cf8, #10b981);
212
+ color: #ffffff !important;
213
+ border-radius: 12px;
214
+ text-decoration: none !important;
215
+ font-weight: 700;
216
+ font-size: 18px;
217
+ transition: opacity 0.2s;
218
+ }}
219
+ .instance-card .btn-go:hover {{
220
+ opacity: 0.85;
221
+ text-decoration: none !important;
222
+ }}
223
+ .instance-card .btn-go:hover {{
224
+ opacity: 0.85;
225
+ }}
226
+ .link-bar {{
227
+ padding: 8px 24px;
228
+ background: rgba(16, 185, 129, 0.08);
229
+ border-top: 1px solid rgba(16, 185, 129, 0.2);
230
+ font-size: 12px;
231
+ color: #94a3b8;
232
+ text-align: center;
233
+ }}
234
+ .link-bar a {{
235
+ color: #10b981;
236
+ text-decoration: none;
237
+ }}
238
+ .link-bar a:hover {{ text-decoration: underline; }}
239
+ </style>
240
+ </head>
241
+ <body>
242
+ <div class="header">
243
+ <h1>Pixal3D</h1>
244
+ <span class="notice"></span>
245
+ <div class="status">
246
+ <div class="status-dot"></div>
247
+ <span>{status_text}</span>
248
+ </div>
249
+ </div>
250
+ {content}
251
+ </body>
252
+ </html>
253
+ """
254
+
255
+
256
+ def build_page():
257
+ # If multi-instance URLs are configured, show cards
258
+ if REMOTE_URLS:
259
+ status_color = "#10b981"
260
+ status_anim = "pulse 2s infinite"
261
+ status_text = f"{len(REMOTE_URLS)} instance(s) available"
262
+
263
+ cards_html = ""
264
+ for i, inst in enumerate(REMOTE_URLS):
265
+ cards_html += f"""
266
+ <div class="instance-card">
267
+ <h3>🖥️ {inst['name']}</h3>
268
+ <p style="color:#94a3b8; font-size:14px; margin-bottom:8px;">⚡ Shared GPU — requests are queued</p>
269
+ <div class="queue-status" id="queue-status-{i}">
270
+ <span class="queue-dot"></span>
271
+ <span id="queue-text-{i}">Checking...</span>
272
+ </div>
273
+ <a href="{inst['url']}" target="_blank" rel="noopener noreferrer" class="btn-go">
274
+ Open Instance {i}
275
+ </a>
276
+ <p class="url-hint"><code>{inst['url']}</code></p>
277
+ </div>
278
+ """
279
+
280
+ # Build JS array of instance URLs for direct polling (Gradio share links support CORS natively)
281
+ urls_js = ", ".join(['"' + inst["url"].rstrip("/") + '"' for inst in REMOTE_URLS])
282
+
283
+ content = f"""
284
+ <div class="cards-container">
285
+ <div style="width:100%; text-align:center; margin-bottom:16px;">
286
+ <h2 style="font-size:28px; margin-bottom:12px;">🚀 Choose a Pixal3D Instance</h2>
287
+ <p style="color:#fbbf24; font-size:15px; margin-bottom:8px;">⚠️ Due to a temporary HuggingFace error, this Space is currently unavailable. Please use one of the instances below.</p>
288
+ <p style="color:#10b981; font-size:14px; margin-top:10px; font-weight:600;">💡 Choose the instance with the shortest queue!</p>
289
+ </div>
290
+ <div class="cards-grid">
291
+ {cards_html}
292
+ </div>
293
+ </div>
294
+ """
295
+
296
+ poll_script = f"""
297
+ const INSTANCE_URLS = [{urls_js}];
298
+ async function pollQueues() {{
299
+ for (let i = 0; i < INSTANCE_URLS.length; i++) {{
300
+ try {{
301
+ const controller = new AbortController();
302
+ const timeout = setTimeout(() => controller.abort(), 5000);
303
+ const resp = await fetch(INSTANCE_URLS[i] + '/queue?session_id=', {{
304
+ signal: controller.signal
305
+ }});
306
+ clearTimeout(timeout);
307
+ if (resp.ok) {{
308
+ const data = await resp.json();
309
+ const total = data.total_waiting + (data.gpu_busy ? 1 : 0);
310
+ const el = document.getElementById('queue-text-' + i);
311
+ const status = document.getElementById('queue-status-' + i);
312
+ if (total === 0) {{
313
+ el.textContent = 'Idle — no queue';
314
+ status.className = 'queue-status idle';
315
+ }} else {{
316
+ el.textContent = total + ' in queue';
317
+ status.className = 'queue-status busy';
318
+ }}
319
+ }} else {{
320
+ const el = document.getElementById('queue-text-' + i);
321
+ const status = document.getElementById('queue-status-' + i);
322
+ if (el) {{
323
+ el.textContent = 'Offline';
324
+ status.className = 'queue-status offline';
325
+ }}
326
+ }}
327
+ }} catch (e) {{
328
+ const el = document.getElementById('queue-text-' + i);
329
+ const status = document.getElementById('queue-status-' + i);
330
+ if (el) {{
331
+ el.textContent = 'Offline';
332
+ status.className = 'queue-status offline';
333
+ }}
334
+ }}
335
+ }}
336
+ }}
337
+ pollQueues();
338
+ setInterval(pollQueues, 5000);
339
+ """
340
+ else:
341
+ status_color = "#ef4444"
342
+ status_anim = "pulse 1.5s infinite"
343
+ status_text = "Remote instance not configured"
344
+ poll_script = ""
345
+ content = """
346
+ <div class="no-url">
347
+ <div class="no-url-card">
348
+ <h2>⚡ Remote GPU Instance Not Connected</h2>
349
+ <p>This Space acts as a proxy to a locally-deployed Pixal3D instance running on a dedicated GPU.</p>
350
+ <p>To connect, set the <code>REMOTE_URL</code> secret in this Space's settings to your Gradio share link.</p>
351
+ <p style="margin-top:24px; font-size:13px;">
352
+ Example: <code>https://abcdef123456.gradio.live</code>
353
+ </p>
354
+ </div>
355
+ </div>
356
+ """
357
+
358
+ html = PROXY_HTML.format(
359
+ status_color=status_color,
360
+ status_anim=status_anim,
361
+ status_text=status_text,
362
+ gpu_name=GPU_NAME,
363
+ content=content,
364
+ )
365
+ return html, poll_script
366
+
367
+
368
+ # Use a simple Gradio Blocks app with HTML component
369
+ page_html, page_script = build_page()
370
+
371
+ with gr.Blocks(
372
+ title="Pixal3D | AI Image-to-3D",
373
+ css="footer {display:none !important;} .gradio-container {padding:0 !important; max-width:100% !important; height:100vh !important; overflow:hidden !important;} #proxy-frame {height:100%; max-height:100vh; padding:0; overflow:hidden;}",
374
+ theme=gr.themes.Base(),
375
+ ) as demo:
376
+ gr.HTML(page_html, elem_id="proxy-frame", js_on_load=page_script if page_script else None)
377
+
378
+ if __name__ == "__main__":
379
+ demo.launch(share=True)
app_bak.py ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import argparse
4
+ import math
5
+ import time
6
+ import shutil
7
+ import cv2
8
+ import torch
9
+ import numpy as np
10
+ import base64
11
+ import io
12
+ import json
13
+ from datetime import datetime
14
+ from typing import *
15
+ from PIL import Image
16
+
17
+ import threading
18
+ try:
19
+ import nest_asyncio
20
+ nest_asyncio.apply()
21
+ except ImportError:
22
+ pass
23
+
24
+ # Lock for model initialization
25
+ init_lock = threading.Lock()
26
+
27
+ os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '1'
28
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
29
+ os.environ["ATTN_BACKEND"] = "flash_attn_3"
30
+ os.environ["FLEX_GEMM_AUTOTUNE_CACHE_PATH"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'autotune_cache.json')
31
+ os.environ["FLEX_GEMM_AUTOTUNER_VERBOSE"] = '1'
32
+
33
+ import spaces
34
+ from gradio import Server
35
+ from gradio.data_classes import FileData
36
+ from fastapi.responses import HTMLResponse
37
+ from fastapi.staticfiles import StaticFiles
38
+
39
+ from trellis2.modules.sparse import SparseTensor
40
+ from trellis2.pipelines import Pixal3DImageTo3DPipeline
41
+ from trellis2.renderers import EnvMap
42
+ from trellis2.utils import render_utils
43
+ import o_voxel
44
+
45
+ # ============================================================================
46
+ # Constants & Defaults
47
+ # ============================================================================
48
+
49
+ MAX_SEED = np.iinfo(np.int32).max
50
+ TMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tmp')
51
+ os.makedirs(TMP_DIR, exist_ok=True)
52
+
53
+ MODES = [
54
+ {"name": "Normal", "icon": "assets/app/normal.png", "render_key": "normal"},
55
+ {"name": "Clay render", "icon": "assets/app/clay.png", "render_key": "clay"},
56
+ {"name": "Base color", "icon": "assets/app/basecolor.png", "render_key": "base_color"},
57
+ {"name": "HDRI forest", "icon": "assets/app/hdri_forest.png", "render_key": "shaded_forest"},
58
+ {"name": "HDRI sunset", "icon": "assets/app/hdri_sunset.png", "render_key": "shaded_sunset"},
59
+ {"name": "HDRI courtyard", "icon": "assets/app/hdri_courtyard.png", "render_key": "shaded_courtyard"},
60
+ ]
61
+ STEPS = 8
62
+
63
+ # Cascade parameters
64
+ CASCADE_LR_RESOLUTION = 512
65
+ CASCADE_MAX_NUM_TOKENS = 49152
66
+
67
+ # MoGe defaults
68
+ MOGE_MODEL_NAME = "Ruicheng/moge-2-vitl"
69
+ WILD_MESH_SCALE = 1.0
70
+ WILD_EXTEND_PIXEL = 0
71
+ WILD_IMAGE_RESOLUTION = 512
72
+
73
+ # Image Cond Model configs
74
+ IMAGE_COND_CONFIGS = {
75
+ "ss": {
76
+ "model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
77
+ "image_size": 512,
78
+ "grid_resolution": 16,
79
+ },
80
+ "shape_512": {
81
+ "model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
82
+ "image_size": 512,
83
+ "grid_resolution": 32,
84
+ "use_naf_upsample": True,
85
+ "naf_target_size": 512,
86
+ },
87
+ "shape_1024": {
88
+ "model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
89
+ "image_size": 1024,
90
+ "grid_resolution": 64,
91
+ "use_naf_upsample": True,
92
+ "naf_target_size": 512,
93
+ },
94
+ "tex_1024": {
95
+ "model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
96
+ "image_size": 1024,
97
+ "grid_resolution": 64,
98
+ "use_naf_upsample": True,
99
+ "naf_target_size": 1024,
100
+ },
101
+ }
102
+
103
+ # ============================================================================
104
+ # Model Loading
105
+ # ============================================================================
106
+
107
+ def build_image_cond_model(config: dict):
108
+ from trellis2.trainers.flow_matching.mixins.image_conditioned_proj import DinoV3ProjFeatureExtractor
109
+ model = DinoV3ProjFeatureExtractor(**config)
110
+ model.eval()
111
+ return model
112
+
113
+ def load_moge_model(device="cuda", model_name=MOGE_MODEL_NAME):
114
+ from moge.model.v2 import MoGeModel
115
+ moge_model = MoGeModel.from_pretrained(model_name).to(device)
116
+ moge_model.eval()
117
+ return moge_model
118
+
119
+ # Global instances (lazy loaded or loaded at start)
120
+ pipeline = None
121
+ moge_model = None
122
+ envmap = None
123
+
124
+ def init_models():
125
+ global pipeline, moge_model, envmap
126
+ with init_lock:
127
+ if pipeline is not None:
128
+ return
129
+
130
+ # GPU / CUDA Diagnostics (runs when GPU is allocated)
131
+ import subprocess as _sp
132
+ print("=" * 60)
133
+ print("[Diagnostics] PyTorch version:", torch.__version__)
134
+ print("[Diagnostics] CUDA available:", torch.cuda.is_available())
135
+ if torch.cuda.is_available():
136
+ print("[Diagnostics] CUDA version:", torch.version.cuda)
137
+ print("[Diagnostics] cuDNN version:", torch.backends.cudnn.version())
138
+ for i in range(torch.cuda.device_count()):
139
+ name = torch.cuda.get_device_name(i)
140
+ cap = torch.cuda.get_device_capability(i)
141
+ mem = torch.cuda.get_device_properties(i).total_memory / 1024**3
142
+ print(f"[Diagnostics] GPU {i}: {name}, sm_{cap[0]}{cap[1]}, {mem:.1f} GB")
143
+ try:
144
+ res = _sp.run(["nvidia-smi", "--query-gpu=name,compute_cap,memory.total", "--format=csv,noheader"], capture_output=True, text=True, timeout=10)
145
+ print("[Diagnostics] nvidia-smi:", res.stdout.strip())
146
+ except Exception as e:
147
+ print(f"[Diagnostics] nvidia-smi failed: {e}")
148
+ print("=" * 60)
149
+
150
+ model_path = "TencentARC/Pixal3D-T"
151
+ print(f"[Pipeline] Loading from {model_path}...")
152
+ pipeline = Pixal3DImageTo3DPipeline.from_pretrained(model_path)
153
+
154
+ print("[ImageCond] Building DinoV3ProjFeatureExtractor models...")
155
+ pipeline.image_cond_model_ss = build_image_cond_model(IMAGE_COND_CONFIGS["ss"])
156
+ pipeline.image_cond_model_shape_512 = build_image_cond_model(IMAGE_COND_CONFIGS["shape_512"])
157
+ pipeline.image_cond_model_shape_1024 = build_image_cond_model(IMAGE_COND_CONFIGS["shape_1024"])
158
+ pipeline.image_cond_model_tex_1024 = build_image_cond_model(IMAGE_COND_CONFIGS["tex_1024"])
159
+
160
+ pipeline.low_vram = False
161
+ pipeline.cuda()
162
+
163
+ # Ensure image_cond_models are on GPU
164
+ pipeline.image_cond_model_ss.cuda()
165
+ pipeline.image_cond_model_shape_512.cuda()
166
+ pipeline.image_cond_model_shape_1024.cuda()
167
+ pipeline.image_cond_model_tex_1024.cuda()
168
+
169
+ print("[NAF] Pre-loading NAF upsampler model...")
170
+ for attr in ['image_cond_model_ss', 'image_cond_model_shape_512', 'image_cond_model_shape_1024', 'image_cond_model_tex_1024']:
171
+ model = getattr(pipeline, attr, None)
172
+ if model is not None and getattr(model, 'use_naf_upsample', False):
173
+ model._load_naf()
174
+
175
+ print("[MoGe-2] Loading model for camera estimation...")
176
+ moge_model = load_moge_model(device="cuda")
177
+
178
+ print("[EnvMap] Loading environment maps...")
179
+ _base = os.path.dirname(os.path.abspath(__file__))
180
+ envmap = {
181
+ 'forest': EnvMap(torch.tensor(cv2.cvtColor(cv2.imread(os.path.join(_base, 'assets/hdri/forest.exr'), cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB), dtype=torch.float32, device='cuda')),
182
+ 'sunset': EnvMap(torch.tensor(cv2.cvtColor(cv2.imread(os.path.join(_base, 'assets/hdri/sunset.exr'), cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB), dtype=torch.float32, device='cuda')),
183
+ 'courtyard': EnvMap(torch.tensor(cv2.cvtColor(cv2.imread(os.path.join(_base, 'assets/hdri/courtyard.exr'), cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB), dtype=torch.float32, device='cuda')),
184
+ }
185
+
186
+ # ============================================================================
187
+ # Utilities
188
+ # ============================================================================
189
+
190
+ def compute_f_pixels(camera_angle_x: float, resolution: int) -> float:
191
+ focal_length = 16.0 / torch.tan(torch.tensor(camera_angle_x / 2.0))
192
+ f_pixels = focal_length * resolution / 32.0
193
+ return float(f_pixels.item())
194
+
195
+ def distance_from_fov(camera_angle_x, grid_point, target_point, mesh_scale, image_resolution):
196
+ rotation_matrix = torch.tensor([[1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]])
197
+ gp = grid_point.to(torch.float32) @ rotation_matrix.T
198
+ gp = gp / mesh_scale / 2
199
+ xw, yw, zw = gp[0].item(), gp[1].item(), gp[2].item()
200
+ xt, yt = float(target_point[0].item()), float(target_point[1].item())
201
+ f_pixels = compute_f_pixels(camera_angle_x, image_resolution)
202
+ x_ndc = xt - image_resolution / 2.0
203
+ y_ndc = -(yt - image_resolution / 2.0)
204
+ distance_x = f_pixels * xw / x_ndc - yw
205
+ return {"distance_from_x": float(distance_x), "f_pixels": float(f_pixels)}
206
+
207
+ def get_camera_params_wild_moge(image_path, device="cuda", mesh_scale=1.0, extend_pixel=0, image_resolution=512):
208
+ pil_image = Image.open(image_path).convert("RGB")
209
+ width, height = pil_image.size
210
+ image_np = np.array(pil_image).astype(np.float32) / 255.0
211
+ image_tensor = torch.from_numpy(image_np).permute(2, 0, 1).to(device)
212
+ with torch.no_grad():
213
+ output = moge_model.infer(image_tensor)
214
+ intrinsics = output["intrinsics"].squeeze().cpu().numpy()
215
+ fx_normalized = intrinsics[0, 0]
216
+ fx = fx_normalized * width
217
+ camera_angle_x = 2 * math.atan(width / (2 * fx))
218
+
219
+ grid_point = torch.tensor([-1.0, 0.0, 0.0])
220
+ distance = distance_from_fov(
221
+ camera_angle_x, grid_point,
222
+ torch.tensor([0 - extend_pixel, image_resolution - 1 + extend_pixel]),
223
+ mesh_scale, image_resolution
224
+ )["distance_from_x"]
225
+ return {'camera_angle_x': camera_angle_x, 'distance': distance, 'mesh_scale': mesh_scale}
226
+
227
+ def pack_state(shape_slat, tex_slat, res):
228
+ state_data = {
229
+ 'shape_slat_feats': shape_slat.feats.cpu().numpy(),
230
+ 'tex_slat_feats': tex_slat.feats.cpu().numpy(),
231
+ 'coords': shape_slat.coords.cpu().numpy(),
232
+ 'res': res,
233
+ }
234
+ import random
235
+ state_path = os.path.join(TMP_DIR, f"state_{int(time.time()*1000)}_{random.randint(0,9999):04d}.npz")
236
+ np.savez_compressed(state_path, **state_data)
237
+ return state_path
238
+
239
+ def unpack_state(state_path):
240
+ data = np.load(state_path)
241
+ shape_slat = SparseTensor(
242
+ feats=torch.from_numpy(data['shape_slat_feats']).cuda(),
243
+ coords=torch.from_numpy(data['coords']).cuda(),
244
+ )
245
+ tex_slat = shape_slat.replace(torch.from_numpy(data['tex_slat_feats']).cuda())
246
+ return shape_slat, tex_slat, int(data['res'])
247
+
248
+ # ============================================================================
249
+ # Progress Tracking (file-based, cross-process safe for @spaces.GPU)
250
+ # ============================================================================
251
+
252
+ import asyncio
253
+ from fastapi.responses import JSONResponse
254
+ from fastapi import Request
255
+
256
+ PROGRESS_DIR = os.path.join(TMP_DIR, '_progress')
257
+ os.makedirs(PROGRESS_DIR, exist_ok=True)
258
+
259
+ _thread_local = threading.local()
260
+
261
+ def _progress_file(session_id: str) -> str:
262
+ """Return path to a session's progress JSON file."""
263
+ return os.path.join(PROGRESS_DIR, f"{session_id}.json")
264
+
265
+ def _reset_progress(session_id: str):
266
+ _thread_local.active_session = session_id
267
+ _write_progress_file(session_id, {"stage": "Initializing...", "step": 0, "total": 0, "done": False})
268
+
269
+ def _update_progress(stage: str, step: int, total: int):
270
+ session_id = getattr(_thread_local, 'active_session', '')
271
+ if session_id:
272
+ _write_progress_file(session_id, {"stage": stage, "step": step, "total": total, "done": False})
273
+
274
+ def _finish_progress():
275
+ session_id = getattr(_thread_local, 'active_session', '')
276
+ if session_id:
277
+ _write_progress_file(session_id, {"done": True})
278
+
279
+ def _write_progress_file(session_id: str, data: dict):
280
+ """Atomically write progress JSON to a file (cross-process safe)."""
281
+ path = _progress_file(session_id)
282
+ tmp_path = path + ".tmp"
283
+ try:
284
+ with open(tmp_path, 'w') as f:
285
+ json.dump(data, f)
286
+ os.replace(tmp_path, path) # atomic on POSIX
287
+ except Exception:
288
+ pass
289
+
290
+ # Monkey-patch tqdm to intercept progress
291
+ import tqdm as _tqdm_module
292
+
293
+ _original_tqdm = _tqdm_module.tqdm
294
+
295
+ class _TqdmProgressInterceptor(_original_tqdm):
296
+ """Wraps tqdm to push progress updates to SSE."""
297
+ def __init__(self, *args, **kwargs):
298
+ self._stage_desc = kwargs.get('desc', 'Processing')
299
+ super().__init__(*args, **kwargs)
300
+
301
+ def set_description(self, desc=None, refresh=True):
302
+ self._stage_desc = desc or 'Processing'
303
+ super().set_description(desc, refresh)
304
+
305
+ def update(self, n=1):
306
+ super().update(n)
307
+ _update_progress(self._stage_desc, self.n, self.total or 0)
308
+
309
+ # Patch tqdm globally
310
+ _tqdm_module.tqdm = _TqdmProgressInterceptor
311
+ # Also patch the direct import in the sampler module and render_utils
312
+ import trellis2.pipelines.samplers.flow_euler as _fe_module
313
+ _fe_module.tqdm = _TqdmProgressInterceptor
314
+ import trellis2.utils.render_utils as _ru_module
315
+ _ru_module.tqdm = _TqdmProgressInterceptor
316
+ import o_voxel.postprocess as _ovp_module
317
+ _ovp_module.tqdm = _TqdmProgressInterceptor
318
+
319
+ # ============================================================================
320
+ # API Implementation
321
+ # ============================================================================
322
+
323
+ app = Server()
324
+
325
+ @app.get("/")
326
+ async def homepage():
327
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
328
+ with open(html_path, "r", encoding="utf-8") as f:
329
+ return HTMLResponse(content=f.read())
330
+
331
+ @app.get("/progress")
332
+ async def progress_poll(request: Request):
333
+ """Polling endpoint for real-time progress updates during generation."""
334
+ session_id = request.query_params.get("session_id", "")
335
+ path = _progress_file(session_id)
336
+ try:
337
+ with open(path, 'r') as f:
338
+ data = json.load(f)
339
+ return JSONResponse(data)
340
+ except (FileNotFoundError, json.JSONDecodeError):
341
+ return JSONResponse({"stage": "Waiting...", "step": 0, "total": 0, "done": False})
342
+
343
+ @app.api()
344
+ @spaces.GPU(duration=30)
345
+ def preprocess(image: FileData) -> FileData:
346
+ init_models()
347
+ img = Image.open(image["path"])
348
+ processed = pipeline.preprocess_image(img)
349
+ out_path = os.path.join(TMP_DIR, f"preprocessed_{int(time.time()*1000)}.png")
350
+ processed.save(out_path)
351
+ return FileData(path=out_path)
352
+
353
+ @app.api()
354
+ @spaces.GPU(duration=120)
355
+ def generate_3d(
356
+ image: FileData,
357
+ seed: int,
358
+ resolution: int,
359
+ ss_guidance_strength: float = 7.5,
360
+ ss_guidance_rescale: float = 0.7,
361
+ ss_sampling_steps: int = 12,
362
+ ss_rescale_t: float = 5.0,
363
+ shape_slat_guidance_strength: float = 7.5,
364
+ shape_slat_guidance_rescale: float = 0.5,
365
+ shape_slat_sampling_steps: int = 12,
366
+ shape_slat_rescale_t: float = 3.0,
367
+ tex_slat_guidance_strength: float = 1.0,
368
+ tex_slat_guidance_rescale: float = 0.0,
369
+ tex_slat_sampling_steps: int = 12,
370
+ tex_slat_rescale_t: float = 3.0,
371
+ session_id: str = "",
372
+ ) -> Dict:
373
+ init_models()
374
+ _reset_progress(session_id)
375
+ _update_progress("Preprocessing & Camera Estimation", 0, 1)
376
+
377
+ torch.manual_seed(seed)
378
+ hr_resolution = int(resolution)
379
+
380
+ img = Image.open(image["path"])
381
+ # Image is already preprocessed by /preprocess endpoint, use directly
382
+ image_preprocessed = img
383
+ temp_processed_path = os.path.join(TMP_DIR, f"temp_proc_{session_id[:8]}_{int(time.time()*1000)}.png")
384
+ image_preprocessed.save(temp_processed_path)
385
+
386
+ camera_params = get_camera_params_wild_moge(
387
+ temp_processed_path, device="cuda",
388
+ mesh_scale=WILD_MESH_SCALE, extend_pixel=WILD_EXTEND_PIXEL,
389
+ image_resolution=WILD_IMAGE_RESOLUTION,
390
+ )
391
+ _update_progress("Preprocessing & Camera Estimation", 1, 1)
392
+
393
+ ss_sampler_override = {"steps": ss_sampling_steps, "guidance_strength": ss_guidance_strength,
394
+ "guidance_rescale": ss_guidance_rescale, "rescale_t": ss_rescale_t}
395
+ shape_sampler_override = {"steps": shape_slat_sampling_steps, "guidance_strength": shape_slat_guidance_strength,
396
+ "guidance_rescale": shape_slat_guidance_rescale, "rescale_t": shape_slat_rescale_t}
397
+ tex_sampler_override = {"steps": tex_slat_sampling_steps, "guidance_strength": tex_slat_guidance_strength,
398
+ "guidance_rescale": tex_slat_guidance_rescale, "rescale_t": tex_slat_rescale_t}
399
+
400
+ pipeline_type = f"{hr_resolution}_cascade"
401
+ mesh_list, (shape_slat, tex_slat, res) = pipeline.run(
402
+ image_preprocessed,
403
+ camera_params=camera_params,
404
+ seed=seed,
405
+ sparse_structure_sampler_params=ss_sampler_override,
406
+ shape_slat_sampler_params=shape_sampler_override,
407
+ tex_slat_sampler_params=tex_sampler_override,
408
+ preprocess_image=False,
409
+ return_latent=True,
410
+ pipeline_type=pipeline_type,
411
+ max_num_tokens=CASCADE_MAX_NUM_TOKENS,
412
+ )
413
+
414
+ mesh = mesh_list[0]
415
+ state_path = pack_state(shape_slat, tex_slat, res)
416
+
417
+ _update_progress("Rendering views", 0, 1)
418
+ mesh.simplify(16777216)
419
+ cam_dist = camera_params['distance']
420
+ near = max(0.01, cam_dist - 2.0)
421
+ far = cam_dist + 10.0
422
+ renders = render_utils.render_proj_aligned_video(
423
+ mesh, camera_angle_x=camera_params['camera_angle_x'],
424
+ distance=cam_dist, resolution=1024,
425
+ num_frames=STEPS, envmap=envmap,
426
+ near=near, far=far,
427
+ )
428
+ _update_progress("Rendering views", 1, 1)
429
+
430
+ # Save renders and return paths
431
+ render_files = {}
432
+ for mode_key, frames in renders.items():
433
+ mode_files = []
434
+ for i, frame in enumerate(frames):
435
+ p = os.path.abspath(os.path.join(TMP_DIR, f"render_{mode_key}_{i}_{int(time.time()*1000)}.jpg"))
436
+ Image.fromarray(frame).save(p, quality=85)
437
+ mode_files.append(FileData(path=p))
438
+ render_files[mode_key] = mode_files
439
+
440
+ _finish_progress()
441
+ return {
442
+ "render_paths": render_files,
443
+ "state_path": os.path.abspath(state_path),
444
+ "camera_angle_x": camera_params['camera_angle_x'],
445
+ "distance": camera_params['distance'],
446
+ }
447
+
448
+ @app.api()
449
+ @spaces.GPU(duration=240)
450
+ def extract_glb_api(state_path: str, decimation_target: int, texture_size: int, session_id: str = "") -> FileData:
451
+ init_models()
452
+ _reset_progress(session_id)
453
+ _update_progress("Decoding latent", 0, 1)
454
+
455
+ shape_slat, tex_slat, res = unpack_state(state_path)
456
+ mesh = pipeline.decode_latent(shape_slat, tex_slat, res)[0]
457
+ _update_progress("Decoding latent", 1, 1)
458
+
459
+ glb = o_voxel.postprocess.to_glb(
460
+ vertices=mesh.vertices, faces=mesh.faces, attr_volume=mesh.attrs,
461
+ coords=mesh.coords, attr_layout=pipeline.pbr_attr_layout,
462
+ grid_size=res, aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
463
+ decimation_target=decimation_target, texture_size=texture_size,
464
+ remesh=True, remesh_band=1, remesh_project=0, use_tqdm=True,
465
+ )
466
+ rot = np.array([
467
+ [-1, 0, 0, 0],
468
+ [ 0, 0, -1, 0],
469
+ [ 0, -1, 0, 0],
470
+ [ 0, 0, 0, 1],
471
+ ], dtype=np.float64)
472
+ glb.apply_transform(rot)
473
+
474
+ out_glb = os.path.join(TMP_DIR, f"result_{int(time.time()*1000)}.glb")
475
+ glb.export(out_glb, extension_webp=True)
476
+ _finish_progress()
477
+ return FileData(path=out_glb)
478
+
479
+ # Mount assets and tmp for direct access
480
+ app.mount("/assets", StaticFiles(directory="assets"), name="assets")
481
+ app.mount("/tmp", StaticFiles(directory=TMP_DIR), name="tmp")
482
+
483
+ if __name__ == "__main__":
484
+ # Re-install utils3d as in original app.py
485
+ subprocess.run([
486
+ "pip", "install", "--force-reinstall", "--no-deps",
487
+ "https://github.com/LDYang694/Storages/releases/download/20260430/utils3d-0.0.2-py3-none-any.whl"
488
+ ], check=True)
489
+
490
+ # Pre-initialize models before launching the server
491
+ init_models()
492
+
493
+ app.launch(show_error=True, share=True)
app_local.py ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import argparse
4
+ import math
5
+ import time
6
+ import shutil
7
+ import cv2
8
+ import torch
9
+ import numpy as np
10
+ import base64
11
+ import io
12
+ import json
13
+ from datetime import datetime
14
+ from typing import *
15
+ from PIL import Image
16
+
17
+ import threading
18
+ from contextlib import contextmanager
19
+ try:
20
+ import nest_asyncio
21
+ nest_asyncio.apply()
22
+ except ImportError:
23
+ pass
24
+
25
+ # Lock for model initialization
26
+ init_lock = threading.Lock()
27
+ # Lock for serializing GPU inference (one user at a time)
28
+ inference_lock = threading.Lock()
29
+ # Queue tracking
30
+ _queue_lock = threading.Lock()
31
+ _queue_running_session = "" # session_id of the currently running request
32
+ _queue_start_time = 0.0 # when the current request started
33
+ _pending_sessions: list = [] # ordered list of ALL session_ids waiting
34
+ _pending_times: dict = {} # session_id -> registration timestamp
35
+ _PENDING_TIMEOUT = 600 # auto-remove after 10 minutes (safety net)
36
+
37
+ @contextmanager
38
+ def acquire_inference(session_id: str = ""):
39
+ """Context manager that serializes GPU access and tracks queue state."""
40
+ global _queue_running_session, _queue_start_time
41
+ # Register in pending list BEFORE waiting for lock
42
+ with _queue_lock:
43
+ if session_id and session_id not in _pending_sessions:
44
+ _pending_sessions.append(session_id)
45
+ _pending_times[session_id] = time.time()
46
+ try:
47
+ with inference_lock:
48
+ with _queue_lock:
49
+ if session_id and session_id in _pending_sessions:
50
+ _pending_sessions.remove(session_id)
51
+ _pending_times.pop(session_id, None)
52
+ _queue_running_session = session_id
53
+ _queue_start_time = time.time()
54
+ try:
55
+ yield
56
+ finally:
57
+ with _queue_lock:
58
+ _queue_running_session = ""
59
+ _queue_start_time = 0.0
60
+ except BaseException:
61
+ with _queue_lock:
62
+ if session_id and session_id in _pending_sessions:
63
+ _pending_sessions.remove(session_id)
64
+ _pending_times.pop(session_id, None)
65
+ raise
66
+
67
+ os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '1'
68
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
69
+ os.environ["ATTN_BACKEND"] = "flash_attn_3"
70
+ os.environ["FLEX_GEMM_AUTOTUNE_CACHE_PATH"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'autotune_cache.json')
71
+ os.environ["FLEX_GEMM_AUTOTUNER_VERBOSE"] = '1'
72
+
73
+ try:
74
+ import spaces
75
+ except ImportError:
76
+ # Local deployment: create a no-op decorator
77
+ class _FakeSpaces:
78
+ @staticmethod
79
+ def GPU(*args, **kwargs):
80
+ def decorator(fn):
81
+ return fn
82
+ return decorator
83
+ spaces = _FakeSpaces()
84
+ from gradio import Server
85
+ from gradio.data_classes import FileData
86
+ from fastapi.responses import HTMLResponse
87
+ from fastapi.staticfiles import StaticFiles
88
+
89
+ from trellis2.modules.sparse import SparseTensor
90
+ from trellis2.pipelines import Pixal3DImageTo3DPipeline
91
+ from trellis2.renderers import EnvMap
92
+ from trellis2.utils import render_utils
93
+ import o_voxel
94
+
95
+ # ============================================================================
96
+ # Constants & Defaults
97
+ # ============================================================================
98
+
99
+ MAX_SEED = np.iinfo(np.int32).max
100
+ TMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tmp')
101
+ os.makedirs(TMP_DIR, exist_ok=True)
102
+
103
+ MODES = [
104
+ {"name": "Normal", "icon": "assets/app/normal.png", "render_key": "normal"},
105
+ {"name": "Clay render", "icon": "assets/app/clay.png", "render_key": "clay"},
106
+ {"name": "Base color", "icon": "assets/app/basecolor.png", "render_key": "base_color"},
107
+ {"name": "HDRI forest", "icon": "assets/app/hdri_forest.png", "render_key": "shaded_forest"},
108
+ {"name": "HDRI sunset", "icon": "assets/app/hdri_sunset.png", "render_key": "shaded_sunset"},
109
+ {"name": "HDRI courtyard", "icon": "assets/app/hdri_courtyard.png", "render_key": "shaded_courtyard"},
110
+ ]
111
+ STEPS = 8
112
+
113
+ # Cascade parameters
114
+ CASCADE_LR_RESOLUTION = 512
115
+ CASCADE_MAX_NUM_TOKENS = 49152
116
+
117
+ # MoGe defaults
118
+ MOGE_MODEL_NAME = "Ruicheng/moge-2-vitl"
119
+ WILD_MESH_SCALE = 1.0
120
+ WILD_EXTEND_PIXEL = 0
121
+ WILD_IMAGE_RESOLUTION = 512
122
+
123
+ # Image Cond Model configs
124
+ IMAGE_COND_CONFIGS = {
125
+ "ss": {
126
+ "model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
127
+ "image_size": 512,
128
+ "grid_resolution": 16,
129
+ },
130
+ "shape_512": {
131
+ "model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
132
+ "image_size": 512,
133
+ "grid_resolution": 32,
134
+ "use_naf_upsample": True,
135
+ "naf_target_size": 512,
136
+ },
137
+ "shape_1024": {
138
+ "model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
139
+ "image_size": 1024,
140
+ "grid_resolution": 64,
141
+ "use_naf_upsample": True,
142
+ "naf_target_size": 512,
143
+ },
144
+ "tex_1024": {
145
+ "model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
146
+ "image_size": 1024,
147
+ "grid_resolution": 64,
148
+ "use_naf_upsample": True,
149
+ "naf_target_size": 1024,
150
+ },
151
+ }
152
+
153
+ # ============================================================================
154
+ # Model Loading
155
+ # ============================================================================
156
+
157
+ def build_image_cond_model(config: dict):
158
+ from trellis2.trainers.flow_matching.mixins.image_conditioned_proj import DinoV3ProjFeatureExtractor
159
+ model = DinoV3ProjFeatureExtractor(**config)
160
+ model.eval()
161
+ return model
162
+
163
+ def load_moge_model(device="cuda", model_name=MOGE_MODEL_NAME):
164
+ from moge.model.v2 import MoGeModel
165
+ moge_model = MoGeModel.from_pretrained(model_name).to(device)
166
+ moge_model.eval()
167
+ return moge_model
168
+
169
+ # Global instances (lazy loaded or loaded at start)
170
+ pipeline = None
171
+ moge_model = None
172
+ envmap = None
173
+
174
+ def init_models():
175
+ global pipeline, moge_model, envmap
176
+ with init_lock:
177
+ if pipeline is not None:
178
+ return
179
+
180
+ # GPU / CUDA Diagnostics (runs when GPU is allocated)
181
+ import subprocess as _sp
182
+ print("=" * 60)
183
+ print("[Diagnostics] PyTorch version:", torch.__version__)
184
+ print("[Diagnostics] CUDA available:", torch.cuda.is_available())
185
+ if torch.cuda.is_available():
186
+ print("[Diagnostics] CUDA version:", torch.version.cuda)
187
+ print("[Diagnostics] cuDNN version:", torch.backends.cudnn.version())
188
+ for i in range(torch.cuda.device_count()):
189
+ name = torch.cuda.get_device_name(i)
190
+ cap = torch.cuda.get_device_capability(i)
191
+ mem = torch.cuda.get_device_properties(i).total_memory / 1024**3
192
+ print(f"[Diagnostics] GPU {i}: {name}, sm_{cap[0]}{cap[1]}, {mem:.1f} GB")
193
+ try:
194
+ res = _sp.run(["nvidia-smi", "--query-gpu=name,compute_cap,memory.total", "--format=csv,noheader"], capture_output=True, text=True, timeout=10)
195
+ print("[Diagnostics] nvidia-smi:", res.stdout.strip())
196
+ except Exception as e:
197
+ print(f"[Diagnostics] nvidia-smi failed: {e}")
198
+ print("=" * 60)
199
+
200
+ model_path = "TencentARC/Pixal3D-T"
201
+ print(f"[Pipeline] Loading from {model_path}...")
202
+ pipeline = Pixal3DImageTo3DPipeline.from_pretrained(model_path)
203
+
204
+ print("[ImageCond] Building DinoV3ProjFeatureExtractor models...")
205
+ pipeline.image_cond_model_ss = build_image_cond_model(IMAGE_COND_CONFIGS["ss"])
206
+ pipeline.image_cond_model_shape_512 = build_image_cond_model(IMAGE_COND_CONFIGS["shape_512"])
207
+ pipeline.image_cond_model_shape_1024 = build_image_cond_model(IMAGE_COND_CONFIGS["shape_1024"])
208
+ pipeline.image_cond_model_tex_1024 = build_image_cond_model(IMAGE_COND_CONFIGS["tex_1024"])
209
+
210
+ pipeline.low_vram = False
211
+ pipeline.cuda()
212
+
213
+ # Ensure image_cond_models are on GPU
214
+ pipeline.image_cond_model_ss.cuda()
215
+ pipeline.image_cond_model_shape_512.cuda()
216
+ pipeline.image_cond_model_shape_1024.cuda()
217
+ pipeline.image_cond_model_tex_1024.cuda()
218
+
219
+ print("[NAF] Pre-loading NAF upsampler model...")
220
+ for attr in ['image_cond_model_ss', 'image_cond_model_shape_512', 'image_cond_model_shape_1024', 'image_cond_model_tex_1024']:
221
+ model = getattr(pipeline, attr, None)
222
+ if model is not None and getattr(model, 'use_naf_upsample', False):
223
+ model._load_naf()
224
+
225
+ print("[MoGe-2] Loading model for camera estimation...")
226
+ moge_model = load_moge_model(device="cuda")
227
+
228
+ print("[EnvMap] Loading environment maps...")
229
+ _base = os.path.dirname(os.path.abspath(__file__))
230
+ envmap = {
231
+ 'forest': EnvMap(torch.tensor(cv2.cvtColor(cv2.imread(os.path.join(_base, 'assets/hdri/forest.exr'), cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB), dtype=torch.float32, device='cuda')),
232
+ 'sunset': EnvMap(torch.tensor(cv2.cvtColor(cv2.imread(os.path.join(_base, 'assets/hdri/sunset.exr'), cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB), dtype=torch.float32, device='cuda')),
233
+ 'courtyard': EnvMap(torch.tensor(cv2.cvtColor(cv2.imread(os.path.join(_base, 'assets/hdri/courtyard.exr'), cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB), dtype=torch.float32, device='cuda')),
234
+ }
235
+
236
+ # ============================================================================
237
+ # Utilities
238
+ # ============================================================================
239
+
240
+ def compute_f_pixels(camera_angle_x: float, resolution: int) -> float:
241
+ focal_length = 16.0 / torch.tan(torch.tensor(camera_angle_x / 2.0))
242
+ f_pixels = focal_length * resolution / 32.0
243
+ return float(f_pixels.item())
244
+
245
+ def distance_from_fov(camera_angle_x, grid_point, target_point, mesh_scale, image_resolution):
246
+ rotation_matrix = torch.tensor([[1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]])
247
+ gp = grid_point.to(torch.float32) @ rotation_matrix.T
248
+ gp = gp / mesh_scale / 2
249
+ xw, yw, zw = gp[0].item(), gp[1].item(), gp[2].item()
250
+ xt, yt = float(target_point[0].item()), float(target_point[1].item())
251
+ f_pixels = compute_f_pixels(camera_angle_x, image_resolution)
252
+ x_ndc = xt - image_resolution / 2.0
253
+ y_ndc = -(yt - image_resolution / 2.0)
254
+ distance_x = f_pixels * xw / x_ndc - yw
255
+ return {"distance_from_x": float(distance_x), "f_pixels": float(f_pixels)}
256
+
257
+ def get_camera_params_wild_moge(image_path, device="cuda", mesh_scale=1.0, extend_pixel=0, image_resolution=512):
258
+ pil_image = Image.open(image_path).convert("RGB")
259
+ width, height = pil_image.size
260
+ image_np = np.array(pil_image).astype(np.float32) / 255.0
261
+ image_tensor = torch.from_numpy(image_np).permute(2, 0, 1).to(device)
262
+ with torch.no_grad():
263
+ output = moge_model.infer(image_tensor)
264
+ intrinsics = output["intrinsics"].squeeze().cpu().numpy()
265
+ fx_normalized = intrinsics[0, 0]
266
+ fx = fx_normalized * width
267
+ camera_angle_x = 2 * math.atan(width / (2 * fx))
268
+
269
+ grid_point = torch.tensor([-1.0, 0.0, 0.0])
270
+ distance = distance_from_fov(
271
+ camera_angle_x, grid_point,
272
+ torch.tensor([0 - extend_pixel, image_resolution - 1 + extend_pixel]),
273
+ mesh_scale, image_resolution
274
+ )["distance_from_x"]
275
+ return {'camera_angle_x': camera_angle_x, 'distance': distance, 'mesh_scale': mesh_scale}
276
+
277
+ def pack_state(shape_slat, tex_slat, res):
278
+ state_data = {
279
+ 'shape_slat_feats': shape_slat.feats.cpu().numpy(),
280
+ 'tex_slat_feats': tex_slat.feats.cpu().numpy(),
281
+ 'coords': shape_slat.coords.cpu().numpy(),
282
+ 'res': res,
283
+ }
284
+ import random
285
+ state_path = os.path.join(TMP_DIR, f"state_{int(time.time()*1000)}_{random.randint(0,9999):04d}.npz")
286
+ np.savez_compressed(state_path, **state_data)
287
+ return state_path
288
+
289
+ def unpack_state(state_path):
290
+ data = np.load(state_path)
291
+ shape_slat = SparseTensor(
292
+ feats=torch.from_numpy(data['shape_slat_feats']).cuda(),
293
+ coords=torch.from_numpy(data['coords']).cuda(),
294
+ )
295
+ tex_slat = shape_slat.replace(torch.from_numpy(data['tex_slat_feats']).cuda())
296
+ return shape_slat, tex_slat, int(data['res'])
297
+
298
+ # ============================================================================
299
+ # Progress Tracking (file-based, cross-process safe for @spaces.GPU)
300
+ # ============================================================================
301
+
302
+ import asyncio
303
+ from fastapi.responses import JSONResponse
304
+ from fastapi import Request
305
+
306
+ PROGRESS_DIR = os.path.join(TMP_DIR, '_progress')
307
+ os.makedirs(PROGRESS_DIR, exist_ok=True)
308
+
309
+ _thread_local = threading.local()
310
+
311
+ def _progress_file(session_id: str) -> str:
312
+ """Return path to a session's progress JSON file."""
313
+ return os.path.join(PROGRESS_DIR, f"{session_id}.json")
314
+
315
+ def _reset_progress(session_id: str):
316
+ _thread_local.active_session = session_id
317
+ _write_progress_file(session_id, {"stage": "Initializing...", "step": 0, "total": 0, "done": False})
318
+
319
+ def _update_progress(stage: str, step: int, total: int):
320
+ session_id = getattr(_thread_local, 'active_session', '')
321
+ if session_id:
322
+ _write_progress_file(session_id, {"stage": stage, "step": step, "total": total, "done": False})
323
+
324
+ def _finish_progress():
325
+ session_id = getattr(_thread_local, 'active_session', '')
326
+ if session_id:
327
+ _write_progress_file(session_id, {"done": True})
328
+
329
+ def _write_progress_file(session_id: str, data: dict):
330
+ """Atomically write progress JSON to a file (cross-process safe)."""
331
+ path = _progress_file(session_id)
332
+ tmp_path = path + ".tmp"
333
+ try:
334
+ with open(tmp_path, 'w') as f:
335
+ json.dump(data, f)
336
+ os.replace(tmp_path, path) # atomic on POSIX
337
+ except Exception:
338
+ pass
339
+
340
+ # Monkey-patch tqdm to intercept progress
341
+ import tqdm as _tqdm_module
342
+
343
+ _original_tqdm = _tqdm_module.tqdm
344
+
345
+ class _TqdmProgressInterceptor(_original_tqdm):
346
+ """Wraps tqdm to push progress updates to SSE."""
347
+ def __init__(self, *args, **kwargs):
348
+ self._stage_desc = kwargs.get('desc', 'Processing')
349
+ super().__init__(*args, **kwargs)
350
+
351
+ def set_description(self, desc=None, refresh=True):
352
+ self._stage_desc = desc or 'Processing'
353
+ super().set_description(desc, refresh)
354
+
355
+ def update(self, n=1):
356
+ super().update(n)
357
+ _update_progress(self._stage_desc, self.n, self.total or 0)
358
+
359
+ # Patch tqdm globally
360
+ _tqdm_module.tqdm = _TqdmProgressInterceptor
361
+ # Also patch the direct import in the sampler module and render_utils
362
+ import trellis2.pipelines.samplers.flow_euler as _fe_module
363
+ _fe_module.tqdm = _TqdmProgressInterceptor
364
+ import trellis2.utils.render_utils as _ru_module
365
+ _ru_module.tqdm = _TqdmProgressInterceptor
366
+ import o_voxel.postprocess as _ovp_module
367
+ _ovp_module.tqdm = _TqdmProgressInterceptor
368
+
369
+ # ============================================================================
370
+ # API Implementation
371
+ # ============================================================================
372
+
373
+ app = Server()
374
+
375
+ @app.get("/")
376
+ async def homepage():
377
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
378
+ with open(html_path, "r", encoding="utf-8") as f:
379
+ return HTMLResponse(content=f.read())
380
+
381
+ @app.get("/progress")
382
+ async def progress_poll(request: Request):
383
+ """Polling endpoint for real-time progress updates during generation."""
384
+ session_id = request.query_params.get("session_id", "")
385
+ path = _progress_file(session_id)
386
+ try:
387
+ with open(path, 'r') as f:
388
+ data = json.load(f)
389
+ return JSONResponse(data)
390
+ except (FileNotFoundError, json.JSONDecodeError):
391
+ return JSONResponse({"stage": "Waiting...", "step": 0, "total": 0, "done": False})
392
+
393
+ @app.get("/queue/join")
394
+ async def queue_join(request: Request):
395
+ """Register a session in the pending queue BEFORE the actual API call.
396
+ This ensures accurate queue position even when Gradio's thread pool delays dispatch."""
397
+ session_id = request.query_params.get("session_id", "")
398
+ if session_id:
399
+ with _queue_lock:
400
+ if session_id not in _pending_sessions and session_id != _queue_running_session:
401
+ _pending_sessions.append(session_id)
402
+ _pending_times[session_id] = time.time()
403
+ return JSONResponse({"ok": True})
404
+
405
+ @app.get("/queue")
406
+ async def queue_status(request: Request):
407
+ """Query queue status: how many are waiting, who is running."""
408
+ session_id = request.query_params.get("session_id", "")
409
+ now = time.time()
410
+ with _queue_lock:
411
+ # Auto-cleanup stale sessions (safety net for disconnected clients)
412
+ stale = [s for s in _pending_sessions if now - _pending_times.get(s, now) > _PENDING_TIMEOUT]
413
+ for s in stale:
414
+ _pending_sessions.remove(s)
415
+ _pending_times.pop(s, None)
416
+
417
+ running_session = _queue_running_session
418
+ pending = list(_pending_sessions) # snapshot
419
+ gpu_busy = bool(running_session)
420
+
421
+ # Calculate position for the requesting session
422
+ # position > 0: number of tasks ahead; position == 0: currently running; position == -1: not registered
423
+ if session_id and session_id == running_session:
424
+ position = 0 # you are currently being processed
425
+ elif session_id and session_id in pending:
426
+ idx = pending.index(session_id)
427
+ running_count = 1 if gpu_busy else 0
428
+ ahead = idx + running_count
429
+ # If nothing is ahead (we're first and GPU free), treat as "about to start"
430
+ # Use position = -2 to distinguish from "not registered" (-1)
431
+ position = ahead if ahead > 0 else -2
432
+ else:
433
+ position = -1 # not registered yet
434
+
435
+ # Total ahead for an unregistered session (they'd join at the back)
436
+ total_ahead_for_unregistered = len(pending) + (1 if gpu_busy else 0)
437
+
438
+ return JSONResponse({
439
+ "position": position,
440
+ "total_waiting": len(pending),
441
+ "gpu_busy": gpu_busy,
442
+ "total_ahead_for_unregistered": total_ahead_for_unregistered,
443
+ })
444
+
445
+ @app.api()
446
+ @spaces.GPU(duration=30)
447
+ def preprocess(image: FileData) -> FileData:
448
+ init_models()
449
+ img = Image.open(image["path"])
450
+ processed = pipeline.preprocess_image(img)
451
+ out_path = os.path.join(TMP_DIR, f"preprocessed_{int(time.time()*1000)}.png")
452
+ processed.save(out_path)
453
+ return FileData(path=out_path)
454
+
455
+ @app.api()
456
+ @spaces.GPU(duration=120)
457
+ def generate_3d(
458
+ image: FileData,
459
+ seed: int,
460
+ resolution: int,
461
+ ss_guidance_strength: float = 7.5,
462
+ ss_guidance_rescale: float = 0.7,
463
+ ss_sampling_steps: int = 12,
464
+ ss_rescale_t: float = 5.0,
465
+ shape_slat_guidance_strength: float = 7.5,
466
+ shape_slat_guidance_rescale: float = 0.5,
467
+ shape_slat_sampling_steps: int = 12,
468
+ shape_slat_rescale_t: float = 3.0,
469
+ tex_slat_guidance_strength: float = 1.0,
470
+ tex_slat_guidance_rescale: float = 0.0,
471
+ tex_slat_sampling_steps: int = 12,
472
+ tex_slat_rescale_t: float = 3.0,
473
+ session_id: str = "",
474
+ ) -> Dict:
475
+ with acquire_inference(session_id):
476
+ init_models()
477
+ _reset_progress(session_id)
478
+ _update_progress("Preprocessing & Camera Estimation", 0, 1)
479
+
480
+ torch.manual_seed(seed)
481
+ hr_resolution = int(resolution)
482
+
483
+ img = Image.open(image["path"])
484
+ # Image is already preprocessed by /preprocess endpoint, use directly
485
+ image_preprocessed = img
486
+ temp_processed_path = os.path.join(TMP_DIR, f"temp_proc_{session_id[:8]}_{int(time.time()*1000)}.png")
487
+ image_preprocessed.save(temp_processed_path)
488
+
489
+ camera_params = get_camera_params_wild_moge(
490
+ temp_processed_path, device="cuda",
491
+ mesh_scale=WILD_MESH_SCALE, extend_pixel=WILD_EXTEND_PIXEL,
492
+ image_resolution=WILD_IMAGE_RESOLUTION,
493
+ )
494
+ _update_progress("Preprocessing & Camera Estimation", 1, 1)
495
+
496
+ ss_sampler_override = {"steps": ss_sampling_steps, "guidance_strength": ss_guidance_strength,
497
+ "guidance_rescale": ss_guidance_rescale, "rescale_t": ss_rescale_t}
498
+ shape_sampler_override = {"steps": shape_slat_sampling_steps, "guidance_strength": shape_slat_guidance_strength,
499
+ "guidance_rescale": shape_slat_guidance_rescale, "rescale_t": shape_slat_rescale_t}
500
+ tex_sampler_override = {"steps": tex_slat_sampling_steps, "guidance_strength": tex_slat_guidance_strength,
501
+ "guidance_rescale": tex_slat_guidance_rescale, "rescale_t": tex_slat_rescale_t}
502
+
503
+ pipeline_type = f"{hr_resolution}_cascade"
504
+ mesh_list, (shape_slat, tex_slat, res) = pipeline.run(
505
+ image_preprocessed,
506
+ camera_params=camera_params,
507
+ seed=seed,
508
+ sparse_structure_sampler_params=ss_sampler_override,
509
+ shape_slat_sampler_params=shape_sampler_override,
510
+ tex_slat_sampler_params=tex_sampler_override,
511
+ preprocess_image=False,
512
+ return_latent=True,
513
+ pipeline_type=pipeline_type,
514
+ max_num_tokens=CASCADE_MAX_NUM_TOKENS,
515
+ )
516
+
517
+ mesh = mesh_list[0]
518
+ state_path = pack_state(shape_slat, tex_slat, res)
519
+
520
+ _update_progress("Rendering views", 0, 1)
521
+ mesh.simplify(16777216)
522
+ cam_dist = camera_params['distance']
523
+ near = max(0.01, cam_dist - 2.0)
524
+ far = cam_dist + 10.0
525
+ renders = render_utils.render_proj_aligned_video(
526
+ mesh, camera_angle_x=camera_params['camera_angle_x'],
527
+ distance=cam_dist, resolution=1024,
528
+ num_frames=STEPS, envmap=envmap,
529
+ near=near, far=far,
530
+ )
531
+ _update_progress("Rendering views", 1, 1)
532
+
533
+ # Save renders and return paths
534
+ render_files = {}
535
+ for mode_key, frames in renders.items():
536
+ mode_files = []
537
+ for i, frame in enumerate(frames):
538
+ p = os.path.abspath(os.path.join(TMP_DIR, f"render_{mode_key}_{i}_{int(time.time()*1000)}.jpg"))
539
+ Image.fromarray(frame).save(p, quality=85)
540
+ mode_files.append(FileData(path=p))
541
+ render_files[mode_key] = mode_files
542
+
543
+ _finish_progress()
544
+ return {
545
+ "render_paths": render_files,
546
+ "state_path": os.path.abspath(state_path),
547
+ "camera_angle_x": camera_params['camera_angle_x'],
548
+ "distance": camera_params['distance'],
549
+ }
550
+
551
+ @app.api()
552
+ @spaces.GPU(duration=240)
553
+ def extract_glb_api(state_path: str, decimation_target: int, texture_size: int, session_id: str = "") -> FileData:
554
+ with acquire_inference(session_id):
555
+ init_models()
556
+ _reset_progress(session_id)
557
+ _update_progress("Decoding latent", 0, 1)
558
+
559
+ shape_slat, tex_slat, res = unpack_state(state_path)
560
+ mesh = pipeline.decode_latent(shape_slat, tex_slat, res)[0]
561
+ _update_progress("Decoding latent", 1, 1)
562
+
563
+ glb = o_voxel.postprocess.to_glb(
564
+ vertices=mesh.vertices, faces=mesh.faces, attr_volume=mesh.attrs,
565
+ coords=mesh.coords, attr_layout=pipeline.pbr_attr_layout,
566
+ grid_size=res, aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
567
+ decimation_target=decimation_target, texture_size=texture_size,
568
+ remesh=True, remesh_band=1, remesh_project=0, use_tqdm=True,
569
+ )
570
+ rot = np.array([
571
+ [-1, 0, 0, 0],
572
+ [ 0, 0, -1, 0],
573
+ [ 0, -1, 0, 0],
574
+ [ 0, 0, 0, 1],
575
+ ], dtype=np.float64)
576
+ glb.apply_transform(rot)
577
+
578
+ out_glb = os.path.join(TMP_DIR, f"result_{int(time.time()*1000)}.glb")
579
+ glb.export(out_glb, extension_webp=True)
580
+ _finish_progress()
581
+ return FileData(path=out_glb)
582
+
583
+ # Mount assets and tmp for direct access
584
+ app.mount("/assets", StaticFiles(directory="assets"), name="assets")
585
+ app.mount("/tmp", StaticFiles(directory=TMP_DIR), name="tmp")
586
+
587
+ if __name__ == "__main__":
588
+ # Re-install utils3d as in original app.py
589
+ subprocess.run([
590
+ "pip", "install", "--force-reinstall", "--no-deps",
591
+ "https://github.com/LDYang694/Storages/releases/download/20260430/utils3d-0.0.2-py3-none-any.whl"
592
+ ], check=True)
593
+
594
+ # Pre-initialize models before launching the server
595
+ init_models()
596
+
597
+ app.launch(show_error=True, share=True)
app_proxy.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pixal3D HF Space Proxy
3
+ ======================
4
+ This is a lightweight proxy app for HF Space that redirects users to a
5
+ locally deployed Gradio share link.
6
+
7
+ Setup:
8
+ 1. Deploy this as your HF Space app.py
9
+ 2. Set HF Space Secret: REMOTE_URL = your local share link (e.g. https://xxxxx.gradio.live)
10
+ 3. Users visiting the HF Space will be seamlessly redirected to your local instance.
11
+
12
+ To update the share link:
13
+ - Go to HF Space Settings -> Variables and secrets -> Update REMOTE_URL
14
+ """
15
+
16
+ import os
17
+ import gradio as gr
18
+
19
+ REMOTE_URL = os.environ.get("REMOTE_URL", "")
20
+ GPU_NAME = os.environ.get("GPU_NAME", "")
21
+
22
+ # Multi-instance support: REMOTE_URL as #0, REMOTE_URL_1, REMOTE_URL_2, REMOTE_URL_3
23
+ REMOTE_URLS = []
24
+ if REMOTE_URL:
25
+ name0 = os.environ.get("REMOTE_NAME", "Instance 0")
26
+ REMOTE_URLS.append({"url": REMOTE_URL, "name": name0})
27
+ for i in range(1, 4):
28
+ url = os.environ.get(f"REMOTE_URL_{i}", "")
29
+ name = os.environ.get(f"REMOTE_NAME_{i}", f"Instance {i}")
30
+ if url:
31
+ REMOTE_URLS.append({"url": url, "name": name})
32
+
33
+ PROXY_HTML = """
34
+ <!DOCTYPE html>
35
+ <html lang="en">
36
+ <head>
37
+ <meta charset="UTF-8">
38
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
39
+ <title>Pixal3D | AI Image-to-3D</title>
40
+ <style>
41
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
42
+ html, body {{
43
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44
+ background: #0b0f1a;
45
+ color: #f1f5f9;
46
+ height: 100%;
47
+ overflow: hidden;
48
+ display: flex;
49
+ flex-direction: column;
50
+ }}
51
+ .header {{
52
+ padding: 8px 24px;
53
+ background: rgba(22, 28, 45, 0.9);
54
+ border-bottom: 1px solid rgba(255,255,255,0.08);
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 16px;
58
+ backdrop-filter: blur(12px);
59
+ }}
60
+ .header h1 {{
61
+ font-size: 16px;
62
+ font-weight: 700;
63
+ background: linear-gradient(135deg, #818cf8, #10b981);
64
+ -webkit-background-clip: text;
65
+ -webkit-text-fill-color: transparent;
66
+ white-space: nowrap;
67
+ }}
68
+ .header .notice {{
69
+ flex: 1;
70
+ font-size: 12px;
71
+ color: #fbbf24;
72
+ text-align: center;
73
+ }}
74
+ .status {{
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 6px;
78
+ font-size: 12px;
79
+ color: #94a3b8;
80
+ white-space: nowrap;
81
+ }}
82
+ .status-dot {{
83
+ width: 7px;
84
+ height: 7px;
85
+ border-radius: 50%;
86
+ background: {status_color};
87
+ animation: {status_anim};
88
+ }}
89
+ @keyframes pulse {{
90
+ 0%, 100% {{ opacity: 1; }}
91
+ 50% {{ opacity: 0.4; }}
92
+ }}
93
+ .iframe-container {{
94
+ flex: 1;
95
+ position: relative;
96
+ }}
97
+ .iframe-container iframe {{
98
+ width: 100%;
99
+ height: 100%;
100
+ border: none;
101
+ position: absolute;
102
+ top: 0;
103
+ left: 0;
104
+ }}
105
+ .no-url {{
106
+ flex: 1;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ padding: 40px;
111
+ }}
112
+ .no-url-card {{
113
+ max-width: 560px;
114
+ background: rgba(22, 28, 45, 0.8);
115
+ border: 1px solid rgba(255,255,255,0.08);
116
+ border-radius: 16px;
117
+ padding: 48px;
118
+ text-align: center;
119
+ }}
120
+ .no-url-card h2 {{
121
+ font-size: 24px;
122
+ margin-bottom: 16px;
123
+ }}
124
+ .no-url-card p {{
125
+ color: #94a3b8;
126
+ line-height: 1.7;
127
+ margin-bottom: 12px;
128
+ }}
129
+ .no-url-card code {{
130
+ background: rgba(129, 140, 248, 0.15);
131
+ color: #818cf8;
132
+ padding: 2px 8px;
133
+ border-radius: 4px;
134
+ font-size: 13px;
135
+ }}
136
+ .cards-container {{
137
+ flex: 1;
138
+ display: flex;
139
+ flex-direction: column;
140
+ align-items: center;
141
+ justify-content: center;
142
+ padding: 40px;
143
+ overflow-y: auto;
144
+ }}
145
+ .cards-grid {{
146
+ display: grid;
147
+ grid-template-columns: repeat(2, 1fr);
148
+ gap: 28px;
149
+ max-width: 1000px;
150
+ width: 100%;
151
+ }}
152
+ .instance-card {{
153
+ width: 100%;
154
+ background: rgba(22, 28, 45, 0.8);
155
+ border: 1px solid rgba(255,255,255,0.08);
156
+ border-radius: 24px;
157
+ padding: 60px 48px;
158
+ text-align: center;
159
+ transition: transform 0.2s, border-color 0.2s;
160
+ }}
161
+ .instance-card:hover {{
162
+ transform: translateY(-4px);
163
+ border-color: rgba(129, 140, 248, 0.4);
164
+ }}
165
+ .instance-card h3 {{
166
+ font-size: 26px;
167
+ margin-bottom: 16px;
168
+ color: #f1f5f9;
169
+ }}
170
+ .queue-status {{
171
+ display: inline-flex;
172
+ align-items: center;
173
+ gap: 8px;
174
+ padding: 8px 16px;
175
+ border-radius: 20px;
176
+ font-size: 15px;
177
+ font-weight: 600;
178
+ margin-bottom: 8px;
179
+ background: rgba(148, 163, 184, 0.1);
180
+ color: #94a3b8;
181
+ }}
182
+ .queue-status.idle {{
183
+ background: rgba(16, 185, 129, 0.15);
184
+ color: #10b981;
185
+ }}
186
+ .queue-status.busy {{
187
+ background: rgba(251, 146, 60, 0.15);
188
+ color: #fb923c;
189
+ }}
190
+ .queue-status.offline {{
191
+ background: rgba(239, 68, 68, 0.15);
192
+ color: #ef4444;
193
+ }}
194
+ .queue-dot {{
195
+ width: 8px;
196
+ height: 8px;
197
+ border-radius: 50%;
198
+ background: currentColor;
199
+ animation: pulse 2s infinite;
200
+ }}
201
+ .instance-card .url-hint {{
202
+ font-size: 13px;
203
+ color: #64748b;
204
+ margin-top: 18px;
205
+ word-break: break-all;
206
+ }}
207
+ .instance-card .btn-go {{
208
+ display: inline-block;
209
+ margin-top: 24px;
210
+ padding: 16px 44px;
211
+ background: linear-gradient(135deg, #818cf8, #10b981);
212
+ color: #ffffff !important;
213
+ border-radius: 12px;
214
+ text-decoration: none !important;
215
+ font-weight: 700;
216
+ font-size: 18px;
217
+ transition: opacity 0.2s;
218
+ }}
219
+ .instance-card .btn-go:hover {{
220
+ opacity: 0.85;
221
+ text-decoration: none !important;
222
+ }}
223
+ .instance-card .btn-go:hover {{
224
+ opacity: 0.85;
225
+ }}
226
+ .link-bar {{
227
+ padding: 8px 24px;
228
+ background: rgba(16, 185, 129, 0.08);
229
+ border-top: 1px solid rgba(16, 185, 129, 0.2);
230
+ font-size: 12px;
231
+ color: #94a3b8;
232
+ text-align: center;
233
+ }}
234
+ .link-bar a {{
235
+ color: #10b981;
236
+ text-decoration: none;
237
+ }}
238
+ .link-bar a:hover {{ text-decoration: underline; }}
239
+ </style>
240
+ </head>
241
+ <body>
242
+ <div class="header">
243
+ <h1>Pixal3D</h1>
244
+ <span class="notice"></span>
245
+ <div class="status">
246
+ <div class="status-dot"></div>
247
+ <span>{status_text}</span>
248
+ </div>
249
+ </div>
250
+ {content}
251
+ </body>
252
+ </html>
253
+ """
254
+
255
+
256
+ def build_page():
257
+ # If multi-instance URLs are configured, show cards
258
+ if REMOTE_URLS:
259
+ status_color = "#10b981"
260
+ status_anim = "pulse 2s infinite"
261
+ status_text = f"{len(REMOTE_URLS)} instance(s) available"
262
+
263
+ cards_html = ""
264
+ for i, inst in enumerate(REMOTE_URLS):
265
+ cards_html += f"""
266
+ <div class="instance-card">
267
+ <h3>🖥️ {inst['name']}</h3>
268
+ <p style="color:#94a3b8; font-size:14px; margin-bottom:8px;">⚡ Shared GPU — requests are queued</p>
269
+ <div class="queue-status" id="queue-status-{i}">
270
+ <span class="queue-dot"></span>
271
+ <span id="queue-text-{i}">Checking...</span>
272
+ </div>
273
+ <a href="{inst['url']}" target="_blank" rel="noopener noreferrer" class="btn-go">
274
+ Open Instance {i}
275
+ </a>
276
+ <p class="url-hint"><code>{inst['url']}</code></p>
277
+ </div>
278
+ """
279
+
280
+ # Build JS array of instance URLs for direct polling (Gradio share links support CORS natively)
281
+ urls_js = ", ".join(['"' + inst["url"].rstrip("/") + '"' for inst in REMOTE_URLS])
282
+
283
+ content = f"""
284
+ <div class="cards-container">
285
+ <div style="width:100%; text-align:center; margin-bottom:16px;">
286
+ <h2 style="font-size:28px; margin-bottom:12px;">🚀 Choose a Pixal3D Instance</h2>
287
+ <p style="color:#fbbf24; font-size:15px; margin-bottom:8px;">⚠️ Due to a temporary HuggingFace error, this Space is currently unavailable. Please use one of the instances below.</p>
288
+ <p style="color:#10b981; font-size:14px; margin-top:10px; font-weight:600;">💡 Choose the instance with the shortest queue!</p>
289
+ </div>
290
+ <div class="cards-grid">
291
+ {cards_html}
292
+ </div>
293
+ </div>
294
+ """
295
+
296
+ poll_script = f"""
297
+ const INSTANCE_URLS = [{urls_js}];
298
+ async function pollQueues() {{
299
+ for (let i = 0; i < INSTANCE_URLS.length; i++) {{
300
+ try {{
301
+ const controller = new AbortController();
302
+ const timeout = setTimeout(() => controller.abort(), 5000);
303
+ const resp = await fetch(INSTANCE_URLS[i] + '/queue?session_id=', {{
304
+ signal: controller.signal
305
+ }});
306
+ clearTimeout(timeout);
307
+ if (resp.ok) {{
308
+ const data = await resp.json();
309
+ const total = data.total_waiting + (data.gpu_busy ? 1 : 0);
310
+ const el = document.getElementById('queue-text-' + i);
311
+ const status = document.getElementById('queue-status-' + i);
312
+ if (total === 0) {{
313
+ el.textContent = 'Idle — no queue';
314
+ status.className = 'queue-status idle';
315
+ }} else {{
316
+ el.textContent = total + ' in queue';
317
+ status.className = 'queue-status busy';
318
+ }}
319
+ }} else {{
320
+ const el = document.getElementById('queue-text-' + i);
321
+ const status = document.getElementById('queue-status-' + i);
322
+ if (el) {{
323
+ el.textContent = 'Offline';
324
+ status.className = 'queue-status offline';
325
+ }}
326
+ }}
327
+ }} catch (e) {{
328
+ const el = document.getElementById('queue-text-' + i);
329
+ const status = document.getElementById('queue-status-' + i);
330
+ if (el) {{
331
+ el.textContent = 'Offline';
332
+ status.className = 'queue-status offline';
333
+ }}
334
+ }}
335
+ }}
336
+ }}
337
+ pollQueues();
338
+ setInterval(pollQueues, 5000);
339
+ """
340
+ else:
341
+ status_color = "#ef4444"
342
+ status_anim = "pulse 1.5s infinite"
343
+ status_text = "Remote instance not configured"
344
+ poll_script = ""
345
+ content = """
346
+ <div class="no-url">
347
+ <div class="no-url-card">
348
+ <h2>⚡ Remote GPU Instance Not Connected</h2>
349
+ <p>This Space acts as a proxy to a locally-deployed Pixal3D instance running on a dedicated GPU.</p>
350
+ <p>To connect, set the <code>REMOTE_URL</code> secret in this Space's settings to your Gradio share link.</p>
351
+ <p style="margin-top:24px; font-size:13px;">
352
+ Example: <code>https://abcdef123456.gradio.live</code>
353
+ </p>
354
+ </div>
355
+ </div>
356
+ """
357
+
358
+ html = PROXY_HTML.format(
359
+ status_color=status_color,
360
+ status_anim=status_anim,
361
+ status_text=status_text,
362
+ gpu_name=GPU_NAME,
363
+ content=content,
364
+ )
365
+ return html, poll_script
366
+
367
+
368
+ # Use a simple Gradio Blocks app with HTML component
369
+ page_html, page_script = build_page()
370
+
371
+ with gr.Blocks(
372
+ title="Pixal3D | AI Image-to-3D",
373
+ css="footer {display:none !important;} .gradio-container {padding:0 !important; max-width:100% !important; height:100vh !important; overflow:hidden !important;} #proxy-frame {height:100%; max-height:100vh; padding:0; overflow:hidden;}",
374
+ theme=gr.themes.Base(),
375
+ ) as demo:
376
+ gr.HTML(page_html, elem_id="proxy-frame", js_on_load=page_script if page_script else None)
377
+
378
+ if __name__ == "__main__":
379
+ demo.launch(share=True)
assets/app/basecolor.png ADDED
assets/app/clay.png ADDED
assets/app/hdri_city.png ADDED
assets/app/hdri_courtyard.png ADDED
assets/app/hdri_forest.png ADDED
assets/app/hdri_interior.png ADDED
assets/app/hdri_night.png ADDED
assets/app/hdri_studio.png ADDED
assets/app/hdri_sunrise.png ADDED
assets/app/hdri_sunset.png ADDED
assets/app/normal.png ADDED
assets/hdri/city.exr ADDED

Git LFS Details

  • SHA256: 9e42abcd2fa3231e5c2485ca6dd64800534d157b7194ab7fe9cc3bf5a56d0256
  • Pointer size: 131 Bytes
  • Size of remote file: 205 kB
assets/hdri/courtyard.exr ADDED

Git LFS Details

  • SHA256: 6690b47725965531559380121e6878373eb599655e35fefd109a4bd0911366f3
  • Pointer size: 131 Bytes
  • Size of remote file: 255 kB
assets/hdri/forest.exr ADDED

Git LFS Details

  • SHA256: bdf2298244affa0f85509380fd130ac6d4dfaa3c856df065998f7f4c1a93dc0d
  • Pointer size: 131 Bytes
  • Size of remote file: 553 kB
assets/hdri/interior.exr ADDED

Git LFS Details

  • SHA256: e945ff5c1ddd7a3aaf05e9fb5c3bc9cb93c5518414febd44ed2c394e013f0cbd
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB
assets/hdri/license.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ All HDRIs are licensed as CC0.
2
+
3
+ These were created by Greg Zaal (Poly Haven https://polyhaven.com).
4
+ Originals used for each HDRI:
5
+ - City: https://polyhaven.com/a/portland_landing_pad
6
+ - Courtyard: https://polyhaven.com/a/courtyard
7
+ - Forest: https://polyhaven.com/a/ninomaru_teien
8
+ - Interior: https://polyhaven.com/a/hotel_room
9
+ - Night: Probably https://polyhaven.com/a/moonless_golf
10
+ - Studio: Probably https://polyhaven.com/a/studio_small_01
11
+ - Sunrise: https://polyhaven.com/a/spruit_sunrise
12
+ - Sunset: https://polyhaven.com/a/venice_sunset
13
+
14
+ 1K resolution of each was taken, and compressed with oiiotool:
15
+ oiiotool input.exr --ch R,G,B -d float --compression dwab:300 --clamp:min=0.0:max=32000.0 -o output.exr
assets/hdri/night.exr ADDED

Git LFS Details

  • SHA256: 17480af5547465d160f7307c92585cf30820ca563f87f066395decbad8ac32a4
  • Pointer size: 131 Bytes
  • Size of remote file: 140 kB
assets/hdri/studio.exr ADDED
assets/hdri/sunrise.exr ADDED

Git LFS Details

  • SHA256: 6a1180126e4db7d01f134c5430ea43c1b263ab1a12faf58a444d9ce9c03f3a84
  • Pointer size: 131 Bytes
  • Size of remote file: 252 kB
assets/hdri/sunset.exr ADDED

Git LFS Details

  • SHA256: 3bcafdda4f2d7b9759cc1d73004d34d721a274c29b1a0947be88a602dbac426b
  • Pointer size: 131 Bytes
  • Size of remote file: 171 kB
assets/images/0_img.png ADDED

Git LFS Details

  • SHA256: 6959e517ee4bc6852791f69bd6ece696a435abcda8321727c07db8daf7f457cf
  • Pointer size: 133 Bytes
  • Size of remote file: 14 MB
assets/images/10_img.webp ADDED

Git LFS Details

  • SHA256: 9558307682a1e723f86291af377f0b53970c0d301304c1f43dec741fedc209d7
  • Pointer size: 131 Bytes
  • Size of remote file: 118 kB
assets/images/11_img.png ADDED

Git LFS Details

  • SHA256: fbef7bb66e4ffa0cf4866399cf2b0841c56239086772f2fedea87a76718dde4d
  • Pointer size: 132 Bytes
  • Size of remote file: 8.41 MB
assets/images/12_img.png ADDED

Git LFS Details

  • SHA256: c8a4b4e92b4fa6a8eeec352f25af3f66b65ee0c743ad483b727404c9cb9a6914
  • Pointer size: 132 Bytes
  • Size of remote file: 7.96 MB
assets/images/17_img.png ADDED

Git LFS Details

  • SHA256: 134136dd4086cfc1b887ab0a134c4a2b906223762a0d5959a8b90cc68f11f4f0
  • Pointer size: 132 Bytes
  • Size of remote file: 1.49 MB
assets/images/1_img.png ADDED

Git LFS Details

  • SHA256: fdd82d60b7ec11e6d5699df29693d8ab538f9dab4b04e3f2abaa59ccd7b4709a
  • Pointer size: 131 Bytes
  • Size of remote file: 226 kB
assets/images/21_img.png ADDED

Git LFS Details

  • SHA256: 5793b52a21507a5cdd661f8d87681b303e420057a8c65aaa16e0560409a7a34e
  • Pointer size: 131 Bytes
  • Size of remote file: 725 kB
assets/images/3_img.webp ADDED

Git LFS Details

  • SHA256: d15269d0c0b427eabcca39d6d093cc2cfdaab19cb8a40b6158988a70a91ffe45
  • Pointer size: 131 Bytes
  • Size of remote file: 207 kB
assets/images/4_img.png ADDED

Git LFS Details

  • SHA256: 73d09b20fb4e7ab6513a3ac99f7c87166ec655a731bd1be9b14cec6aef36c4ef
  • Pointer size: 132 Bytes
  • Size of remote file: 1.51 MB
assets/images/5_img.webp ADDED

Git LFS Details

  • SHA256: a319ace2549835da92a6ffa5db73eebd7fce29079e5865cb32dfbdac21d9b900
  • Pointer size: 131 Bytes
  • Size of remote file: 100 kB
assets/images/5c80e5e03a3b60b6f03eaf555ba1dafc0e4230c472d7e8c8e2c5ca0a0dfcef10.webp ADDED

Git LFS Details

  • SHA256: fbd98cf5da79c56f8efc6cf86804391e71bdad2e935d21a7262472653a0674dc
  • Pointer size: 131 Bytes
  • Size of remote file: 127 kB
assets/images/6_img.png ADDED

Git LFS Details

  • SHA256: e8afc2d5a553e965ae13671eaeb198f807c5e6021a6f96b1d8f900b056b70d9d
  • Pointer size: 133 Bytes
  • Size of remote file: 10 MB
assets/images/7_img.png ADDED

Git LFS Details

  • SHA256: c4d0c266c01dc8e24cf33bb0d1155a18bd851ea09b6de914e79a78f021024d5b
  • Pointer size: 133 Bytes
  • Size of remote file: 11.2 MB
assets/images/9_img.png ADDED

Git LFS Details

  • SHA256: d7e716abe8f8895080f562d1dc26b14fa0e20a05aa5beb2770c6fb3b87b3476a
  • Pointer size: 131 Bytes
  • Size of remote file: 594 kB
assets/images/c9340e744541f310bf89838f652602961d3e5950b31cd349bcbfc7e59e15cd2e.webp ADDED

Git LFS Details

  • SHA256: 4533ce41604e7aff386c71f37f0b2727242a4615ef0e37c3cd62273678ad1809
  • Pointer size: 131 Bytes
  • Size of remote file: 143 kB
assets/images/f94e2b76494ce2cf1874611273e5fb3d76b395793bb5647492fa85c2ce0a248b.webp ADDED
assets/images/musicman.png ADDED

Git LFS Details

  • SHA256: 1d481cc4d806dc5c56eff89afc776522d273c4605cd5f5bec80a1141e3f5010d
  • Pointer size: 131 Bytes
  • Size of remote file: 897 kB
assets/images/pizza.png ADDED

Git LFS Details

  • SHA256: 0171aff5b61055522660f1c5b7d588f50a65822e20060d285d8909046cfa916a
  • Pointer size: 131 Bytes
  • Size of remote file: 719 kB
assets/images/s_13_img.jpg ADDED

Git LFS Details

  • SHA256: d63c45cf9f39b9e410a3a700a8deefe295654a1437d238c0b61a37fbe2752adc
  • Pointer size: 132 Bytes
  • Size of remote file: 1.36 MB
assets/images/s_14_img.jpg ADDED

Git LFS Details

  • SHA256: f199f6f12d8f363fe62bfb5cf6927f27864581debd8df68bf30c3dacdde9e301
  • Pointer size: 131 Bytes
  • Size of remote file: 182 kB
assets/images/s_15_img.png ADDED

Git LFS Details

  • SHA256: 68e9363e8d93dddfc9aaeb4ee52dcbec37cb423c75981d29da4eff370575846a
  • Pointer size: 132 Bytes
  • Size of remote file: 2.29 MB
assets/images/s_16_img.png ADDED

Git LFS Details

  • SHA256: a90ee0608ad7137ad0aac7e4dadf5f8909110673fed663df276e955fb10e05fb
  • Pointer size: 132 Bytes
  • Size of remote file: 2.41 MB
assets/images/s_18_img.png ADDED

Git LFS Details

  • SHA256: 4019f8e13be59da80e3bdcc2c359b04f514ee04a7889c9e66ec621429af9a236
  • Pointer size: 131 Bytes
  • Size of remote file: 769 kB