mk3d / app.py
yongyeol's picture
Update app.py
7591227 verified
raw
history blame
7.79 kB
# ────────────────────────────────────────────────────────────────────────────
# app.py – Text ➜ 2D (FLUX-mini Kontext) ➜ 3D (Hunyuan3D-2)
# • Fits into 16 GB system RAM: 경량 모델 + lazy loading + offload
# • Requires: GPU (A10G 24 GB ideal, T4 16 GB OK with fp16)
# ────────────────────────────────────────────────────────────────────────────
import os
import tempfile
from typing import List, Tuple
import gradio as gr
import torch
from PIL import Image
from huggingface_hub import login
# ─────────────────────── Auth ───────────────────────
HF_TOKEN = os.getenv("HF_TOKEN")
if not HF_TOKEN:
raise RuntimeError(
"HF_TOKEN이 설정되지 않았습니다. Space Settings → Secrets에서 "
"HF_TOKEN=your_read_token 을 등록한 뒤 재시작하세요."
)
login(token=HF_TOKEN, add_to_git_credential=False)
# ─────────────────────── Device & dtype ───────────────────────
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
DTYPE = torch.float16 if torch.cuda.is_available() else torch.float32
# ─────────────────────── Lazy loaders ───────────────────────
from diffusers import FluxKontextPipeline, FluxPipeline
from accelerate import init_empty_weights, load_checkpoint_and_dispatch
# Global caches
kontext_pipe = None # type: FluxKontextPipeline | None
_text2img_pipe = None # type: FluxPipeline | None
shape_pipe = None
paint_pipe = None
MINI_KONTEXT_REPO = "black-forest-labs/FLUX.1-Kontext-mini"
MINI_T2I_REPO = "black-forest-labs/FLUX.1-mini"
HUNYUAN_REPO = "tencent/Hunyuan3D-2"
def load_kontext() -> FluxKontextPipeline:
global kontext_pipe
if kontext_pipe is None:
print("[+] Loading FLUX.1-Kontext-mini … (low_cpu_mem_usage)")
kontext_pipe = FluxKontextPipeline.from_pretrained(
MINI_KONTEXT_REPO,
torch_dtype=DTYPE,
device_map="auto",
low_cpu_mem_usage=True,
)
kontext_pipe.set_progress_bar_config(disable=True)
return kontext_pipe
def load_text2img() -> FluxPipeline:
"""Lazy-load light text→image model only when 필요."""
global _text2img_pipe
if _text2img_pipe is None:
print("[+] Loading FLUX.1-mini (text → image)…")
_text2img_pipe = FluxPipeline.from_pretrained(
MINI_T2I_REPO,
torch_dtype=DTYPE,
device_map="auto",
low_cpu_mem_usage=True,
)
_text2img_pipe.set_progress_bar_config(disable=True)
return _text2img_pipe
def load_hunyuan() -> tuple:
global shape_pipe, paint_pipe
if shape_pipe is None or paint_pipe is None:
print("[+] Loading Hunyuan3D-2 (shape & texture)…")
from hy3dgen.shapegen import Hunyuan3DDiTFlowMatchingPipeline
from hy3dgen.texgen import Hunyuan3DPaintPipeline
shape_pipe = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained(
HUNYUAN_REPO,
torch_dtype=DTYPE,
device_map="auto",
low_cpu_mem_usage=True,
)
shape_pipe.set_progress_bar_config(disable=True)
paint_pipe = Hunyuan3DPaintPipeline.from_pretrained(
HUNYUAN_REPO,
torch_dtype=DTYPE,
device_map="auto",
low_cpu_mem_usage=True,
)
paint_pipe.set_progress_bar_config(disable=True)
return shape_pipe, paint_pipe
# ───────────────────────────────────────────────
# Helper functions
# ───────────────────────────────────────────────
def generate_single_2d(prompt: str, image: Image.Image | None, guidance_scale: float) -> Image.Image:
kontext = load_kontext()
if image is None:
# 텍스트→이미지 : 경량 text2img 파이프라인 사용
t2i = load_text2img()
result = t2i(prompt=prompt, guidance_scale=guidance_scale).images[0]
else:
result = kontext(image=image, prompt=prompt, guidance_scale=guidance_scale).images[0]
return result
def generate_multiview(prompt: str, base_image: Image.Image, guidance_scale: float) -> List[Image.Image]:
kontext = load_kontext()
views = [
base_image,
kontext(image=base_image, prompt=f"{prompt}, left side view", guidance_scale=guidance_scale).images[0],
kontext(image=base_image, prompt=f"{prompt}, right side view", guidance_scale=guidance_scale).images[0],
kontext(image=base_image, prompt=f"{prompt}, back view", guidance_scale=guidance_scale).images[0],
]
return views # [front, left, right, back]
def build_3d_mesh(prompt: str, images: List[Image.Image]) -> str:
shape, paint = load_hunyuan()
single_or_multi = images if len(images) > 1 else images[0]
mesh = shape(image=single_or_multi, prompt=prompt)[0]
mesh = paint(mesh, image=single_or_multi)
tmpdir = tempfile.mkdtemp()
out_path = os.path.join(tmpdir, "mesh.glb")
mesh.export(out_path)
return out_path
# ──────────────────────────────── UI ────────────────────────────────
CSS = """footer {visibility:hidden;}"""
def workflow(prompt: str, input_image: Image.Image | None, multiview: bool, guidance_scale: float):
if not prompt:
raise gr.Error("프롬프트(설명)를 입력하세요 📌")
base_img = generate_single_2d(prompt, input_image, guidance_scale)
images = generate_multiview(prompt, base_img, guidance_scale) if multiview else [base_img]
model_path = build_3d_mesh(prompt, images)
return images, model_path, model_path
def build_ui():
with gr.Blocks(css=CSS, title="Text ➜ 2D ➜ 3D (mini)") as demo:
gr.Markdown("# 🌀 텍스트 → 2D → 3D 생성기 (경량 버전)")
gr.Markdown("Kontext-mini + Hunyuan3D-2. 16 GB RAM에서도 동작합니다.")
with gr.Row():
with gr.Column():
prompt = gr.Textbox(label="프롬프트 / 설명", placeholder="예: 파란 모자를 쓴 귀여운 로봇")
input_image = gr.Image(label="(선택) 편집할 참조 이미지", type="pil")
multiview = gr.Checkbox(label="멀티뷰(좌/우/후면 포함)", value=True)
guidance = gr.Slider(0.5, 7.5, 2.5, step=0.1, label="Guidance Scale")
run_btn = gr.Button("🚀 생성하기", variant="primary")
with gr.Column():
gallery = gr.Gallery(label="🎨 2D 결과", columns=2, height="auto")
model3d = gr.Model3D(label="🧱 3D 미리보기", clear_color=[1, 1, 1, 0])
download = gr.File(label="⬇️ GLB 다운로드")
run_btn.click(
fn=workflow,
inputs=[prompt, input_image, multiview, guidance],
outputs=[gallery, model3d, download],
api_name="generate",
scroll_to_output=True,
show_progress="full",
)
return demo
if __name__ == "__main__":
build_ui().queue(max_size=3, concurrency_count=1).launch()