Metadata-Version: 2.4
Name: gradio_mmd_viewer
Version: 0.0.21
Summary: Gradio custom component: 3D MMD model viewer powered by Babylon.js + babylon-mmd
Project-URL: repository, https://github.com/Winterreisender/gradio-mmd-viewer
Author-email: Winterreisender <winterreisender@foxmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: 3d-viewer,babylonjs,gradio-custom-component,miku-miku-dance,mmd
Classifier: Development Status :: 3 - Alpha
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.13
Requires-Dist: gradio<7.0,>=6.0
Requires-Dist: uvicorn>=0.51.0
Requires-Dist: websockets>=16.1
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Description-Content-Type: text/markdown

﻿# gradio_mmd_widget

> [!WARNING] Unewviewed AI code
> Unewviewed AI code still remains.

> A Gradio custom component for rendering MMD motion data in the browser with Babylon.js and babylon-mmd.

## Why this component?

- Play VMD motion files with a built-in MMD character
- Use it directly as a Gradio input or output component
- Great for demos, interactive storytelling, and lightweight 3D previews

## Installation

```bash
pip install gradio_mmd_viewer
```

## Quick start

```python
import gradio as gr
from gradio_mmd_viewer import MMDViewer

with gr.Blocks() as demo:
    gr.Markdown("## MMD Viewer")
    viewer = MMDViewer(
        value="asset/demo.vmd",
        label="MMD Viewer",
        scale=2,
        min_width=800,
    )

if __name__ == "__main__":
    demo.launch()
```

## User guide

### Component parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `value` | `str \| None` | `None` | VMD motion file path or URL |
| `model_url` | `str \| None` | `None` | PMX/BPMX model file path or URL. If `None`, uses the built-in model |
| `pmx_zip` | `str \| None` | `None` | Path to a ZIP containing a PMX model. Extracted automatically → `model_url` |
| `physics_enabled` | `bool` | `False` | Enable physics simulation for the model |
| `grey_mode` | `bool` | `False` | Render in flat grey (no textures) |
| `camera_position` | `tuple[float, float, float]` | `(0, 38, -25)` | Initial camera position (x, y, z) |
| `camera_rotation` | `tuple[float, float]` | `(0, 0)` | Initial camera rotation (alpha, beta) in radians |
| `audio_url` | `str \| None` | `None` | Audio file path for animation sync |

### 1. Built-in model + VMD motion

The simplest usage — uses the built-in MMD model and plays a VMD motion:

```python
import gradio as gr
from gradio_mmd_viewer import MMDViewer

with gr.Blocks() as demo:
    viewer = MMDViewer(
        value="asset/demo.vmd",
        label="MMD Viewer",
        scale=2,
        min_width=800,
    )

demo.launch()
```

### 2. Upload VMD motion dynamically

Use `gr.File` to let users upload `.vmd` files, then update the viewer via callback:

```python
import gradio as gr
from gradio_mmd_viewer import MMDViewer

def load_motion(vmd_file):
    if vmd_file is None:
        return gr.update()
    return gr.update(value=vmd_file)

with gr.Blocks() as demo:
    vmd_input = gr.File(label="Upload VMD", file_types=[".vmd"])
    viewer = MMDViewer(label="MMD Viewer", scale=2, min_width=800)
    vmd_input.change(fn=load_motion, inputs=vmd_input, outputs=viewer)

demo.launch()
```

### 3. Upload PMX model via ZIP

Users can upload a ZIP containing a PMX model (with textures). The component extracts it and serves the model + textures automatically:

```python
import gradio as gr
from gradio_mmd_viewer import MMDViewer, extract_pmx_zip

def load_model(pmx_zip):
    if pmx_zip is None:
        return gr.update()
    model_url = extract_pmx_zip(pmx_zip)
    return gr.update(model_url=model_url)

with gr.Blocks() as demo:
    zip_input = gr.File(label="Upload PMX ZIP", file_types=[".zip"])
    viewer = MMDViewer(label="MMD Viewer", scale=2, min_width=800)
    zip_input.change(fn=load_model, inputs=zip_input, outputs=viewer)

demo.launch()
```

> **ZIP structure**: The ZIP should contain a `.pmx` file and its texture directory (typically `Tex/`). The extractor searches recursively for the first `.pmx` file.

### 4. Full example: VMD + PMX ZIP + audio + camera control

```python
import gradio as gr
from gradio_mmd_viewer import MMDViewer, extract_pmx_zip

def update_viewer(vmd_file, pmx_zip, audio_file, physics, grey, cam_x, cam_y, cam_z):
    updates = {
        "physics_enabled": physics,
        "grey_mode": grey,
        "camera_position": (cam_x, cam_y, cam_z),
    }
    if vmd_file is not None:
        updates["value"] = vmd_file
    if pmx_zip is not None:
        updates["model_url"] = extract_pmx_zip(pmx_zip)
    if audio_file is not None:
        updates["audio_url"] = audio_file
    return gr.update(**updates)

with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column(scale=1):
            vmd_input = gr.File(label="VMD Motion", file_types=[".vmd"])
            pmx_input = gr.File(label="PMX Model (ZIP)", file_types=[".zip"])
            audio_input = gr.File(label="Audio", file_types=[".mp3", ".wav", ".ogg"])
            physics = gr.Checkbox(label="Physics", value=False)
            grey = gr.Checkbox(label="Grey Mode", value=False)
            cam_x = gr.Number(label="Camera X", value=0)
            cam_y = gr.Number(label="Camera Y", value=38)
            cam_z = gr.Number(label="Camera Z", value=-25)
            btn = gr.Button("Load", variant="primary")
        with gr.Column(scale=2):
            viewer = MMDViewer(
                label="3D Viewport",
                scale=1,
                min_width=600,
                camera_position=(0, 38, -25),
            )
    btn.click(
        fn=update_viewer,
        inputs=[vmd_input, pmx_input, audio_input, physics, grey, cam_x, cam_y, cam_z],
        outputs=viewer,
    )

demo.launch()
```

### Path handling

The component handles file paths automatically:

- **Relative paths** (e.g. `"asset/demo.vmd"`): passed through as-is, served by Gradio's asset system
- **Absolute paths** (e.g. `"C:/models/demo.vmd"`): auto-converted to `/gradio_api/file=...` URLs via `postprocess()`
- **ZIP extraction**: `extract_pmx_zip()` extracts to a temp cache directory, registers it via `gr.set_static_paths()`, and returns a servable `/gradio_api/file=...` URL

> **Note**: When using `gr.update()` in callbacks, absolute paths in `model_url` or `audio_url` are **not** auto-converted. Use `extract_pmx_zip()` for ZIP files — it returns the correct URL. For plain file paths, the `postprocess()` hook handles conversion on `value`, but for `model_url` and `audio_url` you should convert manually or pass them through the constructor.

## Demo

Run the bundled demo with:

```bash
python demo/app.py
```

## Development notes

Technical implementation details, architecture notes, build steps, and troubleshooting guidance are maintained in [AGENTS.md](AGENTS.md).
