Guytron commited on
Commit
6f9ac43
1 Parent(s): 1e2685f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +352 -0
app.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Video Keyword Finder with Gradio Interface
3
+ ========================================
4
+
5
+ A Python script that finds and timestamps specific keywords within video files.
6
+ It transcribes the audio using Whisper AI and finds all occurrences of specified keywords
7
+ with their timestamps and surrounding context. Supports both local video files and YouTube URLs.
8
+
9
+ Requirements
10
+ -----------
11
+ - Python 3.8 or higher
12
+ - ffmpeg (must be installed and accessible in system PATH)
13
+ - GPU recommended but not required
14
+ """
15
+
16
+ import whisper_timestamped as whisper
17
+ import datetime
18
+ import os
19
+ import tempfile
20
+ import yt_dlp
21
+ import re
22
+ import logging
23
+ import math
24
+ import subprocess
25
+ import glob
26
+ import gradio as gr
27
+
28
+ # Set up logging
29
+ logging.basicConfig(
30
+ level=logging.INFO,
31
+ format='%(asctime)s - %(levelname)s - %(message)s',
32
+ handlers=[
33
+ logging.StreamHandler()
34
+ ]
35
+ )
36
+
37
+ def parse_time(time_str):
38
+ """Convert time string (HH:MM:SS) to seconds"""
39
+ if not time_str or time_str.strip() == "":
40
+ return None
41
+ try:
42
+ time_parts = list(map(int, time_str.split(':')))
43
+ if len(time_parts) == 3: # HH:MM:SS
44
+ return time_parts[0] * 3600 + time_parts[1] * 60 + time_parts[2]
45
+ elif len(time_parts) == 2: # MM:SS
46
+ return time_parts[0] * 60 + time_parts[1]
47
+ else:
48
+ return int(time_str) # Just seconds
49
+ except:
50
+ raise ValueError("Time must be in HH:MM:SS, MM:SS, or seconds format")
51
+
52
+ def format_time(seconds):
53
+ """Convert seconds to HH:MM:SS format"""
54
+ return str(datetime.timedelta(seconds=int(seconds)))
55
+
56
+ def is_youtube_url(url):
57
+ """Check if the provided string is a YouTube URL"""
58
+ if not url or url.strip() == "":
59
+ return False
60
+ youtube_regex = (
61
+ r'(https?://)?(www\.)?'
62
+ r'(youtube|youtu|youtube-nocookie)\.(com|be)/'
63
+ r'(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})'
64
+ )
65
+ return bool(re.match(youtube_regex, url))
66
+
67
+ def download_youtube_video(url):
68
+ """Download YouTube video and return path to temporary file"""
69
+ temp_dir = tempfile.gettempdir()
70
+ temp_file = os.path.join(temp_dir, 'youtube_video.mp4')
71
+
72
+ ydl_opts = {
73
+ 'format': 'best[ext=mp4]',
74
+ 'outtmpl': temp_file,
75
+ 'quiet': False,
76
+ 'progress_hooks': [lambda d: logging.info(f"Download progress: {d.get('status', 'unknown')}")],
77
+ 'socket_timeout': 30,
78
+ }
79
+
80
+ try:
81
+ logging.info(f"Starting download of YouTube video: {url}")
82
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
83
+ info = ydl.extract_info(url, download=True)
84
+ logging.info(f"Video info extracted: {info.get('title', 'Unknown title')}")
85
+
86
+ if os.path.exists(temp_file):
87
+ file_size = os.path.getsize(temp_file)
88
+ logging.info(f"Download complete. File size: {file_size / (1024*1024):.2f} MB")
89
+ return temp_file
90
+ else:
91
+ raise Exception("Download completed but file not found")
92
+ except Exception as e:
93
+ logging.error(f"Error downloading YouTube video: {str(e)}")
94
+ raise
95
+
96
+ def get_video_duration(video_path):
97
+ """Get video duration using ffprobe"""
98
+ cmd = [
99
+ 'ffprobe',
100
+ '-v', 'error',
101
+ '-show_entries', 'format=duration',
102
+ '-of', 'default=noprint_wrappers=1:nokey=1',
103
+ video_path
104
+ ]
105
+ try:
106
+ output = subprocess.check_output(cmd).decode().strip()
107
+ return float(output)
108
+ except subprocess.CalledProcessError as e:
109
+ logging.error(f"Error getting video duration: {str(e)}")
110
+ raise
111
+
112
+ def split_video(video_path, segment_duration=120):
113
+ """Split video into segments using ffmpeg"""
114
+ try:
115
+ temp_dir = tempfile.gettempdir()
116
+ segment_pattern = os.path.join(temp_dir, 'segment_%03d.mp4')
117
+
118
+ # Remove any existing segments
119
+ for old_segment in glob.glob(os.path.join(temp_dir, 'segment_*.mp4')):
120
+ try:
121
+ os.remove(old_segment)
122
+ except:
123
+ pass
124
+
125
+ # Split video into segments
126
+ cmd = [
127
+ 'ffmpeg',
128
+ '-i', video_path,
129
+ '-f', 'segment',
130
+ '-segment_time', str(segment_duration),
131
+ '-c', 'copy',
132
+ '-reset_timestamps', '1',
133
+ segment_pattern
134
+ ]
135
+
136
+ logging.info("Splitting video into segments...")
137
+ subprocess.run(cmd, check=True, capture_output=True)
138
+
139
+ # Get list of generated segments
140
+ segments = sorted(glob.glob(os.path.join(temp_dir, 'segment_*.mp4')))
141
+ logging.info(f"Created {len(segments)} segments")
142
+
143
+ return segments
144
+
145
+ except Exception as e:
146
+ logging.error(f"Error splitting video: {str(e)}")
147
+ raise
148
+
149
+ def process_segments(segments, keywords):
150
+ """Process each segment sequentially"""
151
+ results = {keyword: [] for keyword in keywords}
152
+
153
+ # Load whisper model once
154
+ logging.info("Loading Whisper model...")
155
+ model = whisper.load_model("base")
156
+
157
+ for i, segment_path in enumerate(segments):
158
+ try:
159
+ segment_num = int(re.search(r'segment_(\d+)', segment_path).group(1))
160
+ start_time = segment_num * 120 # Each segment is 120 seconds
161
+
162
+ logging.info(f"Processing segment {i+1}/{len(segments)} (starting at {format_time(start_time)})")
163
+
164
+ # Transcribe segment
165
+ audio = whisper.load_audio(segment_path)
166
+ transcription = whisper.transcribe(model, audio)
167
+
168
+ # Process results
169
+ for segment in transcription['segments']:
170
+ text = segment['text'].lower()
171
+ timestamp = segment['start'] + start_time # Adjust timestamp relative to full video
172
+
173
+ for keyword in keywords:
174
+ if keyword.lower() in text:
175
+ results[keyword].append((
176
+ timestamp,
177
+ segment['text']
178
+ ))
179
+ logging.info(f"Found keyword '{keyword}' at {format_time(timestamp)}: {segment['text']}")
180
+
181
+ except Exception as e:
182
+ logging.error(f"Error processing segment {segment_path}: {str(e)}")
183
+ continue
184
+
185
+ finally:
186
+ # Clean up segment file
187
+ try:
188
+ os.remove(segment_path)
189
+ except:
190
+ pass
191
+
192
+ return results
193
+
194
+ def find_keywords_in_video(video_path, keywords, begin_time=None, end_time=None):
195
+ """Find timestamps for keywords in video transcription"""
196
+ try:
197
+ logging.info(f"Processing video: {video_path}")
198
+ logging.info(f"Searching for keywords: {keywords}")
199
+
200
+ # Convert keywords string to list and clean up
201
+ if isinstance(keywords, str):
202
+ keywords = [k.strip() for k in keywords.split(',')]
203
+
204
+ # Get video duration
205
+ duration = get_video_duration(video_path)
206
+ logging.info(f"Video duration: {duration:.2f} seconds")
207
+
208
+ # Set time bounds
209
+ start = parse_time(begin_time) if begin_time else 0
210
+ end = min(parse_time(end_time) if end_time else duration, duration)
211
+
212
+ if start is not None and end is not None and start >= end:
213
+ raise ValueError("End time must be greater than start time")
214
+
215
+ # Split video into segments
216
+ segment_duration = 120 # 2 minutes per segment
217
+ segments = split_video(video_path, segment_duration)
218
+
219
+ # Process segments sequentially
220
+ results = process_segments(segments, keywords)
221
+
222
+ return results
223
+
224
+ except Exception as e:
225
+ logging.error(f"Error processing video: {str(e)}")
226
+ raise
227
+
228
+ def process_video(video_file, youtube_url, keywords_input, start_time="0:00", end_time=None):
229
+ """
230
+ Process video file or YouTube URL and find keywords
231
+ """
232
+ try:
233
+ # Convert keywords string to list
234
+ keywords = [k.strip() for k in keywords_input.split(',') if k.strip()]
235
+ if not keywords:
236
+ return "Please enter at least one keyword (separated by commas)"
237
+
238
+ # Handle input source
239
+ if youtube_url and youtube_url.strip():
240
+ if not is_youtube_url(youtube_url):
241
+ return "Invalid YouTube URL. Please provide a valid URL."
242
+ video_path = download_youtube_video(youtube_url)
243
+ cleanup_needed = True
244
+ elif video_file is not None:
245
+ video_path = video_file
246
+ cleanup_needed = False
247
+ else:
248
+ return "Please provide either a video file or a YouTube URL"
249
+
250
+ try:
251
+ # Find keywords
252
+ results = find_keywords_in_video(
253
+ video_path=video_path,
254
+ keywords=keywords,
255
+ begin_time=start_time if start_time else None,
256
+ end_time=end_time if end_time else None
257
+ )
258
+
259
+ # Format results
260
+ output = []
261
+ total_matches = sum(len(matches) for matches in results.values())
262
+
263
+ output.append(f"Total matches found: {total_matches}\n")
264
+
265
+ if total_matches == 0:
266
+ output.append("No matches found for any keywords.")
267
+ else:
268
+ for keyword, matches in results.items():
269
+ if matches:
270
+ output.append(f"\nResults for '{keyword}':")
271
+ for timestamp, context in matches:
272
+ output.append(f"[{format_time(timestamp)}] {context}")
273
+ else:
274
+ output.append(f"\nNo occurrences found for '{keyword}'")
275
+
276
+ return "\n".join(output)
277
+
278
+ finally:
279
+ # Cleanup temporary files
280
+ if cleanup_needed and os.path.exists(video_path):
281
+ try:
282
+ os.remove(video_path)
283
+ except:
284
+ pass
285
+
286
+ except Exception as e:
287
+ logging.error(f"Error processing video: {str(e)}")
288
+ return f"Error processing video: {str(e)}"
289
+
290
+ # Create Gradio interface
291
+ with gr.Blocks(title="Video Keyword Finder", theme=gr.themes.Soft()) as demo:
292
+ gr.Markdown("""
293
+ # 🎥 Video Keyword Finder
294
+ Find timestamps for specific keywords in your videos using AI transcription.
295
+
296
+ Upload a video file or provide a YouTube URL, then enter the keywords you want to find.
297
+ """)
298
+
299
+ with gr.Row():
300
+ with gr.Column():
301
+ video_input = gr.File(
302
+ label="Upload Video File",
303
+ file_types=["video"],
304
+ type="filepath"
305
+ )
306
+ youtube_url = gr.Textbox(
307
+ label="Or Enter YouTube URL",
308
+ placeholder="https://www.youtube.com/watch?v=..."
309
+ )
310
+ keywords = gr.Textbox(
311
+ label="Keywords (comma-separated)",
312
+ placeholder="enter, your, keywords, here"
313
+ )
314
+
315
+ with gr.Row():
316
+ start_time = gr.Textbox(
317
+ label="Start Time (HH:MM:SS)",
318
+ placeholder="0:00",
319
+ value="0:00"
320
+ )
321
+ end_time = gr.Textbox(
322
+ label="End Time (HH:MM:SS)",
323
+ placeholder="Optional"
324
+ )
325
+
326
+ submit_btn = gr.Button("Find Keywords", variant="primary")
327
+
328
+ with gr.Column():
329
+ output = gr.Textbox(
330
+ label="Results",
331
+ placeholder="Keywords and timestamps will appear here...",
332
+ lines=20
333
+ )
334
+
335
+ gr.Markdown("""
336
+ ### Instructions:
337
+ 1. Upload a video file or paste a YouTube URL
338
+ 2. Enter keywords separated by commas (e.g., "hello, world, python")
339
+ 3. Optionally set start and end times (format: HH:MM:SS)
340
+ 4. Click "Find Keywords" and wait for results
341
+
342
+ Note: Processing time depends on video length. A 1-hour video typically takes 15-30 minutes.
343
+ """)
344
+
345
+ submit_btn.click(
346
+ fn=process_video,
347
+ inputs=[video_input, youtube_url, keywords, start_time, end_time],
348
+ outputs=output
349
+ )
350
+
351
+ if __name__ == "__main__":
352
+ demo.launch()