import gradio as gr import os import base64 from io import BytesIO from PIL import Image import tempfile from tools import generate_pixel_character, animate_pixel_character, extract_sprite_frames # --- Helper Functions for Gradio Logic --- def process_generate_sprite(prompt, ref_img): """ Generates a static 2D sprite character based on a text description in any art style. Args: prompt: Description of the character and style (e.g., "A cute cat wizard, cartoon style" or "anime cat hero"). ref_img: Optional reference image to influence style. Returns: The generated sprite image and its base64 encoding. """ try: ref_b64 = None if ref_img is not None: # Convert numpy array or PIL image to base64 if isinstance(ref_img, str): # path with open(ref_img, "rb") as f: ref_b64 = base64.b64encode(f.read()).decode('utf-8') elif hasattr(ref_img, "save"): # PIL Image buffered = BytesIO() ref_img.save(buffered, format="PNG") ref_b64 = base64.b64encode(buffered.getvalue()).decode('utf-8') b64_result = generate_pixel_character(prompt, ref_b64) # Convert back to PIL for display img_data = base64.b64decode(b64_result) return Image.open(BytesIO(img_data)), b64_result except Exception as e: raise gr.Error(str(e)) def process_animate_sprite(sprite_img, animation_type, extra_prompt): """ Animates a static 2D sprite using Google's Veo model. Args: sprite_img: The input static sprite image. animation_type: Type of animation - one of "idle", "walk", "run", "jump". extra_prompt: Optional additional instructions for the motion. Returns: The generated animation video path and its base64 encoding. """ try: if sprite_img is None: raise ValueError("Please provide a sprite image first.") # Convert input image to base64 sprite_b64 = None if isinstance(sprite_img, str): # path provided by Gradio example or upload with open(sprite_img, "rb") as f: sprite_b64 = base64.b64encode(f.read()).decode('utf-8') elif hasattr(sprite_img, "save"): # PIL Image buffered = BytesIO() sprite_img.save(buffered, format="PNG") sprite_b64 = base64.b64encode(buffered.getvalue()).decode('utf-8') elif isinstance(sprite_img, tuple): # Sometimes Gradio returns (path, meta) # Handle other formats if necessary pass # If sprite_b64 is still None (e.g. numpy array), try to convert if sprite_b64 is None: # Assuming numpy array -> PIL -> Base64 im = Image.fromarray(sprite_img) buffered = BytesIO() im.save(buffered, format="PNG") sprite_b64 = base64.b64encode(buffered.getvalue()).decode('utf-8') video_b64 = animate_pixel_character(sprite_b64, animation_type, extra_prompt) # Save to temp file for Gradio to display video_bytes = base64.b64decode(video_b64) with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as f: f.write(video_bytes) video_path = f.name return video_path, video_b64 except Exception as e: raise gr.Error(str(e)) def process_extract_frames(video_file, fps): """ Extracts frames from an MP4 video animation and returns them as individual images and a ZIP file. Args: video_file: The input MP4 video file path. fps: Frames per second to extract (default 8). Returns: A gallery of extracted frames and a ZIP file containing all frames. """ try: if video_file is None: raise ValueError("Please upload a video file.") # Read video file to base64 with open(video_file, "rb") as f: video_b64 = base64.b64encode(f.read()).decode('utf-8') zip_b64, frames_b64 = extract_sprite_frames(video_b64, fps) # Save zip to temp file with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as f: f.write(base64.b64decode(zip_b64)) zip_path = f.name # Convert frames to gallery format (list of paths or PIL images) gallery_images = [] for fb64 in frames_b64: img_data = base64.b64decode(fb64) gallery_images.append(Image.open(BytesIO(img_data))) return gallery_images, zip_path except Exception as e: raise gr.Error(str(e)) # --- Gradio UI Layout --- with gr.Blocks(title="GameSmith AI - Game Asset Studio") as demo: gr.Markdown( """ # 🎮 GameSmith AI ### The Intelligent Game Asset Studio Generate, animate, and export 2D game sprites in any art style using Google Gemini & Veo. *Built for the Hugging Face MCP 1st Birthday Hackathon.* """ ) with gr.Tab("1. Generate Sprite"): with gr.Row(): with gr.Column(): prompt_input = gr.Textbox( label="Character Description", placeholder="A cute cat wearing a wizard hat, side view... (cartoon, anime, pixel art, etc.)", lines=3 ) ref_input = gr.Image(label="Style Reference (Optional)", type="pil") gen_btn = gr.Button("Generate Sprite", variant="primary") with gr.Column(): result_image = gr.Image(label="Generated Sprite", type="pil", interactive=False) # Hidden state to pass base64 to next tab if needed sprite_b64_state = gr.State() gen_btn.click( process_generate_sprite, inputs=[prompt_input, ref_input], outputs=[result_image, sprite_b64_state], api_name="generate_pixel_character" ) with gr.Tab("2. Animate"): with gr.Row(): with gr.Column(): # Allow user to use generated image or upload new anim_input_image = gr.Image(label="Input Sprite", type="pil") anim_type = gr.Dropdown( choices=["idle", "walk", "run", "jump"], value="idle", label="Animation Type" ) extra_anim_prompt = gr.Textbox( label="Motion Tweaks (Optional)", placeholder="Make it bounce more..." ) anim_btn = gr.Button("Animate", variant="primary") with gr.Column(): result_video = gr.Video(label="Generated Animation", interactive=False) video_b64_state = gr.State() # Link previous tab result to this input result_image.change( lambda x: x, inputs=[result_image], outputs=[anim_input_image] ) anim_btn.click( process_animate_sprite, inputs=[anim_input_image, anim_type, extra_anim_prompt], outputs=[result_video, video_b64_state], api_name="animate_pixel_character" ) with gr.Tab("3. Extract Frames"): with gr.Row(): with gr.Column(): # Allow user to use generated video or upload new extract_input_video = gr.Video(label="Input Animation") fps_slider = gr.Slider(minimum=4, maximum=24, value=8, step=1, label="FPS") extract_btn = gr.Button("Extract Frames", variant="primary") with gr.Column(): frames_gallery = gr.Gallery(label="Sprite Sheet Frames") download_zip = gr.File(label="Download Sprite Sheet (ZIP)") # Link previous tab result result_video.change( lambda x: x, inputs=[result_video], outputs=[extract_input_video] ) extract_btn.click( process_extract_frames, inputs=[extract_input_video, fps_slider], outputs=[frames_gallery, download_zip], api_name="extract_sprite_frames" ) gr.Markdown("---") gr.Markdown("### 🤖 Model Context Protocol (MCP)") gr.Markdown( """ This app doubles as an MCP Server! Connect it to Claude or Cursor to generate assets directly in your chat. **Tools Exposed:** - `generate_pixel_character(prompt)` - `animate_pixel_character(sprite_b64, animation_type)` - `extract_sprite_frames(video_b64)` """ ) if __name__ == "__main__": # Launch the app # MCP server is auto-enabled via GRADIO_MCP_SERVER env var or newer Gradio versions demo.launch()