ziadtarek12's picture
Improve UI layout, clip display, and download functionality
d862e49
Raw
History Blame Contribute Delete
17.1 kB
"""
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
)