Milhaud commited on
Commit
6186218
Β·
1 Parent(s): 752321e

feat: implement SVG Animation Generator with decomposition and animation capabilities

Browse files
Files changed (2) hide show
  1. main.py +414 -0
  2. requirements.txt +4 -0
main.py ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import xml.etree.ElementTree as ET
3
+ import re
4
+ import anthropic
5
+ import os
6
+ from dotenv import load_dotenv
7
+ from utils import svg_to_png_base64
8
+ import pathlib
9
+
10
+ load_dotenv(override=True)
11
+
12
+
13
+ class SVGAnimationGenerator:
14
+ def __init__(self):
15
+ self.semantic_groups = [
16
+ "Characters",
17
+ "Objects",
18
+ "Background",
19
+ "Text",
20
+ "Effects",
21
+ "Details",
22
+ ]
23
+ self.client = None
24
+ self.predict_decompose_group_prompt = self._get_prompt(
25
+ "prompts/predict_decompose_group.txt"
26
+ )
27
+ self.feedback_decompose_group_prompt = self._get_prompt(
28
+ "prompts/feedback_decompose_group.txt"
29
+ )
30
+ self.generate_animation_prompt = self._get_prompt(
31
+ "prompts/generate_animation.txt"
32
+ )
33
+ if "ANTHROPIC_API_KEY" in os.environ:
34
+ self.client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
35
+
36
+ def _get_prompt(self, prompt_file_path: str) -> str:
37
+ try:
38
+ with open(prompt_file_path, "r", encoding="utf-8") as f:
39
+ return f.read()
40
+ except FileNotFoundError:
41
+ return "Prompt file not found. Please check the path."
42
+
43
+ def parse_svg(self, svg_content: str) -> dict:
44
+ try:
45
+ # Remove namespace definitions and simplify the <svg> tag
46
+ svg_content = re.sub(r'xmlns[^=]*="[^"]*"', "", svg_content)
47
+ svg_content = re.sub(r"<svg[^>]*>", "<svg>", svg_content)
48
+ return {"svg_content": svg_content}
49
+ except Exception as e:
50
+ return {"error": f"SVG parsing error: {e}"}
51
+
52
+ def predict_decompose_group(self, parsed_svg: dict, object_name: str) -> dict:
53
+ try:
54
+ svg_content = parsed_svg["svg_content"]
55
+
56
+ # Convert SVG to PNG for MLLM analysis
57
+ image_media_type, image_data = svg_to_png_base64(svg_content)
58
+
59
+ if not image_data:
60
+ return {"error": "Failed to convert SVG to PNG"}
61
+
62
+ prompt = self.predict_decompose_group_prompt.format(
63
+ object_name=object_name, svg_content=svg_content
64
+ )
65
+ response = self.client.messages.create(
66
+ model="claude-sonnet-4-20250514",
67
+ max_tokens=10000,
68
+ messages=[
69
+ {
70
+ "role": "user",
71
+ "content": [
72
+ {
73
+ "type": "image",
74
+ "source": {
75
+ "type": "base64",
76
+ "media_type": image_media_type,
77
+ "data": image_data,
78
+ },
79
+ },
80
+ {"type": "text", "text": prompt},
81
+ ],
82
+ }
83
+ ],
84
+ )
85
+ response_text = response.content[0].text
86
+ decomposed_svg_match = re.search(
87
+ r"<decomposed_svg>(.*?)</decomposed_svg>", response_text, re.DOTALL
88
+ )
89
+ animation_suggenstions_match = re.search(
90
+ r"<animation_suggestions>(.*?)</animation_suggestions>",
91
+ response_text,
92
+ re.DOTALL,
93
+ )
94
+ print(
95
+ "[SVG Decompose] Decomposed SVG found", decomposed_svg_match is not None
96
+ )
97
+ if decomposed_svg_match and animation_suggenstions_match:
98
+ decomposed_svg_text = decomposed_svg_match.group(1).strip()
99
+ animation_suggestions = animation_suggenstions_match.group(1).strip()
100
+ print("[SVG Decompose] Animation suggestions found")
101
+ return {
102
+ "svg_content": decomposed_svg_text,
103
+ "animation_suggestions": animation_suggestions,
104
+ }
105
+ else:
106
+ return {
107
+ "error": "Decomposed SVG and Animation Suggestion not found in response."
108
+ }
109
+ except Exception as e:
110
+ return {"error": f"Error during MLLM prediction: {e}"}
111
+
112
+ def feedback_decompose_group(self, svg_content: str, feedback: str) -> dict:
113
+ try:
114
+ # Parse the SVG content first
115
+ parsed_svg = self.parse_svg(svg_content["svg_content"])
116
+ if "error" in parsed_svg:
117
+ return parsed_svg
118
+
119
+ prompt = self.feedback_decompose_group_prompt.format(
120
+ parsed_svg=parsed_svg, feedback=feedback
121
+ )
122
+ response = self.client.messages.create(
123
+ model="claude-sonnet-4-20250514",
124
+ max_tokens=10000,
125
+ messages=[
126
+ {
127
+ "role": "user",
128
+ "content": [
129
+ {"type": "text", "text": prompt},
130
+ ],
131
+ }
132
+ ],
133
+ )
134
+ response_text = response.content[0].text
135
+ decomposed_svg_match = re.search(
136
+ r"<decomposed_svg>(.*?)</decomposed_svg>", response_text, re.DOTALL
137
+ )
138
+ animation_suggenstions_match = re.search(
139
+ r"<animation_suggestions>(.*?)</animation_suggestions>",
140
+ response_text,
141
+ re.DOTALL,
142
+ )
143
+ print(
144
+ "[SVG Decompose] Decomposed SVG found", decomposed_svg_match is not None
145
+ )
146
+ if decomposed_svg_match and animation_suggenstions_match:
147
+ decomposed_svg_text = decomposed_svg_match.group(1).strip()
148
+ animation_suggestions = animation_suggenstions_match.group(1).strip()
149
+ print("[SVG Decompose] Animation suggestions found")
150
+ return {
151
+ "svg_content": decomposed_svg_text,
152
+ "animation_suggestions": animation_suggestions,
153
+ }
154
+ else:
155
+ return {
156
+ "error": "Decomposed SVG and Animation Suggestion not found in response."
157
+ }
158
+ except Exception as e:
159
+ return {"error": f"Error during MLLM feedback prediction: {e}"}
160
+
161
+ def generate_animation(self, proposed_animation: str, svg_content: str) -> str:
162
+ try:
163
+ prompt = self.predict_decompose_group_prompt.format(
164
+ svg_content=svg_content, proposed_animation=proposed_animation
165
+ )
166
+ if self.client:
167
+ response = self.client.messages.create(
168
+ model="claude-sonnet-4-20250514",
169
+ max_tokens=10000,
170
+ messages=[{"role": "user", "content": prompt}],
171
+ )
172
+ print("[ANIMATION] Response received.")
173
+ print(response.content[0].text)
174
+ return response.content[0].text
175
+ except Exception as e:
176
+ return f"<html><body><h3>Error generating animation: {e}</h3></body></html>"
177
+
178
+
179
+ generator = SVGAnimationGenerator()
180
+
181
+
182
+ def process_svg(svg_file):
183
+ if svg_file is None:
184
+ return "Please upload an SVG file"
185
+ try:
186
+ with open(svg_file, "r", encoding="utf-8") as f:
187
+ svg_content = f.read()
188
+ parsed_svg = generator.parse_svg(svg_content)
189
+ return parsed_svg.get("svg_content", "")
190
+ except FileNotFoundError:
191
+ return "File not found. Please upload a valid SVG file."
192
+ except ET.ParseError:
193
+ return "Invalid SVG file format. Please upload a valid SVG file."
194
+ except Exception as e:
195
+ return f"Error processing file: {e}"
196
+
197
+
198
+ def predict_decompose_group(svg_file, svg_text, object_name):
199
+ if not object_name.strip():
200
+ return "Please enter a valid object name for the SVG"
201
+
202
+ if svg_file is not None:
203
+ svg_content_inner = process_svg(svg_file)
204
+ else:
205
+ svg_content_inner = svg_text.strip()
206
+
207
+ if not svg_content_inner:
208
+ return "Please upload an SVG file or enter SVG markup", "", "", "", ""
209
+
210
+ parsed_svg = generator.parse_svg(svg_content_inner)
211
+ if "error" in parsed_svg:
212
+ return parsed_svg["error"], "", "", "", ""
213
+
214
+ decompose_result = generator.predict_decompose_group(parsed_svg, object_name)
215
+ if "error" in decompose_result:
216
+ return decompose_result["error"], "", "", "", ""
217
+
218
+ decomposed_svg = decompose_result["svg_content"]
219
+ animation_suggestions = decompose_result["animation_suggestions"]
220
+
221
+ # Create viewer HTML
222
+ decomposed_svg_viewer = f"""
223
+ <div style='padding: 20px; background: #fff; border: 1px solid #eee; border-radius: 8px; display: block;'>
224
+ <div style='display: block; align-items: center; margin-bottom: 10px;'>
225
+ </div>
226
+ <div id='animation-container' style='min-height: 300px; display: block; justify-content: center; align-items: center; background: #fafafa; border-radius: 4px; padding: 20px;'>
227
+ {decomposed_svg}
228
+ </div>
229
+ </div>
230
+ """
231
+
232
+ return (
233
+ decomposed_svg, # For svg_content_hidden
234
+ decomposed_svg, # For decomposed_svg
235
+ animation_suggestions, # For animation_suggestion
236
+ decomposed_svg_viewer, # For decomposed_svg_viewer
237
+ )
238
+
239
+
240
+ def create_animation_preview(animation_desc: str, svg_content: str) -> str:
241
+ """Create animation preview from description and SVG content."""
242
+ if not svg_content.strip():
243
+ return create_error_html("⚠️ Please process SVG first")
244
+
245
+ if not animation_desc.strip():
246
+ return create_error_html("⚠️ Please describe the animation you want")
247
+
248
+ try:
249
+ animation_html = generator.generate_animation(animation_desc, svg_content)
250
+ if not animation_html:
251
+ return create_error_html("❌ Failed to generate animation")
252
+
253
+ # Extract HTML content from Claude's response
254
+ html_match = re.search(
255
+ r"<html_output>(.*?)</html_output>", animation_html, re.DOTALL
256
+ )
257
+ if not html_match:
258
+ return create_error_html("❌ Invalid animation HTML format")
259
+
260
+ # Get the actual HTML content
261
+ html_content = html_match.group(1).strip()
262
+
263
+ # Save the HTML content to the output directory
264
+ output_dir = "output"
265
+ os.makedirs(output_dir, exist_ok=True)
266
+ html_path = os.path.join(output_dir, "animation_preview.html")
267
+ with open(html_path, "w", encoding="utf-8") as f:
268
+ f.write(html_content)
269
+ print(f"Animation preview saved to: {html_path}")
270
+
271
+ html_path = pathlib.Path("output/animation_preview.html").read_text(
272
+ encoding="utf-8"
273
+ )
274
+
275
+ # Wrap in a container with preview styling
276
+ return f"""
277
+ <div style='padding: 20px; background: #fff; border: 1px solid #eee; border-radius: 8px; display: block;'>
278
+ <div style='display: block; align-items: center; margin-bottom: 10px;'>
279
+ </div>
280
+ <div id='animation-container' style='min-height: 300px; display: block; justify-content: center; align-items: center; background: #fafafa; border-radius: 4px; padding: 20px;'>
281
+ <iframe srcdoc="{html_path.replace('"', '&quot;')}"
282
+ width="100%" height="600px"
283
+ style="border:none; overflow:hidden;"
284
+ sandbox="allow-scripts allow-same-origin">
285
+ </iframe>
286
+ </div>
287
+ </div>
288
+ """
289
+ except Exception as e:
290
+ return create_error_html(f"❌ Error creating animation: {str(e)}")
291
+
292
+
293
+ def create_error_html(message: str) -> str:
294
+ """Create formatted error message HTML."""
295
+ return f"""
296
+ <div style='padding: 40px; text-align: center; color: #666; border: 2px dashed #ddd; border-radius: 10px;'>
297
+ <h3>{message}</h3>
298
+ </div>
299
+ """
300
+
301
+
302
+ with gr.Blocks(title="SVG Animation Generator", theme=gr.themes.Soft()) as demo:
303
+ gr.Markdown("# 🎨 SVG Decomposition & Animation Generator")
304
+ gr.Markdown(
305
+ "Intelligent SVG decomposition and animation generation powered by MLLM. This tool decomposes SVG structures and generates animations based on your descriptions."
306
+ )
307
+ with gr.Column():
308
+ with gr.Row(scale=2):
309
+ with gr.Column(scale=1):
310
+ gr.Markdown("## πŸ“€ Input SVG")
311
+ with gr.Row(scale=2):
312
+ svg_file = gr.File(label="Upload SVG File", file_types=[".svg"])
313
+ svg_text = gr.Textbox(
314
+ label="Or Paste SVG Code",
315
+ lines=8.4,
316
+ )
317
+ with gr.Row(scale=1):
318
+ with gr.Column(scale=1):
319
+ gr.Markdown("## πŸ” SVG Analysis")
320
+ object_name = gr.Textbox(
321
+ label="Name Your Object",
322
+ placeholder="Give a name to your SVG (e.g., 'dove', 'robot')",
323
+ value="object",
324
+ )
325
+ process_btn = gr.Button("πŸ”„ Decompose Structure", variant="primary")
326
+ groups_summary = gr.Textbox(
327
+ label="Decomposition Results",
328
+ placeholder="MLLM will analyze and decompose the SVG structure...",
329
+ lines=6,
330
+ interactive=False,
331
+ )
332
+
333
+ gr.Markdown("## πŸ’‘ Decomposed Elements")
334
+ groups_feedback = gr.Textbox(
335
+ label="Element Structure",
336
+ placeholder="If you have specific decomposition in mind, describe it here...",
337
+ lines=2,
338
+ )
339
+ groups_feedback_btn = gr.Button(
340
+ "πŸ’­ Get Decomposition Feedback", variant="primary"
341
+ )
342
+
343
+ gr.Markdown("## 🎯 Animation Design")
344
+ animation_suggestion = gr.Textbox(
345
+ label="AI Suggestions",
346
+ placeholder="MLLM will suggest animations based on the decomposed structure...",
347
+ lines=4,
348
+ )
349
+
350
+ gr.Markdown("## ✨ Create Animation")
351
+ gr.Markdown("πŸ’¬ **What animation would you like to create?**")
352
+ describe_animation = gr.Textbox(
353
+ label="Animation Description",
354
+ placeholder="Describe your desired animation (e.g., 'gentle floating motion')",
355
+ lines=2,
356
+ )
357
+ animate_btn = gr.Button("🎬 Generate Animation", variant="primary")
358
+
359
+ with gr.Column(scale=2):
360
+ svg_content_hidden = gr.Textbox(visible=False)
361
+ gr.Markdown("## πŸ–ΌοΈ Decomposed Structure")
362
+ decomposed_svg_viewer = gr.HTML(
363
+ label="Decomposed SVG",
364
+ value="""
365
+ <div style='padding: 40px; text-align: center; color: #666; border: 2px dashed #ddd; border-radius: 10px;'>
366
+ <div id='decomposed-svg-container' style='min-height: 150px; display: flex; justify-content: center; align-items: center; border-radius: 4px; padding: 10px;'>
367
+ <div style='color: #999; text-align: center;'>Decomposed SVG structure will appear here</div>
368
+ </div>
369
+ </div>
370
+ """,
371
+ )
372
+ gr.Markdown("## 🎭 Animation Preview")
373
+ animation_preview = gr.HTML(
374
+ label="Live Preview",
375
+ value="""
376
+ <div style='padding: 40px; text-align: center; color: #666; border: 2px dashed #ddd; border-radius: 10px;'>
377
+ AI-powered animation generation</p>
378
+ </div>
379
+ """,
380
+ )
381
+
382
+ process_btn.click(
383
+ fn=predict_decompose_group,
384
+ inputs=[svg_file, svg_text, object_name],
385
+ outputs=[
386
+ svg_content_hidden, # Store decomposed SVG for later use
387
+ groups_summary, # Show analysis results
388
+ animation_suggestion, # Show animation suggestions
389
+ decomposed_svg_viewer, # Show SVG preview
390
+ ],
391
+ )
392
+ groups_feedback_btn.click(
393
+ fn=generator.feedback_decompose_group,
394
+ inputs=[
395
+ svg_content_hidden, # Pass the SVG content directly
396
+ groups_feedback, # Pass the feedback text
397
+ ],
398
+ outputs=[
399
+ svg_content_hidden, # Update hidden SVG content
400
+ animation_suggestion, # Update animation suggestions
401
+ decomposed_svg_viewer, # Update SVG preview
402
+ ],
403
+ )
404
+ animate_btn.click(
405
+ fn=create_animation_preview,
406
+ inputs=[
407
+ describe_animation,
408
+ svg_content_hidden,
409
+ ],
410
+ outputs=[animation_preview],
411
+ )
412
+
413
+ if __name__ == "__main__":
414
+ demo.launch(share=True)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=4.26.0
2
+ pillow
3
+ cairosvg
4
+ anthropic