ismdrobiul489 commited on
Commit
755423c
·
1 Parent(s): 1b10369

Add AI Drawing Animator with Groq + fallback templates, update UI

Browse files
modules/art_reels/router.py CHANGED
@@ -20,6 +20,7 @@ from .schemas import (
20
  )
21
  from .services.block_art import BlockArt
22
  from .services.drawing_animator import DrawingAnimator
 
23
  from .services.stick_figure import StickFigure
24
  from .services.video_composer import VideoComposer
25
 
@@ -102,18 +103,22 @@ async def generate_minecraft_video(job_id: str, description: str, blocks: int, s
102
 
103
 
104
  async def generate_drawing_video(job_id: str, subject: str, style: str, colors: bool):
105
- """Background task to generate drawing animation video"""
106
  temp_dir = f"temp/art_{job_id}"
107
 
108
  try:
109
  update_job(job_id, "processing", 10)
110
 
111
- # Generate frames
112
- logger.info(f"Generating drawing frames for job {job_id}")
113
- frame_paths = drawing_animator.generate_drawing_animation(
114
- subject=subject,
 
 
 
115
  output_dir=temp_dir,
116
- with_colors=colors
 
117
  )
118
 
119
  update_job(job_id, "processing", 60)
@@ -126,18 +131,38 @@ async def generate_drawing_video(job_id: str, subject: str, style: str, colors:
126
  fps=30
127
  )
128
 
129
- update_job(job_id, "processing", 90)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  # Cleanup
132
  video_composer.cleanup_frames(frame_paths)
133
  if os.path.exists(temp_dir):
134
  shutil.rmtree(temp_dir)
135
 
136
- update_job(job_id, "ready", 100, video_url=f"/api/art/video/{job_id}")
137
- logger.info(f"Drawing video ready: {job_id}")
 
138
 
139
  except Exception as e:
140
  logger.error(f"Error generating drawing video: {e}")
 
 
141
  update_job(job_id, "failed", error=str(e))
142
 
143
 
 
20
  )
21
  from .services.block_art import BlockArt
22
  from .services.drawing_animator import DrawingAnimator
23
+ from .services.ai_drawing_animator import AIDrawingAnimator
24
  from .services.stick_figure import StickFigure
25
  from .services.video_composer import VideoComposer
26
 
 
103
 
104
 
105
  async def generate_drawing_video(job_id: str, subject: str, style: str, colors: bool):
106
+ """Background task to generate AI-powered drawing animation video"""
107
  temp_dir = f"temp/art_{job_id}"
108
 
109
  try:
110
  update_job(job_id, "processing", 10)
111
 
112
+ # Create AI Drawing Animator
113
+ ai_drawer = AIDrawingAnimator()
114
+
115
+ # Generate frames using AI paths
116
+ logger.info(f"Generating AI drawing frames for job {job_id}: '{subject}'")
117
+ frame_paths = ai_drawer.generate_animation(
118
+ prompt=subject,
119
  output_dir=temp_dir,
120
+ frames_per_path=12,
121
+ fps=30
122
  )
123
 
124
  update_job(job_id, "processing", 60)
 
131
  fps=30
132
  )
133
 
134
+ update_job(job_id, "processing", 85)
135
+
136
+ # Upload to HF Hub (if enabled)
137
+ from pathlib import Path
138
+ from modules.shared.services.hf_storage import get_hf_storage
139
+
140
+ hf_storage = get_hf_storage()
141
+ cloud_url = None
142
+
143
+ if hf_storage and hf_storage.enabled:
144
+ logger.info(f"Uploading to HF Hub for job {job_id}")
145
+ cloud_url = hf_storage.upload_video(
146
+ local_path=Path(video_path),
147
+ video_id=job_id,
148
+ folder="art_reels"
149
+ )
150
+ if cloud_url:
151
+ logger.info(f"Uploaded to cloud: {cloud_url}")
152
 
153
  # Cleanup
154
  video_composer.cleanup_frames(frame_paths)
155
  if os.path.exists(temp_dir):
156
  shutil.rmtree(temp_dir)
157
 
158
+ video_url = cloud_url or f"/api/art/video/{job_id}"
159
+ update_job(job_id, "ready", 100, video_url=video_url)
160
+ logger.info(f"AI Drawing video ready: {job_id}")
161
 
162
  except Exception as e:
163
  logger.error(f"Error generating drawing video: {e}")
164
+ import traceback
165
+ logger.error(traceback.format_exc())
166
  update_job(job_id, "failed", error=str(e))
167
 
168
 
modules/art_reels/services/ai_drawing_animator.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI Drawing Animator - Dynamic SVG Path Generation with AI
3
+ Generates complex drawings from any prompt using AI-generated paths
4
+ """
5
+ import logging
6
+ import os
7
+ import json
8
+ import math
9
+ import re
10
+ from PIL import Image, ImageDraw, ImageFont
11
+ from typing import List, Tuple, Dict, Optional
12
+ from groq import Groq
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ # AI System Prompt for SVG path generation
18
+ DRAWING_PROMPT = """You are an expert SVG artist. Generate detailed drawing paths for any subject.
19
+
20
+ OUTPUT FORMAT (JSON):
21
+ {
22
+ "title": "Drawing Name",
23
+ "paths": [
24
+ {
25
+ "type": "line",
26
+ "start": [x1, y1],
27
+ "end": [x2, y2],
28
+ "color": "#000000",
29
+ "width": 3,
30
+ "order": 1
31
+ },
32
+ {
33
+ "type": "circle",
34
+ "center": [cx, cy],
35
+ "radius": r,
36
+ "color": "#FF0000",
37
+ "fill": true,
38
+ "order": 2
39
+ },
40
+ {
41
+ "type": "polygon",
42
+ "points": [[x1,y1], [x2,y2], [x3,y3]...],
43
+ "color": "#00FF00",
44
+ "fill": true,
45
+ "order": 3
46
+ },
47
+ {
48
+ "type": "arc",
49
+ "center": [cx, cy],
50
+ "radius": r,
51
+ "start_angle": 0,
52
+ "end_angle": 180,
53
+ "color": "#0000FF",
54
+ "width": 2,
55
+ "order": 4
56
+ },
57
+ {
58
+ "type": "bezier",
59
+ "points": [[x1,y1], [x2,y2], [x3,y3], [x4,y4]],
60
+ "color": "#000000",
61
+ "width": 2,
62
+ "order": 5
63
+ }
64
+ ]
65
+ }
66
+
67
+ CANVAS SIZE: 1080 x 1920 (9:16 portrait)
68
+ CENTER: (540, 960)
69
+ DRAWING AREA: (100, 300) to (980, 1600)
70
+
71
+ RULES:
72
+ 1. Create DETAILED paths - at least 15-30 elements for complex subjects
73
+ 2. Use logical drawing order (background → foreground)
74
+ 3. Include both outline (lines) and fill (polygons/circles)
75
+ 4. Use varied line widths (2-8) for depth
76
+ 5. Keep everything within the drawing area
77
+ 6. Use colors that make sense for the subject
78
+ 7. For complex shapes, break into multiple simple paths
79
+
80
+ EXAMPLES:
81
+ - "car" → wheels (circles), body (polygon), windows (rectangles), details (lines)
82
+ - "face" → head (circle), eyes (circles), nose (lines), mouth (arc), hair (beziers)
83
+ - "tree" → trunk (rectangle), branches (lines), leaves (many small circles/polygons)
84
+ - "house" → walls (polygons), roof (polygon), windows (rectangles), door (rectangle)
85
+
86
+ Return ONLY valid JSON, no other text."""
87
+
88
+
89
+ class AIDrawingAnimator:
90
+ """
91
+ AI-powered drawing animator that generates complex SVG-like paths
92
+ from any text prompt and animates them line-by-line.
93
+ """
94
+
95
+ # Canvas dimensions
96
+ WIDTH = 1080
97
+ HEIGHT = 1920
98
+
99
+ # Default colors
100
+ BG_COLOR = (255, 255, 255)
101
+ LINE_COLOR = (30, 30, 30)
102
+
103
+ def __init__(self, groq_api_key: str = None):
104
+ self.groq_api_key = groq_api_key or os.environ.get("GROQ_API") or os.environ.get("GROQ_API_KEY")
105
+ if self.groq_api_key:
106
+ self.groq = Groq(api_key=self.groq_api_key)
107
+ logger.info("AI Drawing Animator initialized with Groq")
108
+ else:
109
+ self.groq = None
110
+ logger.warning("Groq API key not found - using fallback templates")
111
+
112
+ self._load_fonts()
113
+
114
+ def _load_fonts(self):
115
+ """Load fonts with fallbacks"""
116
+ try:
117
+ self.font_title = ImageFont.truetype("arial.ttf", 48)
118
+ self.font_label = ImageFont.truetype("arial.ttf", 32)
119
+ except:
120
+ self.font_title = ImageFont.load_default()
121
+ self.font_label = ImageFont.load_default()
122
+
123
+ def generate_paths_with_ai(self, prompt: str) -> Dict:
124
+ """Generate drawing paths using AI"""
125
+ if not self.groq:
126
+ return self._get_fallback_drawing(prompt)
127
+
128
+ try:
129
+ user_prompt = f"""Create a detailed drawing of: "{prompt}"
130
+
131
+ Generate all the paths needed to draw this from scratch, line by line.
132
+ Make it detailed and professional-looking.
133
+ Return ONLY valid JSON."""
134
+
135
+ response = self.groq.chat.completions.create(
136
+ model="llama-3.3-70b-versatile",
137
+ messages=[
138
+ {"role": "system", "content": DRAWING_PROMPT},
139
+ {"role": "user", "content": user_prompt}
140
+ ],
141
+ temperature=0.7,
142
+ max_tokens=4000
143
+ )
144
+
145
+ content = response.choices[0].message.content.strip()
146
+
147
+ # Parse JSON
148
+ if "```" in content:
149
+ content = content.split("```")[1]
150
+ if content.startswith("json"):
151
+ content = content[4:]
152
+ content = content.strip()
153
+
154
+ drawing = json.loads(content)
155
+ logger.info(f"AI generated {len(drawing.get('paths', []))} paths for '{prompt}'")
156
+ return drawing
157
+
158
+ except Exception as e:
159
+ logger.error(f"AI path generation failed: {e}")
160
+ return self._get_fallback_drawing(prompt)
161
+
162
+ def _get_fallback_drawing(self, prompt: str) -> Dict:
163
+ """Fallback templates for common subjects"""
164
+ prompt_lower = prompt.lower()
165
+
166
+ # Detect subject and return appropriate template
167
+ if any(word in prompt_lower for word in ["car", "vehicle", "auto"]):
168
+ return self._car_template()
169
+ elif any(word in prompt_lower for word in ["face", "person", "human", "head"]):
170
+ return self._face_template()
171
+ elif any(word in prompt_lower for word in ["tree", "plant", "forest"]):
172
+ return self._tree_template()
173
+ elif any(word in prompt_lower for word in ["house", "home", "building"]):
174
+ return self._house_template()
175
+ elif any(word in prompt_lower for word in ["flower", "rose", "tulip"]):
176
+ return self._flower_template()
177
+ elif any(word in prompt_lower for word in ["star", "stars"]):
178
+ return self._star_template()
179
+ elif any(word in prompt_lower for word in ["heart", "love"]):
180
+ return self._heart_template()
181
+ elif any(word in prompt_lower for word in ["sun", "sunrise"]):
182
+ return self._sun_template()
183
+ else:
184
+ # Default to a simple abstract pattern
185
+ return self._abstract_template(prompt)
186
+
187
+ def _car_template(self) -> Dict:
188
+ """Detailed car template"""
189
+ return {
190
+ "title": "Sports Car",
191
+ "paths": [
192
+ # Car body
193
+ {"type": "polygon", "points": [[200, 900], [880, 900], [880, 750], [720, 750], [650, 650], [350, 650], [280, 750], [200, 750]], "color": "#E74C3C", "fill": True, "order": 1},
194
+ # Windows
195
+ {"type": "polygon", "points": [[360, 740], [640, 740], [600, 660], [380, 660]], "color": "#AED6F1", "fill": True, "order": 2},
196
+ # Front wheel
197
+ {"type": "circle", "center": [300, 900], "radius": 60, "color": "#2C3E50", "fill": True, "order": 3},
198
+ {"type": "circle", "center": [300, 900], "radius": 35, "color": "#95A5A6", "fill": True, "order": 4},
199
+ # Back wheel
200
+ {"type": "circle", "center": [780, 900], "radius": 60, "color": "#2C3E50", "fill": True, "order": 5},
201
+ {"type": "circle", "center": [780, 900], "radius": 35, "color": "#95A5A6", "fill": True, "order": 6},
202
+ # Headlight
203
+ {"type": "circle", "center": [850, 800], "radius": 20, "color": "#F1C40F", "fill": True, "order": 7},
204
+ # Door line
205
+ {"type": "line", "start": [500, 750], "end": [500, 900], "color": "#C0392B", "width": 3, "order": 8},
206
+ # Door handle
207
+ {"type": "line", "start": [440, 800], "end": [480, 800], "color": "#2C3E50", "width": 4, "order": 9},
208
+ # Ground
209
+ {"type": "line", "start": [100, 960], "end": [980, 960], "color": "#7F8C8D", "width": 5, "order": 10},
210
+ ]
211
+ }
212
+
213
+ def _face_template(self) -> Dict:
214
+ """Detailed face template"""
215
+ cx, cy = 540, 700
216
+ return {
217
+ "title": "Human Face",
218
+ "paths": [
219
+ # Head outline
220
+ {"type": "circle", "center": [cx, cy], "radius": 200, "color": "#FDEBD0", "fill": True, "order": 1},
221
+ {"type": "circle", "center": [cx, cy], "radius": 200, "color": "#2C3E50", "fill": False, "order": 2},
222
+ # Left eye
223
+ {"type": "circle", "center": [cx-60, cy-40], "radius": 35, "color": "#FFFFFF", "fill": True, "order": 3},
224
+ {"type": "circle", "center": [cx-60, cy-40], "radius": 35, "color": "#2C3E50", "fill": False, "order": 4},
225
+ {"type": "circle", "center": [cx-60, cy-40], "radius": 15, "color": "#2C3E50", "fill": True, "order": 5},
226
+ # Right eye
227
+ {"type": "circle", "center": [cx+60, cy-40], "radius": 35, "color": "#FFFFFF", "fill": True, "order": 6},
228
+ {"type": "circle", "center": [cx+60, cy-40], "radius": 35, "color": "#2C3E50", "fill": False, "order": 7},
229
+ {"type": "circle", "center": [cx+60, cy-40], "radius": 15, "color": "#2C3E50", "fill": True, "order": 8},
230
+ # Nose
231
+ {"type": "line", "start": [cx, cy-20], "end": [cx, cy+40], "color": "#D35400", "width": 3, "order": 9},
232
+ {"type": "line", "start": [cx, cy+40], "end": [cx-15, cy+50], "color": "#D35400", "width": 3, "order": 10},
233
+ # Smile
234
+ {"type": "arc", "center": [cx, cy+60], "radius": 60, "start_angle": 10, "end_angle": 170, "color": "#E74C3C", "width": 5, "order": 11},
235
+ # Eyebrows
236
+ {"type": "line", "start": [cx-90, cy-90], "end": [cx-30, cy-85], "color": "#2C3E50", "width": 4, "order": 12},
237
+ {"type": "line", "start": [cx+30, cy-85], "end": [cx+90, cy-90], "color": "#2C3E50", "width": 4, "order": 13},
238
+ # Hair
239
+ {"type": "arc", "center": [cx, cy-100], "radius": 180, "start_angle": 200, "end_angle": 340, "color": "#5D4037", "width": 40, "order": 14},
240
+ ]
241
+ }
242
+
243
+ def _tree_template(self) -> Dict:
244
+ """Detailed tree template"""
245
+ return {
246
+ "title": "Tree",
247
+ "paths": [
248
+ # Trunk
249
+ {"type": "polygon", "points": [[500, 1200], [580, 1200], [570, 900], [510, 900]], "color": "#795548", "fill": True, "order": 1},
250
+ # Main branches
251
+ {"type": "line", "start": [540, 900], "end": [400, 700], "color": "#5D4037", "width": 15, "order": 2},
252
+ {"type": "line", "start": [540, 900], "end": [680, 700], "color": "#5D4037", "width": 15, "order": 3},
253
+ {"type": "line", "start": [540, 850], "end": [540, 600], "color": "#5D4037", "width": 12, "order": 4},
254
+ # Leaves (circles)
255
+ {"type": "circle", "center": [400, 650], "radius": 80, "color": "#27AE60", "fill": True, "order": 5},
256
+ {"type": "circle", "center": [540, 550], "radius": 90, "color": "#2ECC71", "fill": True, "order": 6},
257
+ {"type": "circle", "center": [680, 650], "radius": 80, "color": "#27AE60", "fill": True, "order": 7},
258
+ {"type": "circle", "center": [470, 580], "radius": 70, "color": "#1E8449", "fill": True, "order": 8},
259
+ {"type": "circle", "center": [610, 580], "radius": 70, "color": "#1E8449", "fill": True, "order": 9},
260
+ {"type": "circle", "center": [540, 450], "radius": 60, "color": "#2ECC71", "fill": True, "order": 10},
261
+ # Ground
262
+ {"type": "line", "start": [200, 1200], "end": [880, 1200], "color": "#7F8C8D", "width": 3, "order": 11},
263
+ ]
264
+ }
265
+
266
+ def _house_template(self) -> Dict:
267
+ """Detailed house template"""
268
+ return {
269
+ "title": "House",
270
+ "paths": [
271
+ # Main wall
272
+ {"type": "polygon", "points": [[250, 1100], [830, 1100], [830, 650], [250, 650]], "color": "#F5DEB3", "fill": True, "order": 1},
273
+ # Roof
274
+ {"type": "polygon", "points": [[200, 650], [540, 400], [880, 650]], "color": "#8B4513", "fill": True, "order": 2},
275
+ # Door
276
+ {"type": "polygon", "points": [[480, 1100], [600, 1100], [600, 850], [480, 850]], "color": "#5D4037", "fill": True, "order": 3},
277
+ # Door handle
278
+ {"type": "circle", "center": [570, 975], "radius": 10, "color": "#FFD700", "fill": True, "order": 4},
279
+ # Left window
280
+ {"type": "polygon", "points": [[300, 750], [420, 750], [420, 880], [300, 880]], "color": "#AED6F1", "fill": True, "order": 5},
281
+ {"type": "line", "start": [360, 750], "end": [360, 880], "color": "#FFFFFF", "width": 3, "order": 6},
282
+ {"type": "line", "start": [300, 815], "end": [420, 815], "color": "#FFFFFF", "width": 3, "order": 7},
283
+ # Right window
284
+ {"type": "polygon", "points": [[660, 750], [780, 750], [780, 880], [660, 880]], "color": "#AED6F1", "fill": True, "order": 8},
285
+ {"type": "line", "start": [720, 750], "end": [720, 880], "color": "#FFFFFF", "width": 3, "order": 9},
286
+ {"type": "line", "start": [660, 815], "end": [780, 815], "color": "#FFFFFF", "width": 3, "order": 10},
287
+ # Chimney
288
+ {"type": "polygon", "points": [[700, 550], [760, 550], [760, 430], [700, 430]], "color": "#A0522D", "fill": True, "order": 11},
289
+ # Ground
290
+ {"type": "line", "start": [100, 1100], "end": [980, 1100], "color": "#228B22", "width": 8, "order": 12},
291
+ ]
292
+ }
293
+
294
+ def _flower_template(self) -> Dict:
295
+ """Flower template"""
296
+ cx, cy = 540, 700
297
+ return {
298
+ "title": "Flower",
299
+ "paths": [
300
+ # Stem
301
+ {"type": "line", "start": [cx, cy+100], "end": [cx, cy+400], "color": "#27AE60", "width": 8, "order": 1},
302
+ # Leaves
303
+ {"type": "polygon", "points": [[cx, cy+250], [cx-80, cy+300], [cx-40, cy+350], [cx, cy+300]], "color": "#2ECC71", "fill": True, "order": 2},
304
+ {"type": "polygon", "points": [[cx, cy+200], [cx+80, cy+250], [cx+40, cy+300], [cx, cy+250]], "color": "#2ECC71", "fill": True, "order": 3},
305
+ # Petals
306
+ {"type": "circle", "center": [cx, cy-80], "radius": 50, "color": "#E74C3C", "fill": True, "order": 4},
307
+ {"type": "circle", "center": [cx-70, cy-30], "radius": 50, "color": "#E74C3C", "fill": True, "order": 5},
308
+ {"type": "circle", "center": [cx+70, cy-30], "radius": 50, "color": "#E74C3C", "fill": True, "order": 6},
309
+ {"type": "circle", "center": [cx-45, cy+50], "radius": 50, "color": "#E74C3C", "fill": True, "order": 7},
310
+ {"type": "circle", "center": [cx+45, cy+50], "radius": 50, "color": "#E74C3C", "fill": True, "order": 8},
311
+ # Center
312
+ {"type": "circle", "center": [cx, cy], "radius": 40, "color": "#F1C40F", "fill": True, "order": 9},
313
+ ]
314
+ }
315
+
316
+ def _star_template(self) -> Dict:
317
+ """5-point star template"""
318
+ cx, cy = 540, 700
319
+ # Calculate star points
320
+ outer_r = 200
321
+ inner_r = 80
322
+ points = []
323
+ for i in range(10):
324
+ angle = math.pi/2 + i * math.pi/5
325
+ r = outer_r if i % 2 == 0 else inner_r
326
+ x = cx + r * math.cos(angle)
327
+ y = cy - r * math.sin(angle)
328
+ points.append([int(x), int(y)])
329
+
330
+ return {
331
+ "title": "Star",
332
+ "paths": [
333
+ {"type": "polygon", "points": points, "color": "#F1C40F", "fill": True, "order": 1},
334
+ {"type": "polygon", "points": points, "color": "#D4AC0D", "fill": False, "order": 2},
335
+ ]
336
+ }
337
+
338
+ def _heart_template(self) -> Dict:
339
+ """Heart shape template"""
340
+ cx, cy = 540, 750
341
+ return {
342
+ "title": "Heart",
343
+ "paths": [
344
+ # Left curve
345
+ {"type": "arc", "center": [cx-80, cy-80], "radius": 100, "start_angle": 180, "end_angle": 360, "color": "#E74C3C", "width": 8, "order": 1},
346
+ # Right curve
347
+ {"type": "arc", "center": [cx+80, cy-80], "radius": 100, "start_angle": 180, "end_angle": 360, "color": "#E74C3C", "width": 8, "order": 2},
348
+ # Left line
349
+ {"type": "line", "start": [cx-180, cy-80], "end": [cx, cy+150], "color": "#E74C3C", "width": 8, "order": 3},
350
+ # Right line
351
+ {"type": "line", "start": [cx+180, cy-80], "end": [cx, cy+150], "color": "#E74C3C", "width": 8, "order": 4},
352
+ ]
353
+ }
354
+
355
+ def _sun_template(self) -> Dict:
356
+ """Sun with rays"""
357
+ cx, cy = 540, 600
358
+ rays = []
359
+ for i in range(12):
360
+ angle = i * math.pi / 6
361
+ x1 = cx + 120 * math.cos(angle)
362
+ y1 = cy + 120 * math.sin(angle)
363
+ x2 = cx + 200 * math.cos(angle)
364
+ y2 = cy + 200 * math.sin(angle)
365
+ rays.append({
366
+ "type": "line",
367
+ "start": [int(x1), int(y1)],
368
+ "end": [int(x2), int(y2)],
369
+ "color": "#F39C12",
370
+ "width": 6,
371
+ "order": i + 2
372
+ })
373
+
374
+ return {
375
+ "title": "Sun",
376
+ "paths": [
377
+ {"type": "circle", "center": [cx, cy], "radius": 100, "color": "#F1C40F", "fill": True, "order": 1},
378
+ *rays,
379
+ ]
380
+ }
381
+
382
+ def _abstract_template(self, prompt: str) -> Dict:
383
+ """Generate abstract pattern based on prompt"""
384
+ cx, cy = 540, 800
385
+ return {
386
+ "title": prompt[:30],
387
+ "paths": [
388
+ {"type": "circle", "center": [cx, cy], "radius": 150, "color": "#3498DB", "fill": True, "order": 1},
389
+ {"type": "circle", "center": [cx, cy], "radius": 100, "color": "#2ECC71", "fill": True, "order": 2},
390
+ {"type": "circle", "center": [cx, cy], "radius": 50, "color": "#E74C3C", "fill": True, "order": 3},
391
+ {"type": "line", "start": [cx-200, cy], "end": [cx+200, cy], "color": "#2C3E50", "width": 4, "order": 4},
392
+ {"type": "line", "start": [cx, cy-200], "end": [cx, cy+200], "color": "#2C3E50", "width": 4, "order": 5},
393
+ ]
394
+ }
395
+
396
+ def hex_to_rgb(self, hex_color: str) -> Tuple[int, int, int]:
397
+ """Convert hex to RGB"""
398
+ hex_color = hex_color.lstrip('#')
399
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
400
+
401
+ def draw_path(self, draw: ImageDraw, path: Dict, progress: float = 1.0):
402
+ """Draw a single path element"""
403
+ path_type = path.get("type", "line")
404
+ color = self.hex_to_rgb(path.get("color", "#000000"))
405
+ width = path.get("width", 3)
406
+ fill = path.get("fill", False)
407
+
408
+ if path_type == "line":
409
+ start = path["start"]
410
+ end = path["end"]
411
+ # Partial line based on progress
412
+ actual_end = [
413
+ int(start[0] + (end[0] - start[0]) * progress),
414
+ int(start[1] + (end[1] - start[1]) * progress)
415
+ ]
416
+ draw.line([tuple(start), tuple(actual_end)], fill=color, width=width)
417
+
418
+ elif path_type == "circle":
419
+ center = path["center"]
420
+ radius = int(path["radius"] * progress)
421
+ bbox = [center[0]-radius, center[1]-radius, center[0]+radius, center[1]+radius]
422
+ if fill:
423
+ draw.ellipse(bbox, fill=color)
424
+ else:
425
+ draw.ellipse(bbox, outline=color, width=width)
426
+
427
+ elif path_type == "polygon":
428
+ points = path["points"]
429
+ # Draw polygon with progress controlling how many points
430
+ num_points = max(3, int(len(points) * progress))
431
+ visible_points = [tuple(p) for p in points[:num_points]]
432
+ if len(visible_points) >= 3:
433
+ if fill:
434
+ draw.polygon(visible_points, fill=color)
435
+ else:
436
+ draw.polygon(visible_points, outline=color, width=width)
437
+
438
+ elif path_type == "arc":
439
+ center = path["center"]
440
+ radius = path["radius"]
441
+ start_angle = path.get("start_angle", 0)
442
+ end_angle = path.get("end_angle", 360)
443
+ # Progress affects the arc sweep
444
+ actual_end = start_angle + (end_angle - start_angle) * progress
445
+ bbox = [center[0]-radius, center[1]-radius, center[0]+radius, center[1]+radius]
446
+ draw.arc(bbox, start_angle, actual_end, fill=color, width=width)
447
+
448
+ def create_frame(self, paths: List[Dict], path_progress: Dict[int, float]) -> Image.Image:
449
+ """Create a single frame with given path progress"""
450
+ img = Image.new('RGB', (self.WIDTH, self.HEIGHT), self.BG_COLOR)
451
+ draw = ImageDraw.Draw(img)
452
+
453
+ # Sort paths by order
454
+ sorted_paths = sorted(paths, key=lambda p: p.get("order", 0))
455
+
456
+ for path in sorted_paths:
457
+ order = path.get("order", 0)
458
+ progress = path_progress.get(order, 0.0)
459
+ if progress > 0:
460
+ self.draw_path(draw, path, progress)
461
+
462
+ return img
463
+
464
+ def generate_animation(
465
+ self,
466
+ prompt: str,
467
+ output_dir: str = "temp",
468
+ frames_per_path: int = 10,
469
+ fps: int = 30
470
+ ) -> List[str]:
471
+ """
472
+ Generate frame-by-frame drawing animation from prompt.
473
+
474
+ Args:
475
+ prompt: What to draw
476
+ output_dir: Directory to save frames
477
+ frames_per_path: Frames for each path element
478
+ fps: Target frame rate
479
+
480
+ Returns:
481
+ List of frame file paths
482
+ """
483
+ os.makedirs(output_dir, exist_ok=True)
484
+
485
+ # Generate paths with AI
486
+ logger.info(f"Generating drawing paths for: {prompt}")
487
+ drawing = self.generate_paths_with_ai(prompt)
488
+
489
+ paths = drawing.get("paths", [])
490
+ title = drawing.get("title", prompt)
491
+
492
+ logger.info(f"Drawing '{title}' with {len(paths)} paths")
493
+
494
+ # Sort paths by order
495
+ sorted_paths = sorted(paths, key=lambda p: p.get("order", 0))
496
+
497
+ frame_paths = []
498
+ frame_num = 0
499
+
500
+ # Animate each path
501
+ for path_idx, path in enumerate(sorted_paths):
502
+ order = path.get("order", path_idx)
503
+
504
+ # Previous paths are complete
505
+ base_progress = {p.get("order", i): 1.0 for i, p in enumerate(sorted_paths[:path_idx])}
506
+
507
+ # Animate current path
508
+ for f in range(frames_per_path):
509
+ progress = (f + 1) / frames_per_path
510
+ base_progress[order] = progress
511
+
512
+ frame = self.create_frame(paths, base_progress.copy())
513
+
514
+ # Add title
515
+ draw = ImageDraw.Draw(frame)
516
+ bbox = draw.textbbox((0, 0), title, font=self.font_title)
517
+ title_width = bbox[2] - bbox[0]
518
+ draw.text(((self.WIDTH - title_width) // 2, 100), title,
519
+ fill=(50, 50, 50), font=self.font_title)
520
+
521
+ # Save frame
522
+ frame_path = os.path.join(output_dir, f"frame_{frame_num:05d}.png")
523
+ frame.save(frame_path)
524
+ frame_paths.append(frame_path)
525
+ frame_num += 1
526
+
527
+ # Hold final frame
528
+ for _ in range(fps): # 1 second hold
529
+ frame_path = os.path.join(output_dir, f"frame_{frame_num:05d}.png")
530
+ frame.save(frame_path)
531
+ frame_paths.append(frame_path)
532
+ frame_num += 1
533
+
534
+ logger.info(f"Generated {len(frame_paths)} animation frames")
535
+ return frame_paths
static/index.html CHANGED
@@ -607,32 +607,22 @@
607
  <div id="minecraftStatus" class="status hidden"></div>
608
  </div>
609
 
610
- <!-- Code Drawing Animation -->
611
  <div style="margin-bottom: 2rem; padding: 1.5rem; background: var(--bg-secondary); border-radius: 12px;">
612
- <h3 style="margin-bottom: 1rem;">✏️ Code Drawing Animation</h3>
613
  <p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
614
- Line-by-line drawing with color fill
615
  </p>
616
  <form id="drawingForm">
617
  <div class="form-group">
618
- <label>What to Draw</label>
619
- <select id="drawSubject">
620
- <option value="house">House</option>
621
- <option value="tree">Tree</option>
622
- <option value="star">Star</option>
623
- <option value="heart">Heart</option>
624
- </select>
625
  </div>
626
- <div class="form-row">
627
- <div class="form-group">
628
- <label>Style</label>
629
- <select id="drawStyle">
630
- <option value="outline_first">Outline First</option>
631
- <option value="color_fill">With Colors</option>
632
- </select>
633
- </div>
634
- </div>
635
- <button type="submit" class="submit-btn">✏️ Generate Drawing Video</button>
636
  </form>
637
  <div id="drawingStatus" class="status hidden"></div>
638
  </div>
 
607
  <div id="minecraftStatus" class="status hidden"></div>
608
  </div>
609
 
610
+ <!-- AI Drawing Animation -->
611
  <div style="margin-bottom: 2rem; padding: 1.5rem; background: var(--bg-secondary); border-radius: 12px;">
612
+ <h3 style="margin-bottom: 1rem;">✏️ AI Drawing Animation</h3>
613
  <p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
614
+ AI generates paths → Line-by-line animated drawing. Type anything!
615
  </p>
616
  <form id="drawingForm">
617
  <div class="form-group">
618
+ <label>What to Draw (AI-Powered)</label>
619
+ <input type="text" id="drawSubject"
620
+ placeholder="e.g., sports car, human face, flower, sunset, rocket..." value="sports car">
621
+ <small style="color: var(--text-secondary); display: block; margin-top: 0.5rem;">
622
+ 💡 Try: car, face, tree, house, flower, star, heart, sun, or any custom prompt
623
+ </small>
 
624
  </div>
625
+ <button type="submit" class="submit-btn">✏️ Generate AI Drawing Video</button>
 
 
 
 
 
 
 
 
 
626
  </form>
627
  <div id="drawingStatus" class="status hidden"></div>
628
  </div>