Guytron's picture
Update app.py
26b8f56 verified
"""
Video Keyword Finder with Gradio Interface
========================================
A Python script that finds and timestamps specific keywords within video files.
It transcribes the audio using Whisper AI and finds all occurrences of specified keywords
with their timestamps and surrounding context. Supports both local video files and YouTube URLs.
Requirements
-----------
- Python 3.8 or higher
- ffmpeg (must be installed and accessible in system PATH)
- GPU recommended but not required
"""
import whisper_timestamped as whisper
import datetime
import os
import tempfile
import yt_dlp
import re
import logging
import math
import subprocess
import glob
import gradio as gr
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler()
]
)
def parse_time(time_str):
"""Convert time string (HH:MM:SS) to seconds"""
if not time_str or time_str.strip() == "":
return None
try:
time_parts = list(map(int, time_str.split(':')))
if len(time_parts) == 3: # HH:MM:SS
return time_parts[0] * 3600 + time_parts[1] * 60 + time_parts[2]
elif len(time_parts) == 2: # MM:SS
return time_parts[0] * 60 + time_parts[1]
else:
return int(time_str) # Just seconds
except:
raise ValueError("Time must be in HH:MM:SS, MM:SS, or seconds format")
def format_time(seconds):
"""Convert seconds to HH:MM:SS format"""
return str(datetime.timedelta(seconds=int(seconds)))
def is_youtube_url(url):
"""Check if the provided string is a YouTube URL"""
if not url or url.strip() == "":
return False
youtube_regex = (
r'(https?://)?(www\.)?'
r'(youtube|youtu|youtube-nocookie)\.(com|be)/'
r'(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})'
)
return bool(re.match(youtube_regex, url))
def download_youtube_video(url):
"""Download YouTube video and return path to temporary file"""
temp_dir = tempfile.gettempdir()
temp_file = os.path.join(temp_dir, 'youtube_video.mp4')
ydl_opts = {
'format': 'best[ext=mp4]',
'outtmpl': temp_file,
'quiet': False,
'progress_hooks': [lambda d: logging.info(f"Download progress: {d.get('status', 'unknown')}")],
'socket_timeout': 30,
}
try:
logging.info(f"Starting download of YouTube video: {url}")
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True)
logging.info(f"Video info extracted: {info.get('title', 'Unknown title')}")
if os.path.exists(temp_file):
file_size = os.path.getsize(temp_file)
logging.info(f"Download complete. File size: {file_size / (1024*1024):.2f} MB")
return temp_file
else:
raise Exception("Download completed but file not found")
except Exception as e:
logging.error(f"Error downloading YouTube video: {str(e)}")
raise
def get_video_duration(video_path):
"""Get video duration using ffprobe"""
cmd = [
'ffprobe',
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
video_path
]
try:
output = subprocess.check_output(cmd).decode().strip()
return float(output)
except subprocess.CalledProcessError as e:
logging.error(f"Error getting video duration: {str(e)}")
raise
def split_video(video_path, segment_duration=120):
"""Split video into segments using ffmpeg"""
try:
temp_dir = tempfile.gettempdir()
segment_pattern = os.path.join(temp_dir, 'segment_%03d.mp4')
# Remove any existing segments
for old_segment in glob.glob(os.path.join(temp_dir, 'segment_*.mp4')):
try:
os.remove(old_segment)
except:
pass
# Split video into segments
cmd = [
'ffmpeg',
'-i', video_path,
'-f', 'segment',
'-segment_time', str(segment_duration),
'-c', 'copy',
'-reset_timestamps', '1',
segment_pattern
]
logging.info("Splitting video into segments...")
subprocess.run(cmd, check=True, capture_output=True)
# Get list of generated segments
segments = sorted(glob.glob(os.path.join(temp_dir, 'segment_*.mp4')))
logging.info(f"Created {len(segments)} segments")
return segments
except Exception as e:
logging.error(f"Error splitting video: {str(e)}")
raise
def process_segments(segments, keywords):
"""Process each segment sequentially"""
results = {keyword: [] for keyword in keywords}
# Load whisper model once
logging.info("Loading Whisper model...")
model = whisper.load_model("base")
for i, segment_path in enumerate(segments):
try:
segment_num = int(re.search(r'segment_(\d+)', segment_path).group(1))
start_time = segment_num * 120 # Each segment is 120 seconds
logging.info(f"Processing segment {i+1}/{len(segments)} (starting at {format_time(start_time)})")
# Transcribe segment
audio = whisper.load_audio(segment_path)
transcription = whisper.transcribe(model, audio)
# Process results
for segment in transcription['segments']:
text = segment['text'].lower()
timestamp = segment['start'] + start_time # Adjust timestamp relative to full video
for keyword in keywords:
if keyword.lower() in text:
results[keyword].append((
timestamp,
segment['text']
))
logging.info(f"Found keyword '{keyword}' at {format_time(timestamp)}: {segment['text']}")
except Exception as e:
logging.error(f"Error processing segment {segment_path}: {str(e)}")
continue
finally:
# Clean up segment file
try:
os.remove(segment_path)
except:
pass
return results
def find_keywords_in_video(video_path, keywords, begin_time=None, end_time=None):
"""Find timestamps for keywords in video transcription"""
try:
logging.info(f"Processing video: {video_path}")
logging.info(f"Searching for keywords: {keywords}")
# Convert keywords string to list and clean up
if isinstance(keywords, str):
keywords = [k.strip() for k in keywords.split(',')]
# Get video duration
duration = get_video_duration(video_path)
logging.info(f"Video duration: {duration:.2f} seconds")
# Set time bounds
start = parse_time(begin_time) if begin_time else 0
end = min(parse_time(end_time) if end_time else duration, duration)
if start is not None and end is not None and start >= end:
raise ValueError("End time must be greater than start time")
# Split video into segments
segment_duration = 120 # 2 minutes per segment
segments = split_video(video_path, segment_duration)
# Process segments sequentially
results = process_segments(segments, keywords)
return results
except Exception as e:
logging.error(f"Error processing video: {str(e)}")
raise
def process_video(video_file, youtube_url, keywords_input, start_time="0:00", end_time=None):
"""
Process video file or YouTube URL and find keywords
"""
try:
# Convert keywords string to list
keywords = [k.strip() for k in keywords_input.split(',') if k.strip()]
if not keywords:
return "Please enter at least one keyword (separated by commas)"
# Handle input source
if youtube_url and youtube_url.strip():
if not is_youtube_url(youtube_url):
return "Invalid YouTube URL. Please provide a valid URL."
video_path = download_youtube_video(youtube_url)
cleanup_needed = True
elif video_file is not None:
video_path = video_file
cleanup_needed = False
else:
return "Please provide either a video file or a YouTube URL"
try:
# Find keywords
results = find_keywords_in_video(
video_path=video_path,
keywords=keywords,
begin_time=start_time if start_time else None,
end_time=end_time if end_time else None
)
# Format results
output = []
total_matches = sum(len(matches) for matches in results.values())
output.append(f"Total matches found: {total_matches}\n")
if total_matches == 0:
output.append("No matches found for any keywords.")
else:
for keyword, matches in results.items():
if matches:
output.append(f"\nResults for '{keyword}':")
for timestamp, context in matches:
output.append(f"[{format_time(timestamp)}] {context}")
else:
output.append(f"\nNo occurrences found for '{keyword}'")
return "\n".join(output)
finally:
# Cleanup temporary files
if cleanup_needed and os.path.exists(video_path):
try:
os.remove(video_path)
except:
pass
except Exception as e:
logging.error(f"Error processing video: {str(e)}")
return f"Error processing video: {str(e)}"
# Create Gradio interface
with gr.Blocks(title="Video Keyword Finder", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# 🎥 Video Keyword Finder
Find timestamps for specific keywords in your videos using AI transcription.
Upload a video file or provide a YouTube URL, then enter the keywords you want to find.
""")
with gr.Row():
with gr.Column():
video_input = gr.File(
label="Upload Video File",
file_types=["video"],
type="filepath"
)
youtube_url = gr.Textbox(
label="Or Enter YouTube URL",
placeholder="https://www.youtube.com/watch?v=..."
)
keywords = gr.Textbox(
label="Keywords (comma-separated)",
placeholder="enter, your, keywords, here"
)
with gr.Row():
start_time = gr.Textbox(
label="Start Time (HH:MM:SS)",
placeholder="0:00",
value="0:00"
)
end_time = gr.Textbox(
label="End Time (HH:MM:SS)",
placeholder="Optional"
)
submit_btn = gr.Button("Find Keywords", variant="primary")
with gr.Column():
output = gr.Textbox(
label="Results",
placeholder="Keywords and timestamps will appear here...",
lines=20
)
gr.Markdown("""
### Instructions:
1. Upload a video file or paste a YouTube URL
2. Enter keywords separated by commas (e.g., "hello, world, python")
3. Optionally set start and end times (format: HH:MM:SS)
4. Click "Find Keywords" and wait for results
Note: Processing time depends on video length. A 1-hour video typically takes 15-30 minutes.
""")
submit_btn.click(
fn=process_video,
inputs=[video_input, youtube_url, keywords, start_time, end_time],
outputs=output
)
if __name__ == "__main__":
demo.launch(share=True, debug=True)