Spaces:
Running
Running
| 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() | |