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