Update app.py
Browse files
app.py
CHANGED
|
@@ -81,7 +81,7 @@ UI_LANG = {
|
|
| 81 |
}
|
| 82 |
|
| 83 |
# ============================================
|
| 84 |
-
# 🎨 CSS
|
| 85 |
# ============================================
|
| 86 |
css = """
|
| 87 |
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
|
|
@@ -167,7 +167,7 @@ label { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important
|
|
| 167 |
::-webkit-scrollbar-thumb { background: #3B82F6; border: 2px solid #1F2937; }
|
| 168 |
::selection { background: #FACC15; color: #1F2937; }
|
| 169 |
|
| 170 |
-
/* LLM Result box
|
| 171 |
.llm-result textarea {
|
| 172 |
background: #1F2937 !important;
|
| 173 |
color: #10B981 !important;
|
|
@@ -360,12 +360,12 @@ def use_trending_keyword(kw):
|
|
| 360 |
return kw if kw else ""
|
| 361 |
|
| 362 |
# ============================================
|
| 363 |
-
# 🔍 Main Search Function
|
| 364 |
# ============================================
|
| 365 |
def search_videos(keyword, country, language, sort_by, date_filter, max_results, ui_lang):
|
| 366 |
L = UI_LANG.get(ui_lang, UI_LANG["en"])
|
| 367 |
if not keyword or not keyword.strip():
|
| 368 |
-
return f"⚠️ {L['no_keyword']}", ""
|
| 369 |
|
| 370 |
max_results = int(max_results)
|
| 371 |
all_items, next_page = [], None
|
|
@@ -388,7 +388,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
|
|
| 388 |
try:
|
| 389 |
resp = youtube.search().list(**params).execute()
|
| 390 |
except Exception as e:
|
| 391 |
-
return f"API Error: {e}", ""
|
| 392 |
items = resp.get("items", [])
|
| 393 |
if not items: break
|
| 394 |
all_items.extend(items)
|
|
@@ -396,7 +396,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
|
|
| 396 |
if not next_page: break
|
| 397 |
|
| 398 |
if not all_items:
|
| 399 |
-
return f"{L['no_results']}", ""
|
| 400 |
|
| 401 |
video_ids = [item["id"]["videoId"] for item in all_items]
|
| 402 |
channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
|
|
@@ -432,7 +432,6 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
|
|
| 432 |
ai_ratings = get_ai_pick_rating(videos_data)
|
| 433 |
save_to_db(videos_data, channel_subs_raw, keyword, country, language, sort_by)
|
| 434 |
|
| 435 |
-
# 고유 ID
|
| 436 |
uid = str(uuid.uuid4()).replace("-", "")[:8]
|
| 437 |
|
| 438 |
html = f'''
|
|
@@ -440,11 +439,11 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
|
|
| 440 |
#tbl_{uid} {{ width:100%; border-collapse:collapse; font-family:'Comic Neue',cursive; }}
|
| 441 |
#tbl_{uid} th {{
|
| 442 |
background:#EF4444; color:#fff; padding:12px 6px; border:2px solid #1F2937;
|
| 443 |
-
font-family:'Bangers',cursive; cursor:pointer; user-select:none;
|
| 444 |
}}
|
| 445 |
#tbl_{uid} th:hover {{ background:#DC2626; }}
|
| 446 |
-
#tbl_{uid} th.asc::after {{ content:" ▲"; color:#FACC15; }}
|
| 447 |
-
#tbl_{uid} th.desc::after {{ content:" ▼"; color:#FACC15; }}
|
| 448 |
#tbl_{uid} td {{ padding:8px 6px; border-bottom:2px solid #1F2937; background:#FFF; vertical-align:middle; }}
|
| 449 |
#tbl_{uid} tr:hover td {{ background:#FEF9C3; }}
|
| 450 |
#tbl_{uid} img {{ border-radius:4px; border:2px solid #1F2937; }}
|
|
@@ -460,28 +459,28 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
|
|
| 460 |
🎬 {L["total"]} <b>{len(videos_data)}</b> {L["results"]} | 🔍 "{keyword}" | 🌍 {country}
|
| 461 |
<br><span style="font-size:0.85rem">🤖 AI Pick: ♥ ⭐ ⭐⭐ ⭐⭐⭐ | 💡 {"헤더 클릭 = 정렬" if ui_lang=="ko" else "Click header to sort"}</span>
|
| 462 |
<div style="margin-top:10px">
|
| 463 |
-
<button class="sortbtn_{uid}" onclick="doSort_{uid}(4)">{L["subs"]}</button>
|
| 464 |
-
<button class="sortbtn_{uid}" onclick="doSort_{uid}(5)">{L["views"]}</button>
|
| 465 |
-
<button class="sortbtn_{uid}" onclick="doSort_{uid}(6)">{L["likes"]}</button>
|
| 466 |
-
<button class="sortbtn_{uid}" onclick="doSort_{uid}(7)">{L["comments"]}</button>
|
| 467 |
-
<button class="sortbtn_{uid}" onclick="doSort_{uid}(8)">AI</button>
|
| 468 |
-
<button class="sortbtn_{uid}" onclick="doSort_{uid}(9)">{L["date"]}</button>
|
| 469 |
</div>
|
| 470 |
</div>
|
| 471 |
|
| 472 |
<div style="max-height:700px; overflow-y:auto; border:3px solid #1F2937; border-radius:8px;">
|
| 473 |
<table id="tbl_{uid}">
|
| 474 |
<thead><tr>
|
| 475 |
-
<th
|
| 476 |
<th style="width:120px">{L["thumb"]}</th>
|
| 477 |
-
<th
|
| 478 |
-
<th
|
| 479 |
-
<th
|
| 480 |
-
<th
|
| 481 |
-
<th
|
| 482 |
-
<th
|
| 483 |
-
<th
|
| 484 |
-
<th
|
| 485 |
</tr></thead>
|
| 486 |
<tbody>
|
| 487 |
'''
|
|
@@ -507,7 +506,6 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
|
|
| 507 |
<td data-v="{v['published_at'][:10]}">{v['published_at'][:10]}</td>
|
| 508 |
</tr>'''
|
| 509 |
|
| 510 |
-
# JavaScript 정렬 - 즉시 실행
|
| 511 |
html += f'''
|
| 512 |
</tbody>
|
| 513 |
</table>
|
|
@@ -515,22 +513,22 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
|
|
| 515 |
|
| 516 |
<script>
|
| 517 |
(function() {{
|
| 518 |
-
var
|
| 519 |
-
if (!tbl) return;
|
| 520 |
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
|
|
|
| 524 |
var tbody = tbl.querySelector('tbody');
|
| 525 |
var rows = Array.from(tbody.querySelectorAll('tr'));
|
| 526 |
var headers = tbl.querySelectorAll('th');
|
| 527 |
|
| 528 |
-
var asc =
|
| 529 |
-
|
| 530 |
-
|
| 531 |
|
| 532 |
-
headers.forEach(function(h) {{ h.classList.remove('asc', 'desc'); }});
|
| 533 |
-
if (headers[colIdx]) headers[colIdx].classList.add(asc ? 'asc' : 'desc');
|
| 534 |
|
| 535 |
rows.sort(function(a, b) {{
|
| 536 |
var aCell = a.cells[colIdx];
|
|
@@ -550,21 +548,6 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
|
|
| 550 |
}});
|
| 551 |
|
| 552 |
rows.forEach(function(row) {{ tbody.appendChild(row); }});
|
| 553 |
-
}}
|
| 554 |
-
|
| 555 |
-
// 헤더 클릭 이벤트
|
| 556 |
-
tbl.querySelectorAll('th[data-col]').forEach(function(th) {{
|
| 557 |
-
th.addEventListener('click', function() {{
|
| 558 |
-
var col = parseInt(this.getAttribute('data-col'));
|
| 559 |
-
var type = this.getAttribute('data-type');
|
| 560 |
-
sortTable(col, type);
|
| 561 |
-
}});
|
| 562 |
-
}});
|
| 563 |
-
|
| 564 |
-
// 전역 정렬 함수
|
| 565 |
-
window.doSort_{uid} = function(col) {{
|
| 566 |
-
var type = (col >= 4 && col <= 8) ? 'n' : 's';
|
| 567 |
-
sortTable(col, type);
|
| 568 |
}};
|
| 569 |
}})();
|
| 570 |
</script>
|
|
@@ -574,7 +557,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results,
|
|
| 574 |
return html, f"📊 DB: Videos {stats['videos']} | Records {stats['stats']} | Channels {stats['channels']} | Searches {stats['searches']}"
|
| 575 |
|
| 576 |
# ============================================
|
| 577 |
-
# 🔥 Trending Alerts
|
| 578 |
# ============================================
|
| 579 |
def show_trending_alerts(ui_lang):
|
| 580 |
is_ko = ui_lang == "ko"
|
|
@@ -629,7 +612,7 @@ def show_trending_alerts(ui_lang):
|
|
| 629 |
return html + '</div>'
|
| 630 |
|
| 631 |
# ============================================
|
| 632 |
-
# 📈 Top Growing
|
| 633 |
# ============================================
|
| 634 |
def show_top_growing(ui_lang):
|
| 635 |
is_ko = ui_lang == "ko"
|
|
@@ -694,7 +677,7 @@ def show_top_growing(ui_lang):
|
|
| 694 |
return html + '</div>'
|
| 695 |
|
| 696 |
# ============================================
|
| 697 |
-
# ⭐ AI Pick
|
| 698 |
# ============================================
|
| 699 |
def show_ai_picks(ui_lang):
|
| 700 |
is_ko = ui_lang == "ko"
|
|
@@ -710,7 +693,6 @@ def show_ai_picks(ui_lang):
|
|
| 710 |
top_picks = [(i, v, ratings.get(i, 0)) for i, v in enumerate(videos) if ratings.get(i, 0) >= 3]
|
| 711 |
top_picks.sort(key=lambda x: (-x[2], -x[1]['views']))
|
| 712 |
|
| 713 |
-
# AI 분석 - 색상 수정됨
|
| 714 |
analysis_html = ""
|
| 715 |
if groq_client and top_picks:
|
| 716 |
info = "\n".join([f"- {v['title'][:50]} (Views:{format_count(v['views'])})" for _, v, _ in top_picks[:5]])
|
|
@@ -726,7 +708,7 @@ def show_ai_picks(ui_lang):
|
|
| 726 |
<div style="background:linear-gradient(135deg,#FACC15,#F59E0B);padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
|
| 727 |
<h2 style="font-family:'Bangers',cursive;color:#1F2937;margin:0;text-shadow:2px 2px 0 #FFF;font-size:2rem">⭐ {"AI 추천 - TOP 영상" if is_ko else "AI PICK - TOP RECOMMENDATIONS"}</h2>
|
| 728 |
<p style="color:#1F2937;margin:10px 0 0;font-family:'Comic Neue',cursive;font-weight:700;font-size:15px">
|
| 729 |
-
{"⭐⭐ 이상 등급 영상" if is_ko else "⭐⭐+ rated videos"}: <b>{len(top_picks)}</b>{"개" if is_ko else " videos"}
|
| 730 |
</p>
|
| 731 |
</div>
|
| 732 |
{analysis_html}
|
|
@@ -822,8 +804,8 @@ def show_search_history(ui_lang):
|
|
| 822 |
html += f'''<tr style="background:#FFF;border-bottom:2px solid #1F2937">
|
| 823 |
<td style="padding:12px;font-weight:700;color:#1F2937">{kw}</td>
|
| 824 |
<td style="padding:12px;color:#1F2937">{country}</td>
|
| 825 |
-
<td style="padding:12px;color:#1F2937">{lang[:10]}</td>
|
| 826 |
-
<td style="padding:12px;color:#1F2937">{sort_by[:10]}</td>
|
| 827 |
<td style="padding:12px;color:#3B82F6;font-weight:700">{cnt}</td>
|
| 828 |
<td style="padding:12px;font-size:12px;color:#666">{searched[:16].replace("T"," ")}</td>
|
| 829 |
</tr>'''
|
|
@@ -856,9 +838,9 @@ def switch_ui_language(ui_lang):
|
|
| 856 |
initial_trending = get_real_trending_keywords("US", "en")
|
| 857 |
|
| 858 |
# ============================================
|
| 859 |
-
# 🎨 Gradio UI
|
| 860 |
# ============================================
|
| 861 |
-
with gr.Blocks(
|
| 862 |
ui_lang_state = gr.State("en")
|
| 863 |
|
| 864 |
gr.HTML('''<div style="text-align:center;margin:20px 0">
|
|
@@ -866,11 +848,13 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
|
|
| 866 |
<img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge">
|
| 867 |
</a></div>''')
|
| 868 |
|
| 869 |
-
gr.Markdown("# 🎬 YOUTUBE TREND ANALYZER 📊", elem_classes="header-text")
|
| 870 |
|
| 871 |
with gr.Row():
|
| 872 |
-
|
| 873 |
-
|
|
|
|
|
|
|
| 874 |
|
| 875 |
with gr.Tabs():
|
| 876 |
with gr.Tab("🔍 Search"):
|
|
@@ -909,29 +893,29 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
|
|
| 909 |
with gr.Tab("🏷️ Keyword Suggest"):
|
| 910 |
kw_input = gr.Textbox(label="Enter base keyword", placeholder="e.g., Python tutorial")
|
| 911 |
kw_btn = gr.Button("🔍 Generate Keywords", variant="primary")
|
| 912 |
-
kw_output = gr.Textbox(label="Suggested Keywords", lines=20, elem_classes="llm-result")
|
| 913 |
|
| 914 |
with gr.Tab("🔮 Trend Prediction"):
|
| 915 |
tp_input = gr.Textbox(label="Enter topic", placeholder="e.g., AI tools")
|
| 916 |
tp_btn = gr.Button("🔮 Predict Trend", variant="primary")
|
| 917 |
-
tp_output = gr.Textbox(label="Trend Analysis", lines=20, elem_classes="llm-result")
|
| 918 |
|
| 919 |
with gr.Tab("💡 Content Ideas"):
|
| 920 |
ci_input = gr.Textbox(label="Enter topic", placeholder="e.g., Home workout")
|
| 921 |
ci_btn = gr.Button("💡 Generate Ideas", variant="primary")
|
| 922 |
-
ci_output = gr.Textbox(label="Content Ideas", lines=25, elem_classes="llm-result")
|
| 923 |
|
| 924 |
with gr.Tab("📊 Channel Analysis"):
|
| 925 |
ca_input = gr.Textbox(label="Enter channel/niche", placeholder="e.g., Tech reviews")
|
| 926 |
ca_btn = gr.Button("📊 Analyze", variant="primary")
|
| 927 |
-
ca_output = gr.Textbox(label="Analysis", lines=25, elem_classes="llm-result")
|
| 928 |
|
| 929 |
with gr.Tab("⚔️ Competitor"):
|
| 930 |
with gr.Row():
|
| 931 |
comp_my = gr.Textbox(label="Your Channel", placeholder="My channel")
|
| 932 |
comp_rival = gr.Textbox(label="Competitor", placeholder="Competitor")
|
| 933 |
comp_btn = gr.Button("⚔️ Compare", variant="primary")
|
| 934 |
-
comp_output = gr.Textbox(label="Analysis", lines=25, elem_classes="llm-result")
|
| 935 |
|
| 936 |
with gr.Tab("🕐 History"):
|
| 937 |
history_btn = gr.Button("🔄 Refresh", variant="primary")
|
|
@@ -960,4 +944,6 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
|
|
| 960 |
ca_btn.click(analyze_channel, [ca_input, ui_lang_state], ca_output)
|
| 961 |
comp_btn.click(analyze_competitor, [comp_my, comp_rival, ui_lang_state], comp_output)
|
| 962 |
|
| 963 |
-
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
# ============================================
|
| 84 |
+
# 🎨 CSS
|
| 85 |
# ============================================
|
| 86 |
css = """
|
| 87 |
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
|
|
|
|
| 167 |
::-webkit-scrollbar-thumb { background: #3B82F6; border: 2px solid #1F2937; }
|
| 168 |
::selection { background: #FACC15; color: #1F2937; }
|
| 169 |
|
| 170 |
+
/* LLM Result box */
|
| 171 |
.llm-result textarea {
|
| 172 |
background: #1F2937 !important;
|
| 173 |
color: #10B981 !important;
|
|
|
|
| 360 |
return kw if kw else ""
|
| 361 |
|
| 362 |
# ============================================
|
| 363 |
+
# 🔍 Main Search Function
|
| 364 |
# ============================================
|
| 365 |
def search_videos(keyword, country, language, sort_by, date_filter, max_results, ui_lang):
|
| 366 |
L = UI_LANG.get(ui_lang, UI_LANG["en"])
|
| 367 |
if not keyword or not keyword.strip():
|
| 368 |
+
return f"⚠️ {L['no_keyword']}", "📊 DB: -"
|
| 369 |
|
| 370 |
max_results = int(max_results)
|
| 371 |
all_items, next_page = [], None
|
|
|
|
| 388 |
try:
|
| 389 |
resp = youtube.search().list(**params).execute()
|
| 390 |
except Exception as e:
|
| 391 |
+
return f"API Error: {e}", "📊 DB: Error"
|
| 392 |
items = resp.get("items", [])
|
| 393 |
if not items: break
|
| 394 |
all_items.extend(items)
|
|
|
|
| 396 |
if not next_page: break
|
| 397 |
|
| 398 |
if not all_items:
|
| 399 |
+
return f"{L['no_results']}", "📊 DB: -"
|
| 400 |
|
| 401 |
video_ids = [item["id"]["videoId"] for item in all_items]
|
| 402 |
channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
|
|
|
|
| 432 |
ai_ratings = get_ai_pick_rating(videos_data)
|
| 433 |
save_to_db(videos_data, channel_subs_raw, keyword, country, language, sort_by)
|
| 434 |
|
|
|
|
| 435 |
uid = str(uuid.uuid4()).replace("-", "")[:8]
|
| 436 |
|
| 437 |
html = f'''
|
|
|
|
| 439 |
#tbl_{uid} {{ width:100%; border-collapse:collapse; font-family:'Comic Neue',cursive; }}
|
| 440 |
#tbl_{uid} th {{
|
| 441 |
background:#EF4444; color:#fff; padding:12px 6px; border:2px solid #1F2937;
|
| 442 |
+
font-family:'Bangers',cursive; cursor:pointer; user-select:none;
|
| 443 |
}}
|
| 444 |
#tbl_{uid} th:hover {{ background:#DC2626; }}
|
| 445 |
+
#tbl_{uid} th.sort-asc::after {{ content:" ▲"; color:#FACC15; }}
|
| 446 |
+
#tbl_{uid} th.sort-desc::after {{ content:" ▼"; color:#FACC15; }}
|
| 447 |
#tbl_{uid} td {{ padding:8px 6px; border-bottom:2px solid #1F2937; background:#FFF; vertical-align:middle; }}
|
| 448 |
#tbl_{uid} tr:hover td {{ background:#FEF9C3; }}
|
| 449 |
#tbl_{uid} img {{ border-radius:4px; border:2px solid #1F2937; }}
|
|
|
|
| 459 |
🎬 {L["total"]} <b>{len(videos_data)}</b> {L["results"]} | 🔍 "{keyword}" | 🌍 {country}
|
| 460 |
<br><span style="font-size:0.85rem">🤖 AI Pick: ♥ ⭐ ⭐⭐ ⭐⭐⭐ | 💡 {"헤더 클릭 = 정렬" if ui_lang=="ko" else "Click header to sort"}</span>
|
| 461 |
<div style="margin-top:10px">
|
| 462 |
+
<button class="sortbtn_{uid}" onclick="doSort_{uid}(4,'n')">{L["subs"]}</button>
|
| 463 |
+
<button class="sortbtn_{uid}" onclick="doSort_{uid}(5,'n')">{L["views"]}</button>
|
| 464 |
+
<button class="sortbtn_{uid}" onclick="doSort_{uid}(6,'n')">{L["likes"]}</button>
|
| 465 |
+
<button class="sortbtn_{uid}" onclick="doSort_{uid}(7,'n')">{L["comments"]}</button>
|
| 466 |
+
<button class="sortbtn_{uid}" onclick="doSort_{uid}(8,'n')">AI</button>
|
| 467 |
+
<button class="sortbtn_{uid}" onclick="doSort_{uid}(9,'s')">{L["date"]}</button>
|
| 468 |
</div>
|
| 469 |
</div>
|
| 470 |
|
| 471 |
<div style="max-height:700px; overflow-y:auto; border:3px solid #1F2937; border-radius:8px;">
|
| 472 |
<table id="tbl_{uid}">
|
| 473 |
<thead><tr>
|
| 474 |
+
<th style="width:45px">{L["rank"]}</th>
|
| 475 |
<th style="width:120px">{L["thumb"]}</th>
|
| 476 |
+
<th>{L["title_col"]}</th>
|
| 477 |
+
<th style="width:100px">{L["channel"]}</th>
|
| 478 |
+
<th onclick="doSort_{uid}(4,'n')" style="width:70px;cursor:pointer">{L["subs"]}</th>
|
| 479 |
+
<th onclick="doSort_{uid}(5,'n')" style="width:75px;cursor:pointer">{L["views"]}</th>
|
| 480 |
+
<th onclick="doSort_{uid}(6,'n')" style="width:60px;cursor:pointer">{L["likes"]}</th>
|
| 481 |
+
<th onclick="doSort_{uid}(7,'n')" style="width:55px;cursor:pointer">{L["comments"]}</th>
|
| 482 |
+
<th onclick="doSort_{uid}(8,'n')" style="width:65px;cursor:pointer">{L["ai_pick_col"]}</th>
|
| 483 |
+
<th onclick="doSort_{uid}(9,'s')" style="width:90px;cursor:pointer">{L["date"]}</th>
|
| 484 |
</tr></thead>
|
| 485 |
<tbody>
|
| 486 |
'''
|
|
|
|
| 506 |
<td data-v="{v['published_at'][:10]}">{v['published_at'][:10]}</td>
|
| 507 |
</tr>'''
|
| 508 |
|
|
|
|
| 509 |
html += f'''
|
| 510 |
</tbody>
|
| 511 |
</table>
|
|
|
|
| 513 |
|
| 514 |
<script>
|
| 515 |
(function() {{
|
| 516 |
+
var sortState_{uid} = {{}};
|
|
|
|
| 517 |
|
| 518 |
+
window.doSort_{uid} = function(colIdx, type) {{
|
| 519 |
+
var tbl = document.getElementById('tbl_{uid}');
|
| 520 |
+
if (!tbl) return;
|
| 521 |
+
|
| 522 |
var tbody = tbl.querySelector('tbody');
|
| 523 |
var rows = Array.from(tbody.querySelectorAll('tr'));
|
| 524 |
var headers = tbl.querySelectorAll('th');
|
| 525 |
|
| 526 |
+
var asc = sortState_{uid}[colIdx] !== 'asc';
|
| 527 |
+
sortState_{uid} = {{}};
|
| 528 |
+
sortState_{uid}[colIdx] = asc ? 'asc' : 'desc';
|
| 529 |
|
| 530 |
+
headers.forEach(function(h) {{ h.classList.remove('sort-asc', 'sort-desc'); }});
|
| 531 |
+
if (headers[colIdx]) headers[colIdx].classList.add(asc ? 'sort-asc' : 'sort-desc');
|
| 532 |
|
| 533 |
rows.sort(function(a, b) {{
|
| 534 |
var aCell = a.cells[colIdx];
|
|
|
|
| 548 |
}});
|
| 549 |
|
| 550 |
rows.forEach(function(row) {{ tbody.appendChild(row); }});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
}};
|
| 552 |
}})();
|
| 553 |
</script>
|
|
|
|
| 557 |
return html, f"📊 DB: Videos {stats['videos']} | Records {stats['stats']} | Channels {stats['channels']} | Searches {stats['searches']}"
|
| 558 |
|
| 559 |
# ============================================
|
| 560 |
+
# 🔥 Trending Alerts
|
| 561 |
# ============================================
|
| 562 |
def show_trending_alerts(ui_lang):
|
| 563 |
is_ko = ui_lang == "ko"
|
|
|
|
| 612 |
return html + '</div>'
|
| 613 |
|
| 614 |
# ============================================
|
| 615 |
+
# 📈 Top Growing
|
| 616 |
# ============================================
|
| 617 |
def show_top_growing(ui_lang):
|
| 618 |
is_ko = ui_lang == "ko"
|
|
|
|
| 677 |
return html + '</div>'
|
| 678 |
|
| 679 |
# ============================================
|
| 680 |
+
# ⭐ AI Pick
|
| 681 |
# ============================================
|
| 682 |
def show_ai_picks(ui_lang):
|
| 683 |
is_ko = ui_lang == "ko"
|
|
|
|
| 693 |
top_picks = [(i, v, ratings.get(i, 0)) for i, v in enumerate(videos) if ratings.get(i, 0) >= 3]
|
| 694 |
top_picks.sort(key=lambda x: (-x[2], -x[1]['views']))
|
| 695 |
|
|
|
|
| 696 |
analysis_html = ""
|
| 697 |
if groq_client and top_picks:
|
| 698 |
info = "\n".join([f"- {v['title'][:50]} (Views:{format_count(v['views'])})" for _, v, _ in top_picks[:5]])
|
|
|
|
| 708 |
<div style="background:linear-gradient(135deg,#FACC15,#F59E0B);padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
|
| 709 |
<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>
|
| 710 |
<p style="color:#1F2937;margin:10px 0 0;font-family:'Comic Neue',cursive;font-weight:700;font-size:15px">
|
| 711 |
+
{"⭐⭐ 이상 등급 영상" if is_ko else "⭐⭐+ rated videos"}: <b>{len(top_picks)}</b>{"개" if is_ko else " videos"}
|
| 712 |
</p>
|
| 713 |
</div>
|
| 714 |
{analysis_html}
|
|
|
|
| 804 |
html += f'''<tr style="background:#FFF;border-bottom:2px solid #1F2937">
|
| 805 |
<td style="padding:12px;font-weight:700;color:#1F2937">{kw}</td>
|
| 806 |
<td style="padding:12px;color:#1F2937">{country}</td>
|
| 807 |
+
<td style="padding:12px;color:#1F2937">{lang[:10] if lang else "-"}</td>
|
| 808 |
+
<td style="padding:12px;color:#1F2937">{sort_by[:10] if sort_by else "-"}</td>
|
| 809 |
<td style="padding:12px;color:#3B82F6;font-weight:700">{cnt}</td>
|
| 810 |
<td style="padding:12px;font-size:12px;color:#666">{searched[:16].replace("T"," ")}</td>
|
| 811 |
</tr>'''
|
|
|
|
| 838 |
initial_trending = get_real_trending_keywords("US", "en")
|
| 839 |
|
| 840 |
# ============================================
|
| 841 |
+
# 🎨 Gradio UI (Gradio 6.0 Compatible)
|
| 842 |
# ============================================
|
| 843 |
+
with gr.Blocks(title="YouTube Trend Analyzer") as demo:
|
| 844 |
ui_lang_state = gr.State("en")
|
| 845 |
|
| 846 |
gr.HTML('''<div style="text-align:center;margin:20px 0">
|
|
|
|
| 848 |
<img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge">
|
| 849 |
</a></div>''')
|
| 850 |
|
| 851 |
+
gr.Markdown("# 🎬 YOUTUBE TREND ANALYZER 📊", elem_classes=["header-text"])
|
| 852 |
|
| 853 |
with gr.Row():
|
| 854 |
+
with gr.Column(scale=4):
|
| 855 |
+
db_stats = gr.Markdown("📊 Loading...")
|
| 856 |
+
with gr.Column(scale=1):
|
| 857 |
+
ui_lang_dropdown = gr.Dropdown(choices=["English", "한국어"], value="English", label="🌐 UI Language", interactive=True)
|
| 858 |
|
| 859 |
with gr.Tabs():
|
| 860 |
with gr.Tab("🔍 Search"):
|
|
|
|
| 893 |
with gr.Tab("🏷️ Keyword Suggest"):
|
| 894 |
kw_input = gr.Textbox(label="Enter base keyword", placeholder="e.g., Python tutorial")
|
| 895 |
kw_btn = gr.Button("🔍 Generate Keywords", variant="primary")
|
| 896 |
+
kw_output = gr.Textbox(label="Suggested Keywords", lines=20, elem_classes=["llm-result"])
|
| 897 |
|
| 898 |
with gr.Tab("🔮 Trend Prediction"):
|
| 899 |
tp_input = gr.Textbox(label="Enter topic", placeholder="e.g., AI tools")
|
| 900 |
tp_btn = gr.Button("🔮 Predict Trend", variant="primary")
|
| 901 |
+
tp_output = gr.Textbox(label="Trend Analysis", lines=20, elem_classes=["llm-result"])
|
| 902 |
|
| 903 |
with gr.Tab("💡 Content Ideas"):
|
| 904 |
ci_input = gr.Textbox(label="Enter topic", placeholder="e.g., Home workout")
|
| 905 |
ci_btn = gr.Button("💡 Generate Ideas", variant="primary")
|
| 906 |
+
ci_output = gr.Textbox(label="Content Ideas", lines=25, elem_classes=["llm-result"])
|
| 907 |
|
| 908 |
with gr.Tab("📊 Channel Analysis"):
|
| 909 |
ca_input = gr.Textbox(label="Enter channel/niche", placeholder="e.g., Tech reviews")
|
| 910 |
ca_btn = gr.Button("📊 Analyze", variant="primary")
|
| 911 |
+
ca_output = gr.Textbox(label="Analysis", lines=25, elem_classes=["llm-result"])
|
| 912 |
|
| 913 |
with gr.Tab("⚔️ Competitor"):
|
| 914 |
with gr.Row():
|
| 915 |
comp_my = gr.Textbox(label="Your Channel", placeholder="My channel")
|
| 916 |
comp_rival = gr.Textbox(label="Competitor", placeholder="Competitor")
|
| 917 |
comp_btn = gr.Button("⚔️ Compare", variant="primary")
|
| 918 |
+
comp_output = gr.Textbox(label="Analysis", lines=25, elem_classes=["llm-result"])
|
| 919 |
|
| 920 |
with gr.Tab("🕐 History"):
|
| 921 |
history_btn = gr.Button("🔄 Refresh", variant="primary")
|
|
|
|
| 944 |
ca_btn.click(analyze_channel, [ca_input, ui_lang_state], ca_output)
|
| 945 |
comp_btn.click(analyze_competitor, [comp_my, comp_rival, ui_lang_state], comp_output)
|
| 946 |
|
| 947 |
+
# Launch with CSS (Gradio 6.0 style)
|
| 948 |
+
demo.launch(css=css)
|
| 949 |
+
|