Stanley03 commited on
Commit
6410a94
·
verified ·
1 Parent(s): 8d3a79a

Update video_generation.py

Browse files
Files changed (1) hide show
  1. video_generation.py +79 -577
video_generation.py CHANGED
@@ -3,630 +3,132 @@ 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")
 
3
  import base64
4
  import time
5
  import requests
 
6
  import logging
 
 
 
 
 
7
  import random
8
+ from typing import Optional
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
  class FreeVideoGenerator:
13
  """
14
+ Free video generation using Hugging Face Inference API
15
  """
16
 
17
  def __init__(self, hf_token: Optional[str] = None):
18
  self.hf_token = hf_token or os.getenv('HF_TOKEN', '')
 
19
 
20
+ # Available free models
21
+ self.text_to_video_models = [
22
+ "cerspense/zeroscope_v2_576w",
23
+ "damo-vilab/text-to-video-ms-1.7b"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  ]
25
 
26
+ self.current_model = self.text_to_video_models[0]
27
+ self.timeout = 120
28
+ self.max_retries = 2
29
 
30
+ def enhance_prompt_with_context(self, prompt: str) -> str:
31
+ """Enhance video prompts with cinematic context"""
 
 
 
 
 
 
 
 
 
 
32
  cinematic_enhancements = [
33
  "cinematic, 8k, ultra detailed, high quality, masterpiece",
34
  "epic, dramatic lighting, film grain, cinematic shot, professional",
35
  "beautiful, stunning, visually striking, vivid colors, trending",
36
+ "high resolution, detailed, sharp focus, studio quality"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  ]
38
 
39
+ enhanced = prompt
40
+ enhanced += f", {random.choice(cinematic_enhancements)}, 576x320 resolution, 8 fps"
41
 
42
+ return enhanced
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ def generate_text_to_video(self, prompt: str) -> Optional[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  """
46
+ Generate video from text prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  """
48
  try:
49
+ enhanced_prompt = self.enhance_prompt_with_context(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
+ headers = {}
52
+ if self.hf_token:
53
+ headers["Authorization"] = f"Bearer {self.hf_token}"
 
54
 
55
+ payload = {
56
+ "inputs": enhanced_prompt,
57
+ "parameters": {
58
+ "num_frames": 24,
59
+ "num_inference_steps": 25,
60
+ "guidance_scale": 7.5,
61
+ "fps": 8,
62
+ "height": 320,
63
+ "width": 576
 
 
 
 
 
 
 
64
  }
65
+ }
66
 
67
+ for attempt in range(self.max_retries):
68
+ try:
69
+ logger.info(f"🎬 Generating video (attempt {attempt + 1}): {prompt[:50]}...")
70
+
71
+ response = requests.post(
72
+ f"https://api-inference.huggingface.co/models/{self.current_model}",
73
+ headers=headers,
74
+ json=payload,
75
+ timeout=self.timeout
76
+ )
77
+
78
+ if response.status_code == 200:
79
+ video_b64 = base64.b64encode(response.content).decode('utf-8')
80
+ return f"data:video/mp4;base64,{video_b64}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ elif response.status_code == 503:
83
+ wait_time = (attempt + 1) * 10
84
+ logger.info(f"⏳ Model loading, waiting {wait_time}s...")
85
+ time.sleep(wait_time)
86
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ else:
89
+ logger.error(f"Video API error {response.status_code}")
 
 
90
 
91
+ except requests.exceptions.Timeout:
92
+ logger.warning(f"⏰ Request timeout, attempt {attempt + 1}")
93
+ continue
 
 
 
 
 
 
 
 
 
94
 
 
 
 
 
 
 
 
 
 
 
 
95
  except Exception as e:
96
+ logger.error(f"Video generation error: {e}")
97
+
98
+ return None
99
 
100
  def create_cultural_video(self, theme: str, style: str = "animated") -> Optional[str]:
101
  """
102
+ Create videos with cultural themes
 
 
 
 
 
 
 
103
  """
 
104
  cultural_themes = {
105
+ "safari": "African safari sunset with elephants and giraffes, majestic savanna landscape",
106
+ "dance": "Traditional Maasai warriors dancing, vibrant colors, cultural celebration",
107
+ "market": "Busy African market scene, vibrant colors, people trading goods",
108
+ "coastal": "Swahili coast with traditional dhows sailing, Indian Ocean waves",
109
+ "wildlife": "African wildlife documentary style, lions hunting on savanna",
110
+ "village": "Traditional African village life, community activities, sunset"
 
 
 
111
  }
112
 
113
+ base_prompt = cultural_themes.get(theme, f"African {theme}, cultural, vibrant")
 
114
 
 
115
  style_enhancements = {
116
+ "animated": "animated, cartoon style, smooth motion, vibrant colors",
117
+ "realistic": "realistic, documentary style, cinematic, natural lighting"
 
 
 
118
  }
119
 
120
+ full_prompt = f"{base_prompt}, {style_enhancements.get(style, 'animated, vibrant')}"
 
 
121
 
122
  return self.generate_text_to_video(full_prompt)
123
 
124
+ def get_video_info(self) -> dict:
125
+ """Get information about video generation"""
126
  return {
127
+ "available_models": self.text_to_video_models,
128
+ "current_model": self.current_model,
129
+ "resolution": "576x320",
130
+ "fps": 8,
131
+ "max_frames": 24,
132
+ "max_duration": "3 seconds",
133
+ "free": True
134
+ }