seawolf2357 commited on
Commit
23934ad
·
verified ·
1 Parent(s): e98bce2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +355 -291
app.py CHANGED
@@ -16,11 +16,170 @@ groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
16
  ai_pick_storage = {"videos": [], "ratings": {}, "timestamp": None}
17
 
18
  # ============================================
19
- # 🎨 Comic Classic Theme CSS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  # ============================================
21
  css = """
22
  @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  .gradio-container {
25
  background-color: #FEF9C3 !important;
26
  background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
@@ -29,10 +188,6 @@ css = """
29
  font-family: 'Comic Neue', cursive, sans-serif !important;
30
  }
31
 
32
- footer, .footer, .gradio-container footer, .built-with, [class*="footer"] {
33
- display: none !important;
34
- }
35
-
36
  .header-text h1 {
37
  font-family: 'Bangers', cursive !important;
38
  color: #1F2937 !important;
@@ -93,6 +248,20 @@ label { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important
93
  font-family: 'Courier New', monospace !important;
94
  white-space: pre-wrap !important;
95
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  """
97
 
98
  # DB 초기화
@@ -147,9 +316,9 @@ COUNTRIES = {
147
  }
148
 
149
  LANGUAGES = {
150
- "Auto (by Country)": "", "English": "en", "Spanish": "es", "Portuguese": "pt",
151
- "French": "fr", "German": "de", "Italian": "it", "Russian": "ru",
152
- "Japanese": "ja", "Korean": "ko", "Chinese": "zh", "Hindi": "hi",
153
  "Arabic": "ar", "Turkish": "tr", "Indonesian": "id", "Vietnamese": "vi",
154
  "Thai": "th", "Dutch": "nl", "Polish": "pl", "Swedish": "sv",
155
  "Norwegian": "no", "Danish": "da", "Finnish": "fi", "Greek": "el",
@@ -157,8 +326,8 @@ LANGUAGES = {
157
  "Hungarian": "hu", "Filipino": "tl", "Malay": "ms",
158
  }
159
 
160
- SORT_OPTIONS = {"Most Viewed": "viewCount", "Latest": "date", "Relevance": "relevance", "Top Rated": "rating"}
161
- DATE_OPTIONS = {"All Time": "", "Today": "today", "This Week": "thisWeek", "This Month": "thisMonth", "This Year": "thisYear"}
162
 
163
  def format_count(count):
164
  if count >= 1000000000: return f"{count/1000000000:.1f}B"
@@ -167,18 +336,13 @@ def format_count(count):
167
  return str(count)
168
 
169
  def call_llm(prompt, max_tokens=2000):
170
- """Groq LLM API call"""
171
  if not groq_client:
172
  return None
173
  try:
174
  completion = groq_client.chat.completions.create(
175
  model="openai/gpt-oss-120b",
176
  messages=[{"role": "user", "content": prompt}],
177
- temperature=0.7,
178
- max_completion_tokens=max_tokens,
179
- top_p=1,
180
- stream=True,
181
- stop=None
182
  )
183
  result = ""
184
  for chunk in completion:
@@ -189,26 +353,14 @@ def call_llm(prompt, max_tokens=2000):
189
  return f"Error: {e}"
190
 
191
  def get_ai_pick_rating(videos_data):
192
- """AI Pick rating"""
193
  global ai_pick_storage
194
- if not videos_data:
195
- return {}
196
 
197
  if groq_client:
198
  try:
199
  sample = videos_data[:50]
200
- video_info = "\n".join([
201
- f"#{i+1}. {v['title'][:40]}, Views:{v['views']}, Likes:{v['likes']}, Subs:{v.get('subs',0)}"
202
- for i, v in enumerate(sample)
203
- ])
204
-
205
- prompt = f"""Rate YouTube videos 0-4. 0=None,1=♥,2=⭐,3=⭐⭐,4=⭐⭐⭐
206
- Consider engagement, viral potential. Format: 1:3,2:2,3:4
207
-
208
- {video_info}
209
-
210
- Response (number:rating only):"""
211
-
212
  result = call_llm(prompt, 1500)
213
  if result and "Error" not in result:
214
  ratings = {}
@@ -216,12 +368,10 @@ Response (number:rating only):"""
216
  ratings[int(idx)-1] = int(rating)
217
  if len(videos_data) > 50:
218
  local = calculate_local_rating(videos_data[50:])
219
- for k, v in local.items():
220
- ratings[k + 50] = v
221
  ai_pick_storage = {"videos": videos_data, "ratings": ratings, "timestamp": datetime.now().isoformat()}
222
  return ratings
223
- except:
224
- pass
225
 
226
  ratings = calculate_local_rating(videos_data)
227
  ai_pick_storage = {"videos": videos_data, "ratings": ratings, "timestamp": datetime.now().isoformat()}
@@ -240,10 +390,8 @@ def calculate_local_rating(videos_data):
240
  if views > 0:
241
  score += min(40, (views/max_views)*40)
242
  score += min(30, ((likes+comments*2)/views)*300)
243
- if subs > 0 and views > 0:
244
- score += min(30, (views/subs)*10)
245
- elif views > avg_views:
246
- score += 15
247
 
248
  if score >= 70: ratings[i] = 4
249
  elif score >= 50: ratings[i] = 3
@@ -262,16 +410,12 @@ def get_real_trending_keywords(region_code="US", language="en"):
262
  for item in response.get("items", []):
263
  for tag in item["snippet"].get("tags", [])[:3]:
264
  if tag.lower() not in seen and 2 <= len(tag) <= 20:
265
- keywords.append(tag)
266
- seen.add(tag.lower())
267
  channel = item["snippet"]["channelTitle"]
268
- if channel.lower() not in seen:
269
- keywords.append(channel)
270
- seen.add(channel.lower())
271
  if len(keywords) >= 20: break
272
  return keywords[:20] if keywords else ["AI","gaming","music","vlog","shorts","news"]
273
- except:
274
- return ["AI","ChatGPT","gaming","music","vlog","shorts","news","tech"]
275
 
276
  def save_to_db(videos_data, channels_data, keyword, country, language, sort_by):
277
  conn = sqlite3.connect("youtube_data.db")
@@ -290,18 +434,15 @@ def save_to_db(videos_data, channels_data, keyword, country, language, sort_by):
290
  c.execute('INSERT INTO trending_alerts VALUES (NULL,?,?,?,?,?,?)', (video['video_id'], 'views_surge', prev[0], video['views'], change, now))
291
  for ch_id, subs in channels_data.items():
292
  c.execute('INSERT OR IGNORE INTO channels VALUES (?,?,?)', (ch_id, '', now))
293
- if isinstance(subs, int):
294
- c.execute('INSERT INTO channel_stats VALUES (NULL,?,?,?)', (ch_id, subs, now))
295
- conn.commit()
296
- conn.close()
297
 
298
  def get_db_stats():
299
  conn = sqlite3.connect("youtube_data.db")
300
  c = conn.cursor()
301
  stats = {}
302
  for t, k in [("videos","videos"),("video_stats","stats"),("channels","channels"),("search_history","searches"),("trending_alerts","alerts")]:
303
- c.execute(f"SELECT COUNT(*) FROM {t}")
304
- stats[k] = c.fetchone()[0]
305
  conn.close()
306
  return stats
307
 
@@ -312,21 +453,26 @@ def update_trending(country):
312
  def use_trending_keyword(kw):
313
  return kw if kw else ""
314
 
315
- def search_videos(keyword, country, language, sort_by, date_filter, max_results):
 
316
  if not keyword or not keyword.strip():
317
- return "⚠️ Please enter a search keyword!", ""
318
 
319
  max_results = int(max_results)
320
  all_items, next_page = [], None
321
  region_code, default_lang = COUNTRIES.get(country, ("", ""))
322
- lang_code = default_lang if language == "Auto (by Country)" else LANGUAGES.get(language, "")
323
 
324
- params = {"q": keyword, "part": "snippet", "type": "video", "order": SORT_OPTIONS[sort_by]}
 
 
 
 
325
  if region_code: params["regionCode"] = region_code
326
  if lang_code: params["relevanceLanguage"] = lang_code
327
- if date_filter != "All Time":
328
- deltas = {"Today": 1, "This Week": 7, "This Month": 30, "This Year": 365}
329
- params["publishedAfter"] = (datetime.utcnow() - timedelta(days=deltas.get(date_filter, 0))).strftime("%Y-%m-%dT%H:%M:%SZ")
330
 
331
  while len(all_items) < max_results:
332
  params["maxResults"] = min(50, max_results - len(all_items))
@@ -338,7 +484,8 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
338
  next_page = resp.get("nextPageToken")
339
  if not next_page: break
340
 
341
- if not all_items: return "No results found.", ""
 
342
 
343
  video_ids = [item["id"]["videoId"] for item in all_items]
344
  channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
@@ -353,9 +500,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
353
  for i in range(0, len(channel_ids), 50):
354
  for ch in youtube.channels().list(id=",".join(channel_ids[i:i+50]), part="statistics").execute().get("items", []):
355
  sub = ch["statistics"].get("subscriberCount", "0")
356
- if sub:
357
- channel_subs_raw[ch["id"]] = int(sub)
358
- channel_subs[ch["id"]] = format_count(int(sub))
359
 
360
  videos_data = []
361
  for item in all_items:
@@ -391,29 +536,29 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
391
  .qsort button:hover{{background:#1F2937;color:#FACC15}}
392
  </style>
393
  <div class="res-hdr">
394
- 🎬 <b>{len(all_items)}</b> results | 🔍 "{keyword}" | 🌍 {country}
395
- <br><span style="font-size:0.9rem">🤖 AI Pick: ♥(avg)(good) ⭐⭐(great) ⭐⭐⭐(best)</span>
396
  <div style="margin-top:10px"><span style="font-size:0.9rem">⚡ Sort:</span>
397
  <span class="qsort">
398
- <button onclick="sortT_{table_id}(5,'n')">Views</button>
399
- <button onclick="sortT_{table_id}(6,'n')">Likes</button>
400
- <button onclick="sortT_{table_id}(4,'n')">Subs</button>
401
- <button onclick="sortT_{table_id}(8,'n')">AI Pick</button>
402
- <button onclick="sortT_{table_id}(9,'s')">Date</button>
403
  </span></div>
404
  </div>
405
  <div style="max-height:700px;overflow-y:auto;border:3px solid #1F2937;border-radius:8px">
406
  <table class="yt-tbl" id="{table_id}"><thead><tr>
407
- <th onclick="sortT_{table_id}(0,'n')" style="width:45px">Rank</th>
408
- <th style="width:130px">Thumb</th>
409
  <th onclick="sortT_{table_id}(2,'s')">Title</th>
410
- <th onclick="sortT_{table_id}(3,'s')" style="width:100px">Channel</th>
411
- <th onclick="sortT_{table_id}(4,'n')" style="width:65px">Subs</th>
412
- <th onclick="sortT_{table_id}(5,'n')" style="width:70px">Views</th>
413
- <th onclick="sortT_{table_id}(6,'n')" style="width:55px">Likes</th>
414
- <th onclick="sortT_{table_id}(7,'n')" style="width:50px">Cmts</th>
415
- <th onclick="sortT_{table_id}(8,'n')" style="width:70px">AI Pick</th>
416
- <th onclick="sortT_{table_id}(9,'s')" style="width:85px">Date</th>
417
  </tr></thead><tbody>'''
418
 
419
  for i, v in enumerate(videos_data):
@@ -454,8 +599,8 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
454
  stats = get_db_stats()
455
  return html, f"📊 DB: Videos {stats['videos']} | Records {stats['stats']} | Channels {stats['channels']} | Searches {stats['searches']}"
456
 
457
- def show_trending_alerts():
458
- """🔥 TRENDING - Videos with 20%+ SUDDEN view increase (Event-based alerts)"""
459
  conn = sqlite3.connect("youtube_data.db")
460
  c = conn.cursor()
461
  c.execute('''SELECT ta.video_id, v.title, v.channel_name, v.thumbnail, ta.old_value, ta.new_value, ta.change_percent, ta.detected_at
@@ -463,21 +608,21 @@ def show_trending_alerts():
463
  alerts = c.fetchall()
464
  conn.close()
465
 
466
- info_box = '''
467
  <div style="background:linear-gradient(135deg,#EF4444,#DC2626);color:#fff;padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
468
- <h2 style="font-family:'Bangers',cursive;margin:0 0 10px 0">🔥 TRENDING - Sudden Surge Detection</h2>
469
  <table style="width:100%;color:#fff;font-size:14px">
470
- <tr><td style="width:120px"><b>📌 What is this?</b></td><td>Videos with <b>20%+ sudden view increase</b> compared to previous check</td></tr>
471
- <tr><td><b>🎯 Purpose</b></td><td>Catch <b>viral moments</b> and <b>breaking content</b> in real-time</td></tr>
472
- <tr><td><b>⏱️ Trigger</b></td><td>When same video is searched again and views jumped 20%+</td></tr>
473
- <tr><td><b>💡 Best for</b></td><td>Finding videos going viral RIGHT NOW, news events, trending moments</td></tr>
474
  </table>
475
  </div>'''
476
 
477
  if not alerts:
478
  try:
479
  resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="US", maxResults=20).execute()
480
- html = info_box + '<p style="font-family:Comic Neue,cursive;margin-bottom:15px">📢 No surge alerts yet. Showing current popular videos. <b>Run searches multiple times</b> to detect surges!</p><div style="display:flex;flex-wrap:wrap;gap:15px">'
481
  for i, item in enumerate(resp.get("items", [])[:20], 1):
482
  snip, stats = item["snippet"], item["statistics"]
483
  title = snip["title"][:35] + "..." if len(snip["title"]) > 35 else snip["title"]
@@ -502,8 +647,8 @@ def show_trending_alerts():
502
  </div>'''
503
  return html + '</div>'
504
 
505
- def show_top_growing():
506
- """📈 TOP GROWING - Highest growth rate over 48 hours (Ranking-based)"""
507
  conn = sqlite3.connect("youtube_data.db")
508
  c = conn.cursor()
509
  cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
@@ -516,21 +661,21 @@ def show_top_growing():
516
  results = c.fetchall()
517
  conn.close()
518
 
519
- info_box = '''
520
  <div style="background:linear-gradient(135deg,#3B82F6,#2563EB);color:#fff;padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
521
- <h2 style="font-family:'Bangers',cursive;margin:0 0 10px 0">📈 TOP GROWING - 48h Growth Champions</h2>
522
  <table style="width:100%;color:#fff;font-size:14px">
523
- <tr><td style="width:120px"><b>📌 What is this?</b></td><td>Videos ranked by <b>highest growth RATE over 48 hours</b></td></tr>
524
- <tr><td><b>🎯 Purpose</b></td><td>Find <b>consistently rising</b> content with sustained momentum</td></tr>
525
- <tr><td><b>📊 Calculation</b></td><td>(Max Views - Min Views) / Min Views × 100% over 48h period</td></tr>
526
- <tr><td><b>💡 Best for</b></td><td>Discovering steady performers, evergreen content, reliable trends</td></tr>
527
  </table>
528
  </div>'''
529
 
530
  if not results:
531
  try:
532
  resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="US", maxResults=20).execute()
533
- html = info_box + '<p style="font-family:Comic Neue,cursive;margin-bottom:15px">📢 No growth data yet. <b>Run searches over time</b> to accumulate data. Showing current popular:</p><div style="display:flex;flex-wrap:wrap;gap:15px">'
534
  for i, item in enumerate(resp.get("items", [])[:20], 1):
535
  snip, stats = item["snippet"], item["statistics"]
536
  views, likes = int(stats.get("viewCount", 0)), int(stats.get("likeCount", 0))
@@ -559,11 +704,11 @@ def show_top_growing():
559
  </div>'''
560
  return html + '</div>'
561
 
562
- def show_ai_picks():
563
- """⭐ AI Pick - Top rated videos with LLM analysis"""
564
  global ai_pick_storage
565
  if not ai_pick_storage["videos"]:
566
- return '<div style="background:#FACC15;padding:20px;border:3px solid #1F2937;border-radius:8px;text-align:center;font-family:Comic Neue,cursive"><h2 style="font-family:Bangers,cursive">⭐ AI PICK - No Data Yet</h2><p>Run a search first to see AI-curated picks!</p></div>'
567
 
568
  videos, ratings = ai_pick_storage["videos"], ai_pick_storage["ratings"]
569
  top_picks = [(i, v, ratings.get(i, 0)) for i, v in enumerate(videos) if ratings.get(i, 0) >= 3]
@@ -572,13 +717,14 @@ def show_ai_picks():
572
  analysis = ""
573
  if groq_client and top_picks:
574
  info = "\n".join([f"- {v['title'][:50]} (Views:{format_count(v['views'])})" for _, v, _ in top_picks[:5]])
575
- result = call_llm(f"Analyze these top YouTube videos in 3-4 sentences:\n{info}\n\nProvide: 1) Common theme 2) Why popular 3) Content opportunity", 500)
 
576
  if result and "Error" not in result:
577
- analysis = f'<div style="background:#1F2937;color:#10B981;padding:15px;border:3px solid #10B981;border-radius:8px;margin:15px 0;font-family:Courier New,monospace"><h4 style="color:#FACC15;margin:0 0 10px">🤖 AI ANALYSIS</h4><p style="margin:0;font-size:14px;line-height:1.6">{result}</p></div>'
578
 
579
  html = f'''<div style="background:linear-gradient(135deg,#FACC15,#EF4444);padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
580
- <h2 style="font-family:Bangers,cursive;color:#1F2937;margin:0;text-shadow:2px 2px 0 #FFF">⭐ AI PICK - TOP RECOMMENDATIONS</h2>
581
- <p style="color:#1F2937;margin:5px 0 0;font-family:Comic Neue,cursive;font-weight:700">Showing {len(top_picks)} videos with ⭐⭐+ rating</p>
582
  </div>{analysis}<div style="display:flex;flex-wrap:wrap;gap:20px">'''
583
 
584
  for idx, (_, v, rating) in enumerate(top_picks[:30], 1):
@@ -594,165 +740,41 @@ def show_ai_picks():
594
  <span style="color:#EF4444">❤️ {format_count(v['likes'])}</span>
595
  </div></div>'''
596
 
597
- return html + '</div>' if top_picks else html + '<p style="text-align:center;padding:30px">No ⭐⭐+ rated videos. Try different keywords!</p></div>'
598
-
599
- # ============================================
600
- # 🤖 LLM Analysis Functions
601
- # ============================================
602
-
603
- def analyze_keyword_suggest(keyword):
604
- """Keyword Suggestion based on input"""
605
- if not keyword:
606
- return "⚠️ Please enter a keyword first!"
607
- if not groq_client:
608
- return "⚠️ LLM API not configured. Set GROQ_API_KEY environment variable."
609
-
610
- prompt = f"""You are a YouTube SEO expert. Given the keyword "{keyword}", suggest 15 related keywords that could perform well on YouTube.
611
-
612
- For each keyword, provide:
613
- 1. The keyword
614
- 2. Estimated search volume (High/Medium/Low)
615
- 3. Competition level (High/Medium/Low)
616
- 4. Content type suggestion (Tutorial/Review/Vlog/etc)
617
-
618
- Format as a clear list. Be specific and actionable."""
619
-
620
- result = call_llm(prompt, 1500)
621
- return result if result else "Error generating suggestions"
622
-
623
- def analyze_trend_prediction(keyword):
624
- """Trend Prediction based on keyword"""
625
- if not keyword:
626
- return "⚠️ Please enter a keyword first!"
627
- if not groq_client:
628
- return "⚠️ LLM API not configured."
629
-
630
- prompt = f"""You are a trend analyst. Analyze the YouTube trend potential for "{keyword}".
631
-
632
- Provide:
633
- 1. **Current Trend Status**: Is it rising, stable, or declining?
634
- 2. **Peak Season**: When does this topic typically peak? (months/events)
635
- 3. **6-Month Forecast**: Predicted trajectory with reasoning
636
- 4. **Risk Factors**: What could affect this trend negatively?
637
- 5. **Opportunity Windows**: Best times to publish content
638
- 6. **Related Emerging Topics**: What related topics are gaining momentum?
639
-
640
- Be specific with data-driven reasoning."""
641
-
642
- result = call_llm(prompt, 1500)
643
- return result if result else "Error generating prediction"
644
-
645
- def analyze_content_ideas(keyword):
646
- """Content Ideas generation"""
647
- if not keyword:
648
- return "⚠️ Please enter a keyword first!"
649
- if not groq_client:
650
- return "⚠️ LLM API not configured."
651
-
652
- prompt = f"""You are a creative YouTube content strategist. Generate 10 unique video ideas for the topic "{keyword}".
653
-
654
- For each idea provide:
655
- 1. **Video Title** (clickable, SEO-optimized)
656
- 2. **Hook** (first 5 seconds script)
657
- 3. **Format** (Tutorial/Listicle/Challenge/Story/etc)
658
- 4. **Target Length** (Short <1min / Medium 5-10min / Long 15+min)
659
- 5. **Thumbnail Concept** (brief visual description)
660
- 6. **Viral Potential** (1-10 score with reason)
661
-
662
- Make ideas diverse, creative, and actionable."""
663
-
664
- result = call_llm(prompt, 2000)
665
- return result if result else "Error generating ideas"
666
-
667
- def analyze_channel(channel_url):
668
- """Channel Analysis"""
669
- if not channel_url:
670
- return "⚠️ Please enter a channel name or URL!"
671
- if not groq_client:
672
- return "⚠️ LLM API not configured."
673
-
674
- # Extract channel info if possible
675
- channel_name = channel_url.split("/")[-1] if "/" in channel_url else channel_url
676
-
677
- prompt = f"""You are a YouTube channel growth consultant. Analyze a channel focused on or named "{channel_name}".
678
-
679
- Provide a comprehensive analysis:
680
-
681
- 1. **Niche Assessment**
682
- - Target audience profile
683
- - Content saturation level
684
- - Growth potential in this niche
685
-
686
- 2. **Content Strategy Recommendations**
687
- - Optimal posting frequency
688
- - Best content formats for this niche
689
- - Trending topics to cover
690
-
691
- 3. **Growth Tactics**
692
- - Quick wins (immediate actions)
693
- - Medium-term strategies (1-3 months)
694
- - Long-term positioning (6-12 months)
695
-
696
- 4. **Monetization Opportunities**
697
- - Ad revenue potential
698
- - Sponsorship opportunities
699
- - Alternative revenue streams
700
-
701
- 5. **Competitive Advantages to Build**
702
- - Unique value propositions
703
- - Differentiation strategies
704
-
705
- Be specific and actionable."""
706
-
707
- result = call_llm(prompt, 2000)
708
- return result if result else "Error analyzing channel"
709
-
710
- def analyze_competitor(my_channel, competitor):
711
- """Competitor Analysis"""
712
- if not my_channel or not competitor:
713
- return "⚠️ Please enter both your channel/topic and competitor!"
714
- if not groq_client:
715
- return "⚠️ LLM API not configured."
716
-
717
- prompt = f"""You are a competitive intelligence analyst for YouTube.
718
- Compare "{my_channel}" against competitor "{competitor}".
719
-
720
- Provide detailed competitive analysis:
721
-
722
- 1. **Positioning Comparison**
723
- - Content focus differences
724
- - Target audience overlap
725
- - Brand positioning
726
-
727
- 2. **Content Gap Analysis**
728
- - Topics competitor covers that you don't
729
- - Your unique content opportunities
730
- - Underserved audience needs
731
-
732
- 3. **Performance Benchmarks**
733
- - Typical metrics to aim for
734
- - Engagement rate targets
735
- - Growth rate expectations
736
-
737
- 4. **Competitive Advantages**
738
- - Competitor's strengths to learn from
739
- - Competitor's weaknesses to exploit
740
- - Your potential differentiators
741
-
742
- 5. **Action Plan**
743
- - Immediate actions (this week)
744
- - Short-term goals (this month)
745
- - Strategic moves (this quarter)
746
-
747
- 6. **Content Ideas to Outperform**
748
- - 5 specific video ideas that could beat competitor content
749
-
750
- Be strategic and actionable."""
751
-
752
- result = call_llm(prompt, 2000)
753
- return result if result else "Error analyzing competitor"
754
-
755
- def show_search_history():
756
  conn = sqlite3.connect("youtube_data.db")
757
  c = conn.cursor()
758
  c.execute('SELECT keyword,country,language,sort_by,results_count,searched_at FROM search_history ORDER BY searched_at DESC LIMIT 50')
@@ -760,15 +782,18 @@ def show_search_history():
760
  conn.close()
761
 
762
  if not history:
763
- return "<p style='font-family:Comic Neue,cursive'>No search history yet.</p>"
764
 
765
- html = '''<div style="background:#1F2937;color:#FACC15;padding:15px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
766
- <h3 style="font-family:Bangers,cursive;margin:0">🕐 SEARCH HISTORY</h3></div>
767
  <table style="width:100%;border-collapse:collapse;font-family:Comic Neue,cursive">
768
  <thead><tr style="background:#FACC15;color:#1F2937">
769
- <th style="padding:12px;border:2px solid #1F2937">Keyword</th><th style="padding:12px">Country</th>
770
- <th style="padding:12px">Language</th><th style="padding:12px">Sort</th>
771
- <th style="padding:12px">Results</th><th style="padding:12px">Time</th>
 
 
 
772
  </tr></thead><tbody>'''
773
 
774
  for kw, country, lang, sort_by, cnt, searched in history:
@@ -780,6 +805,23 @@ def show_search_history():
780
 
781
  return html + '</tbody></table>'
782
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
783
  # Initial trending
784
  initial_trending = get_real_trending_keywords("US", "en")
785
 
@@ -787,13 +829,29 @@ initial_trending = get_real_trending_keywords("US", "en")
787
  # 🎨 Gradio UI
788
  # ============================================
789
  with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
 
 
 
790
  gr.HTML('''<div style="text-align:center;margin:20px 0">
791
  <a href="https://www.humangen.ai" target="_blank">
792
  <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge">
793
  </a></div>''')
794
 
795
- gr.Markdown("# 🎬 YOUTUBE TREND ANALYZER 📊", elem_classes="header-text")
796
- db_stats = gr.Markdown("📊 Loading DB stats...")
 
 
 
 
 
 
 
 
 
 
 
 
 
797
 
798
  with gr.Tabs():
799
  # Tab 1: Search
@@ -807,8 +865,8 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
807
  with gr.Row():
808
  country = gr.Dropdown(list(COUNTRIES.keys()), value="United States", label="Country")
809
  language = gr.Dropdown(list(LANGUAGES.keys()), value="Auto (by Country)", label="Language")
810
- sort_by = gr.Dropdown(list(SORT_OPTIONS.keys()), value="Most Viewed", label="Sort By")
811
- date_filter = gr.Dropdown(list(DATE_OPTIONS.keys()), value="All Time", label="Period")
812
  max_results = gr.Slider(10, 1000, value=100, step=10, label="Max Results")
813
  output = gr.HTML()
814
 
@@ -833,37 +891,31 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
833
  # Tab 5: AI Tools
834
  with gr.Tab("🤖 AI Tools"):
835
  gr.Markdown("### 🧠 LLM-Powered Analysis Tools (GPT-OSS-120B)")
836
-
837
  with gr.Tabs():
838
- # Keyword Suggest
839
  with gr.Tab("🏷️ Keyword Suggest"):
840
  gr.Markdown("**Generate related keywords with SEO insights**")
841
  kw_input = gr.Textbox(label="Enter base keyword", placeholder="e.g., Python tutorial")
842
  kw_btn = gr.Button("🔍 Generate Keywords", variant="primary")
843
  kw_output = gr.Textbox(label="Suggested Keywords", lines=20, elem_classes="llm-result")
844
 
845
- # Trend Prediction
846
  with gr.Tab("🔮 Trend Prediction"):
847
  gr.Markdown("**Predict future trend trajectory**")
848
  tp_input = gr.Textbox(label="Enter topic/keyword", placeholder="e.g., AI tools")
849
  tp_btn = gr.Button("🔮 Predict Trend", variant="primary")
850
  tp_output = gr.Textbox(label="Trend Analysis", lines=20, elem_classes="llm-result")
851
 
852
- # Content Ideas
853
  with gr.Tab("💡 Content Ideas"):
854
  gr.Markdown("**Generate creative video ideas**")
855
  ci_input = gr.Textbox(label="Enter topic", placeholder="e.g., Home workout")
856
  ci_btn = gr.Button("💡 Generate Ideas", variant="primary")
857
  ci_output = gr.Textbox(label="Content Ideas", lines=25, elem_classes="llm-result")
858
 
859
- # Channel Analysis
860
  with gr.Tab("📊 Channel Analysis"):
861
  gr.Markdown("**Get growth strategy for your channel**")
862
  ca_input = gr.Textbox(label="Enter channel name or niche", placeholder="e.g., Tech reviews")
863
  ca_btn = gr.Button("📊 Analyze Channel", variant="primary")
864
  ca_output = gr.Textbox(label="Channel Analysis", lines=25, elem_classes="llm-result")
865
 
866
- # Competitor Analysis
867
  with gr.Tab("⚔️ Competitor Analysis"):
868
  gr.Markdown("**Compare against competitors**")
869
  with gr.Row():
@@ -877,24 +929,36 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
877
  history_btn = gr.Button("🔄 Refresh", variant="primary")
878
  history_out = gr.HTML()
879
 
 
 
 
 
 
 
 
 
 
 
 
 
880
  # Events
881
  trending.change(use_trending_keyword, trending, keyword)
882
  country.change(update_trending, country, trending)
883
- btn.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results], [output, db_stats])
884
- keyword.submit(search_videos, [keyword, country, language, sort_by, date_filter, max_results], [output, db_stats])
885
- refresh.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results], [output, db_stats])
886
 
887
- pick_btn.click(show_ai_picks, outputs=pick_out)
888
- alerts_btn.click(show_trending_alerts, outputs=alerts_out)
889
- growing_btn.click(show_top_growing, outputs=growing_out)
890
- history_btn.click(show_search_history, outputs=history_out)
891
 
892
  # AI Tools events
893
- kw_btn.click(analyze_keyword_suggest, kw_input, kw_output)
894
- tp_btn.click(analyze_trend_prediction, tp_input, tp_output)
895
- ci_btn.click(analyze_content_ideas, ci_input, ci_output)
896
- ca_btn.click(analyze_channel, ca_input, ca_output)
897
- comp_btn.click(analyze_competitor, [comp_my, comp_rival], comp_output)
898
 
899
  demo.launch()
900
 
 
16
  ai_pick_storage = {"videos": [], "ratings": {}, "timestamp": None}
17
 
18
  # ============================================
19
+ # 🌐 UI Language Translations
20
+ # ============================================
21
+ UI_LANG = {
22
+ "en": {
23
+ "title": "🎬 YOUTUBE TREND ANALYZER 📊",
24
+ "trending_keywords": "### 🔥 Trending Keywords",
25
+ "click_autofill": "Click to auto-fill",
26
+ "search_keyword": "Search Keyword",
27
+ "enter_keyword": "Enter keyword...",
28
+ "search": "🔍 SEARCH",
29
+ "refresh": "🔄 Refresh",
30
+ "country": "Country",
31
+ "language": "Language",
32
+ "sort_by": "Sort By",
33
+ "period": "Period",
34
+ "max_results": "Max Results",
35
+ "tab_search": "🔍 Search",
36
+ "tab_ai_pick": "⭐ AI Pick",
37
+ "tab_trending": "🔥 Trending",
38
+ "tab_growing": "📈 Top Growing",
39
+ "tab_ai_tools": "🤖 AI Tools",
40
+ "tab_history": "🕐 History",
41
+ "ai_pick_desc": "### 🤖 AI-Curated Top Recommendations (⭐⭐ and above)",
42
+ "trending_desc": "### 🔥 Sudden Surge Detection (20%+ view increase)",
43
+ "growing_desc": "### 📈 48-Hour Growth Champions (Highest growth rate)",
44
+ "ai_tools_desc": "### 🧠 LLM-Powered Analysis Tools (GPT-OSS-120B)",
45
+ "keyword_suggest": "🏷️ Keyword Suggest",
46
+ "trend_prediction": "🔮 Trend Prediction",
47
+ "content_ideas": "💡 Content Ideas",
48
+ "channel_analysis": "📊 Channel Analysis",
49
+ "competitor_analysis": "⚔️ Competitor Analysis",
50
+ "enter_base_keyword": "Enter base keyword",
51
+ "enter_topic": "Enter topic/keyword",
52
+ "enter_channel": "Enter channel name or niche",
53
+ "your_channel": "Your Channel/Topic",
54
+ "competitor": "Competitor",
55
+ "generate_keywords": "🔍 Generate Keywords",
56
+ "predict_trend": "🔮 Predict Trend",
57
+ "generate_ideas": "💡 Generate Ideas",
58
+ "analyze_channel": "📊 Analyze Channel",
59
+ "analyze_competition": "⚔️ Analyze Competition",
60
+ "no_data": "No data yet. Run a search first!",
61
+ "results": "results",
62
+ "total": "Total",
63
+ "views": "Views",
64
+ "likes": "Likes",
65
+ "subs": "Subs",
66
+ "date": "Date",
67
+ "rank": "Rank",
68
+ "thumb": "Thumb",
69
+ "channel": "Channel",
70
+ "comments": "Cmts",
71
+ "ai_pick_col": "AI Pick",
72
+ "sort_options": {"Most Viewed": "viewCount", "Latest": "date", "Relevance": "relevance", "Top Rated": "rating"},
73
+ "date_options": {"All Time": "", "Today": "today", "This Week": "thisWeek", "This Month": "thisMonth", "This Year": "thisYear"},
74
+ },
75
+ "ko": {
76
+ "title": "🎬 유튜브 트렌드 분석기 📊",
77
+ "trending_keywords": "### 🔥 실시간 인기 키워드",
78
+ "click_autofill": "클릭하면 자동 입력",
79
+ "search_keyword": "검색어",
80
+ "enter_keyword": "검색어 입력...",
81
+ "search": "🔍 검색",
82
+ "refresh": "🔄 새로고침",
83
+ "country": "국가",
84
+ "language": "언어",
85
+ "sort_by": "정렬",
86
+ "period": "기간",
87
+ "max_results": "최대 결과",
88
+ "tab_search": "🔍 검색",
89
+ "tab_ai_pick": "⭐ AI 추천",
90
+ "tab_trending": "🔥 급상승",
91
+ "tab_growing": "📈 급성장",
92
+ "tab_ai_tools": "🤖 AI 도구",
93
+ "tab_history": "🕐 기록",
94
+ "ai_pick_desc": "### 🤖 AI 추천 영상 (⭐⭐ 이상)",
95
+ "trending_desc": "### 🔥 급상승 감지 (조회수 20%+ 급증)",
96
+ "growing_desc": "### 📈 48시간 급성장 TOP (최고 성장률)",
97
+ "ai_tools_desc": "### 🧠 LLM 기반 분석 도구 (GPT-OSS-120B)",
98
+ "keyword_suggest": "🏷️ 키워드 추천",
99
+ "trend_prediction": "🔮 트렌드 예측",
100
+ "content_ideas": "💡 콘텐츠 아이디어",
101
+ "channel_analysis": "📊 채널 분석",
102
+ "competitor_analysis": "⚔️ 경쟁 분석",
103
+ "enter_base_keyword": "기본 키워드 입력",
104
+ "enter_topic": "주제/키워드 입력",
105
+ "enter_channel": "채널명 또는 주제 입력",
106
+ "your_channel": "내 채널/주제",
107
+ "competitor": "경쟁자",
108
+ "generate_keywords": "🔍 키워드 생성",
109
+ "predict_trend": "🔮 트렌드 예측",
110
+ "generate_ideas": "💡 아이디어 생성",
111
+ "analyze_channel": "📊 채널 분석",
112
+ "analyze_competition": "⚔️ 경쟁 분석",
113
+ "no_data": "데이터 없음. 먼저 검색하세요!",
114
+ "results": "개 결과",
115
+ "total": "총",
116
+ "views": "조회수",
117
+ "likes": "좋아요",
118
+ "subs": "구독자",
119
+ "date": "날짜",
120
+ "rank": "순위",
121
+ "thumb": "썸네일",
122
+ "channel": "채널",
123
+ "comments": "댓글",
124
+ "ai_pick_col": "AI 추천",
125
+ "sort_options": {"조회수 순": "viewCount", "최신순": "date", "관련성 순": "relevance", "평점 순": "rating"},
126
+ "date_options": {"전체 기간": "", "오늘": "today", "이번 주": "thisWeek", "이번 달": "thisMonth", "올해": "thisYear"},
127
+ }
128
+ }
129
+
130
+ # ============================================
131
+ # 🎨 Comic Classic Theme CSS + Hide HF Elements
132
  # ============================================
133
  css = """
134
  @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
135
 
136
+ /* ===== Hide Hugging Face Header/Footer/Buttons ===== */
137
+ #space-header,
138
+ .space-header,
139
+ header.svelte-1ax1toq,
140
+ .huggingface-space-header,
141
+ [data-testid="space-header"],
142
+ .svelte-1ed2p3z,
143
+ .svelte-kqij2n,
144
+ .gradio-container > header,
145
+ div.svelte-1kyws56,
146
+ .wrap.svelte-1kyws56,
147
+ button.svelte-1kyws56,
148
+ .duplicate-button,
149
+ .settings-button,
150
+ [class*="settings"],
151
+ [class*="duplicate"],
152
+ a[href*="huggingface.co/spaces"][target="_blank"],
153
+ .embed-buttons,
154
+ .buttons-container,
155
+ div[class*="header"] > button,
156
+ header button,
157
+ .gr-button-icon,
158
+ footer,
159
+ .footer,
160
+ .gradio-container footer,
161
+ .built-with,
162
+ [class*="footer"],
163
+ .built-with-gradio,
164
+ a[href*="gradio.app"] {
165
+ display: none !important;
166
+ visibility: hidden !important;
167
+ height: 0 !important;
168
+ width: 0 !important;
169
+ padding: 0 !important;
170
+ margin: 0 !important;
171
+ overflow: hidden !important;
172
+ opacity: 0 !important;
173
+ pointer-events: none !important;
174
+ }
175
+
176
+ /* Force hide top-right buttons */
177
+ .gradio-container > div:first-child button,
178
+ .gradio-container > div > button {
179
+ display: none !important;
180
+ }
181
+
182
+ /* ===== Main Theme ===== */
183
  .gradio-container {
184
  background-color: #FEF9C3 !important;
185
  background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
 
188
  font-family: 'Comic Neue', cursive, sans-serif !important;
189
  }
190
 
 
 
 
 
191
  .header-text h1 {
192
  font-family: 'Bangers', cursive !important;
193
  color: #1F2937 !important;
 
248
  font-family: 'Courier New', monospace !important;
249
  white-space: pre-wrap !important;
250
  }
251
+
252
+ .lang-selector {
253
+ position: fixed !important;
254
+ top: 10px !important;
255
+ right: 10px !important;
256
+ z-index: 9999 !important;
257
+ background: #FACC15 !important;
258
+ border: 3px solid #1F2937 !important;
259
+ border-radius: 8px !important;
260
+ padding: 5px 10px !important;
261
+ font-family: 'Comic Neue', cursive !important;
262
+ font-weight: 700 !important;
263
+ box-shadow: 3px 3px 0 #1F2937 !important;
264
+ }
265
  """
266
 
267
  # DB 초기화
 
316
  }
317
 
318
  LANGUAGES = {
319
+ "Auto (by Country)": "", "English": "en", "Korean": "ko", "Spanish": "es",
320
+ "Portuguese": "pt", "French": "fr", "German": "de", "Italian": "it",
321
+ "Russian": "ru", "Japanese": "ja", "Chinese": "zh", "Hindi": "hi",
322
  "Arabic": "ar", "Turkish": "tr", "Indonesian": "id", "Vietnamese": "vi",
323
  "Thai": "th", "Dutch": "nl", "Polish": "pl", "Swedish": "sv",
324
  "Norwegian": "no", "Danish": "da", "Finnish": "fi", "Greek": "el",
 
326
  "Hungarian": "hu", "Filipino": "tl", "Malay": "ms",
327
  }
328
 
329
+ SORT_OPTIONS_MAP = {"viewCount": "viewCount", "date": "date", "relevance": "relevance", "rating": "rating"}
330
+ DATE_OPTIONS_MAP = {"": "", "today": "today", "thisWeek": "thisWeek", "thisMonth": "thisMonth", "thisYear": "thisYear"}
331
 
332
  def format_count(count):
333
  if count >= 1000000000: return f"{count/1000000000:.1f}B"
 
336
  return str(count)
337
 
338
  def call_llm(prompt, max_tokens=2000):
 
339
  if not groq_client:
340
  return None
341
  try:
342
  completion = groq_client.chat.completions.create(
343
  model="openai/gpt-oss-120b",
344
  messages=[{"role": "user", "content": prompt}],
345
+ temperature=0.7, max_completion_tokens=max_tokens, top_p=1, stream=True, stop=None
 
 
 
 
346
  )
347
  result = ""
348
  for chunk in completion:
 
353
  return f"Error: {e}"
354
 
355
  def get_ai_pick_rating(videos_data):
 
356
  global ai_pick_storage
357
+ if not videos_data: return {}
 
358
 
359
  if groq_client:
360
  try:
361
  sample = videos_data[:50]
362
+ video_info = "\n".join([f"#{i+1}. {v['title'][:40]}, Views:{v['views']}, Likes:{v['likes']}, Subs:{v.get('subs',0)}" for i, v in enumerate(sample)])
363
+ prompt = f"Rate YouTube videos 0-4. 0=None,1=♥,2=⭐,3=⭐⭐,4=⭐⭐⭐\nConsider engagement, viral potential. Format: 1:3,2:2,3:4\n\n{video_info}\n\nResponse (number:rating only):"
 
 
 
 
 
 
 
 
 
 
364
  result = call_llm(prompt, 1500)
365
  if result and "Error" not in result:
366
  ratings = {}
 
368
  ratings[int(idx)-1] = int(rating)
369
  if len(videos_data) > 50:
370
  local = calculate_local_rating(videos_data[50:])
371
+ for k, v in local.items(): ratings[k + 50] = v
 
372
  ai_pick_storage = {"videos": videos_data, "ratings": ratings, "timestamp": datetime.now().isoformat()}
373
  return ratings
374
+ except: pass
 
375
 
376
  ratings = calculate_local_rating(videos_data)
377
  ai_pick_storage = {"videos": videos_data, "ratings": ratings, "timestamp": datetime.now().isoformat()}
 
390
  if views > 0:
391
  score += min(40, (views/max_views)*40)
392
  score += min(30, ((likes+comments*2)/views)*300)
393
+ if subs > 0 and views > 0: score += min(30, (views/subs)*10)
394
+ elif views > avg_views: score += 15
 
 
395
 
396
  if score >= 70: ratings[i] = 4
397
  elif score >= 50: ratings[i] = 3
 
410
  for item in response.get("items", []):
411
  for tag in item["snippet"].get("tags", [])[:3]:
412
  if tag.lower() not in seen and 2 <= len(tag) <= 20:
413
+ keywords.append(tag); seen.add(tag.lower())
 
414
  channel = item["snippet"]["channelTitle"]
415
+ if channel.lower() not in seen: keywords.append(channel); seen.add(channel.lower())
 
 
416
  if len(keywords) >= 20: break
417
  return keywords[:20] if keywords else ["AI","gaming","music","vlog","shorts","news"]
418
+ except: return ["AI","ChatGPT","gaming","music","vlog","shorts","news","tech"]
 
419
 
420
  def save_to_db(videos_data, channels_data, keyword, country, language, sort_by):
421
  conn = sqlite3.connect("youtube_data.db")
 
434
  c.execute('INSERT INTO trending_alerts VALUES (NULL,?,?,?,?,?,?)', (video['video_id'], 'views_surge', prev[0], video['views'], change, now))
435
  for ch_id, subs in channels_data.items():
436
  c.execute('INSERT OR IGNORE INTO channels VALUES (?,?,?)', (ch_id, '', now))
437
+ if isinstance(subs, int): c.execute('INSERT INTO channel_stats VALUES (NULL,?,?,?)', (ch_id, subs, now))
438
+ conn.commit(); conn.close()
 
 
439
 
440
  def get_db_stats():
441
  conn = sqlite3.connect("youtube_data.db")
442
  c = conn.cursor()
443
  stats = {}
444
  for t, k in [("videos","videos"),("video_stats","stats"),("channels","channels"),("search_history","searches"),("trending_alerts","alerts")]:
445
+ c.execute(f"SELECT COUNT(*) FROM {t}"); stats[k] = c.fetchone()[0]
 
446
  conn.close()
447
  return stats
448
 
 
453
  def use_trending_keyword(kw):
454
  return kw if kw else ""
455
 
456
+ def search_videos(keyword, country, language, sort_by, date_filter, max_results, ui_lang):
457
+ L = UI_LANG.get(ui_lang, UI_LANG["en"])
458
  if not keyword or not keyword.strip():
459
+ return f"⚠️ {'검색어를 입력하세요!' if ui_lang == 'ko' else 'Please enter a search keyword!'}", ""
460
 
461
  max_results = int(max_results)
462
  all_items, next_page = [], None
463
  region_code, default_lang = COUNTRIES.get(country, ("", ""))
464
+ lang_code = default_lang if language in ["Auto (by Country)", "자동 (국가 기반)"] else LANGUAGES.get(language, "")
465
 
466
+ # Map sort/date options
467
+ sort_value = L["sort_options"].get(sort_by, "viewCount")
468
+ date_value = L["date_options"].get(date_filter, "")
469
+
470
+ params = {"q": keyword, "part": "snippet", "type": "video", "order": sort_value}
471
  if region_code: params["regionCode"] = region_code
472
  if lang_code: params["relevanceLanguage"] = lang_code
473
+ if date_value:
474
+ deltas = {"today": 1, "thisWeek": 7, "thisMonth": 30, "thisYear": 365}
475
+ params["publishedAfter"] = (datetime.utcnow() - timedelta(days=deltas.get(date_value, 0))).strftime("%Y-%m-%dT%H:%M:%SZ")
476
 
477
  while len(all_items) < max_results:
478
  params["maxResults"] = min(50, max_results - len(all_items))
 
484
  next_page = resp.get("nextPageToken")
485
  if not next_page: break
486
 
487
+ if not all_items:
488
+ return f"{'검색 결과 없음' if ui_lang == 'ko' else 'No results found.'}", ""
489
 
490
  video_ids = [item["id"]["videoId"] for item in all_items]
491
  channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
 
500
  for i in range(0, len(channel_ids), 50):
501
  for ch in youtube.channels().list(id=",".join(channel_ids[i:i+50]), part="statistics").execute().get("items", []):
502
  sub = ch["statistics"].get("subscriberCount", "0")
503
+ if sub: channel_subs_raw[ch["id"]] = int(sub); channel_subs[ch["id"]] = format_count(int(sub))
 
 
504
 
505
  videos_data = []
506
  for item in all_items:
 
536
  .qsort button:hover{{background:#1F2937;color:#FACC15}}
537
  </style>
538
  <div class="res-hdr">
539
+ 🎬 {L["total"]} <b>{len(all_items)}</b> {L["results"]} | 🔍 "{keyword}" | 🌍 {country}
540
+ <br><span style="font-size:0.9rem">🤖 AI Pick: ♥ ⭐ ⭐⭐ ⭐⭐⭐</span>
541
  <div style="margin-top:10px"><span style="font-size:0.9rem">⚡ Sort:</span>
542
  <span class="qsort">
543
+ <button onclick="sortT_{table_id}(5,'n')">{L["views"]}</button>
544
+ <button onclick="sortT_{table_id}(6,'n')">{L["likes"]}</button>
545
+ <button onclick="sortT_{table_id}(4,'n')">{L["subs"]}</button>
546
+ <button onclick="sortT_{table_id}(8,'n')">AI</button>
547
+ <button onclick="sortT_{table_id}(9,'s')">{L["date"]}</button>
548
  </span></div>
549
  </div>
550
  <div style="max-height:700px;overflow-y:auto;border:3px solid #1F2937;border-radius:8px">
551
  <table class="yt-tbl" id="{table_id}"><thead><tr>
552
+ <th onclick="sortT_{table_id}(0,'n')" style="width:45px">{L["rank"]}</th>
553
+ <th style="width:130px">{L["thumb"]}</th>
554
  <th onclick="sortT_{table_id}(2,'s')">Title</th>
555
+ <th onclick="sortT_{table_id}(3,'s')" style="width:100px">{L["channel"]}</th>
556
+ <th onclick="sortT_{table_id}(4,'n')" style="width:65px">{L["subs"]}</th>
557
+ <th onclick="sortT_{table_id}(5,'n')" style="width:70px">{L["views"]}</th>
558
+ <th onclick="sortT_{table_id}(6,'n')" style="width:55px">{L["likes"]}</th>
559
+ <th onclick="sortT_{table_id}(7,'n')" style="width:50px">{L["comments"]}</th>
560
+ <th onclick="sortT_{table_id}(8,'n')" style="width:70px">{L["ai_pick_col"]}</th>
561
+ <th onclick="sortT_{table_id}(9,'s')" style="width:85px">{L["date"]}</th>
562
  </tr></thead><tbody>'''
563
 
564
  for i, v in enumerate(videos_data):
 
599
  stats = get_db_stats()
600
  return html, f"📊 DB: Videos {stats['videos']} | Records {stats['stats']} | Channels {stats['channels']} | Searches {stats['searches']}"
601
 
602
+ def show_trending_alerts(ui_lang):
603
+ is_ko = ui_lang == "ko"
604
  conn = sqlite3.connect("youtube_data.db")
605
  c = conn.cursor()
606
  c.execute('''SELECT ta.video_id, v.title, v.channel_name, v.thumbnail, ta.old_value, ta.new_value, ta.change_percent, ta.detected_at
 
608
  alerts = c.fetchall()
609
  conn.close()
610
 
611
+ info_box = f'''
612
  <div style="background:linear-gradient(135deg,#EF4444,#DC2626);color:#fff;padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
613
+ <h2 style="font-family:'Bangers',cursive;margin:0 0 10px 0">🔥 {"급상승 - 갑작스런 조회수 급증 감지" if is_ko else "TRENDING - Sudden Surge Detection"}</h2>
614
  <table style="width:100%;color:#fff;font-size:14px">
615
+ <tr><td style="width:120px"><b>📌 {"이게 뭐야?" if is_ko else "What is this?"}</b></td><td>{"이전 대비 조회수 20% 이상 급증한 영상" if is_ko else "Videos with 20%+ sudden view increase"}</td></tr>
616
+ <tr><td><b>🎯 {"목적" if is_ko else "Purpose"}</b></td><td>{"실시간 바이럴 영상 및 핫이슈 포착" if is_ko else "Catch viral moments and breaking content"}</td></tr>
617
+ <tr><td><b>⏱️ {"작동방식" if is_ko else "Trigger"}</b></td><td>{"동일 영상 재검색시 조회수 20% 이상 증가 감지" if is_ko else "When same video is searched again and views jumped 20%+"}</td></tr>
618
+ <tr><td><b>💡 {"활용" if is_ko else "Best for"}</b></td><td>{"지금 바로 바이럴 중인 영상, 뉴스, 트렌드 발굴" if is_ko else "Finding videos going viral RIGHT NOW"}</td></tr>
619
  </table>
620
  </div>'''
621
 
622
  if not alerts:
623
  try:
624
  resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="US", maxResults=20).execute()
625
+ html = info_box + f'<p style="font-family:Comic Neue,cursive;margin-bottom:15px">📢 {"아직 급상승 알림 없음. 검색을 여러 번 실행하세요!" if is_ko else "No surge alerts yet. Run searches multiple times!"}</p><div style="display:flex;flex-wrap:wrap;gap:15px">'
626
  for i, item in enumerate(resp.get("items", [])[:20], 1):
627
  snip, stats = item["snippet"], item["statistics"]
628
  title = snip["title"][:35] + "..." if len(snip["title"]) > 35 else snip["title"]
 
647
  </div>'''
648
  return html + '</div>'
649
 
650
+ def show_top_growing(ui_lang):
651
+ is_ko = ui_lang == "ko"
652
  conn = sqlite3.connect("youtube_data.db")
653
  c = conn.cursor()
654
  cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
 
661
  results = c.fetchall()
662
  conn.close()
663
 
664
+ info_box = f'''
665
  <div style="background:linear-gradient(135deg,#3B82F6,#2563EB);color:#fff;padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
666
+ <h2 style="font-family:'Bangers',cursive;margin:0 0 10px 0">📈 {"급성장 - 48시간 성장률 TOP" if is_ko else "TOP GROWING - 48h Growth Champions"}</h2>
667
  <table style="width:100%;color:#fff;font-size:14px">
668
+ <tr><td style="width:120px"><b>📌 {"이게 뭐야?" if is_ko else "What is this?"}</b></td><td>{"48시간 동안 가장 높은 성장률을 기록한 영상 순위" if is_ko else "Videos ranked by highest growth RATE over 48 hours"}</td></tr>
669
+ <tr><td><b>🎯 {"목적" if is_ko else "Purpose"}</b></td><td>{"꾸준히 성장하는 콘텐츠 발굴" if is_ko else "Find consistently rising content"}</td></tr>
670
+ <tr><td><b>📊 {"계산방식" if is_ko else "Calculation"}</b></td><td>({"최대 조회수" if is_ko else "Max Views"} - {"최소 조회수" if is_ko else "Min Views"}) / {"최소 조회수" if is_ko else "Min Views"} × 100%</td></tr>
671
+ <tr><td><b>💡 {"활용" if is_ko else "Best for"}</b></td><td>{"에버그린 콘텐츠, 안정적 트렌드 발굴" if is_ko else "Evergreen content, reliable trends"}</td></tr>
672
  </table>
673
  </div>'''
674
 
675
  if not results:
676
  try:
677
  resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="US", maxResults=20).execute()
678
+ html = info_box + f'<p style="font-family:Comic Neue,cursive;margin-bottom:15px">📢 {"데이터 축적 중. 검색을 여러 번 실행하세요!" if is_ko else "No growth data yet. Run searches over time!"}</p><div style="display:flex;flex-wrap:wrap;gap:15px">'
679
  for i, item in enumerate(resp.get("items", [])[:20], 1):
680
  snip, stats = item["snippet"], item["statistics"]
681
  views, likes = int(stats.get("viewCount", 0)), int(stats.get("likeCount", 0))
 
704
  </div>'''
705
  return html + '</div>'
706
 
707
+ def show_ai_picks(ui_lang):
708
+ is_ko = ui_lang == "ko"
709
  global ai_pick_storage
710
  if not ai_pick_storage["videos"]:
711
+ return f'<div style="background:#FACC15;padding:20px;border:3px solid #1F2937;border-radius:8px;text-align:center;font-family:Comic Neue,cursive"><h2 style="font-family:Bangers,cursive">⭐ {"AI 추천 - 데이터 없음" if is_ko else "AI PICK - No Data Yet"}</h2><p>{"먼저 검색을 실행하세요!" if is_ko else "Run a search first!"}</p></div>'
712
 
713
  videos, ratings = ai_pick_storage["videos"], ai_pick_storage["ratings"]
714
  top_picks = [(i, v, ratings.get(i, 0)) for i, v in enumerate(videos) if ratings.get(i, 0) >= 3]
 
717
  analysis = ""
718
  if groq_client and top_picks:
719
  info = "\n".join([f"- {v['title'][:50]} (Views:{format_count(v['views'])})" for _, v, _ in top_picks[:5]])
720
+ lang_prompt = "한국어로 답변해주세요." if is_ko else ""
721
+ result = call_llm(f"Analyze top YouTube videos in 3-4 sentences:\n{info}\n\nProvide: 1) Common theme 2) Why popular 3) Content opportunity {lang_prompt}", 500)
722
  if result and "Error" not in result:
723
+ analysis = f'<div style="background:#1F2937;color:#10B981;padding:15px;border:3px solid #10B981;border-radius:8px;margin:15px 0;font-family:Courier New,monospace"><h4 style="color:#FACC15;margin:0 0 10px">🤖 {"AI 분석" if is_ko else "AI ANALYSIS"}</h4><p style="margin:0;font-size:14px;line-height:1.6">{result}</p></div>'
724
 
725
  html = f'''<div style="background:linear-gradient(135deg,#FACC15,#EF4444);padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
726
+ <h2 style="font-family:Bangers,cursive;color:#1F2937;margin:0;text-shadow:2px 2px 0 #FFF">⭐ {"AI 추천 - TOP 영상" if is_ko else "AI PICK - TOP RECOMMENDATIONS"}</h2>
727
+ <p style="color:#1F2937;margin:5px 0 0;font-family:Comic Neue,cursive;font-weight:700">{"⭐⭐ 이상 등급 영상" if is_ko else "⭐⭐+ rated videos"}: {len(top_picks)}{"개" if is_ko else ""}</p>
728
  </div>{analysis}<div style="display:flex;flex-wrap:wrap;gap:20px">'''
729
 
730
  for idx, (_, v, rating) in enumerate(top_picks[:30], 1):
 
740
  <span style="color:#EF4444">❤️ {format_count(v['likes'])}</span>
741
  </div></div>'''
742
 
743
+ return html + '</div>' if top_picks else html + f'<p style="text-align:center;padding:30px">{"⭐⭐+ 등급 영상 없음. 다른 키워드로 검색하세요!" if is_ko else "No ⭐⭐+ videos. Try different keywords!"}</p></div>'
744
+
745
+ # LLM Analysis Functions
746
+ def analyze_keyword_suggest(keyword, ui_lang):
747
+ if not keyword: return "⚠️ Please enter a keyword!" if ui_lang == "en" else "⚠️ 키워드를 입력하세요!"
748
+ if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
749
+ lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
750
+ return call_llm(f'YouTube SEO expert. For "{keyword}", suggest 15 related keywords with search volume, competition, content type. {lang}', 1500) or "Error"
751
+
752
+ def analyze_trend_prediction(keyword, ui_lang):
753
+ if not keyword: return "⚠️ Please enter a keyword!" if ui_lang == "en" else "⚠️ 키워드를 입력하세요!"
754
+ if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
755
+ lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
756
+ return call_llm(f'Trend analyst. For "{keyword}": 1) Current status 2) Peak season 3) 6-month forecast 4) Risk factors 5) Opportunity windows 6) Emerging topics. {lang}', 1500) or "Error"
757
+
758
+ def analyze_content_ideas(keyword, ui_lang):
759
+ if not keyword: return "⚠️ Please enter a topic!" if ui_lang == "en" else "⚠️ 주제를 입력하세요!"
760
+ if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
761
+ lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
762
+ return call_llm(f'YouTube strategist. For "{keyword}", generate 10 video ideas with: Title, Hook, Format, Length, Thumbnail concept, Viral score 1-10. {lang}', 2000) or "Error"
763
+
764
+ def analyze_channel(channel_name, ui_lang):
765
+ if not channel_name: return "⚠️ Please enter channel name!" if ui_lang == "en" else "⚠️ 채널명을 입력하세요!"
766
+ if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
767
+ lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
768
+ return call_llm(f'YouTube consultant. For "{channel_name}": 1) Niche assessment 2) Content strategy 3) Growth tactics 4) Monetization 5) Competitive advantages. {lang}', 2000) or "Error"
769
+
770
+ def analyze_competitor(my_channel, competitor, ui_lang):
771
+ if not my_channel or not competitor: return "⚠️ Please enter both channels!" if ui_lang == "en" else "⚠️ 둘 다 입력하세요!"
772
+ if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
773
+ lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
774
+ return call_llm(f'Compare "{my_channel}" vs "{competitor}": 1) Positioning 2) Content gap 3) Benchmarks 4) Advantages 5) Action plan 6) 5 video ideas to beat them. {lang}', 2000) or "Error"
775
+
776
+ def show_search_history(ui_lang):
777
+ is_ko = ui_lang == "ko"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
778
  conn = sqlite3.connect("youtube_data.db")
779
  c = conn.cursor()
780
  c.execute('SELECT keyword,country,language,sort_by,results_count,searched_at FROM search_history ORDER BY searched_at DESC LIMIT 50')
 
782
  conn.close()
783
 
784
  if not history:
785
+ return f"<p style='font-family:Comic Neue,cursive'>{'검색 기록 없음' if is_ko else 'No search history yet.'}</p>"
786
 
787
+ html = f'''<div style="background:#1F2937;color:#FACC15;padding:15px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
788
+ <h3 style="font-family:Bangers,cursive;margin:0">🕐 {"검색 기록" if is_ko else "SEARCH HISTORY"}</h3></div>
789
  <table style="width:100%;border-collapse:collapse;font-family:Comic Neue,cursive">
790
  <thead><tr style="background:#FACC15;color:#1F2937">
791
+ <th style="padding:12px;border:2px solid #1F2937">{"검색어" if is_ko else "Keyword"}</th>
792
+ <th style="padding:12px">{"국가" if is_ko else "Country"}</th>
793
+ <th style="padding:12px">{"언어" if is_ko else "Language"}</th>
794
+ <th style="padding:12px">{"정렬" if is_ko else "Sort"}</th>
795
+ <th style="padding:12px">{"결과" if is_ko else "Results"}</th>
796
+ <th style="padding:12px">{"시간" if is_ko else "Time"}</th>
797
  </tr></thead><tbody>'''
798
 
799
  for kw, country, lang, sort_by, cnt, searched in history:
 
805
 
806
  return html + '</tbody></table>'
807
 
808
+ # UI Language switch functions
809
+ def switch_language(lang):
810
+ L = UI_LANG.get(lang, UI_LANG["en"])
811
+ sort_opts = list(L["sort_options"].keys())
812
+ date_opts = list(L["date_options"].keys())
813
+ return (
814
+ gr.update(label=L["search_keyword"], placeholder=L["enter_keyword"]),
815
+ gr.update(value=L["search"]),
816
+ gr.update(value=L["refresh"]),
817
+ gr.update(label=L["country"]),
818
+ gr.update(label=L["language"]),
819
+ gr.update(choices=sort_opts, value=sort_opts[0], label=L["sort_by"]),
820
+ gr.update(choices=date_opts, value=date_opts[0], label=L["period"]),
821
+ gr.update(label=L["max_results"]),
822
+ gr.update(label=L["click_autofill"]),
823
+ )
824
+
825
  # Initial trending
826
  initial_trending = get_real_trending_keywords("US", "en")
827
 
 
829
  # 🎨 Gradio UI
830
  # ============================================
831
  with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
832
+ # Hidden state for UI language
833
+ ui_lang_state = gr.State("en")
834
+
835
  gr.HTML('''<div style="text-align:center;margin:20px 0">
836
  <a href="https://www.humangen.ai" target="_blank">
837
  <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge">
838
  </a></div>''')
839
 
840
+ # Language selector at top
841
+ with gr.Row():
842
+ gr.Markdown("# 🎬 YOUTUBE TREND ANALYZER 📊", elem_classes="header-text")
843
+
844
+ with gr.Row():
845
+ with gr.Column(scale=4):
846
+ db_stats = gr.Markdown("📊 Loading DB stats...")
847
+ with gr.Column(scale=1):
848
+ ui_lang_dropdown = gr.Dropdown(
849
+ choices=["English", "한국어"],
850
+ value="English",
851
+ label="🌐 UI Language",
852
+ interactive=True,
853
+ elem_classes="lang-selector"
854
+ )
855
 
856
  with gr.Tabs():
857
  # Tab 1: Search
 
865
  with gr.Row():
866
  country = gr.Dropdown(list(COUNTRIES.keys()), value="United States", label="Country")
867
  language = gr.Dropdown(list(LANGUAGES.keys()), value="Auto (by Country)", label="Language")
868
+ sort_by = gr.Dropdown(list(UI_LANG["en"]["sort_options"].keys()), value="Most Viewed", label="Sort By")
869
+ date_filter = gr.Dropdown(list(UI_LANG["en"]["date_options"].keys()), value="All Time", label="Period")
870
  max_results = gr.Slider(10, 1000, value=100, step=10, label="Max Results")
871
  output = gr.HTML()
872
 
 
891
  # Tab 5: AI Tools
892
  with gr.Tab("🤖 AI Tools"):
893
  gr.Markdown("### 🧠 LLM-Powered Analysis Tools (GPT-OSS-120B)")
 
894
  with gr.Tabs():
 
895
  with gr.Tab("🏷️ Keyword Suggest"):
896
  gr.Markdown("**Generate related keywords with SEO insights**")
897
  kw_input = gr.Textbox(label="Enter base keyword", placeholder="e.g., Python tutorial")
898
  kw_btn = gr.Button("🔍 Generate Keywords", variant="primary")
899
  kw_output = gr.Textbox(label="Suggested Keywords", lines=20, elem_classes="llm-result")
900
 
 
901
  with gr.Tab("🔮 Trend Prediction"):
902
  gr.Markdown("**Predict future trend trajectory**")
903
  tp_input = gr.Textbox(label="Enter topic/keyword", placeholder="e.g., AI tools")
904
  tp_btn = gr.Button("🔮 Predict Trend", variant="primary")
905
  tp_output = gr.Textbox(label="Trend Analysis", lines=20, elem_classes="llm-result")
906
 
 
907
  with gr.Tab("💡 Content Ideas"):
908
  gr.Markdown("**Generate creative video ideas**")
909
  ci_input = gr.Textbox(label="Enter topic", placeholder="e.g., Home workout")
910
  ci_btn = gr.Button("💡 Generate Ideas", variant="primary")
911
  ci_output = gr.Textbox(label="Content Ideas", lines=25, elem_classes="llm-result")
912
 
 
913
  with gr.Tab("📊 Channel Analysis"):
914
  gr.Markdown("**Get growth strategy for your channel**")
915
  ca_input = gr.Textbox(label="Enter channel name or niche", placeholder="e.g., Tech reviews")
916
  ca_btn = gr.Button("📊 Analyze Channel", variant="primary")
917
  ca_output = gr.Textbox(label="Channel Analysis", lines=25, elem_classes="llm-result")
918
 
 
919
  with gr.Tab("⚔️ Competitor Analysis"):
920
  gr.Markdown("**Compare against competitors**")
921
  with gr.Row():
 
929
  history_btn = gr.Button("🔄 Refresh", variant="primary")
930
  history_out = gr.HTML()
931
 
932
+ # Language switch handler
933
+ def on_lang_change(lang_choice):
934
+ lang_code = "ko" if lang_choice == "한국어" else "en"
935
+ return lang_code
936
+
937
+ ui_lang_dropdown.change(on_lang_change, ui_lang_dropdown, ui_lang_state)
938
+ ui_lang_dropdown.change(
939
+ switch_language,
940
+ gr.State("ko" if ui_lang_dropdown.value == "한국어" else "en"),
941
+ [keyword, btn, refresh, country, language, sort_by, date_filter, max_results, trending]
942
+ )
943
+
944
  # Events
945
  trending.change(use_trending_keyword, trending, keyword)
946
  country.change(update_trending, country, trending)
947
+ btn.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results, ui_lang_state], [output, db_stats])
948
+ keyword.submit(search_videos, [keyword, country, language, sort_by, date_filter, max_results, ui_lang_state], [output, db_stats])
949
+ refresh.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results, ui_lang_state], [output, db_stats])
950
 
951
+ pick_btn.click(show_ai_picks, ui_lang_state, pick_out)
952
+ alerts_btn.click(show_trending_alerts, ui_lang_state, alerts_out)
953
+ growing_btn.click(show_top_growing, ui_lang_state, growing_out)
954
+ history_btn.click(show_search_history, ui_lang_state, history_out)
955
 
956
  # AI Tools events
957
+ kw_btn.click(analyze_keyword_suggest, [kw_input, ui_lang_state], kw_output)
958
+ tp_btn.click(analyze_trend_prediction, [tp_input, ui_lang_state], tp_output)
959
+ ci_btn.click(analyze_content_ideas, [ci_input, ui_lang_state], ci_output)
960
+ ca_btn.click(analyze_channel, [ca_input, ui_lang_state], ca_output)
961
+ comp_btn.click(analyze_competitor, [comp_my, comp_rival, ui_lang_state], comp_output)
962
 
963
  demo.launch()
964