| 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" |
| |
| |
| self.models = { |
| |
| "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": { |
| "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": { |
| "animate_diff": "guoyww/animatediff", |
| "magic_animate": "zcxu-eric/MagicAnimate", |
| "text2video_zero": "PAIR/Text2Video-Zero" |
| } |
| } |
| |
| |
| 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" |
| } |
| |
| |
| self.timeout = 120 |
| self.max_retries = 3 |
| self.wait_between_retries = [10, 20, 30] |
| |
| |
| self.default_fps = 8 |
| self.default_frames = 24 |
| self.default_width = 576 |
| self.default_height = 320 |
| |
| |
| 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_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, '') |
| |
| |
| 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""" |
| |
| |
| 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_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_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 |
| |
| |
| enhanced_prompt += f", {random.choice(cinematic_enhancements)}" |
| |
| |
| enhanced_prompt += f", {random.choice(motion_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 |
| |
| |
| 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] |
| |
| |
| if len(self.video_cache) >= self.cache_size: |
| |
| 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 |
| """ |
| |
| |
| 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}" |
| |
| |
| payload = { |
| "inputs": prompt, |
| "parameters": { |
| "num_frames": self.default_frames, |
| "num_inference_steps": 25, |
| "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: |
| |
| video_bytes = response.content |
| video_b64 = base64.b64encode(video_bytes).decode('utf-8') |
| |
| |
| 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}" |
| |
| |
| self.cache_video(prompt, video_data) |
| |
| return video_data |
| |
| elif response.status_code == 503: |
| |
| 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 |
| |
| |
| 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: |
| |
| if image_data.startswith('data:image'): |
| |
| image_b64 = image_data.split(',')[1] |
| image_bytes = base64.b64decode(image_b64) |
| image = Image.open(io.BytesIO(image_bytes)) |
| else: |
| |
| 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) |
| |
| |
| image = image.resize((self.default_width, self.default_height), Image.Resampling.LANCZOS) |
| |
| |
| img_byte_arr = io.BytesIO() |
| image.save(img_byte_arr, format='PNG') |
| img_byte_arr = img_byte_arr.getvalue() |
| |
| |
| 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 "" |
| } |
| |
| |
| 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 |
| ) |
| |
| 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: |
| |
| with tempfile.TemporaryDirectory() as tmpdir: |
| image_paths = [] |
| |
| |
| 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) |
| |
| |
| 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] |
| |
| |
| fps = 10 |
| output_path = os.path.join(tmpdir, 'output.mp4') |
| fourcc = cv2.VideoWriter_fourcc(*'mp4v') |
| out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) |
| |
| |
| frames_per_image = int(fps * duration_per_image) |
| transition_frames = int(fps * 0.5) |
| |
| for i in range(len(image_paths)): |
| current_img = cv2.imread(image_paths[i]) |
| if current_img is None: |
| continue |
| |
| |
| current_img = cv2.resize(current_img, (width, height)) |
| |
| |
| main_frames = frames_per_image - transition_frames |
| for _ in range(main_frames): |
| out.write(current_img) |
| |
| |
| 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)) |
| |
| |
| 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() |
| |
| |
| 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: |
| |
| with tempfile.TemporaryDirectory() as tmpdir: |
| |
| fps = 10 |
| duration = 4 |
| 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)) |
| |
| |
| colors = [ |
| (41, 128, 185), |
| (39, 174, 96), |
| (142, 68, 173), |
| (230, 126, 34), |
| (231, 76, 60) |
| ] |
| |
| for frame_num in range(total_frames): |
| |
| frame = np.zeros((height, width, 3), dtype=np.uint8) |
| |
| |
| color_idx = (frame_num // (total_frames // len(colors))) % len(colors) |
| bg_color = colors[color_idx] |
| |
| |
| for i in range(height): |
| |
| 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 |
| frame[i, :, 1] = g |
| frame[i, :, 2] = r |
| |
| |
| font = cv2.FONT_HERSHEY_SIMPLEX |
| |
| |
| text_lines = text.split(' ') |
| y_start = height // 2 - (len(text_lines) * 40) // 2 |
| |
| for i, line in enumerate(text_lines): |
| |
| 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) |
| |
| |
| text_size = cv2.getTextSize(line, font, font_scale, thickness)[0] |
| text_x = (width - text_size[0]) // 2 |
| text_y = y_start + i * 40 |
| |
| |
| shadow_color = (0, 0, 0) |
| cv2.putText(frame, line, (text_x + 2, text_y + 2), font, |
| font_scale, shadow_color, thickness + 1) |
| |
| |
| text_color = (255, 255, 255) |
| cv2.putText(frame, line, (text_x, text_y), font, |
| font_scale, text_color, thickness) |
| |
| |
| if frame_num % 10 < 5: |
| |
| 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() |
| |
| |
| 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 = { |
| "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" |
| } |
| |
| |
| base_prompt = cultural_themes.get(theme, f"African {theme}, cultural, vibrant, dynamic") |
| |
| |
| 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: |
| |
| 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") |