"""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)) @spaces.GPU(duration=gpu_duration, size='xlarge') 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('''
SOLRICKS / LoRA MODELS

Bring your imagination to life in 3D.

Create 3D animation from a scene description or a starting image with MiniMax H3.

Text to VideoImage to Video 3dsrxNative audio
''') 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)