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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +363 -365
app.py CHANGED
@@ -5,6 +5,7 @@ import os
5
  import sqlite3
6
  from datetime import datetime, timedelta
7
  import re
 
8
 
9
  API_KEY = os.getenv("YOUTUBE_API_KEY")
10
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
@@ -21,61 +22,35 @@ ai_pick_storage = {"videos": [], "ratings": {}, "timestamp": None}
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": "🔍 검색",
@@ -85,83 +60,43 @@ UI_LANG = {
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;
@@ -171,15 +106,10 @@ a[href*="gradio.app"] {
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;
@@ -194,7 +124,6 @@ a[href*="gradio.app"] {
194
  font-size: 2.8rem !important;
195
  text-align: center !important;
196
  text-shadow: 4px 4px 0px #FACC15, 6px 6px 0px #1F2937 !important;
197
- letter-spacing: 3px !important;
198
  }
199
 
200
  .gr-panel, .gr-box, .gr-form, .block, .gr-group {
@@ -213,7 +142,6 @@ a[href*="gradio.app"] {
213
  font-size: 1.2rem !important;
214
  box-shadow: 5px 5px 0px #1F2937 !important;
215
  }
216
-
217
  .gr-button-primary:hover { background: #2563EB !important; }
218
 
219
  .gr-button-secondary, button.secondary {
@@ -239,28 +167,15 @@ label { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important
239
  ::-webkit-scrollbar-thumb { background: #3B82F6; border: 2px solid #1F2937; }
240
  ::selection { background: #FACC15; color: #1F2937; }
241
 
242
- .llm-result {
 
243
  background: #1F2937 !important;
244
  color: #10B981 !important;
245
  border: 3px solid #10B981 !important;
246
  border-radius: 8px !important;
247
- padding: 15px !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
 
@@ -270,26 +185,20 @@ def init_db():
270
  c = conn.cursor()
271
  c.execute('''CREATE TABLE IF NOT EXISTS videos (
272
  video_id TEXT PRIMARY KEY, title TEXT, channel_id TEXT, channel_name TEXT,
273
- thumbnail TEXT, published_at TEXT, first_seen TEXT
274
- )''')
275
  c.execute('''CREATE TABLE IF NOT EXISTS video_stats (
276
  id INTEGER PRIMARY KEY AUTOINCREMENT, video_id TEXT, views INTEGER,
277
- likes INTEGER, comments INTEGER, recorded_at TEXT
278
- )''')
279
  c.execute('''CREATE TABLE IF NOT EXISTS channels (
280
- channel_id TEXT PRIMARY KEY, channel_name TEXT, first_seen TEXT
281
- )''')
282
  c.execute('''CREATE TABLE IF NOT EXISTS channel_stats (
283
- id INTEGER PRIMARY KEY AUTOINCREMENT, channel_id TEXT, subscribers INTEGER, recorded_at TEXT
284
- )''')
285
  c.execute('''CREATE TABLE IF NOT EXISTS search_history (
286
  id INTEGER PRIMARY KEY AUTOINCREMENT, keyword TEXT, country TEXT,
287
- language TEXT, sort_by TEXT, results_count INTEGER, searched_at TEXT
288
- )''')
289
  c.execute('''CREATE TABLE IF NOT EXISTS trending_alerts (
290
  id INTEGER PRIMARY KEY AUTOINCREMENT, video_id TEXT, alert_type TEXT,
291
- old_value INTEGER, new_value INTEGER, change_percent REAL, detected_at TEXT
292
- )''')
293
  conn.commit()
294
  conn.close()
295
 
@@ -305,31 +214,23 @@ COUNTRIES = {
305
  "Indonesia": ("ID", "id"), "Thailand": ("TH", "th"), "Vietnam": ("VN", "vi"),
306
  "Philippines": ("PH", "tl"), "Turkey": ("TR", "tr"), "Saudi Arabia": ("SA", "ar"),
307
  "Egypt": ("EG", "ar"), "South Africa": ("ZA", "en"), "Nigeria": ("NG", "en"),
308
- "Argentina": ("AR", "es"), "Colombia": ("CO", "es"), "Chile": ("CL", "es"),
309
- "Poland": ("PL", "pl"), "Netherlands": ("NL", "nl"), "Belgium": ("BE", "nl"),
310
- "Sweden": ("SE", "sv"), "Norway": ("NO", "no"), "Denmark": ("DK", "da"),
311
- "Finland": ("FI", "fi"), "Austria": ("AT", "de"), "Switzerland": ("CH", "de"),
312
- "Portugal": ("PT", "pt"), "Greece": ("GR", "el"), "Czech Republic": ("CZ", "cs"),
313
- "Romania": ("RO", "ro"), "Ukraine": ("UA", "uk"), "Israel": ("IL", "he"),
314
- "UAE": ("AE", "ar"), "Singapore": ("SG", "en"), "Malaysia": ("MY", "ms"),
315
  "Taiwan": ("TW", "zh"), "Hong Kong": ("HK", "zh"), "China": ("CN", "zh"),
 
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",
325
- "Hebrew": "he", "Czech": "cs", "Romanian": "ro", "Ukrainian": "uk",
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"
334
  elif count >= 1000000: return f"{count/1000000:.1f}M"
335
  elif count >= 1000: return f"{count/1000:.1f}K"
@@ -337,7 +238,7 @@ def format_count(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",
@@ -359,10 +260,13 @@ def get_ai_pick_rating(videos_data):
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 = {}
367
  for idx, rating in re.findall(r'(\d+):(\d)', result):
368
  ratings[int(idx)-1] = int(rating)
@@ -381,7 +285,7 @@ def calculate_local_rating(videos_data):
381
  ratings = {}
382
  if not videos_data: return ratings
383
  views_list = [v['views'] for v in videos_data if v['views'] > 0]
384
- if not views_list: return ratings
385
  avg_views, max_views = sum(views_list)/len(views_list), max(views_list)
386
 
387
  for i, v in enumerate(videos_data):
@@ -401,7 +305,7 @@ def calculate_local_rating(videos_data):
401
  return ratings
402
 
403
  def get_rating_display(rating):
404
- return {0: "", 1: "♥", 2: "⭐", 3: "⭐⭐", 4: "⭐⭐⭐"}.get(rating, "")
405
 
406
  def get_real_trending_keywords(region_code="US", language="en"):
407
  try:
@@ -438,13 +342,15 @@ def save_to_db(videos_data, channels_data, keyword, country, language, sort_by):
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
 
449
  def update_trending(country):
450
  region, lang = COUNTRIES.get(country, ("", ""))
@@ -453,17 +359,19 @@ def update_trending(country):
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
 
@@ -477,7 +385,10 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
477
  while len(all_items) < max_results:
478
  params["maxResults"] = min(50, max_results - len(all_items))
479
  if next_page: params["pageToken"] = next_page
480
- resp = youtube.search().list(**params).execute()
 
 
 
481
  items = resp.get("items", [])
482
  if not items: break
483
  all_items.extend(items)
@@ -485,27 +396,31 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
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]))
492
 
493
  video_stats = {}
494
  for i in range(0, len(video_ids), 50):
495
- for v in youtube.videos().list(id=",".join(video_ids[i:i+50]), part="statistics").execute().get("items", []):
496
- s = v["statistics"]
497
- video_stats[v["id"]] = {"views": int(s.get("viewCount", 0)), "likes": int(s.get("likeCount", 0)), "comments": int(s.get("commentCount", 0))}
 
 
498
 
499
  channel_subs, channel_subs_raw = {}, {}
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:
507
  vid, snip = item["id"]["videoId"], item["snippet"]
508
- st = video_stats.get(vid, {})
509
  videos_data.append({
510
  "video_id": vid, "title": snip["title"], "channel_id": snip["channelId"],
511
  "channel_name": snip["channelTitle"], "thumbnail": snip["thumbnails"]["medium"]["url"],
@@ -517,90 +432,153 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
517
  ai_ratings = get_ai_pick_rating(videos_data)
518
  save_to_db(videos_data, channel_subs_raw, keyword, country, language, sort_by)
519
 
520
- table_id = f"yt_{datetime.now().strftime('%H%M%S%f')}"
 
521
 
522
  html = f'''
523
  <style>
524
- .yt-tbl{{width:100%;border-collapse:collapse;font-family:'Comic Neue',cursive}}
525
- .yt-tbl th{{background:#EF4444;color:#fff;padding:12px 8px;cursor:pointer;border:2px solid #1F2937;font-family:'Bangers',cursive}}
526
- .yt-tbl th:hover{{background:#DC2626}}
527
- .yt-tbl td{{padding:10px 8px;border-bottom:2px solid #1F2937;background:#FFF;vertical-align:middle}}
528
- .yt-tbl tr:hover td{{background:#FEF9C3}}
529
- .yt-tbl img{{border-radius:6px;border:2px solid #1F2937}}
530
- .yt-tbl a{{color:#3B82F6;text-decoration:none;font-weight:700}}
531
- .yt-tbl a:hover{{color:#EF4444}}
532
- .ai-pick{{color:#FACC15;text-shadow:1px 1px 0 #1F2937;font-size:1.1rem}}
533
- .res-hdr{{background:#3B82F6;color:#fff;padding:15px;border-radius:8px;border:3px solid #1F2937;box-shadow:4px 4px 0 #1F2937;margin-bottom:15px;font-family:'Comic Neue',cursive}}
534
- .qsort{{display:inline-flex;gap:5px;margin-left:10px}}
535
- .qsort button{{background:#FACC15;border:2px solid #1F2937;border-radius:4px;padding:5px 10px;cursor:pointer;font-weight:700}}
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):
565
- title = v["title"][:45] + "..." if len(v["title"]) > 45 else v["title"]
566
- channel = v["channel_name"][:12] + "..." if len(v["channel_name"]) > 12 else v["channel_name"]
567
- url, ch_url = f"https://youtube.com/watch?v={v['video_id']}", f"https://youtube.com/channel/{v['channel_id']}"
 
568
  rating = ai_ratings.get(i, 0)
569
- rank_style = "color:#FFD700;font-size:1.3rem" if i==0 else ("color:#C0C0C0;font-size:1.2rem" if i==1 else ("color:#CD7F32;font-size:1.2rem" if i==2 else ""))
570
 
571
  html += f'''<tr>
572
- <td style="text-align:center;font-family:'Bangers',cursive;color:#EF4444;{rank_style}" data-v="{i+1}">{i+1}</td>
573
- <td><a href="{url}" target="_blank"><img src="{v['thumbnail']}" width="120" height="68"></a></td>
574
- <td data-v="{v['title'][:80]}"><a href="{url}" target="_blank" title="{v['title']}">{title}</a></td>
575
- <td data-v="{v['channel_name']}"><a href="{ch_url}" target="_blank">{channel}</a></td>
576
- <td data-v="{v['subs']}">{format_count(v['subs'])}</td>
577
- <td data-v="{v['views']}" style="color:#3B82F6;font-weight:700">{format_count(v['views'])}</td>
578
- <td data-v="{v['likes']}">{format_count(v['likes'])}</td>
579
- <td data-v="{v['comments']}">{format_count(v['comments'])}</td>
580
- <td class="ai-pick" data-v="{rating}">{get_rating_display(rating)}</td>
581
  <td data-v="{v['published_at'][:10]}">{v['published_at'][:10]}</td>
582
  </tr>'''
583
 
584
- html += f'''</tbody></table></div>
 
 
 
 
 
585
  <script>
586
- var ss_{table_id}={{}};
587
- function sortT_{table_id}(c,t){{
588
- var tb=document.getElementById('{table_id}'),tbody=tb.querySelector('tbody'),rows=Array.from(tbody.querySelectorAll('tr'));
589
- var asc=ss_{table_id}[c]!=='asc';ss_{table_id}={{}};ss_{table_id}[c]=asc?'asc':'desc';
590
- rows.sort((a,b)=>{{
591
- var av=a.cells[c].dataset.v||a.cells[c].textContent,bv=b.cells[c].dataset.v||b.cells[c].textContent;
592
- if(t==='n'){{av=parseFloat(av)||0;bv=parseFloat(bv)||0;return asc?av-bv:bv-av}}
593
- return asc?av.localeCompare(bv):bv.localeCompare(av)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  }});
595
- rows.forEach(r=>tbody.appendChild(r))
596
- }}
597
- </script>'''
 
 
 
 
 
 
598
 
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
@@ -610,26 +588,29 @@ def show_trending_alerts(ui_lang):
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"]
629
- html += f'''<div style="width:200px;background:#FFF;border:3px solid #1F2937;border-radius:8px;padding:12px;box-shadow:4px 4px 0 #1F2937;font-family:'Comic Neue',cursive">
630
  <a href="https://youtube.com/watch?v={item['id']}" target="_blank"><img src="{snip['thumbnails']['medium']['url']}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
631
- <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{i}. {title}</p>
632
- <p style="margin:0;font-size:15px;color:#EF4444;font-weight:700;font-family:'Bangers',cursive">👀 {format_count(int(stats.get('viewCount',0)))}</p>
633
  </div>'''
634
  return html + '</div>'
635
  except Exception as e:
@@ -637,24 +618,28 @@ def show_trending_alerts(ui_lang):
637
 
638
  html = info_box + '<div style="display:flex;flex-wrap:wrap;gap:15px">'
639
  for vid, title, channel, thumb, old_v, new_v, pct, detected in alerts:
640
- title = title[:30] + "..." if len(title) > 30 else title
641
- html += f'''<div style="width:200px;background:#FFF;border:3px solid #EF4444;border-radius:8px;padding:12px;box-shadow:4px 4px 0 #1F2937;font-family:'Comic Neue',cursive">
642
  <a href="https://youtube.com/watch?v={vid}" target="_blank"><img src="{thumb}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
643
  <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{title}</p>
644
- <p style="margin:0;font-size:11px;color:#666">{channel[:20]}</p>
645
- <p style="margin:5px 0 0;font-size:20px;color:#EF4444;font-weight:700;font-family:'Bangers',cursive">🔥 +{pct:.1f}%</p>
646
  <p style="margin:2px 0 0;font-size:11px;color:#888">{format_count(old_v)} → {format_count(new_v)}</p>
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()
655
  c.execute('''SELECT v.video_id, v.title, v.channel_name, v.thumbnail,
656
  MIN(vs.views) as min_v, MAX(vs.views) as max_v,
657
- ((MAX(vs.views) - MIN(vs.views)) * 100.0 / MIN(vs.views)) as growth
658
  FROM videos v JOIN video_stats vs ON v.video_id = vs.video_id
659
  WHERE vs.recorded_at > ? GROUP BY v.video_id HAVING min_v > 0 AND max_v > min_v
660
  ORDER BY growth DESC LIMIT 20''', (cutoff,))
@@ -663,28 +648,31 @@ def show_top_growing(ui_lang):
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))
682
  engagement = (likes / views * 100) if views > 0 else 0
683
- title = snip["title"][:30] + "..." if len(snip["title"]) > 30 else snip["title"]
684
- html += f'''<div style="width:200px;background:#FFF;border:3px solid #1F2937;border-radius:8px;padding:12px;box-shadow:4px 4px 0 #1F2937;font-family:'Comic Neue',cursive">
685
  <a href="https://youtube.com/watch?v={item['id']}" target="_blank"><img src="{snip['thumbnails']['medium']['url']}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
686
  <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{i}. {title}</p>
687
- <p style="margin:0;font-size:11px;color:#666">{snip['channelTitle'][:18]}</p>
688
  <p style="margin:5px 0 0;font-size:15px;color:#3B82F6;font-weight:700">👀 {format_count(views)}</p>
689
  <p style="margin:2px 0 0;font-size:12px;color:#EF4444">❤️ {engagement:.2f}%</p>
690
  </div>'''
@@ -694,87 +682,116 @@ def show_top_growing(ui_lang):
694
 
695
  html = info_box + '<div style="display:flex;flex-wrap:wrap;gap:15px">'
696
  for i, (vid, title, channel, thumb, min_v, max_v, growth) in enumerate(results, 1):
697
- title = title[:30] + "..." if len(title) > 30 else title
698
- html += f'''<div style="width:200px;background:#FFF;border:3px solid #3B82F6;border-radius:8px;padding:12px;box-shadow:4px 4px 0 #1F2937;font-family:'Comic Neue',cursive">
 
699
  <a href="https://youtube.com/watch?v={vid}" target="_blank"><img src="{thumb}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
700
  <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{i}. {title}</p>
701
- <p style="margin:0;font-size:11px;color:#666">{channel[:18]}</p>
702
- <p style="margin:5px 0 0;font-size:20px;color:#3B82F6;font-weight:700;font-family:'Bangers',cursive">📈 +{growth:.1f}%</p>
703
  <p style="margin:2px 0 0;font-size:11px;color:#888">{format_count(int(min_v))} → {format_count(int(max_v))}</p>
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]
715
  top_picks.sort(key=lambda x: (-x[2], -x[1]['views']))
716
 
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):
731
  border = "#FFD700" if rating == 4 else "#C0C0C0"
732
- title = v["title"][:40] + "..." if len(v["title"]) > 40 else v["title"]
733
- html += f'''<div style="width:220px;background:#FFF;border:4px solid {border};border-radius:12px;padding:15px;box-shadow:6px 6px 0 #1F2937;font-family:Comic Neue,cursive">
734
- <div style="position:relative"><a href="https://youtube.com/watch?v={v['video_id']}" target="_blank"><img src="{v['thumbnail']}" style="width:100%;border-radius:8px;border:2px solid #1F2937"></a>
735
- <span style="position:absolute;top:5px;right:5px;background:#1F2937;color:#FACC15;padding:3px 8px;border-radius:4px;font-family:Bangers,cursive;font-size:18px">{get_rating_display(rating)}</span></div>
736
- <p style="margin:12px 0 5px;font-size:13px;font-weight:700">{idx}. {title}</p>
737
- <p style="margin:0;font-size:11px;color:#666">{v["channel_name"][:20]}</p>
738
- <div style="display:flex;justify-content:space-between;margin-top:10px;font-size:12px">
 
 
739
  <span style="color:#3B82F6;font-weight:700">👀 {format_count(v['views'])}</span>
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,32 +799,45 @@ def show_search_history(ui_lang):
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:
800
  html += f'''<tr style="background:#FFF;border-bottom:2px solid #1F2937">
801
- <td style="padding:12px;font-weight:700">{kw}</td><td style="padding:12px">{country}</td>
802
- <td style="padding:12px">{lang}</td><td style="padding:12px">{sort_by}</td>
803
- <td style="padding:12px">{cnt}</td><td style="padding:12px;font-size:12px">{searched[:16].replace("T"," ")}</td>
 
 
 
804
  </tr>'''
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 (
@@ -829,7 +859,6 @@ initial_trending = get_real_trending_keywords("US", "en")
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">
@@ -837,31 +866,20 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
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
858
  with gr.Tab("🔍 Search"):
859
  gr.Markdown("### 🔥 Trending Keywords")
860
  trending = gr.Radio(choices=initial_trending, label="Click to auto-fill", interactive=True)
861
  with gr.Row():
862
  keyword = gr.Textbox(label="Search Keyword", placeholder="Enter keyword...", scale=3)
863
  btn = gr.Button("🔍 SEARCH", variant="primary", scale=1)
864
- refresh = gr.Button("🔄 Refresh", variant="secondary", scale=1)
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")
@@ -870,90 +888,72 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
870
  max_results = gr.Slider(10, 1000, value=100, step=10, label="Max Results")
871
  output = gr.HTML()
872
 
873
- # Tab 2: AI Pick
874
  with gr.Tab("⭐ AI Pick"):
875
  gr.Markdown("### 🤖 AI-Curated Top Recommendations (⭐⭐ and above)")
876
  pick_btn = gr.Button("🔄 Refresh AI Picks", variant="primary")
877
  pick_out = gr.HTML()
878
 
879
- # Tab 3: Trending
880
  with gr.Tab("🔥 Trending"):
881
  gr.Markdown("### 🔥 Sudden Surge Detection (20%+ view increase)")
882
  alerts_btn = gr.Button("🔄 Refresh", variant="primary")
883
  alerts_out = gr.HTML()
884
 
885
- # Tab 4: Top Growing
886
  with gr.Tab("📈 Top Growing"):
887
- gr.Markdown("### 📈 48-Hour Growth Champions (Highest growth rate)")
888
  growing_btn = gr.Button("🔄 Refresh", variant="primary")
889
  growing_out = gr.HTML()
890
 
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():
922
- comp_my = gr.Textbox(label="Your Channel/Topic", placeholder="e.g., My cooking channel")
923
- comp_rival = gr.Textbox(label="Competitor", placeholder="e.g., Gordon Ramsay")
924
- comp_btn = gr.Button("⚔️ Analyze Competition", variant="primary")
925
- comp_output = gr.Textbox(label="Competitive Analysis", lines=25, elem_classes="llm-result")
926
 
927
- # Tab 6: History
928
  with gr.Tab("🕐 History"):
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)
@@ -961,5 +961,3 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
961
  comp_btn.click(analyze_competitor, [comp_my, comp_rival, ui_lang_state], comp_output)
962
 
963
  demo.launch()
964
-
965
-
 
5
  import sqlite3
6
  from datetime import datetime, timedelta
7
  import re
8
+ import uuid
9
 
10
  API_KEY = os.getenv("YOUTUBE_API_KEY")
11
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
 
22
  UI_LANG = {
23
  "en": {
24
  "title": "🎬 YOUTUBE TREND ANALYZER 📊",
 
 
25
  "search_keyword": "Search Keyword",
26
  "enter_keyword": "Enter keyword...",
27
  "search": "🔍 SEARCH",
28
  "refresh": "🔄 Refresh",
29
  "country": "Country",
30
+ "language": "Language",
31
  "sort_by": "Sort By",
32
  "period": "Period",
33
  "max_results": "Max Results",
34
+ "click_autofill": "Click to auto-fill",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  "total": "Total",
36
+ "results": "results",
37
  "views": "Views",
38
  "likes": "Likes",
39
  "subs": "Subs",
40
  "date": "Date",
41
  "rank": "Rank",
42
  "thumb": "Thumb",
43
+ "title_col": "Title",
44
  "channel": "Channel",
45
  "comments": "Cmts",
46
  "ai_pick_col": "AI Pick",
47
+ "no_keyword": "Please enter a search keyword!",
48
+ "no_results": "No results found.",
49
  "sort_options": {"Most Viewed": "viewCount", "Latest": "date", "Relevance": "relevance", "Top Rated": "rating"},
50
  "date_options": {"All Time": "", "Today": "today", "This Week": "thisWeek", "This Month": "thisMonth", "This Year": "thisYear"},
51
  },
52
  "ko": {
53
  "title": "🎬 유튜브 트렌드 분석기 📊",
 
 
54
  "search_keyword": "검색어",
55
  "enter_keyword": "검색어 입력...",
56
  "search": "🔍 검색",
 
60
  "sort_by": "정렬",
61
  "period": "기간",
62
  "max_results": "최대 결과",
63
+ "click_autofill": "클릭시 자동 입력",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  "total": "총",
65
+ "results": "개 결과",
66
  "views": "조회수",
67
  "likes": "좋아요",
68
  "subs": "구독자",
69
  "date": "날짜",
70
  "rank": "순위",
71
  "thumb": "썸네일",
72
+ "title_col": "제목",
73
  "channel": "채널",
74
  "comments": "댓글",
75
+ "ai_pick_col": "AI추천",
76
+ "no_keyword": "검색어를 입력하세요!",
77
+ "no_results": "검색 결과가 없습니다.",
78
  "sort_options": {"조회수 순": "viewCount", "최신순": "date", "관련성 순": "relevance", "평점 순": "rating"},
79
  "date_options": {"전체 기간": "", "오늘": "today", "이번 주": "thisWeek", "이번 달": "thisMonth", "올해": "thisYear"},
80
  }
81
  }
82
 
83
  # ============================================
84
+ # 🎨 CSS - Hide HF Elements + Comic Theme
85
  # ============================================
86
  css = """
87
  @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
88
 
89
+ /* Hide ALL Hugging Face elements */
90
+ #space-header, .space-header, header, .huggingface-space-header,
91
+ [data-testid="space-header"], .svelte-1ed2p3z, .svelte-kqij2n,
92
+ .svelte-1kyws56, .wrap.svelte-1kyws56, button.svelte-1kyws56,
93
+ .duplicate-button, .settings-button, [class*="settings"],
94
+ [class*="duplicate"], .embed-buttons, .buttons-container,
95
+ header button, .gr-button-icon, footer, .footer,
96
+ .gradio-container footer, .built-with, [class*="footer"],
97
+ .built-with-gradio, a[href*="gradio.app"],
98
+ .gradio-container > div:first-child > button,
99
+ .gradio-container > header {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  display: none !important;
101
  visibility: hidden !important;
102
  height: 0 !important;
 
106
  overflow: hidden !important;
107
  opacity: 0 !important;
108
  pointer-events: none !important;
109
+ position: absolute !important;
110
+ left: -9999px !important;
111
  }
112
 
 
 
 
 
 
 
 
113
  .gradio-container {
114
  background-color: #FEF9C3 !important;
115
  background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
 
124
  font-size: 2.8rem !important;
125
  text-align: center !important;
126
  text-shadow: 4px 4px 0px #FACC15, 6px 6px 0px #1F2937 !important;
 
127
  }
128
 
129
  .gr-panel, .gr-box, .gr-form, .block, .gr-group {
 
142
  font-size: 1.2rem !important;
143
  box-shadow: 5px 5px 0px #1F2937 !important;
144
  }
 
145
  .gr-button-primary:hover { background: #2563EB !important; }
146
 
147
  .gr-button-secondary, button.secondary {
 
167
  ::-webkit-scrollbar-thumb { background: #3B82F6; border: 2px solid #1F2937; }
168
  ::selection { background: #FACC15; color: #1F2937; }
169
 
170
+ /* LLM Result box - 녹색 글자 on 검정 배경 */
171
+ .llm-result textarea {
172
  background: #1F2937 !important;
173
  color: #10B981 !important;
174
  border: 3px solid #10B981 !important;
175
  border-radius: 8px !important;
 
176
  font-family: 'Courier New', monospace !important;
177
+ font-size: 14px !important;
178
+ line-height: 1.6 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  }
180
  """
181
 
 
185
  c = conn.cursor()
186
  c.execute('''CREATE TABLE IF NOT EXISTS videos (
187
  video_id TEXT PRIMARY KEY, title TEXT, channel_id TEXT, channel_name TEXT,
188
+ thumbnail TEXT, published_at TEXT, first_seen TEXT)''')
 
189
  c.execute('''CREATE TABLE IF NOT EXISTS video_stats (
190
  id INTEGER PRIMARY KEY AUTOINCREMENT, video_id TEXT, views INTEGER,
191
+ likes INTEGER, comments INTEGER, recorded_at TEXT)''')
 
192
  c.execute('''CREATE TABLE IF NOT EXISTS channels (
193
+ channel_id TEXT PRIMARY KEY, channel_name TEXT, first_seen TEXT)''')
 
194
  c.execute('''CREATE TABLE IF NOT EXISTS channel_stats (
195
+ id INTEGER PRIMARY KEY AUTOINCREMENT, channel_id TEXT, subscribers INTEGER, recorded_at TEXT)''')
 
196
  c.execute('''CREATE TABLE IF NOT EXISTS search_history (
197
  id INTEGER PRIMARY KEY AUTOINCREMENT, keyword TEXT, country TEXT,
198
+ language TEXT, sort_by TEXT, results_count INTEGER, searched_at TEXT)''')
 
199
  c.execute('''CREATE TABLE IF NOT EXISTS trending_alerts (
200
  id INTEGER PRIMARY KEY AUTOINCREMENT, video_id TEXT, alert_type TEXT,
201
+ old_value INTEGER, new_value INTEGER, change_percent REAL, detected_at TEXT)''')
 
202
  conn.commit()
203
  conn.close()
204
 
 
214
  "Indonesia": ("ID", "id"), "Thailand": ("TH", "th"), "Vietnam": ("VN", "vi"),
215
  "Philippines": ("PH", "tl"), "Turkey": ("TR", "tr"), "Saudi Arabia": ("SA", "ar"),
216
  "Egypt": ("EG", "ar"), "South Africa": ("ZA", "en"), "Nigeria": ("NG", "en"),
217
+ "Argentina": ("AR", "es"), "Colombia": ("CO", "es"), "Poland": ("PL", "pl"),
218
+ "Netherlands": ("NL", "nl"), "Sweden": ("SE", "sv"), "Switzerland": ("CH", "de"),
 
 
 
 
 
219
  "Taiwan": ("TW", "zh"), "Hong Kong": ("HK", "zh"), "China": ("CN", "zh"),
220
+ "Singapore": ("SG", "en"), "Malaysia": ("MY", "ms"), "UAE": ("AE", "ar"),
221
  }
222
 
223
  LANGUAGES = {
224
+ "Auto (by Country)": "", "English": "en", "Korean": "ko", "Spanish": "es",
225
+ "Portuguese": "pt", "French": "fr", "German": "de", "Italian": "it",
226
  "Russian": "ru", "Japanese": "ja", "Chinese": "zh", "Hindi": "hi",
227
  "Arabic": "ar", "Turkish": "tr", "Indonesian": "id", "Vietnamese": "vi",
228
  "Thai": "th", "Dutch": "nl", "Polish": "pl", "Swedish": "sv",
 
 
 
229
  }
230
 
 
 
 
231
  def format_count(count):
232
+ if count is None: return "0"
233
+ count = int(count)
234
  if count >= 1000000000: return f"{count/1000000000:.1f}B"
235
  elif count >= 1000000: return f"{count/1000000:.1f}M"
236
  elif count >= 1000: return f"{count/1000:.1f}K"
 
238
 
239
  def call_llm(prompt, max_tokens=2000):
240
  if not groq_client:
241
+ return "⚠️ LLM API not configured. Set GROQ_API_KEY."
242
  try:
243
  completion = groq_client.chat.completions.create(
244
  model="openai/gpt-oss-120b",
 
260
  if groq_client:
261
  try:
262
  sample = videos_data[:50]
263
+ video_info = "\n".join([
264
+ f"#{i+1}. {v['title'][:40]}, Views:{v['views']}, Likes:{v['likes']}, Subs:{v.get('subs',0)}"
265
+ for i, v in enumerate(sample)
266
+ ])
267
+ prompt = f"Rate YouTube videos 0-4. 0=None,1=♥,2=⭐,3=⭐⭐,4=⭐⭐⭐. Consider engagement, viral potential. Format: 1:3,2:2,3:4\n\n{video_info}\n\nResponse (number:rating only):"
268
  result = call_llm(prompt, 1500)
269
+ if result and "Error" not in result and "⚠️" not in result:
270
  ratings = {}
271
  for idx, rating in re.findall(r'(\d+):(\d)', result):
272
  ratings[int(idx)-1] = int(rating)
 
285
  ratings = {}
286
  if not videos_data: return ratings
287
  views_list = [v['views'] for v in videos_data if v['views'] > 0]
288
+ if not views_list: return {i: 0 for i in range(len(videos_data))}
289
  avg_views, max_views = sum(views_list)/len(views_list), max(views_list)
290
 
291
  for i, v in enumerate(videos_data):
 
305
  return ratings
306
 
307
  def get_rating_display(rating):
308
+ return {0: "-", 1: "♥", 2: "⭐", 3: "⭐⭐", 4: "⭐⭐⭐"}.get(rating, "-")
309
 
310
  def get_real_trending_keywords(region_code="US", language="en"):
311
  try:
 
342
  conn.commit(); conn.close()
343
 
344
  def get_db_stats():
345
+ try:
346
+ conn = sqlite3.connect("youtube_data.db")
347
+ c = conn.cursor()
348
+ stats = {}
349
+ for t, k in [("videos","videos"),("video_stats","stats"),("channels","channels"),("search_history","searches"),("trending_alerts","alerts")]:
350
+ c.execute(f"SELECT COUNT(*) FROM {t}"); stats[k] = c.fetchone()[0]
351
+ conn.close()
352
+ return stats
353
+ except: return {"videos":0,"stats":0,"channels":0,"searches":0,"alerts":0}
354
 
355
  def update_trending(country):
356
  region, lang = COUNTRIES.get(country, ("", ""))
 
359
  def use_trending_keyword(kw):
360
  return kw if kw else ""
361
 
362
+ # ============================================
363
+ # 🔍 Main Search Function with WORKING SORT
364
+ # ============================================
365
  def search_videos(keyword, country, language, sort_by, date_filter, max_results, ui_lang):
366
  L = UI_LANG.get(ui_lang, UI_LANG["en"])
367
  if not keyword or not keyword.strip():
368
+ return f"⚠️ {L['no_keyword']}", ""
369
 
370
  max_results = int(max_results)
371
  all_items, next_page = [], None
372
  region_code, default_lang = COUNTRIES.get(country, ("", ""))
373
  lang_code = default_lang if language in ["Auto (by Country)", "자동 (국가 기반)"] else LANGUAGES.get(language, "")
374
 
 
375
  sort_value = L["sort_options"].get(sort_by, "viewCount")
376
  date_value = L["date_options"].get(date_filter, "")
377
 
 
385
  while len(all_items) < max_results:
386
  params["maxResults"] = min(50, max_results - len(all_items))
387
  if next_page: params["pageToken"] = next_page
388
+ try:
389
+ resp = youtube.search().list(**params).execute()
390
+ except Exception as e:
391
+ return f"API Error: {e}", ""
392
  items = resp.get("items", [])
393
  if not items: break
394
  all_items.extend(items)
 
396
  if not next_page: break
397
 
398
  if not all_items:
399
+ return f"{L['no_results']}", ""
400
 
401
  video_ids = [item["id"]["videoId"] for item in all_items]
402
  channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
403
 
404
  video_stats = {}
405
  for i in range(0, len(video_ids), 50):
406
+ try:
407
+ for v in youtube.videos().list(id=",".join(video_ids[i:i+50]), part="statistics").execute().get("items", []):
408
+ s = v["statistics"]
409
+ video_stats[v["id"]] = {"views": int(s.get("viewCount", 0)), "likes": int(s.get("likeCount", 0)), "comments": int(s.get("commentCount", 0))}
410
+ except: pass
411
 
412
  channel_subs, channel_subs_raw = {}, {}
413
  for i in range(0, len(channel_ids), 50):
414
+ try:
415
+ for ch in youtube.channels().list(id=",".join(channel_ids[i:i+50]), part="statistics").execute().get("items", []):
416
+ sub = ch["statistics"].get("subscriberCount", "0")
417
+ if sub: channel_subs_raw[ch["id"]] = int(sub); channel_subs[ch["id"]] = format_count(int(sub))
418
+ except: pass
419
 
420
  videos_data = []
421
  for item in all_items:
422
  vid, snip = item["id"]["videoId"], item["snippet"]
423
+ st = video_stats.get(vid, {"views": 0, "likes": 0, "comments": 0})
424
  videos_data.append({
425
  "video_id": vid, "title": snip["title"], "channel_id": snip["channelId"],
426
  "channel_name": snip["channelTitle"], "thumbnail": snip["thumbnails"]["medium"]["url"],
 
432
  ai_ratings = get_ai_pick_rating(videos_data)
433
  save_to_db(videos_data, channel_subs_raw, keyword, country, language, sort_by)
434
 
435
+ # 고유 ID
436
+ uid = str(uuid.uuid4()).replace("-", "")[:8]
437
 
438
  html = f'''
439
  <style>
440
+ #tbl_{uid} {{ width:100%; border-collapse:collapse; font-family:'Comic Neue',cursive; }}
441
+ #tbl_{uid} th {{
442
+ background:#EF4444; color:#fff; padding:12px 6px; border:2px solid #1F2937;
443
+ font-family:'Bangers',cursive; cursor:pointer; user-select:none; position:relative;
444
+ }}
445
+ #tbl_{uid} th:hover {{ background:#DC2626; }}
446
+ #tbl_{uid} th.asc::after {{ content:" ▲"; color:#FACC15; }}
447
+ #tbl_{uid} th.desc::after {{ content:" ▼"; color:#FACC15; }}
448
+ #tbl_{uid} td {{ padding:8px 6px; border-bottom:2px solid #1F2937; background:#FFF; vertical-align:middle; }}
449
+ #tbl_{uid} tr:hover td {{ background:#FEF9C3; }}
450
+ #tbl_{uid} img {{ border-radius:4px; border:2px solid #1F2937; }}
451
+ #tbl_{uid} a {{ color:#3B82F6; text-decoration:none; font-weight:700; }}
452
+ #tbl_{uid} a:hover {{ color:#EF4444; }}
453
+ .ai-pick {{ color:#FACC15; text-shadow:1px 1px 0 #1F2937; font-size:1rem; }}
454
+ .hdr_{uid} {{ background:#3B82F6; color:#fff; padding:15px; border-radius:8px; border:3px solid #1F2937; box-shadow:4px 4px 0 #1F2937; margin-bottom:15px; font-family:'Comic Neue',cursive; }}
455
+ .sortbtn_{uid} {{ background:#FACC15; border:2px solid #1F2937; border-radius:4px; padding:6px 12px; margin:2px; cursor:pointer; font-weight:700; font-family:'Comic Neue',cursive; }}
456
+ .sortbtn_{uid}:hover {{ background:#1F2937; color:#FACC15; }}
457
  </style>
458
+
459
+ <div class="hdr_{uid}">
460
+ 🎬 {L["total"]} <b>{len(videos_data)}</b> {L["results"]} | 🔍 "{keyword}" | 🌍 {country}
461
+ <br><span style="font-size:0.85rem">🤖 AI Pick: ♥ ⭐ ⭐⭐ ⭐⭐⭐ | 💡 {"헤더 클릭 = 정렬" if ui_lang=="ko" else "Click header to sort"}</span>
462
+ <div style="margin-top:10px">
463
+ <button class="sortbtn_{uid}" onclick="doSort_{uid}(4)">{L["subs"]}</button>
464
+ <button class="sortbtn_{uid}" onclick="doSort_{uid}(5)">{L["views"]}</button>
465
+ <button class="sortbtn_{uid}" onclick="doSort_{uid}(6)">{L["likes"]}</button>
466
+ <button class="sortbtn_{uid}" onclick="doSort_{uid}(7)">{L["comments"]}</button>
467
+ <button class="sortbtn_{uid}" onclick="doSort_{uid}(8)">AI</button>
468
+ <button class="sortbtn_{uid}" onclick="doSort_{uid}(9)">{L["date"]}</button>
469
+ </div>
470
  </div>
471
+
472
+ <div style="max-height:700px; overflow-y:auto; border:3px solid #1F2937; border-radius:8px;">
473
+ <table id="tbl_{uid}">
474
+ <thead><tr>
475
+ <th data-col="0" data-type="n" style="width:45px">{L["rank"]}</th>
476
+ <th style="width:120px">{L["thumb"]}</th>
477
+ <th data-col="2" data-type="s">{L["title_col"]}</th>
478
+ <th data-col="3" data-type="s" style="width:100px">{L["channel"]}</th>
479
+ <th data-col="4" data-type="n" style="width:70px">{L["subs"]}</th>
480
+ <th data-col="5" data-type="n" style="width:75px">{L["views"]}</th>
481
+ <th data-col="6" data-type="n" style="width:60px">{L["likes"]}</th>
482
+ <th data-col="7" data-type="n" style="width:55px">{L["comments"]}</th>
483
+ <th data-col="8" data-type="n" style="width:65px">{L["ai_pick_col"]}</th>
484
+ <th data-col="9" data-type="s" style="width:90px">{L["date"]}</th>
485
+ </tr></thead>
486
+ <tbody>
487
+ '''
488
 
489
  for i, v in enumerate(videos_data):
490
+ title_short = v["title"][:42] + "..." if len(v["title"]) > 42 else v["title"]
491
+ ch_short = v["channel_name"][:12] + "..." if len(v["channel_name"]) > 12 else v["channel_name"]
492
+ url = f"https://youtube.com/watch?v={v['video_id']}"
493
+ ch_url = f"https://youtube.com/channel/{v['channel_id']}"
494
  rating = ai_ratings.get(i, 0)
495
+ rank_color = "#FFD700" if i==0 else ("#C0C0C0" if i==1 else ("#CD7F32" if i==2 else "#EF4444"))
496
 
497
  html += f'''<tr>
498
+ <td style="text-align:center;font-family:'Bangers',cursive;color:{rank_color};font-size:1.1rem" data-v="{i+1}">{i+1}</td>
499
+ <td><a href="{url}" target="_blank"><img src="{v['thumbnail']}" width="110" height="62"></a></td>
500
+ <td data-v="{v['title'][:60].replace('"','&quot;')}"><a href="{url}" target="_blank" title="{v['title']}">{title_short}</a></td>
501
+ <td data-v="{v['channel_name'][:30].replace('"','&quot;')}"><a href="{ch_url}" target="_blank">{ch_short}</a></td>
502
+ <td data-v="{v['subs']}" style="text-align:right">{format_count(v['subs'])}</td>
503
+ <td data-v="{v['views']}" style="text-align:right;color:#3B82F6;font-weight:700">{format_count(v['views'])}</td>
504
+ <td data-v="{v['likes']}" style="text-align:right">{format_count(v['likes'])}</td>
505
+ <td data-v="{v['comments']}" style="text-align:right">{format_count(v['comments'])}</td>
506
+ <td data-v="{rating}" style="text-align:center" class="ai-pick">{get_rating_display(rating)}</td>
507
  <td data-v="{v['published_at'][:10]}">{v['published_at'][:10]}</td>
508
  </tr>'''
509
 
510
+ # JavaScript 정렬 - 즉시 실행
511
+ html += f'''
512
+ </tbody>
513
+ </table>
514
+ </div>
515
+
516
  <script>
517
+ (function() {{
518
+ var tbl = document.getElementById('tbl_{uid}');
519
+ if (!tbl) return;
520
+
521
+ var sortState = {{}};
522
+
523
+ function sortTable(colIdx, type) {{
524
+ var tbody = tbl.querySelector('tbody');
525
+ var rows = Array.from(tbody.querySelectorAll('tr'));
526
+ var headers = tbl.querySelectorAll('th');
527
+
528
+ var asc = sortState[colIdx] !== 'asc';
529
+ sortState = {{}};
530
+ sortState[colIdx] = asc ? 'asc' : 'desc';
531
+
532
+ headers.forEach(function(h) {{ h.classList.remove('asc', 'desc'); }});
533
+ if (headers[colIdx]) headers[colIdx].classList.add(asc ? 'asc' : 'desc');
534
+
535
+ rows.sort(function(a, b) {{
536
+ var aCell = a.cells[colIdx];
537
+ var bCell = b.cells[colIdx];
538
+ if (!aCell || !bCell) return 0;
539
+
540
+ var aVal = aCell.getAttribute('data-v') || aCell.textContent.trim();
541
+ var bVal = bCell.getAttribute('data-v') || bCell.textContent.trim();
542
+
543
+ if (type === 'n') {{
544
+ aVal = parseFloat(aVal) || 0;
545
+ bVal = parseFloat(bVal) || 0;
546
+ return asc ? aVal - bVal : bVal - aVal;
547
+ }} else {{
548
+ return asc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
549
+ }}
550
+ }});
551
+
552
+ rows.forEach(function(row) {{ tbody.appendChild(row); }});
553
+ }}
554
+
555
+ // 헤더 클릭 이벤트
556
+ tbl.querySelectorAll('th[data-col]').forEach(function(th) {{
557
+ th.addEventListener('click', function() {{
558
+ var col = parseInt(this.getAttribute('data-col'));
559
+ var type = this.getAttribute('data-type');
560
+ sortTable(col, type);
561
+ }});
562
  }});
563
+
564
+ // 전역 정렬 함수
565
+ window.doSort_{uid} = function(col) {{
566
+ var type = (col >= 4 && col <= 8) ? 'n' : 's';
567
+ sortTable(col, type);
568
+ }};
569
+ }})();
570
+ </script>
571
+ '''
572
 
573
  stats = get_db_stats()
574
  return html, f"📊 DB: Videos {stats['videos']} | Records {stats['stats']} | Channels {stats['channels']} | Searches {stats['searches']}"
575
 
576
+ # ============================================
577
+ # 🔥 Trending Alerts (20%+ Surge)
578
+ # ============================================
579
  def show_trending_alerts(ui_lang):
580
  is_ko = ui_lang == "ko"
581
+
582
  conn = sqlite3.connect("youtube_data.db")
583
  c = conn.cursor()
584
  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
 
588
 
589
  info_box = f'''
590
  <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">
591
+ <h2 style="font-family:'Bangers',cursive;margin:0 0 15px 0;font-size:1.8rem">🔥 {"급상승 - 조회수 20%+ 급증 감지" if is_ko else "TRENDING - 20%+ Sudden View Surge"}</h2>
592
+ <table style="width:100%;color:#fff;font-size:14px;line-height:1.8">
593
+ <tr><td style="width:100px;font-weight:bold">📌 {"정의" if is_ko else "What"}</td><td>{"이전 대비 조회수가 20% 이상 급증한 영상" if is_ko else "Videos with 20%+ view increase vs. previous check"}</td></tr>
594
+ <tr><td style="font-weight:bold">🎯 {"목적" if is_ko else "Purpose"}</td><td>{"지금 바로 바이럴 중인 영상 포착" if is_ko else "Catch videos going viral RIGHT NOW"}</td></tr>
595
+ <tr><td style="font-weight:bold">⏱️ {"작동" if is_ko else "Trigger"}</td><td>{"동일 영상 재검색시 조회수 변화 감지" if is_ko else "Detected when same video is searched again"}</td></tr>
596
+ <tr><td style="font-weight:bold">💡 {"활용" if is_ko else "Best for"}</td><td>{"뉴스, 이슈, 핫토픽 발굴" if is_ko else "News, breaking stories, hot topics"}</td></tr>
597
  </table>
598
  </div>'''
599
 
600
  if not alerts:
601
  try:
602
+ resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR" if is_ko else "US", maxResults=20).execute()
603
+ html = info_box + f'''<div style="background:#FEF9C3;padding:15px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
604
+ <p style="margin:0">📢 {"아직 급상승 알림이 없습니다. 검색을 여러 번 실행하면 조회수 변화를 감지합니다!" if is_ko else "No surge alerts yet. Run searches multiple times to detect view changes!"}</p>
605
+ </div><h3 style="font-family:'Bangers',cursive;color:#1F2937">{"현재 인기 영상" if is_ko else "Current Popular Videos"}</h3><div style="display:flex;flex-wrap:wrap;gap:15px">'''
606
+
607
  for i, item in enumerate(resp.get("items", [])[:20], 1):
608
  snip, stats = item["snippet"], item["statistics"]
609
+ title = snip["title"][:32] + "..." if len(snip["title"]) > 32 else snip["title"]
610
+ html += f'''<div style="width:190px;background:#FFF;border:3px solid #1F2937;border-radius:8px;padding:12px;box-shadow:4px 4px 0 #1F2937;font-family:'Comic Neue',cursive">
611
  <a href="https://youtube.com/watch?v={item['id']}" target="_blank"><img src="{snip['thumbnails']['medium']['url']}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
612
+ <p style="margin:10px 0 5px;font-size:12px;font-weight:700;color:#1F2937">{i}. {title}</p>
613
+ <p style="margin:0;font-size:16px;color:#EF4444;font-weight:700;font-family:'Bangers',cursive">👀 {format_count(int(stats.get('viewCount',0)))}</p>
614
  </div>'''
615
  return html + '</div>'
616
  except Exception as e:
 
618
 
619
  html = info_box + '<div style="display:flex;flex-wrap:wrap;gap:15px">'
620
  for vid, title, channel, thumb, old_v, new_v, pct, detected in alerts:
621
+ title = title[:28] + "..." if len(title) > 28 else title
622
+ html += f'''<div style="width:190px;background:#FFF;border:3px solid #EF4444;border-radius:8px;padding:12px;box-shadow:4px 4px 0 #1F2937;font-family:'Comic Neue',cursive">
623
  <a href="https://youtube.com/watch?v={vid}" target="_blank"><img src="{thumb}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
624
  <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{title}</p>
625
+ <p style="margin:0;font-size:11px;color:#666">{channel[:18]}</p>
626
+ <p style="margin:5px 0 0;font-size:22px;color:#EF4444;font-weight:700;font-family:'Bangers',cursive">🔥 +{pct:.1f}%</p>
627
  <p style="margin:2px 0 0;font-size:11px;color:#888">{format_count(old_v)} → {format_count(new_v)}</p>
628
  </div>'''
629
  return html + '</div>'
630
 
631
+ # ============================================
632
+ # 📈 Top Growing (48h Growth Rate)
633
+ # ============================================
634
  def show_top_growing(ui_lang):
635
  is_ko = ui_lang == "ko"
636
+
637
  conn = sqlite3.connect("youtube_data.db")
638
  c = conn.cursor()
639
  cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
640
  c.execute('''SELECT v.video_id, v.title, v.channel_name, v.thumbnail,
641
  MIN(vs.views) as min_v, MAX(vs.views) as max_v,
642
+ ((MAX(vs.views) - MIN(vs.views)) * 100.0 / NULLIF(MIN(vs.views),0)) as growth
643
  FROM videos v JOIN video_stats vs ON v.video_id = vs.video_id
644
  WHERE vs.recorded_at > ? GROUP BY v.video_id HAVING min_v > 0 AND max_v > min_v
645
  ORDER BY growth DESC LIMIT 20''', (cutoff,))
 
648
 
649
  info_box = f'''
650
  <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">
651
+ <h2 style="font-family:'Bangers',cursive;margin:0 0 15px 0;font-size:1.8rem">📈 {"급성장 TOP - 48시간 성장률 순위" if is_ko else "TOP GROWING - 48h Growth Rate Ranking"}</h2>
652
+ <table style="width:100%;color:#fff;font-size:14px;line-height:1.8">
653
+ <tr><td style="width:100px;font-weight:bold">📌 {"정의" if is_ko else "What"}</td><td>{"48시간 동안 가장 높은 성장률을 기록한 영상" if is_ko else "Videos with highest growth RATE over 48 hours"}</td></tr>
654
+ <tr><td style="font-weight:bold">🎯 {"목적" if is_ko else "Purpose"}</td><td>{"꾸준히 성장하는 콘텐츠 발굴" if is_ko else "Find consistently rising content"}</td></tr>
655
+ <tr><td style="font-weight:bold">📊 {"계산" if is_ko else "Formula"}</td><td>(Max - Min) / Min × 100%</td></tr>
656
+ <tr><td style="font-weight:bold">💡 {"활용" if is_ko else "Best for"}</td><td>{"에버그린 콘텐츠, 안정적 트렌드" if is_ko else "Evergreen content, stable trends"}</td></tr>
657
  </table>
658
  </div>'''
659
 
660
  if not results:
661
  try:
662
+ resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR" if is_ko else "US", maxResults=20).execute()
663
+ html = info_box + f'''<div style="background:#FEF9C3;padding:15px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
664
+ <p style="margin:0">📢 {"데이터 축적 중입니다. 검색을 여러 번 실행하면 성장률이 계산됩니다!" if is_ko else "Accumulating data. Run searches over time to calculate growth rates!"}</p>
665
+ </div><h3 style="font-family:'Bangers',cursive;color:#1F2937">{"현재 인기 영상" if is_ko else "Current Popular Videos"}</h3><div style="display:flex;flex-wrap:wrap;gap:15px">'''
666
+
667
  for i, item in enumerate(resp.get("items", [])[:20], 1):
668
  snip, stats = item["snippet"], item["statistics"]
669
  views, likes = int(stats.get("viewCount", 0)), int(stats.get("likeCount", 0))
670
  engagement = (likes / views * 100) if views > 0 else 0
671
+ title = snip["title"][:28] + "..." if len(snip["title"]) > 28 else snip["title"]
672
+ html += f'''<div style="width:190px;background:#FFF;border:3px solid #1F2937;border-radius:8px;padding:12px;box-shadow:4px 4px 0 #1F2937;font-family:'Comic Neue',cursive">
673
  <a href="https://youtube.com/watch?v={item['id']}" target="_blank"><img src="{snip['thumbnails']['medium']['url']}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
674
  <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{i}. {title}</p>
675
+ <p style="margin:0;font-size:11px;color:#666">{snip['channelTitle'][:16]}</p>
676
  <p style="margin:5px 0 0;font-size:15px;color:#3B82F6;font-weight:700">👀 {format_count(views)}</p>
677
  <p style="margin:2px 0 0;font-size:12px;color:#EF4444">❤️ {engagement:.2f}%</p>
678
  </div>'''
 
682
 
683
  html = info_box + '<div style="display:flex;flex-wrap:wrap;gap:15px">'
684
  for i, (vid, title, channel, thumb, min_v, max_v, growth) in enumerate(results, 1):
685
+ title = title[:28] + "..." if len(title) > 28 else title
686
+ growth_val = growth if growth else 0
687
+ html += f'''<div style="width:190px;background:#FFF;border:3px solid #3B82F6;border-radius:8px;padding:12px;box-shadow:4px 4px 0 #1F2937;font-family:'Comic Neue',cursive">
688
  <a href="https://youtube.com/watch?v={vid}" target="_blank"><img src="{thumb}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
689
  <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{i}. {title}</p>
690
+ <p style="margin:0;font-size:11px;color:#666">{channel[:16]}</p>
691
+ <p style="margin:5px 0 0;font-size:22px;color:#3B82F6;font-weight:700;font-family:'Bangers',cursive">📈 +{growth_val:.1f}%</p>
692
  <p style="margin:2px 0 0;font-size:11px;color:#888">{format_count(int(min_v))} → {format_count(int(max_v))}</p>
693
  </div>'''
694
  return html + '</div>'
695
 
696
+ # ============================================
697
+ # ⭐ AI Pick (Fixed colors)
698
+ # ============================================
699
  def show_ai_picks(ui_lang):
700
  is_ko = ui_lang == "ko"
701
  global ai_pick_storage
702
+
703
  if not ai_pick_storage["videos"]:
704
+ return f'''<div style="background:#FACC15;padding:30px;border:3px solid #1F2937;border-radius:8px;text-align:center;font-family:'Comic Neue',cursive">
705
+ <h2 style="font-family:'Bangers',cursive;color:#1F2937">⭐ {"AI 추천 - 데이터 없음" if is_ko else "AI PICK - No Data Yet"}</h2>
706
+ <p style="color:#1F2937;font-size:16px">{"먼저 검색 탭에서 검색을 실행하세요!" if is_ko else "Run a search first in the Search tab!"}</p>
707
+ </div>'''
708
 
709
  videos, ratings = ai_pick_storage["videos"], ai_pick_storage["ratings"]
710
  top_picks = [(i, v, ratings.get(i, 0)) for i, v in enumerate(videos) if ratings.get(i, 0) >= 3]
711
  top_picks.sort(key=lambda x: (-x[2], -x[1]['views']))
712
 
713
+ # AI 분석 - 색상 수정됨
714
+ analysis_html = ""
715
  if groq_client and top_picks:
716
  info = "\n".join([f"- {v['title'][:50]} (Views:{format_count(v['views'])})" for _, v, _ in top_picks[:5]])
717
  lang_prompt = "한국어로 답변해주세요." if is_ko else ""
718
+ result = call_llm(f"Analyze top YouTube videos briefly (3-4 sentences):\n{info}\n\n1) Common theme 2) Why popular 3) Content opportunity. {lang_prompt}", 500)
719
+ if result and "Error" not in result and "⚠️" not in result:
720
+ analysis_html = f'''<div style="background:#1F2937;padding:20px;border:3px solid #10B981;border-radius:8px;margin:20px 0">
721
+ <h4 style="color:#FACC15;margin:0 0 10px;font-family:'Bangers',cursive;font-size:1.3rem">🤖 {"AI 분석 결과" if is_ko else "AI ANALYSIS"}</h4>
722
+ <p style="color:#10B981;margin:0;font-size:14px;line-height:1.8;font-family:'Courier New',monospace">{result}</p>
723
+ </div>'''
724
+
725
+ html = f'''
726
+ <div style="background:linear-gradient(135deg,#FACC15,#F59E0B);padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
727
+ <h2 style="font-family:'Bangers',cursive;color:#1F2937;margin:0;text-shadow:2px 2px 0 #FFF;font-size:2rem">⭐ {"AI 추천 - TOP 영상" if is_ko else "AI PICK - TOP RECOMMENDATIONS"}</h2>
728
+ <p style="color:#1F2937;margin:10px 0 0;font-family:'Comic Neue',cursive;font-weight:700;font-size:15px">
729
+ {"⭐⭐ 이상 등급 영상" if is_ko else "⭐⭐+ rated videos"}: <b>{len(top_picks)}</b>{"개" if is_ko else " videos"} | {"마지막 업데이트" if is_ko else "Last updated"}: {ai_pick_storage.get("timestamp", "")[:16].replace("T"," ")}
730
+ </p>
731
+ </div>
732
+ {analysis_html}
733
+ '''
734
 
735
+ if not top_picks:
736
+ html += f'''<div style="background:#FFF;padding:30px;border:3px solid #1F2937;border-radius:8px;text-align:center;font-family:'Comic Neue',cursive">
737
+ <p style="color:#1F2937;font-size:16px">{"⭐⭐ 이상 등급 영상이 없습니다. 다른 키워드로 검색해보세요!" if is_ko else "No ⭐⭐+ rated videos found. Try different keywords!"}</p>
738
+ </div>'''
739
+ return html
740
 
741
+ html += '<div style="display:flex;flex-wrap:wrap;gap:20px">'
742
  for idx, (_, v, rating) in enumerate(top_picks[:30], 1):
743
  border = "#FFD700" if rating == 4 else "#C0C0C0"
744
+ title = v["title"][:35] + "..." if len(v["title"]) > 35 else v["title"]
745
+ html += f'''<div style="width:210px;background:#FFF;border:4px solid {border};border-radius:12px;padding:15px;box-shadow:6px 6px 0 #1F2937;font-family:'Comic Neue',cursive">
746
+ <div style="position:relative">
747
+ <a href="https://youtube.com/watch?v={v['video_id']}" target="_blank"><img src="{v['thumbnail']}" style="width:100%;border-radius:8px;border:2px solid #1F2937"></a>
748
+ <span style="position:absolute;top:8px;right:8px;background:#1F2937;color:#FACC15;padding:4px 10px;border-radius:6px;font-family:'Bangers',cursive;font-size:18px">{get_rating_display(rating)}</span>
749
+ </div>
750
+ <p style="margin:12px 0 5px;font-size:13px;font-weight:700;color:#1F2937">{idx}. {title}</p>
751
+ <p style="margin:0;font-size:11px;color:#666"><a href="https://youtube.com/channel/{v['channel_id']}" target="_blank" style="color:#3B82F6">{v["channel_name"][:18]}</a></p>
752
+ <div style="display:flex;justify-content:space-between;margin-top:12px;font-size:12px">
753
  <span style="color:#3B82F6;font-weight:700">👀 {format_count(v['views'])}</span>
754
+ <span style="color:#EF4444;font-weight:700">❤️ {format_count(v['likes'])}</span>
755
+ <span style="color:#10B981;font-weight:700">💬 {format_count(v['comments'])}</span>
756
+ </div>
757
+ </div>'''
758
 
759
+ return html + '</div>'
760
 
761
+ # ============================================
762
+ # 🤖 AI Tools Functions
763
+ # ============================================
764
  def analyze_keyword_suggest(keyword, ui_lang):
765
+ if not keyword: return "⚠️ 키워드를 입력하세요!" if ui_lang == "ko" else "⚠️ Please enter a keyword!"
 
766
  lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
767
+ return call_llm(f'YouTube SEO expert. For "{keyword}", suggest 15 related keywords.\nFor each: keyword, search volume (High/Med/Low), competition (High/Med/Low), content type.\n{lang}', 1500)
768
 
769
  def analyze_trend_prediction(keyword, ui_lang):
770
+ if not keyword: return "⚠️ 키워드를 입력하세요!" if ui_lang == "ko" else "⚠️ Please enter a keyword!"
 
771
  lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
772
+ return call_llm(f'Trend analyst for "{keyword}":\n1) Current status\n2) Peak season\n3) 6-month forecast\n4) Risk factors\n5) Opportunity windows\n6) Emerging topics\n{lang}', 1500)
773
 
774
  def analyze_content_ideas(keyword, ui_lang):
775
+ if not keyword: return "⚠️ 주제를 입력하세요!" if ui_lang == "ko" else "⚠️ Please enter a topic!"
 
776
  lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
777
+ return call_llm(f'YouTube strategist for "{keyword}". Generate 10 video ideas:\nEach with: Title, Hook (first 5 sec), Format, Length, Thumbnail concept, Viral score 1-10.\n{lang}', 2000)
778
 
779
  def analyze_channel(channel_name, ui_lang):
780
+ if not channel_name: return "⚠️ 채널명을 입력하세요!" if ui_lang == "ko" else "⚠️ Please enter channel name!"
 
781
  lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
782
+ return call_llm(f'YouTube consultant for "{channel_name}":\n1) Niche assessment\n2) Content strategy\n3) Growth tactics\n4) Monetization\n5) Competitive advantages\n{lang}', 2000)
783
 
784
  def analyze_competitor(my_channel, competitor, ui_lang):
785
+ if not my_channel or not competitor: return "⚠️ 입력하세요!" if ui_lang == "ko" else "⚠️ Please enter both!"
 
786
  lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
787
+ return call_llm(f'Compare "{my_channel}" vs "{competitor}":\n1) Positioning\n2) Content gap\n3) Benchmarks\n4) Advantages\n5) Action plan\n6) 5 video ideas to beat them\n{lang}', 2000)
788
 
789
+ # ============================================
790
+ # 🕐 History
791
+ # ============================================
792
  def show_search_history(ui_lang):
793
  is_ko = ui_lang == "ko"
794
+
795
  conn = sqlite3.connect("youtube_data.db")
796
  c = conn.cursor()
797
  c.execute('SELECT keyword,country,language,sort_by,results_count,searched_at FROM search_history ORDER BY searched_at DESC LIMIT 50')
 
799
  conn.close()
800
 
801
  if not history:
802
+ return f'''<div style="background:#FFF;padding:30px;border:3px solid #1F2937;border-radius:8px;text-align:center;font-family:'Comic Neue',cursive">
803
+ <p style="color:#1F2937;font-size:16px">{"검색 기록이 없습니다." if is_ko else "No search history yet."}</p>
804
+ </div>'''
805
 
806
+ html = f'''
807
+ <div style="background:#1F2937;color:#FACC15;padding:15px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
808
+ <h3 style="font-family:'Bangers',cursive;margin:0;font-size:1.5rem">🕐 {"검색 기록" if is_ko else "SEARCH HISTORY"}</h3>
809
+ </div>
810
+ <div style="overflow-x:auto">
811
+ <table style="width:100%;border-collapse:collapse;font-family:'Comic Neue',cursive">
812
  <thead><tr style="background:#FACC15;color:#1F2937">
813
  <th style="padding:12px;border:2px solid #1F2937">{"검색어" if is_ko else "Keyword"}</th>
814
+ <th style="padding:12px;border:2px solid #1F2937">{"국가" if is_ko else "Country"}</th>
815
+ <th style="padding:12px;border:2px solid #1F2937">{"언어" if is_ko else "Lang"}</th>
816
+ <th style="padding:12px;border:2px solid #1F2937">{"정렬" if is_ko else "Sort"}</th>
817
+ <th style="padding:12px;border:2px solid #1F2937">{"결과" if is_ko else "Results"}</th>
818
+ <th style="padding:12px;border:2px solid #1F2937">{"시간" if is_ko else "Time"}</th>
819
  </tr></thead><tbody>'''
820
 
821
  for kw, country, lang, sort_by, cnt, searched in history:
822
  html += f'''<tr style="background:#FFF;border-bottom:2px solid #1F2937">
823
+ <td style="padding:12px;font-weight:700;color:#1F2937">{kw}</td>
824
+ <td style="padding:12px;color:#1F2937">{country}</td>
825
+ <td style="padding:12px;color:#1F2937">{lang[:10]}</td>
826
+ <td style="padding:12px;color:#1F2937">{sort_by[:10]}</td>
827
+ <td style="padding:12px;color:#3B82F6;font-weight:700">{cnt}</td>
828
+ <td style="padding:12px;font-size:12px;color:#666">{searched[:16].replace("T"," ")}</td>
829
  </tr>'''
830
 
831
+ return html + '</tbody></table></div>'
832
+
833
+ # ============================================
834
+ # 🌐 Language Switch
835
+ # ============================================
836
+ def on_lang_change(lang_choice):
837
+ return "ko" if lang_choice == "한국어" else "en"
838
 
839
+ def switch_ui_language(ui_lang):
840
+ L = UI_LANG.get(ui_lang, UI_LANG["en"])
 
841
  sort_opts = list(L["sort_options"].keys())
842
  date_opts = list(L["date_options"].keys())
843
  return (
 
859
  # 🎨 Gradio UI
860
  # ============================================
861
  with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
 
862
  ui_lang_state = gr.State("en")
863
 
864
  gr.HTML('''<div style="text-align:center;margin:20px 0">
 
866
  <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge">
867
  </a></div>''')
868
 
869
+ gr.Markdown("# 🎬 YOUTUBE TREND ANALYZER 📊", elem_classes="header-text")
 
 
870
 
871
  with gr.Row():
872
+ db_stats = gr.Markdown("📊 Loading...", scale=4)
873
+ ui_lang_dropdown = gr.Dropdown(choices=["English", "한국어"], value="English", label="🌐 UI Language", scale=1, interactive=True)
 
 
 
 
 
 
 
 
874
 
875
  with gr.Tabs():
 
876
  with gr.Tab("🔍 Search"):
877
  gr.Markdown("### 🔥 Trending Keywords")
878
  trending = gr.Radio(choices=initial_trending, label="Click to auto-fill", interactive=True)
879
  with gr.Row():
880
  keyword = gr.Textbox(label="Search Keyword", placeholder="Enter keyword...", scale=3)
881
  btn = gr.Button("🔍 SEARCH", variant="primary", scale=1)
882
+ refresh_btn = gr.Button("🔄 Refresh", variant="secondary", scale=1)
883
  with gr.Row():
884
  country = gr.Dropdown(list(COUNTRIES.keys()), value="United States", label="Country")
885
  language = gr.Dropdown(list(LANGUAGES.keys()), value="Auto (by Country)", label="Language")
 
888
  max_results = gr.Slider(10, 1000, value=100, step=10, label="Max Results")
889
  output = gr.HTML()
890
 
 
891
  with gr.Tab("⭐ AI Pick"):
892
  gr.Markdown("### 🤖 AI-Curated Top Recommendations (⭐⭐ and above)")
893
  pick_btn = gr.Button("🔄 Refresh AI Picks", variant="primary")
894
  pick_out = gr.HTML()
895
 
 
896
  with gr.Tab("🔥 Trending"):
897
  gr.Markdown("### 🔥 Sudden Surge Detection (20%+ view increase)")
898
  alerts_btn = gr.Button("🔄 Refresh", variant="primary")
899
  alerts_out = gr.HTML()
900
 
 
901
  with gr.Tab("📈 Top Growing"):
902
+ gr.Markdown("### 📈 48-Hour Growth Champions")
903
  growing_btn = gr.Button("🔄 Refresh", variant="primary")
904
  growing_out = gr.HTML()
905
 
 
906
  with gr.Tab("🤖 AI Tools"):
907
+ gr.Markdown("### 🧠 LLM-Powered Analysis (GPT-OSS-120B)")
908
  with gr.Tabs():
909
  with gr.Tab("🏷️ Keyword Suggest"):
 
910
  kw_input = gr.Textbox(label="Enter base keyword", placeholder="e.g., Python tutorial")
911
  kw_btn = gr.Button("🔍 Generate Keywords", variant="primary")
912
  kw_output = gr.Textbox(label="Suggested Keywords", lines=20, elem_classes="llm-result")
913
 
914
  with gr.Tab("🔮 Trend Prediction"):
915
+ tp_input = gr.Textbox(label="Enter topic", placeholder="e.g., AI tools")
 
916
  tp_btn = gr.Button("🔮 Predict Trend", variant="primary")
917
  tp_output = gr.Textbox(label="Trend Analysis", lines=20, elem_classes="llm-result")
918
 
919
  with gr.Tab("💡 Content Ideas"):
 
920
  ci_input = gr.Textbox(label="Enter topic", placeholder="e.g., Home workout")
921
  ci_btn = gr.Button("💡 Generate Ideas", variant="primary")
922
  ci_output = gr.Textbox(label="Content Ideas", lines=25, elem_classes="llm-result")
923
 
924
  with gr.Tab("📊 Channel Analysis"):
925
+ ca_input = gr.Textbox(label="Enter channel/niche", placeholder="e.g., Tech reviews")
926
+ ca_btn = gr.Button("📊 Analyze", variant="primary")
927
+ ca_output = gr.Textbox(label="Analysis", lines=25, elem_classes="llm-result")
 
928
 
929
+ with gr.Tab("⚔️ Competitor"):
 
930
  with gr.Row():
931
+ comp_my = gr.Textbox(label="Your Channel", placeholder="My channel")
932
+ comp_rival = gr.Textbox(label="Competitor", placeholder="Competitor")
933
+ comp_btn = gr.Button("⚔️ Compare", variant="primary")
934
+ comp_output = gr.Textbox(label="Analysis", lines=25, elem_classes="llm-result")
935
 
 
936
  with gr.Tab("🕐 History"):
937
  history_btn = gr.Button("🔄 Refresh", variant="primary")
938
  history_out = gr.HTML()
939
 
940
+ # Events
 
 
 
 
941
  ui_lang_dropdown.change(on_lang_change, ui_lang_dropdown, ui_lang_state)
942
+ ui_lang_dropdown.change(lambda x: switch_ui_language("ko" if x == "한국어" else "en"), ui_lang_dropdown,
943
+ [keyword, btn, refresh_btn, country, language, sort_by, date_filter, max_results, trending])
 
 
 
944
 
 
945
  trending.change(use_trending_keyword, trending, keyword)
946
  country.change(update_trending, country, trending)
947
+
948
  btn.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results, ui_lang_state], [output, db_stats])
949
  keyword.submit(search_videos, [keyword, country, language, sort_by, date_filter, max_results, ui_lang_state], [output, db_stats])
950
+ refresh_btn.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results, ui_lang_state], [output, db_stats])
951
 
952
  pick_btn.click(show_ai_picks, ui_lang_state, pick_out)
953
  alerts_btn.click(show_trending_alerts, ui_lang_state, alerts_out)
954
  growing_btn.click(show_top_growing, ui_lang_state, growing_out)
955
  history_btn.click(show_search_history, ui_lang_state, history_out)
956
 
 
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)
 
961
  comp_btn.click(analyze_competitor, [comp_my, comp_rival, ui_lang_state], comp_output)
962
 
963
  demo.launch()