ismdrobiul489 commited on
Commit
10d1cee
·
1 Parent(s): 0e9e349

Add Art Reels module - Minecraft Block Art, Code Drawing, Stick Figure Motivation

Browse files
modules/art_reels/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Art Reels Module - Animated Art Content Generator
3
+ Pure Python implementation using Pillow and MoviePy
4
+ """
5
+ import logging
6
+ from config import NCAkitConfig
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def register(app, config: NCAkitConfig):
12
+ """Register art_reels module with the app"""
13
+ try:
14
+ from .router import router
15
+
16
+ # Include router
17
+ app.include_router(router, prefix="/api/art", tags=["Art Reels"])
18
+
19
+ logger.info("Art Reels module registered successfully")
20
+ return True
21
+
22
+ except Exception as e:
23
+ logger.error(f"Failed to register art_reels module: {e}")
24
+ import traceback
25
+ logger.error(traceback.format_exc())
26
+ return False
modules/art_reels/router.py ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Art Reels Module Router
3
+ API endpoints for art content generation
4
+ """
5
+ import logging
6
+ import uuid
7
+ import asyncio
8
+ import os
9
+ import shutil
10
+ from typing import Dict
11
+ from fastapi import APIRouter, HTTPException, BackgroundTasks
12
+ from fastapi.responses import FileResponse
13
+
14
+ from .schemas import (
15
+ MinecraftRequest,
16
+ DrawingRequest,
17
+ StickFigureRequest,
18
+ JobResponse,
19
+ JobStatus
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
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ router = APIRouter()
29
+
30
+ # In-memory job storage
31
+ jobs: Dict[str, Dict] = {}
32
+
33
+ # Initialize services
34
+ block_art = BlockArt()
35
+ drawing_animator = DrawingAnimator()
36
+ stick_figure = StickFigure()
37
+ video_composer = VideoComposer(output_dir="videos/art_reels")
38
+
39
+
40
+ def update_job(job_id: str, status: str, progress: int = 0,
41
+ video_url: str = None, error: str = None):
42
+ """Update job status"""
43
+ if job_id in jobs:
44
+ jobs[job_id].update({
45
+ "status": status,
46
+ "progress": progress,
47
+ "video_url": video_url,
48
+ "error": error
49
+ })
50
+
51
+
52
+ async def generate_minecraft_video(job_id: str, description: str, blocks: int, speed: float):
53
+ """Background task to generate Minecraft block art video"""
54
+ temp_dir = f"temp/art_{job_id}"
55
+
56
+ try:
57
+ update_job(job_id, "processing", 10)
58
+
59
+ # Generate frames
60
+ logger.info(f"Generating Minecraft frames for job {job_id}")
61
+ frames_per_block = max(1, int(3 / speed))
62
+ frame_paths = block_art.generate_build_animation(
63
+ description=description,
64
+ output_dir=temp_dir,
65
+ blocks_per_frame=frames_per_block
66
+ )
67
+
68
+ update_job(job_id, "processing", 60)
69
+
70
+ # Compose video
71
+ logger.info(f"Composing video for job {job_id}")
72
+ video_path = video_composer.compose_video(
73
+ frame_paths=frame_paths,
74
+ output_name=f"minecraft_{job_id}.mp4",
75
+ fps=30
76
+ )
77
+
78
+ update_job(job_id, "processing", 90)
79
+
80
+ # Cleanup frames
81
+ video_composer.cleanup_frames(frame_paths)
82
+ if os.path.exists(temp_dir):
83
+ shutil.rmtree(temp_dir)
84
+
85
+ update_job(job_id, "ready", 100, video_url=f"/api/art/video/{job_id}")
86
+ logger.info(f"Minecraft video ready: {job_id}")
87
+
88
+ except Exception as e:
89
+ logger.error(f"Error generating Minecraft video: {e}")
90
+ update_job(job_id, "failed", error=str(e))
91
+
92
+
93
+ async def generate_drawing_video(job_id: str, subject: str, style: str, colors: bool):
94
+ """Background task to generate drawing animation video"""
95
+ temp_dir = f"temp/art_{job_id}"
96
+
97
+ try:
98
+ update_job(job_id, "processing", 10)
99
+
100
+ # Generate frames
101
+ logger.info(f"Generating drawing frames for job {job_id}")
102
+ frame_paths = drawing_animator.generate_drawing_animation(
103
+ subject=subject,
104
+ output_dir=temp_dir,
105
+ with_colors=colors
106
+ )
107
+
108
+ update_job(job_id, "processing", 60)
109
+
110
+ # Compose video
111
+ logger.info(f"Composing video for job {job_id}")
112
+ video_path = video_composer.compose_video(
113
+ frame_paths=frame_paths,
114
+ output_name=f"drawing_{job_id}.mp4",
115
+ fps=30
116
+ )
117
+
118
+ update_job(job_id, "processing", 90)
119
+
120
+ # Cleanup
121
+ video_composer.cleanup_frames(frame_paths)
122
+ if os.path.exists(temp_dir):
123
+ shutil.rmtree(temp_dir)
124
+
125
+ update_job(job_id, "ready", 100, video_url=f"/api/art/video/{job_id}")
126
+ logger.info(f"Drawing video ready: {job_id}")
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error generating drawing video: {e}")
130
+ update_job(job_id, "failed", error=str(e))
131
+
132
+
133
+ async def generate_stick_figure_video(job_id: str, script: str, voice: str):
134
+ """Background task to generate stick figure motivation video"""
135
+ temp_dir = f"temp/art_{job_id}"
136
+
137
+ try:
138
+ update_job(job_id, "processing", 10)
139
+
140
+ # Parse script into scenes (simple keyword matching)
141
+ scenes = parse_script_to_scenes(script)
142
+
143
+ update_job(job_id, "processing", 30)
144
+
145
+ # Generate frames
146
+ logger.info(f"Generating stick figure frames for job {job_id}")
147
+ frame_paths = stick_figure.generate_motivation_frames(
148
+ scenes=scenes,
149
+ output_dir=temp_dir
150
+ )
151
+
152
+ update_job(job_id, "processing", 60)
153
+
154
+ # Compose video
155
+ logger.info(f"Composing video for job {job_id}")
156
+ video_path = video_composer.compose_video(
157
+ frame_paths=frame_paths,
158
+ output_name=f"stick_{job_id}.mp4",
159
+ fps=30
160
+ )
161
+
162
+ update_job(job_id, "processing", 90)
163
+
164
+ # Cleanup
165
+ video_composer.cleanup_frames(frame_paths)
166
+ if os.path.exists(temp_dir):
167
+ shutil.rmtree(temp_dir)
168
+
169
+ update_job(job_id, "ready", 100, video_url=f"/api/art/video/{job_id}")
170
+ logger.info(f"Stick figure video ready: {job_id}")
171
+
172
+ except Exception as e:
173
+ logger.error(f"Error generating stick figure video: {e}")
174
+ update_job(job_id, "failed", error=str(e))
175
+
176
+
177
+ def parse_script_to_scenes(script: str) -> list:
178
+ """Parse script text into scenes with poses and keywords"""
179
+ scenes = []
180
+
181
+ # Split into sentences
182
+ sentences = [s.strip() for s in script.replace("।", ".").split(".") if s.strip()]
183
+
184
+ # Keyword to pose/prop mapping
185
+ keyword_mapping = {
186
+ # Poses
187
+ "ঘুম": ("sleeping", []),
188
+ "sleep": ("sleeping", []),
189
+ "দৌড়": ("running", []),
190
+ "run": ("running", []),
191
+ "হাঁট": ("walking", []),
192
+ "walk": ("walking", []),
193
+ "বস": ("sitting", []),
194
+ "sit": ("sitting", []),
195
+ "ভাব": ("thinking", []),
196
+ "think": ("thinking", []),
197
+ # Props
198
+ "রাজা": ("standing", ["crown"]),
199
+ "king": ("standing", ["crown"]),
200
+ "বড়লোক": ("standing", ["money", "suit"]),
201
+ "rich": ("standing", ["money", "suit"]),
202
+ "টাকা": ("standing", ["money"]),
203
+ "money": ("standing", ["money"]),
204
+ }
205
+
206
+ for sentence in sentences:
207
+ sentence_lower = sentence.lower()
208
+ pose = "standing"
209
+ props = []
210
+
211
+ # Check for keywords
212
+ for keyword, (detected_pose, detected_props) in keyword_mapping.items():
213
+ if keyword in sentence_lower:
214
+ pose = detected_pose
215
+ props.extend(detected_props)
216
+ break
217
+
218
+ # Check for text overlay triggers
219
+ text = None
220
+ text_triggers = ["get ready", "remember", "important", "key point", "মনে রাখ"]
221
+ for trigger in text_triggers:
222
+ if trigger in sentence_lower:
223
+ text = sentence[:50] + "..." if len(sentence) > 50 else sentence
224
+
225
+ scenes.append({
226
+ "pose": pose,
227
+ "props": list(set(props)),
228
+ "text": text,
229
+ "duration": max(1.5, len(sentence) / 20) # ~20 chars per second
230
+ })
231
+
232
+ return scenes
233
+
234
+
235
+ # ===================
236
+ # API Endpoints
237
+ # ===================
238
+
239
+ @router.post("/minecraft",
240
+ response_model=JobResponse,
241
+ summary="Generate Minecraft block art video",
242
+ description="Create a Minecraft-style block-by-block building animation"
243
+ )
244
+ async def create_minecraft_video(request: MinecraftRequest, background_tasks: BackgroundTasks):
245
+ """Generate Minecraft block art video"""
246
+ job_id = uuid.uuid4().hex[:12]
247
+
248
+ jobs[job_id] = {
249
+ "job_id": job_id,
250
+ "type": "minecraft",
251
+ "status": "queued",
252
+ "progress": 0,
253
+ "video_url": None,
254
+ "error": None
255
+ }
256
+
257
+ background_tasks.add_task(
258
+ generate_minecraft_video,
259
+ job_id,
260
+ request.description,
261
+ request.blocks,
262
+ request.speed
263
+ )
264
+
265
+ return JobResponse(
266
+ job_id=job_id,
267
+ status="queued",
268
+ message="Minecraft video generation started"
269
+ )
270
+
271
+
272
+ @router.post("/drawing",
273
+ response_model=JobResponse,
274
+ summary="Generate code drawing animation",
275
+ description="Create a line-by-line drawing animation video"
276
+ )
277
+ async def create_drawing_video(request: DrawingRequest, background_tasks: BackgroundTasks):
278
+ """Generate code drawing animation video"""
279
+ job_id = uuid.uuid4().hex[:12]
280
+
281
+ jobs[job_id] = {
282
+ "job_id": job_id,
283
+ "type": "drawing",
284
+ "status": "queued",
285
+ "progress": 0,
286
+ "video_url": None,
287
+ "error": None
288
+ }
289
+
290
+ background_tasks.add_task(
291
+ generate_drawing_video,
292
+ job_id,
293
+ request.subject,
294
+ request.style.value,
295
+ request.colors
296
+ )
297
+
298
+ return JobResponse(
299
+ job_id=job_id,
300
+ status="queued",
301
+ message="Drawing video generation started"
302
+ )
303
+
304
+
305
+ @router.post("/stick-figure",
306
+ response_model=JobResponse,
307
+ summary="Generate stick figure motivation video",
308
+ description="Create a stick figure motivation/story video"
309
+ )
310
+ async def create_stick_figure_video(request: StickFigureRequest, background_tasks: BackgroundTasks):
311
+ """Generate stick figure motivation video"""
312
+ job_id = uuid.uuid4().hex[:12]
313
+
314
+ jobs[job_id] = {
315
+ "job_id": job_id,
316
+ "type": "stick_figure",
317
+ "status": "queued",
318
+ "progress": 0,
319
+ "video_url": None,
320
+ "error": None
321
+ }
322
+
323
+ background_tasks.add_task(
324
+ generate_stick_figure_video,
325
+ job_id,
326
+ request.script,
327
+ request.voice
328
+ )
329
+
330
+ return JobResponse(
331
+ job_id=job_id,
332
+ status="queued",
333
+ message="Stick figure video generation started"
334
+ )
335
+
336
+
337
+ @router.get("/{job_id}/status",
338
+ response_model=JobStatus,
339
+ summary="Get job status",
340
+ description="Check the status of a video generation job"
341
+ )
342
+ async def get_job_status(job_id: str):
343
+ """Get video generation job status"""
344
+ if job_id not in jobs:
345
+ raise HTTPException(status_code=404, detail="Job not found")
346
+
347
+ job = jobs[job_id]
348
+ return JobStatus(**job)
349
+
350
+
351
+ @router.get("/video/{job_id}",
352
+ summary="Download video",
353
+ description="Download the generated video file"
354
+ )
355
+ async def download_video(job_id: str):
356
+ """Download generated video"""
357
+ if job_id not in jobs:
358
+ raise HTTPException(status_code=404, detail="Job not found")
359
+
360
+ job = jobs[job_id]
361
+ if job["status"] != "ready":
362
+ raise HTTPException(status_code=400, detail=f"Video not ready. Status: {job['status']}")
363
+
364
+ # Find video file
365
+ video_dir = "videos/art_reels"
366
+ for filename in os.listdir(video_dir):
367
+ if job_id in filename:
368
+ video_path = os.path.join(video_dir, filename)
369
+ return FileResponse(
370
+ video_path,
371
+ media_type="video/mp4",
372
+ filename=filename
373
+ )
374
+
375
+ raise HTTPException(status_code=404, detail="Video file not found")
376
+
377
+
378
+ @router.get("/templates",
379
+ summary="List drawing templates",
380
+ description="Get available drawing templates"
381
+ )
382
+ async def list_templates():
383
+ """List available drawing templates"""
384
+ return {
385
+ "templates": list(DrawingAnimator.TEMPLATES.keys())
386
+ }
modules/art_reels/schemas.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Art Reels Module Schemas
3
+ Request/Response models for art content generation
4
+ """
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, List
7
+ from enum import Enum
8
+
9
+
10
+ # ===================
11
+ # Enums
12
+ # ===================
13
+
14
+ class BlockTypeEnum(str, Enum):
15
+ """Minecraft block types"""
16
+ oak_wood = "oak_wood"
17
+ stone = "stone"
18
+ cobblestone = "cobblestone"
19
+ grass = "grass"
20
+ dirt = "dirt"
21
+ glass = "glass"
22
+ brick = "brick"
23
+
24
+
25
+ class DrawingStyleEnum(str, Enum):
26
+ """Drawing animation styles"""
27
+ outline_first = "outline_first"
28
+ color_fill = "color_fill"
29
+ sketch = "sketch"
30
+
31
+
32
+ class StickPoseEnum(str, Enum):
33
+ """Stick figure poses"""
34
+ standing = "standing"
35
+ walking = "walking"
36
+ running = "running"
37
+ sitting = "sitting"
38
+ sleeping = "sleeping"
39
+ waving = "waving"
40
+ thinking = "thinking"
41
+
42
+
43
+ # ===================
44
+ # Request Models
45
+ # ===================
46
+
47
+ class MinecraftRequest(BaseModel):
48
+ """Request for Minecraft block art video"""
49
+ description: str = Field(..., description="What to build, e.g., 'wooden survival house'")
50
+ blocks: int = Field(50, ge=10, le=200, description="Approximate number of blocks")
51
+ speed: float = Field(1.0, ge=0.5, le=3.0, description="Animation speed multiplier")
52
+
53
+
54
+ class DrawingRequest(BaseModel):
55
+ """Request for code drawing animation"""
56
+ subject: str = Field(..., description="What to draw, e.g., 'a house', 'a tree'")
57
+ style: DrawingStyleEnum = Field(DrawingStyleEnum.outline_first, description="Drawing style")
58
+ colors: bool = Field(True, description="Add colors after outline")
59
+
60
+
61
+ class StickFigureRequest(BaseModel):
62
+ """Request for stick figure motivation video"""
63
+ script: str = Field(..., description="Voice script text")
64
+ voice: str = Field("af_heart", description="Voice ID for TTS")
65
+
66
+
67
+ # ===================
68
+ # Response Models
69
+ # ===================
70
+
71
+ class JobResponse(BaseModel):
72
+ """Job creation response"""
73
+ job_id: str
74
+ status: str
75
+ message: str
76
+
77
+
78
+ class JobStatus(BaseModel):
79
+ """Job status response"""
80
+ job_id: str
81
+ status: str
82
+ progress: int
83
+ video_url: Optional[str] = None
84
+ error: Optional[str] = None
modules/art_reels/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Services init
modules/art_reels/services/block_art.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Block Art - Minecraft-style Isometric Block Drawing
3
+ Pure Python implementation using Pillow
4
+ """
5
+ import logging
6
+ import math
7
+ from PIL import Image, ImageDraw, ImageFont
8
+ from typing import List, Tuple, Dict
9
+ import os
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class BlockArt:
15
+ """
16
+ Creates Minecraft-style isometric block art.
17
+
18
+ Features:
19
+ - Isometric 3D blocks
20
+ - Multiple block types (wood, stone, grass, etc.)
21
+ - Frame-by-frame build animation
22
+ """
23
+
24
+ # Video dimensions (9:16 portrait)
25
+ WIDTH = 1080
26
+ HEIGHT = 1920
27
+
28
+ # Block size and offset for isometric projection
29
+ BLOCK_SIZE = 40
30
+
31
+ # Block colors (top, left, right faces)
32
+ BLOCK_COLORS = {
33
+ "oak_wood": {
34
+ "top": "#8B7355",
35
+ "left": "#6B5344",
36
+ "right": "#5A4535"
37
+ },
38
+ "stone": {
39
+ "top": "#8C8C8C",
40
+ "left": "#6E6E6E",
41
+ "right": "#5A5A5A"
42
+ },
43
+ "cobblestone": {
44
+ "top": "#7A7A7A",
45
+ "left": "#5C5C5C",
46
+ "right": "#4A4A4A"
47
+ },
48
+ "grass": {
49
+ "top": "#4CAF50",
50
+ "left": "#8B7355",
51
+ "right": "#6B5344"
52
+ },
53
+ "dirt": {
54
+ "top": "#8B6914",
55
+ "left": "#6B5010",
56
+ "right": "#5A420D"
57
+ },
58
+ "glass": {
59
+ "top": "#ADD8E6",
60
+ "left": "#87CEEB",
61
+ "right": "#7EC8E3"
62
+ },
63
+ "brick": {
64
+ "top": "#CB4154",
65
+ "left": "#A33545",
66
+ "right": "#8B2D3A"
67
+ },
68
+ "planks": {
69
+ "top": "#C4A35A",
70
+ "left": "#A08040",
71
+ "right": "#8A6B30"
72
+ },
73
+ "roof": {
74
+ "top": "#8B4513",
75
+ "left": "#6B3410",
76
+ "right": "#5A2B0D"
77
+ }
78
+ }
79
+
80
+ def __init__(self):
81
+ self.frames = []
82
+
83
+ def hex_to_rgb(self, hex_color: str) -> Tuple[int, int, int]:
84
+ """Convert hex color to RGB tuple"""
85
+ hex_color = hex_color.lstrip('#')
86
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
87
+
88
+ def iso_to_screen(self, x: int, y: int, z: int) -> Tuple[int, int]:
89
+ """Convert 3D isometric coordinates to 2D screen position"""
90
+ # Isometric projection
91
+ screen_x = self.WIDTH // 2 + (x - y) * (self.BLOCK_SIZE // 2)
92
+ screen_y = self.HEIGHT // 2 + (x + y) * (self.BLOCK_SIZE // 4) - z * (self.BLOCK_SIZE // 2)
93
+ return (screen_x, screen_y)
94
+
95
+ def draw_block(self, draw: ImageDraw, x: int, y: int, z: int, block_type: str = "oak_wood"):
96
+ """Draw a single isometric block"""
97
+ colors = self.BLOCK_COLORS.get(block_type, self.BLOCK_COLORS["oak_wood"])
98
+
99
+ # Calculate screen position
100
+ sx, sy = self.iso_to_screen(x, y, z)
101
+
102
+ half = self.BLOCK_SIZE // 2
103
+ quarter = self.BLOCK_SIZE // 4
104
+
105
+ # Top face (diamond shape)
106
+ top_points = [
107
+ (sx, sy - half), # top
108
+ (sx + half, sy - quarter), # right
109
+ (sx, sy), # bottom
110
+ (sx - half, sy - quarter) # left
111
+ ]
112
+ draw.polygon(top_points, fill=self.hex_to_rgb(colors["top"]), outline=(0, 0, 0))
113
+
114
+ # Left face
115
+ left_points = [
116
+ (sx - half, sy - quarter), # top
117
+ (sx, sy), # right
118
+ (sx, sy + half), # bottom
119
+ (sx - half, sy + quarter) # left
120
+ ]
121
+ draw.polygon(left_points, fill=self.hex_to_rgb(colors["left"]), outline=(0, 0, 0))
122
+
123
+ # Right face
124
+ right_points = [
125
+ (sx, sy), # left
126
+ (sx + half, sy - quarter), # top
127
+ (sx + half, sy + quarter), # right
128
+ (sx, sy + half) # bottom
129
+ ]
130
+ draw.polygon(right_points, fill=self.hex_to_rgb(colors["right"]), outline=(0, 0, 0))
131
+
132
+ def generate_simple_house(self) -> List[Dict]:
133
+ """Generate block positions for a simple house"""
134
+ blocks = []
135
+
136
+ # Ground/Floor (5x5)
137
+ for x in range(5):
138
+ for y in range(5):
139
+ blocks.append({"x": x, "y": y, "z": 0, "type": "grass"})
140
+
141
+ # Floor (3x3 planks)
142
+ for x in range(1, 4):
143
+ for y in range(1, 4):
144
+ blocks.append({"x": x, "y": y, "z": 1, "type": "planks"})
145
+
146
+ # Walls - Layer 1
147
+ for x in range(1, 4):
148
+ blocks.append({"x": x, "y": 0, "z": 2, "type": "oak_wood"})
149
+ blocks.append({"x": x, "y": 4, "z": 2, "type": "oak_wood"})
150
+ for y in range(1, 4):
151
+ blocks.append({"x": 0, "y": y, "z": 2, "type": "oak_wood"})
152
+ blocks.append({"x": 4, "y": y, "z": 2, "type": "oak_wood"})
153
+
154
+ # Walls - Layer 2
155
+ for x in range(1, 4):
156
+ blocks.append({"x": x, "y": 0, "z": 3, "type": "oak_wood"})
157
+ blocks.append({"x": x, "y": 4, "z": 3, "type": "oak_wood"})
158
+ for y in range(1, 4):
159
+ blocks.append({"x": 0, "y": y, "z": 3, "type": "oak_wood"})
160
+ blocks.append({"x": 4, "y": y, "z": 3, "type": "oak_wood"})
161
+
162
+ # Corner posts
163
+ for z in [2, 3, 4]:
164
+ blocks.append({"x": 0, "y": 0, "z": z, "type": "oak_wood"})
165
+ blocks.append({"x": 4, "y": 0, "z": z, "type": "oak_wood"})
166
+ blocks.append({"x": 0, "y": 4, "z": z, "type": "oak_wood"})
167
+ blocks.append({"x": 4, "y": 4, "z": z, "type": "oak_wood"})
168
+
169
+ # Window (glass)
170
+ blocks.append({"x": 2, "y": 0, "z": 3, "type": "glass"})
171
+ blocks.append({"x": 2, "y": 4, "z": 3, "type": "glass"})
172
+
173
+ # Roof
174
+ for x in range(5):
175
+ for y in range(5):
176
+ blocks.append({"x": x, "y": y, "z": 5, "type": "roof"})
177
+
178
+ return blocks
179
+
180
+ def create_frame(self, blocks_to_draw: List[Dict], bg_color: str = "#87CEEB") -> Image.Image:
181
+ """Create a single frame with given blocks"""
182
+ # Create image with sky blue background
183
+ img = Image.new('RGB', (self.WIDTH, self.HEIGHT), self.hex_to_rgb(bg_color))
184
+ draw = ImageDraw.Draw(img)
185
+
186
+ # Sort blocks for proper rendering (back to front, bottom to top)
187
+ sorted_blocks = sorted(blocks_to_draw, key=lambda b: (b["z"], b["x"] + b["y"]))
188
+
189
+ for block in sorted_blocks:
190
+ self.draw_block(draw, block["x"], block["y"], block["z"], block["type"])
191
+
192
+ return img
193
+
194
+ def generate_build_animation(
195
+ self,
196
+ description: str = "wooden survival house",
197
+ output_dir: str = "temp",
198
+ blocks_per_frame: int = 2
199
+ ) -> List[str]:
200
+ """
201
+ Generate frame-by-frame build animation.
202
+
203
+ Args:
204
+ description: What to build (for future AI integration)
205
+ output_dir: Directory to save frames
206
+ blocks_per_frame: How many blocks to add per frame
207
+
208
+ Returns:
209
+ List of frame file paths
210
+ """
211
+ os.makedirs(output_dir, exist_ok=True)
212
+
213
+ # Get house blocks
214
+ all_blocks = self.generate_simple_house()
215
+
216
+ logger.info(f"Generating animation with {len(all_blocks)} blocks")
217
+
218
+ frame_paths = []
219
+ frame_num = 0
220
+
221
+ # Generate frames progressively
222
+ for i in range(0, len(all_blocks), blocks_per_frame):
223
+ current_blocks = all_blocks[:i + blocks_per_frame]
224
+
225
+ # Create frame
226
+ frame = self.create_frame(current_blocks)
227
+
228
+ # Add "Building..." text
229
+ draw = ImageDraw.Draw(frame)
230
+ try:
231
+ font = ImageFont.truetype("arial.ttf", 48)
232
+ except:
233
+ font = ImageFont.load_default()
234
+
235
+ progress = min(100, int((i + blocks_per_frame) / len(all_blocks) * 100))
236
+ text = f"Building... {progress}%"
237
+
238
+ # Center text at bottom
239
+ bbox = draw.textbbox((0, 0), text, font=font)
240
+ text_width = bbox[2] - bbox[0]
241
+ draw.text(
242
+ ((self.WIDTH - text_width) // 2, self.HEIGHT - 150),
243
+ text,
244
+ fill=(255, 255, 255),
245
+ font=font,
246
+ stroke_width=2,
247
+ stroke_fill=(0, 0, 0)
248
+ )
249
+
250
+ # Save frame
251
+ frame_path = os.path.join(output_dir, f"frame_{frame_num:04d}.png")
252
+ frame.save(frame_path)
253
+ frame_paths.append(frame_path)
254
+ frame_num += 1
255
+
256
+ # Hold final frame for a bit
257
+ final_frame = self.create_frame(all_blocks)
258
+ draw = ImageDraw.Draw(final_frame)
259
+ try:
260
+ font = ImageFont.truetype("arial.ttf", 60)
261
+ except:
262
+ font = ImageFont.load_default()
263
+
264
+ text = "✓ Complete!"
265
+ bbox = draw.textbbox((0, 0), text, font=font)
266
+ text_width = bbox[2] - bbox[0]
267
+ draw.text(
268
+ ((self.WIDTH - text_width) // 2, self.HEIGHT - 150),
269
+ text,
270
+ fill=(76, 175, 80),
271
+ font=font,
272
+ stroke_width=2,
273
+ stroke_fill=(0, 0, 0)
274
+ )
275
+
276
+ for _ in range(30): # Hold for 1 second at 30fps
277
+ frame_path = os.path.join(output_dir, f"frame_{frame_num:04d}.png")
278
+ final_frame.save(frame_path)
279
+ frame_paths.append(frame_path)
280
+ frame_num += 1
281
+
282
+ logger.info(f"Generated {len(frame_paths)} frames")
283
+ return frame_paths
modules/art_reels/services/drawing_animator.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Drawing Animator - Line-by-line Code Drawing Animation
3
+ Pure Python implementation using Pillow
4
+ """
5
+ import logging
6
+ import math
7
+ from PIL import Image, ImageDraw, ImageFont
8
+ from typing import List, Tuple, Dict
9
+ import os
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class DrawingAnimator:
15
+ """
16
+ Creates line-by-line drawing animations.
17
+
18
+ Features:
19
+ - Outline drawing animation
20
+ - Color fill animation
21
+ - Multiple drawing templates
22
+ """
23
+
24
+ # Video dimensions (9:16 portrait)
25
+ WIDTH = 1080
26
+ HEIGHT = 1920
27
+
28
+ # Colors
29
+ BG_COLOR = (255, 255, 255) # White
30
+ OUTLINE_COLOR = (50, 50, 50) # Dark gray
31
+ LINE_WIDTH = 4
32
+
33
+ # Drawing templates
34
+ TEMPLATES = {
35
+ "house": {
36
+ "lines": [
37
+ # Base rectangle
38
+ {"start": (340, 1200), "end": (740, 1200), "order": 1}, # bottom
39
+ {"start": (740, 1200), "end": (740, 800), "order": 2}, # right
40
+ {"start": (740, 800), "end": (340, 800), "order": 3}, # top
41
+ {"start": (340, 800), "end": (340, 1200), "order": 4}, # left
42
+ # Roof
43
+ {"start": (300, 800), "end": (540, 550), "order": 5}, # left roof
44
+ {"start": (540, 550), "end": (780, 800), "order": 6}, # right roof
45
+ # Door
46
+ {"start": (480, 1200), "end": (480, 1000), "order": 7},
47
+ {"start": (480, 1000), "end": (600, 1000), "order": 8},
48
+ {"start": (600, 1000), "end": (600, 1200), "order": 9},
49
+ # Window
50
+ {"start": (400, 900), "end": (400, 950), "order": 10},
51
+ {"start": (400, 950), "end": (460, 950), "order": 11},
52
+ {"start": (460, 950), "end": (460, 900), "order": 12},
53
+ {"start": (460, 900), "end": (400, 900), "order": 13},
54
+ ],
55
+ "fills": [
56
+ {"points": [(340, 800), (740, 800), (740, 1200), (340, 1200)], "color": (255, 230, 150), "order": 1},
57
+ {"points": [(300, 800), (540, 550), (780, 800)], "color": (180, 80, 80), "order": 2},
58
+ {"points": [(480, 1000), (600, 1000), (600, 1200), (480, 1200)], "color": (139, 90, 43), "order": 3},
59
+ ]
60
+ },
61
+ "tree": {
62
+ "lines": [
63
+ # Trunk
64
+ {"start": (500, 1300), "end": (580, 1300), "order": 1},
65
+ {"start": (580, 1300), "end": (580, 1000), "order": 2},
66
+ {"start": (580, 1000), "end": (500, 1000), "order": 3},
67
+ {"start": (500, 1000), "end": (500, 1300), "order": 4},
68
+ # Leaves (triangle layers)
69
+ {"start": (400, 1000), "end": (540, 750), "order": 5},
70
+ {"start": (540, 750), "end": (680, 1000), "order": 6},
71
+ {"start": (680, 1000), "end": (400, 1000), "order": 7},
72
+ {"start": (420, 850), "end": (540, 600), "order": 8},
73
+ {"start": (540, 600), "end": (660, 850), "order": 9},
74
+ {"start": (660, 850), "end": (420, 850), "order": 10},
75
+ ],
76
+ "fills": [
77
+ {"points": [(500, 1000), (580, 1000), (580, 1300), (500, 1300)], "color": (139, 90, 43), "order": 1},
78
+ {"points": [(400, 1000), (540, 750), (680, 1000)], "color": (34, 139, 34), "order": 2},
79
+ {"points": [(420, 850), (540, 600), (660, 850)], "color": (50, 160, 50), "order": 3},
80
+ ]
81
+ },
82
+ "star": {
83
+ "lines": [
84
+ {"start": (540, 600), "end": (580, 750), "order": 1},
85
+ {"start": (580, 750), "end": (740, 750), "order": 2},
86
+ {"start": (740, 750), "end": (620, 850), "order": 3},
87
+ {"start": (620, 850), "end": (680, 1000), "order": 4},
88
+ {"start": (680, 1000), "end": (540, 900), "order": 5},
89
+ {"start": (540, 900), "end": (400, 1000), "order": 6},
90
+ {"start": (400, 1000), "end": (460, 850), "order": 7},
91
+ {"start": (460, 850), "end": (340, 750), "order": 8},
92
+ {"start": (340, 750), "end": (500, 750), "order": 9},
93
+ {"start": (500, 750), "end": (540, 600), "order": 10},
94
+ ],
95
+ "fills": [
96
+ {"points": [(540, 600), (580, 750), (740, 750), (620, 850), (680, 1000),
97
+ (540, 900), (400, 1000), (460, 850), (340, 750), (500, 750)],
98
+ "color": (255, 215, 0), "order": 1},
99
+ ]
100
+ },
101
+ "heart": {
102
+ "lines": [
103
+ # Left curve (approximated with lines)
104
+ {"start": (540, 750), "end": (440, 650), "order": 1},
105
+ {"start": (440, 650), "end": (380, 700), "order": 2},
106
+ {"start": (380, 700), "end": (380, 800), "order": 3},
107
+ {"start": (380, 800), "end": (540, 1000), "order": 4},
108
+ # Right curve
109
+ {"start": (540, 750), "end": (640, 650), "order": 5},
110
+ {"start": (640, 650), "end": (700, 700), "order": 6},
111
+ {"start": (700, 700), "end": (700, 800), "order": 7},
112
+ {"start": (700, 800), "end": (540, 1000), "order": 8},
113
+ ],
114
+ "fills": [
115
+ {"points": [(540, 750), (440, 650), (380, 700), (380, 800), (540, 1000),
116
+ (700, 800), (700, 700), (640, 650)],
117
+ "color": (255, 100, 100), "order": 1},
118
+ ]
119
+ }
120
+ }
121
+
122
+ def __init__(self):
123
+ self.frames = []
124
+
125
+ def draw_line_animated(
126
+ self,
127
+ start: Tuple[int, int],
128
+ end: Tuple[int, int],
129
+ progress: float
130
+ ) -> Tuple[Tuple[int, int], Tuple[int, int]]:
131
+ """Calculate partial line based on progress (0.0 to 1.0)"""
132
+ x1, y1 = start
133
+ x2, y2 = end
134
+
135
+ current_x = int(x1 + (x2 - x1) * progress)
136
+ current_y = int(y1 + (y2 - y1) * progress)
137
+
138
+ return (start, (current_x, current_y))
139
+
140
+ def create_frame(
141
+ self,
142
+ template_name: str,
143
+ line_progress: Dict[int, float], # order -> progress
144
+ fill_progress: Dict[int, bool] # order -> filled
145
+ ) -> Image.Image:
146
+ """Create a single frame of the drawing animation"""
147
+ template = self.TEMPLATES.get(template_name, self.TEMPLATES["house"])
148
+
149
+ # Create white background
150
+ img = Image.new('RGB', (self.WIDTH, self.HEIGHT), self.BG_COLOR)
151
+ draw = ImageDraw.Draw(img)
152
+
153
+ # Draw completed fills
154
+ for fill in template.get("fills", []):
155
+ if fill_progress.get(fill["order"], False):
156
+ draw.polygon(fill["points"], fill=fill["color"])
157
+
158
+ # Draw lines with progress
159
+ for line in template["lines"]:
160
+ order = line["order"]
161
+ progress = line_progress.get(order, 0.0)
162
+
163
+ if progress > 0:
164
+ if progress >= 1.0:
165
+ # Complete line
166
+ draw.line([line["start"], line["end"]],
167
+ fill=self.OUTLINE_COLOR, width=self.LINE_WIDTH)
168
+ else:
169
+ # Partial line
170
+ start, end = self.draw_line_animated(
171
+ line["start"], line["end"], progress
172
+ )
173
+ draw.line([start, end],
174
+ fill=self.OUTLINE_COLOR, width=self.LINE_WIDTH)
175
+
176
+ return img
177
+
178
+ def generate_drawing_animation(
179
+ self,
180
+ subject: str = "house",
181
+ output_dir: str = "temp",
182
+ with_colors: bool = True,
183
+ frames_per_line: int = 15
184
+ ) -> List[str]:
185
+ """
186
+ Generate frame-by-frame drawing animation.
187
+
188
+ Args:
189
+ subject: What to draw (house, tree, star, heart)
190
+ output_dir: Directory to save frames
191
+ with_colors: Whether to add color fill after outline
192
+ frames_per_line: Frames per line segment
193
+
194
+ Returns:
195
+ List of frame file paths
196
+ """
197
+ os.makedirs(output_dir, exist_ok=True)
198
+
199
+ template_name = subject.lower()
200
+ if template_name not in self.TEMPLATES:
201
+ template_name = "house"
202
+
203
+ template = self.TEMPLATES[template_name]
204
+ lines = sorted(template["lines"], key=lambda x: x["order"])
205
+ fills = sorted(template.get("fills", []), key=lambda x: x["order"])
206
+
207
+ frame_paths = []
208
+ frame_num = 0
209
+
210
+ # Track progress
211
+ line_progress = {}
212
+ fill_progress = {}
213
+
214
+ # Animate each line
215
+ for line in lines:
216
+ order = line["order"]
217
+
218
+ for i in range(frames_per_line):
219
+ progress = (i + 1) / frames_per_line
220
+ line_progress[order] = progress
221
+
222
+ # Create frame
223
+ frame = self.create_frame(template_name, line_progress, fill_progress)
224
+
225
+ # Add "Drawing..." text
226
+ draw = ImageDraw.Draw(frame)
227
+ try:
228
+ font = ImageFont.truetype("arial.ttf", 40)
229
+ except:
230
+ font = ImageFont.load_default()
231
+
232
+ text = f"Drawing {subject.title()}..."
233
+ bbox = draw.textbbox((0, 0), text, font=font)
234
+ text_width = bbox[2] - bbox[0]
235
+ draw.text(
236
+ ((self.WIDTH - text_width) // 2, self.HEIGHT - 150),
237
+ text,
238
+ fill=(100, 100, 100),
239
+ font=font
240
+ )
241
+
242
+ frame_path = os.path.join(output_dir, f"frame_{frame_num:04d}.png")
243
+ frame.save(frame_path)
244
+ frame_paths.append(frame_path)
245
+ frame_num += 1
246
+
247
+ # Animate color fills
248
+ if with_colors and fills:
249
+ for fill in fills:
250
+ fill_progress[fill["order"]] = True
251
+
252
+ # Hold each fill for some frames
253
+ for _ in range(20):
254
+ frame = self.create_frame(template_name, line_progress, fill_progress)
255
+
256
+ draw = ImageDraw.Draw(frame)
257
+ try:
258
+ font = ImageFont.truetype("arial.ttf", 40)
259
+ except:
260
+ font = ImageFont.load_default()
261
+
262
+ text = "Adding colors..."
263
+ bbox = draw.textbbox((0, 0), text, font=font)
264
+ text_width = bbox[2] - bbox[0]
265
+ draw.text(
266
+ ((self.WIDTH - text_width) // 2, self.HEIGHT - 150),
267
+ text,
268
+ fill=(100, 100, 100),
269
+ font=font
270
+ )
271
+
272
+ frame_path = os.path.join(output_dir, f"frame_{frame_num:04d}.png")
273
+ frame.save(frame_path)
274
+ frame_paths.append(frame_path)
275
+ frame_num += 1
276
+
277
+ # Final frame with "Complete!"
278
+ for _ in range(45): # 1.5 seconds
279
+ frame = self.create_frame(template_name, line_progress, fill_progress)
280
+
281
+ draw = ImageDraw.Draw(frame)
282
+ try:
283
+ font = ImageFont.truetype("arial.ttf", 60)
284
+ except:
285
+ font = ImageFont.load_default()
286
+
287
+ text = "✓ Complete!"
288
+ bbox = draw.textbbox((0, 0), text, font=font)
289
+ text_width = bbox[2] - bbox[0]
290
+ draw.text(
291
+ ((self.WIDTH - text_width) // 2, self.HEIGHT - 150),
292
+ text,
293
+ fill=(76, 175, 80),
294
+ font=font
295
+ )
296
+
297
+ frame_path = os.path.join(output_dir, f"frame_{frame_num:04d}.png")
298
+ frame.save(frame_path)
299
+ frame_paths.append(frame_path)
300
+ frame_num += 1
301
+
302
+ logger.info(f"Generated {len(frame_paths)} frames for drawing animation")
303
+ return frame_paths
modules/art_reels/services/stick_figure.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Stick Figure - Simple Stick Figure Drawing for Motivation Videos
3
+ Pure Python implementation using Pillow
4
+ """
5
+ import logging
6
+ import math
7
+ from PIL import Image, ImageDraw, ImageFont
8
+ from typing import List, Tuple, Dict, Optional
9
+ import os
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class StickFigure:
15
+ """
16
+ Creates stick figure drawings for motivation/story videos.
17
+
18
+ Features:
19
+ - Simple black stick figures
20
+ - Various poses (standing, walking, running, etc.)
21
+ - Props (crown, money bag, bed, etc.)
22
+ - Text overlays
23
+ - White background (clean look)
24
+ """
25
+
26
+ # Video dimensions (9:16 portrait)
27
+ WIDTH = 1080
28
+ HEIGHT = 1920
29
+
30
+ # Stick figure size
31
+ HEAD_RADIUS = 40
32
+ BODY_LENGTH = 120
33
+ ARM_LENGTH = 80
34
+ LEG_LENGTH = 100
35
+ LINE_WIDTH = 8
36
+
37
+ # Colors
38
+ BG_COLOR = (255, 255, 255) # White
39
+ FIGURE_COLOR = (0, 0, 0) # Black
40
+
41
+ def __init__(self):
42
+ self.frames = []
43
+
44
+ def draw_stick_figure(
45
+ self,
46
+ draw: ImageDraw,
47
+ x: int,
48
+ y: int,
49
+ pose: str = "standing",
50
+ scale: float = 1.0,
51
+ props: List[str] = None
52
+ ):
53
+ """
54
+ Draw a stick figure at position (x, y).
55
+
56
+ Args:
57
+ draw: PIL ImageDraw object
58
+ x, y: Center position for the figure
59
+ pose: Pose type
60
+ scale: Size multiplier
61
+ props: List of props to add
62
+ """
63
+ props = props or []
64
+
65
+ # Scale dimensions
66
+ head_r = int(self.HEAD_RADIUS * scale)
67
+ body_len = int(self.BODY_LENGTH * scale)
68
+ arm_len = int(self.ARM_LENGTH * scale)
69
+ leg_len = int(self.LEG_LENGTH * scale)
70
+ line_w = max(4, int(self.LINE_WIDTH * scale))
71
+
72
+ # Head position
73
+ head_y = y - body_len - head_r
74
+
75
+ # Draw based on pose
76
+ if pose == "standing":
77
+ self._draw_standing(draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w)
78
+ elif pose == "walking":
79
+ self._draw_walking(draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w)
80
+ elif pose == "running":
81
+ self._draw_running(draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w)
82
+ elif pose == "sitting":
83
+ self._draw_sitting(draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w)
84
+ elif pose == "sleeping":
85
+ self._draw_sleeping(draw, x, y, head_r, body_len, arm_len, leg_len, line_w)
86
+ elif pose == "waving":
87
+ self._draw_waving(draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w)
88
+ elif pose == "thinking":
89
+ self._draw_thinking(draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w)
90
+ else:
91
+ self._draw_standing(draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w)
92
+
93
+ # Draw props
94
+ for prop in props:
95
+ self._draw_prop(draw, x, head_y, head_r, prop, scale)
96
+
97
+ def _draw_standing(self, draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w):
98
+ """Draw standing pose"""
99
+ # Head
100
+ draw.ellipse([x - head_r, head_y - head_r, x + head_r, head_y + head_r],
101
+ outline=self.FIGURE_COLOR, width=line_w)
102
+
103
+ # Body
104
+ body_top = head_y + head_r
105
+ body_bottom = body_top + body_len
106
+ draw.line([x, body_top, x, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
107
+
108
+ # Arms (slightly down)
109
+ arm_y = body_top + body_len // 4
110
+ draw.line([x, arm_y, x - arm_len, arm_y + arm_len // 2], fill=self.FIGURE_COLOR, width=line_w)
111
+ draw.line([x, arm_y, x + arm_len, arm_y + arm_len // 2], fill=self.FIGURE_COLOR, width=line_w)
112
+
113
+ # Legs
114
+ draw.line([x, body_bottom, x - leg_len // 2, body_bottom + leg_len], fill=self.FIGURE_COLOR, width=line_w)
115
+ draw.line([x, body_bottom, x + leg_len // 2, body_bottom + leg_len], fill=self.FIGURE_COLOR, width=line_w)
116
+
117
+ def _draw_walking(self, draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w):
118
+ """Draw walking pose"""
119
+ # Head
120
+ draw.ellipse([x - head_r, head_y - head_r, x + head_r, head_y + head_r],
121
+ outline=self.FIGURE_COLOR, width=line_w)
122
+
123
+ # Body (slightly leaning)
124
+ body_top = head_y + head_r
125
+ body_bottom = body_top + body_len
126
+ draw.line([x, body_top, x + 10, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
127
+
128
+ # Arms (swinging)
129
+ arm_y = body_top + body_len // 4
130
+ draw.line([x, arm_y, x - arm_len // 2, arm_y + arm_len], fill=self.FIGURE_COLOR, width=line_w)
131
+ draw.line([x, arm_y, x + arm_len, arm_y - arm_len // 3], fill=self.FIGURE_COLOR, width=line_w)
132
+
133
+ # Legs (one forward, one back)
134
+ draw.line([x + 10, body_bottom, x - leg_len, body_bottom + leg_len], fill=self.FIGURE_COLOR, width=line_w)
135
+ draw.line([x + 10, body_bottom, x + leg_len, body_bottom + leg_len // 2], fill=self.FIGURE_COLOR, width=line_w)
136
+
137
+ def _draw_running(self, draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w):
138
+ """Draw running pose"""
139
+ # Head (leaning forward)
140
+ draw.ellipse([x - head_r + 20, head_y - head_r, x + head_r + 20, head_y + head_r],
141
+ outline=self.FIGURE_COLOR, width=line_w)
142
+
143
+ # Body (leaning forward)
144
+ body_top = head_y + head_r
145
+ body_bottom = body_top + body_len
146
+ draw.line([x + 20, body_top, x + 40, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
147
+
148
+ # Arms (dynamic swing)
149
+ arm_y = body_top + body_len // 4 + 20
150
+ draw.line([x + 20, arm_y, x - arm_len, arm_y + arm_len // 3], fill=self.FIGURE_COLOR, width=line_w)
151
+ draw.line([x + 20, arm_y, x + arm_len + 30, arm_y - arm_len // 2], fill=self.FIGURE_COLOR, width=line_w)
152
+
153
+ # Legs (wide stride)
154
+ draw.line([x + 40, body_bottom, x - leg_len - 20, body_bottom + leg_len // 2], fill=self.FIGURE_COLOR, width=line_w)
155
+ draw.line([x + 40, body_bottom, x + leg_len + 40, body_bottom + leg_len // 3], fill=self.FIGURE_COLOR, width=line_w)
156
+
157
+ def _draw_sitting(self, draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w):
158
+ """Draw sitting pose"""
159
+ head_y = head_y + leg_len # Lower position
160
+
161
+ # Head
162
+ draw.ellipse([x - head_r, head_y - head_r, x + head_r, head_y + head_r],
163
+ outline=self.FIGURE_COLOR, width=line_w)
164
+
165
+ # Body
166
+ body_top = head_y + head_r
167
+ body_bottom = body_top + body_len // 2
168
+ draw.line([x, body_top, x, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
169
+
170
+ # Arms (on lap)
171
+ arm_y = body_top + body_len // 4
172
+ draw.line([x, arm_y, x - arm_len // 2, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
173
+ draw.line([x, arm_y, x + arm_len // 2, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
174
+
175
+ # Legs (bent)
176
+ draw.line([x, body_bottom, x - leg_len, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
177
+ draw.line([x - leg_len, body_bottom, x - leg_len, body_bottom + leg_len // 2], fill=self.FIGURE_COLOR, width=line_w)
178
+ draw.line([x, body_bottom, x + leg_len, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
179
+ draw.line([x + leg_len, body_bottom, x + leg_len, body_bottom + leg_len // 2], fill=self.FIGURE_COLOR, width=line_w)
180
+
181
+ def _draw_sleeping(self, draw, x, y, head_r, body_len, arm_len, leg_len, line_w):
182
+ """Draw sleeping pose (horizontal)"""
183
+ # Horizontal figure
184
+ y = y - leg_len # Adjust position
185
+
186
+ # Head
187
+ draw.ellipse([x - body_len - head_r, y - head_r, x - body_len + head_r, y + head_r],
188
+ outline=self.FIGURE_COLOR, width=line_w)
189
+
190
+ # Body (horizontal)
191
+ draw.line([x - body_len + head_r, y, x, y], fill=self.FIGURE_COLOR, width=line_w)
192
+
193
+ # Arms (tucked)
194
+ draw.line([x - body_len // 2, y, x - body_len // 2, y - arm_len // 3], fill=self.FIGURE_COLOR, width=line_w)
195
+
196
+ # Legs (horizontal)
197
+ draw.line([x, y, x + leg_len, y + 10], fill=self.FIGURE_COLOR, width=line_w)
198
+ draw.line([x, y, x + leg_len, y - 10], fill=self.FIGURE_COLOR, width=line_w)
199
+
200
+ # Zzz
201
+ draw.text((x - body_len, y - head_r - 60), "Zzz", fill=self.FIGURE_COLOR)
202
+
203
+ def _draw_waving(self, draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w):
204
+ """Draw waving pose"""
205
+ # Head
206
+ draw.ellipse([x - head_r, head_y - head_r, x + head_r, head_y + head_r],
207
+ outline=self.FIGURE_COLOR, width=line_w)
208
+
209
+ # Body
210
+ body_top = head_y + head_r
211
+ body_bottom = body_top + body_len
212
+ draw.line([x, body_top, x, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
213
+
214
+ # Arms (one waving up)
215
+ arm_y = body_top + body_len // 4
216
+ draw.line([x, arm_y, x - arm_len, arm_y + arm_len // 2], fill=self.FIGURE_COLOR, width=line_w)
217
+ draw.line([x, arm_y, x + arm_len // 2, arm_y - arm_len], fill=self.FIGURE_COLOR, width=line_w)
218
+
219
+ # Legs
220
+ draw.line([x, body_bottom, x - leg_len // 2, body_bottom + leg_len], fill=self.FIGURE_COLOR, width=line_w)
221
+ draw.line([x, body_bottom, x + leg_len // 2, body_bottom + leg_len], fill=self.FIGURE_COLOR, width=line_w)
222
+
223
+ def _draw_thinking(self, draw, x, head_y, head_r, body_len, arm_len, leg_len, line_w):
224
+ """Draw thinking pose"""
225
+ # Head
226
+ draw.ellipse([x - head_r, head_y - head_r, x + head_r, head_y + head_r],
227
+ outline=self.FIGURE_COLOR, width=line_w)
228
+
229
+ # Body
230
+ body_top = head_y + head_r
231
+ body_bottom = body_top + body_len
232
+ draw.line([x, body_top, x, body_bottom], fill=self.FIGURE_COLOR, width=line_w)
233
+
234
+ # Arms (one on chin)
235
+ arm_y = body_top + body_len // 4
236
+ draw.line([x, arm_y, x - arm_len, arm_y + arm_len // 2], fill=self.FIGURE_COLOR, width=line_w)
237
+ # Arm to chin
238
+ draw.line([x, arm_y, x + arm_len // 3, arm_y - arm_len // 2], fill=self.FIGURE_COLOR, width=line_w)
239
+ draw.line([x + arm_len // 3, arm_y - arm_len // 2, x + head_r // 2, head_y + head_r // 2],
240
+ fill=self.FIGURE_COLOR, width=line_w)
241
+
242
+ # Legs
243
+ draw.line([x, body_bottom, x - leg_len // 2, body_bottom + leg_len], fill=self.FIGURE_COLOR, width=line_w)
244
+ draw.line([x, body_bottom, x + leg_len // 2, body_bottom + leg_len], fill=self.FIGURE_COLOR, width=line_w)
245
+
246
+ # Thought bubble
247
+ draw.ellipse([x + head_r + 20, head_y - head_r - 80, x + head_r + 120, head_y - head_r],
248
+ outline=self.FIGURE_COLOR, width=3)
249
+ draw.text((x + head_r + 45, head_y - head_r - 55), "?", fill=self.FIGURE_COLOR)
250
+
251
+ def _draw_prop(self, draw, x, head_y, head_r, prop: str, scale: float):
252
+ """Draw props on the figure"""
253
+ if prop == "crown":
254
+ # Crown on head
255
+ crown_y = head_y - head_r - 10
256
+ points = [
257
+ (x - 30, crown_y),
258
+ (x - 20, crown_y - 30),
259
+ (x, crown_y - 10),
260
+ (x + 20, crown_y - 30),
261
+ (x + 30, crown_y)
262
+ ]
263
+ draw.polygon(points, fill=(255, 215, 0), outline=(0, 0, 0))
264
+
265
+ elif prop == "money":
266
+ # Money bag
267
+ bag_x = x + 80
268
+ bag_y = head_y + 100
269
+ draw.ellipse([bag_x, bag_y, bag_x + 50, bag_y + 60], fill=(34, 139, 34), outline=(0, 0, 0))
270
+ draw.text((bag_x + 15, bag_y + 15), "$", fill=(255, 255, 255))
271
+
272
+ elif prop == "suit":
273
+ # Simple tie
274
+ tie_y = head_y + head_r + 10
275
+ draw.polygon([
276
+ (x, tie_y),
277
+ (x - 15, tie_y + 20),
278
+ (x, tie_y + 80),
279
+ (x + 15, tie_y + 20)
280
+ ], fill=(128, 0, 0))
281
+
282
+ def add_text_overlay(
283
+ self,
284
+ img: Image.Image,
285
+ text: str,
286
+ position: str = "center",
287
+ font_size: int = 80
288
+ ) -> Image.Image:
289
+ """Add text overlay to image"""
290
+ draw = ImageDraw.Draw(img)
291
+
292
+ try:
293
+ font = ImageFont.truetype("arial.ttf", font_size)
294
+ except:
295
+ font = ImageFont.load_default()
296
+
297
+ bbox = draw.textbbox((0, 0), text, font=font)
298
+ text_width = bbox[2] - bbox[0]
299
+ text_height = bbox[3] - bbox[1]
300
+
301
+ if position == "center":
302
+ pos = ((self.WIDTH - text_width) // 2, (self.HEIGHT - text_height) // 2)
303
+ elif position == "top":
304
+ pos = ((self.WIDTH - text_width) // 2, 100)
305
+ elif position == "bottom":
306
+ pos = ((self.WIDTH - text_width) // 2, self.HEIGHT - text_height - 100)
307
+ else:
308
+ pos = ((self.WIDTH - text_width) // 2, (self.HEIGHT - text_height) // 2)
309
+
310
+ # Draw text with outline
311
+ draw.text(pos, text, fill=self.FIGURE_COLOR, font=font,
312
+ stroke_width=3, stroke_fill=(200, 200, 200))
313
+
314
+ return img
315
+
316
+ def create_scene(
317
+ self,
318
+ pose: str = "standing",
319
+ props: List[str] = None,
320
+ text: str = None,
321
+ text_position: str = "bottom"
322
+ ) -> Image.Image:
323
+ """Create a single scene with stick figure"""
324
+ # Create white background
325
+ img = Image.new('RGB', (self.WIDTH, self.HEIGHT), self.BG_COLOR)
326
+ draw = ImageDraw.Draw(img)
327
+
328
+ # Draw figure in center
329
+ self.draw_stick_figure(draw, self.WIDTH // 2, self.HEIGHT // 2 + 100,
330
+ pose=pose, scale=1.5, props=props)
331
+
332
+ # Add text if provided
333
+ if text:
334
+ self.add_text_overlay(img, text, position=text_position)
335
+
336
+ return img
337
+
338
+ def generate_motivation_frames(
339
+ self,
340
+ scenes: List[Dict],
341
+ output_dir: str = "temp"
342
+ ) -> List[str]:
343
+ """
344
+ Generate frames for motivation video.
345
+
346
+ Args:
347
+ scenes: List of scene dicts with pose, props, text, duration
348
+ output_dir: Directory to save frames
349
+
350
+ Returns:
351
+ List of frame file paths
352
+ """
353
+ os.makedirs(output_dir, exist_ok=True)
354
+ frame_paths = []
355
+ frame_num = 0
356
+
357
+ for scene in scenes:
358
+ pose = scene.get("pose", "standing")
359
+ props = scene.get("props", [])
360
+ text = scene.get("text", None)
361
+ duration = scene.get("duration", 1.0) # seconds
362
+
363
+ # Create scene image
364
+ img = self.create_scene(pose=pose, props=props, text=text)
365
+
366
+ # Generate frames for duration (30 fps)
367
+ num_frames = int(duration * 30)
368
+ for _ in range(num_frames):
369
+ frame_path = os.path.join(output_dir, f"frame_{frame_num:04d}.png")
370
+ img.save(frame_path)
371
+ frame_paths.append(frame_path)
372
+ frame_num += 1
373
+
374
+ logger.info(f"Generated {len(frame_paths)} frames for motivation video")
375
+ return frame_paths
modules/art_reels/services/video_composer.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Video Composer - Combines frames into final video
3
+ Uses MoviePy for video rendering
4
+ """
5
+ import logging
6
+ import os
7
+ import uuid
8
+ from typing import List, Optional
9
+ from moviepy.editor import ImageSequenceClip, AudioFileClip, CompositeAudioClip
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class VideoComposer:
15
+ """
16
+ Combines image frames into final video.
17
+
18
+ Features:
19
+ - Frame sequence to video
20
+ - Audio overlay
21
+ - Background music
22
+ """
23
+
24
+ # Video settings
25
+ FPS = 30
26
+
27
+ def __init__(self, output_dir: str = "videos"):
28
+ self.output_dir = output_dir
29
+ os.makedirs(output_dir, exist_ok=True)
30
+
31
+ def compose_video(
32
+ self,
33
+ frame_paths: List[str],
34
+ audio_path: Optional[str] = None,
35
+ music_path: Optional[str] = None,
36
+ output_name: Optional[str] = None,
37
+ fps: int = None
38
+ ) -> str:
39
+ """
40
+ Compose video from frames.
41
+
42
+ Args:
43
+ frame_paths: List of frame image paths
44
+ audio_path: Optional voice audio path
45
+ music_path: Optional background music path
46
+ output_name: Custom output filename
47
+ fps: Frames per second
48
+
49
+ Returns:
50
+ Path to output video
51
+ """
52
+ fps = fps or self.FPS
53
+ output_name = output_name or f"art_video_{uuid.uuid4().hex[:8]}.mp4"
54
+ output_path = os.path.join(self.output_dir, output_name)
55
+
56
+ try:
57
+ logger.info(f"Composing video from {len(frame_paths)} frames")
58
+
59
+ # Create video clip from frames
60
+ clip = ImageSequenceClip(frame_paths, fps=fps)
61
+
62
+ # Add audio if provided
63
+ audio_clips = []
64
+
65
+ if audio_path and os.path.exists(audio_path):
66
+ voice_audio = AudioFileClip(audio_path)
67
+ audio_clips.append(voice_audio)
68
+
69
+ if music_path and os.path.exists(music_path):
70
+ music_audio = AudioFileClip(music_path)
71
+ # Loop music if needed
72
+ if music_audio.duration < clip.duration:
73
+ music_audio = music_audio.loop(duration=clip.duration)
74
+ else:
75
+ music_audio = music_audio.subclip(0, clip.duration)
76
+ # Lower volume for background
77
+ music_audio = music_audio.volumex(0.3)
78
+ audio_clips.append(music_audio)
79
+
80
+ if audio_clips:
81
+ if len(audio_clips) > 1:
82
+ final_audio = CompositeAudioClip(audio_clips)
83
+ else:
84
+ final_audio = audio_clips[0]
85
+ clip = clip.set_audio(final_audio)
86
+
87
+ # Write video
88
+ clip.write_videofile(
89
+ output_path,
90
+ codec='libx264',
91
+ audio_codec='aac',
92
+ fps=fps,
93
+ preset='medium',
94
+ threads=4
95
+ )
96
+
97
+ # Cleanup
98
+ clip.close()
99
+ for ac in audio_clips:
100
+ ac.close()
101
+
102
+ logger.info(f"Video saved to: {output_path}")
103
+ return output_path
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error composing video: {e}")
107
+ raise
108
+
109
+ def cleanup_frames(self, frame_paths: List[str]):
110
+ """Delete temporary frame files"""
111
+ for path in frame_paths:
112
+ try:
113
+ if os.path.exists(path):
114
+ os.remove(path)
115
+ except Exception as e:
116
+ logger.warning(f"Failed to delete frame {path}: {e}")
static/index.html CHANGED
@@ -276,6 +276,9 @@
276
  <button class="tab-btn" data-tab="trends">
277
  📊 Trends
278
  </button>
 
 
 
279
  </div>
280
 
281
  <!-- Story Reels Tab -->
@@ -569,6 +572,91 @@
569
  </div>
570
  </div>
571
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  <script>
573
  // Tab switching
574
  document.querySelectorAll('.tab-btn').forEach(btn => {
@@ -882,6 +970,132 @@
882
  }
883
  });
884
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
885
  // ==========================================
886
  // GEMINI CHATBOT TEST
887
  // ==========================================
 
276
  <button class="tab-btn" data-tab="trends">
277
  📊 Trends
278
  </button>
279
+ <button class="tab-btn" data-tab="art">
280
+ 🎨 Art Reels
281
+ </button>
282
  </div>
283
 
284
  <!-- Story Reels Tab -->
 
572
  </div>
573
  </div>
574
 
575
+ <!-- Art Reels Tab -->
576
+ <div id="art-tab" class="tab-content">
577
+ <div class="card">
578
+ <h2>🎨 Art Reels Generator</h2>
579
+ <p style="color: var(--text-secondary); margin-bottom: 1.5rem;">
580
+ Create animated art videos using pure Python - No AI API needed!
581
+ </p>
582
+
583
+ <!-- Minecraft Block Art -->
584
+ <div style="margin-bottom: 2rem; padding: 1.5rem; background: var(--bg-secondary); border-radius: 12px;">
585
+ <h3 style="margin-bottom: 1rem;">🏠 Minecraft Block Art</h3>
586
+ <p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
587
+ Isometric block-by-block building animation
588
+ </p>
589
+ <form id="minecraftForm">
590
+ <div class="form-group">
591
+ <label>Description</label>
592
+ <input type="text" id="mcDescription" value="wooden survival house"
593
+ placeholder="e.g., wooden survival house">
594
+ </div>
595
+ <div class="form-row">
596
+ <div class="form-group">
597
+ <label>Speed</label>
598
+ <select id="mcSpeed">
599
+ <option value="0.5">Slow</option>
600
+ <option value="1.0" selected>Normal</option>
601
+ <option value="2.0">Fast</option>
602
+ </select>
603
+ </div>
604
+ </div>
605
+ <button type="submit" class="submit-btn">🏠 Generate Minecraft Video</button>
606
+ </form>
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>
639
+
640
+ <!-- Stick Figure Motivation -->
641
+ <div style="padding: 1.5rem; background: var(--bg-secondary); border-radius: 12px;">
642
+ <h3 style="margin-bottom: 1rem;">🎭 Stick Figure Motivation</h3>
643
+ <p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
644
+ White background + black stick figures + voice sync
645
+ </p>
646
+ <form id="stickForm">
647
+ <div class="form-group">
648
+ <label>Script (Keywords: রাজা, বড়লোক, ঘুম, দৌড়, etc.)</label>
649
+ <textarea id="stickScript" rows="4"
650
+ placeholder="একজন বড়লোক মানুষ সকালে ঘুম থেকে উঠল। সে মনে মনে ভাবল আজকে কি করব। Get ready for success!"></textarea>
651
+ </div>
652
+ <button type="submit" class="submit-btn">🎭 Generate Stick Figure Video</button>
653
+ </form>
654
+ <div id="stickStatus" class="status hidden"></div>
655
+ </div>
656
+
657
+ </div>
658
+ </div>
659
+
660
  <script>
661
  // Tab switching
662
  document.querySelectorAll('.tab-btn').forEach(btn => {
 
970
  }
971
  });
972
 
973
+ // ==========================================
974
+ // ART REELS MODULE
975
+ // ==========================================
976
+
977
+ // Helper function for polling art job status
978
+ async function pollArtStatus(jobId, statusDiv, type) {
979
+ const checkStatus = async () => {
980
+ try {
981
+ const res = await fetch(`/api/art/${jobId}/status`);
982
+ const status = await res.json();
983
+
984
+ if (status.status === 'ready') {
985
+ statusDiv.className = 'status success';
986
+ statusDiv.innerHTML = `✅ Video Ready! <a href="${status.video_url}" target="_blank" style="color: var(--accent);">Download Video</a>`;
987
+ return;
988
+ } else if (status.status === 'failed') {
989
+ statusDiv.className = 'status error';
990
+ statusDiv.innerHTML = `❌ Failed: ${status.error}`;
991
+ return;
992
+ } else {
993
+ statusDiv.innerHTML = `⏳ ${status.status}... ${status.progress}%`;
994
+ setTimeout(checkStatus, 2000);
995
+ }
996
+ } catch (err) {
997
+ statusDiv.className = 'status error';
998
+ statusDiv.innerHTML = `❌ Error: ${err.message}`;
999
+ }
1000
+ };
1001
+ checkStatus();
1002
+ }
1003
+
1004
+ // Minecraft Form
1005
+ document.getElementById('minecraftForm').addEventListener('submit', async (e) => {
1006
+ e.preventDefault();
1007
+ const status = document.getElementById('minecraftStatus');
1008
+ status.className = 'status';
1009
+ status.classList.remove('hidden');
1010
+ status.innerHTML = '⏳ Starting Minecraft video generation...';
1011
+
1012
+ const data = {
1013
+ description: document.getElementById('mcDescription').value,
1014
+ blocks: 50,
1015
+ speed: parseFloat(document.getElementById('mcSpeed').value)
1016
+ };
1017
+
1018
+ try {
1019
+ const res = await fetch('/api/art/minecraft', {
1020
+ method: 'POST',
1021
+ headers: { 'Content-Type': 'application/json' },
1022
+ body: JSON.stringify(data)
1023
+ });
1024
+ const result = await res.json();
1025
+
1026
+ if (result.job_id) {
1027
+ status.innerHTML = `⏳ Job started: ${result.job_id}`;
1028
+ pollArtStatus(result.job_id, status, 'minecraft');
1029
+ }
1030
+ } catch (err) {
1031
+ status.className = 'status error';
1032
+ status.innerHTML = '❌ Error: ' + err.message;
1033
+ }
1034
+ });
1035
+
1036
+ // Drawing Form
1037
+ document.getElementById('drawingForm').addEventListener('submit', async (e) => {
1038
+ e.preventDefault();
1039
+ const status = document.getElementById('drawingStatus');
1040
+ status.className = 'status';
1041
+ status.classList.remove('hidden');
1042
+ status.innerHTML = '⏳ Starting drawing animation...';
1043
+
1044
+ const data = {
1045
+ subject: document.getElementById('drawSubject').value,
1046
+ style: document.getElementById('drawStyle').value,
1047
+ colors: true
1048
+ };
1049
+
1050
+ try {
1051
+ const res = await fetch('/api/art/drawing', {
1052
+ method: 'POST',
1053
+ headers: { 'Content-Type': 'application/json' },
1054
+ body: JSON.stringify(data)
1055
+ });
1056
+ const result = await res.json();
1057
+
1058
+ if (result.job_id) {
1059
+ status.innerHTML = `⏳ Job started: ${result.job_id}`;
1060
+ pollArtStatus(result.job_id, status, 'drawing');
1061
+ }
1062
+ } catch (err) {
1063
+ status.className = 'status error';
1064
+ status.innerHTML = '❌ Error: ' + err.message;
1065
+ }
1066
+ });
1067
+
1068
+ // Stick Figure Form
1069
+ document.getElementById('stickForm').addEventListener('submit', async (e) => {
1070
+ e.preventDefault();
1071
+ const status = document.getElementById('stickStatus');
1072
+ status.className = 'status';
1073
+ status.classList.remove('hidden');
1074
+ status.innerHTML = '⏳ Starting stick figure video...';
1075
+
1076
+ const data = {
1077
+ script: document.getElementById('stickScript').value,
1078
+ voice: 'af_heart'
1079
+ };
1080
+
1081
+ try {
1082
+ const res = await fetch('/api/art/stick-figure', {
1083
+ method: 'POST',
1084
+ headers: { 'Content-Type': 'application/json' },
1085
+ body: JSON.stringify(data)
1086
+ });
1087
+ const result = await res.json();
1088
+
1089
+ if (result.job_id) {
1090
+ status.innerHTML = `⏳ Job started: ${result.job_id}`;
1091
+ pollArtStatus(result.job_id, status, 'stick');
1092
+ }
1093
+ } catch (err) {
1094
+ status.className = 'status error';
1095
+ status.innerHTML = '❌ Error: ' + err.message;
1096
+ }
1097
+ });
1098
+
1099
  // ==========================================
1100
  // GEMINI CHATBOT TEST
1101
  // ==========================================