modelGen / app.py
NiranjanWaghmare's picture
3D identity GLB + orbit video (not stacked 2D stills)
94dee35 verified
Raw History Blame Contribute Delete
6.94 kB
"""Model Gen — photos of one adult → 3D identity mesh + orbit video."""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from PIL import Image
from modelgen.config import APP_NAME, ASSETS, DEFAULT_PORT, REJECTED_ASSETS, running_on_spaces
from modelgen.pipeline import build_model_and_video
try:
import spaces
except ImportError: # local / CI
class spaces: # type: ignore[no-redef]
@staticmethod
def GPU(*_args: Any, **_kwargs: Any):
def decorator(fn):
return fn
return decorator
def _patch_gradio_json_schema() -> None:
"""gradio_client 1.3 crashes when additionalProperties is a JSON-schema bool."""
import gradio_client.utils as gcu
_get_type = gcu.get_type
_convert = gcu._json_schema_to_python_type
def get_type(schema): # noqa: ANN001
if not isinstance(schema, dict):
return "any"
return _get_type(schema)
def convert(schema, defs): # noqa: ANN001
if isinstance(schema, bool):
return "Any"
return _convert(schema, defs)
gcu.get_type = get_type # type: ignore[method-assign]
gcu._json_schema_to_python_type = convert # type: ignore[method-assign]
_patch_gradio_json_schema()
def _as_images(files: Any) -> tuple[list[Image.Image], list[str]]:
if not files:
return [], []
if not isinstance(files, list):
files = [files]
images: list[Image.Image] = []
names: list[str] = []
for item in files:
path = item.name if hasattr(item, "name") else item
if not path:
continue
img = Image.open(path).convert("RGB")
images.append(img)
names.append(Path(str(path)).name)
return images, names
def _status_md(engine: str, warnings: list[str], n_photos: int) -> str:
lines = [
f"**3D engine:** {engine}",
f"**Photos used:** {n_photos}",
"**Outputs:** `identity.glb` (3D model) + orbit video of that mesh",
]
if warnings:
lines.append("")
lines.append("**Notes**")
lines.extend(f"- {w}" for w in warnings)
return "\n".join(lines)
EMPTY_STATUS = (
"Upload 2–8 photos of **one consenting adult** (front plus other angles). "
"Generate builds a **3D identity mesh** and an **orbit video of that mesh** — not a collage of stills. "
"This path runs on CPU so it still works when ZeroGPU quota is exhausted."
)
def run_generate(files, adult_ok, progress=None):
import gradio as gr
if not adult_ok:
raise gr.Error("Confirm that every photo is of a consenting adult (18+).")
images, names = _as_images(files)
if not images:
raise gr.Error("Upload at least one photo — a clear front view plus extra angles works best.")
if len(images) > 8:
raise gr.Error("Use at most 8 photos.")
if progress is not None:
progress(0.1, desc="Building 3D identity mesh…")
try:
bundle = build_model_and_video(images, names)
except gr.Error:
raise
except Exception as exc: # noqa: BLE001
raise gr.Error(f"Generation failed: {exc}") from exc
if progress is not None:
progress(1.0, desc="Done")
status = _status_md(bundle.engine, bundle.warnings, len(images))
return str(bundle.glb_path), str(bundle.video_path), status
def _license_markdown() -> str:
rows = [
"| Role | Source | License | Gated |",
"| --- | --- | --- | --- |",
]
for asset in ASSETS:
rows.append(
f"| {asset.role} | [`{asset.repo_id}`]({asset.source}) | {asset.license} | "
f"{'yes' if asset.gated else 'no'} |"
)
rejected = "\n".join(f"- **{a.repo_id}** ({a.license}): {a.notes}" for a in REJECTED_ASSETS)
return (
"### Models and assets (free / OSS)\n\n"
+ "\n".join(rows)
+ "\n\nNot used (license, gating, or not 3D):\n\n"
+ rejected
)
def _patch_gradio_localhost_probe() -> None:
import gradio.networking as networking
networking.url_ok = lambda _url: True # type: ignore[method-assign]
def _launch_demo(demo) -> None:
demo.queue(max_size=8, api_open=False)
_patch_gradio_localhost_probe()
if running_on_spaces():
server_name = os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0")
server_port = int(os.environ.get("GRADIO_SERVER_PORT") or os.environ.get("PORT") or "7860")
else:
server_name = "0.0.0.0"
server_port = int(os.environ.get("PORT", DEFAULT_PORT))
demo.launch(
server_name=server_name,
server_port=server_port,
share=False,
show_error=True,
show_api=False,
)
def build_demo():
import gradio as gr
css = """
.gradio-container {max-width: 1180px !important;}
footer {opacity: 0.7;}
"""
with gr.Blocks(title=APP_NAME, css=css, theme=gr.themes.Soft()) as demo:
gr.Markdown(
f"""
# {APP_NAME}
Photos of **one adult** → a **3D identity model** (GLB) → an **orbit video of that model**.
This is not a 2D collage. Extra angles improve the bust; the video camera flies around the mesh.
Geometry uses **Depth-Anything V2 Small** (Apache-2.0, ungated) when weights load, otherwise a CPU shape-from-shading relief. No InsightFace, no gated FLUX.1-dev, no ZeroGPU required for this slice.
"""
)
with gr.Row():
with gr.Column(scale=1):
files = gr.File(
label="Photos of one person (front + other angles / lighting)",
file_count="multiple",
file_types=["image"],
)
adult_ok = gr.Checkbox(
label="I confirm every photo is of a consenting adult (18+).",
value=False,
)
generate_btn = gr.Button("Build 3D model + orbit video", variant="primary")
with gr.Column(scale=1):
model3d = gr.Model3D(label="3D identity (GLB)")
video = gr.Video(label="Orbit video of the 3D model")
status = gr.Markdown(EMPTY_STATUS)
generate_btn.click(
fn=run_generate,
inputs=[files, adult_ok],
outputs=[model3d, video, status],
)
gr.Markdown(_license_markdown())
gr.Markdown(
"""
### Video pipeline
1. **Now:** orbit render of the generated GLB (same asset, moving camera).
2. **Next:** import `identity.glb` into Blender / Unreal, or use orbit frames as control for **CogVideoX-2b** (Apache-2.0).
Neural image-to-3D (TripoSR MIT, InstantMesh Apache) is stronger mesh quality but TripoSR is Hub-gated and InstantMesh is too heavy for a 60s ZeroGPU slot — documented, not loaded.
"""
)
return demo
def main() -> None:
_launch_demo(build_demo())
if __name__ == "__main__":
main()