developer28 commited on
Commit
eb0c1c2
Β·
verified Β·
1 Parent(s): 3889bc6

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +999 -0
app.py ADDED
@@ -0,0 +1,999 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def format_scene_breakdown(scenes):
2
+ rows = """
3
+ <table style='width:100%; border-collapse: collapse; background-color:#1a1a1a; color: #FFFFFF; border: 2px solid #FF8C00; font-size: 14px;box-shadow: 0 4px 8px rgba(0,0,0,0.3);'>
4
+ <tr style='background-color:#FF8C00; color: #000000;'>
5
+ <th style='padding: 8px; border: 1px solid #FF8C00;color: #000000;'>Timestamp</th>
6
+ <th style='padding: 8px; border: 1px solid #FF8C00;color: #000000;'> Description</th>
7
+ </tr>
8
+ """
9
+ pattern = re.compile(r"\*\*\[(.*?)\]\*\*:\s*(.*)")
10
+
11
+
12
+ for scene in scenes:
13
+ match = pattern.match(scene)
14
+ if match:
15
+ timestamp = match.group(1).strip()
16
+ description = match.group(2).strip()
17
+ rows += f"""
18
+ <tr style='background-color:#1a1a1a;'>
19
+ <td style='padding: 8px; border: 1px solid #444; color: #87CEEB; font-weight: bold;font-size: 12px;vertical-align: top;'>{timestamp}</td>
20
+ <td style='padding: 8px; border: 1px solid #444; color: #87CEEB; font-weight: bold;font-size: 12px;line-height: 1.4;'>{description}</td>
21
+ </tr>
22
+ """
23
+
24
+ rows += "</table>"
25
+ return rows
26
+
27
+
28
+ import gradio as gr
29
+ import yt_dlp
30
+ import os
31
+ import tempfile
32
+ import shutil
33
+ from pathlib import Path
34
+ import re
35
+ import uuid
36
+ import json
37
+ from datetime import datetime
38
+ import google.generativeai as genai
39
+ from xhtml2pdf import pisa
40
+ from io import BytesIO
41
+
42
+
43
+ cached_reports = {}
44
+
45
+ def generate_pdf_from_html(html_content):
46
+ """Generate a compact PDF that matches app appearance"""
47
+ try:
48
+ pdf_html = f"""
49
+ <!DOCTYPE html>
50
+ <html>
51
+ <head>
52
+ <meta charset='UTF-8'>
53
+ <style>
54
+ @page {{
55
+ size: A4;
56
+ margin: 0.7cm;
57
+ }}
58
+ body {{
59
+ font-family: 'Segoe UI', sans-serif;
60
+ font-size: 9px;
61
+ line-height: 1.3;
62
+ color: #000;
63
+ background-color: #fff;
64
+ }}
65
+ table {{
66
+ width: 100%;
67
+ border-collapse: collapse;
68
+ font-size: 9px;
69
+ margin-bottom: 10px;
70
+ }}
71
+ th, td {{
72
+ border: 1px solid #ccc;
73
+ padding: 2px;
74
+ vertical-align: top;
75
+ line-height: 1.1;
76
+ }}
77
+ h1, h2, h3 {{
78
+ font-size: 11px;
79
+ background-color: #D3D3D3;
80
+ color: #000;
81
+ padding: 4px;
82
+ margin: 4px 0;
83
+ }}
84
+ </style>
85
+ </head>
86
+ <body>
87
+ {html_content}
88
+ </body>
89
+ </html>
90
+ """
91
+
92
+ result = BytesIO()
93
+ pisa_status = pisa.CreatePDF(pdf_html, dest=result)
94
+ if pisa_status.err:
95
+ print("❌ Pisa PDF creation error")
96
+ return None
97
+ result.seek(0)
98
+ return result
99
+
100
+ except Exception as e:
101
+ print(f"❌ PDF generation exception: {e}")
102
+ return None
103
+
104
+ def generate_pdf_from_html_debug(html_content):
105
+ """Debug version to identify PDF generation issues"""
106
+ try:
107
+ print("πŸ› Starting PDF generation...")
108
+ print(f"πŸ› HTML content length: {len(html_content)} characters")
109
+ print(f"πŸ› HTML preview: {html_content[:200]}...")
110
+
111
+ # Check if we have the required imports
112
+ try:
113
+ from xhtml2pdf import pisa
114
+ print("βœ… pisa import successful")
115
+ except ImportError as e:
116
+ print(f"❌ pisa import failed: {e}")
117
+ return None
118
+
119
+ try:
120
+ import re
121
+ print("βœ… re import successful")
122
+ except ImportError as e:
123
+ print(f"❌ re import failed: {e}")
124
+ return None
125
+
126
+ # Simple HTML cleanup (minimal for debugging)
127
+ simplified_html = html_content.replace(
128
+ "background: linear-gradient(135deg, #2d3748, #1a202c);",
129
+ "background-color: #ffffff;"
130
+ ).replace(
131
+ "color: #FFFFFF;",
132
+ "color: #000000;"
133
+ )
134
+
135
+ print("πŸ› HTML cleanup completed")
136
+
137
+ # Create a very simple PDF HTML
138
+ pdf_html = f"""
139
+ <!DOCTYPE html>
140
+ <html>
141
+ <head>
142
+ <meta charset="UTF-8">
143
+ <style>
144
+ body {{
145
+ font-family: Arial, sans-serif;
146
+ font-size: 12px;
147
+ color: #000000;
148
+ background-color: #ffffff;
149
+ }}
150
+ table {{ border-collapse: collapse; width: 100%; }}
151
+ th, td {{ border: 1px solid #ccc; padding: 4px; }}
152
+ th {{ background-color: #FF8C00; color: #000000; }}
153
+
154
+ </style>
155
+ </head>
156
+ <body>
157
+ {simplified_html}
158
+ </body>
159
+ </html>
160
+ """
161
+
162
+ print("πŸ› PDF HTML template created")
163
+
164
+ from io import BytesIO
165
+ result = BytesIO()
166
+
167
+ print("πŸ› Creating PDF with pisa...")
168
+ pisa_status = pisa.CreatePDF(pdf_html, dest=result)
169
+
170
+ print(f"πŸ› Pisa status - err: {pisa_status.err}")
171
+ print(f"πŸ› PDF buffer size: {len(result.getvalue())} bytes")
172
+
173
+ if pisa_status.err:
174
+ print(f"❌ PDF generation error: {pisa_status.err}")
175
+ return None
176
+
177
+ if len(result.getvalue()) == 0:
178
+ print("❌ PDF buffer is empty")
179
+ return None
180
+
181
+ result.seek(0)
182
+ print("βœ… PDF generation successful!")
183
+ return result
184
+
185
+ except Exception as e:
186
+ print(f"❌ PDF generation exception: {e}")
187
+ import traceback
188
+ traceback.print_exc()
189
+ return None
190
+
191
+ class YouTubeDownloader:
192
+ def __init__(self):
193
+ self.download_dir = tempfile.mkdtemp()
194
+ # Use temp directory for Gradio compatibility
195
+ self.temp_downloads = tempfile.mkdtemp(prefix="youtube_downloads_")
196
+ # Also create user downloads folder for copying
197
+ self.downloads_folder = os.path.join(os.path.expanduser("~"), "Downloads", "YouTube_Downloads")
198
+ os.makedirs(self.downloads_folder, exist_ok=True)
199
+ self.gemini_model = None
200
+
201
+ def configure_gemini(self, api_key):
202
+ """Configure Gemini API with the provided key"""
203
+ try:
204
+ genai.configure(api_key=api_key)
205
+ self.gemini_model = genai.GenerativeModel(model_name="gemini-1.5-flash-latest")
206
+ return True, "βœ… Gemini API configured successfully!"
207
+ except Exception as e:
208
+ return False, f"❌ Failed to configure Gemini API: {str(e)}"
209
+
210
+ def cleanup(self):
211
+ """Clean up temporary directories and files"""
212
+ try:
213
+ if hasattr(self, 'download_dir') and os.path.exists(self.download_dir):
214
+ shutil.rmtree(self.download_dir)
215
+ print(f"βœ… Cleaned up temporary directory: {self.download_dir}")
216
+ if hasattr(self, 'temp_downloads') and os.path.exists(self.temp_downloads):
217
+ shutil.rmtree(self.temp_downloads)
218
+ print(f"βœ… Cleaned up temp downloads directory: {self.temp_downloads}")
219
+ except Exception as e:
220
+ print(f"⚠️ Warning: Could not clean up temporary directory: {e}")
221
+
222
+ def is_valid_youtube_url(self, url):
223
+ youtube_regex = re.compile(
224
+ r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/'
225
+ r'(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})'
226
+ )
227
+ return youtube_regex.match(url) is not None
228
+
229
+ def generate_scene_breakdown_gemini(self, video_info):
230
+ """Generate AI-powered scene breakdown using Gemini"""
231
+ if not self.gemini_model:
232
+ return self.generate_scene_breakdown_fallback(video_info)
233
+
234
+ try:
235
+ duration = video_info.get('duration', 0)
236
+ title = video_info.get('title', '')
237
+ description = video_info.get('description', '')[:1500] # Increased limit for better context
238
+
239
+ if not duration:
240
+ return ["**[Duration Unknown]**: Unable to generate timestamped breakdown - video duration not available"]
241
+
242
+ # Create enhanced prompt for Gemini
243
+ prompt = f"""
244
+ Analyze this YouTube video and create a highly detailed, scene-by-scene breakdown with precise timestamps and specific descriptions:
245
+
246
+ Title: {title}
247
+ Duration: {duration} seconds
248
+ Description: {description}
249
+
250
+ IMPORTANT INSTRUCTIONS:
251
+ 1. Create detailed scene descriptions that include:
252
+ - Physical appearance of people (age, gender, clothing, hair, etc.)
253
+ - Exact actions being performed
254
+ - Dialogue or speech (include actual lines if audible, or infer probable spoken lines based on actions and setting; format them as "Character: line...")
255
+ - Setting and environment details
256
+ - Props, objects, or products being shown
257
+ - Visual effects, text overlays, or graphics
258
+ - Mood, tone, and atmosphere
259
+ - Camera movements or angles (if apparent)
260
+ 2. Dialogue Emphasis:
261
+ - Include short dialogue lines in **every scene** wherever plausible.
262
+ - Write lines like: Character: "Actual or inferred line..."
263
+ - If dialogue is not available, intelligently infer probable phrases (e.g., "Welcome!", "Try this now!", "It feels amazing!").
264
+ - Do NOT skip dialogue unless it's clearly impossible.
265
+
266
+ 3. Timestamp Guidelines:
267
+ - For videos under 1 minute: 2-3 second segments
268
+ - For videos 1-5 minutes: 3-5 second segments
269
+ - For videos 5-15 minutes: 5-10 second segments
270
+ - For videos over 15 minutes: 10-15 second segments
271
+ - Maximum 20 scenes total for longer videos
272
+
273
+ 4. Format each scene EXACTLY like this:
274
+ **[MM:SS-MM:SS]**: Detailed description including who is visible, what they're wearing, what they're doing, what they're saying (if applicable), setting details, objects shown, and any visual elements.
275
+
276
+
277
+ 5. Write descriptions as if you're watching the video in real-time, noting everything visible and audible.
278
+
279
+ Based on the title and description, intelligently infer what would likely happen in each time segment. Consider the video type and create contextually appropriate, detailed descriptions.
280
+ """
281
+
282
+ response = self.gemini_model.generate_content(prompt)
283
+
284
+ # Parse the response into individual scenes
285
+ if response and response.text:
286
+ scenes = []
287
+ lines = response.text.split('\n')
288
+ current_scene = ""
289
+
290
+ for line in lines:
291
+ line = line.strip()
292
+ if line.strip().startswith("**[") and "]**:" in line:
293
+ # This is a new scene timestamp line
294
+ if current_scene:
295
+ scenes.append(current_scene.strip())
296
+ current_scene = line.strip()
297
+ elif current_scene:
298
+ # This is continuation of the current scene description
299
+ current_scene += "\n" + line.strip()
300
+
301
+ # Add the last scene if exists
302
+ if current_scene:
303
+ scenes.append(current_scene.strip())
304
+
305
+ return scenes if scenes else self.generate_scene_breakdown_fallback(video_info)
306
+ else:
307
+ return self.generate_scene_breakdown_fallback(video_info)
308
+
309
+ except Exception as e:
310
+ print(f"Gemini API error: {e}")
311
+ return self.generate_scene_breakdown_fallback(video_info)
312
+
313
+ def generate_scene_breakdown_fallback(self, video_info):
314
+ """Enhanced fallback scene generation when Gemini is not available"""
315
+ duration = video_info.get('duration', 0)
316
+ title = video_info.get('title', '').lower()
317
+ description = video_info.get('description', '').lower()
318
+ uploader = video_info.get('uploader', 'Content creator')
319
+
320
+ if not duration:
321
+ return ["**[Duration Unknown]**: Unable to generate timestamped breakdown"]
322
+
323
+ # Determine segment length based on duration
324
+ if duration <= 60:
325
+ segment_length = 3
326
+ elif duration <= 300:
327
+ segment_length = 5
328
+ elif duration <= 900:
329
+ segment_length = 10
330
+ else:
331
+ segment_length = 15
332
+
333
+ scenes = []
334
+ num_segments = min(duration // segment_length + 1, 20)
335
+
336
+ # Detect video type for better descriptions
337
+ video_type = self.detect_video_type_detailed(title, description)
338
+
339
+ for i in range(num_segments):
340
+ start_time = i * segment_length
341
+ end_time = min(start_time + segment_length - 1, duration)
342
+
343
+ start_formatted = f"{start_time//60}:{start_time%60:02d}"
344
+ end_formatted = f"{end_time//60}:{end_time%60:02d}"
345
+
346
+ # Generate contextual descriptions based on video type and timing
347
+ desc = self.generate_contextual_description(i, num_segments, video_type, uploader, title)
348
+
349
+ scenes.append(f"**[{start_formatted}-{end_formatted}]**: {desc}")
350
+
351
+ return scenes
352
+
353
+ def detect_video_type_detailed(self, title, description):
354
+ """Detect video type with more detail for better fallback descriptions"""
355
+ text = (title + " " + description).lower()
356
+
357
+ if any(word in text for word in ['tutorial', 'how to', 'guide', 'learn', 'diy', 'step by step']):
358
+ return 'tutorial'
359
+ elif any(word in text for word in ['review', 'unboxing', 'test', 'comparison', 'vs']):
360
+ return 'review'
361
+ elif any(word in text for word in ['vlog', 'daily', 'routine', 'day in', 'morning', 'skincare']):
362
+ return 'vlog'
363
+ elif any(word in text for word in ['music', 'song', 'cover', 'lyrics', 'dance']):
364
+ return 'music'
365
+ elif any(word in text for word in ['comedy', 'funny', 'prank', 'challenge', 'reaction']):
366
+ return 'entertainment'
367
+ elif any(word in text for word in ['news', 'breaking', 'update', 'report']):
368
+ return 'news'
369
+ elif any(word in text for word in ['cooking', 'recipe', 'food', 'kitchen']):
370
+ return 'cooking'
371
+ elif any(word in text for word in ['workout', 'fitness', 'exercise', 'yoga']):
372
+ return 'fitness'
373
+ else:
374
+ return 'general'
375
+
376
+ def generate_contextual_description(self, scene_index, total_scenes, video_type, uploader, title):
377
+ """Generate contextual descriptions based on video type and scene position"""
378
+
379
+ # Common elements
380
+ presenter_desc = f"The content creator"
381
+ if 'woman' in title.lower() or 'girl' in title.lower():
382
+ presenter_desc = "A woman"
383
+ elif 'man' in title.lower() or 'guy' in title.lower():
384
+ presenter_desc = "A man"
385
+
386
+ # Position-based descriptions
387
+ if scene_index == 0:
388
+ # Opening scene
389
+ if video_type == 'tutorial':
390
+ return f"{presenter_desc} appears on screen, likely introducing themselves and the topic. They may be in a well-lit indoor setting, wearing casual clothing, and addressing the camera directly with a welcoming gesture."
391
+ elif video_type == 'vlog':
392
+ return f"{presenter_desc} greets the camera with a smile, possibly waving. They appear to be in their usual filming location, wearing their typical style, and beginning their introduction to today's content."
393
+ elif video_type == 'review':
394
+ return f"{presenter_desc} introduces the product or topic they'll be reviewing, likely holding or displaying the item. The setting appears organized, possibly with the product prominently featured."
395
+ else:
396
+ return f"{presenter_desc} appears on screen to begin the video, introducing the topic with engaging body language and clear speech directed at the audience."
397
+
398
+ elif scene_index == total_scenes - 1:
399
+ # Closing scene
400
+ if video_type == 'tutorial':
401
+ return f"{presenter_desc} concludes the tutorial, possibly showing the final result. They may be thanking viewers, asking for engagement (likes/comments), and suggesting related content."
402
+ elif video_type == 'vlog':
403
+ return f"{presenter_desc} wraps up their vlog, possibly reflecting on the day's events. They appear relaxed and are likely saying goodbye to viewers with a friendly gesture."
404
+ else:
405
+ return f"{presenter_desc} concludes the video with final thoughts, thanking viewers for watching, and encouraging engagement through likes, comments, and subscriptions."
406
+
407
+ else:
408
+ # Middle scenes - content-specific
409
+ if video_type == 'tutorial':
410
+ step_num = scene_index
411
+ return f"{presenter_desc} demonstrates step {step_num} of the process, showing specific techniques and explaining the procedure. They may be using tools or materials, with close-up shots of their hands working."
412
+
413
+ elif video_type == 'review':
414
+ return f"{presenter_desc} examines different aspects of the product, pointing out features and sharing their opinions. They may be holding, using, or demonstrating the item while speaking to the camera."
415
+
416
+ elif video_type == 'vlog':
417
+ return f"{presenter_desc} continues sharing their experience, possibly showing different locations or activities. The scene captures candid moments with natural lighting and casual interactions."
418
+
419
+ elif video_type == 'cooking':
420
+ return f"{presenter_desc} works in the kitchen, preparing ingredients or cooking. They demonstrate techniques while explaining each step, with kitchen tools and ingredients visible on the counter."
421
+
422
+ elif video_type == 'fitness':
423
+ return f"{presenter_desc} demonstrates exercise movements, likely in workout attire in a gym or home setting. They show proper form while providing instruction and motivation."
424
+
425
+ else:
426
+ return f"{presenter_desc} continues with the main content, engaging with the audience through clear explanations and demonstrations. The setting remains consistent with good lighting and clear audio."
427
+
428
+ def detect_video_type(self, title, description):
429
+ """Detect video type based on title and description"""
430
+ text = (title + " " + description).lower()
431
+
432
+ if any(word in text for word in ['music', 'song', 'album', 'artist', 'band', 'lyrics']):
433
+ return "Music Video"
434
+ elif any(word in text for word in ['tutorial', 'how to', 'guide', 'learn', 'teaching']):
435
+ return "Tutorial/Educational"
436
+ elif any(word in text for word in ['funny', 'comedy', 'entertainment', 'vlog', 'challenge']):
437
+ return "Entertainment/Comedy"
438
+ elif any(word in text for word in ['news', 'breaking', 'report', 'update']):
439
+ return "News/Information"
440
+ elif any(word in text for word in ['review', 'unboxing', 'test', 'comparison']):
441
+ return "Review/Unboxing/Promotional"
442
+ elif any(word in text for word in ['commercial', 'ad', 'brand', 'product']):
443
+ return "Commercial/Advertisement"
444
+ else:
445
+ return "General Content"
446
+
447
+ def detect_background_music(self, video_info):
448
+ """Detect background music style"""
449
+ title = video_info.get('title', '').lower()
450
+ description = video_info.get('description', '').lower()
451
+
452
+ if any(word in title for word in ['music', 'song', 'soundtrack']):
453
+ return "Original Music/Soundtrack - Primary audio content"
454
+ elif any(word in title for word in ['commercial', 'ad', 'brand']):
455
+ return "Upbeat Commercial Music - Designed to enhance brand appeal"
456
+ elif any(word in title for word in ['tutorial', 'how to', 'guide']):
457
+ return "Minimal/No Background Music - Focus on instruction"
458
+ elif any(word in title for word in ['vlog', 'daily', 'life']):
459
+ return "Ambient Background Music - Complementary to narration"
460
+ else:
461
+ return "Background Music - Complementing video mood and pacing"
462
+
463
+ def detect_influencer_status(self, video_info):
464
+ """Detect influencer status"""
465
+ subscriber_count = video_info.get('channel_followers', 0)
466
+ view_count = video_info.get('view_count', 0)
467
+
468
+ if subscriber_count > 10000000:
469
+ return "Mega Influencer (10M+ subscribers)"
470
+ elif subscriber_count > 1000000:
471
+ return "Major Influencer (1M+ subscribers)"
472
+ elif subscriber_count > 100000:
473
+ return "Mid-tier Influencer (100K+ subscribers)"
474
+ elif subscriber_count > 10000:
475
+ return "Micro Influencer (10K+ subscribers)"
476
+ elif view_count > 100000:
477
+ return "Viral Content Creator"
478
+ else:
479
+ return "Regular Content Creator"
480
+
481
+ def format_number(self, num):
482
+ if num is None or num == 0:
483
+ return "0"
484
+ if num >= 1_000_000_000:
485
+ return f"{num/1_000_000_000:.1f}B"
486
+ elif num >= 1_000_000:
487
+ return f"{num/1_000_000:.1f}M"
488
+ elif num >= 1_000:
489
+ return f"{num/1_000:.1f}K"
490
+ return str(num)
491
+
492
+ def format_video_info(self, video_info):
493
+ """Compact video information formatting with tabular layout"""
494
+ if not video_info:
495
+ return "❌ No video information available."
496
+
497
+ # Basic information
498
+ title = video_info.get("title", "Unknown")
499
+ uploader = video_info.get("uploader", "Unknown")
500
+ duration = video_info.get("duration", 0)
501
+ duration_str = f"{duration//60}:{duration%60:02d}" if duration else "Unknown"
502
+ view_count = video_info.get("view_count", 0)
503
+ like_count = video_info.get("like_count", 0)
504
+ comment_count = video_info.get("comment_count", 0)
505
+ upload_date = video_info.get("upload_date", "Unknown")
506
+
507
+ # Format upload date
508
+ if len(upload_date) == 8:
509
+ formatted_date = f"{upload_date[:4]}-{upload_date[4:6]}-{upload_date[6:8]}"
510
+ else:
511
+ formatted_date = upload_date
512
+
513
+ # Generate enhanced analysis
514
+ scene_descriptions = self.generate_scene_breakdown_gemini(video_info)
515
+ scene_table_html = format_scene_breakdown(scene_descriptions)
516
+ video_type = self.detect_video_type(title, video_info.get('description', ''))
517
+ background_music = self.detect_background_music(video_info)
518
+ influencer_status = self.detect_influencer_status(video_info)
519
+
520
+ # Calculate engagement metrics
521
+ engagement_rate = (like_count / view_count) * 100 if view_count > 0 else 0
522
+
523
+ # Generate compact report with contrasting background
524
+ report = f"""
525
+ <div style='font-family: "Roboto", "Segoe UI", "Open Sans", sans-serif; background-color: rgb(250, 250, 250); padding: 12px; border-radius: 6px; border: 1px solid #ddd;'>
526
+ <!-- TITLE -->
527
+ <div style='text-align: center; margin-bottom: 8px;'>
528
+ <h1 style='font-size: 16px; color: #FF6F00; margin-bottom: 3px; line-height: 1.2;'>{title}</h1>
529
+ <div style='height: 2px; background-color:rgb(211, 211, 211); width: 80px; margin: 0 auto;'></div>
530
+ </div>
531
+ <!-- INFO GRID (2 Columns) -->
532
+ <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;'>
533
+ <!-- LEFT: BASIC INFO -->
534
+ <div style='background-color:rgb(211, 211, 211); border: 1px solid #CCC; border-left: 3px solid #FF6F00; border-radius: 4px; padding: 6px; max-width: 100%;'>
535
+ <h3 style='margin: 0 0 4px 0; font-size: 14px; background-color:#D3D3D3; color: #000000 !important; line-height: 1.2;'>Basic Information</h3>
536
+ <table style='width: 100%; font-size: 11px; color: #212121; border-spacing: 0;'>
537
+ <tr><td style='padding: 0px 1px;'><strong>Creator</strong></td><td style='padding: 1px 2px;'>{uploader[:20]}{'...' if len(uploader) > 20 else ''}</td></tr>
538
+ <tr><td style='padding: 0px 1px;'><strong>Date</strong></td><td style='padding: 1px 2px;'>{formatted_date}</td></tr>
539
+ <tr><td style='padding: 0px 1px;'><strong>Duration</strong></td><td style='padding: 1px 2px;'>{duration_str}</td></tr>
540
+ </table>
541
+ </div>
542
+ <!-- RIGHT: METRICS + ANALYSIS in a NESTED GRID -->
543
+ <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 6px;'>
544
+ <!-- METRICS -->
545
+ <div style='background-color:rgb(211, 211, 211); border: 1px solid #CCC; border-left: 3px solid #FF6F00; border-radius: 4px; padding: 6px;'>
546
+ <h3 style='margin: 0 0 4px 0; font-size: 14px; color: #000000 !important; line-height: 1.2;'>Metrics</h3>
547
+ <table style='width: 100%; font-size: 11px; color: #212121;'>
548
+ <tr><td style='padding: 0px 1px;'><strong>Views</strong></td><td style='padding: 1px 2px;'>{self.format_number(view_count)}</td></tr>
549
+ <tr><td style='padding: 0px 1px;'><strong>Likes</strong></td><td style='padding: 1px 2px;'>{self.format_number(like_count)}</td></tr>
550
+ <tr><td style='padding: 0px 1px;'><strong>Comments</strong></td><td style='padding: 1px 2px;'>{self.format_number(comment_count)}</td></tr>
551
+ <tr><td style='padding: 0px 1px;'><strong>Engagement</strong></td><td style='padding: 1px 2px;'>{engagement_rate:.2f}%</td></tr>
552
+ </table>
553
+ </div>
554
+ <!-- ANALYSIS -->
555
+ <div style='background-color:rgb(211, 211, 211); border: 1px solid #CCC; border-left: 3px solid #FF6F00; border-radius: 4px; padding: 6px;'>
556
+ <h3 style='margin: 0 0 4px 0; font-size: 14px; color: #000000 !important; line-height: 1.2;'>Analysis</h3>
557
+ <table style='width: 100%; font-size: 11px; color: #212121;'>
558
+ <tr><td style='padding: 0px 1px;'><strong>Type</strong></td><td style='padding: 1px 2px;'>{video_type[:15]}{'...' if len(video_type) > 15 else ''}</td></tr>
559
+ <tr><td style='padding: 0px 1px;'><strong>Music</strong></td><td style='padding: 1px 2px;'>{background_music[:25]}{'...' if len(background_music) > 25 else ''}</td></tr>
560
+ <tr><td style='padding: 0px 1px;'><strong>Status</strong></td><td style='padding: 1px 2px;'>{influencer_status[:25]}{'...' if len(influencer_status) > 25 else ''}</td></tr>
561
+ </table>
562
+ </div>
563
+ </div>
564
+ </div>
565
+ <!-- SCENE-BY-SCENE SECTION (FULL WIDTH) -->
566
+ <div style='background-color:rgb(211, 211, 211); border: 1px solid #CCC; border-radius: 4px; padding: 8px;'>
567
+ <h3 style='text-align: center; font-size: 16px; color: #000000 !important; margin-bottom: 6px; line-height: 1.2;'> Scene-by-Scene Breakdown</h3>
568
+ {scene_table_html}
569
+ </div>
570
+ </div>
571
+ """
572
+ return report.strip()
573
+
574
+ def get_video_info(self, url, progress=gr.Progress(), cookiefile=None):
575
+ """Extract video information"""
576
+ if not url or not url.strip():
577
+ return None, "❌ Please enter a YouTube URL"
578
+
579
+ if not self.is_valid_youtube_url(url):
580
+ return None, "❌ Invalid YouTube URL format"
581
+
582
+ try:
583
+ progress(0.1, desc="Initializing YouTube extractor...")
584
+
585
+ ydl_opts = {
586
+ 'noplaylist': True,
587
+ 'extract_flat': False,
588
+ }
589
+
590
+ if cookiefile and os.path.exists(cookiefile):
591
+ ydl_opts['cookiefile'] = cookiefile
592
+ else:
593
+ print(f"⚠️ Cookie file not provided or not found: {cookiefile}")
594
+
595
+ # πŸ§ͺ Log yt_dlp options
596
+ print("πŸ” yt_dlp options:")
597
+ print(json.dumps(ydl_opts, indent=2))
598
+
599
+ progress(0.5, desc="Extracting video metadata...")
600
+
601
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
602
+ info = ydl.extract_info(url, download=False)
603
+
604
+ progress(1.0, desc="βœ… Analysis complete!")
605
+
606
+ return info, "βœ… Video information extracted successfully"
607
+
608
+ except Exception as e:
609
+ error_msg = str(e)
610
+ print(f"❌ yt_dlp extraction error: {error_msg}")
611
+
612
+ if "Video unavailable" in error_msg or "This content isn’t available" in error_msg:
613
+ return None, (
614
+ "❌ This video is unavailable or restricted. "
615
+ "Please check if it's private, deleted, age-restricted, or try again with a valid cookies.txt file."
616
+ )
617
+ elif "cookies" in error_msg.lower():
618
+ return None, f"❌ Error: {str(e)}"
619
+
620
+
621
+ def download_video(self, url, quality="best", audio_only=False, progress=gr.Progress(), cookiefile=None):
622
+ """Download video with progress tracking"""
623
+ if not url or not url.strip():
624
+ return None, "❌ Please enter a YouTube URL"
625
+
626
+ if not self.is_valid_youtube_url(url):
627
+ return None, "❌ Invalid YouTube URL format"
628
+
629
+ try:
630
+ progress(0.1, desc="Preparing download...")
631
+
632
+ # Create unique filename
633
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
634
+
635
+ # Download to temp directory first (Gradio compatible)
636
+ ydl_opts = {
637
+ 'outtmpl': os.path.join(self.temp_downloads, f'%(title)s_{timestamp}.%(ext)s'),
638
+ 'noplaylist': True,
639
+ }
640
+
641
+ if audio_only:
642
+ ydl_opts['format'] = 'bestaudio/best'
643
+ ydl_opts['postprocessors'] = [{
644
+ 'key': 'FFmpegExtractAudio',
645
+ 'preferredcodec': 'mp3',
646
+ 'preferredquality': '192',
647
+ }]
648
+ else:
649
+ if quality == "best":
650
+ ydl_opts['format'] = 'best[height<=1080]'
651
+ elif quality == "720p":
652
+ ydl_opts['format'] = 'best[height<=720]'
653
+ elif quality == "480p":
654
+ ydl_opts['format'] = 'best[height<=480]'
655
+ else:
656
+ ydl_opts['format'] = 'best'
657
+
658
+ if cookiefile and os.path.exists(cookiefile):
659
+ ydl_opts['cookiefile'] = cookiefile
660
+
661
+ # Progress hook
662
+ def progress_hook(d):
663
+ if d['status'] == 'downloading':
664
+ if 'total_bytes' in d:
665
+ percent = (d['downloaded_bytes'] / d['total_bytes']) * 100
666
+ progress(0.1 + (percent / 100) * 0.7, desc=f"Downloading... {percent:.1f}%")
667
+ else:
668
+ progress(0.5, desc="Downloading...")
669
+ elif d['status'] == 'finished':
670
+ progress(0.8, desc="Processing download...")
671
+
672
+ ydl_opts['progress_hooks'] = [progress_hook]
673
+
674
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
675
+ info = ydl.extract_info(url, download=True)
676
+
677
+ progress(0.9, desc="Copying to Downloads folder...")
678
+
679
+ # Find the downloaded file in temp directory
680
+ downloaded_file_temp = None
681
+
682
+ for file in os.listdir(self.temp_downloads):
683
+ if timestamp in file:
684
+ downloaded_file_temp = os.path.join(self.temp_downloads, file)
685
+ break
686
+
687
+ if not downloaded_file_temp:
688
+ return None, "❌ Downloaded file not found in temp directory"
689
+
690
+ # Copy to user's Downloads folder
691
+ final_filename = os.path.basename(downloaded_file_temp)
692
+ final_path = os.path.join(self.downloads_folder, final_filename)
693
+
694
+ try:
695
+ shutil.copy2(downloaded_file_temp, final_path)
696
+ copy_success = True
697
+ except Exception as e:
698
+ print(f"Warning: Could not copy to Downloads folder: {e}")
699
+ copy_success = False
700
+ final_path = "File downloaded to temp location only"
701
+
702
+ progress(1.0, desc="βœ… Download complete!")
703
+
704
+ success_msg = f"""βœ… Download successful!
705
+ πŸ“ Temp file (for download): {os.path.basename(downloaded_file_temp)}
706
+ πŸ“ Permanent location: {final_path if copy_success else 'Copy failed'}
707
+ 🎯 File size: {os.path.getsize(downloaded_file_temp) / (1024*1024):.1f} MB"""
708
+
709
+ return downloaded_file_temp, success_msg
710
+
711
+ except Exception as e:
712
+ return None, f"❌ Download failed: {str(e)}"
713
+
714
+ # Initialize global downloader
715
+ downloader = YouTubeDownloader()
716
+
717
+ def configure_api_key(api_key):
718
+ """Configure Gemini API key"""
719
+ if not api_key or not api_key.strip():
720
+ return "❌ Please enter a valid Google API key", gr.update(visible=False)
721
+
722
+ success, message = downloader.configure_gemini(api_key.strip())
723
+
724
+ if success:
725
+ return message, gr.update(visible=True)
726
+ else:
727
+ return message, gr.update(visible=False)
728
+
729
+ def analyze_with_cookies(url, cookies_file, progress=gr.Progress()):
730
+ """Main analysis function"""
731
+ try:
732
+ progress(0.05, desc="Starting analysis...")
733
+
734
+ cookiefile = cookies_file if cookies_file and os.path.exists(cookies_file) else None
735
+ info, msg = downloader.get_video_info(url, progress=progress, cookiefile=cookiefile)
736
+
737
+ if info:
738
+ progress(0.95, desc="Generating comprehensive report...")
739
+ formatted_info = downloader.format_video_info(info)
740
+ cached_reports[url] = formatted_info # Cache the result
741
+ progress(1.0, desc="βœ… Complete!")
742
+ return formatted_info
743
+ else:
744
+ return f"❌ Analysis Failed: {msg}"
745
+
746
+ except Exception as e:
747
+ return f"❌ System Error: {str(e)}"
748
+
749
+
750
+ def analyze_and_generate_pdf(url, cookies_file, progress=None):
751
+ """Generate PDF from cached HTML only"""
752
+ try:
753
+ if progress: progress(0.1, desc="Checking cached analysis...")
754
+
755
+ if url not in cached_reports:
756
+ print("❌ No cached report found.")
757
+ return None
758
+
759
+ report_html = cached_reports[url]
760
+ if progress: progress(0.8, desc="Generating PDF...")
761
+
762
+ pdf_buffer = generate_pdf_from_html(report_html)
763
+ if pdf_buffer is None:
764
+ print("❌ PDF buffer is empty.")
765
+ return None
766
+
767
+ pdf_path = os.path.join(tempfile.gettempdir(), f"analysis_report_{uuid.uuid4().hex}.pdf")
768
+ with open(pdf_path, "wb") as f:
769
+ f.write(pdf_buffer.read())
770
+
771
+ if progress: progress(1.0, desc="βœ… PDF ready!")
772
+ return pdf_path
773
+
774
+ except Exception as e:
775
+ print(f"❌ Error during PDF generation: {e}")
776
+ return None
777
+
778
+ def download_with_cookies(url, quality, audio_only, cookies_file, progress=gr.Progress()):
779
+ """Main download function"""
780
+ try:
781
+ progress(0.05, desc="Preparing download...")
782
+
783
+ cookiefile = None
784
+ if cookies_file and os.path.exists(cookies_file):
785
+ cookiefile = cookies_file
786
+
787
+ file_path, msg = downloader.download_video(url, quality, audio_only, progress=progress, cookiefile=cookiefile)
788
+
789
+ if file_path:
790
+ return file_path, msg
791
+ else:
792
+ return None, msg
793
+
794
+ except Exception as e:
795
+ return None, f"❌ System Error: {str(e)}"
796
+
797
+ def create_interface():
798
+ """Create and configure the Gradio interface"""
799
+ with gr.Blocks(
800
+ css="""
801
+ /* Main dark theme background and text */
802
+ .gradio-container, .app, body {
803
+ background-color: #1a1a1a !important;
804
+ color: #87CEEB !important;
805
+ font-weight: bold !important;
806
+ }
807
+ /* πŸ”΅ Dark blue overrides for key labels */
808
+ h3, .gr-group h3, .gradio-container h3 {
809
+ color: #87CEEB !important;
810
+ }
811
+
812
+ label, .gr-textbox label, .gr-file label, .gr-dropdown label, .gr-checkbox label {
813
+ color: #00008B !important;
814
+ font-weight: bold !important;
815
+ }
816
+
817
+ .gr-file .file-name {
818
+ color: #00008B !important;
819
+ font-weight: bold !important;
820
+ }
821
+
822
+ /* Make tab labels dark blue too */
823
+ .gr-tab-nav button {
824
+ color: #00008B !important;
825
+ }
826
+
827
+ .gr-tab-nav button.selected {
828
+ background-color: #FF8C00 !important;
829
+ color: #000000 !important;
830
+ }
831
+
832
+ /* Light blue text for API status */
833
+ .light-blue-text textarea {
834
+ color: #87CEEB !important;
835
+ background-color: #2a2a2a !important;
836
+ }
837
+ .gr-file {
838
+ background-color: #2a2a2a !important;
839
+ border: 2px dashed #444 !important;
840
+ }
841
+
842
+ .gr-group, .gr-form, .gr-row {
843
+ background-color: #1a1a1a !important;
844
+ border: 1px solid #444 !important;
845
+ border-radius: 10px;
846
+ padding: 15px;
847
+ }
848
+
849
+ """,
850
+ theme=gr.themes.Soft(),
851
+ title="πŸ“Š YouTube Video Analyzer & Downloader"
852
+ ) as demo:
853
+
854
+ # API Key Configuration Section
855
+ with gr.Group():
856
+ gr.HTML("<h3>πŸ”‘ Google Gemini API Configuration</h3>")
857
+ with gr.Row():
858
+ api_key_input = gr.Textbox(
859
+ label="πŸ”‘ Google API Key",
860
+ placeholder="Enter your Google API Key for enhanced AI analysis...",
861
+ type="password",
862
+ value=""
863
+ )
864
+ configure_btn = gr.Button("πŸ”§ Configure API", variant="secondary")
865
+
866
+ api_status = gr.Textbox(
867
+ label="API Status",
868
+ value="❌ Gemini API not configured - Using fallback analysis",
869
+ interactive=False,
870
+ lines=1,
871
+ elem_classes="light-blue-text"
872
+ )
873
+
874
+ # Main Interface (initially hidden until API is configured)
875
+ main_interface = gr.Group(visible=False)
876
+
877
+ with main_interface:
878
+ with gr.Row():
879
+ url_input = gr.Textbox(
880
+ label="πŸ”— YouTube URL",
881
+ placeholder="Paste your YouTube video URL here...",
882
+ value=""
883
+ )
884
+
885
+ cookies_input = gr.File(
886
+ label="πŸͺ Upload cookies.txt (Mandatory)",
887
+ file_types=[".txt"],
888
+ type="filepath"
889
+ )
890
+
891
+ with gr.Tabs():
892
+ with gr.TabItem("πŸ“Š Video Analysis"):
893
+ analyze_btn = gr.Button("πŸ” Analyze Video", variant="primary")
894
+
895
+ analysis_output = gr.HTML(
896
+ label="πŸ“Š Analysis Report",
897
+ )
898
+ download_pdf_btn = gr.Button("πŸ“„ Download Report as PDF", variant="secondary")
899
+ pdf_file_output = gr.File(label="πŸ“₯ PDF Report", visible=True,interactive=False)
900
+
901
+ analyze_btn.click(
902
+ fn=analyze_with_cookies,
903
+ inputs=[url_input, cookies_input],
904
+ outputs=analysis_output,
905
+ show_progress=True
906
+ )
907
+ download_pdf_btn.click(
908
+ fn=analyze_and_generate_pdf,
909
+ inputs=[url_input, cookies_input],
910
+ outputs=pdf_file_output,
911
+ show_progress=True
912
+ )
913
+
914
+
915
+ with gr.TabItem("⬇️ Video Download"):
916
+ with gr.Row():
917
+ quality_dropdown = gr.Dropdown(
918
+ choices=["best", "720p", "480p"],
919
+ value="best",
920
+ label="πŸ“Ί Video Quality"
921
+ )
922
+
923
+ audio_only_checkbox = gr.Checkbox(
924
+ label="🎡 Audio Only (MP3)",
925
+ value=False
926
+ )
927
+
928
+ download_btn = gr.Button("⬇️ Download Video", variant="primary")
929
+
930
+ download_status = gr.Textbox(
931
+ label="πŸ“₯ Download Status",
932
+ lines=5,
933
+ show_copy_button=True
934
+ )
935
+
936
+ download_file = gr.File(
937
+ label="πŸ“ Downloaded File",
938
+ visible=False
939
+ )
940
+
941
+ def download_and_update(url, quality, audio_only, cookies_file, progress=gr.Progress()):
942
+ file_path, status = download_with_cookies(url, quality, audio_only, cookies_file, progress)
943
+ if file_path and os.path.exists(file_path):
944
+ return status, gr.update(value=file_path, visible=True)
945
+ else:
946
+ return status, gr.update(visible=False)
947
+
948
+ download_btn.click(
949
+ fn=download_and_update,
950
+ inputs=[url_input, quality_dropdown, audio_only_checkbox, cookies_input],
951
+ outputs=[download_status, download_file],
952
+ show_progress=True
953
+ )
954
+
955
+ # Configure API key button action
956
+ configure_btn.click(
957
+ fn=configure_api_key,
958
+ inputs=[api_key_input],
959
+ outputs=[api_status, main_interface]
960
+ )
961
+
962
+ # Always show interface option (for fallback mode)
963
+ with gr.Row():
964
+ show_interface_btn = gr.Button("πŸš€ Use Without Gemini API (Fallback Mode)", variant="secondary")
965
+
966
+ def show_fallback_interface():
967
+ return "⚠️ Using fallback analysis mode", gr.update(visible=True)
968
+
969
+ show_interface_btn.click(
970
+ fn=show_fallback_interface,
971
+ outputs=[api_status, main_interface]
972
+ )
973
+
974
+ gr.HTML("""
975
+ <div style="margin-top: 20px; padding: 15px; background-color: #2a2a2a; border-radius: 10px; border-left: 5px solid #FF8C00; color: #87CEEB !important;">
976
+ <h3 style="color: #87CEEB !important; font-weight: bold;">πŸ”‘ How to Get Google API Key:</h3>
977
+ <ol style="color: #87CEEB !important; font-weight: bold;">
978
+ <li style="color: #87CEEB !important;">Go to <a href="https://console.cloud.google.com/" target="_blank" style="color: #87CEEB !important;">Google Cloud Console</a></li>
979
+ <li style="color: #87CEEB !important;">Create a new project or select an existing one</li>
980
+ <li style="color: #87CEEB !important;">Enable the "Generative Language API"</li>
981
+ <li style="color: #87CEEB !important;">Go to "Credentials" and create an API key</li>
982
+ <li style="color: #87CEEB !important;">Copy the API key and paste it above</li>
983
+ </ol>
984
+ <h3 style="color: #87CEEB !important; font-weight: bold;">✨ Benefits of using Gemini API:</h3>
985
+ <ul style="color: #87CEEB !important; font-weight: bold;">
986
+ <li style="color: #87CEEB !important;">πŸ€– AI-powered scene descriptions with contextual understanding</li>
987
+ <li style="color: #87CEEB !important;">🎯 More accurate content type detection</li>
988
+ <li style="color: #87CEEB !important;">πŸ“Š Enhanced analysis based on video content</li>
989
+ <li style="color: #87CEEB !important;">⏰ Intelligent timestamp segmentation</li>
990
+ </ul>
991
+ </div>
992
+ """)
993
+
994
+ return demo
995
+ if __name__ == "__main__":
996
+ demo = create_interface()
997
+ import atexit
998
+ atexit.register(downloader.cleanup)
999
+ demo.launch(debug=True, show_error=True)