import os import io import base64 import time import requests import json import logging from PIL import Image import numpy as np from typing import List, Optional, Dict, Any import cv2 import tempfile import random logger = logging.getLogger(__name__) class FreeVideoGenerator: """ Free video generation using open-source models on Hugging Face """ def __init__(self, hf_token: Optional[str] = None): self.hf_token = hf_token or os.getenv('HF_TOKEN', '') self.base_url = "https://api-inference.huggingface.co/models" # Available free models for different tasks self.models = { # Text-to-Video models (FREE) "text_to_video": { "zeroscope_v2": "cerspense/zeroscope_v2_576w", "modelscope": "damo-vilab/text-to-video-ms-1.7b", "stable_video": "stabilityai/stable-video-diffusion-img2vid-xt", "video_crafter": "VideoCrafter/VideoCrafter2", "animatediff": "guoyww/animatediff" }, # Image-to-Video models (FREE) "image_to_video": { "stable_video": "stabilityai/stable-video-diffusion-img2vid-xt", "img2vid_xt": "stabilityai/stable-video-diffusion-img2vid-xt-1-1", "zeroscope_img2vid": "cerspense/zeroscope_v2_XL" }, # Animation models (FREE) "animation": { "animate_diff": "guoyww/animatediff", "magic_animate": "zcxu-eric/MagicAnimate", "text2video_zero": "PAIR/Text2Video-Zero" } } # Free API endpoints that work without token self.free_endpoints = { "text_to_video": "https://api-inference.huggingface.co/models/cerspense/zeroscope_v2_576w", "image_to_video": "https://api-inference.huggingface.co/models/stabilityai/stable-video-diffusion-img2vid-xt", "animation": "https://api-inference.huggingface.co/models/PAIR/Text2Video-Zero" } # Performance settings self.timeout = 120 # Longer timeout for videos self.max_retries = 3 self.wait_between_retries = [10, 20, 30] # Progressive waiting # Video settings self.default_fps = 8 self.default_frames = 24 self.default_width = 576 self.default_height = 320 # Cache for generated videos self.video_cache = {} self.cache_size = 50 def detect_video_request(self, text: str) -> bool: """Detect if user wants to generate a video""" video_triggers = [ 'generate video', 'create video', 'make a video', 'video of', 'animate', 'animation', 'moving picture', 'motion picture', 'video generation', 'create animation', 'make animation', 'video clip', 'short video', 'motion graphics', 'cinematic', 'film', 'movie', 'moving image', 'dynamic image', 'animated video' ] text_lower = text.lower() return any(trigger in text_lower for trigger in video_triggers) def extract_video_prompt(self, text: str) -> str: """Extract video description from user message""" prompt = text.lower() # Remove common video request phrases remove_phrases = [ 'generate video of', 'create video of', 'make a video of', 'create animation of', 'make animation of', 'animate', 'generate animation of', 'video of', 'animation of', 'make a film about', 'create a film about', 'produce video of', 'can you make a video', 'i want a video', 'show me a video', 'video showing', 'animate this', 'create moving image of' ] for phrase in remove_phrases: prompt = prompt.replace(phrase, '') # Remove question words question_words = ['how to', 'what is', 'can you', 'could you', 'would you'] for word in question_words: if prompt.startswith(word): prompt = prompt[len(word):].strip() return prompt.strip().capitalize() def enhance_prompt_with_context(self, prompt: str, context_type: str = "general") -> str: """Enhance video prompts with cinematic and cultural context""" # Basic cinematic enhancements cinematic_enhancements = [ "cinematic, 8k, ultra detailed, high quality, masterpiece", "epic, dramatic lighting, film grain, cinematic shot, professional", "beautiful, stunning, visually striking, vivid colors, trending", "high resolution, detailed, sharp focus, studio quality, professional", "film still, movie scene, cinematic photography, 35mm film" ] # Cultural/Kiswahili enhancements cultural_enhancements = { "safari": "African safari, wildlife documentary style, national geographic, savanna", "cultural": "traditional African culture, vibrant colors, community celebration, authentic", "coastal": "Swahili coast, Indian Ocean, dhows sailing, traditional architecture, beach", "urban": "modern African city, bustling streets, contemporary life, urban landscape", "historical": "historical Africa, ancient kingdoms, traditional ceremonies, heritage", "wildlife": "African wildlife, natural habitat, animal behavior, nature documentary", "village": "traditional African village, community life, rural setting, authentic" } # Motion and animation enhancements motion_enhancements = [ "smooth motion, fluid animation, dynamic movement, cinematic motion", "slow motion, dramatic pacing, epic timing, filmic movement", "fast paced, energetic movement, dynamic action, lively animation" ] enhanced_prompt = prompt # Add cinematic quality enhanced_prompt += f", {random.choice(cinematic_enhancements)}" # Add motion enhancement enhanced_prompt += f", {random.choice(motion_enhancements)}" # Add context-specific enhancements context_keywords = { "safari": ["safari", "wildlife", "animal", "lion", "elephant", "giraffe"], "cultural": ["culture", "traditional", "dance", "ceremony", "ritual"], "coastal": ["coast", "beach", "ocean", "sea", "dhow", "swahili"], "urban": ["city", "urban", "street", "building", "modern", "skyline"], "historical": ["history", "ancient", "kingdom", "heritage", "traditional"], "wildlife": ["animal", "bird", "nature", "wild", "savanna", "forest"], "village": ["village", "rural", "community", "hut", "traditional"] } prompt_lower = enhanced_prompt.lower() for theme, keywords in context_keywords.items(): if any(keyword in prompt_lower for keyword in keywords): enhanced_prompt += f", {cultural_enhancements.get(theme, '')}" break # Add technical specifications for better results technical_specs = [ f"{self.default_width}x{self.default_height} resolution", f"{self.default_fps} fps", "high bitrate", "stable diffusion", "consistent quality" ] enhanced_prompt += f", {', '.join(random.sample(technical_specs, 2))}" return enhanced_prompt def get_cached_video(self, prompt: str) -> Optional[str]: """Get cached video if available""" cache_key = prompt.lower().strip()[:100] return self.video_cache.get(cache_key) def cache_video(self, prompt: str, video_data: str): """Cache generated video""" cache_key = prompt.lower().strip()[:100] # Limit cache size if len(self.video_cache) >= self.cache_size: # Remove oldest entry self.video_cache.pop(next(iter(self.video_cache))) self.video_cache[cache_key] = video_data def generate_text_to_video(self, prompt: str, model: str = "zeroscope_v2") -> Optional[str]: """ Generate video from text prompt using free models Args: prompt: Text description of the video model: Model to use ('zeroscope_v2', 'modelscope', etc.) Returns: Base64 encoded video or None """ # Check cache first cached_video = self.get_cached_video(prompt) if cached_video: logger.info("๐ŸŽฌ Using cached video") return cached_video model_id = self.models["text_to_video"].get(model, "cerspense/zeroscope_v2_576w") api_url = f"{self.base_url}/{model_id}" headers = {} if self.hf_token: headers["Authorization"] = f"Bearer {self.hf_token}" # Optimized parameters for faster generation payload = { "inputs": prompt, "parameters": { "num_frames": self.default_frames, "num_inference_steps": 25, # Reduced for speed "guidance_scale": 7.5, "fps": self.default_fps, "height": self.default_height, "width": self.default_width, "negative_prompt": "blurry, low quality, distorted, bad anatomy, watermark, text" } } for attempt in range(self.max_retries): try: logger.info(f"๐ŸŽฌ Generating video (attempt {attempt + 1}): {prompt[:50]}...") response = requests.post( api_url, headers=headers, json=payload, timeout=self.timeout ) if response.status_code == 200: # Convert to base64 video_bytes = response.content video_b64 = base64.b64encode(video_bytes).decode('utf-8') # Determine format content_type = response.headers.get('content-type', 'video/mp4') if 'webm' in content_type: format_str = "webm" else: format_str = "mp4" video_data = f"data:video/{format_str};base64,{video_b64}" # Cache the result self.cache_video(prompt, video_data) return video_data elif response.status_code == 503: # Model is loading wait_time = self.wait_between_retries[min(attempt, len(self.wait_between_retries)-1)] logger.info(f"โณ Video model loading, waiting {wait_time}s...") time.sleep(wait_time) continue else: logger.error(f"Video API error {response.status_code}: {response.text[:200]}") except requests.exceptions.Timeout: logger.warning(f"โฐ Video generation timeout, attempt {attempt + 1}") time.sleep(self.wait_between_retries[min(attempt, len(self.wait_between_retries)-1)]) continue except Exception as e: logger.error(f"Video generation error: {e}") if attempt < self.max_retries - 1: time.sleep(self.wait_between_retries[min(attempt, len(self.wait_between_retries)-1)]) continue break # Fallback to simpler animation if video generation fails logger.info("๐Ÿ”„ Falling back to text animation") return self.generate_animation_from_text(prompt) def generate_image_to_video(self, image_data: str, prompt: str = "") -> Optional[str]: """ Generate video from an image using free models Args: image_data: Base64 encoded image or image URL prompt: Optional text prompt for guidance Returns: Base64 encoded video or None """ try: # Prepare image if image_data.startswith('data:image'): # Extract base64 from data URL image_b64 = image_data.split(',')[1] image_bytes = base64.b64decode(image_b64) image = Image.open(io.BytesIO(image_bytes)) else: # Assume it's a file path or URL if image_data.startswith('http'): response = requests.get(image_data, timeout=30) image = Image.open(io.BytesIO(response.content)) else: image = Image.open(image_data) # Resize image for faster processing image = image.resize((self.default_width, self.default_height), Image.Resampling.LANCZOS) # Convert to bytes img_byte_arr = io.BytesIO() image.save(img_byte_arr, format='PNG') img_byte_arr = img_byte_arr.getvalue() # Use free model (Stable Video Diffusion) model_id = "stabilityai/stable-video-diffusion-img2vid-xt" api_url = f"{self.base_url}/{model_id}" headers = { "Authorization": f"Bearer {self.hf_token}" if self.hf_token else "" } # If prompt is provided, use it as guidance params = {} if prompt: params = { "parameters": { "motion_bucket_id": 127, "noise_aug_strength": 0.02 } } response = requests.post( api_url, headers=headers, data=img_byte_arr, json=params if params else None, timeout=150 # Longer timeout for image-to-video ) if response.status_code == 200: video_b64 = base64.b64encode(response.content).decode('utf-8') return f"data:video/mp4;base64,{video_b64}" else: logger.error(f"Image-to-video API error: {response.status_code}") return None except Exception as e: logger.error(f"Image to video error: {e}") return None def create_slideshow_video(self, images: List[str], duration_per_image: float = 2.0) -> Optional[str]: """ Create a simple slideshow video from multiple images Args: images: List of base64 encoded images duration_per_image: Duration for each image in seconds Returns: Base64 encoded video """ try: # Create temporary directory with tempfile.TemporaryDirectory() as tmpdir: image_paths = [] # Save all images for i, img_data in enumerate(images): if img_data.startswith('data:image'): img_b64 = img_data.split(',')[1] img_bytes = base64.b64decode(img_b64) else: img_bytes = base64.b64decode(img_data) img_path = os.path.join(tmpdir, f'frame_{i:03d}.png') with open(img_path, 'wb') as f: f.write(img_bytes) image_paths.append(img_path) # Read first image to get dimensions first_img = cv2.imread(image_paths[0]) if first_img is None: logger.error("Failed to read first image") return None height, width = first_img.shape[:2] # Create video writer fps = 10 output_path = os.path.join(tmpdir, 'output.mp4') fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) # Write frames with smooth transitions frames_per_image = int(fps * duration_per_image) transition_frames = int(fps * 0.5) # Half second transition for i in range(len(image_paths)): current_img = cv2.imread(image_paths[i]) if current_img is None: continue # Resize to match dimensions current_img = cv2.resize(current_img, (width, height)) # Write main frames main_frames = frames_per_image - transition_frames for _ in range(main_frames): out.write(current_img) # Add transition to next image if exists if i < len(image_paths) - 1: next_img = cv2.imread(image_paths[i + 1]) if next_img is not None: next_img = cv2.resize(next_img, (width, height)) # Create crossfade transition for t in range(transition_frames): alpha = t / transition_frames beta = 1.0 - alpha blended = cv2.addWeighted(current_img, beta, next_img, alpha, 0) out.write(blended) out.release() # Read and encode video with open(output_path, 'rb') as f: video_bytes = f.read() video_b64 = base64.b64encode(video_bytes).decode('utf-8') return f"data:video/mp4;base64,{video_b64}" except Exception as e: logger.error(f"Slideshow error: {e}") return None def generate_animation_from_text(self, text: str) -> Optional[str]: """ Create simple text animation Args: text: Text to animate Returns: Base64 encoded video """ try: # Create temporary directory with tempfile.TemporaryDirectory() as tmpdir: # Create frames with text fps = 10 duration = 4 # seconds total_frames = fps * duration height, width = self.default_height, self.default_width output_path = os.path.join(tmpdir, 'animation.mp4') fourcc = cv2.VideoWriter_fourcc(*'mp4v') out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) # Create gradient background colors colors = [ (41, 128, 185), # Blue (39, 174, 96), # Green (142, 68, 173), # Purple (230, 126, 34), # Orange (231, 76, 60) # Red ] for frame_num in range(total_frames): # Create gradient background frame = np.zeros((height, width, 3), dtype=np.uint8) # Select color based on frame color_idx = (frame_num // (total_frames // len(colors))) % len(colors) bg_color = colors[color_idx] # Apply gradient for i in range(height): # Gradient from top to bottom factor = i / height r = int(bg_color[2] * (1 - factor) + 10 * factor) g = int(bg_color[1] * (1 - factor) + 10 * factor) b = int(bg_color[0] * (1 - factor) + 10 * factor) frame[i, :, 0] = b # OpenCV uses BGR frame[i, :, 1] = g frame[i, :, 2] = r # Add text with animation font = cv2.FONT_HERSHEY_SIMPLEX # Calculate text position (center) text_lines = text.split(' ') y_start = height // 2 - (len(text_lines) * 40) // 2 for i, line in enumerate(text_lines): # Calculate font size with pulse effect pulse = 0.7 + 0.3 * np.sin(2 * np.pi * (frame_num / fps) + i * 0.5) font_scale = 1.2 * pulse thickness = int(2 * pulse) # Calculate text size and position text_size = cv2.getTextSize(line, font, font_scale, thickness)[0] text_x = (width - text_size[0]) // 2 text_y = y_start + i * 40 # Add text shadow shadow_color = (0, 0, 0) cv2.putText(frame, line, (text_x + 2, text_y + 2), font, font_scale, shadow_color, thickness + 1) # Add main text text_color = (255, 255, 255) # White cv2.putText(frame, line, (text_x, text_y), font, font_scale, text_color, thickness) # Add decorative elements if frame_num % 10 < 5: # Add twinkling stars for _ in range(3): star_x = random.randint(0, width) star_y = random.randint(0, height) cv2.circle(frame, (star_x, star_y), 2, (255, 255, 255), -1) out.write(frame) out.release() # Read and encode video with open(output_path, 'rb') as f: video_bytes = f.read() video_b64 = base64.b64encode(video_bytes).decode('utf-8') return f"data:video/mp4;base64,{video_b64}" except Exception as e: logger.error(f"Text animation error: {e}") return None def create_cultural_video(self, theme: str, style: str = "animated") -> Optional[str]: """ Create videos with Kiswahili cultural themes Args: theme: Cultural theme (safari, ceremony, dance, etc.) style: Animation style Returns: Base64 encoded video """ # Cultural themes and prompts cultural_themes = { "safari": "African safari sunset with elephants and giraffes walking, majestic savanna landscape", "dance": "Traditional Maasai warriors dancing, vibrant colors, cultural celebration, energetic movement", "market": "Busy African market scene, vibrant colors, people trading goods, lively atmosphere", "coastal": "Swahili coast with traditional dhows sailing, Indian Ocean waves, beach scenery", "wildlife": "African wildlife documentary style, lions hunting on savanna, dramatic nature scene", "village": "Traditional African village life, community activities, sunset over huts", "ceremony": "African wedding ceremony, traditional attire, dancing, celebration, cultural rituals", "sunset": "African sunset over savanna, acacia trees silhouette, warm colors, peaceful scene", "city": "Modern African city at night, Nairobi skyline, lights, urban life, contemporary" } # Get prompt for theme base_prompt = cultural_themes.get(theme, f"African {theme}, cultural, vibrant, dynamic") # Add style-specific enhancements style_enhancements = { "animated": "animated, cartoon style, smooth motion, vibrant colors, lively", "realistic": "realistic, documentary style, cinematic, natural lighting, photorealistic", "painting": "painting style, brush strokes, artistic, masterpiece, textured", "watercolor": "watercolor painting, soft edges, dreamy, artistic, blended colors", "cinematic": "cinematic, film grain, dramatic lighting, movie scene, professional" } style_enhancement = style_enhancements.get(style, "animated, vibrant, smooth motion") full_prompt = f"{base_prompt}, {style_enhancement}, {self.default_width}x{self.default_height}, {self.default_fps} fps" return self.generate_text_to_video(full_prompt) def get_video_info(self) -> Dict[str, Any]: """Get information about available video generation options""" return { "available_models": { "text_to_video": list(self.models["text_to_video"].keys()), "image_to_video": list(self.models["image_to_video"].keys()), "animation": list(self.models["animation"].keys()) }, "free_models": ["zeroscope_v2", "stable_video", "text2video_zero"], "max_duration": "4 seconds", "max_frames": self.default_frames, "resolution": f"{self.default_width}x{self.default_height}", "fps": self.default_fps, "formats": ["MP4", "WebM"], "features": [ "Text-to-Video", "Image-to-Video", "Slideshow Creation", "Text Animation", "Cultural Themes", "Crossfade Transitions", "Animated Text Effects" ], "cultural_themes": [ "safari", "dance", "market", "coastal", "wildlife", "village", "ceremony", "sunset", "city" ], "styles": ["animated", "realistic", "painting", "watercolor", "cinematic"], "cache_enabled": True, "cache_size": self.cache_size, "timeout_seconds": self.timeout, "max_retries": self.max_retries } def cleanup_cache(self): """Cleanup old cache entries""" if len(self.video_cache) > self.cache_size: # Remove oldest entries keys_to_remove = list(self.video_cache.keys())[:len(self.video_cache) - self.cache_size] for key in keys_to_remove: del self.video_cache[key] logger.info(f"๐Ÿงน Cleaned up {len(keys_to_remove)} cache entries")