import gradio as gr import xml.etree.ElementTree as ET import re import anthropic import os from dotenv import load_dotenv from utils import svg_to_png_base64 import pathlib from logger import setup_logger load_dotenv(override=True) logger = setup_logger() class SVGAnimationGenerator: def __init__(self): self.client = None self.predict_decompose_group_prompt = self._get_prompt( "prompts/predict_decompose_group.txt" ) self.feedback_decompose_group_prompt = self._get_prompt( "prompts/feedback_decompose_group.txt" ) self.generate_animation_prompt = self._get_prompt( "prompts/generate_animation.txt" ) if "ANTHROPIC_API_KEY" in os.environ: self.client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) def _get_prompt(self, prompt_file_path: str) -> str: try: with open(prompt_file_path, "r", encoding="utf-8") as f: return f.read() except FileNotFoundError: return "Prompt file not found. Please check the path." def parse_svg(self, svg_content: str) -> dict: try: # Remove namespace definitions and simplify the tag svg_content = re.sub(r'xmlns[^=]*="[^"]*"', "", svg_content) svg_content = re.sub(r"]*>", "", svg_content) return {"svg_content": svg_content} except Exception as e: return {"error": f"SVG parsing error: {e}"} def predict_decompose_group(self, parsed_svg: dict, object_name: str) -> dict: try: svg_content = parsed_svg["svg_content"] # Convert SVG to PNG for MLLM analysis image_media_type, image_data = svg_to_png_base64(svg_content) if not image_data: return {"error": "Failed to convert SVG to PNG"} prompt = self.predict_decompose_group_prompt.format( object_name=object_name, svg_content=svg_content ) logger.info(f"Decomposition Prompt for {object_name}:\n{prompt}") response = self.client.messages.create( model="claude-sonnet-4-20250514", max_tokens=10000, messages=[ { "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": image_media_type, "data": image_data, }, }, {"type": "text", "text": prompt}, ], }, { "role": "assistant", "content": [ { "type": "text", "text": "" } ] } ], ) response_text = response.content[0].text logger.info(f"Model Response:\n{response_text}") decomposed_svg_match = re.search( r"(.*?)", response_text, re.DOTALL ) animation_suggenstions_match = re.search( r"(.*?)", response_text, re.DOTALL, ) print( "[SVG Decompose] Decomposed SVG found", decomposed_svg_match is not None ) if decomposed_svg_match and animation_suggenstions_match: decomposed_svg_text = decomposed_svg_match.group(1).strip() animation_suggestions = animation_suggenstions_match.group(1).strip() print("[SVG Decompose] Animation suggestions found") return { "svg_content": decomposed_svg_text, "animation_suggestions": animation_suggestions, } else: return { "error": "Decomposed SVG and Animation Suggestion not found in response." } except Exception as e: return {"error": f"Error during MLLM prediction: {e}"} def feedback_decompose_group(self, svg_content: str, feedback: str) -> tuple: try: # Parse the SVG content first parsed_svg = self.parse_svg(svg_content) # Remove ["svg_content"] access if "error" in parsed_svg: error_message = parsed_svg["error"] error_html = create_error_html(error_message) return "", "", error_html # Return tuple of 3 values prompt = self.feedback_decompose_group_prompt.format( parsed_svg=parsed_svg, feedback=feedback ) logger.info(f"Feedback Prompt:\n{prompt}") response = self.client.messages.create( model="claude-sonnet-4-20250514", max_tokens=10000, messages=[ { "role": "user", "content": [ {"type": "text", "text": prompt}, ], } ], ) response_text = response.content[0].text logger.info(f"Model Response:\n{response_text}") decomposed_svg_match = re.search( r"(.*?)", response_text, re.DOTALL ) animation_suggenstions_match = re.search( r"(.*?)", response_text, re.DOTALL, ) if decomposed_svg_match and animation_suggenstions_match: decomposed_svg_text = decomposed_svg_match.group(1).strip() animation_suggestions = animation_suggenstions_match.group(1).strip() # Create viewer HTML viewer_html = f"""
{decomposed_svg_text}
""" return decomposed_svg_text, animation_suggestions, viewer_html # Return tuple of 3 values else: error_message = "Decomposed SVG and Animation Suggestion not found in response." error_html = create_error_html(error_message) return "", "", error_html # Return tuple of 3 values except Exception as e: error_message = f"Error during MLLM feedback prediction: {e}" error_html = create_error_html(error_message) return "", "", error_html # Return tuple of 3 values def generate_animation(self, proposed_animation: str, svg_content: str) -> str: try: prompt = self.generate_animation_prompt.format( svg_content=svg_content, proposed_animation=proposed_animation ) logger.info(f"Animation Generation Prompt:\n{prompt}") if self.client: response = self.client.messages.create( model="claude-sonnet-4-20250514", max_tokens=10000, messages=[{"role": "user", "content": prompt}], ) response_text = response.content[0].text logger.info(f"Model Response:\n{response_text}") return response_text except Exception as e: return f"

Error generating animation: {e}

" generator = SVGAnimationGenerator() def process_svg(svg_file): if svg_file is None: return "Please upload an SVG file" try: with open(svg_file, "r", encoding="utf-8") as f: svg_content = f.read() parsed_svg = generator.parse_svg(svg_content) return parsed_svg.get("svg_content", "") except FileNotFoundError: return "File not found. Please upload a valid SVG file." except ET.ParseError: return "Invalid SVG file format. Please upload a valid SVG file." except Exception as e: return f"Error processing file: {e}" def predict_decompose_group(svg_file, svg_text, object_name): if not object_name.strip(): error_msg = "Please enter a valid object name for the SVG" error_html = (error_msg) return "", error_msg, "", error_html if svg_file is not None: svg_content_inner = process_svg(svg_file) else: svg_content_inner = svg_text.strip() if not svg_content_inner: error_msg = "Please upload an SVG file or enter SVG markup" error_html = create_error_html(error_msg) return "", error_msg, "", error_html parsed_svg = generator.parse_svg(svg_content_inner) if "error" in parsed_svg: error_msg = parsed_svg["error"] error_html = create_error_html(error_msg) return "", error_msg, "", error_html decompose_result = generator.predict_decompose_group(parsed_svg, object_name) if "error" in decompose_result: error_msg = decompose_result["error"] error_html = create_error_html(error_msg) return "", error_msg, "", error_html decomposed_svg = decompose_result["svg_content"] animation_suggestions = decompose_result["animation_suggestions"] # Create viewer HTML decomposed_svg_viewer = f"""
{decomposed_svg}
""" return ( decomposed_svg, # For svg_content_hidden decomposed_svg, # For groups_summary (분석 결과 표시) animation_suggestions, # For animation_suggestion decomposed_svg_viewer, # For decomposed_svg_viewer ) def create_animation_preview(animation_desc: str, svg_content: str) -> str: """Create animation preview from description and SVG content.""" if not svg_content.strip(): return create_error_html("⚠️ Please process SVG first") if not animation_desc.strip(): return create_error_html("⚠️ Please describe the animation you want") try: animation_html = generator.generate_animation(animation_desc, svg_content) if not animation_html: return create_error_html("❌ Failed to generate animation") # Extract HTML content from Claude's response html_match = re.search( r"(.*?)", animation_html, re.DOTALL ) if not html_match: return create_error_html("❌ Invalid animation HTML format") # Get the actual HTML content html_content = html_match.group(1).strip() # Save the HTML content to the output directory output_dir = "output" os.makedirs(output_dir, exist_ok=True) html_path = os.path.join(output_dir, "animation_preview.html") with open(html_path, "w", encoding="utf-8") as f: f.write(html_content) print(f"Animation preview saved to: {html_path}") html_path = pathlib.Path("output/animation_preview.html").read_text( encoding="utf-8" ) # Wrap in a container with preview styling return f"""
""" except Exception as e: return create_error_html(f"❌ Error creating animation: {str(e)}") def create_error_html(message: str) -> str: """Create formatted error message HTML.""" return f"""

{message}

""" # Define examples with proper path handling and categories example_list = { "Animals": [ [os.path.join(os.path.dirname(__file__), "examples/corgi.svg"), "corgi"], [os.path.join(os.path.dirname(__file__), "examples/duck.svg"), "duck"], [os.path.join(os.path.dirname(__file__), "examples/whale.svg"), "whale"], ], "Objects": [ [os.path.join(os.path.dirname(__file__), "examples/rocket.svg"), "rocket"] ], } def load_example(example_choice): # Find the selected example in the categories for category, examples in example_list.items(): for example in examples: if example[1] == example_choice: return example[0] # Return the file path return None # Flatten choices for dropdown example_choices = [ example[1] for category in example_list.values() for example in category ] demo = gr.Blocks(title="SVG Animation Generator", theme=gr.themes.Soft()) with demo: gr.Markdown("# 🎨 SVG Decomposition & Animation Generator") gr.Markdown( "Intelligent SVG decomposition and animation generation powered by MLLM. This tool decomposes SVG structures and generates animations based on your descriptions." ) with gr.Column(): with gr.Row(scale=2): with gr.Column(scale=1): gr.Markdown("## 📤 Input SVG") with gr.Row(scale=2): svg_file = gr.File(label="Upload SVG File", file_types=[".svg"]) svg_text = gr.Textbox( label="Or Paste SVG Code", lines=8.4, ) # Add example dropdown example_dropdown = gr.Dropdown( choices=example_choices, label="Try an Example", value=None ) # Add dropdown change event example_dropdown.change( fn=load_example, inputs=[example_dropdown], outputs=[svg_file] ) with gr.Column(scale=1): with gr.Row(scale=1): with gr.Column(scale=1): gr.Markdown("## 🔍 SVG Analysis") object_name = gr.Textbox( label="Name Your Object", placeholder="Give a name to your SVG (e.g., 'dove', 'robot')", value="corgi", ) process_btn = gr.Button("🔄 Decompose Structure", variant="primary") groups_summary = gr.Textbox( label="Decomposition Results", placeholder="MLLM will analyze and decompose the SVG structure...", lines=6, interactive=False, ) with gr.Column(scale=1): gr.Markdown("## 🎯 Animation Design") animation_suggestion = gr.Textbox( label="AI Suggestions", placeholder="MLLM will suggest animations based on the decomposed structure...", lines=14.5, ) with gr.Row(scale=1): with gr.Column(scale=1): gr.Markdown("## 💡 Decomposed Elements") groups_feedback = gr.Textbox( label="Element Structure", placeholder="If you have specific decomposition in mind, describe it here...", lines=2, ) groups_feedback_btn = gr.Button( "💭 Apply Decomposition Feedback", variant="primary" ) with gr.Column(scale=1): gr.Markdown("## ✨ Create Animation") describe_animation = gr.Textbox( label="Animation Description", placeholder="Describe your desired animation (e.g., 'gentle floating motion')", lines=2, ) animate_btn = gr.Button("🎬 Generate Animation", variant="primary") with gr.Row(scale=3): with gr.Column(scale=1): svg_content_hidden = gr.Textbox(visible=False) gr.Markdown("## 🖼️ Decomposed Structure") decomposed_svg_viewer = gr.HTML( label="Decomposed SVG", value="""
Decomposed SVG structure will appear here
""", ) with gr.Column(scale=1): gr.Markdown("## 🎭 Animation Preview") animation_preview = gr.HTML( label="Live Preview", value="""
Amination preview will appear here
""", ) with gr.Column(): with gr.Row(scale=1): with gr.Column(scale=1): gr.Markdown("## 📂 HTML Output") output_html = gr.Textbox( label="Output HTML", lines=10, placeholder="Generated HTML will be saved here.", ) process_btn.click( fn=predict_decompose_group, inputs=[svg_file, svg_text, object_name], outputs=[ svg_content_hidden, # Store decomposed SVG for later use groups_summary, # Show analysis results animation_suggestion, # Show animation suggestions decomposed_svg_viewer, # Show SVG preview ], ) groups_feedback_btn.click( fn=generator.feedback_decompose_group, inputs=[ svg_content_hidden, # Pass the SVG content directly groups_feedback, # Pass the feedback text ], outputs=[ svg_content_hidden, # Update hidden SVG content animation_suggestion, # Update animation suggestions decomposed_svg_viewer, # Update SVG preview ], ) animate_btn.click( fn=create_animation_preview, inputs=[ describe_animation, svg_content_hidden, ], outputs=[ animation_preview, output_html ], ) if __name__ == "__main__": demo.launch(share=True)