Daankular commited on
Commit
911a67c
Β·
verified Β·
1 Parent(s): ba431af

Upload pipeline/pshuman_client.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. pipeline/pshuman_client.py +309 -0
pipeline/pshuman_client.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ pshuman_client.py
3
+ =================
4
+ Call PSHuman to generate a high-detail 3D face mesh from a portrait image.
5
+
6
+ Two modes:
7
+ - Direct (default when service_url is localhost): runs PSHuman inference.py
8
+ as a subprocess without going through Gradio HTTP. Avoids the gradio_client
9
+ API-info bug that affects the pshuman Gradio env.
10
+ - Remote: uses gradio_client to call a running pshuman_app.py service.
11
+
12
+ Usage (standalone)
13
+ ------------------
14
+ python -m pipeline.pshuman_client \\
15
+ --image /path/to/portrait.png \\
16
+ --output /tmp/pshuman_face.obj \\
17
+ [--url http://remote-host:7862] # omit for direct/local mode
18
+
19
+ Requires: gradio-client (remote mode only)
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import glob
25
+ import os
26
+ import shutil
27
+ import subprocess
28
+ import time
29
+ from pathlib import Path
30
+
31
+ # Default: assume running on the same instance (local)
32
+ _DEFAULT_URL = os.environ.get("PSHUMAN_URL", "http://localhost:7862")
33
+
34
+ # ── Paths (on the Vast instance) ──────────────────────────────────────────────
35
+ PSHUMAN_DIR = "/root/PSHuman"
36
+ CONDA_PYTHON = "/root/miniconda/envs/pshuman/bin/python"
37
+ CONFIG = f"{PSHUMAN_DIR}/configs/inference-768-6view.yaml"
38
+ HF_MODEL_DIR = f"{PSHUMAN_DIR}/checkpoints/PSHuman_Unclip_768_6views"
39
+ HF_MODEL_HUB = "pengHTYX/PSHuman_Unclip_768_6views"
40
+
41
+
42
+ def _run_pshuman_direct(image_path: str, work_dir: str) -> str:
43
+ """
44
+ Run PSHuman inference.py directly as a subprocess.
45
+ Returns path to the colored OBJ mesh.
46
+ """
47
+ img_dir = os.path.join(work_dir, "input")
48
+ out_dir = os.path.join(work_dir, "out")
49
+ os.makedirs(img_dir, exist_ok=True)
50
+ os.makedirs(out_dir, exist_ok=True)
51
+
52
+ scene = "face"
53
+ dst = os.path.join(img_dir, f"{scene}.png")
54
+ shutil.copy(image_path, dst)
55
+
56
+ hf_model = HF_MODEL_DIR if Path(HF_MODEL_DIR).exists() else HF_MODEL_HUB
57
+
58
+ cmd = [
59
+ CONDA_PYTHON, f"{PSHUMAN_DIR}/inference.py",
60
+ "--config", CONFIG,
61
+ f"pretrained_model_name_or_path={hf_model}",
62
+ f"validation_dataset.root_dir={img_dir}",
63
+ f"save_dir={out_dir}",
64
+ "validation_dataset.crop_size=740",
65
+ "with_smpl=false",
66
+ "num_views=7",
67
+ "save_mode=rgb",
68
+ "seed=42",
69
+ ]
70
+
71
+ print(f"[pshuman] Running direct inference: {' '.join(cmd[:4])} ...")
72
+ t0 = time.time()
73
+
74
+ # Set CUDA_HOME + extra include dirs so nvdiffrast/torch JIT can compile.
75
+ # On Vast.ai, triposg conda env ships nvcc at bin/nvcc and CUDA headers
76
+ # scattered across site-packages/nvidia/{pkg}/include/ directories.
77
+ env = os.environ.copy()
78
+ # Strip env vars that the triposg parent sets but pshuman's older PyTorch can't handle.
79
+ # PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True is a PyTorch 2.0+ feature β€” older
80
+ # pshuman env crashes at CUDA allocator init with an INTERNAL ASSERT FAILED if it's set.
81
+ for _bad in ("PYTORCH_CUDA_ALLOC_CONF", "TORCH_CUDA_ARCH_LIST"):
82
+ env.pop(_bad, None)
83
+
84
+ if "CUDA_HOME" not in env:
85
+ _triposg = "/root/miniconda/envs/triposg"
86
+ _targets = os.path.join(_triposg, "targets", "x86_64-linux")
87
+ _nvcc_bin = os.path.join(_triposg, "bin")
88
+ _cuda_home = _targets # has include/cuda_runtime_api.h
89
+
90
+ _nvvm_bin = os.path.join(_triposg, "nvvm", "bin") # contains cicc
91
+ _nvcc_real = os.path.join(_targets, "bin") # contains nvcc (real one)
92
+
93
+ if (os.path.exists(os.path.join(_cuda_home, "include", "cuda_runtime_api.h"))
94
+ and (os.path.exists(os.path.join(_nvcc_bin, "nvcc"))
95
+ or os.path.exists(os.path.join(_nvcc_real, "nvcc")))):
96
+ env["CUDA_HOME"] = _cuda_home
97
+ # Build PATH: nvvm/bin (cicc) + targets/.../bin (nvcc real) + conda bin (nvcc wrapper)
98
+ path_parts = []
99
+ if os.path.isdir(_nvvm_bin):
100
+ path_parts.append(_nvvm_bin)
101
+ if os.path.isdir(_nvcc_real):
102
+ path_parts.append(_nvcc_real)
103
+ path_parts.append(_nvcc_bin)
104
+ env["PATH"] = ":".join(path_parts) + ":" + env.get("PATH", "")
105
+
106
+ # Collect all nvidia sub-package include dirs (cusparse, cublas, etc.)
107
+ _nvidia_site = os.path.join(_triposg, "lib", "python3.10",
108
+ "site-packages", "nvidia")
109
+ _extra_incs = []
110
+ if os.path.isdir(_nvidia_site):
111
+ import glob as _glob
112
+ for _inc in _glob.glob(os.path.join(_nvidia_site, "*/include")):
113
+ if os.path.isdir(_inc):
114
+ _extra_incs.append(_inc)
115
+ if _extra_incs:
116
+ _sep = ":"
117
+ _existing = env.get("CPATH", "")
118
+ env["CPATH"] = _sep.join(_extra_incs) + (_sep + _existing if _existing else "")
119
+ print(f"[pshuman] CUDA_HOME={_cuda_home}, {len(_extra_incs)} nvidia include dirs added")
120
+
121
+ proc = subprocess.run(
122
+ cmd, cwd=PSHUMAN_DIR,
123
+ capture_output=False,
124
+ text=True,
125
+ timeout=600,
126
+ env=env,
127
+ )
128
+ elapsed = time.time() - t0
129
+ print(f"[pshuman] Inference done in {elapsed:.1f}s (exit={proc.returncode})")
130
+
131
+ if proc.returncode != 0:
132
+ raise RuntimeError(f"PSHuman inference failed (exit {proc.returncode})")
133
+
134
+ # Locate output OBJ β€” PSHuman may save relative to its CWD (/root/PSHuman/out/)
135
+ # rather than to the specified save_dir, so check both locations.
136
+ # IMPORTANT: always check for colored OBJs (result_clr_scale*) before falling
137
+ # back to any OBJ β€” the colored file has vertex colors (v x y z r g b) which
138
+ # are required for the face to render with skin tones.
139
+ cwd_out_dir = os.path.join(PSHUMAN_DIR, "out", scene)
140
+ colored_patterns = [
141
+ f"{out_dir}/{scene}/result_clr_scale4_{scene}.obj",
142
+ f"{out_dir}/{scene}/result_clr_scale*_{scene}.obj",
143
+ f"{cwd_out_dir}/result_clr_scale4_{scene}.obj",
144
+ f"{cwd_out_dir}/result_clr_scale*_{scene}.obj",
145
+ f"{PSHUMAN_DIR}/out/**/result_clr_scale*_{scene}.obj",
146
+ ]
147
+ fallback_patterns = [
148
+ f"{out_dir}/**/*.obj",
149
+ f"{cwd_out_dir}/*.obj",
150
+ f"{PSHUMAN_DIR}/out/**/*.obj",
151
+ ]
152
+ obj_path = None
153
+ # First pass: colored OBJs only
154
+ for pat in colored_patterns:
155
+ hits = sorted(glob.glob(pat, recursive=True))
156
+ if hits:
157
+ obj_path = hits[-1]
158
+ print(f"[pshuman] Found colored OBJ: {obj_path}")
159
+ break
160
+ # Second pass: any OBJ (fallback)
161
+ if not obj_path:
162
+ for pat in fallback_patterns:
163
+ hits = sorted(glob.glob(pat, recursive=True))
164
+ if hits:
165
+ colored = [h for h in hits if "clr" in h]
166
+ obj_path = (colored or hits)[-1]
167
+ if colored:
168
+ print(f"[pshuman] Found colored OBJ (fallback): {obj_path}")
169
+ else:
170
+ print(f"[pshuman] WARNING: no colored OBJ found, using: {obj_path}")
171
+ break
172
+
173
+ if not obj_path:
174
+ all_files = list(Path(out_dir).rglob("*"))
175
+ objs = [str(f) for f in all_files if f.suffix in (".obj", ".ply", ".glb")]
176
+ if objs:
177
+ obj_path = objs[-1]
178
+ if not obj_path and Path(cwd_out_dir).exists():
179
+ for f in Path(cwd_out_dir).rglob("*.obj"):
180
+ obj_path = str(f)
181
+ break
182
+ if not obj_path:
183
+ raise FileNotFoundError(
184
+ f"No mesh output found in {out_dir}. "
185
+ f"Files: {[str(f) for f in all_files[:20]]}"
186
+ )
187
+
188
+ print(f"[pshuman] Output mesh: {obj_path}")
189
+ return obj_path
190
+
191
+
192
+ def generate_pshuman_mesh(
193
+ image_path: str,
194
+ output_path: str,
195
+ service_url: str = _DEFAULT_URL,
196
+ timeout: float = 600.0,
197
+ ) -> str:
198
+ """
199
+ Generate a PSHuman face mesh and save it to *output_path*.
200
+
201
+ When service_url points to localhost, PSHuman inference.py is run directly
202
+ (no Gradio HTTP, avoids gradio_client API-info bug).
203
+ For remote URLs, gradio_client is used.
204
+
205
+ Parameters
206
+ ----------
207
+ image_path : local PNG/JPG path of the portrait
208
+ output_path : where to save the downloaded OBJ
209
+ service_url : base URL of pshuman_app.py, or "direct" to skip HTTP
210
+ timeout : seconds to wait for inference (used in remote mode)
211
+
212
+ Returns
213
+ -------
214
+ output_path (convenience)
215
+ """
216
+ import tempfile
217
+
218
+ output_path = str(output_path)
219
+ os.makedirs(Path(output_path).parent, exist_ok=True)
220
+
221
+ is_local = (
222
+ "localhost" in service_url
223
+ or "127.0.0.1" in service_url
224
+ or service_url.strip().lower() == "direct"
225
+ or not service_url.strip()
226
+ )
227
+
228
+ if is_local:
229
+ # ── Direct mode: run subprocess ───────────────────────────────────────
230
+ print(f"[pshuman] Direct mode (no HTTP) β€” running inference on {image_path}")
231
+ work_dir = tempfile.mkdtemp(prefix="pshuman_direct_")
232
+ obj_tmp = _run_pshuman_direct(image_path, work_dir)
233
+ else:
234
+ # ── Remote mode: call Gradio service ──────────────────────────────────
235
+ try:
236
+ from gradio_client import Client
237
+ except ImportError:
238
+ raise ImportError("pip install gradio-client")
239
+
240
+ print(f"[pshuman] Connecting to {service_url}")
241
+ client = Client(service_url)
242
+
243
+ print(f"[pshuman] Submitting: {image_path}")
244
+ result = client.predict(
245
+ image=image_path,
246
+ api_name="/gradio_generate_face",
247
+ )
248
+
249
+ if isinstance(result, (list, tuple)):
250
+ obj_tmp = result[0]
251
+ status = result[1] if len(result) > 1 else "ok"
252
+ elif isinstance(result, dict):
253
+ obj_tmp = result.get("obj_path") or result.get("value")
254
+ status = result.get("status", "ok")
255
+ else:
256
+ obj_tmp = result
257
+ status = "ok"
258
+
259
+ if not obj_tmp or "Error" in str(status):
260
+ raise RuntimeError(f"PSHuman service error: {status}")
261
+
262
+ if isinstance(obj_tmp, dict):
263
+ obj_tmp = obj_tmp.get("path") or obj_tmp.get("name") or str(obj_tmp)
264
+
265
+ work_dir = str(Path(str(obj_tmp)).parent)
266
+
267
+ # ── Copy OBJ + companions to output location ───────────────────────────
268
+ shutil.copy(str(obj_tmp), output_path)
269
+ print(f"[pshuman] Saved OBJ -> {output_path}")
270
+
271
+ src_dir = Path(str(obj_tmp)).parent
272
+ out_dir = Path(output_path).parent
273
+ for ext in ("*.mtl", "*.png", "*.jpg"):
274
+ for f in src_dir.glob(ext):
275
+ dest = out_dir / f.name
276
+ if not dest.exists():
277
+ shutil.copy(str(f), str(dest))
278
+
279
+ return output_path
280
+
281
+
282
+ # ---------------------------------------------------------------------------
283
+ # CLI
284
+ # ---------------------------------------------------------------------------
285
+
286
+ def main():
287
+ parser = argparse.ArgumentParser(
288
+ description="Generate PSHuman face mesh from portrait image"
289
+ )
290
+ parser.add_argument("--image", required=True, help="Portrait image path")
291
+ parser.add_argument("--output", required=True, help="Output OBJ path")
292
+ parser.add_argument(
293
+ "--url", default=_DEFAULT_URL,
294
+ help="PSHuman service URL, or 'direct' to run inference locally "
295
+ "(default: http://localhost:7862 β†’ auto-selects direct mode)",
296
+ )
297
+ parser.add_argument("--timeout", type=float, default=600.0)
298
+ args = parser.parse_args()
299
+
300
+ generate_pshuman_mesh(
301
+ image_path = args.image,
302
+ output_path = args.output,
303
+ service_url = args.url,
304
+ timeout = args.timeout,
305
+ )
306
+
307
+
308
+ if __name__ == "__main__":
309
+ main()