Spaces:
Running on Zero
Running on Zero
Download app.py from SOLRICKS/3D-Animation-Style-MiniMax-H3: direct link, hf CLI and curl.
- Browser
- Download file 20.2 kB
-
https://huggingface.co/spaces/SOLRICKS/3D-Animation-Style-MiniMax-H3/resolve/main/app.py
- Command line
-
hf download hf://spaces/SOLRICKS/3D-Animation-Style-MiniMax-H3/app.py
-
curl -L -o app.py https://huggingface.co/spaces/SOLRICKS/3D-Animation-Style-MiniMax-H3/resolve/main/app.py
20.2 kB
| """SOLRICKS 3D Animation Style for MiniMax H3 — ZeroGPU T2V and I2V demo.""" | |
| from __future__ import annotations | |
| print('[boot] app.py started', flush=True) | |
| import faulthandler | |
| faulthandler.enable() | |
| faulthandler.dump_traceback_later(180, repeat=True) | |
| import os | |
| import secrets | |
| import tempfile | |
| import traceback | |
| from pathlib import Path | |
| # Must precede torch/diffusers: ZeroGPU intercepts startup CUDA allocations. | |
| print('[boot] importing ZeroGPU runtime', flush=True) | |
| import spaces | |
| print('[boot] ZeroGPU runtime imported; importing Gradio', flush=True) | |
| import gradio as gr | |
| print('[boot] Gradio imported', flush=True) | |
| from h3_helpers import CANVASES, DEFAULT_CANVAS, FPS, frame_count, prepare_image, prepare_prompt | |
| MODEL_REPO = 'MiniMaxAI/MiniMax-H3' | |
| STYLE_REPO = 'SOLRICKS/3D-Animation-Style-MiniMax-H3' | |
| STYLE_FILE = '3dsrx_minimax_h3_rank16-step00001250.safetensors' | |
| TURBO_REPO = 'lightx2v/Minimax-h3-Turbo' | |
| TURBO_FILE = 'minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors' | |
| CONDITIONER = os.getenv('H3_CONDITIONER', 'multimodalart/qwen3vl-conditioner') | |
| OUTPUT_DIR = Path(tempfile.gettempdir()) / 'solricks-3dsrx-outputs' | |
| OUTPUT_DIR.mkdir(exist_ok=True) | |
| PIPE = None | |
| LOAD_ERROR = '' | |
| def load_models(): | |
| global PIPE, LOAD_ERROR | |
| try: | |
| print('[load 1/7] importing adapter tools', flush=True) | |
| from huggingface_hub import hf_hub_download | |
| from h3_adapters import read_adapter, install_adapter | |
| # Check gated access before downloading the large base. | |
| print('[load 2/7] downloading or opening 3dsrx style checkpoint', flush=True) | |
| style = read_adapter(hf_hub_download(STYLE_REPO, STYLE_FILE, token=os.getenv('HF_TOKEN'))) | |
| print('[load 3/7] style adapter prepared; downloading or opening Turbo checkpoint', flush=True) | |
| turbo = read_adapter(hf_hub_download(TURBO_REPO, TURBO_FILE, token=False)) | |
| print('[load 4/7] adapters prepared; importing H3 pipeline', flush=True) | |
| import torch | |
| from diffusers import ComponentsManager | |
| from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline | |
| from h3_split_blocks import AnimationGeneratorBlocks | |
| MiniMaxH3ModularPipeline.min_duration = property(lambda self: 4.0) | |
| manager = ComponentsManager() | |
| pipe = AnimationGeneratorBlocks().init_pipeline( | |
| MODEL_REPO, components_manager=manager, collection='solricks-h3') | |
| print('[load 5/7] downloading and loading H3 transformer and VAEs (about 77 GB on a cold cache)', flush=True) | |
| pipe.load_components(dtype=torch.bfloat16) | |
| print('[3dsrx] installing Turbo adapter', flush=True) | |
| install_adapter(pipe.transformer, turbo, 'turbo') | |
| print('[3dsrx] installing 3D Animation Style adapter', flush=True) | |
| install_adapter(pipe.transformer, style, 'animation') | |
| pipe.transformer.set_adapters(['turbo', 'animation'], weights=[1.0, 0.8]) | |
| pipe.transformer.set_attention_backend('_native_cudnn') | |
| # Pack only the transformer, keeping duplicated tensors under the disk quota. | |
| print('[load 6/7] preparing transformer for ZeroGPU', flush=True) | |
| pipe.transformer.to('cuda') | |
| PIPE = pipe | |
| print('[load 7/7] models ready', flush=True) | |
| except Exception as error: | |
| from huggingface_hub.errors import GatedRepoError | |
| traceback.print_exc() | |
| if isinstance(error, GatedRepoError): | |
| LOAD_ERROR = ('Model access is required. The Space owner needs to add an HF_TOKEN ' | |
| 'secret with read access to the 3D Animation Style model, then restart the Space.') | |
| else: | |
| LOAD_ERROR = 'The generation model could not start. The Space owner can check the runtime logs.' | |
| def gpu_duration(prompt_embeds, tags, image, height, width, frames, seed, strength): | |
| latent_frames = (frames - 5) // 17 * 5 + 2 | |
| patches = (height // 32) * (width // 32) | |
| rows = latent_frames * patches + int(image is not None) * patches | |
| denoise_time = 8 * (1.1745e-4 * rows + 3.8396e-9 * rows ** 2) | |
| return max(90, int(denoise_time * 1.8 + 65)) | |
| def denoise(prompt_embeds, tags, image, height, width, frames, seed, strength): | |
| import torch | |
| with torch.inference_mode(): | |
| PIPE.vae.to('cuda') | |
| PIPE.audio_vae.to('cuda') | |
| PIPE.transformer.set_adapters(['turbo', 'animation'], weights=[1.0, float(strength)]) | |
| result = PIPE(prompt_embeds=prompt_embeds.to('cuda'), text_token_tags=tags, image=image, | |
| height=int(height), width=int(width), num_frames=int(frames), num_inference_steps=8, | |
| generator=torch.Generator('cpu').manual_seed(int(seed))) | |
| return result.get('videos')[0], result.get('audio')[0].cpu(), result.get('sampling_rate') | |
| def generate(mode, prompt, image, canvas, duration, strength, seed, randomize, | |
| request: gr.Request, progress=gr.Progress()): | |
| if LOAD_ERROR or PIPE is None: | |
| raise gr.Error(LOAD_ERROR or 'The generation model is still loading.') | |
| try: | |
| full_prompt = prepare_prompt(prompt) | |
| frames = frame_count(duration) | |
| if canvas not in CANVASES: | |
| raise ValueError('Choose one of the available video formats.') | |
| if not 0 <= float(strength) <= 1.2: | |
| raise ValueError('LoRA strength must be between 0 and 1.2.') | |
| seed = secrets.randbelow(2 ** 31) if randomize else int(seed) | |
| if not 0 <= seed < 2 ** 31: | |
| raise ValueError('Seed must be between 0 and 2147483647.') | |
| if mode not in ('Text to Video', 'Image to Video'): | |
| raise ValueError('Choose Text to Video or Image to Video.') | |
| if mode == 'Image to Video' and not image: | |
| raise ValueError('Upload a starting image for Image to Video.') | |
| except (ValueError, TypeError) as error: | |
| raise gr.Error(str(error)) from error | |
| from gradio_client import Client, handle_file | |
| from safetensors import safe_open | |
| from PIL import Image | |
| from diffusers.utils import encode_video | |
| # Keep the reference deployment's guard on image-conditioned requests. | |
| if mode == 'Image to Video': | |
| import ncii_guard | |
| if ncii_guard.classify(full_prompt)['label'] == 'ncii': | |
| raise gr.Error('This prompt was flagged by a content filter and was not run.') | |
| # Forward only the visitor's quota header, never the owner's HF_TOKEN. | |
| ip_token = request.headers.get('x-ip-token') if request is not None else None | |
| headers = {'x-ip-token': ip_token} if ip_token else {} | |
| try: | |
| with tempfile.TemporaryDirectory(prefix='3dsrx-input-') as directory: | |
| prepared = prepare_image(image, canvas, directory) if mode == 'Image to Video' else None | |
| progress(0.05, desc='Preparing your scene') | |
| client = Client(CONDITIONER, headers=headers, token=False) | |
| embedding_file, plan = client.predict( | |
| prompt=full_prompt, image_path=handle_file(prepared) if prepared else None, | |
| last_image_path=None, canvas=canvas, num_frames=frames, | |
| rewrite_prompt=False, api_name='/encode') | |
| with safe_open(embedding_file, framework='pt', device='cpu') as handle: | |
| meta = handle.metadata() | |
| embeds, tags = handle.get_tensor('prompt_embeds'), handle.get_tensor('text_token_tags') | |
| height, width, frames = (int(meta[key]) for key in ('height', 'width', 'num_frames')) | |
| first_frame = Image.open(prepared).convert('RGB') if prepared else None | |
| progress(0.25, desc='Generating 3D animation') | |
| video, audio, rate = denoise(embeds, tags, first_frame, height, width, frames, seed, strength) | |
| output = OUTPUT_DIR / f'3dsrx-{secrets.token_hex(12)}.mp4' | |
| progress(0.95, desc='Finishing your video') | |
| encode_video(video, fps=FPS, output_path=str(output), audio=audio, audio_sample_rate=rate) | |
| return str(output), f'{width} × {height} · {frames / FPS:.2f} s · LoRA {float(strength):.2f} · Seed {seed}', seed | |
| except gr.Error: | |
| raise | |
| except Exception as error: | |
| traceback.print_exc() | |
| if any(word in str(error).lower() for word in ('quota', 'gpu limit', 'allocate', 'capacity', 'concurrent', 'schedule')): | |
| raise gr.Error('ZeroGPU is busy or your GPU time is used up. Please try again later.') from error | |
| raise gr.Error('Generation could not finish. Please retry; the Space logs contain the error details.') from error | |
| # Match the Generate Visuals palette in both browser color schemes. | |
| SOLRICKS_COLORS = { | |
| 'body_background_fill': '#0a1020', | |
| 'body_text_color': '#f2f4f8', | |
| 'body_text_color_subdued': '#b8c2d4', | |
| 'background_fill_primary': '#0b1423', | |
| 'background_fill_secondary': '#18243a', | |
| 'border_color_primary': '#2c3b56', | |
| 'border_color_accent': '#7268ff', | |
| 'border_color_accent_subdued': '#3b456f', | |
| 'color_accent_soft': '#252d50', | |
| 'block_background_fill': 'transparent', | |
| 'block_border_color': 'transparent', | |
| 'block_info_text_color': '#b8c2d4', | |
| 'block_label_background_fill': 'transparent', | |
| 'block_label_border_color': 'transparent', | |
| 'block_label_text_color': '#c4ccda', | |
| 'block_title_background_fill': 'transparent', | |
| 'block_title_border_color': 'transparent', | |
| 'block_title_text_color': '#f2f4f8', | |
| 'block_shadow': 'none', | |
| 'panel_background_fill': '#101a2d', | |
| 'panel_border_color': '#2c3b56', | |
| 'input_background_fill': '#252d3d', | |
| 'input_background_fill_focus': '#252d3d', | |
| 'input_background_fill_hover': '#2b3549', | |
| 'input_border_color': '#424b5e', | |
| 'input_border_color_focus': '#8692ff', | |
| 'input_border_color_hover': '#59657c', | |
| 'input_placeholder_color': '#98a4b9', | |
| 'input_shadow': 'none', | |
| 'input_shadow_focus': '0 0 0 2px #7268ff30', | |
| 'checkbox_background_color': '#252d3d', | |
| 'checkbox_background_color_focus': '#343e58', | |
| 'checkbox_background_color_hover': '#343e58', | |
| 'checkbox_background_color_selected': '#7268ff', | |
| 'checkbox_border_color': '#56617a', | |
| 'checkbox_border_color_focus': '#8692ff', | |
| 'checkbox_border_color_hover': '#8692ff', | |
| 'checkbox_border_color_selected': '#7268ff', | |
| 'checkbox_label_background_fill': '#252d3d', | |
| 'checkbox_label_background_fill_hover': '#2b3549', | |
| 'checkbox_label_background_fill_selected': '#2d365e', | |
| 'checkbox_label_border_color': '#424b5e', | |
| 'checkbox_label_border_color_hover': '#677393', | |
| 'checkbox_label_border_color_selected': '#7d89f5', | |
| 'checkbox_label_text_color': '#c4ccda', | |
| 'checkbox_label_text_color_selected': '#ffffff', | |
| 'checkbox_label_shadow': 'none', | |
| 'accordion_text_color': '#f2f4f8', | |
| 'button_primary_background_fill': 'linear-gradient(90deg, #7268ff, #596dff)', | |
| 'button_primary_background_fill_hover': 'linear-gradient(90deg, #7a71ff, #6276ff)', | |
| 'button_primary_border_color': '#7268ff', | |
| 'button_primary_border_color_hover': '#9c9aff', | |
| 'button_primary_text_color': '#ffffff', | |
| 'button_primary_text_color_hover': '#ffffff', | |
| 'button_primary_shadow': 'none', | |
| 'button_secondary_background_fill': '#262d3c', | |
| 'button_secondary_background_fill_hover': '#2d3547', | |
| 'button_secondary_border_color': '#424b5e', | |
| 'button_secondary_border_color_hover': '#677393', | |
| 'button_secondary_text_color': '#f2f4f8', | |
| 'button_secondary_text_color_hover': '#ffffff', | |
| 'button_secondary_shadow': 'none', | |
| 'link_text_color': '#a8b7ff', | |
| 'link_text_color_active': '#d1d9ff', | |
| 'link_text_color_hover': '#d1d9ff', | |
| 'link_text_color_visited': '#a8b7ff', | |
| 'code_background_fill': '#252d3d', | |
| 'table_text_color': '#f2f4f8', | |
| 'table_border_color': '#3a455a', | |
| 'table_even_background_fill': '#18243a', | |
| 'table_odd_background_fill': '#101a2d', | |
| 'table_row_focus': '#2d365e', | |
| 'loader_color': '#8190ff', | |
| 'slider_color': '#7268ff', | |
| } | |
| SOLRICKS_THEME = gr.themes.Base( | |
| primary_hue='indigo', secondary_hue='blue', neutral_hue='slate', | |
| font=['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'], | |
| ).set( | |
| **SOLRICKS_COLORS, | |
| **{f'{key}_dark': value for key, value in SOLRICKS_COLORS.items()}, | |
| color_accent='#7268ff', block_radius='0px', input_radius='0px', | |
| container_radius='0px', button_large_radius='0px', | |
| button_medium_radius='0px', button_small_radius='0px', | |
| block_label_text_weight='600', block_label_shadow='none', | |
| shadow_drop='none', shadow_drop_lg='none', | |
| ) | |
| CSS = ''' | |
| html, body {margin:0;background:#0a1020!important;} | |
| .gradio-container { | |
| max-width:none!important;min-height:100dvh!important;margin:0!important; | |
| padding:36px 24px 24px!important;color:#f2f4f8!important; | |
| background: | |
| radial-gradient(circle at 18% 12%,rgba(32,107,194,.22),transparent 28%), | |
| radial-gradient(circle at 82% 16%,rgba(78,54,177,.20),transparent 34%), | |
| linear-gradient(135deg,#08131f 0%,#0a1020 50%,#15173b 100%)!important; | |
| } | |
| .gradio-container .main,.gradio-container main { | |
| max-width:1160px!important;width:100%;margin-inline:auto!important; | |
| } | |
| #hero,#h3-input-panel,#h3-output-panel { | |
| border:1px solid rgba(125,167,255,.17)!important;border-radius:0!important; | |
| background:linear-gradient(90deg,rgba(28,41,70,.84) 0%, | |
| rgba(10,18,33,.95) 42%,rgba(6,13,25,.99) 100%)!important; | |
| box-shadow:none!important; | |
| } | |
| #hero {padding:28px 30px;margin-bottom:2px;} | |
| #hero h1 { | |
| font-size:clamp(28px,4vw,40px);line-height:1.15;margin:12px 0; | |
| color:#f2f4f8;letter-spacing:-.035em;font-weight:800; | |
| } | |
| #hero p {font-size:15px;max-width:780px;line-height:1.65;color:#b8c2d4;} | |
| #hero .eyebrow {font-size:11px;letter-spacing:.18em;font-weight:700;color:#a8b7ff;} | |
| #hero .chips {display:flex;flex-wrap:wrap;gap:8px;margin-top:18px;} | |
| #hero .chips span { | |
| border:1px solid rgba(125,167,255,.24);background:rgba(77,107,160,.16); | |
| color:#ced8ef;border-radius:3px;padding:5px 11px;font-size:12px; | |
| } | |
| #h3-main-grid {gap:18px!important;} | |
| #h3-input-panel,#h3-output-panel {padding:24px!important;} | |
| #h3-output-panel {background:linear-gradient(90deg,rgba(28,41,70,.74),rgba(6,13,25,.99))!important;} | |
| #h3-input-panel .form,#h3-output-panel .form { | |
| background:transparent!important;border:0!important;box-shadow:none!important; | |
| } | |
| .gradio-container [data-testid="block-info"] { | |
| color:#c4ccda!important;background:transparent!important; | |
| font-size:12px!important;font-weight:600!important;letter-spacing:.035em; | |
| } | |
| #h3-prompt textarea,#h3-report textarea { | |
| background:#252d3d!important;color:#f2f4f8!important; | |
| border-radius:0!important;line-height:1.6!important; | |
| } | |
| #h3-prompt textarea {min-height:142px;padding:16px!important;} | |
| #h3-report textarea {color:#c4ccda!important;} | |
| #h3-video {background:rgba(4,10,20,.35)!important;border:1px solid #2c3b56!important;} | |
| #generate { | |
| min-height:50px;background:linear-gradient(90deg,#7268ff,#596dff)!important; | |
| color:#fff!important;border:1px solid rgba(114,104,255,.22)!important; | |
| border-radius:0!important;font-weight:700!important;box-shadow:none!important; | |
| } | |
| #generate:hover {background:linear-gradient(90deg,#7a71ff,#6276ff)!important;} | |
| #generate:focus-visible {outline:2px solid #b0b8ff;outline-offset:3px;} | |
| .gradio-container .prose {color:#b8c2d4;} | |
| .gradio-container .prose strong {color:#e5ebf5;} | |
| .gradio-container .prose a {color:#a8b7ff!important;} | |
| #model-status {font-size:13px;color:#b8c2d4;} | |
| @media (max-width:700px) { | |
| .gradio-container {padding:20px 12px!important;} | |
| #hero {padding:24px 20px;} | |
| #h3-input-panel,#h3-output-panel {padding:18px!important;} | |
| } | |
| .gradio-container footer, | |
| [aria-label="Gradio footer navigation"] { | |
| display: none !important; | |
| } | |
| ''' | |
| def change_mode(mode): | |
| is_image = mode == 'Image to Video' | |
| return gr.update(visible=is_image), 0.4 if is_image else 0.8 | |
| with gr.Blocks(title='3D Animation Style MiniMax H3', delete_cache=(3600, 3600)) as demo: | |
| gr.HTML('''<div id="hero"><div class="eyebrow">SOLRICKS / LoRA MODELS</div> | |
| <h1>Bring your imagination to life in 3D.</h1> | |
| <p>Create 3D animation from a scene description or a starting image | |
| with MiniMax H3.</p> | |
| <div class="chips"><span>Text to Video</span><span>Image to Video</span> | |
| <span>3dsrx</span><span>Native audio</span></div></div>''') | |
| with gr.Row(equal_height=True, elem_id='h3-main-grid'): | |
| with gr.Column(scale=1, elem_id='h3-input-panel'): | |
| mode = gr.Radio(['Text to Video', 'Image to Video'], value='Text to Video', label='Create with') | |
| image = gr.Image(type='filepath', label='Starting image', visible=False, height=280) | |
| prompt = gr.Textbox(label='Your scene', lines=5, elem_id='h3-prompt', | |
| value='A curious young girl in a mustard hoodie looks through a telescope on a rooftop at dusk. ' | |
| 'She slowly looks up at the stars and smiles. Soft twilight, expressive character animation, one continuous shot.', | |
| placeholder='Describe the character, setting, action and camera movement…') | |
| gr.Markdown('The **3dsrx** style trigger is added automatically.') | |
| with gr.Row(): | |
| canvas = gr.Dropdown(list(CANVASES), value=DEFAULT_CANVAS, label='Video format') | |
| duration = gr.Slider(4, 8, value=5, step=1, label='Duration (seconds)') | |
| with gr.Accordion('Style & Seed', open=False): | |
| strength = gr.Slider(0, 1.2, value=0.8, step=0.05, label='3D animation strength') | |
| gr.Markdown('Recommended: **0.8** for text, **0.4** for images. Try **1.0** for animals and fantasy scenes.') | |
| with gr.Row(): | |
| seed = gr.Number(value=42, precision=0, label='Seed', minimum=0, maximum=2147483647) | |
| randomize = gr.Checkbox(value=True, label='Random seed') | |
| button = gr.Button('Generate animation', variant='primary', size='lg', elem_id='generate') | |
| with gr.Column(scale=1, elem_id='h3-output-panel'): | |
| output = gr.Video(label='Your animation', height=420, autoplay=False, format='mp4', elem_id='h3-video') | |
| report = gr.Textbox(label='Generation details', interactive=False, elem_id='h3-report') | |
| gr.Markdown('**Tips:** Start with one subject and a simple action. For images, choose a matching video format; ' | |
| 'the starting frame is center-cropped to fit.') | |
| with gr.Accordion('Example scenes', open=False): | |
| gr.Examples([ | |
| ['A playful raccoon and a tiny hedgehog explore a sunlit garden. The raccoon carefully offers the hedgehog a red berry. Gentle camera push, warm light, expressive 3D character animation.'], | |
| ['A cheerful otter floats on its back in a calm river, holding a red apple with both paws. It takes a small bite, then glances toward the camera. Soft sunlight, detailed fur, one continuous shot.'], | |
| ['A woman in a cozy cafe holds a ceramic cup with both hands. She looks out the window and smiles softly. Warm morning light, shallow depth of field, subtle natural movement.'], | |
| ], inputs=[prompt], cache_examples=False) | |
| gr.Markdown(f'[Model & downloads](https://huggingface.co/{STYLE_REPO}) · ' | |
| 'Prompts and starting images are sent to the shared ' | |
| '[H3 conditioner](https://huggingface.co/spaces/multimodalart/qwen3vl-conditioner) for scene encoding.') | |
| model_status = gr.Markdown(elem_id='model-status') | |
| mode.change(change_mode, [mode], [image, strength], queue=False) | |
| button.click(generate, [mode, prompt, image, canvas, duration, strength, seed, randomize], | |
| [output, report, seed], concurrency_limit=1, api_name='generate') | |
| demo.load(lambda: LOAD_ERROR or '', outputs=model_status, queue=False) | |
| print('[boot] interface constructed', flush=True) | |
| if __name__ == '__main__': | |
| load_models() | |
| faulthandler.cancel_dump_traceback_later() | |
| print('[boot] launching Gradio server', flush=True) | |
| demo.queue(max_size=10, default_concurrency_limit=1).launch( | |
| theme=SOLRICKS_THEME, css=CSS, | |
| allowed_paths=[str(OUTPUT_DIR)], show_error=False) | |