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

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1175
app.py DELETED
@@ -1,1175 +0,0 @@
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: 16px;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; font-weight: bold;'>⏱️ Timestamp</th>
6
- <th style='padding: 8px; border: 1px solid #FF8C00; color: #000000; font-weight: bold;'>📝 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: 16px;vertical-align: top;'>{timestamp}</td>
20
- <td style='padding: 8px; border: 1px solid #444; color: #87CEEB; font-weight: bold;font-size: 16px;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
- def generate_pdf_from_html(html_content):
44
- """Generate PDF with simplified HTML that works better with xhtml2pdf"""
45
- try:
46
- # Create a simplified version of the HTML for PDF generation
47
- # Remove complex CSS that xhtml2pdf can't handle
48
- simplified_html = html_content.replace(
49
- "background: linear-gradient(135deg, #2d3748, #1a202c);",
50
- "background-color: #f5f5f5;"
51
- ).replace(
52
- "background: linear-gradient(90deg, #FF8C00, #87CEEB);",
53
- "background-color: #FF8C00;"
54
- ).replace(
55
- "rgba(135, 206, 235, 0.1)",
56
- "#f9f9f9"
57
- ).replace(
58
- "rgba(0, 0, 0, 0.3)",
59
- "#ffffff"
60
- ).replace(
61
- "text-shadow: 2px 2px 4px rgba(0,0,0,0.5);",
62
- ""
63
- ).replace(
64
- "box-shadow: 0 8px 32px rgba(255, 140, 0, 0.3);",
65
- ""
66
- ).replace(
67
- "box-shadow: 0 4px 8px rgba(0,0,0,0.3);",
68
- ""
69
- ).replace(
70
- "display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px;",
71
- "display: block;"
72
- ).replace(
73
- "background-color:#1a1a1a;",
74
- "background-color:#ffffff;"
75
- ).replace(
76
- "color: #FFFFFF;",
77
- "color: #000000;"
78
- ).replace(
79
- "background-color:#FF8C00; color: #000000;",
80
- "background-color:#FF8C00; color: #000000;"
81
- ).replace(
82
- "color: #87CEEB;",
83
- "color: #000080;"
84
- ).replace(
85
- "border: 2px solid #FF8C00;",
86
- "border: 1px solid #FF8C00;"
87
- )
88
-
89
- # Remove table styling that causes issues
90
- simplified_html = re.sub(r"style='[^']*background-color:#1a1a1a[^']*'", "style='background-color:#ffffff;'", simplified_html)
91
- simplified_html = re.sub(r"style='[^']*color: #87CEEB[^']*'", "style='color: #000080; padding: 8px;'", simplified_html)
92
-
93
- # Wrap in a complete HTML document with PDF-friendly CSS
94
- pdf_html = f"""
95
- <!DOCTYPE html>
96
- <html>
97
- <head>
98
- <meta charset="UTF-8">
99
- <style>
100
- @page {{
101
- size: A4;
102
- margin: 1cm;
103
- }}
104
- body {{
105
- font-family: Arial, sans-serif;
106
- font-size: 12px;
107
- line-height: 1.4;
108
- color: #000000;
109
- background-color: #ffffff;
110
- }}
111
- .report-container {{
112
- background-color: #ffffff;
113
- padding: 15px;
114
- border: 2px solid #FF8C00;
115
- border-radius: 8px;
116
- }}
117
- .header {{
118
- text-align: center;
119
- color: #FF8C00;
120
- font-size: 20px;
121
- font-weight: bold;
122
- margin-bottom: 15px;
123
- border-bottom: 2px solid #FF8C00;
124
- padding-bottom: 8px;
125
- }}
126
- .info-card {{
127
- background-color: #f9f9f9;
128
- padding: 12px;
129
- margin: 8px 0;
130
- border-left: 3px solid #87CEEB;
131
- border-radius: 4px;
132
- page-break-inside: avoid;
133
- }}
134
- .info-title {{
135
- color: #000080;
136
- font-size: 14px;
137
- font-weight: bold;
138
- margin-bottom: 8px;
139
- }}
140
- table {{
141
- width: 100%;
142
- border-collapse: collapse;
143
- margin: 8px 0;
144
- page-break-inside: avoid;
145
- }}
146
- th, td {{
147
- padding: 6px 8px;
148
- border: 1px solid #cccccc;
149
- text-align: left;
150
- vertical-align: top;
151
- font-size: 11px;
152
- }}
153
- th {{
154
- background-color: #FF8C00;
155
- color: #000000;
156
- font-weight: bold;
157
- }}
158
- tr:nth-child(even) {{
159
- background-color: #f9f9f9;
160
- }}
161
- .scene-table {{
162
- margin-top: 15px;
163
- }}
164
- .scene-header {{
165
- color: #000080;
166
- font-size: 16px;
167
- font-weight: bold;
168
- text-align: center;
169
- margin-bottom: 10px;
170
- }}
171
- div[style*="display: grid"] {{
172
- display: block !important;
173
- }}
174
- div[style*="grid-template-columns"] > div {{
175
- display: block !important;
176
- margin-bottom: 10px !important;
177
- width: 100% !important;
178
- }}
179
- </style>
180
- </head>
181
- <body>
182
- <div class="report-container">
183
- {simplified_html}
184
- </div>
185
- </body>
186
- </html>
187
- """
188
-
189
- result = BytesIO()
190
- pisa_status = pisa.CreatePDF(pdf_html, dest=result)
191
- print("PDF buffer length:", len(result.getvalue()))
192
-
193
- if pisa_status.err:
194
- print(f"PDF generation error: {pisa_status.err}")
195
- return None
196
-
197
- result.seek(0)
198
- return result
199
-
200
- except Exception as e:
201
- print(f"PDF generation exception: {e}")
202
- return None
203
-
204
- class YouTubeDownloader:
205
- def __init__(self):
206
- self.download_dir = tempfile.mkdtemp()
207
- # Use temp directory for Gradio compatibility
208
- self.temp_downloads = tempfile.mkdtemp(prefix="youtube_downloads_")
209
- # Also create user downloads folder for copying
210
- self.downloads_folder = os.path.join(os.path.expanduser("~"), "Downloads", "YouTube_Downloads")
211
- os.makedirs(self.downloads_folder, exist_ok=True)
212
- self.gemini_model = None
213
-
214
- def configure_gemini(self, api_key):
215
- """Configure Gemini API with the provided key"""
216
- try:
217
- genai.configure(api_key=api_key)
218
- self.gemini_model = genai.GenerativeModel(model_name="gemini-1.5-flash-latest")
219
- return True, "✅ Gemini API configured successfully!"
220
- except Exception as e:
221
- return False, f"❌ Failed to configure Gemini API: {str(e)}"
222
-
223
- def cleanup(self):
224
- """Clean up temporary directories and files"""
225
- try:
226
- if hasattr(self, 'download_dir') and os.path.exists(self.download_dir):
227
- shutil.rmtree(self.download_dir)
228
- print(f"✅ Cleaned up temporary directory: {self.download_dir}")
229
- if hasattr(self, 'temp_downloads') and os.path.exists(self.temp_downloads):
230
- shutil.rmtree(self.temp_downloads)
231
- print(f"✅ Cleaned up temp downloads directory: {self.temp_downloads}")
232
- except Exception as e:
233
- print(f"⚠️ Warning: Could not clean up temporary directory: {e}")
234
-
235
- def is_valid_youtube_url(self, url):
236
- youtube_regex = re.compile(
237
- r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/'
238
- r'(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})'
239
- )
240
- return youtube_regex.match(url) is not None
241
-
242
- def generate_scene_breakdown_gemini(self, video_info):
243
- """Generate AI-powered scene breakdown using Gemini"""
244
- if not self.gemini_model:
245
- return self.generate_scene_breakdown_fallback(video_info)
246
-
247
- try:
248
- duration = video_info.get('duration', 0)
249
- title = video_info.get('title', '')
250
- description = video_info.get('description', '')[:1500] # Increased limit for better context
251
-
252
- if not duration:
253
- return ["**[Duration Unknown]**: Unable to generate timestamped breakdown - video duration not available"]
254
-
255
- # Create enhanced prompt for Gemini
256
- prompt = f"""
257
- Analyze this YouTube video and create a highly detailed, scene-by-scene breakdown with precise timestamps and specific descriptions:
258
-
259
- Title: {title}
260
- Duration: {duration} seconds
261
- Description: {description}
262
-
263
- IMPORTANT INSTRUCTIONS:
264
- 1. Create detailed scene descriptions that include:
265
- - Physical appearance of people (age, gender, clothing, hair, etc.)
266
- - Exact actions being performed
267
- - Dialogue or speech (include actual lines if audible, or infer probable spoken lines based on actions and setting; format them as "Character: line...")
268
- - Setting and environment details
269
- - Props, objects, or products being shown
270
- - Visual effects, text overlays, or graphics
271
- - Mood, tone, and atmosphere
272
- - Camera movements or angles (if apparent)
273
- 2. Dialogue Emphasis:
274
- - Include short dialogue lines in **every scene** wherever plausible.
275
- - Write lines like: Character: "Actual or inferred line..."
276
- - If dialogue is not available, intelligently infer probable phrases (e.g., "Welcome!", "Try this now!", "It feels amazing!").
277
- - Do NOT skip dialogue unless it's clearly impossible.
278
-
279
- 3. Timestamp Guidelines:
280
- - For videos under 1 minute: 2-3 second segments
281
- - For videos 1-5 minutes: 3-5 second segments
282
- - For videos 5-15 minutes: 5-10 second segments
283
- - For videos over 15 minutes: 10-15 second segments
284
- - Maximum 20 scenes total for longer videos
285
-
286
- 4. Format each scene EXACTLY like this:
287
- **[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.
288
-
289
-
290
- 5. Write descriptions as if you're watching the video in real-time, noting everything visible and audible.
291
-
292
- 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.
293
- """
294
-
295
- response = self.gemini_model.generate_content(prompt)
296
-
297
- # Parse the response into individual scenes
298
- if response and response.text:
299
- scenes = []
300
- lines = response.text.split('\n')
301
- current_scene = ""
302
-
303
- for line in lines:
304
- line = line.strip()
305
- if line.strip().startswith("**[") and "]**:" in line:
306
- # This is a new scene timestamp line
307
- if current_scene:
308
- scenes.append(current_scene.strip())
309
- current_scene = line.strip()
310
- elif current_scene:
311
- # This is continuation of the current scene description
312
- current_scene += "\n" + line.strip()
313
-
314
- # Add the last scene if exists
315
- if current_scene:
316
- scenes.append(current_scene.strip())
317
-
318
- return scenes if scenes else self.generate_scene_breakdown_fallback(video_info)
319
- else:
320
- return self.generate_scene_breakdown_fallback(video_info)
321
-
322
- except Exception as e:
323
- print(f"Gemini API error: {e}")
324
- return self.generate_scene_breakdown_fallback(video_info)
325
-
326
- def generate_scene_breakdown_fallback(self, video_info):
327
- """Enhanced fallback scene generation when Gemini is not available"""
328
- duration = video_info.get('duration', 0)
329
- title = video_info.get('title', '').lower()
330
- description = video_info.get('description', '').lower()
331
- uploader = video_info.get('uploader', 'Content creator')
332
-
333
- if not duration:
334
- return ["**[Duration Unknown]**: Unable to generate timestamped breakdown"]
335
-
336
- # Determine segment length based on duration
337
- if duration <= 60:
338
- segment_length = 3
339
- elif duration <= 300:
340
- segment_length = 5
341
- elif duration <= 900:
342
- segment_length = 10
343
- else:
344
- segment_length = 15
345
-
346
- scenes = []
347
- num_segments = min(duration // segment_length + 1, 20)
348
-
349
- # Detect video type for better descriptions
350
- video_type = self.detect_video_type_detailed(title, description)
351
-
352
- for i in range(num_segments):
353
- start_time = i * segment_length
354
- end_time = min(start_time + segment_length - 1, duration)
355
-
356
- start_formatted = f"{start_time//60}:{start_time%60:02d}"
357
- end_formatted = f"{end_time//60}:{end_time%60:02d}"
358
-
359
- # Generate contextual descriptions based on video type and timing
360
- desc = self.generate_contextual_description(i, num_segments, video_type, uploader, title)
361
-
362
- scenes.append(f"**[{start_formatted}-{end_formatted}]**: {desc}")
363
-
364
- return scenes
365
-
366
- def detect_video_type_detailed(self, title, description):
367
- """Detect video type with more detail for better fallback descriptions"""
368
- text = (title + " " + description).lower()
369
-
370
- if any(word in text for word in ['tutorial', 'how to', 'guide', 'learn', 'diy', 'step by step']):
371
- return 'tutorial'
372
- elif any(word in text for word in ['review', 'unboxing', 'test', 'comparison', 'vs']):
373
- return 'review'
374
- elif any(word in text for word in ['vlog', 'daily', 'routine', 'day in', 'morning', 'skincare']):
375
- return 'vlog'
376
- elif any(word in text for word in ['music', 'song', 'cover', 'lyrics', 'dance']):
377
- return 'music'
378
- elif any(word in text for word in ['comedy', 'funny', 'prank', 'challenge', 'reaction']):
379
- return 'entertainment'
380
- elif any(word in text for word in ['news', 'breaking', 'update', 'report']):
381
- return 'news'
382
- elif any(word in text for word in ['cooking', 'recipe', 'food', 'kitchen']):
383
- return 'cooking'
384
- elif any(word in text for word in ['workout', 'fitness', 'exercise', 'yoga']):
385
- return 'fitness'
386
- else:
387
- return 'general'
388
-
389
- def generate_contextual_description(self, scene_index, total_scenes, video_type, uploader, title):
390
- """Generate contextual descriptions based on video type and scene position"""
391
-
392
- # Common elements
393
- presenter_desc = f"The content creator"
394
- if 'woman' in title.lower() or 'girl' in title.lower():
395
- presenter_desc = "A woman"
396
- elif 'man' in title.lower() or 'guy' in title.lower():
397
- presenter_desc = "A man"
398
-
399
- # Position-based descriptions
400
- if scene_index == 0:
401
- # Opening scene
402
- if video_type == 'tutorial':
403
- 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."
404
- elif video_type == 'vlog':
405
- 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."
406
- elif video_type == 'review':
407
- 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."
408
- else:
409
- 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."
410
-
411
- elif scene_index == total_scenes - 1:
412
- # Closing scene
413
- if video_type == 'tutorial':
414
- 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."
415
- elif video_type == 'vlog':
416
- 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."
417
- else:
418
- return f"{presenter_desc} concludes the video with final thoughts, thanking viewers for watching, and encouraging engagement through likes, comments, and subscriptions."
419
-
420
- else:
421
- # Middle scenes - content-specific
422
- if video_type == 'tutorial':
423
- step_num = scene_index
424
- 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."
425
-
426
- elif video_type == 'review':
427
- 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."
428
-
429
- elif video_type == 'vlog':
430
- 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."
431
-
432
- elif video_type == 'cooking':
433
- 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."
434
-
435
- elif video_type == 'fitness':
436
- 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."
437
-
438
- else:
439
- 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."
440
-
441
- def detect_video_type(self, title, description):
442
- """Detect video type based on title and description"""
443
- text = (title + " " + description).lower()
444
-
445
- if any(word in text for word in ['music', 'song', 'album', 'artist', 'band', 'lyrics']):
446
- return "🎵 Music Video"
447
- elif any(word in text for word in ['tutorial', 'how to', 'guide', 'learn', 'teaching']):
448
- return "📚 Tutorial/Educational"
449
- elif any(word in text for word in ['funny', 'comedy', 'entertainment', 'vlog', 'challenge']):
450
- return "🎭 Entertainment/Comedy"
451
- elif any(word in text for word in ['news', 'breaking', 'report', 'update']):
452
- return "📰 News/Information"
453
- elif any(word in text for word in ['review', 'unboxing', 'test', 'comparison']):
454
- return "⭐ Review/Unboxing"
455
- elif any(word in text for word in ['commercial', 'ad', 'brand', 'product']):
456
- return "📺 Commercial/Advertisement"
457
- else:
458
- return "🎬 General Content"
459
-
460
- def detect_background_music(self, video_info):
461
- """Detect background music style"""
462
- title = video_info.get('title', '').lower()
463
- description = video_info.get('description', '').lower()
464
-
465
- if any(word in title for word in ['music', 'song', 'soundtrack']):
466
- return "🎵 Original Music/Soundtrack - Primary audio content"
467
- elif any(word in title for word in ['commercial', 'ad', 'brand']):
468
- return "🎶 Upbeat Commercial Music - Designed to enhance brand appeal"
469
- elif any(word in title for word in ['tutorial', 'how to', 'guide']):
470
- return "🔇 Minimal/No Background Music - Focus on instruction"
471
- elif any(word in title for word in ['vlog', 'daily', 'life']):
472
- return "🎼 Ambient Background Music - Complementary to narration"
473
- else:
474
- return "🎵 Background Music - Complementing video mood and pacing"
475
-
476
- def detect_influencer_status(self, video_info):
477
- """Detect influencer status"""
478
- subscriber_count = video_info.get('channel_followers', 0)
479
- view_count = video_info.get('view_count', 0)
480
-
481
- if subscriber_count > 10000000:
482
- return "🌟 Mega Influencer (10M+ subscribers)"
483
- elif subscriber_count > 1000000:
484
- return "⭐ Major Influencer (1M+ subscribers)"
485
- elif subscriber_count > 100000:
486
- return "🎯 Mid-tier Influencer (100K+ subscribers)"
487
- elif subscriber_count > 10000:
488
- return "📈 Micro Influencer (10K+ subscribers)"
489
- elif view_count > 100000:
490
- return "🔥 Viral Content Creator"
491
- else:
492
- return "👤 Regular Content Creator"
493
-
494
- def format_number(self, num):
495
- if num is None or num == 0:
496
- return "0"
497
- if num >= 1_000_000_000:
498
- return f"{num/1_000_000_000:.1f}B"
499
- elif num >= 1_000_000:
500
- return f"{num/1_000_000:.1f}M"
501
- elif num >= 1_000:
502
- return f"{num/1_000:.1f}K"
503
- return str(num)
504
-
505
- def format_video_info(self, video_info):
506
- """Compact video information formatting with tabular layout"""
507
- if not video_info:
508
- return "❌ No video information available."
509
-
510
- # Basic information
511
- title = video_info.get("title", "Unknown")
512
- uploader = video_info.get("uploader", "Unknown")
513
- duration = video_info.get("duration", 0)
514
- duration_str = f"{duration//60}:{duration%60:02d}" if duration else "Unknown"
515
- view_count = video_info.get("view_count", 0)
516
- like_count = video_info.get("like_count", 0)
517
- comment_count = video_info.get("comment_count", 0)
518
- upload_date = video_info.get("upload_date", "Unknown")
519
-
520
- # Format upload date
521
- if len(upload_date) == 8:
522
- formatted_date = f"{upload_date[:4]}-{upload_date[4:6]}-{upload_date[6:8]}"
523
- else:
524
- formatted_date = upload_date
525
-
526
- # Generate enhanced analysis
527
- scene_descriptions = self.generate_scene_breakdown_gemini(video_info)
528
- scene_table_html = format_scene_breakdown(scene_descriptions)
529
- video_type = self.detect_video_type(title, video_info.get('description', ''))
530
- background_music = self.detect_background_music(video_info)
531
- influencer_status = self.detect_influencer_status(video_info)
532
-
533
- # Calculate engagement metrics
534
- engagement_rate = (like_count / view_count) * 100 if view_count > 0 else 0
535
-
536
- # Generate compact report with contrasting background
537
- report = f"""
538
- <div style='font-family: Arial, sans-serif; background: linear-gradient(135deg, #2d3748, #1a202c); padding: 20px; border-radius: 15px; border: 2px solid #FF8C00; box-shadow: 0 8px 32px rgba(255, 140, 0, 0.3);'>
539
-
540
- <div style='text-align: center; margin-bottom: 20px;'>
541
- <h2 style='color: #FF8C00; font-size: 24px; margin: 0; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);'>🎬 YouTube Video Analysis Report</h2>
542
- <div style='height: 3px; background: linear-gradient(90deg, #FF8C00, #87CEEB); margin: 10px 0; border-radius: 5px;'></div>
543
- </div>
544
-
545
- <!-- Compact Information Grid -->
546
- <div style='display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-bottom: 20px;'>
547
-
548
- <!-- Basic Information Card -->
549
- <div style='background: rgba(135, 206, 235, 0.1); padding: 15px; border-radius: 10px; border-left: 4px solid #87CEEB;'>
550
- <h3 style='color: #FF8C00; margin: 0 0 10px 0; font-size: 16px;'>📋 Basic Info</h3>
551
- <table style='width: 100%; font-size: 14px;'>
552
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>📹 Title:</td></tr>
553
- <tr><td style='color: #87CEEB; font-weight: bold: padding: 4px 0 8px 0; word-wrap: wrap-word; white-space: normal; max-width: 200px;'>{title}</td></tr>
554
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>👤 Creator:</td><td style='color: #87CEEB; padding: 2px 0;'>{uploader[:20]}{'...' if len(uploader) > 20 else ''}</td></tr>
555
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>📅 Date:</td><td style='color: #87CEEB; padding: 2px 0;'>{formatted_date}</td></tr>
556
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>⏱️ Duration:</td><td style='color: #87CEEB; padding: 2px 0;'>{duration_str}</td></tr>
557
- </table>
558
- </div>
559
-
560
- <!-- Performance Metrics Card -->
561
- <div style='background: rgba(135, 206, 235, 0.1); padding: 15px; border-radius: 10px; border-left: 4px solid #FF8C00;border: 1px solid #444'>
562
- <h3 style='color: #FF8C00; margin: 0 0 10px 0; font-size: 16px;'>📊 Metrics</h3>
563
- <table style='width: 100%; font-size: 12px;'>
564
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>👀 Views:</td><td style='color: #87CEEB; padding: 4px 0;'>{self.format_number(view_count)}</td></tr>
565
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>👍 Likes:</td><td style='color: #87CEEB; padding: 4px 0;'>{self.format_number(like_count)}</td></tr>
566
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>💬 Comments:</td><td style='color: #87CEEB; padding: 4px 0;'>{self.format_number(comment_count)}</td></tr>
567
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>📈 Engagement:</td><td style='color: #87CEEB; padding: 4px 0;'>{engagement_rate:.2f}%</td></tr>
568
- </table>
569
- </div>
570
-
571
- <!-- Content Analysis Card -->
572
- <div style='background:rgba(135, 206, 235, 0.1); padding: 15px; border-radius: 10px; border-left: 4px solid #87CEEB;border: 1px solid #444'>
573
- <h3 style='color:#FF8C00; margin: 0 0 10px 0; font-size: 16px;'>🎯 Analysis</h3>
574
- <table style='width: 100%; font-size: 12px;'>
575
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>📂 Type:</td></tr>
576
- <tr><td style='color: 87CEEB; padding: 4px 0 8px 0; word-break: break-word;'>{video_type}</td></tr>
577
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>🎵 Music:</td></tr>
578
- <tr><td style='color: #87CEEB; padding: 4px 0 8px 0; word-break: break-word;'>{background_music[:30]}{'...' if len(background_music) > 30 else ''}</td></tr>
579
- <tr><td style='color: #87CEEB; font-weight: bold; padding: 4px 0;'>👑 Status:</td></tr>
580
- <tr><td style='color: #87CEEB; padding: 4px 0; word-break: break-word;'>{influencer_status[:25]}{'...' if len(influencer_status) > 25 else ''}</td></tr>
581
- </table>
582
- </div>
583
- </div>
584
-
585
- <!-- Scene Breakdown Section -->
586
- <div style='background: rgba(0, 0, 0, 0.3); padding: 15px; border-radius: 10px; border: 1px solid #444;'>
587
- <h3 style='color: #87CEEB; margin: 0 0 15px 0; font-size: 18px; text-align: center;'>🎬 Scene-by-Scene Breakdown</h3>
588
- {scene_table_html}
589
- </div>
590
-
591
- </div>
592
- """
593
-
594
- return report.strip()
595
-
596
- def get_video_info(self, url, progress=gr.Progress(), cookiefile=None):
597
- """Extract video information"""
598
- if not url or not url.strip():
599
- return None, "❌ Please enter a YouTube URL"
600
-
601
- if not self.is_valid_youtube_url(url):
602
- return None, "❌ Invalid YouTube URL format"
603
-
604
- try:
605
- progress(0.1, desc="Initializing YouTube extractor...")
606
-
607
- ydl_opts = {
608
- 'noplaylist': True,
609
- 'extract_flat': False,
610
- }
611
-
612
- if cookiefile and os.path.exists(cookiefile):
613
- ydl_opts['cookiefile'] = cookiefile
614
-
615
- progress(0.5, desc="Extracting video metadata...")
616
-
617
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
618
- info = ydl.extract_info(url, download=False)
619
-
620
- progress(1.0, desc="✅ Analysis complete!")
621
-
622
- return info, "✅ Video information extracted successfully"
623
-
624
- except Exception as e:
625
- return None, f"❌ Error: {str(e)}"
626
-
627
- def download_video(self, url, quality="best", audio_only=False, progress=gr.Progress(), cookiefile=None):
628
- """Download video with progress tracking"""
629
- if not url or not url.strip():
630
- return None, "❌ Please enter a YouTube URL"
631
-
632
- if not self.is_valid_youtube_url(url):
633
- return None, "❌ Invalid YouTube URL format"
634
-
635
- try:
636
- progress(0.1, desc="Preparing download...")
637
-
638
- # Create unique filename
639
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
640
-
641
- # Download to temp directory first (Gradio compatible)
642
- ydl_opts = {
643
- 'outtmpl': os.path.join(self.temp_downloads, f'%(title)s_{timestamp}.%(ext)s'),
644
- 'noplaylist': True,
645
- }
646
-
647
- if audio_only:
648
- ydl_opts['format'] = 'bestaudio/best'
649
- ydl_opts['postprocessors'] = [{
650
- 'key': 'FFmpegExtractAudio',
651
- 'preferredcodec': 'mp3',
652
- 'preferredquality': '192',
653
- }]
654
- else:
655
- if quality == "best":
656
- ydl_opts['format'] = 'best[height<=1080]'
657
- elif quality == "720p":
658
- ydl_opts['format'] = 'best[height<=720]'
659
- elif quality == "480p":
660
- ydl_opts['format'] = 'best[height<=480]'
661
- else:
662
- ydl_opts['format'] = 'best'
663
-
664
- if cookiefile and os.path.exists(cookiefile):
665
- ydl_opts['cookiefile'] = cookiefile
666
-
667
- # Progress hook
668
- def progress_hook(d):
669
- if d['status'] == 'downloading':
670
- if 'total_bytes' in d:
671
- percent = (d['downloaded_bytes'] / d['total_bytes']) * 100
672
- progress(0.1 + (percent / 100) * 0.7, desc=f"Downloading... {percent:.1f}%")
673
- else:
674
- progress(0.5, desc="Downloading...")
675
- elif d['status'] == 'finished':
676
- progress(0.8, desc="Processing download...")
677
-
678
- ydl_opts['progress_hooks'] = [progress_hook]
679
-
680
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
681
- info = ydl.extract_info(url, download=True)
682
-
683
- progress(0.9, desc="Copying to Downloads folder...")
684
-
685
- # Find the downloaded file in temp directory
686
- downloaded_file_temp = None
687
-
688
- for file in os.listdir(self.temp_downloads):
689
- if timestamp in file:
690
- downloaded_file_temp = os.path.join(self.temp_downloads, file)
691
- break
692
-
693
- if not downloaded_file_temp:
694
- return None, "❌ Downloaded file not found in temp directory"
695
-
696
- # Copy to user's Downloads folder
697
- final_filename = os.path.basename(downloaded_file_temp)
698
- final_path = os.path.join(self.downloads_folder, final_filename)
699
-
700
- try:
701
- shutil.copy2(downloaded_file_temp, final_path)
702
- copy_success = True
703
- except Exception as e:
704
- print(f"Warning: Could not copy to Downloads folder: {e}")
705
- copy_success = False
706
- final_path = "File downloaded to temp location only"
707
-
708
- progress(1.0, desc="✅ Download complete!")
709
-
710
- success_msg = f"""✅ Download successful!
711
- 📁 Temp file (for download): {os.path.basename(downloaded_file_temp)}
712
- 📁 Permanent location: {final_path if copy_success else 'Copy failed'}
713
- 🎯 File size: {os.path.getsize(downloaded_file_temp) / (1024*1024):.1f} MB"""
714
-
715
- return downloaded_file_temp, success_msg
716
-
717
- except Exception as e:
718
- return None, f"❌ Download failed: {str(e)}"
719
-
720
- # Initialize global downloader
721
- downloader = YouTubeDownloader()
722
-
723
- def configure_api_key(api_key):
724
- """Configure Gemini API key"""
725
- if not api_key or not api_key.strip():
726
- return "❌ Please enter a valid Google API key", gr.update(visible=False)
727
-
728
- success, message = downloader.configure_gemini(api_key.strip())
729
-
730
- if success:
731
- return message, gr.update(visible=True)
732
- else:
733
- return message, gr.update(visible=False)
734
-
735
- def analyze_with_cookies(url, cookies_file, progress=gr.Progress()):
736
- """Main analysis function"""
737
- try:
738
- progress(0.05, desc="Starting analysis...")
739
-
740
- cookiefile = None
741
- if cookies_file and os.path.exists(cookies_file):
742
- cookiefile = cookies_file
743
-
744
- info, msg = downloader.get_video_info(url, progress=progress, cookiefile=cookiefile)
745
-
746
- if info:
747
- progress(0.95, desc="Generating comprehensive report...")
748
- formatted_info = downloader.format_video_info(info)
749
- progress(1.0, desc="✅ Complete!")
750
- return formatted_info
751
- else:
752
- return f"❌ Analysis Failed: {msg}"
753
-
754
- except Exception as e:
755
- return f"❌ System Error: {str(e)}"
756
-
757
-
758
- def analyze_and_generate_pdf(url, cookies_file, progress=gr.Progress()):
759
- try:
760
- progress(0.1, desc="Extracting video info...")
761
- cookiefile = cookies_file if cookies_file and os.path.exists(cookies_file) else None
762
-
763
- info, _ = downloader.get_video_info(url, progress=progress, cookiefile=cookiefile)
764
- if not info:
765
- return "❌ Failed to extract video info"
766
-
767
- progress(0.6, desc="Generating HTML report...")
768
- report_html = downloader.format_video_info(info)
769
-
770
- progress(0.8, desc="Creating PDF...")
771
-
772
- # Generate PDF from HTML
773
- pdf_data = BytesIO()
774
- result = pisa.CreatePDF(report_html, dest=pdf_data)
775
-
776
- if result.err:
777
- return "❌ PDF generation failed"
778
-
779
- pdf_data.seek(0)
780
- pdf_path = os.path.join(tempfile.gettempdir(), f"analysis_report_{uuid.uuid4().hex}.pdf")
781
- with open(pdf_path, "wb") as f:
782
- f.write(pdf_data.read())
783
-
784
- progress(1.0, desc="✅ PDF ready!")
785
- print("✅ PDF generated at:", pdf_path)
786
- return pdf_path # ✅ Return direct path like in test code
787
-
788
- except Exception as e:
789
- print("❌ Exception:", e)
790
- return f"❌ Error: {str(e)}"
791
-
792
- def generate_pdf_from_html(html_content):
793
- """Generate PDF with simplified HTML that works better with xhtml2pdf"""
794
- try:
795
- # Create a simplified version of the HTML for PDF generation
796
- # Remove complex CSS that xhtml2pdf can't handle
797
- simplified_html = html_content.replace(
798
- "background: linear-gradient(135deg, #2d3748, #1a202c);",
799
- "background-color: #f5f5f5;"
800
- ).replace(
801
- "background: linear-gradient(90deg, #FF8C00, #87CEEB);",
802
- "background-color: #FF8C00;"
803
- ).replace(
804
- "rgba(135, 206, 235, 0.1)",
805
- "#f9f9f9"
806
- ).replace(
807
- "rgba(0, 0, 0, 0.3)",
808
- "#ffffff"
809
- ).replace(
810
- "text-shadow: 2px 2px 4px rgba(0,0,0,0.5);",
811
- ""
812
- ).replace(
813
- "box-shadow: 0 8px 32px rgba(255, 140, 0, 0.3);",
814
- ""
815
- ).replace(
816
- "box-shadow: 0 4px 8px rgba(0,0,0,0.3);",
817
- ""
818
- ).replace(
819
- "display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px;",
820
- "display: block;"
821
- ).replace(
822
- "background-color:#1a1a1a;",
823
- "background-color:#ffffff;"
824
- ).replace(
825
- "color: #FFFFFF;",
826
- "color: #000000;"
827
- ).replace(
828
- "background-color:#FF8C00; color: #000000;",
829
- "background-color:#FF8C00; color: #000000;"
830
- ).replace(
831
- "color: #87CEEB;",
832
- "color: #000080;"
833
- ).replace(
834
- "border: 2px solid #FF8C00;",
835
- "border: 1px solid #FF8C00;"
836
- )
837
-
838
- # Remove table styling that causes issues
839
- simplified_html = re.sub(r"style='[^']*background-color:#1a1a1a[^']*'", "style='background-color:#ffffff;'", simplified_html)
840
- simplified_html = re.sub(r"style='[^']*color: #87CEEB[^']*'", "style='color: #000080; padding: 8px;'", simplified_html)
841
-
842
- # Wrap in a complete HTML document with PDF-friendly CSS
843
- pdf_html = f"""
844
- <!DOCTYPE html>
845
- <html>
846
- <head>
847
- <meta charset="UTF-8">
848
- <style>
849
- @page {{
850
- size: A4;
851
- margin: 1cm;
852
- }}
853
- body {{
854
- font-family: Arial, sans-serif;
855
- font-size: 12px;
856
- line-height: 1.4;
857
- color: #000000;
858
- background-color: #ffffff;
859
- }}
860
- .report-container {{
861
- background-color: #ffffff;
862
- padding: 15px;
863
- border: 2px solid #FF8C00;
864
- border-radius: 8px;
865
- }}
866
- .header {{
867
- text-align: center;
868
- color: #FF8C00;
869
- font-size: 20px;
870
- font-weight: bold;
871
- margin-bottom: 15px;
872
- border-bottom: 2px solid #FF8C00;
873
- padding-bottom: 8px;
874
- }}
875
- .info-card {{
876
- background-color: #f9f9f9;
877
- padding: 12px;
878
- margin: 8px 0;
879
- border-left: 3px solid #87CEEB;
880
- border-radius: 4px;
881
- page-break-inside: avoid;
882
- }}
883
- .info-title {{
884
- color: #000080;
885
- font-size: 14px;
886
- font-weight: bold;
887
- margin-bottom: 8px;
888
- }}
889
- table {{
890
- width: 100%;
891
- border-collapse: collapse;
892
- margin: 8px 0;
893
- page-break-inside: avoid;
894
- }}
895
- th, td {{
896
- padding: 6px 8px;
897
- border: 1px solid #cccccc;
898
- text-align: left;
899
- vertical-align: top;
900
- font-size: 11px;
901
- }}
902
- th {{
903
- background-color: #FF8C00;
904
- color: #000000;
905
- font-weight: bold;
906
- }}
907
- tr:nth-child(even) {{
908
- background-color: #f9f9f9;
909
- }}
910
- .scene-table {{
911
- margin-top: 15px;
912
- }}
913
- .scene-header {{
914
- color: #000080;
915
- font-size: 16px;
916
- font-weight: bold;
917
- text-align: center;
918
- margin-bottom: 10px;
919
- }}
920
- div[style*="display: grid"] {{
921
- display: block !important;
922
- }}
923
- div[style*="grid-template-columns"] > div {{
924
- display: block !important;
925
- margin-bottom: 10px !important;
926
- width: 100% !important;
927
- }}
928
- </style>
929
- </head>
930
- <body>
931
- <div class="report-container">
932
- {simplified_html}
933
- </div>
934
- </body>
935
- </html>
936
- """
937
-
938
- result = BytesIO()
939
- pisa_status = pisa.CreatePDF(pdf_html, dest=result)
940
-
941
- if pisa_status.err:
942
- print(f"PDF generation error: {pisa_status.err}")
943
- return None
944
-
945
- result.seek(0)
946
- return result
947
-
948
- except Exception as e:
949
- print(f"PDF generation exception: {e}")
950
- return None
951
-
952
-
953
-
954
- def download_with_cookies(url, quality, audio_only, cookies_file, progress=gr.Progress()):
955
- """Main download function"""
956
- try:
957
- progress(0.05, desc="Preparing download...")
958
-
959
- cookiefile = None
960
- if cookies_file and os.path.exists(cookies_file):
961
- cookiefile = cookies_file
962
-
963
- file_path, msg = downloader.download_video(url, quality, audio_only, progress=progress, cookiefile=cookiefile)
964
-
965
- if file_path:
966
- return file_path, msg
967
- else:
968
- return None, msg
969
-
970
- except Exception as e:
971
- return None, f"❌ System Error: {str(e)}"
972
-
973
- def create_interface():
974
- """Create and configure the Gradio interface"""
975
- with gr.Blocks(
976
- css="""
977
- /* Main dark theme background and text */
978
- .gradio-container, .app, body {
979
- background-color: #1a1a1a !important;
980
- color: #87CEEB !important;
981
- font-weight: bold !important;
982
- }
983
- /* 🔵 Dark blue overrides for key labels */
984
- h3, .gr-group h3, .gradio-container h3 {
985
- color: #87CEEB !important;
986
- }
987
-
988
- label, .gr-textbox label, .gr-file label, .gr-dropdown label, .gr-checkbox label {
989
- color: #00008B !important;
990
- font-weight: bold !important;
991
- }
992
-
993
- .gr-file .file-name {
994
- color: #00008B !important;
995
- font-weight: bold !important;
996
- }
997
-
998
- /* Make tab labels dark blue too */
999
- .gr-tab-nav button {
1000
- color: #00008B !important;
1001
- }
1002
-
1003
- .gr-tab-nav button.selected {
1004
- background-color: #FF8C00 !important;
1005
- color: #000000 !important;
1006
- }
1007
-
1008
- /* Light blue text for API status */
1009
- .light-blue-text textarea {
1010
- color: #87CEEB !important;
1011
- background-color: #2a2a2a !important;
1012
- }
1013
- .gr-file {
1014
- background-color: #2a2a2a !important;
1015
- border: 2px dashed #444 !important;
1016
- }
1017
-
1018
- .gr-group, .gr-form, .gr-row {
1019
- background-color: #1a1a1a !important;
1020
- border: 1px solid #444 !important;
1021
- border-radius: 10px;
1022
- padding: 15px;
1023
- }
1024
-
1025
- """,
1026
- theme=gr.themes.Soft(),
1027
- title="📊 YouTube Video Analyzer & Downloader"
1028
- ) as demo:
1029
-
1030
- # API Key Configuration Section
1031
- with gr.Group():
1032
- gr.HTML("<h3>🔑 Google Gemini API Configuration</h3>")
1033
- with gr.Row():
1034
- api_key_input = gr.Textbox(
1035
- label="🔑 Google API Key",
1036
- placeholder="Enter your Google API Key for enhanced AI analysis...",
1037
- type="password",
1038
- value=""
1039
- )
1040
- configure_btn = gr.Button("🔧 Configure API", variant="secondary")
1041
-
1042
- api_status = gr.Textbox(
1043
- label="API Status",
1044
- value="❌ Gemini API not configured - Using fallback analysis",
1045
- interactive=False,
1046
- lines=1,
1047
- elem_classes="light-blue-text"
1048
- )
1049
-
1050
- # Main Interface (initially hidden until API is configured)
1051
- main_interface = gr.Group(visible=False)
1052
-
1053
- with main_interface:
1054
- with gr.Row():
1055
- url_input = gr.Textbox(
1056
- label="🔗 YouTube URL",
1057
- placeholder="Paste your YouTube video URL here...",
1058
- value=""
1059
- )
1060
-
1061
- cookies_input = gr.File(
1062
- label="🍪 Upload cookies.txt (Mandatory)",
1063
- file_types=[".txt"],
1064
- type="filepath"
1065
- )
1066
-
1067
- with gr.Tabs():
1068
- with gr.TabItem("📊 Video Analysis"):
1069
- analyze_btn = gr.Button("🔍 Analyze Video", variant="primary")
1070
-
1071
- analysis_output = gr.HTML(
1072
- label="📊 Analysis Report",
1073
- )
1074
- download_pdf_btn = gr.Button("📄 Download Report as PDF", variant="secondary")
1075
- pdf_file_output = gr.File(label="📥 PDF Report", visible=True,interactive=False)
1076
-
1077
- analyze_btn.click(
1078
- fn=analyze_with_cookies,
1079
- inputs=[url_input, cookies_input],
1080
- outputs=analysis_output,
1081
- show_progress=True
1082
- )
1083
- download_pdf_btn.click(
1084
- fn=analyze_and_generate_pdf,
1085
- inputs=[url_input, cookies_input],
1086
- outputs=pdf_file_output,
1087
- show_progress=True
1088
- )
1089
-
1090
-
1091
- with gr.TabItem("⬇️ Video Download"):
1092
- with gr.Row():
1093
- quality_dropdown = gr.Dropdown(
1094
- choices=["best", "720p", "480p"],
1095
- value="best",
1096
- label="📺 Video Quality"
1097
- )
1098
-
1099
- audio_only_checkbox = gr.Checkbox(
1100
- label="🎵 Audio Only (MP3)",
1101
- value=False
1102
- )
1103
-
1104
- download_btn = gr.Button("⬇️ Download Video", variant="primary")
1105
-
1106
- download_status = gr.Textbox(
1107
- label="📥 Download Status",
1108
- lines=5,
1109
- show_copy_button=True
1110
- )
1111
-
1112
- download_file = gr.File(
1113
- label="📁 Downloaded File",
1114
- visible=False
1115
- )
1116
-
1117
- def download_and_update(url, quality, audio_only, cookies_file, progress=gr.Progress()):
1118
- file_path, status = download_with_cookies(url, quality, audio_only, cookies_file, progress)
1119
- if file_path and os.path.exists(file_path):
1120
- return status, gr.update(value=file_path, visible=True)
1121
- else:
1122
- return status, gr.update(visible=False)
1123
-
1124
- download_btn.click(
1125
- fn=download_and_update,
1126
- inputs=[url_input, quality_dropdown, audio_only_checkbox, cookies_input],
1127
- outputs=[download_status, download_file],
1128
- show_progress=True
1129
- )
1130
-
1131
- # Configure API key button action
1132
- configure_btn.click(
1133
- fn=configure_api_key,
1134
- inputs=[api_key_input],
1135
- outputs=[api_status, main_interface]
1136
- )
1137
-
1138
- # Always show interface option (for fallback mode)
1139
- with gr.Row():
1140
- show_interface_btn = gr.Button("🚀 Use Without Gemini API (Fallback Mode)", variant="secondary")
1141
-
1142
- def show_fallback_interface():
1143
- return "⚠️ Using fallback analysis mode", gr.update(visible=True)
1144
-
1145
- show_interface_btn.click(
1146
- fn=show_fallback_interface,
1147
- outputs=[api_status, main_interface]
1148
- )
1149
-
1150
- gr.HTML("""
1151
- <div style="margin-top: 20px; padding: 15px; background-color: #2a2a2a; border-radius: 10px; border-left: 5px solid #FF8C00; color: #87CEEB !important;">
1152
- <h3 style="color: #87CEEB !important; font-weight: bold;">🔑 How to Get Google API Key:</h3>
1153
- <ol style="color: #87CEEB !important; font-weight: bold;">
1154
- <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>
1155
- <li style="color: #87CEEB !important;">Create a new project or select an existing one</li>
1156
- <li style="color: #87CEEB !important;">Enable the "Generative Language API"</li>
1157
- <li style="color: #87CEEB !important;">Go to "Credentials" and create an API key</li>
1158
- <li style="color: #87CEEB !important;">Copy the API key and paste it above</li>
1159
- </ol>
1160
- <h3 style="color: #87CEEB !important; font-weight: bold;">✨ Benefits of using Gemini API:</h3>
1161
- <ul style="color: #87CEEB !important; font-weight: bold;">
1162
- <li style="color: #87CEEB !important;">🤖 AI-powered scene descriptions with contextual understanding</li>
1163
- <li style="color: #87CEEB !important;">🎯 More accurate content type detection</li>
1164
- <li style="color: #87CEEB !important;">📊 Enhanced analysis based on video content</li>
1165
- <li style="color: #87CEEB !important;">⏰ Intelligent timestamp segmentation</li>
1166
- </ul>
1167
- </div>
1168
- """)
1169
-
1170
- return demo
1171
- if __name__ == "__main__":
1172
- demo = create_interface()
1173
- import atexit
1174
- atexit.register(downloader.cleanup)
1175
- demo.launch(debug=True, show_error=True)