Stanley03 commited on
Commit
dcdabb2
·
verified ·
1 Parent(s): ff5081e

Create video_generation.py

Browse files
Files changed (1) hide show
  1. video_generation.py +632 -0
video_generation.py ADDED
@@ -0,0 +1,632 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ import time
5
+ import requests
6
+ import json
7
+ import logging
8
+ from PIL import Image
9
+ import numpy as np
10
+ from typing import List, Optional, Dict, Any
11
+ import cv2
12
+ import tempfile
13
+ import random
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class FreeVideoGenerator:
18
+ """
19
+ Free video generation using open-source models on Hugging Face
20
+ """
21
+
22
+ def __init__(self, hf_token: Optional[str] = None):
23
+ self.hf_token = hf_token or os.getenv('HF_TOKEN', '')
24
+ self.base_url = "https://api-inference.huggingface.co/models"
25
+
26
+ # Available free models for different tasks
27
+ self.models = {
28
+ # Text-to-Video models (FREE)
29
+ "text_to_video": {
30
+ "zeroscope_v2": "cerspense/zeroscope_v2_576w",
31
+ "modelscope": "damo-vilab/text-to-video-ms-1.7b",
32
+ "stable_video": "stabilityai/stable-video-diffusion-img2vid-xt",
33
+ "video_crafter": "VideoCrafter/VideoCrafter2",
34
+ "animatediff": "guoyww/animatediff"
35
+ },
36
+
37
+ # Image-to-Video models (FREE)
38
+ "image_to_video": {
39
+ "stable_video": "stabilityai/stable-video-diffusion-img2vid-xt",
40
+ "img2vid_xt": "stabilityai/stable-video-diffusion-img2vid-xt-1-1",
41
+ "zeroscope_img2vid": "cerspense/zeroscope_v2_XL"
42
+ },
43
+
44
+ # Animation models (FREE)
45
+ "animation": {
46
+ "animate_diff": "guoyww/animatediff",
47
+ "magic_animate": "zcxu-eric/MagicAnimate",
48
+ "text2video_zero": "PAIR/Text2Video-Zero"
49
+ }
50
+ }
51
+
52
+ # Free API endpoints that work without token
53
+ self.free_endpoints = {
54
+ "text_to_video": "https://api-inference.huggingface.co/models/cerspense/zeroscope_v2_576w",
55
+ "image_to_video": "https://api-inference.huggingface.co/models/stabilityai/stable-video-diffusion-img2vid-xt",
56
+ "animation": "https://api-inference.huggingface.co/models/PAIR/Text2Video-Zero"
57
+ }
58
+
59
+ # Performance settings
60
+ self.timeout = 120 # Longer timeout for videos
61
+ self.max_retries = 3
62
+ self.wait_between_retries = [10, 20, 30] # Progressive waiting
63
+
64
+ # Video settings
65
+ self.default_fps = 8
66
+ self.default_frames = 24
67
+ self.default_width = 576
68
+ self.default_height = 320
69
+
70
+ # Cache for generated videos
71
+ self.video_cache = {}
72
+ self.cache_size = 50
73
+
74
+ def detect_video_request(self, text: str) -> bool:
75
+ """Detect if user wants to generate a video"""
76
+ video_triggers = [
77
+ 'generate video', 'create video', 'make a video', 'video of',
78
+ 'animate', 'animation', 'moving picture', 'motion picture',
79
+ 'video generation', 'create animation', 'make animation',
80
+ 'video clip', 'short video', 'motion graphics', 'cinematic',
81
+ 'film', 'movie', 'moving image', 'dynamic image', 'animated video'
82
+ ]
83
+ text_lower = text.lower()
84
+ return any(trigger in text_lower for trigger in video_triggers)
85
+
86
+ def extract_video_prompt(self, text: str) -> str:
87
+ """Extract video description from user message"""
88
+ prompt = text.lower()
89
+
90
+ # Remove common video request phrases
91
+ remove_phrases = [
92
+ 'generate video of', 'create video of', 'make a video of',
93
+ 'create animation of', 'make animation of', 'animate',
94
+ 'generate animation of', 'video of', 'animation of',
95
+ 'make a film about', 'create a film about', 'produce video of',
96
+ 'can you make a video', 'i want a video', 'show me a video',
97
+ 'video showing', 'animate this', 'create moving image of'
98
+ ]
99
+
100
+ for phrase in remove_phrases:
101
+ prompt = prompt.replace(phrase, '')
102
+
103
+ # Remove question words
104
+ question_words = ['how to', 'what is', 'can you', 'could you', 'would you']
105
+ for word in question_words:
106
+ if prompt.startswith(word):
107
+ prompt = prompt[len(word):].strip()
108
+
109
+ return prompt.strip().capitalize()
110
+
111
+ def enhance_prompt_with_context(self, prompt: str, context_type: str = "general") -> str:
112
+ """Enhance video prompts with cinematic and cultural context"""
113
+
114
+ # Basic cinematic enhancements
115
+ cinematic_enhancements = [
116
+ "cinematic, 8k, ultra detailed, high quality, masterpiece",
117
+ "epic, dramatic lighting, film grain, cinematic shot, professional",
118
+ "beautiful, stunning, visually striking, vivid colors, trending",
119
+ "high resolution, detailed, sharp focus, studio quality, professional",
120
+ "film still, movie scene, cinematic photography, 35mm film"
121
+ ]
122
+
123
+ # Cultural/Kiswahili enhancements
124
+ cultural_enhancements = {
125
+ "safari": "African safari, wildlife documentary style, national geographic, savanna",
126
+ "cultural": "traditional African culture, vibrant colors, community celebration, authentic",
127
+ "coastal": "Swahili coast, Indian Ocean, dhows sailing, traditional architecture, beach",
128
+ "urban": "modern African city, bustling streets, contemporary life, urban landscape",
129
+ "historical": "historical Africa, ancient kingdoms, traditional ceremonies, heritage",
130
+ "wildlife": "African wildlife, natural habitat, animal behavior, nature documentary",
131
+ "village": "traditional African village, community life, rural setting, authentic"
132
+ }
133
+
134
+ # Motion and animation enhancements
135
+ motion_enhancements = [
136
+ "smooth motion, fluid animation, dynamic movement, cinematic motion",
137
+ "slow motion, dramatic pacing, epic timing, filmic movement",
138
+ "fast paced, energetic movement, dynamic action, lively animation"
139
+ ]
140
+
141
+ enhanced_prompt = prompt
142
+
143
+ # Add cinematic quality
144
+ enhanced_prompt += f", {random.choice(cinematic_enhancements)}"
145
+
146
+ # Add motion enhancement
147
+ enhanced_prompt += f", {random.choice(motion_enhancements)}"
148
+
149
+ # Add context-specific enhancements
150
+ context_keywords = {
151
+ "safari": ["safari", "wildlife", "animal", "lion", "elephant", "giraffe"],
152
+ "cultural": ["culture", "traditional", "dance", "ceremony", "ritual"],
153
+ "coastal": ["coast", "beach", "ocean", "sea", "dhow", "swahili"],
154
+ "urban": ["city", "urban", "street", "building", "modern", "skyline"],
155
+ "historical": ["history", "ancient", "kingdom", "heritage", "traditional"],
156
+ "wildlife": ["animal", "bird", "nature", "wild", "savanna", "forest"],
157
+ "village": ["village", "rural", "community", "hut", "traditional"]
158
+ }
159
+
160
+ prompt_lower = enhanced_prompt.lower()
161
+ for theme, keywords in context_keywords.items():
162
+ if any(keyword in prompt_lower for keyword in keywords):
163
+ enhanced_prompt += f", {cultural_enhancements.get(theme, '')}"
164
+ break
165
+
166
+ # Add technical specifications for better results
167
+ technical_specs = [
168
+ f"{self.default_width}x{self.default_height} resolution",
169
+ f"{self.default_fps} fps",
170
+ "high bitrate",
171
+ "stable diffusion",
172
+ "consistent quality"
173
+ ]
174
+
175
+ enhanced_prompt += f", {', '.join(random.sample(technical_specs, 2))}"
176
+
177
+ return enhanced_prompt
178
+
179
+ def get_cached_video(self, prompt: str) -> Optional[str]:
180
+ """Get cached video if available"""
181
+ cache_key = prompt.lower().strip()[:100]
182
+ return self.video_cache.get(cache_key)
183
+
184
+ def cache_video(self, prompt: str, video_data: str):
185
+ """Cache generated video"""
186
+ cache_key = prompt.lower().strip()[:100]
187
+
188
+ # Limit cache size
189
+ if len(self.video_cache) >= self.cache_size:
190
+ # Remove oldest entry
191
+ self.video_cache.pop(next(iter(self.video_cache)))
192
+
193
+ self.video_cache[cache_key] = video_data
194
+
195
+ def generate_text_to_video(self, prompt: str, model: str = "zeroscope_v2") -> Optional[str]:
196
+ """
197
+ Generate video from text prompt using free models
198
+
199
+ Args:
200
+ prompt: Text description of the video
201
+ model: Model to use ('zeroscope_v2', 'modelscope', etc.)
202
+
203
+ Returns:
204
+ Base64 encoded video or None
205
+ """
206
+
207
+ # Check cache first
208
+ cached_video = self.get_cached_video(prompt)
209
+ if cached_video:
210
+ logger.info("🎬 Using cached video")
211
+ return cached_video
212
+
213
+ model_id = self.models["text_to_video"].get(model, "cerspense/zeroscope_v2_576w")
214
+ api_url = f"{self.base_url}/{model_id}"
215
+
216
+ headers = {}
217
+ if self.hf_token:
218
+ headers["Authorization"] = f"Bearer {self.hf_token}"
219
+
220
+ # Optimized parameters for faster generation
221
+ payload = {
222
+ "inputs": prompt,
223
+ "parameters": {
224
+ "num_frames": self.default_frames,
225
+ "num_inference_steps": 25, # Reduced for speed
226
+ "guidance_scale": 7.5,
227
+ "fps": self.default_fps,
228
+ "height": self.default_height,
229
+ "width": self.default_width,
230
+ "negative_prompt": "blurry, low quality, distorted, bad anatomy, watermark, text"
231
+ }
232
+ }
233
+
234
+ for attempt in range(self.max_retries):
235
+ try:
236
+ logger.info(f"🎬 Generating video (attempt {attempt + 1}): {prompt[:50]}...")
237
+
238
+ response = requests.post(
239
+ api_url,
240
+ headers=headers,
241
+ json=payload,
242
+ timeout=self.timeout
243
+ )
244
+
245
+ if response.status_code == 200:
246
+ # Convert to base64
247
+ video_bytes = response.content
248
+ video_b64 = base64.b64encode(video_bytes).decode('utf-8')
249
+
250
+ # Determine format
251
+ content_type = response.headers.get('content-type', 'video/mp4')
252
+ if 'webm' in content_type:
253
+ format_str = "webm"
254
+ else:
255
+ format_str = "mp4"
256
+
257
+ video_data = f"data:video/{format_str};base64,{video_b64}"
258
+
259
+ # Cache the result
260
+ self.cache_video(prompt, video_data)
261
+
262
+ return video_data
263
+
264
+ elif response.status_code == 503:
265
+ # Model is loading
266
+ wait_time = self.wait_between_retries[min(attempt, len(self.wait_between_retries)-1)]
267
+ logger.info(f"⏳ Video model loading, waiting {wait_time}s...")
268
+ time.sleep(wait_time)
269
+ continue
270
+
271
+ else:
272
+ logger.error(f"Video API error {response.status_code}: {response.text[:200]}")
273
+
274
+ except requests.exceptions.Timeout:
275
+ logger.warning(f"⏰ Video generation timeout, attempt {attempt + 1}")
276
+ time.sleep(self.wait_between_retries[min(attempt, len(self.wait_between_retries)-1)])
277
+ continue
278
+ except Exception as e:
279
+ logger.error(f"Video generation error: {e}")
280
+ if attempt < self.max_retries - 1:
281
+ time.sleep(self.wait_between_retries[min(attempt, len(self.wait_between_retries)-1)])
282
+ continue
283
+ break
284
+
285
+ # Fallback to simpler animation if video generation fails
286
+ logger.info("🔄 Falling back to text animation")
287
+ return self.generate_animation_from_text(prompt)
288
+
289
+ def generate_image_to_video(self, image_data: str, prompt: str = "") -> Optional[str]:
290
+ """
291
+ Generate video from an image using free models
292
+
293
+ Args:
294
+ image_data: Base64 encoded image or image URL
295
+ prompt: Optional text prompt for guidance
296
+
297
+ Returns:
298
+ Base64 encoded video or None
299
+ """
300
+ try:
301
+ # Prepare image
302
+ if image_data.startswith('data:image'):
303
+ # Extract base64 from data URL
304
+ image_b64 = image_data.split(',')[1]
305
+ image_bytes = base64.b64decode(image_b64)
306
+ image = Image.open(io.BytesIO(image_bytes))
307
+ else:
308
+ # Assume it's a file path or URL
309
+ if image_data.startswith('http'):
310
+ response = requests.get(image_data, timeout=30)
311
+ image = Image.open(io.BytesIO(response.content))
312
+ else:
313
+ image = Image.open(image_data)
314
+
315
+ # Resize image for faster processing
316
+ image = image.resize((self.default_width, self.default_height), Image.Resampling.LANCZOS)
317
+
318
+ # Convert to bytes
319
+ img_byte_arr = io.BytesIO()
320
+ image.save(img_byte_arr, format='PNG')
321
+ img_byte_arr = img_byte_arr.getvalue()
322
+
323
+ # Use free model (Stable Video Diffusion)
324
+ model_id = "stabilityai/stable-video-diffusion-img2vid-xt"
325
+ api_url = f"{self.base_url}/{model_id}"
326
+
327
+ headers = {
328
+ "Authorization": f"Bearer {self.hf_token}" if self.hf_token else ""
329
+ }
330
+
331
+ # If prompt is provided, use it as guidance
332
+ params = {}
333
+ if prompt:
334
+ params = {
335
+ "parameters": {
336
+ "motion_bucket_id": 127,
337
+ "noise_aug_strength": 0.02
338
+ }
339
+ }
340
+
341
+ response = requests.post(
342
+ api_url,
343
+ headers=headers,
344
+ data=img_byte_arr,
345
+ json=params if params else None,
346
+ timeout=150 # Longer timeout for image-to-video
347
+ )
348
+
349
+ if response.status_code == 200:
350
+ video_b64 = base64.b64encode(response.content).decode('utf-8')
351
+ return f"data:video/mp4;base64,{video_b64}"
352
+ else:
353
+ logger.error(f"Image-to-video API error: {response.status_code}")
354
+ return None
355
+
356
+ except Exception as e:
357
+ logger.error(f"Image to video error: {e}")
358
+ return None
359
+
360
+ def create_slideshow_video(self, images: List[str], duration_per_image: float = 2.0) -> Optional[str]:
361
+ """
362
+ Create a simple slideshow video from multiple images
363
+
364
+ Args:
365
+ images: List of base64 encoded images
366
+ duration_per_image: Duration for each image in seconds
367
+
368
+ Returns:
369
+ Base64 encoded video
370
+ """
371
+ try:
372
+ # Create temporary directory
373
+ with tempfile.TemporaryDirectory() as tmpdir:
374
+ image_paths = []
375
+
376
+ # Save all images
377
+ for i, img_data in enumerate(images):
378
+ if img_data.startswith('data:image'):
379
+ img_b64 = img_data.split(',')[1]
380
+ img_bytes = base64.b64decode(img_b64)
381
+ else:
382
+ img_bytes = base64.b64decode(img_data)
383
+
384
+ img_path = os.path.join(tmpdir, f'frame_{i:03d}.png')
385
+ with open(img_path, 'wb') as f:
386
+ f.write(img_bytes)
387
+ image_paths.append(img_path)
388
+
389
+ # Read first image to get dimensions
390
+ first_img = cv2.imread(image_paths[0])
391
+ if first_img is None:
392
+ logger.error("Failed to read first image")
393
+ return None
394
+
395
+ height, width = first_img.shape[:2]
396
+
397
+ # Create video writer
398
+ fps = 10
399
+ output_path = os.path.join(tmpdir, 'output.mp4')
400
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
401
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
402
+
403
+ # Write frames with smooth transitions
404
+ frames_per_image = int(fps * duration_per_image)
405
+ transition_frames = int(fps * 0.5) # Half second transition
406
+
407
+ for i in range(len(image_paths)):
408
+ current_img = cv2.imread(image_paths[i])
409
+ if current_img is None:
410
+ continue
411
+
412
+ # Resize to match dimensions
413
+ current_img = cv2.resize(current_img, (width, height))
414
+
415
+ # Write main frames
416
+ main_frames = frames_per_image - transition_frames
417
+ for _ in range(main_frames):
418
+ out.write(current_img)
419
+
420
+ # Add transition to next image if exists
421
+ if i < len(image_paths) - 1:
422
+ next_img = cv2.imread(image_paths[i + 1])
423
+ if next_img is not None:
424
+ next_img = cv2.resize(next_img, (width, height))
425
+
426
+ # Create crossfade transition
427
+ for t in range(transition_frames):
428
+ alpha = t / transition_frames
429
+ beta = 1.0 - alpha
430
+ blended = cv2.addWeighted(current_img, beta, next_img, alpha, 0)
431
+ out.write(blended)
432
+
433
+ out.release()
434
+
435
+ # Read and encode video
436
+ with open(output_path, 'rb') as f:
437
+ video_bytes = f.read()
438
+
439
+ video_b64 = base64.b64encode(video_bytes).decode('utf-8')
440
+ return f"data:video/mp4;base64,{video_b64}"
441
+
442
+ except Exception as e:
443
+ logger.error(f"Slideshow error: {e}")
444
+ return None
445
+
446
+ def generate_animation_from_text(self, text: str) -> Optional[str]:
447
+ """
448
+ Create simple text animation
449
+
450
+ Args:
451
+ text: Text to animate
452
+
453
+ Returns:
454
+ Base64 encoded video
455
+ """
456
+ try:
457
+ # Create temporary directory
458
+ with tempfile.TemporaryDirectory() as tmpdir:
459
+ # Create frames with text
460
+ fps = 10
461
+ duration = 4 # seconds
462
+ total_frames = fps * duration
463
+ height, width = self.default_height, self.default_width
464
+
465
+ output_path = os.path.join(tmpdir, 'animation.mp4')
466
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
467
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
468
+
469
+ # Create gradient background colors
470
+ colors = [
471
+ (41, 128, 185), # Blue
472
+ (39, 174, 96), # Green
473
+ (142, 68, 173), # Purple
474
+ (230, 126, 34), # Orange
475
+ (231, 76, 60) # Red
476
+ ]
477
+
478
+ for frame_num in range(total_frames):
479
+ # Create gradient background
480
+ frame = np.zeros((height, width, 3), dtype=np.uint8)
481
+
482
+ # Select color based on frame
483
+ color_idx = (frame_num // (total_frames // len(colors))) % len(colors)
484
+ bg_color = colors[color_idx]
485
+
486
+ # Apply gradient
487
+ for i in range(height):
488
+ # Gradient from top to bottom
489
+ factor = i / height
490
+ r = int(bg_color[2] * (1 - factor) + 10 * factor)
491
+ g = int(bg_color[1] * (1 - factor) + 10 * factor)
492
+ b = int(bg_color[0] * (1 - factor) + 10 * factor)
493
+
494
+ frame[i, :, 0] = b # OpenCV uses BGR
495
+ frame[i, :, 1] = g
496
+ frame[i, :, 2] = r
497
+
498
+ # Add text with animation
499
+ font = cv2.FONT_HERSHEY_SIMPLEX
500
+
501
+ # Calculate text position (center)
502
+ text_lines = text.split(' ')
503
+ y_start = height // 2 - (len(text_lines) * 40) // 2
504
+
505
+ for i, line in enumerate(text_lines):
506
+ # Calculate font size with pulse effect
507
+ pulse = 0.7 + 0.3 * np.sin(2 * np.pi * (frame_num / fps) + i * 0.5)
508
+ font_scale = 1.2 * pulse
509
+ thickness = int(2 * pulse)
510
+
511
+ # Calculate text size and position
512
+ text_size = cv2.getTextSize(line, font, font_scale, thickness)[0]
513
+ text_x = (width - text_size[0]) // 2
514
+ text_y = y_start + i * 40
515
+
516
+ # Add text shadow
517
+ shadow_color = (0, 0, 0)
518
+ cv2.putText(frame, line, (text_x + 2, text_y + 2), font,
519
+ font_scale, shadow_color, thickness + 1)
520
+
521
+ # Add main text
522
+ text_color = (255, 255, 255) # White
523
+ cv2.putText(frame, line, (text_x, text_y), font,
524
+ font_scale, text_color, thickness)
525
+
526
+ # Add decorative elements
527
+ if frame_num % 10 < 5:
528
+ # Add twinkling stars
529
+ for _ in range(3):
530
+ star_x = random.randint(0, width)
531
+ star_y = random.randint(0, height)
532
+ cv2.circle(frame, (star_x, star_y), 2, (255, 255, 255), -1)
533
+
534
+ out.write(frame)
535
+
536
+ out.release()
537
+
538
+ # Read and encode video
539
+ with open(output_path, 'rb') as f:
540
+ video_bytes = f.read()
541
+
542
+ video_b64 = base64.b64encode(video_bytes).decode('utf-8')
543
+ return f"data:video/mp4;base64,{video_b64}"
544
+
545
+ except Exception as e:
546
+ logger.error(f"Text animation error: {e}")
547
+ return None
548
+
549
+ def create_cultural_video(self, theme: str, style: str = "animated") -> Optional[str]:
550
+ """
551
+ Create videos with Kiswahili cultural themes
552
+
553
+ Args:
554
+ theme: Cultural theme (safari, ceremony, dance, etc.)
555
+ style: Animation style
556
+
557
+ Returns:
558
+ Base64 encoded video
559
+ """
560
+ # Cultural themes and prompts
561
+ cultural_themes = {
562
+ "safari": "African safari sunset with elephants and giraffes walking, majestic savanna landscape",
563
+ "dance": "Traditional Maasai warriors dancing, vibrant colors, cultural celebration, energetic movement",
564
+ "market": "Busy African market scene, vibrant colors, people trading goods, lively atmosphere",
565
+ "coastal": "Swahili coast with traditional dhows sailing, Indian Ocean waves, beach scenery",
566
+ "wildlife": "African wildlife documentary style, lions hunting on savanna, dramatic nature scene",
567
+ "village": "Traditional African village life, community activities, sunset over huts",
568
+ "ceremony": "African wedding ceremony, traditional attire, dancing, celebration, cultural rituals",
569
+ "sunset": "African sunset over savanna, acacia trees silhouette, warm colors, peaceful scene",
570
+ "city": "Modern African city at night, Nairobi skyline, lights, urban life, contemporary"
571
+ }
572
+
573
+ # Get prompt for theme
574
+ base_prompt = cultural_themes.get(theme, f"African {theme}, cultural, vibrant, dynamic")
575
+
576
+ # Add style-specific enhancements
577
+ style_enhancements = {
578
+ "animated": "animated, cartoon style, smooth motion, vibrant colors, lively",
579
+ "realistic": "realistic, documentary style, cinematic, natural lighting, photorealistic",
580
+ "painting": "painting style, brush strokes, artistic, masterpiece, textured",
581
+ "watercolor": "watercolor painting, soft edges, dreamy, artistic, blended colors",
582
+ "cinematic": "cinematic, film grain, dramatic lighting, movie scene, professional"
583
+ }
584
+
585
+ style_enhancement = style_enhancements.get(style, "animated, vibrant, smooth motion")
586
+
587
+ full_prompt = f"{base_prompt}, {style_enhancement}, {self.default_width}x{self.default_height}, {self.default_fps} fps"
588
+
589
+ return self.generate_text_to_video(full_prompt)
590
+
591
+ def get_video_info(self) -> Dict[str, Any]:
592
+ """Get information about available video generation options"""
593
+ return {
594
+ "available_models": {
595
+ "text_to_video": list(self.models["text_to_video"].keys()),
596
+ "image_to_video": list(self.models["image_to_video"].keys()),
597
+ "animation": list(self.models["animation"].keys())
598
+ },
599
+ "free_models": ["zeroscope_v2", "stable_video", "text2video_zero"],
600
+ "max_duration": "4 seconds",
601
+ "max_frames": self.default_frames,
602
+ "resolution": f"{self.default_width}x{self.default_height}",
603
+ "fps": self.default_fps,
604
+ "formats": ["MP4", "WebM"],
605
+ "features": [
606
+ "Text-to-Video",
607
+ "Image-to-Video",
608
+ "Slideshow Creation",
609
+ "Text Animation",
610
+ "Cultural Themes",
611
+ "Crossfade Transitions",
612
+ "Animated Text Effects"
613
+ ],
614
+ "cultural_themes": [
615
+ "safari", "dance", "market", "coastal",
616
+ "wildlife", "village", "ceremony", "sunset", "city"
617
+ ],
618
+ "styles": ["animated", "realistic", "painting", "watercolor", "cinematic"],
619
+ "cache_enabled": True,
620
+ "cache_size": self.cache_size,
621
+ "timeout_seconds": self.timeout,
622
+ "max_retries": self.max_retries
623
+ }
624
+
625
+ def cleanup_cache(self):
626
+ """Cleanup old cache entries"""
627
+ if len(self.video_cache) > self.cache_size:
628
+ # Remove oldest entries
629
+ keys_to_remove = list(self.video_cache.keys())[:len(self.video_cache) - self.cache_size]
630
+ for key in keys_to_remove:
631
+ del self.video_cache[key]
632
+ logger.info(f"🧹 Cleaned up {len(keys_to_remove)} cache entries")