Face2Mesh / app.py
ELumya's picture
feat: use api instead
557a557
Raw
History Blame Contribute Delete
12.8 kB
"""
Portrait to 3D Pipeline β€” Hugging Face Space
Step 1 : FLUX.2 [dev] API β†’ repositions portrait to 3/4 face + white studio lighting
Step 2 : Hunyuan3D-2.1 (hy3dshape) β†’ generates a 3D model (GLB)
"""
import os
import sys
import io
import time
import base64
import random
import traceback
from pathlib import Path
import spaces
import gradio as gr
import torch
import numpy as np
from PIL import Image
OUTPUT_DIR = Path("/tmp/face2mesh_outputs")
OUTPUT_DIR.mkdir(exist_ok=True)
MAX_SEED = np.iinfo(np.int32).max
# ──────────────────────────────────────────────────────────────
# Default prompts
# ──────────────────────────────────────────────────────────────
EDIT_PROMPT = (
"Turn the person faces for a 3/4 face portrait, "
"maintaining exact body proportions and facial identity. "
"Apply clean white studio lighting coming from the upper right, "
"with soft shadows on the left side. "
"Change the background to light grey. "
"Do not alter clothing, hairstyle or skin tone. "
"In a 3d volumetric style."
)
# ──────────────────────────────────────────────────────────────
# FLUX.2 via Gradio API
# ──────────────────────────────────────────────────────────────
def run_flux_edit(
input_image: Image.Image,
prompt: str,
seed: int,
guidance_scale: float,
num_steps: int,
prompt_upsampling: bool,
progress,
) -> Image.Image:
"""
Call the black-forest-labs/FLUX.2-dev Gradio Space API.
Replaces the previous local pipeline + remote text encoder approach.
"""
from gradio_client import Client, handle_file
# Save the input PIL image to a temp file so handle_file can reference it
ts = int(time.time())
tmp_input_path = str(OUTPUT_DIR / f"flux_input_{ts}.png")
input_image.save(tmp_input_path)
# Compute output dimensions matching input aspect ratio
orig_w, orig_h = input_image.size
aspect = orig_w / orig_h
if aspect >= 1:
out_w = 1024
out_h = int(1024 / aspect)
else:
out_h = 1024
out_w = int(1024 * aspect)
out_w = max(256, min(1024, round(out_w / 8) * 8))
out_h = max(256, min(1024, round(out_h / 8) * 8))
progress(0.20, desc="[Step 1/2] Calling FLUX.2 API…")
client = Client("black-forest-labs/FLUX.2-dev")
result = client.predict(
prompt=prompt,
input_images=[
{
"image": handle_file(tmp_input_path),
"caption": None,
}
],
seed=int(seed),
randomize_seed=False,
width=out_w,
height=out_h,
num_inference_steps=int(num_steps),
guidance_scale=float(guidance_scale),
prompt_upsampling=prompt_upsampling,
api_name="/infer",
)
# result[0] is the image dict; result[1] is the used seed
image_info = result[0]
# The API can return a local path or a URL
if image_info.get("path"):
edited_image = Image.open(image_info["path"]).convert("RGB")
elif image_info.get("url"):
import urllib.request
tmp_out_path = str(OUTPUT_DIR / f"flux_output_{ts}.png")
urllib.request.urlretrieve(image_info["url"], tmp_out_path)
edited_image = Image.open(tmp_out_path).convert("RGB")
else:
raise ValueError(f"FLUX.2 API returned unexpected image info: {image_info}")
return edited_image
# ──────────────────────────────────────────────────────────────
# Hunyuan3D-2.1 globals
# ──────────────────────────────────────────────────────────────
_hunyuan_pipeline = None
def get_hunyuan_pipeline():
global _hunyuan_pipeline
if _hunyuan_pipeline is None:
repo_root = Path(__file__).parent
for sub in ("hy3dshape", "hy3dpaint"):
p = str(repo_root / sub)
if p not in sys.path:
sys.path.insert(0, p)
from hy3dshape.pipelines import Hunyuan3DDiTFlowMatchingPipeline
print("[INFO] Loading Hunyuan3D-2.1…")
_hunyuan_pipeline = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained(
"tencent/Hunyuan3D-2.1",
subfolder="hunyuan3d-dit-v2-1",
use_safetensors=False,
device="cuda",
)
print("[INFO] Hunyuan3D-2.1 ready.")
return _hunyuan_pipeline
# ──────────────────────────────────────────────────────────────
# Step 2 β€” Hunyuan3D-2.1: image β†’ GLB mesh
# ──────────────────────────────────────────────────────────────
@spaces.GPU
def run_hunyuan(
edited_image: Image.Image,
num_steps: int,
guidance_scale: float,
octree_resolution: int,
seed: int,
) -> str:
from hy3dshape.rembg import BackgroundRemover
rmbg_worker = BackgroundRemover()
pipe = get_hunyuan_pipeline()
ts = int(time.time())
out_dir = OUTPUT_DIR / str(ts)
out_dir.mkdir(parents=True, exist_ok=True)
glb_path = str(out_dir / "model.glb")
with torch.inference_mode():
mesh = pipe(
image=rmbg_worker(edited_image),
num_inference_steps=num_steps,
guidance_scale=guidance_scale,
octree_resolution=octree_resolution,
generator=torch.manual_seed(seed),
output_type="trimesh",
)[0]
mesh.export(glb_path)
return glb_path
# ──────────────────────────────────────────────────────────────
# Full pipeline orchestration
# ──────────────────────────────────────────────────────────────
def full_pipeline(
input_image,
edit_prompt,
flux_seed,
flux_guidance,
flux_steps,
flux_prompt_upsampling,
hy_steps,
hy_guidance,
hy_octree_res,
hy_seed,
skip_flux,
progress=gr.Progress(track_tqdm=True),
):
if input_image is None:
raise gr.Error("Please upload a portrait image.")
logs = []
try:
# ── Step 1: FLUX.2 API edit ────────────────────────────
if skip_flux:
progress(0.20, desc="[Step 1 skipped] Using original image")
edited_image = input_image
logs.append("⏭ Step 1 skipped β€” original image passed to Hunyuan3D.")
else:
progress(0.05, desc="[Step 1/2] FLUX.2 API editing…")
logs.append("🎨 FLUX.2 [dev] API β€” repositioning portrait + studio lighting…")
edited_image = run_flux_edit(
input_image=input_image,
prompt=edit_prompt,
seed=int(flux_seed),
guidance_scale=float(flux_guidance),
num_steps=int(flux_steps),
prompt_upsampling=flux_prompt_upsampling,
progress=progress,
)
logs.append("βœ… Step 1 done.")
progress(0.45, desc="[Step 1/2] Editing complete")
# Save intermediate result
ts = int(time.time())
edited_path = str(OUTPUT_DIR / f"edited_{ts}.png")
edited_image.save(edited_path)
# ── Step 2: Hunyuan3D-2.1 ─────────────────────────────
progress(0.50, desc="[Step 2/2] Generating 3D model…")
logs.append("🧊 Hunyuan3D-2.1 β€” generating 3D mesh…")
glb_path = run_hunyuan(
edited_image=edited_image,
num_steps=int(hy_steps),
guidance_scale=float(hy_guidance),
octree_resolution=int(hy_octree_res),
seed=int(hy_seed),
)
logs.append("βœ… Step 2 done.")
logs.append(f" β€’ GLB: {glb_path}")
progress(1.0, desc="Pipeline complete βœ“")
return edited_image, glb_path, "\n".join(logs)
except Exception as e:
logs.append(f"❌ Error: {e}\n\n{traceback.format_exc()}")
raise gr.Error(str(e))
# ──────────────────────────────────────────────────────────────
# Gradio UI
# ──────────────────────────────────────────────────────────────
with gr.Blocks(
title="Portrait β†’ 3D Studio",
theme=gr.themes.Soft(primary_hue="violet"),
) as demo:
gr.Markdown(
"""
## πŸ§‘β€πŸŽ¨ Portrait β†’ 3D Studio
**Step 1** β€” FLUX.2 [dev] reshapes the portrait to a 3/4 angle with studio lighting.
**Step 2** β€” Hunyuan3D-2.1 converts the edited image into a 3D GLB mesh.
"""
)
with gr.Row():
# ── Left column: inputs ───────────────────────────────
with gr.Column(scale=1):
gr.Markdown("### πŸ“· Source image")
input_image = gr.Image(type="pil", label="Portrait photo", height=320)
with gr.Accordion("βš™οΈ FLUX.2 settings (Step 1)", open=False):
skip_flux = gr.Checkbox(
label="Skip FLUX.2 step (use image as-is)",
value=False,
)
edit_prompt = gr.Textbox(
label="Edit prompt",
value=EDIT_PROMPT,
lines=5,
)
flux_prompt_upsampling = gr.Checkbox(
label="Prompt upsampling (built-in FLUX.2 refinement)",
value=True,
info="Lets the FLUX.2 API refine the prompt internally before generation.",
)
with gr.Row():
flux_seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
flux_steps = gr.Slider(10, 60, value=30, step=1, label="Steps")
flux_guidance = gr.Slider(
0.0, 10.0, value=4.0, step=0.1, label="Guidance scale"
)
with gr.Accordion("βš™οΈ Hunyuan3D-2.1 settings (Step 2)", open=False):
with gr.Row():
hy_steps = gr.Slider(10, 50, value=30, step=1, label="DiT steps")
hy_guidance = gr.Slider(
1.0, 10.0, value=5.5, step=0.5, label="Guidance scale"
)
with gr.Row():
hy_octree_res = gr.Slider(
256, 512, value=380, step=1, label="Octree resolution"
)
hy_seed = gr.Slider(0, 9999, value=0, step=1, label="3D seed")
run_btn = gr.Button("πŸš€ Run pipeline", variant="primary", size="lg")
# ── Right column: outputs ─────────────────────────────
with gr.Column(scale=1):
gr.Markdown("### πŸ–ΌοΈ Edited image (FLUX.2)")
edited_out = gr.Image(
label="3/4-face portrait β€” studio lighting", height=300
)
gr.Markdown("### 🧊 3D model (Hunyuan3D-2.1)")
glb_out = gr.File(label="GLB mesh (untextured)")
log_out = gr.Textbox(
label="πŸ“‹ Execution log", lines=8, interactive=False
)
run_btn.click(
fn=full_pipeline,
inputs=[
input_image,
edit_prompt,
flux_seed,
flux_guidance,
flux_steps,
flux_prompt_upsampling,
hy_steps,
hy_guidance,
hy_octree_res,
hy_seed,
skip_flux,
],
outputs=[edited_out, glb_out, log_out],
)
demo.launch()