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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +583 -552
app.py CHANGED
@@ -12,6 +12,9 @@ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
12
  youtube = build("youtube", "v3", developerKey=API_KEY)
13
  groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
14
 
 
 
 
15
  # ============================================
16
  # 🎨 Comic Classic Theme CSS
17
  # ============================================
@@ -37,7 +40,6 @@ footer, .footer, .gradio-container footer, .built-with, [class*="footer"] {
37
  text-align: center !important;
38
  text-shadow: 4px 4px 0px #FACC15, 6px 6px 0px #1F2937 !important;
39
  letter-spacing: 3px !important;
40
- -webkit-text-stroke: 1px #1F2937 !important;
41
  }
42
 
43
  .gr-panel, .gr-box, .gr-form, .block, .gr-group {
@@ -54,13 +56,10 @@ footer, .footer, .gradio-container footer, .built-with, [class*="footer"] {
54
  color: #FFFFFF !important;
55
  font-family: 'Bangers', cursive !important;
56
  font-size: 1.2rem !important;
57
- letter-spacing: 2px !important;
58
  box-shadow: 5px 5px 0px #1F2937 !important;
59
- text-shadow: 1px 1px 0px #1F2937 !important;
60
  }
61
 
62
- .gr-button-primary:hover { background: #2563EB !important; transform: translate(-2px, -2px) !important; }
63
- .gr-button-primary:active { transform: translate(3px, 3px) !important; box-shadow: 2px 2px 0px #1F2937 !important; }
64
 
65
  .gr-button-secondary, button.secondary {
66
  background: #EF4444 !important;
@@ -70,105 +69,96 @@ footer, .footer, .gradio-container footer, .built-with, [class*="footer"] {
70
  box-shadow: 4px 4px 0px #1F2937 !important;
71
  }
72
 
73
- textarea, input[type="text"], input[type="number"] {
74
  background: #FFFFFF !important;
75
  border: 3px solid #1F2937 !important;
76
  border-radius: 8px !important;
77
- color: #1F2937 !important;
78
  font-family: 'Comic Neue', cursive !important;
79
  font-weight: 700 !important;
80
  }
81
 
82
- label, .gr-input-label, .gr-block-label {
83
- color: #1F2937 !important;
84
- font-family: 'Comic Neue', cursive !important;
85
- font-weight: 700 !important;
86
- }
87
-
88
- .gr-accordion {
89
- background: #FACC15 !important;
90
- border: 3px solid #1F2937 !important;
91
- box-shadow: 4px 4px 0px #1F2937 !important;
92
- }
93
 
94
  ::-webkit-scrollbar { width: 12px; }
95
  ::-webkit-scrollbar-track { background: #FEF9C3; border: 2px solid #1F2937; }
96
  ::-webkit-scrollbar-thumb { background: #3B82F6; border: 2px solid #1F2937; }
97
- ::-webkit-scrollbar-thumb:hover { background: #EF4444; }
98
-
99
  ::selection { background: #FACC15; color: #1F2937; }
 
 
 
 
 
 
 
 
 
 
100
  """
101
 
102
  # DB 초기화
103
  def init_db():
104
  conn = sqlite3.connect("youtube_data.db")
105
  c = conn.cursor()
106
-
107
  c.execute('''CREATE TABLE IF NOT EXISTS videos (
108
  video_id TEXT PRIMARY KEY, title TEXT, channel_id TEXT, channel_name TEXT,
109
  thumbnail TEXT, published_at TEXT, first_seen TEXT
110
  )''')
111
-
112
  c.execute('''CREATE TABLE IF NOT EXISTS video_stats (
113
  id INTEGER PRIMARY KEY AUTOINCREMENT, video_id TEXT, views INTEGER,
114
  likes INTEGER, comments INTEGER, recorded_at TEXT
115
  )''')
116
-
117
  c.execute('''CREATE TABLE IF NOT EXISTS channels (
118
  channel_id TEXT PRIMARY KEY, channel_name TEXT, first_seen TEXT
119
  )''')
120
-
121
  c.execute('''CREATE TABLE IF NOT EXISTS channel_stats (
122
  id INTEGER PRIMARY KEY AUTOINCREMENT, channel_id TEXT, subscribers INTEGER, recorded_at TEXT
123
  )''')
124
-
125
  c.execute('''CREATE TABLE IF NOT EXISTS search_history (
126
  id INTEGER PRIMARY KEY AUTOINCREMENT, keyword TEXT, country TEXT,
127
  language TEXT, sort_by TEXT, results_count INTEGER, searched_at TEXT
128
  )''')
129
-
130
  c.execute('''CREATE TABLE IF NOT EXISTS trending_alerts (
131
  id INTEGER PRIMARY KEY AUTOINCREMENT, video_id TEXT, alert_type TEXT,
132
  old_value INTEGER, new_value INTEGER, change_percent REAL, detected_at TEXT
133
  )''')
134
-
135
  conn.commit()
136
  conn.close()
137
 
138
  init_db()
139
 
140
- # 국가 코드
141
  COUNTRIES = {
142
- "전세계": ("", ""), "중국": ("CN", "zh"), "인도": ("IN", "hi"), "미국": ("US", "en"),
143
- "인도네시아": ("ID", "id"), "파키스탄": ("PK", "ur"), "브라질": ("BR", "pt"),
144
- "나이지리아": ("NG", "en"), "방글라데시": ("BD", "bn"), "러시아": ("RU", "ru"),
145
- "멕시코": ("MX", "es"), "일본": ("JP", "ja"), "에티오피아": ("ET", "am"),
146
- "필리핀": ("PH", "tl"), "이집트": ("EG", "ar"), "베트남": ("VN", "vi"),
147
- "콩고민주공화국": ("CD", "fr"), "터키": ("TR", "tr"), "이란": ("IR", "fa"),
148
- "독일": ("DE", "de"), "태국": ("TH", "th"), "영국": ("GB", "en"),
149
- "프랑스": ("FR", "fr"), "이탈리아": ("IT", "it"), "남아공": ("ZA", "en"),
150
- "탄자니아": ("TZ", "sw"), "미얀마": ("MM", "my"), "한국": ("KR", "ko"),
151
- "콜롬비아": ("CO", "es"), "케냐": ("KE", "sw"), "스페인": ("ES", "es"),
152
- "아르헨티나": ("AR", "es"), "알제리": ("DZ", "ar"), "우크라이나": ("UA", "uk"),
153
- "이라크": ("IQ", "ar"), "캐나다": ("CA", "en"), "폴란드": ("PL", "pl"),
154
- "모로코": ("MA", "ar"), "사우디": ("SA", "ar"), "페루": ("PE", "es"), "말레이시아": ("MY", "ms"),
 
 
 
155
  }
156
 
157
  LANGUAGES = {
158
- "자동 (국가 기반)": "", "영어": "en", "중국어": "zh", "힌디어": "hi",
159
- "스페인어": "es", "아랍어": "ar", "벵골어": "bn", "포르투갈어": "pt",
160
- "러시아어": "ru", "일본어": "ja", "펀자브어": "pa", "독일어": "de",
161
- "인도네시아어": "id", "페르시아어": "fa", "프랑스어": "fr", "베트남어": "vi",
162
- "터키어": "tr", "한국어": "ko", "타밀어": "ta", "이탈리아어": "it",
163
- "텔루구어": "te", "마라티어": "mr", "우르두어": "ur", "태국어": "th",
164
- "구자라트어": "gu", "폴란드어": "pl", "말라얄람어": "ml", "칸나다어": "kn",
165
- "우크라이나어": "uk", "필리핀어": "tl", "미얀마어": "my", "네덜란드어": "nl",
166
- "루마니아어": "ro", "스와힐리어": "sw", "그리스어": "el", "체코어": "cs",
167
- "헝가리어": "hu", "스웨덴어": "sv", "히브리어": "he", "핀란드어": "fi", "노르웨이어": "no",
168
  }
169
 
170
- SORT_OPTIONS = {"조회수 ": "viewCount", "최신순": "date", "관련성 순": "relevance", "평점 ": "rating"}
171
- DATE_OPTIONS = {"전체 기간": "", "오늘": "today", "이번 ": "thisWeek", "이번 ": "thisMonth", "올해": "thisYear"}
172
 
173
  def format_count(count):
174
  if count >= 1000000000: return f"{count/1000000000:.1f}B"
@@ -176,201 +166,132 @@ def format_count(count):
176
  elif count >= 1000: return f"{count/1000:.1f}K"
177
  return str(count)
178
 
179
- def get_ai_pick_rating(videos_data):
180
- """Groq API (GPT-OSS-120B)로 AI Pick 등급 산정"""
181
- if not groq_client or not videos_data:
182
- return calculate_local_rating(videos_data)
183
-
184
  try:
185
- # 상위 50개만 분석 (토큰 제한)
186
- sample = videos_data[:50]
187
-
188
- video_info = "\n".join([
189
- f"#{i+1}. 제목:{v['title'][:40]}, 채널:{v['channel_name'][:15]}, "
190
- f"조회수:{v['views']}, 좋아요:{v['likes']}, 댓글:{v['comments']}, 구독자:{v.get('subs',0)}"
191
- for i, v in enumerate(sample)
192
- ])
193
-
194
- prompt = f"""YouTube 영상 목록을 분석하여 각 영상의 종합 품질 등급을 매겨주세요.
195
-
196
- 평가 기준:
197
- - 조회수 대비 좋아요/댓글 비율 (engagement)
198
- - 채널 구독자 대비 조회수 (viral potential)
199
- - 전반적인 인기도
200
-
201
- 등급 (숫자로만):
202
- 0=없음(평균이하), 1=♥(평균), 2=⭐(좋음), 3=⭐⭐(매우좋음), 4=⭐⭐⭐(최고)
203
-
204
- 각 영상 번호와 등급만 응답하세요.
205
- 형식: 1:3,2:2,3:4,4:1,5:0
206
-
207
- 영상 목록:
208
- {video_info}
209
-
210
- 응답 (번호:등급 형식으로만):"""
211
-
212
- # Groq API 호출 (스트리밍)
213
  completion = groq_client.chat.completions.create(
214
  model="openai/gpt-oss-120b",
215
  messages=[{"role": "user", "content": prompt}],
216
  temperature=0.7,
217
- max_completion_tokens=2000,
218
  top_p=1,
219
  stream=True,
220
  stop=None
221
  )
222
-
223
- # 스트리밍 응답 수집
224
- result_text = ""
225
  for chunk in completion:
226
  if chunk.choices[0].delta.content:
227
- result_text += chunk.choices[0].delta.content
228
-
229
- # 응답 파싱
230
- ratings = {}
231
- pairs = re.findall(r'(\d+):(\d)', result_text)
232
- for idx, rating in pairs:
233
- ratings[int(idx)-1] = int(rating)
234
-
235
- # 나머지 영상은 로컬 알고리즘으로
236
- if len(videos_data) > 50:
237
- local_ratings = calculate_local_rating(videos_data[50:])
238
- for k, v in local_ratings.items():
239
- ratings[k + 50] = v
240
-
241
- return ratings
242
-
243
  except Exception as e:
244
- print(f"Groq AI Rating Error: {e}")
245
- return calculate_local_rating(videos_data)
246
 
247
- def calculate_local_rating(videos_data):
248
- """로컬 알고리즘으로 등급 계산"""
249
- ratings = {}
250
-
251
  if not videos_data:
252
- return ratings
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  views_list = [v['views'] for v in videos_data if v['views'] > 0]
255
- if not views_list:
256
- return ratings
257
-
258
- avg_views = sum(views_list) / len(views_list)
259
- max_views = max(views_list)
260
 
261
  for i, v in enumerate(videos_data):
262
- views = v['views']
263
- likes = v['likes']
264
- comments = v['comments']
265
- subs = v.get('subs', 0)
266
-
267
  score = 0
268
-
269
- # 조회수 점수 (0-40)
270
  if views > 0:
271
- score += min(40, (views / max_views) * 40)
272
-
273
- # engagement 점수 (0-30)
274
- if views > 0:
275
- engagement = ((likes + comments * 2) / views) * 100
276
- score += min(30, engagement * 3)
277
-
278
- # viral 점수 (0-30)
279
  if subs > 0 and views > 0:
280
- score += min(30, (views / subs) * 10)
281
  elif views > avg_views:
282
  score += 15
283
 
284
- # 등급 결정
285
  if score >= 70: ratings[i] = 4
286
  elif score >= 50: ratings[i] = 3
287
  elif score >= 30: ratings[i] = 2
288
  elif score >= 15: ratings[i] = 1
289
  else: ratings[i] = 0
290
-
291
  return ratings
292
 
293
  def get_rating_display(rating):
294
- """등급을 이모지로 변환"""
295
  return {0: "", 1: "♥", 2: "⭐", 3: "⭐⭐", 4: "⭐⭐⭐"}.get(rating, "")
296
 
297
- def get_real_trending_keywords(region_code="KR", language="ko"):
298
  try:
299
- response = youtube.videos().list(
300
- part="snippet", chart="mostPopular",
301
- regionCode=region_code if region_code else "US", maxResults=50
302
- ).execute()
303
-
304
- keywords = []
305
- seen = set()
306
-
307
  for item in response.get("items", []):
308
- title = item["snippet"]["title"]
309
- channel = item["snippet"]["channelTitle"]
310
- tags = item["snippet"].get("tags", [])
311
-
312
- for tag in tags[:3]:
313
- tag_clean = tag.strip().lower()
314
- if tag_clean not in seen and 2 <= len(tag_clean) <= 20:
315
  keywords.append(tag)
316
- seen.add(tag_clean)
317
-
318
- if channel not in seen:
319
  keywords.append(channel)
320
  seen.add(channel.lower())
321
-
322
- words = re.findall(r'[가-힣]{2,10}' if region_code == "KR" else r'[A-Za-z]{3,15}', title)
323
- for word in words[:2]:
324
- if word.lower() not in seen and word.lower() not in ['the','and','for','with']:
325
- keywords.append(word)
326
- seen.add(word.lower())
327
-
328
- if len(keywords) >= 30:
329
- break
330
-
331
- popular = {
332
- "KR": ["뉴진스","BTS","먹방","브이로그","게임","shorts","드라마","영화","음악","코딩","AI","축구"],
333
- "US": ["Taylor Swift","NBA","NFL","gaming","vlog","music","AI","coding","shorts","news"],
334
- "JP": ["アニメ","ゲーム","音楽","vlog","料理","猫","映画","ASMR","shorts","AI"],
335
- }
336
- for kw in popular.get(region_code, popular["US"]):
337
- if kw.lower() not in seen:
338
- keywords.append(kw)
339
- seen.add(kw.lower())
340
-
341
- return keywords[:20] if keywords else ["AI","ChatGPT","뉴진스","BTS","게임","먹방","브이로그","shorts"]
342
  except:
343
- return ["AI","ChatGPT","뉴진스","BTS","게임","먹방","브이로그","shorts","음악","축구"]
344
 
345
  def save_to_db(videos_data, channels_data, keyword, country, language, sort_by):
346
  conn = sqlite3.connect("youtube_data.db")
347
  c = conn.cursor()
348
  now = datetime.now().isoformat()
349
-
350
- c.execute('INSERT INTO search_history (keyword,country,language,sort_by,results_count,searched_at) VALUES (?,?,?,?,?,?)',
351
- (keyword, country, language, sort_by, len(videos_data), now))
352
-
353
  for video in videos_data:
354
- c.execute('INSERT OR IGNORE INTO videos (video_id,title,channel_id,channel_name,thumbnail,published_at,first_seen) VALUES (?,?,?,?,?,?,?)',
355
  (video['video_id'], video['title'], video['channel_id'], video['channel_name'], video['thumbnail'], video['published_at'], now))
356
-
357
  c.execute('SELECT views FROM video_stats WHERE video_id=? ORDER BY recorded_at DESC LIMIT 1', (video['video_id'],))
358
  prev = c.fetchone()
359
-
360
- c.execute('INSERT INTO video_stats (video_id,views,likes,comments,recorded_at) VALUES (?,?,?,?,?)',
361
- (video['video_id'], video['views'], video['likes'], video['comments'], now))
362
-
363
  if prev and prev[0] > 0:
364
  change = ((video['views'] - prev[0]) / prev[0]) * 100
365
  if change >= 20:
366
- c.execute('INSERT INTO trending_alerts (video_id,alert_type,old_value,new_value,change_percent,detected_at) VALUES (?,?,?,?,?,?)',
367
- (video['video_id'], 'views_surge', prev[0], video['views'], change, now))
368
-
369
  for ch_id, subs in channels_data.items():
370
- c.execute('INSERT OR IGNORE INTO channels (channel_id,channel_name,first_seen) VALUES (?,?,?)', (ch_id, '', now))
371
  if isinstance(subs, int):
372
- c.execute('INSERT INTO channel_stats (channel_id,subscribers,recorded_at) VALUES (?,?,?)', (ch_id, subs, now))
373
-
374
  conn.commit()
375
  conn.close()
376
 
@@ -378,400 +299,458 @@ def get_db_stats():
378
  conn = sqlite3.connect("youtube_data.db")
379
  c = conn.cursor()
380
  stats = {}
381
- for table, key in [("videos","videos"),("video_stats","stats"),("channels","channels"),("search_history","searches"),("trending_alerts","alerts")]:
382
- c.execute(f"SELECT COUNT(*) FROM {table}")
383
- stats[key] = c.fetchone()[0]
384
  conn.close()
385
  return stats
386
 
387
  def update_trending(country):
388
  region, lang = COUNTRIES.get(country, ("", ""))
389
- keywords = get_real_trending_keywords(region or "US", lang)
390
- return gr.update(choices=keywords, value=None)
391
 
392
  def use_trending_keyword(kw):
393
  return kw if kw else ""
394
 
395
  def search_videos(keyword, country, language, sort_by, date_filter, max_results):
396
  if not keyword or not keyword.strip():
397
- return "⚠️ 검색어를 입력하세요!", ""
398
 
399
  max_results = int(max_results)
400
- all_items = []
401
- next_page = None
402
-
403
  region_code, default_lang = COUNTRIES.get(country, ("", ""))
404
- lang_code = default_lang if language == "자동 (국가 기반)" else LANGUAGES.get(language, "")
405
 
406
  params = {"q": keyword, "part": "snippet", "type": "video", "order": SORT_OPTIONS[sort_by]}
407
  if region_code: params["regionCode"] = region_code
408
  if lang_code: params["relevanceLanguage"] = lang_code
 
 
 
409
 
410
- if date_filter != "전체 기간":
411
- now = datetime.utcnow()
412
- deltas = {"오늘": 1, "이번 주": 7, "이번 달": 30, "올해": 365}
413
- after = now - timedelta(days=deltas.get(date_filter, 0))
414
- params["publishedAfter"] = after.strftime("%Y-%m-%dT%H:%M:%SZ")
415
-
416
- # 페이지네이션으로 최대 1000개까지
417
  while len(all_items) < max_results:
418
  params["maxResults"] = min(50, max_results - len(all_items))
419
  if next_page: params["pageToken"] = next_page
420
-
421
  resp = youtube.search().list(**params).execute()
422
  items = resp.get("items", [])
423
  if not items: break
424
-
425
  all_items.extend(items)
426
  next_page = resp.get("nextPageToken")
427
  if not next_page: break
428
 
429
- if not all_items:
430
- return "검색 결과가 없습니다.", ""
431
 
432
  video_ids = [item["id"]["videoId"] for item in all_items]
433
  channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
434
 
435
- # 비디오 통계
436
  video_stats = {}
437
  for i in range(0, len(video_ids), 50):
438
- batch = video_ids[i:i+50]
439
- details = youtube.videos().list(id=",".join(batch), part="statistics").execute()
440
- for v in details.get("items", []):
441
  s = v["statistics"]
442
- video_stats[v["id"]] = {
443
- "views": int(s.get("viewCount", 0)),
444
- "likes": int(s.get("likeCount", 0)),
445
- "comments": int(s.get("commentCount", 0)),
446
- }
447
-
448
- # 채널 통계
449
- channel_subs = {}
450
- channel_subs_raw = {}
451
  for i in range(0, len(channel_ids), 50):
452
- batch = channel_ids[i:i+50]
453
- details = youtube.channels().list(id=",".join(batch), part="statistics").execute()
454
- for ch in details.get("items", []):
455
  sub = ch["statistics"].get("subscriberCount", "0")
456
  if sub:
457
  channel_subs_raw[ch["id"]] = int(sub)
458
  channel_subs[ch["id"]] = format_count(int(sub))
459
- else:
460
- channel_subs[ch["id"]] = "비공개"
461
 
462
- # 데이터 준비
463
  videos_data = []
464
  for item in all_items:
465
- vid = item["id"]["videoId"]
466
- snip = item["snippet"]
467
  st = video_stats.get(vid, {})
468
  videos_data.append({
469
- "video_id": vid, "title": snip["title"],
470
- "channel_id": snip["channelId"], "channel_name": snip["channelTitle"],
471
- "thumbnail": snip["thumbnails"]["default"]["url"],
472
- "published_at": snip["publishedAt"],
473
- "views": st.get("views", 0), "likes": st.get("likes", 0),
474
- "comments": st.get("comments", 0),
475
  "subs": channel_subs_raw.get(snip["channelId"], 0),
476
  })
477
 
478
- # AI Pick 등급 계산 (Groq GPT-OSS-120B)
479
  ai_ratings = get_ai_pick_rating(videos_data)
480
-
481
- # DB 저장
482
  save_to_db(videos_data, channel_subs_raw, keyword, country, language, sort_by)
483
 
484
- # HTML 생성
 
485
  html = f'''
486
  <style>
487
- .yt-table {{ width:100%; border-collapse:collapse; font-family:'Comic Neue',cursive; }}
488
- .yt-table th {{
489
- background:#EF4444; color:white; padding:12px 8px; text-align:left;
490
- cursor:pointer; user-select:none; border:2px solid #1F2937;
491
- font-family:'Bangers',cursive; letter-spacing:1px;
492
- }}
493
- .yt-table th:hover {{ background:#DC2626; }}
494
- .yt-table th.sort-asc::after {{ content:" ▲"; }}
495
- .yt-table th.sort-desc::after {{ content:" ▼"; }}
496
- .yt-table td {{ padding:10px 8px; border-bottom:2px solid #1F2937; vertical-align:middle; background:#FFFFFF; }}
497
- .yt-table tr:hover td {{ background:#FEF9C3; }}
498
- .yt-table img {{ border-radius:4px; border:2px solid #1F2937; }}
499
- .yt-table a {{ color:#3B82F6; text-decoration:none; font-weight:700; }}
500
- .yt-table a:hover {{ color:#EF4444; }}
501
- .yt-rank {{ font-family:'Bangers',cursive; color:#EF4444; text-align:center; font-size:1.1rem; }}
502
- .yt-views {{ font-weight:700; color:#3B82F6; }}
503
- .ai-pick {{ color:#FACC15; text-shadow:1px 1px 0 #1F2937; font-size:1.1rem; }}
504
- .result-header {{
505
- background:#3B82F6; color:white; padding:15px; border-radius:8px;
506
- border:3px solid #1F2937; box-shadow:4px 4px 0 #1F2937;
507
- font-family:'Comic Neue',cursive; font-weight:700; margin-bottom:15px;
508
- }}
509
  </style>
510
-
511
- <div class="result-header">
512
- 🎬 총 <b>{len(all_items)}개</b> 결과 | 🔍 "{keyword}" | 🌍 {country} | 📊 {sort_by}
513
- <br><span style="font-size:0.9rem;">💡 헤더 클릭 = 정렬 | 🤖 AI Pick (GPT-OSS-120B): ♥ ⭐ ⭐⭐ ⭐⭐⭐</span>
 
 
 
 
 
 
 
514
  </div>
515
-
516
- <div style="max-height:700px; overflow-y:auto; border:3px solid #1F2937; border-radius:8px;">
517
- <table class="yt-table" id="ytTable">
518
- <thead><tr>
519
- <th data-col="0" data-type="num" style="width:50px;">순위</th>
520
- <th style="width:100px;">썸네일</th>
521
- <th data-col="2" data-type="str">제목</th>
522
- <th data-col="3" data-type="str" style="width:120px;">채널</th>
523
- <th data-col="4" data-type="num" style="width:70px;">구독자</th>
524
- <th data-col="5" data-type="num" style="width:70px;">조회수</th>
525
- <th data-col="6" data-type="num" style="width:55px;">좋아요</th>
526
- <th data-col="7" data-type="num" style="width:55px;">댓글</th>
527
- <th data-col="8" data-type="num" style="width:70px;">AI Pick</th>
528
- <th data-col="9" data-type="str" style="width:85px;">게시일</th>
529
- </tr></thead>
530
- <tbody>
531
- '''
532
 
533
  for i, v in enumerate(videos_data):
534
- vid = v["video_id"]
535
  title = v["title"][:45] + "..." if len(v["title"]) > 45 else v["title"]
536
  channel = v["channel_name"][:12] + "..." if len(v["channel_name"]) > 12 else v["channel_name"]
537
- url = f"https://youtube.com/watch?v={vid}"
538
- ch_url = f"https://youtube.com/channel/{v['channel_id']}"
539
- published = v["published_at"][:10]
540
-
541
- views = v["views"]
542
- likes = v["likes"]
543
- comments = v["comments"]
544
- subs = v["subs"]
545
-
546
  rating = ai_ratings.get(i, 0)
547
- rating_display = get_rating_display(rating)
548
-
549
- rank_style = ""
550
- if i == 0: rank_style = "color:#FFD700; font-size:1.3rem;"
551
- elif i == 1: rank_style = "color:#C0C0C0; font-size:1.2rem;"
552
- elif i == 2: rank_style = "color:#CD7F32; font-size:1.2rem;"
553
 
554
- html += f'''
555
- <tr>
556
- <td class="yt-rank" style="{rank_style}" data-v="{i+1}">{i+1}</td>
557
- <td><a href="{url}" target="_blank"><img src="{v['thumbnail']}" width="90"></a></td>
558
- <td data-v="{v['title']}"><a href="{url}" target="_blank">{title}</a></td>
559
  <td data-v="{v['channel_name']}"><a href="{ch_url}" target="_blank">{channel}</a></td>
560
- <td data-v="{subs}">{format_count(subs)}</td>
561
- <td class="yt-views" data-v="{views}">{format_count(views)}</td>
562
- <td data-v="{likes}">{format_count(likes)}</td>
563
- <td data-v="{comments}">{format_count(comments)}</td>
564
- <td class="ai-pick" data-v="{rating}">{rating_display}</td>
565
- <td data-v="{published}">{published}</td>
566
- </tr>
567
- '''
568
-
569
- html += '''
570
- </tbody>
571
- </table>
572
- </div>
573
-
574
  <script>
575
- (function(){
576
- var table = document.getElementById('ytTable');
577
- if(!table) return;
578
- var headers = table.querySelectorAll('th[data-col]');
579
- var sortCol = -1, sortAsc = true;
580
-
581
- headers.forEach(function(th){
582
- th.onclick = function(){
583
- var col = parseInt(this.dataset.col);
584
- var type = this.dataset.type;
585
- var tbody = table.querySelector('tbody');
586
- var rows = Array.from(tbody.querySelectorAll('tr'));
587
-
588
- if(sortCol === col){ sortAsc = !sortAsc; }
589
- else { sortCol = col; sortAsc = true; }
590
-
591
- headers.forEach(function(h){ h.classList.remove('sort-asc','sort-desc'); });
592
- this.classList.add(sortAsc ? 'sort-asc' : 'sort-desc');
593
-
594
- rows.sort(function(a,b){
595
- var aVal = a.cells[col].dataset.v || a.cells[col].textContent;
596
- var bVal = b.cells[col].dataset.v || b.cells[col].textContent;
597
- var result = 0;
598
- if(type === 'num'){
599
- result = parseFloat(aVal||0) - parseFloat(bVal||0);
600
- } else {
601
- result = (aVal||'').localeCompare(bVal||'','ko');
602
- }
603
- return sortAsc ? result : -result;
604
- });
605
-
606
- rows.forEach(function(row){ tbody.appendChild(row); });
607
- };
608
- });
609
- })();
610
- </script>
611
- '''
612
 
613
  stats = get_db_stats()
614
- stats_html = f"📊 DB: 영상 {stats['videos']} | 기록 {stats['stats']} | 채널 {stats['channels']} | 검색 {stats['searches']}"
615
-
616
- return html, stats_html
617
 
618
  def show_trending_alerts():
619
- """급상승 알림"""
620
  conn = sqlite3.connect("youtube_data.db")
621
  c = conn.cursor()
622
-
623
- c.execute('''
624
- SELECT ta.video_id, v.title, v.channel_name, ta.old_value, ta.new_value, ta.change_percent, ta.detected_at
625
- FROM trending_alerts ta
626
- JOIN videos v ON ta.video_id = v.video_id
627
- ORDER BY ta.detected_at DESC LIMIT 30
628
- ''')
629
  alerts = c.fetchall()
630
  conn.close()
631
 
 
 
 
 
 
 
 
 
 
 
 
632
  if not alerts:
633
  try:
634
- resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR", maxResults=20).execute()
635
-
636
- html = '''
637
- <div style="background:#FACC15; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px; font-family:'Comic Neue',cursive;">
638
- <h3 style="font-family:'Bangers',cursive; margin:0;">🔥 현재 급상승 인기 영상 (한국)</h3>
639
- <p style="margin:5px 0 0 0; font-size:0.9rem;">검색을 여러 번 실행하면 변화율 기반 알림이 표시됩니다</p>
640
- </div>
641
- <div style="display:flex; flex-wrap:wrap; gap:15px;">
642
- '''
643
-
644
  for i, item in enumerate(resp.get("items", [])[:20], 1):
645
- snip = item["snippet"]
646
- stats = item["statistics"]
647
- title = snip["title"][:30] + "..." if len(snip["title"]) > 30 else snip["title"]
648
- views = format_count(int(stats.get("viewCount", 0)))
649
- url = f"https://youtube.com/watch?v={item['id']}"
650
- thumb = snip["thumbnails"]["medium"]["url"]
651
-
652
- html += f'''
653
- <div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937; font-family:'Comic Neue',cursive;">
654
- <a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
655
- <p style="margin:8px 0 5px 0; font-size:12px; font-weight:700; color:#1F2937;">{i}. {title}</p>
656
- <p style="margin:0; font-size:14px; color:#EF4444; font-weight:700; font-family:'Bangers',cursive;">👀 {views}</p>
657
- </div>
658
- '''
659
-
660
- html += '</div>'
661
- return html
662
  except Exception as e:
663
- return f"<p style='font-family:Comic Neue,cursive;'>데이터 로드 오류: {e}</p>"
664
-
665
- html = '''
666
- <div style="background:#EF4444; color:white; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
667
- <h3 style="font-family:'Bangers',cursive; margin:0;">🔥 급상승 알림 (조회수 20%+ 증가)</h3>
668
- </div>
669
- <table style="width:100%; border-collapse:collapse; font-family:'Comic Neue',cursive;">
670
- <thead><tr style="background:#1F2937; color:#FACC15;">
671
- <th style="padding:10px; border:2px solid #1F2937;">영상</th>
672
- <th style="padding:10px;">채널</th><th style="padding:10px;">이전</th>
673
- <th style="padding:10px;">현재</th><th style="padding:10px;">증가율</th><th style="padding:10px;">감지시간</th>
674
- </tr></thead><tbody>
675
- '''
676
-
677
- for vid, title, channel, old_v, new_v, pct, detected in alerts:
678
- title = title[:25] + "..." if len(title) > 25 else title
679
- url = f"https://youtube.com/watch?v={vid}"
680
- html += f'''
681
- <tr style="background:#FFF; border-bottom:2px solid #1F2937;">
682
- <td style="padding:10px;"><a href="{url}" target="_blank" style="color:#3B82F6; font-weight:700;">{title}</a></td>
683
- <td style="padding:10px;">{channel}</td>
684
- <td style="padding:10px;">{format_count(old_v)}</td>
685
- <td style="padding:10px;">{format_count(new_v)}</td>
686
- <td style="padding:10px; color:#EF4444; font-weight:700;">+{pct:.1f}%</td>
687
- <td style="padding:10px; font-size:12px;">{detected[:16].replace("T"," ")}</td>
688
- </tr>
689
- '''
690
-
691
- html += '</tbody></table>'
692
- return html
693
 
694
  def show_top_growing():
695
- """급성장 TOP"""
696
  conn = sqlite3.connect("youtube_data.db")
697
  c = conn.cursor()
698
  cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
699
-
700
- c.execute('''
701
- SELECT v.video_id, v.title, v.channel_name, v.thumbnail,
702
  MIN(vs.views) as min_v, MAX(vs.views) as max_v,
703
  ((MAX(vs.views) - MIN(vs.views)) * 100.0 / MIN(vs.views)) as growth
704
- FROM videos v
705
- JOIN video_stats vs ON v.video_id = vs.video_id
706
- WHERE vs.recorded_at > ?
707
- GROUP BY v.video_id
708
- HAVING min_v > 0 AND max_v > min_v
709
- ORDER BY growth DESC LIMIT 20
710
- ''', (cutoff,))
711
  results = c.fetchall()
712
  conn.close()
713
 
 
 
 
 
 
 
 
 
 
 
 
714
  if not results:
715
  try:
716
- resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR", maxResults=20).execute()
717
-
718
- html = '''
719
- <div style="background:#3B82F6; color:white; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
720
- <h3 style="font-family:'Bangers',cursive; margin:0;">📈 현재 인기 급상승 TOP 20</h3>
721
- <p style="margin:5px 0 0 0; font-size:0.9rem; font-family:'Comic Neue',cursive;">검색을 여러 번 실행하면 성장률 데이터가 축적됩니다</p>
722
- </div>
723
- <div style="display:flex; flex-wrap:wrap; gap:15px;">
724
- '''
725
-
726
  for i, item in enumerate(resp.get("items", [])[:20], 1):
727
- snip = item["snippet"]
728
- stats = item["statistics"]
729
- views = int(stats.get("viewCount", 0))
730
- likes = int(stats.get("likeCount", 0))
731
  engagement = (likes / views * 100) if views > 0 else 0
732
-
733
- title = snip["title"][:25] + "..." if len(snip["title"]) > 25 else snip["title"]
734
- url = f"https://youtube.com/watch?v={item['id']}"
735
- thumb = snip["thumbnails"]["medium"]["url"]
736
-
737
- html += f'''
738
- <div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937; font-family:'Comic Neue',cursive;">
739
- <a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
740
- <p style="margin:8px 0 5px 0; font-size:12px; font-weight:700;">{i}. {title}</p>
741
- <p style="margin:0; font-size:11px; color:#666;">{snip["channelTitle"][:15]}</p>
742
- <p style="margin:5px 0 0 0; font-size:14px; color:#3B82F6; font-weight:700; font-family:'Bangers',cursive;">👀 {format_count(views)}</p>
743
- <p style="margin:2px 0 0 0; font-size:12px; color:#EF4444;">❤️ {engagement:.2f}%</p>
744
- </div>
745
- '''
746
-
747
- html += '</div>'
748
- return html
749
  except Exception as e:
750
- return f"<p>데이터 로드 오류: {e}</p>"
751
-
752
- html = '''
753
- <div style="background:#3B82F6; color:white; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
754
- <h3 style="font-family:'Bangers',cursive; margin:0;">📈 48시간 급성장 TOP 20</h3>
755
- </div>
756
- <div style="display:flex; flex-wrap:wrap; gap:15px;">
757
- '''
758
 
 
759
  for i, (vid, title, channel, thumb, min_v, max_v, growth) in enumerate(results, 1):
760
- title = title[:25] + "..." if len(title) > 25 else title
761
- url = f"https://youtube.com/watch?v={vid}"
762
-
763
- html += f'''
764
- <div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937; font-family:'Comic Neue',cursive;">
765
- <a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
766
- <p style="margin:8px 0 5px 0; font-size:12px; font-weight:700;">{i}. {title}</p>
767
- <p style="margin:0; font-size:11px; color:#666;">{channel[:15]}</p>
768
- <p style="margin:5px 0 0 0; font-size:16px; color:#EF4444; font-weight:700; font-family:'Bangers',cursive;">📈 +{growth:.1f}%</p>
769
- <p style="margin:2px 0 0 0; font-size:11px; color:#888;">{format_count(int(min_v))} → {format_count(int(max_v))}</p>
770
- </div>
771
- '''
772
-
773
- html += '</div>'
774
- return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
775
 
776
  def show_search_history():
777
  conn = sqlite3.connect("youtube_data.db")
@@ -781,90 +760,142 @@ def show_search_history():
781
  conn.close()
782
 
783
  if not history:
784
- return "<p style='font-family:Comic Neue,cursive;'>검색 기록이 없습니다.</p>"
785
 
786
- html = '''
787
- <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;">🕐 최근 검색 기록</h3>
789
- </div>
790
- <table style="width:100%; border-collapse:collapse; font-family:'Comic Neue',cursive;">
791
- <thead><tr style="background:#FACC15; color:#1F2937;">
792
- <th style="padding:10px; border:2px solid #1F2937;">검색어</th>
793
- <th style="padding:10px;">국가</th><th style="padding:10px;">언어</th>
794
- <th style="padding:10px;">정렬</th><th style="padding:10px;">결과</th><th style="padding:10px;">시간</th>
795
- </tr></thead><tbody>
796
- '''
797
 
798
  for kw, country, lang, sort_by, cnt, searched in history:
799
- html += f'''
800
- <tr style="background:#FFF; border-bottom:2px solid #1F2937;">
801
- <td style="padding:10px; font-weight:700;">{kw}</td>
802
- <td style="padding:10px;">{country}</td><td style="padding:10px;">{lang}</td>
803
- <td style="padding:10px;">{sort_by}</td><td style="padding:10px;">{cnt}</td>
804
- <td style="padding:10px; font-size:12px;">{searched[:16].replace("T"," ")}</td>
805
- </tr>
806
- '''
807
-
808
- html += '</tbody></table>'
809
- return html
810
-
811
- # 초기 트렌딩
812
- initial_trending = get_real_trending_keywords("KR", "ko")
813
-
814
- with gr.Blocks(css=css) as demo:
815
- gr.HTML('''
816
- <div style="text-align:center; margin:20px 0;">
817
- <a href="https://www.humangen.ai" target="_blank">
818
- <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge">
819
- </a>
820
- </div>
821
- ''')
822
 
823
- gr.Markdown("# 🎬 YOUTUBE TREND ANALYZER 📊", elem_classes="header-text")
 
 
 
 
 
 
 
 
 
 
 
 
824
 
825
- db_stats = gr.Markdown("📊 DB 로딩중...")
 
826
 
827
  with gr.Tabs():
828
- with gr.Tab("🔍 검색"):
829
- gr.Markdown("### 🔥 실시간 인기 검색어")
830
- trending = gr.Radio(choices=initial_trending, label="클릭시 자동 입력", interactive=True)
831
-
832
  with gr.Row():
833
- keyword = gr.Textbox(label="검색어", placeholder="검색어 입력...", scale=3)
834
- btn = gr.Button("🔍 검색", variant="primary", scale=1)
835
- refresh = gr.Button("🔄 갱신", variant="secondary", scale=1)
836
-
837
  with gr.Row():
838
- country = gr.Dropdown(list(COUNTRIES.keys()), value="한국", label="국가")
839
- language = gr.Dropdown(list(LANGUAGES.keys()), value="자동 (국가 기반)", label="언어")
840
- sort_by = gr.Dropdown(list(SORT_OPTIONS.keys()), value="조회수 ", label="정렬")
841
- date_filter = gr.Dropdown(list(DATE_OPTIONS.keys()), value="전체 기간", label="기간")
842
- max_results = gr.Slider(10, 1000, value=100, step=10, label="결과 ")
843
-
844
  output = gr.HTML()
845
 
846
- with gr.Tab("🔥 급상승"):
847
- alerts_btn = gr.Button("🔄 새로고침", variant="primary")
 
 
 
 
 
 
 
 
848
  alerts_out = gr.HTML()
849
 
850
- with gr.Tab("📈 급성장 TOP"):
851
- growing_btn = gr.Button("🔄 새로고침", variant="primary")
 
 
852
  growing_out = gr.HTML()
853
 
854
- with gr.Tab("🕐 검색 기록"):
855
- history_btn = gr.Button("🔄 새로고침", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
856
  history_out = gr.HTML()
857
 
858
- # 이벤트
859
  trending.change(use_trending_keyword, trending, keyword)
860
  country.change(update_trending, country, trending)
861
  btn.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results], [output, db_stats])
862
  keyword.submit(search_videos, [keyword, country, language, sort_by, date_filter, max_results], [output, db_stats])
863
  refresh.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results], [output, db_stats])
864
 
 
865
  alerts_btn.click(show_trending_alerts, outputs=alerts_out)
866
  growing_btn.click(show_top_growing, outputs=growing_out)
867
  history_btn.click(show_search_history, outputs=history_out)
 
 
 
 
 
 
 
868
 
869
  demo.launch()
870
 
 
 
12
  youtube = build("youtube", "v3", developerKey=API_KEY)
13
  groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
14
 
15
+ # Global storage
16
+ ai_pick_storage = {"videos": [], "ratings": {}, "timestamp": None}
17
+
18
  # ============================================
19
  # 🎨 Comic Classic Theme CSS
20
  # ============================================
 
40
  text-align: center !important;
41
  text-shadow: 4px 4px 0px #FACC15, 6px 6px 0px #1F2937 !important;
42
  letter-spacing: 3px !important;
 
43
  }
44
 
45
  .gr-panel, .gr-box, .gr-form, .block, .gr-group {
 
56
  color: #FFFFFF !important;
57
  font-family: 'Bangers', cursive !important;
58
  font-size: 1.2rem !important;
 
59
  box-shadow: 5px 5px 0px #1F2937 !important;
 
60
  }
61
 
62
+ .gr-button-primary:hover { background: #2563EB !important; }
 
63
 
64
  .gr-button-secondary, button.secondary {
65
  background: #EF4444 !important;
 
69
  box-shadow: 4px 4px 0px #1F2937 !important;
70
  }
71
 
72
+ textarea, input[type="text"] {
73
  background: #FFFFFF !important;
74
  border: 3px solid #1F2937 !important;
75
  border-radius: 8px !important;
 
76
  font-family: 'Comic Neue', cursive !important;
77
  font-weight: 700 !important;
78
  }
79
 
80
+ label { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important; font-weight: 700 !important; }
 
 
 
 
 
 
 
 
 
 
81
 
82
  ::-webkit-scrollbar { width: 12px; }
83
  ::-webkit-scrollbar-track { background: #FEF9C3; border: 2px solid #1F2937; }
84
  ::-webkit-scrollbar-thumb { background: #3B82F6; border: 2px solid #1F2937; }
 
 
85
  ::selection { background: #FACC15; color: #1F2937; }
86
+
87
+ .llm-result {
88
+ background: #1F2937 !important;
89
+ color: #10B981 !important;
90
+ border: 3px solid #10B981 !important;
91
+ border-radius: 8px !important;
92
+ padding: 15px !important;
93
+ font-family: 'Courier New', monospace !important;
94
+ white-space: pre-wrap !important;
95
+ }
96
  """
97
 
98
  # DB 초기화
99
  def init_db():
100
  conn = sqlite3.connect("youtube_data.db")
101
  c = conn.cursor()
 
102
  c.execute('''CREATE TABLE IF NOT EXISTS videos (
103
  video_id TEXT PRIMARY KEY, title TEXT, channel_id TEXT, channel_name TEXT,
104
  thumbnail TEXT, published_at TEXT, first_seen TEXT
105
  )''')
 
106
  c.execute('''CREATE TABLE IF NOT EXISTS video_stats (
107
  id INTEGER PRIMARY KEY AUTOINCREMENT, video_id TEXT, views INTEGER,
108
  likes INTEGER, comments INTEGER, recorded_at TEXT
109
  )''')
 
110
  c.execute('''CREATE TABLE IF NOT EXISTS channels (
111
  channel_id TEXT PRIMARY KEY, channel_name TEXT, first_seen TEXT
112
  )''')
 
113
  c.execute('''CREATE TABLE IF NOT EXISTS channel_stats (
114
  id INTEGER PRIMARY KEY AUTOINCREMENT, channel_id TEXT, subscribers INTEGER, recorded_at TEXT
115
  )''')
 
116
  c.execute('''CREATE TABLE IF NOT EXISTS search_history (
117
  id INTEGER PRIMARY KEY AUTOINCREMENT, keyword TEXT, country TEXT,
118
  language TEXT, sort_by TEXT, results_count INTEGER, searched_at TEXT
119
  )''')
 
120
  c.execute('''CREATE TABLE IF NOT EXISTS trending_alerts (
121
  id INTEGER PRIMARY KEY AUTOINCREMENT, video_id TEXT, alert_type TEXT,
122
  old_value INTEGER, new_value INTEGER, change_percent REAL, detected_at TEXT
123
  )''')
 
124
  conn.commit()
125
  conn.close()
126
 
127
  init_db()
128
 
129
+ # Country & Language codes
130
  COUNTRIES = {
131
+ "Worldwide": ("", ""), "United States": ("US", "en"), "United Kingdom": ("GB", "en"),
132
+ "Canada": ("CA", "en"), "Australia": ("AU", "en"), "Germany": ("DE", "de"),
133
+ "France": ("FR", "fr"), "Japan": ("JP", "ja"), "South Korea": ("KR", "ko"),
134
+ "Brazil": ("BR", "pt"), "Mexico": ("MX", "es"), "Spain": ("ES", "es"),
135
+ "Italy": ("IT", "it"), "Russia": ("RU", "ru"), "India": ("IN", "hi"),
136
+ "Indonesia": ("ID", "id"), "Thailand": ("TH", "th"), "Vietnam": ("VN", "vi"),
137
+ "Philippines": ("PH", "tl"), "Turkey": ("TR", "tr"), "Saudi Arabia": ("SA", "ar"),
138
+ "Egypt": ("EG", "ar"), "South Africa": ("ZA", "en"), "Nigeria": ("NG", "en"),
139
+ "Argentina": ("AR", "es"), "Colombia": ("CO", "es"), "Chile": ("CL", "es"),
140
+ "Poland": ("PL", "pl"), "Netherlands": ("NL", "nl"), "Belgium": ("BE", "nl"),
141
+ "Sweden": ("SE", "sv"), "Norway": ("NO", "no"), "Denmark": ("DK", "da"),
142
+ "Finland": ("FI", "fi"), "Austria": ("AT", "de"), "Switzerland": ("CH", "de"),
143
+ "Portugal": ("PT", "pt"), "Greece": ("GR", "el"), "Czech Republic": ("CZ", "cs"),
144
+ "Romania": ("RO", "ro"), "Ukraine": ("UA", "uk"), "Israel": ("IL", "he"),
145
+ "UAE": ("AE", "ar"), "Singapore": ("SG", "en"), "Malaysia": ("MY", "ms"),
146
+ "Taiwan": ("TW", "zh"), "Hong Kong": ("HK", "zh"), "China": ("CN", "zh"),
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",
156
+ "Hebrew": "he", "Czech": "cs", "Romanian": "ro", "Ukrainian": "uk",
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"
 
166
  elif count >= 1000: return f"{count/1000:.1f}K"
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:
185
  if chunk.choices[0].delta.content:
186
+ result += chunk.choices[0].delta.content
187
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  except Exception as e:
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 = {}
215
+ for idx, rating in re.findall(r'(\d+):(\d)', result):
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()}
228
+ return ratings
229
+
230
+ def calculate_local_rating(videos_data):
231
+ ratings = {}
232
+ if not videos_data: return ratings
233
  views_list = [v['views'] for v in videos_data if v['views'] > 0]
234
+ if not views_list: return ratings
235
+ avg_views, max_views = sum(views_list)/len(views_list), max(views_list)
 
 
 
236
 
237
  for i, v in enumerate(videos_data):
238
+ views, likes, comments, subs = v['views'], v['likes'], v['comments'], v.get('subs', 0)
 
 
 
 
239
  score = 0
 
 
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
250
  elif score >= 30: ratings[i] = 2
251
  elif score >= 15: ratings[i] = 1
252
  else: ratings[i] = 0
 
253
  return ratings
254
 
255
  def get_rating_display(rating):
 
256
  return {0: "", 1: "♥", 2: "⭐", 3: "⭐⭐", 4: "⭐⭐⭐"}.get(rating, "")
257
 
258
+ def get_real_trending_keywords(region_code="US", language="en"):
259
  try:
260
+ response = youtube.videos().list(part="snippet", chart="mostPopular", regionCode=region_code or "US", maxResults=50).execute()
261
+ keywords, seen = [], set()
 
 
 
 
 
 
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")
278
  c = conn.cursor()
279
  now = datetime.now().isoformat()
280
+ c.execute('INSERT INTO search_history VALUES (NULL,?,?,?,?,?,?)', (keyword, country, language, sort_by, len(videos_data), now))
 
 
 
281
  for video in videos_data:
282
+ c.execute('INSERT OR IGNORE INTO videos VALUES (?,?,?,?,?,?,?)',
283
  (video['video_id'], video['title'], video['channel_id'], video['channel_name'], video['thumbnail'], video['published_at'], now))
 
284
  c.execute('SELECT views FROM video_stats WHERE video_id=? ORDER BY recorded_at DESC LIMIT 1', (video['video_id'],))
285
  prev = c.fetchone()
286
+ c.execute('INSERT INTO video_stats VALUES (NULL,?,?,?,?,?)', (video['video_id'], video['views'], video['likes'], video['comments'], now))
 
 
 
287
  if prev and prev[0] > 0:
288
  change = ((video['views'] - prev[0]) / prev[0]) * 100
289
  if change >= 20:
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
 
 
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
 
308
  def update_trending(country):
309
  region, lang = COUNTRIES.get(country, ("", ""))
310
+ return gr.update(choices=get_real_trending_keywords(region or "US", lang or "en"), value=None)
 
311
 
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))
333
  if next_page: params["pageToken"] = next_page
 
334
  resp = youtube.search().list(**params).execute()
335
  items = resp.get("items", [])
336
  if not items: break
 
337
  all_items.extend(items)
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]))
345
 
 
346
  video_stats = {}
347
  for i in range(0, len(video_ids), 50):
348
+ for v in youtube.videos().list(id=",".join(video_ids[i:i+50]), part="statistics").execute().get("items", []):
 
 
349
  s = v["statistics"]
350
+ video_stats[v["id"]] = {"views": int(s.get("viewCount", 0)), "likes": int(s.get("likeCount", 0)), "comments": int(s.get("commentCount", 0))}
351
+
352
+ channel_subs, channel_subs_raw = {}, {}
 
 
 
 
 
 
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:
362
+ vid, snip = item["id"]["videoId"], item["snippet"]
 
363
  st = video_stats.get(vid, {})
364
  videos_data.append({
365
+ "video_id": vid, "title": snip["title"], "channel_id": snip["channelId"],
366
+ "channel_name": snip["channelTitle"], "thumbnail": snip["thumbnails"]["medium"]["url"],
367
+ "published_at": snip["publishedAt"], "views": st.get("views", 0),
368
+ "likes": st.get("likes", 0), "comments": st.get("comments", 0),
 
 
369
  "subs": channel_subs_raw.get(snip["channelId"], 0),
370
  })
371
 
 
372
  ai_ratings = get_ai_pick_rating(videos_data)
 
 
373
  save_to_db(videos_data, channel_subs_raw, keyword, country, language, sort_by)
374
 
375
+ table_id = f"yt_{datetime.now().strftime('%H%M%S%f')}"
376
+
377
  html = f'''
378
  <style>
379
+ .yt-tbl{{width:100%;border-collapse:collapse;font-family:'Comic Neue',cursive}}
380
+ .yt-tbl th{{background:#EF4444;color:#fff;padding:12px 8px;cursor:pointer;border:2px solid #1F2937;font-family:'Bangers',cursive}}
381
+ .yt-tbl th:hover{{background:#DC2626}}
382
+ .yt-tbl td{{padding:10px 8px;border-bottom:2px solid #1F2937;background:#FFF;vertical-align:middle}}
383
+ .yt-tbl tr:hover td{{background:#FEF9C3}}
384
+ .yt-tbl img{{border-radius:6px;border:2px solid #1F2937}}
385
+ .yt-tbl a{{color:#3B82F6;text-decoration:none;font-weight:700}}
386
+ .yt-tbl a:hover{{color:#EF4444}}
387
+ .ai-pick{{color:#FACC15;text-shadow:1px 1px 0 #1F2937;font-size:1.1rem}}
388
+ .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}}
389
+ .qsort{{display:inline-flex;gap:5px;margin-left:10px}}
390
+ .qsort button{{background:#FACC15;border:2px solid #1F2937;border-radius:4px;padding:5px 10px;cursor:pointer;font-weight:700}}
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):
 
420
  title = v["title"][:45] + "..." if len(v["title"]) > 45 else v["title"]
421
  channel = v["channel_name"][:12] + "..." if len(v["channel_name"]) > 12 else v["channel_name"]
422
+ url, ch_url = f"https://youtube.com/watch?v={v['video_id']}", f"https://youtube.com/channel/{v['channel_id']}"
 
 
 
 
 
 
 
 
423
  rating = ai_ratings.get(i, 0)
424
+ 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 ""))
 
 
 
 
 
425
 
426
+ html += f'''<tr>
427
+ <td style="text-align:center;font-family:'Bangers',cursive;color:#EF4444;{rank_style}" data-v="{i+1}">{i+1}</td>
428
+ <td><a href="{url}" target="_blank"><img src="{v['thumbnail']}" width="120" height="68"></a></td>
429
+ <td data-v="{v['title'][:80]}"><a href="{url}" target="_blank" title="{v['title']}">{title}</a></td>
 
430
  <td data-v="{v['channel_name']}"><a href="{ch_url}" target="_blank">{channel}</a></td>
431
+ <td data-v="{v['subs']}">{format_count(v['subs'])}</td>
432
+ <td data-v="{v['views']}" style="color:#3B82F6;font-weight:700">{format_count(v['views'])}</td>
433
+ <td data-v="{v['likes']}">{format_count(v['likes'])}</td>
434
+ <td data-v="{v['comments']}">{format_count(v['comments'])}</td>
435
+ <td class="ai-pick" data-v="{rating}">{get_rating_display(rating)}</td>
436
+ <td data-v="{v['published_at'][:10]}">{v['published_at'][:10]}</td>
437
+ </tr>'''
438
+
439
+ html += f'''</tbody></table></div>
 
 
 
 
 
440
  <script>
441
+ var ss_{table_id}={{}};
442
+ function sortT_{table_id}(c,t){{
443
+ var tb=document.getElementById('{table_id}'),tbody=tb.querySelector('tbody'),rows=Array.from(tbody.querySelectorAll('tr'));
444
+ var asc=ss_{table_id}[c]!=='asc';ss_{table_id}={{}};ss_{table_id}[c]=asc?'asc':'desc';
445
+ rows.sort((a,b)=>{{
446
+ var av=a.cells[c].dataset.v||a.cells[c].textContent,bv=b.cells[c].dataset.v||b.cells[c].textContent;
447
+ if(t==='n'){{av=parseFloat(av)||0;bv=parseFloat(bv)||0;return asc?av-bv:bv-av}}
448
+ return asc?av.localeCompare(bv):bv.localeCompare(av)
449
+ }});
450
+ rows.forEach(r=>tbody.appendChild(r))
451
+ }}
452
+ </script>'''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
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
462
+ FROM trending_alerts ta JOIN videos v ON ta.video_id = v.video_id ORDER BY ta.detected_at DESC LIMIT 30''')
 
 
 
 
 
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"]
484
+ 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">
485
+ <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>
486
+ <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{i}. {title}</p>
487
+ <p style="margin:0;font-size:15px;color:#EF4444;font-weight:700;font-family:'Bangers',cursive">👀 {format_count(int(stats.get('viewCount',0)))}</p>
488
+ </div>'''
489
+ return html + '</div>'
 
 
 
 
 
 
 
 
 
490
  except Exception as e:
491
+ return info_box + f"<p>Error: {e}</p>"
492
+
493
+ html = info_box + '<div style="display:flex;flex-wrap:wrap;gap:15px">'
494
+ for vid, title, channel, thumb, old_v, new_v, pct, detected in alerts:
495
+ title = title[:30] + "..." if len(title) > 30 else title
496
+ 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">
497
+ <a href="https://youtube.com/watch?v={vid}" target="_blank"><img src="{thumb}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
498
+ <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{title}</p>
499
+ <p style="margin:0;font-size:11px;color:#666">{channel[:20]}</p>
500
+ <p style="margin:5px 0 0;font-size:20px;color:#EF4444;font-weight:700;font-family:'Bangers',cursive">🔥 +{pct:.1f}%</p>
501
+ <p style="margin:2px 0 0;font-size:11px;color:#888">{format_count(old_v)} → {format_count(new_v)}</p>
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()
510
+ c.execute('''SELECT v.video_id, v.title, v.channel_name, v.thumbnail,
 
 
511
  MIN(vs.views) as min_v, MAX(vs.views) as max_v,
512
  ((MAX(vs.views) - MIN(vs.views)) * 100.0 / MIN(vs.views)) as growth
513
+ FROM videos v JOIN video_stats vs ON v.video_id = vs.video_id
514
+ WHERE vs.recorded_at > ? GROUP BY v.video_id HAVING min_v > 0 AND max_v > min_v
515
+ ORDER BY growth DESC LIMIT 20''', (cutoff,))
 
 
 
 
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))
 
 
537
  engagement = (likes / views * 100) if views > 0 else 0
538
+ title = snip["title"][:30] + "..." if len(snip["title"]) > 30 else snip["title"]
539
+ 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">
540
+ <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>
541
+ <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{i}. {title}</p>
542
+ <p style="margin:0;font-size:11px;color:#666">{snip['channelTitle'][:18]}</p>
543
+ <p style="margin:5px 0 0;font-size:15px;color:#3B82F6;font-weight:700">👀 {format_count(views)}</p>
544
+ <p style="margin:2px 0 0;font-size:12px;color:#EF4444">❤️ {engagement:.2f}%</p>
545
+ </div>'''
546
+ return html + '</div>'
 
 
 
 
 
 
 
 
547
  except Exception as e:
548
+ return info_box + f"<p>Error: {e}</p>"
 
 
 
 
 
 
 
549
 
550
+ html = info_box + '<div style="display:flex;flex-wrap:wrap;gap:15px">'
551
  for i, (vid, title, channel, thumb, min_v, max_v, growth) in enumerate(results, 1):
552
+ title = title[:30] + "..." if len(title) > 30 else title
553
+ 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">
554
+ <a href="https://youtube.com/watch?v={vid}" target="_blank"><img src="{thumb}" style="width:100%;border-radius:5px;border:2px solid #1F2937"></a>
555
+ <p style="margin:10px 0 5px;font-size:12px;font-weight:700">{i}. {title}</p>
556
+ <p style="margin:0;font-size:11px;color:#666">{channel[:18]}</p>
557
+ <p style="margin:5px 0 0;font-size:20px;color:#3B82F6;font-weight:700;font-family:'Bangers',cursive">📈 +{growth:.1f}%</p>
558
+ <p style="margin:2px 0 0;font-size:11px;color:#888">{format_count(int(min_v))} {format_count(int(max_v))}</p>
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]
570
+ top_picks.sort(key=lambda x: (-x[2], -x[1]['views']))
571
+
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):
585
+ border = "#FFD700" if rating == 4 else "#C0C0C0"
586
+ title = v["title"][:40] + "..." if len(v["title"]) > 40 else v["title"]
587
+ 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">
588
+ <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>
589
+ <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>
590
+ <p style="margin:12px 0 5px;font-size:13px;font-weight:700">{idx}. {title}</p>
591
+ <p style="margin:0;font-size:11px;color:#666">{v["channel_name"][:20]}</p>
592
+ <div style="display:flex;justify-content:space-between;margin-top:10px;font-size:12px">
593
+ <span style="color:#3B82F6;font-weight:700">👀 {format_count(v['views'])}</span>
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")
 
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:
775
+ html += f'''<tr style="background:#FFF;border-bottom:2px solid #1F2937">
776
+ <td style="padding:12px;font-weight:700">{kw}</td><td style="padding:12px">{country}</td>
777
+ <td style="padding:12px">{lang}</td><td style="padding:12px">{sort_by}</td>
778
+ <td style="padding:12px">{cnt}</td><td style="padding:12px;font-size:12px">{searched[:16].replace("T"," ")}</td>
779
+ </tr>'''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
780
 
781
+ return html + '</tbody></table>'
782
+
783
+ # Initial trending
784
+ initial_trending = get_real_trending_keywords("US", "en")
785
+
786
+ # ============================================
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
800
+ with gr.Tab("🔍 Search"):
801
+ gr.Markdown("### 🔥 Trending Keywords")
802
+ trending = gr.Radio(choices=initial_trending, label="Click to auto-fill", interactive=True)
803
  with gr.Row():
804
+ keyword = gr.Textbox(label="Search Keyword", placeholder="Enter keyword...", scale=3)
805
+ btn = gr.Button("🔍 SEARCH", variant="primary", scale=1)
806
+ refresh = gr.Button("🔄 Refresh", variant="secondary", scale=1)
 
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
 
815
+ # Tab 2: AI Pick
816
+ with gr.Tab(" AI Pick"):
817
+ gr.Markdown("### 🤖 AI-Curated Top Recommendations (⭐⭐ and above)")
818
+ pick_btn = gr.Button("🔄 Refresh AI Picks", variant="primary")
819
+ pick_out = gr.HTML()
820
+
821
+ # Tab 3: Trending
822
+ with gr.Tab("🔥 Trending"):
823
+ gr.Markdown("### 🔥 Sudden Surge Detection (20%+ view increase)")
824
+ alerts_btn = gr.Button("🔄 Refresh", variant="primary")
825
  alerts_out = gr.HTML()
826
 
827
+ # Tab 4: Top Growing
828
+ with gr.Tab("📈 Top Growing"):
829
+ gr.Markdown("### 📈 48-Hour Growth Champions (Highest growth rate)")
830
+ growing_btn = gr.Button("🔄 Refresh", variant="primary")
831
  growing_out = gr.HTML()
832
 
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():
870
+ comp_my = gr.Textbox(label="Your Channel/Topic", placeholder="e.g., My cooking channel")
871
+ comp_rival = gr.Textbox(label="Competitor", placeholder="e.g., Gordon Ramsay")
872
+ comp_btn = gr.Button("⚔️ Analyze Competition", variant="primary")
873
+ comp_output = gr.Textbox(label="Competitive Analysis", lines=25, elem_classes="llm-result")
874
+
875
+ # Tab 6: History
876
+ with gr.Tab("🕐 History"):
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
 
901
+