seawolf2357 commited on
Commit
dd401c6
·
verified ·
1 Parent(s): 0cd2b77

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -84
app.py CHANGED
@@ -1,16 +1,16 @@
1
  import gradio as gr
2
  from googleapiclient.discovery import build
 
3
  import os
4
  import sqlite3
5
  from datetime import datetime, timedelta
6
  import re
7
- import anthropic
8
 
9
  API_KEY = os.getenv("YOUTUBE_API_KEY")
10
- ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
11
 
12
  youtube = build("youtube", "v3", developerKey=API_KEY)
13
- claude_client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) if ANTHROPIC_API_KEY else None
14
 
15
  # ============================================
16
  # 🎨 Comic Classic Theme CSS
@@ -177,62 +177,71 @@ def format_count(count):
177
  return str(count)
178
 
179
  def get_ai_pick_rating(videos_data):
180
- """Claude API로 AI Pick 등급 산정"""
181
- if not claude_client or not videos_data:
182
- # API 없으면 자체 알고리즘으로 계산
183
  return calculate_local_rating(videos_data)
184
 
185
  try:
186
- # 상위 100개만 분석 (API 비용 절감)
187
- sample = videos_data[:100]
188
 
189
  video_info = "\n".join([
190
- f"#{i+1}. 제목: {v['title'][:50]}, 채널: {v['channel_name']}, "
191
- f"조회수: {v['views']}, 좋아요: {v['likes']}, 댓글: {v['comments']}, "
192
- f"구독자: {v.get('subs', 0)}"
193
  for i, v in enumerate(sample)
194
  ])
195
 
196
- response = claude_client.messages.create(
197
- model="claude-sonnet-4-20250514",
198
- max_tokens=2000,
199
- messages=[{
200
- "role": "user",
201
- "content": f"""다음 YouTube 영상 목록을 분석하여 각 영상의 종합 품질 등급을 매겨주세요.
202
 
203
  평가 기준:
204
  - 조회수 대비 좋아요/댓글 비율 (engagement)
205
  - 채널 구독자 대비 조회수 (viral potential)
206
- - 전반적인 인기도와 성장 가능성
207
 
208
- 등급 (숫자로만 응답):
209
- 0 = 없음 (평균 이하)
210
- 1 = ♥ (평균)
211
- 2 = ⭐ (좋음)
212
- 3 = ⭐⭐ (매우 좋음)
213
- 4 = ⭐⭐⭐ (최고)
214
 
215
- 각 영상 번호와 등급만 쉼표로 구분하여 응답하세요.
216
- 예시: 1:3,2:2,3:4,4:1,5:0
217
 
218
  영상 목록:
219
  {video_info}
220
 
221
  응답 (번호:등급 형식으로만):"""
222
- }]
 
 
 
 
 
 
 
 
 
223
  )
224
 
 
 
 
 
 
 
225
  # 응답 파싱
226
  ratings = {}
227
- result_text = response.content[0].text.strip()
228
  pairs = re.findall(r'(\d+):(\d)', result_text)
229
  for idx, rating in pairs:
230
  ratings[int(idx)-1] = int(rating)
231
 
 
 
 
 
 
 
232
  return ratings
233
 
234
  except Exception as e:
235
- print(f"AI Rating Error: {e}")
236
  return calculate_local_rating(videos_data)
237
 
238
  def calculate_local_rating(videos_data):
@@ -242,7 +251,6 @@ def calculate_local_rating(videos_data):
242
  if not videos_data:
243
  return ratings
244
 
245
- # 통계 계산
246
  views_list = [v['views'] for v in videos_data if v['views'] > 0]
247
  if not views_list:
248
  return ratings
@@ -256,52 +264,35 @@ def calculate_local_rating(videos_data):
256
  comments = v['comments']
257
  subs = v.get('subs', 0)
258
 
259
- # 스코어 계산
260
  score = 0
261
 
262
  # 조회수 점수 (0-40)
263
  if views > 0:
264
- view_score = min(40, (views / max_views) * 40)
265
- score += view_score
266
 
267
  # engagement 점수 (0-30)
268
  if views > 0:
269
  engagement = ((likes + comments * 2) / views) * 100
270
- engagement_score = min(30, engagement * 3)
271
- score += engagement_score
272
 
273
- # viral 점수 (구독자 대비 조회수) (0-30)
274
  if subs > 0 and views > 0:
275
- viral = views / subs
276
- viral_score = min(30, viral * 10)
277
- score += viral_score
278
  elif views > avg_views:
279
  score += 15
280
 
281
  # 등급 결정
282
- if score >= 70:
283
- ratings[i] = 4 # ⭐⭐⭐
284
- elif score >= 50:
285
- ratings[i] = 3 # ⭐⭐
286
- elif score >= 30:
287
- ratings[i] = 2 # ⭐
288
- elif score >= 15:
289
- ratings[i] = 1 # ♥
290
- else:
291
- ratings[i] = 0 # 없음
292
 
293
  return ratings
294
 
295
  def get_rating_display(rating):
296
  """등급을 이모지로 변환"""
297
- displays = {
298
- 0: "",
299
- 1: "♥",
300
- 2: "⭐",
301
- 3: "⭐⭐",
302
- 4: "⭐⭐⭐"
303
- }
304
- return displays.get(rating, "")
305
 
306
  def get_real_trending_keywords(region_code="KR", language="ko"):
307
  try:
@@ -484,7 +475,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
484
  "subs": channel_subs_raw.get(snip["channelId"], 0),
485
  })
486
 
487
- # AI Pick 등급 계산
488
  ai_ratings = get_ai_pick_rating(videos_data)
489
 
490
  # DB 저장
@@ -519,7 +510,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
519
 
520
  <div class="result-header">
521
  🎬 총 <b>{len(all_items)}개</b> 결과 | 🔍 "{keyword}" | 🌍 {country} | 📊 {sort_by}
522
- <br><span style="font-size:0.9rem;">💡 헤더 클릭 = 정렬 | AI Pick: (평균) ⭐(좋음) ⭐⭐(매우좋음) ⭐⭐⭐(최고)</span>
523
  </div>
524
 
525
  <div style="max-height:700px; overflow-y:auto; border:3px solid #1F2937; border-radius:8px;">
@@ -625,11 +616,10 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
625
  return html, stats_html
626
 
627
  def show_trending_alerts():
628
- """급상승 알림 + 실시간 데이터 갱신"""
629
  conn = sqlite3.connect("youtube_data.db")
630
  c = conn.cursor()
631
 
632
- # 최근 급상승 알림
633
  c.execute('''
634
  SELECT ta.video_id, v.title, v.channel_name, ta.old_value, ta.new_value, ta.change_percent, ta.detected_at
635
  FROM trending_alerts ta
@@ -640,14 +630,13 @@ def show_trending_alerts():
640
  conn.close()
641
 
642
  if not alerts:
643
- # 알림 없으면 현재 인기 급상승 영상 직접 조회
644
  try:
645
  resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR", maxResults=20).execute()
646
 
647
  html = '''
648
- <div style="background:#FACC15; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
649
  <h3 style="font-family:'Bangers',cursive; margin:0;">🔥 현재 급상승 인기 영상 (한국)</h3>
650
- <p style="margin:5px 0 0 0; font-size:0.9rem;">데이터 축적 변화율 기반 알림이 표시됩니다</p>
651
  </div>
652
  <div style="display:flex; flex-wrap:wrap; gap:15px;">
653
  '''
@@ -655,24 +644,23 @@ def show_trending_alerts():
655
  for i, item in enumerate(resp.get("items", [])[:20], 1):
656
  snip = item["snippet"]
657
  stats = item["statistics"]
658
- vid = item["id"]
659
  title = snip["title"][:30] + "..." if len(snip["title"]) > 30 else snip["title"]
660
  views = format_count(int(stats.get("viewCount", 0)))
661
- url = f"https://youtube.com/watch?v={vid}"
662
  thumb = snip["thumbnails"]["medium"]["url"]
663
 
664
  html += f'''
665
- <div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937;">
666
  <a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
667
  <p style="margin:8px 0 5px 0; font-size:12px; font-weight:700; color:#1F2937;">{i}. {title}</p>
668
- <p style="margin:0; font-size:14px; color:#EF4444; font-weight:700;">👀 {views}</p>
669
  </div>
670
  '''
671
 
672
  html += '</div>'
673
  return html
674
  except Exception as e:
675
- return f"<p>데이터 로드 오류: {e}</p>"
676
 
677
  html = '''
678
  <div style="background:#EF4444; color:white; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
@@ -681,11 +669,8 @@ def show_trending_alerts():
681
  <table style="width:100%; border-collapse:collapse; font-family:'Comic Neue',cursive;">
682
  <thead><tr style="background:#1F2937; color:#FACC15;">
683
  <th style="padding:10px; border:2px solid #1F2937;">영상</th>
684
- <th style="padding:10px;">채널</th>
685
- <th style="padding:10px;">이전</th>
686
- <th style="padding:10px;">현재</th>
687
- <th style="padding:10px;">증가율</th>
688
- <th style="padding:10px;">감지시간</th>
689
  </tr></thead><tbody>
690
  '''
691
 
@@ -707,7 +692,7 @@ def show_trending_alerts():
707
  return html
708
 
709
  def show_top_growing():
710
- """급성장 TOP - DB + 실시간 조회"""
711
  conn = sqlite3.connect("youtube_data.db")
712
  c = conn.cursor()
713
  cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
@@ -727,26 +712,22 @@ def show_top_growing():
727
  conn.close()
728
 
729
  if not results:
730
- # DB 없으면 실시간 인기 영상 표시
731
  try:
732
  resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR", maxResults=20).execute()
733
 
734
  html = '''
735
  <div style="background:#3B82F6; color:white; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
736
  <h3 style="font-family:'Bangers',cursive; margin:0;">📈 현재 인기 급상승 TOP 20</h3>
737
- <p style="margin:5px 0 0 0; font-size:0.9rem;">검색을 여러 번 수행하면 성장률 데이터가 축적됩니다</p>
738
  </div>
739
  <div style="display:flex; flex-wrap:wrap; gap:15px;">
740
  '''
741
 
742
- items = resp.get("items", [])
743
- for i, item in enumerate(items[:20], 1):
744
  snip = item["snippet"]
745
  stats = item["statistics"]
746
  views = int(stats.get("viewCount", 0))
747
  likes = int(stats.get("likeCount", 0))
748
-
749
- # 간단한 engagement 계산
750
  engagement = (likes / views * 100) if views > 0 else 0
751
 
752
  title = snip["title"][:25] + "..." if len(snip["title"]) > 25 else snip["title"]
@@ -754,11 +735,11 @@ def show_top_growing():
754
  thumb = snip["thumbnails"]["medium"]["url"]
755
 
756
  html += f'''
757
- <div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937;">
758
  <a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
759
  <p style="margin:8px 0 5px 0; font-size:12px; font-weight:700;">{i}. {title}</p>
760
  <p style="margin:0; font-size:11px; color:#666;">{snip["channelTitle"][:15]}</p>
761
- <p style="margin:5px 0 0 0; font-size:14px; color:#3B82F6; font-weight:700;">👀 {format_count(views)}</p>
762
  <p style="margin:2px 0 0 0; font-size:12px; color:#EF4444;">❤️ {engagement:.2f}%</p>
763
  </div>
764
  '''
@@ -780,7 +761,7 @@ def show_top_growing():
780
  url = f"https://youtube.com/watch?v={vid}"
781
 
782
  html += f'''
783
- <div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937;">
784
  <a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
785
  <p style="margin:8px 0 5px 0; font-size:12px; font-weight:700;">{i}. {title}</p>
786
  <p style="margin:0; font-size:11px; color:#666;">{channel[:15]}</p>
@@ -886,3 +867,4 @@ with gr.Blocks(css=css) as demo:
886
  history_btn.click(show_search_history, outputs=history_out)
887
 
888
  demo.launch()
 
 
1
  import gradio as gr
2
  from googleapiclient.discovery import build
3
+ from groq import Groq
4
  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")
11
 
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
 
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):
 
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
 
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:
 
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 저장
 
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;">
 
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
 
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
  '''
 
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;">
 
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
 
 
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()
 
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"]
 
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
  '''
 
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>
 
867
  history_btn.click(show_search_history, outputs=history_out)
868
 
869
  demo.launch()
870
+