Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| ) | |