""" Gradio UI for Video Summarizer """ import gradio as gr import os import json import shutil import tempfile import re import yt_dlp from video_summarizer import VideoSummarizer from dotenv import load_dotenv # Load environment variables load_dotenv() # Get OpenRouter API key from environment or use default OPENROUTER_API_KEY = os.getenv( "OPENROUTER_API_KEY", "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" ) # Initialize summarizer (lazy loading) summarizer = None def initialize_summarizer(): """Initialize the summarizer (lazy loading)""" global summarizer if summarizer is None: summarizer = VideoSummarizer( openrouter_api_key=OPENROUTER_API_KEY, openrouter_model="kwaipilot/kat-coder-pro:free" ) return summarizer def is_youtube_url(url): """Check if the input is a valid YouTube URL""" youtube_patterns = [ r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/', r'(https?://)?(www\.)?youtube\.com/watch\?v=', r'(https?://)?(www\.)?youtu\.be/', ] return any(re.match(pattern, url) for pattern in youtube_patterns) def download_youtube_video(url, output_dir="temp_videos"): """ Download YouTube video Args: url: YouTube video URL output_dir: Directory to save the video Returns: Path to downloaded video file or None if failed """ try: os.makedirs(output_dir, exist_ok=True) # Configure yt-dlp options with better error handling ydl_opts = { 'format': 'best[ext=mp4][filesize<20M]/best[ext=mp4]/best', # Prefer MP4, limit size 'outtmpl': os.path.join(output_dir, '%(id)s.%(ext)s'), 'quiet': False, 'no_warnings': False, 'socket_timeout': 30, 'retries': 3, 'fragment_retries': 3, 'extractor_retries': 3, # Add proxy support if needed 'nocheckcertificate': True, } print(f"📥 Downloading YouTube video: {url}") with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=True) video_path = ydl.prepare_filename(info) # Check file size if os.path.exists(video_path): file_size_mb = os.path.getsize(video_path) / (1024 * 1024) if file_size_mb > 20: os.remove(video_path) return None, f"❌ Video size ({file_size_mb:.1f} MB) exceeds 20 MB limit" print(f"✅ Video downloaded: {video_path} ({file_size_mb:.1f} MB)") return video_path, None return None, "❌ Failed to download video" except Exception as e: error_msg = str(e) print(f"❌ Error downloading video: {error_msg}") # Provide more helpful error messages if "Failed to resolve" in error_msg or "No address associated" in error_msg: return None, "❌ Network error: Cannot connect to YouTube. This may be due to network restrictions on Hugging Face Spaces. Please try uploading the video file directly instead." elif "HTTP Error 429" in error_msg: return None, "❌ YouTube rate limit reached. Please try again later or upload the video file directly." elif "Video unavailable" in error_msg: return None, "❌ This video is unavailable or private. Please check the URL or upload the video file directly." else: return None, f"❌ Error downloading video: {error_msg}. Please try uploading the video file directly." def list_existing_clips(clips_dir="clips"): """ List existing video clips in the clips directory Args: clips_dir: Directory to search for clips Returns: List of video clip paths and status message """ try: if not os.path.exists(clips_dir): return [], f"📁 Clips directory not found: {clips_dir}" # Find all video files in the directory video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.webm'] clip_paths = [] for filename in os.listdir(clips_dir): file_path = os.path.join(clips_dir, filename) if os.path.isfile(file_path): # Check if it's a video file _, ext = os.path.splitext(filename.lower()) if ext in video_extensions: clip_paths.append(file_path) # Sort by filename for consistent ordering clip_paths.sort() if clip_paths: status_msg = ( f"✅ Found {len(clip_paths)} existing clip(s)\n" f"📁 Location: {clips_dir}\n" f"💡 Click on clips to preview" ) return clip_paths, status_msg else: return [], "📭 No video clips found. Generate some clips first!" except Exception as e: error_msg = f"❌ Error listing clips: {str(e)}" print(error_msg) return [], error_msg def view_identified_segments(clips_dir="clips"): """ View identified segments (including failed ones) from the last processing run Args: clips_dir: Directory to search for segment files Returns: Formatted string with segment information """ try: if not os.path.exists(clips_dir): return "📁 Clips directory not found" segments_path = os.path.join(clips_dir, "segments.json") failed_path = os.path.join(clips_dir, "failed_segments.json") output_lines = [] # Load identified segments if os.path.exists(segments_path): with open(segments_path, 'r', encoding='utf-8') as f: segments = json.load(f) output_lines.append(f"## 📋 Identified Segments ({len(segments)})\n\n") for i, seg in enumerate(segments, 1): start = seg.get('start', 0) end = seg.get('end', 0) text = seg.get('text', 'No description') duration = end - start output_lines.append( f"**Clip {i}:** `{start:.2f}s - {end:.2f}s` ({duration:.2f}s)\n" f"> {text}\n\n" ) else: output_lines.append("📋 No segments file found\n") # Load failed segments if os.path.exists(failed_path): with open(failed_path, 'r', encoding='utf-8') as f: failed = json.load(f) if failed: output_lines.append(f"\n---\n\n## ⚠️ Failed Clips ({len(failed)})\n\n") for seg in failed: start = seg.get('start', 0) end = seg.get('end', 0) error = seg.get('error', 'Unknown error') output_lines.append( f"**Segment {seg.get('segment_num', '?')}:** `{start:.2f}s - {end:.2f}s`\n" f"> ❌ Error: {error}\n\n" ) if len(output_lines) == 1 and "No segments file found" in output_lines[0]: return "## 📭 No Information Available\n\nGenerate clips first to see segment details." return "".join(output_lines) except Exception as e: error_msg = f"❌ Error reading segments: {str(e)}" print(error_msg) return error_msg def process_video(video_file, youtube_url, num_clips, progress=gr.Progress()): """ Process video and generate clips Args: video_file: Uploaded video file youtube_url: YouTube URL (optional) num_clips: Number of clips to generate progress: Gradio progress tracker Returns: List of video clip paths and status message """ video_path = None temp_video = None try: # Validate inputs progress(0, desc="🔍 Validating inputs...") num_clips = int(num_clips) if num_clips < 1 or num_clips > 5: return None, "❌ Number of clips must be between 1 and 5" # Check if YouTube URL is provided if youtube_url and youtube_url.strip(): if not is_youtube_url(youtube_url): return None, "❌ Invalid YouTube URL" # Download YouTube video progress(0.05, desc="📥 Downloading video from YouTube...") video_path, error = download_youtube_video(youtube_url) if error: return None, error temp_video = video_path # Mark for cleanup progress(0.15, desc="✅ Video downloaded successfully") # Otherwise use uploaded file elif video_file is not None: progress(0.05, desc="📤 Processing uploaded video...") # Handle video file path (Gradio 6.x returns string directly) if isinstance(video_file, str): video_path = video_file elif hasattr(video_file, 'name'): video_path = video_file.name else: return None, "❌ Invalid video file format" # Check file size (20 MB limit) if os.path.exists(video_path): file_size_mb = os.path.getsize(video_path) / (1024 * 1024) if file_size_mb > 20: return None, f"❌ File size ({file_size_mb:.1f} MB) exceeds 20 MB limit" progress(0.15, desc="✅ Video file ready") else: return None, "❌ Please upload a video file or provide a YouTube URL" # Initialize summarizer progress(0.2, desc="🔧 Initializing summarizer...") summarizer = initialize_summarizer() # Use absolute path in current working directory (Gradio can access this) clips_dir = os.path.abspath("clips") os.makedirs(clips_dir, exist_ok=True) # Process video with detailed progress tracking def progress_update(value, desc): progress(value, desc=desc) clip_paths = summarizer.process_video( video_path=video_path, num_clips=num_clips, output_dir=clips_dir, progress_callback=progress_update ) if not clip_paths: return None, "❌ Failed to generate clips" progress(1.0, desc="✅ Complete!") # Return clips with detailed status status_msg = ( f"✅ **Success!** Generated {len(clip_paths)} clip(s)\n" f"📁 Clips saved to: {clips_dir}\n" f"💡 Click on clips to preview, or use 'View Segment Details' for more info" ) return clip_paths, status_msg except Exception as e: error_msg = f"❌ Error processing video: {str(e)}" print(error_msg) return None, error_msg finally: # Clean up temporary YouTube video if temp_video and os.path.exists(temp_video): try: os.remove(temp_video) print(f"🗑️ Cleaned up temporary file: {temp_video}") except: pass # Create Gradio interface # Gradio 6.x compatibility - theme is set differently demo = gr.Blocks(title="Video Summarizer - Arabic") with demo: gr.Markdown(""" # 🎬 Video Summarizer - Arabic Edition AI-powered tool that processes Arabic videos and automatically generates short highlight clips. **Features:** - 🎤 Arabic speech recognition using Whisper + LoRA - 🤖 AI-powered highlight detection via OpenRouter - ✂️ Automatic video clip generation - 🔗 Support for YouTube URLs (may not work on all platforms due to network restrictions) **Limitations:** - Maximum file size: 20 MB - Maximum duration: 12 minutes - Number of clips: 1-5 **Note:** If YouTube download fails, please download the video and upload it directly. """) with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 📥 Input") youtube_url = gr.Textbox( label="🔗 YouTube URL (Optional)", placeholder="https://www.youtube.com/watch?v=...", lines=1, info="Paste a YouTube URL here" ) gr.Markdown("**— OR —**", elem_classes="text-center") video_input = gr.Video( label="📤 Upload Video File", # type parameter removed for Gradio 6.x compatibility ) num_clips = gr.Slider( minimum=1, maximum=5, value=3, step=1, label="🎬 Number of Clips to Generate", info="Choose between 1-5 highlight clips" ) with gr.Row(): generate_btn = gr.Button( "🚀 Generate Shorts", variant="primary", size="lg", scale=2 ) view_clips_btn = gr.Button( "📁 View Saved Clips", variant="secondary", scale=1 ) status = gr.Textbox( label="📊 Status", interactive=False, lines=2 ) with gr.Column(scale=1): gr.Markdown("### 🎥 Generated Clips") clips_gallery = gr.Gallery( label="Video Clips", show_label=False, elem_id="gallery", columns=2, rows=3, height="600px", object_fit="contain", preview=True ) view_segments_btn = gr.Button( "📋 View Segment Details", variant="secondary", size="sm" ) segments_info = gr.Markdown( value="", visible=True ) # Set up event handlers generate_btn.click( fn=process_video, inputs=[video_input, youtube_url, num_clips], outputs=[clips_gallery, status] ) view_clips_btn.click( fn=list_existing_clips, inputs=[], outputs=[clips_gallery, status] ) view_segments_btn.click( fn=view_identified_segments, inputs=[], outputs=[segments_info] ) with gr.Accordion("📖 Instructions & Details", open=False): gr.Markdown(""" ### 📝 How to Use: 1. **Choose Input Method:** - Paste a YouTube URL, **OR** - Upload your Arabic video file (max 20 MB) 2. **Select Number of Clips:** - Use the slider to choose 1-5 highlight clips 3. **Generate:** - Click "🚀 Generate Shorts" and wait for processing - Progress will be shown in real-time 4. **View Results:** - Clips appear in the gallery on the right - Click on any clip to preview it - Use "📋 View Segment Details" to see timestamps and descriptions --- ### 🔧 Features: - **📁 View Saved Clips**: Access previously generated clips - **📋 Segment Details**: See what the AI identified in your video - **Real-time Progress**: Track each processing step --- ### ⚙️ Technical Details: - **ASR Model**: Whisper-small + LoRA (Arabic Egyptian dialect) - **AI Analysis**: OpenRouter API (kwaipilot/kat-coder-pro:free) - **Video Processing**: MoviePy with FFmpeg - **Supported Formats**: MP4, AVI, MOV, MKV, WebM, and more --- ### ⚠️ Limitations: - Maximum file size: 20 MB - Maximum duration: ~12 minutes - Clips range: 1-5 per video - YouTube downloads may fail due to network restrictions (use file upload instead) """) if __name__ == "__main__": # Create clips directory in current working directory (Gradio can access this) clips_dir = os.path.abspath("clips") os.makedirs(clips_dir, exist_ok=True) # Also allow /tmp/clips as fallback (for any existing clips) import tempfile temp_clips_dir = "/tmp/clips" # Launch Gradio app with allowed paths # Clips are saved in "clips" directory in current working directory demo.launch( allowed_paths=[clips_dir, temp_clips_dir] # Allow both directories )