Update app.py
Browse files
app.py
CHANGED
|
@@ -16,11 +16,170 @@ groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
|
|
| 16 |
ai_pick_storage = {"videos": [], "ratings": {}, "timestamp": None}
|
| 17 |
|
| 18 |
# ============================================
|
| 19 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# ============================================
|
| 21 |
css = """
|
| 22 |
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
.gradio-container {
|
| 25 |
background-color: #FEF9C3 !important;
|
| 26 |
background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
|
|
@@ -29,10 +188,6 @@ css = """
|
|
| 29 |
font-family: 'Comic Neue', cursive, sans-serif !important;
|
| 30 |
}
|
| 31 |
|
| 32 |
-
footer, .footer, .gradio-container footer, .built-with, [class*="footer"] {
|
| 33 |
-
display: none !important;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
.header-text h1 {
|
| 37 |
font-family: 'Bangers', cursive !important;
|
| 38 |
color: #1F2937 !important;
|
|
@@ -93,6 +248,20 @@ label { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important
|
|
| 93 |
font-family: 'Courier New', monospace !important;
|
| 94 |
white-space: pre-wrap !important;
|
| 95 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
"""
|
| 97 |
|
| 98 |
# DB 초기화
|
|
@@ -147,9 +316,9 @@ COUNTRIES = {
|
|
| 147 |
}
|
| 148 |
|
| 149 |
LANGUAGES = {
|
| 150 |
-
"Auto (by Country)": "", "English": "en", "
|
| 151 |
-
"
|
| 152 |
-
"
|
| 153 |
"Arabic": "ar", "Turkish": "tr", "Indonesian": "id", "Vietnamese": "vi",
|
| 154 |
"Thai": "th", "Dutch": "nl", "Polish": "pl", "Swedish": "sv",
|
| 155 |
"Norwegian": "no", "Danish": "da", "Finnish": "fi", "Greek": "el",
|
|
@@ -157,8 +326,8 @@ LANGUAGES = {
|
|
| 157 |
"Hungarian": "hu", "Filipino": "tl", "Malay": "ms",
|
| 158 |
}
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
|
| 163 |
def format_count(count):
|
| 164 |
if count >= 1000000000: return f"{count/1000000000:.1f}B"
|
|
@@ -167,18 +336,13 @@ def format_count(count):
|
|
| 167 |
return str(count)
|
| 168 |
|
| 169 |
def call_llm(prompt, max_tokens=2000):
|
| 170 |
-
"""Groq LLM API call"""
|
| 171 |
if not groq_client:
|
| 172 |
return None
|
| 173 |
try:
|
| 174 |
completion = groq_client.chat.completions.create(
|
| 175 |
model="openai/gpt-oss-120b",
|
| 176 |
messages=[{"role": "user", "content": prompt}],
|
| 177 |
-
temperature=0.7,
|
| 178 |
-
max_completion_tokens=max_tokens,
|
| 179 |
-
top_p=1,
|
| 180 |
-
stream=True,
|
| 181 |
-
stop=None
|
| 182 |
)
|
| 183 |
result = ""
|
| 184 |
for chunk in completion:
|
|
@@ -189,26 +353,14 @@ def call_llm(prompt, max_tokens=2000):
|
|
| 189 |
return f"Error: {e}"
|
| 190 |
|
| 191 |
def get_ai_pick_rating(videos_data):
|
| 192 |
-
"""AI Pick rating"""
|
| 193 |
global ai_pick_storage
|
| 194 |
-
if not videos_data:
|
| 195 |
-
return {}
|
| 196 |
|
| 197 |
if groq_client:
|
| 198 |
try:
|
| 199 |
sample = videos_data[:50]
|
| 200 |
-
video_info = "\n".join([
|
| 201 |
-
|
| 202 |
-
for i, v in enumerate(sample)
|
| 203 |
-
])
|
| 204 |
-
|
| 205 |
-
prompt = f"""Rate YouTube videos 0-4. 0=None,1=♥,2=⭐,3=⭐⭐,4=⭐⭐⭐
|
| 206 |
-
Consider engagement, viral potential. Format: 1:3,2:2,3:4
|
| 207 |
-
|
| 208 |
-
{video_info}
|
| 209 |
-
|
| 210 |
-
Response (number:rating only):"""
|
| 211 |
-
|
| 212 |
result = call_llm(prompt, 1500)
|
| 213 |
if result and "Error" not in result:
|
| 214 |
ratings = {}
|
|
@@ -216,12 +368,10 @@ Response (number:rating only):"""
|
|
| 216 |
ratings[int(idx)-1] = int(rating)
|
| 217 |
if len(videos_data) > 50:
|
| 218 |
local = calculate_local_rating(videos_data[50:])
|
| 219 |
-
for k, v in local.items():
|
| 220 |
-
ratings[k + 50] = v
|
| 221 |
ai_pick_storage = {"videos": videos_data, "ratings": ratings, "timestamp": datetime.now().isoformat()}
|
| 222 |
return ratings
|
| 223 |
-
except:
|
| 224 |
-
pass
|
| 225 |
|
| 226 |
ratings = calculate_local_rating(videos_data)
|
| 227 |
ai_pick_storage = {"videos": videos_data, "ratings": ratings, "timestamp": datetime.now().isoformat()}
|
|
@@ -240,10 +390,8 @@ def calculate_local_rating(videos_data):
|
|
| 240 |
if views > 0:
|
| 241 |
score += min(40, (views/max_views)*40)
|
| 242 |
score += min(30, ((likes+comments*2)/views)*300)
|
| 243 |
-
if subs > 0 and views > 0:
|
| 244 |
-
|
| 245 |
-
elif views > avg_views:
|
| 246 |
-
score += 15
|
| 247 |
|
| 248 |
if score >= 70: ratings[i] = 4
|
| 249 |
elif score >= 50: ratings[i] = 3
|
|
@@ -262,16 +410,12 @@ def get_real_trending_keywords(region_code="US", language="en"):
|
|
| 262 |
for item in response.get("items", []):
|
| 263 |
for tag in item["snippet"].get("tags", [])[:3]:
|
| 264 |
if tag.lower() not in seen and 2 <= len(tag) <= 20:
|
| 265 |
-
keywords.append(tag)
|
| 266 |
-
seen.add(tag.lower())
|
| 267 |
channel = item["snippet"]["channelTitle"]
|
| 268 |
-
if channel.lower() not in seen:
|
| 269 |
-
keywords.append(channel)
|
| 270 |
-
seen.add(channel.lower())
|
| 271 |
if len(keywords) >= 20: break
|
| 272 |
return keywords[:20] if keywords else ["AI","gaming","music","vlog","shorts","news"]
|
| 273 |
-
except:
|
| 274 |
-
return ["AI","ChatGPT","gaming","music","vlog","shorts","news","tech"]
|
| 275 |
|
| 276 |
def save_to_db(videos_data, channels_data, keyword, country, language, sort_by):
|
| 277 |
conn = sqlite3.connect("youtube_data.db")
|
|
@@ -290,18 +434,15 @@ def save_to_db(videos_data, channels_data, keyword, country, language, sort_by):
|
|
| 290 |
c.execute('INSERT INTO trending_alerts VALUES (NULL,?,?,?,?,?,?)', (video['video_id'], 'views_surge', prev[0], video['views'], change, now))
|
| 291 |
for ch_id, subs in channels_data.items():
|
| 292 |
c.execute('INSERT OR IGNORE INTO channels VALUES (?,?,?)', (ch_id, '', now))
|
| 293 |
-
if isinstance(subs, int):
|
| 294 |
-
|
| 295 |
-
conn.commit()
|
| 296 |
-
conn.close()
|
| 297 |
|
| 298 |
def get_db_stats():
|
| 299 |
conn = sqlite3.connect("youtube_data.db")
|
| 300 |
c = conn.cursor()
|
| 301 |
stats = {}
|
| 302 |
for t, k in [("videos","videos"),("video_stats","stats"),("channels","channels"),("search_history","searches"),("trending_alerts","alerts")]:
|
| 303 |
-
c.execute(f"SELECT COUNT(*) FROM {t}")
|
| 304 |
-
stats[k] = c.fetchone()[0]
|
| 305 |
conn.close()
|
| 306 |
return stats
|
| 307 |
|
|
@@ -312,21 +453,26 @@ def update_trending(country):
|
|
| 312 |
def use_trending_keyword(kw):
|
| 313 |
return kw if kw else ""
|
| 314 |
|
| 315 |
-
def search_videos(keyword, country, language, sort_by, date_filter, max_results):
|
|
|
|
| 316 |
if not keyword or not keyword.strip():
|
| 317 |
-
return "⚠️ Please enter a search keyword!", ""
|
| 318 |
|
| 319 |
max_results = int(max_results)
|
| 320 |
all_items, next_page = [], None
|
| 321 |
region_code, default_lang = COUNTRIES.get(country, ("", ""))
|
| 322 |
-
lang_code = default_lang if language
|
| 323 |
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
if region_code: params["regionCode"] = region_code
|
| 326 |
if lang_code: params["relevanceLanguage"] = lang_code
|
| 327 |
-
if
|
| 328 |
-
deltas = {"
|
| 329 |
-
params["publishedAfter"] = (datetime.utcnow() - timedelta(days=deltas.get(
|
| 330 |
|
| 331 |
while len(all_items) < max_results:
|
| 332 |
params["maxResults"] = min(50, max_results - len(all_items))
|
|
@@ -338,7 +484,8 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
|
|
| 338 |
next_page = resp.get("nextPageToken")
|
| 339 |
if not next_page: break
|
| 340 |
|
| 341 |
-
if not all_items:
|
|
|
|
| 342 |
|
| 343 |
video_ids = [item["id"]["videoId"] for item in all_items]
|
| 344 |
channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
|
|
@@ -353,9 +500,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
|
|
| 353 |
for i in range(0, len(channel_ids), 50):
|
| 354 |
for ch in youtube.channels().list(id=",".join(channel_ids[i:i+50]), part="statistics").execute().get("items", []):
|
| 355 |
sub = ch["statistics"].get("subscriberCount", "0")
|
| 356 |
-
if sub:
|
| 357 |
-
channel_subs_raw[ch["id"]] = int(sub)
|
| 358 |
-
channel_subs[ch["id"]] = format_count(int(sub))
|
| 359 |
|
| 360 |
videos_data = []
|
| 361 |
for item in all_items:
|
|
@@ -391,29 +536,29 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
|
|
| 391 |
.qsort button:hover{{background:#1F2937;color:#FACC15}}
|
| 392 |
</style>
|
| 393 |
<div class="res-hdr">
|
| 394 |
-
🎬 <b>{len(all_items)}</b> results | 🔍 "{keyword}" | 🌍 {country}
|
| 395 |
-
<br><span style="font-size:0.9rem">🤖 AI Pick: ♥
|
| 396 |
<div style="margin-top:10px"><span style="font-size:0.9rem">⚡ Sort:</span>
|
| 397 |
<span class="qsort">
|
| 398 |
-
<button onclick="sortT_{table_id}(5,'n')">
|
| 399 |
-
<button onclick="sortT_{table_id}(6,'n')">
|
| 400 |
-
<button onclick="sortT_{table_id}(4,'n')">
|
| 401 |
-
<button onclick="sortT_{table_id}(8,'n')">AI
|
| 402 |
-
<button onclick="sortT_{table_id}(9,'s')">
|
| 403 |
</span></div>
|
| 404 |
</div>
|
| 405 |
<div style="max-height:700px;overflow-y:auto;border:3px solid #1F2937;border-radius:8px">
|
| 406 |
<table class="yt-tbl" id="{table_id}"><thead><tr>
|
| 407 |
-
<th onclick="sortT_{table_id}(0,'n')" style="width:45px">
|
| 408 |
-
<th style="width:130px">
|
| 409 |
<th onclick="sortT_{table_id}(2,'s')">Title</th>
|
| 410 |
-
<th onclick="sortT_{table_id}(3,'s')" style="width:100px">
|
| 411 |
-
<th onclick="sortT_{table_id}(4,'n')" style="width:65px">
|
| 412 |
-
<th onclick="sortT_{table_id}(5,'n')" style="width:70px">
|
| 413 |
-
<th onclick="sortT_{table_id}(6,'n')" style="width:55px">
|
| 414 |
-
<th onclick="sortT_{table_id}(7,'n')" style="width:50px">
|
| 415 |
-
<th onclick="sortT_{table_id}(8,'n')" style="width:70px">
|
| 416 |
-
<th onclick="sortT_{table_id}(9,'s')" style="width:85px">
|
| 417 |
</tr></thead><tbody>'''
|
| 418 |
|
| 419 |
for i, v in enumerate(videos_data):
|
|
@@ -454,8 +599,8 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
|
|
| 454 |
stats = get_db_stats()
|
| 455 |
return html, f"📊 DB: Videos {stats['videos']} | Records {stats['stats']} | Channels {stats['channels']} | Searches {stats['searches']}"
|
| 456 |
|
| 457 |
-
def show_trending_alerts():
|
| 458 |
-
|
| 459 |
conn = sqlite3.connect("youtube_data.db")
|
| 460 |
c = conn.cursor()
|
| 461 |
c.execute('''SELECT ta.video_id, v.title, v.channel_name, v.thumbnail, ta.old_value, ta.new_value, ta.change_percent, ta.detected_at
|
|
@@ -463,21 +608,21 @@ def show_trending_alerts():
|
|
| 463 |
alerts = c.fetchall()
|
| 464 |
conn.close()
|
| 465 |
|
| 466 |
-
info_box = '''
|
| 467 |
<div style="background:linear-gradient(135deg,#EF4444,#DC2626);color:#fff;padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
|
| 468 |
-
<h2 style="font-family:'Bangers',cursive;margin:0 0 10px 0">🔥 TRENDING - Sudden Surge Detection</h2>
|
| 469 |
<table style="width:100%;color:#fff;font-size:14px">
|
| 470 |
-
<tr><td style="width:120px"><b>📌 What is this
|
| 471 |
-
<tr><td><b>🎯 Purpose</b></td><td>Catch
|
| 472 |
-
<tr><td><b>⏱️ Trigger</b></td><td>When same video is searched again and views jumped 20
|
| 473 |
-
<tr><td><b>💡 Best for</b></td><td>Finding videos going viral RIGHT NOW
|
| 474 |
</table>
|
| 475 |
</div>'''
|
| 476 |
|
| 477 |
if not alerts:
|
| 478 |
try:
|
| 479 |
resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="US", maxResults=20).execute()
|
| 480 |
-
html = info_box + '<p style="font-family:Comic Neue,cursive;margin-bottom:15px">📢 No surge alerts yet.
|
| 481 |
for i, item in enumerate(resp.get("items", [])[:20], 1):
|
| 482 |
snip, stats = item["snippet"], item["statistics"]
|
| 483 |
title = snip["title"][:35] + "..." if len(snip["title"]) > 35 else snip["title"]
|
|
@@ -502,8 +647,8 @@ def show_trending_alerts():
|
|
| 502 |
</div>'''
|
| 503 |
return html + '</div>'
|
| 504 |
|
| 505 |
-
def show_top_growing():
|
| 506 |
-
|
| 507 |
conn = sqlite3.connect("youtube_data.db")
|
| 508 |
c = conn.cursor()
|
| 509 |
cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
|
|
@@ -516,21 +661,21 @@ def show_top_growing():
|
|
| 516 |
results = c.fetchall()
|
| 517 |
conn.close()
|
| 518 |
|
| 519 |
-
info_box = '''
|
| 520 |
<div style="background:linear-gradient(135deg,#3B82F6,#2563EB);color:#fff;padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
|
| 521 |
-
<h2 style="font-family:'Bangers',cursive;margin:0 0 10px 0">📈 TOP GROWING - 48h Growth Champions</h2>
|
| 522 |
<table style="width:100%;color:#fff;font-size:14px">
|
| 523 |
-
<tr><td style="width:120px"><b>📌 What is this
|
| 524 |
-
<tr><td><b>🎯 Purpose</b></td><td>Find
|
| 525 |
-
<tr><td><b>📊 Calculation</b></td><td>(Max Views - Min Views) / Min Views × 100
|
| 526 |
-
<tr><td><b>💡 Best for</b></td><td>
|
| 527 |
</table>
|
| 528 |
</div>'''
|
| 529 |
|
| 530 |
if not results:
|
| 531 |
try:
|
| 532 |
resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="US", maxResults=20).execute()
|
| 533 |
-
html = info_box + '<p style="font-family:Comic Neue,cursive;margin-bottom:15px">📢 No growth data yet.
|
| 534 |
for i, item in enumerate(resp.get("items", [])[:20], 1):
|
| 535 |
snip, stats = item["snippet"], item["statistics"]
|
| 536 |
views, likes = int(stats.get("viewCount", 0)), int(stats.get("likeCount", 0))
|
|
@@ -559,11 +704,11 @@ def show_top_growing():
|
|
| 559 |
</div>'''
|
| 560 |
return html + '</div>'
|
| 561 |
|
| 562 |
-
def show_ai_picks():
|
| 563 |
-
|
| 564 |
global ai_pick_storage
|
| 565 |
if not ai_pick_storage["videos"]:
|
| 566 |
-
return '<div style="background:#FACC15;padding:20px;border:3px solid #1F2937;border-radius:8px;text-align:center;font-family:Comic Neue,cursive"><h2 style="font-family:Bangers,cursive">⭐ AI PICK - No Data Yet</h2><p>Run a search first
|
| 567 |
|
| 568 |
videos, ratings = ai_pick_storage["videos"], ai_pick_storage["ratings"]
|
| 569 |
top_picks = [(i, v, ratings.get(i, 0)) for i, v in enumerate(videos) if ratings.get(i, 0) >= 3]
|
|
@@ -572,13 +717,14 @@ def show_ai_picks():
|
|
| 572 |
analysis = ""
|
| 573 |
if groq_client and top_picks:
|
| 574 |
info = "\n".join([f"- {v['title'][:50]} (Views:{format_count(v['views'])})" for _, v, _ in top_picks[:5]])
|
| 575 |
-
|
|
|
|
| 576 |
if result and "Error" not in result:
|
| 577 |
-
analysis = f'<div style="background:#1F2937;color:#10B981;padding:15px;border:3px solid #10B981;border-radius:8px;margin:15px 0;font-family:Courier New,monospace"><h4 style="color:#FACC15;margin:0 0 10px">🤖 AI ANALYSIS</h4><p style="margin:0;font-size:14px;line-height:1.6">{result}</p></div>'
|
| 578 |
|
| 579 |
html = f'''<div style="background:linear-gradient(135deg,#FACC15,#EF4444);padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
|
| 580 |
-
<h2 style="font-family:Bangers,cursive;color:#1F2937;margin:0;text-shadow:2px 2px 0 #FFF">⭐ AI PICK - TOP RECOMMENDATIONS</h2>
|
| 581 |
-
<p style="color:#1F2937;margin:5px 0 0;font-family:Comic Neue,cursive;font-weight:700">
|
| 582 |
</div>{analysis}<div style="display:flex;flex-wrap:wrap;gap:20px">'''
|
| 583 |
|
| 584 |
for idx, (_, v, rating) in enumerate(top_picks[:30], 1):
|
|
@@ -594,165 +740,41 @@ def show_ai_picks():
|
|
| 594 |
<span style="color:#EF4444">❤️ {format_count(v['likes'])}</span>
|
| 595 |
</div></div>'''
|
| 596 |
|
| 597 |
-
return html + '</div>' if top_picks else html + '<p style="text-align:center;padding:30px">No ⭐⭐+
|
| 598 |
-
|
| 599 |
-
#
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
""
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
if not
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
if not
|
| 626 |
-
|
| 627 |
-
if
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
Provide:
|
| 633 |
-
1. **Current Trend Status**: Is it rising, stable, or declining?
|
| 634 |
-
2. **Peak Season**: When does this topic typically peak? (months/events)
|
| 635 |
-
3. **6-Month Forecast**: Predicted trajectory with reasoning
|
| 636 |
-
4. **Risk Factors**: What could affect this trend negatively?
|
| 637 |
-
5. **Opportunity Windows**: Best times to publish content
|
| 638 |
-
6. **Related Emerging Topics**: What related topics are gaining momentum?
|
| 639 |
-
|
| 640 |
-
Be specific with data-driven reasoning."""
|
| 641 |
-
|
| 642 |
-
result = call_llm(prompt, 1500)
|
| 643 |
-
return result if result else "Error generating prediction"
|
| 644 |
-
|
| 645 |
-
def analyze_content_ideas(keyword):
|
| 646 |
-
"""Content Ideas generation"""
|
| 647 |
-
if not keyword:
|
| 648 |
-
return "⚠️ Please enter a keyword first!"
|
| 649 |
-
if not groq_client:
|
| 650 |
-
return "⚠️ LLM API not configured."
|
| 651 |
-
|
| 652 |
-
prompt = f"""You are a creative YouTube content strategist. Generate 10 unique video ideas for the topic "{keyword}".
|
| 653 |
-
|
| 654 |
-
For each idea provide:
|
| 655 |
-
1. **Video Title** (clickable, SEO-optimized)
|
| 656 |
-
2. **Hook** (first 5 seconds script)
|
| 657 |
-
3. **Format** (Tutorial/Listicle/Challenge/Story/etc)
|
| 658 |
-
4. **Target Length** (Short <1min / Medium 5-10min / Long 15+min)
|
| 659 |
-
5. **Thumbnail Concept** (brief visual description)
|
| 660 |
-
6. **Viral Potential** (1-10 score with reason)
|
| 661 |
-
|
| 662 |
-
Make ideas diverse, creative, and actionable."""
|
| 663 |
-
|
| 664 |
-
result = call_llm(prompt, 2000)
|
| 665 |
-
return result if result else "Error generating ideas"
|
| 666 |
-
|
| 667 |
-
def analyze_channel(channel_url):
|
| 668 |
-
"""Channel Analysis"""
|
| 669 |
-
if not channel_url:
|
| 670 |
-
return "⚠️ Please enter a channel name or URL!"
|
| 671 |
-
if not groq_client:
|
| 672 |
-
return "⚠️ LLM API not configured."
|
| 673 |
-
|
| 674 |
-
# Extract channel info if possible
|
| 675 |
-
channel_name = channel_url.split("/")[-1] if "/" in channel_url else channel_url
|
| 676 |
-
|
| 677 |
-
prompt = f"""You are a YouTube channel growth consultant. Analyze a channel focused on or named "{channel_name}".
|
| 678 |
-
|
| 679 |
-
Provide a comprehensive analysis:
|
| 680 |
-
|
| 681 |
-
1. **Niche Assessment**
|
| 682 |
-
- Target audience profile
|
| 683 |
-
- Content saturation level
|
| 684 |
-
- Growth potential in this niche
|
| 685 |
-
|
| 686 |
-
2. **Content Strategy Recommendations**
|
| 687 |
-
- Optimal posting frequency
|
| 688 |
-
- Best content formats for this niche
|
| 689 |
-
- Trending topics to cover
|
| 690 |
-
|
| 691 |
-
3. **Growth Tactics**
|
| 692 |
-
- Quick wins (immediate actions)
|
| 693 |
-
- Medium-term strategies (1-3 months)
|
| 694 |
-
- Long-term positioning (6-12 months)
|
| 695 |
-
|
| 696 |
-
4. **Monetization Opportunities**
|
| 697 |
-
- Ad revenue potential
|
| 698 |
-
- Sponsorship opportunities
|
| 699 |
-
- Alternative revenue streams
|
| 700 |
-
|
| 701 |
-
5. **Competitive Advantages to Build**
|
| 702 |
-
- Unique value propositions
|
| 703 |
-
- Differentiation strategies
|
| 704 |
-
|
| 705 |
-
Be specific and actionable."""
|
| 706 |
-
|
| 707 |
-
result = call_llm(prompt, 2000)
|
| 708 |
-
return result if result else "Error analyzing channel"
|
| 709 |
-
|
| 710 |
-
def analyze_competitor(my_channel, competitor):
|
| 711 |
-
"""Competitor Analysis"""
|
| 712 |
-
if not my_channel or not competitor:
|
| 713 |
-
return "⚠️ Please enter both your channel/topic and competitor!"
|
| 714 |
-
if not groq_client:
|
| 715 |
-
return "⚠️ LLM API not configured."
|
| 716 |
-
|
| 717 |
-
prompt = f"""You are a competitive intelligence analyst for YouTube.
|
| 718 |
-
Compare "{my_channel}" against competitor "{competitor}".
|
| 719 |
-
|
| 720 |
-
Provide detailed competitive analysis:
|
| 721 |
-
|
| 722 |
-
1. **Positioning Comparison**
|
| 723 |
-
- Content focus differences
|
| 724 |
-
- Target audience overlap
|
| 725 |
-
- Brand positioning
|
| 726 |
-
|
| 727 |
-
2. **Content Gap Analysis**
|
| 728 |
-
- Topics competitor covers that you don't
|
| 729 |
-
- Your unique content opportunities
|
| 730 |
-
- Underserved audience needs
|
| 731 |
-
|
| 732 |
-
3. **Performance Benchmarks**
|
| 733 |
-
- Typical metrics to aim for
|
| 734 |
-
- Engagement rate targets
|
| 735 |
-
- Growth rate expectations
|
| 736 |
-
|
| 737 |
-
4. **Competitive Advantages**
|
| 738 |
-
- Competitor's strengths to learn from
|
| 739 |
-
- Competitor's weaknesses to exploit
|
| 740 |
-
- Your potential differentiators
|
| 741 |
-
|
| 742 |
-
5. **Action Plan**
|
| 743 |
-
- Immediate actions (this week)
|
| 744 |
-
- Short-term goals (this month)
|
| 745 |
-
- Strategic moves (this quarter)
|
| 746 |
-
|
| 747 |
-
6. **Content Ideas to Outperform**
|
| 748 |
-
- 5 specific video ideas that could beat competitor content
|
| 749 |
-
|
| 750 |
-
Be strategic and actionable."""
|
| 751 |
-
|
| 752 |
-
result = call_llm(prompt, 2000)
|
| 753 |
-
return result if result else "Error analyzing competitor"
|
| 754 |
-
|
| 755 |
-
def show_search_history():
|
| 756 |
conn = sqlite3.connect("youtube_data.db")
|
| 757 |
c = conn.cursor()
|
| 758 |
c.execute('SELECT keyword,country,language,sort_by,results_count,searched_at FROM search_history ORDER BY searched_at DESC LIMIT 50')
|
|
@@ -760,15 +782,18 @@ def show_search_history():
|
|
| 760 |
conn.close()
|
| 761 |
|
| 762 |
if not history:
|
| 763 |
-
return "<p style='font-family:Comic Neue,cursive'>No search history yet
|
| 764 |
|
| 765 |
-
html = '''<div style="background:#1F2937;color:#FACC15;padding:15px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
|
| 766 |
-
<h3 style="font-family:Bangers,cursive;margin:0">🕐 SEARCH HISTORY</h3></div>
|
| 767 |
<table style="width:100%;border-collapse:collapse;font-family:Comic Neue,cursive">
|
| 768 |
<thead><tr style="background:#FACC15;color:#1F2937">
|
| 769 |
-
<th style="padding:12px;border:2px solid #1F2937">
|
| 770 |
-
<th style="padding:12px">
|
| 771 |
-
<th style="padding:12px">
|
|
|
|
|
|
|
|
|
|
| 772 |
</tr></thead><tbody>'''
|
| 773 |
|
| 774 |
for kw, country, lang, sort_by, cnt, searched in history:
|
|
@@ -780,6 +805,23 @@ def show_search_history():
|
|
| 780 |
|
| 781 |
return html + '</tbody></table>'
|
| 782 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
# Initial trending
|
| 784 |
initial_trending = get_real_trending_keywords("US", "en")
|
| 785 |
|
|
@@ -787,13 +829,29 @@ initial_trending = get_real_trending_keywords("US", "en")
|
|
| 787 |
# 🎨 Gradio UI
|
| 788 |
# ============================================
|
| 789 |
with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
|
|
|
|
|
|
|
|
|
|
| 790 |
gr.HTML('''<div style="text-align:center;margin:20px 0">
|
| 791 |
<a href="https://www.humangen.ai" target="_blank">
|
| 792 |
<img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge">
|
| 793 |
</a></div>''')
|
| 794 |
|
| 795 |
-
|
| 796 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
|
| 798 |
with gr.Tabs():
|
| 799 |
# Tab 1: Search
|
|
@@ -807,8 +865,8 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
|
|
| 807 |
with gr.Row():
|
| 808 |
country = gr.Dropdown(list(COUNTRIES.keys()), value="United States", label="Country")
|
| 809 |
language = gr.Dropdown(list(LANGUAGES.keys()), value="Auto (by Country)", label="Language")
|
| 810 |
-
sort_by = gr.Dropdown(list(
|
| 811 |
-
date_filter = gr.Dropdown(list(
|
| 812 |
max_results = gr.Slider(10, 1000, value=100, step=10, label="Max Results")
|
| 813 |
output = gr.HTML()
|
| 814 |
|
|
@@ -833,37 +891,31 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
|
|
| 833 |
# Tab 5: AI Tools
|
| 834 |
with gr.Tab("🤖 AI Tools"):
|
| 835 |
gr.Markdown("### 🧠 LLM-Powered Analysis Tools (GPT-OSS-120B)")
|
| 836 |
-
|
| 837 |
with gr.Tabs():
|
| 838 |
-
# Keyword Suggest
|
| 839 |
with gr.Tab("🏷️ Keyword Suggest"):
|
| 840 |
gr.Markdown("**Generate related keywords with SEO insights**")
|
| 841 |
kw_input = gr.Textbox(label="Enter base keyword", placeholder="e.g., Python tutorial")
|
| 842 |
kw_btn = gr.Button("🔍 Generate Keywords", variant="primary")
|
| 843 |
kw_output = gr.Textbox(label="Suggested Keywords", lines=20, elem_classes="llm-result")
|
| 844 |
|
| 845 |
-
# Trend Prediction
|
| 846 |
with gr.Tab("🔮 Trend Prediction"):
|
| 847 |
gr.Markdown("**Predict future trend trajectory**")
|
| 848 |
tp_input = gr.Textbox(label="Enter topic/keyword", placeholder="e.g., AI tools")
|
| 849 |
tp_btn = gr.Button("🔮 Predict Trend", variant="primary")
|
| 850 |
tp_output = gr.Textbox(label="Trend Analysis", lines=20, elem_classes="llm-result")
|
| 851 |
|
| 852 |
-
# Content Ideas
|
| 853 |
with gr.Tab("💡 Content Ideas"):
|
| 854 |
gr.Markdown("**Generate creative video ideas**")
|
| 855 |
ci_input = gr.Textbox(label="Enter topic", placeholder="e.g., Home workout")
|
| 856 |
ci_btn = gr.Button("💡 Generate Ideas", variant="primary")
|
| 857 |
ci_output = gr.Textbox(label="Content Ideas", lines=25, elem_classes="llm-result")
|
| 858 |
|
| 859 |
-
# Channel Analysis
|
| 860 |
with gr.Tab("📊 Channel Analysis"):
|
| 861 |
gr.Markdown("**Get growth strategy for your channel**")
|
| 862 |
ca_input = gr.Textbox(label="Enter channel name or niche", placeholder="e.g., Tech reviews")
|
| 863 |
ca_btn = gr.Button("📊 Analyze Channel", variant="primary")
|
| 864 |
ca_output = gr.Textbox(label="Channel Analysis", lines=25, elem_classes="llm-result")
|
| 865 |
|
| 866 |
-
# Competitor Analysis
|
| 867 |
with gr.Tab("⚔️ Competitor Analysis"):
|
| 868 |
gr.Markdown("**Compare against competitors**")
|
| 869 |
with gr.Row():
|
|
@@ -877,24 +929,36 @@ with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
|
|
| 877 |
history_btn = gr.Button("🔄 Refresh", variant="primary")
|
| 878 |
history_out = gr.HTML()
|
| 879 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 880 |
# Events
|
| 881 |
trending.change(use_trending_keyword, trending, keyword)
|
| 882 |
country.change(update_trending, country, trending)
|
| 883 |
-
btn.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results], [output, db_stats])
|
| 884 |
-
keyword.submit(search_videos, [keyword, country, language, sort_by, date_filter, max_results], [output, db_stats])
|
| 885 |
-
refresh.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results], [output, db_stats])
|
| 886 |
|
| 887 |
-
pick_btn.click(show_ai_picks,
|
| 888 |
-
alerts_btn.click(show_trending_alerts,
|
| 889 |
-
growing_btn.click(show_top_growing,
|
| 890 |
-
history_btn.click(show_search_history,
|
| 891 |
|
| 892 |
# AI Tools events
|
| 893 |
-
kw_btn.click(analyze_keyword_suggest, kw_input, kw_output)
|
| 894 |
-
tp_btn.click(analyze_trend_prediction, tp_input, tp_output)
|
| 895 |
-
ci_btn.click(analyze_content_ideas, ci_input, ci_output)
|
| 896 |
-
ca_btn.click(analyze_channel, ca_input, ca_output)
|
| 897 |
-
comp_btn.click(analyze_competitor, [comp_my, comp_rival], comp_output)
|
| 898 |
|
| 899 |
demo.launch()
|
| 900 |
|
|
|
|
| 16 |
ai_pick_storage = {"videos": [], "ratings": {}, "timestamp": None}
|
| 17 |
|
| 18 |
# ============================================
|
| 19 |
+
# 🌐 UI Language Translations
|
| 20 |
+
# ============================================
|
| 21 |
+
UI_LANG = {
|
| 22 |
+
"en": {
|
| 23 |
+
"title": "🎬 YOUTUBE TREND ANALYZER 📊",
|
| 24 |
+
"trending_keywords": "### 🔥 Trending Keywords",
|
| 25 |
+
"click_autofill": "Click to auto-fill",
|
| 26 |
+
"search_keyword": "Search Keyword",
|
| 27 |
+
"enter_keyword": "Enter keyword...",
|
| 28 |
+
"search": "🔍 SEARCH",
|
| 29 |
+
"refresh": "🔄 Refresh",
|
| 30 |
+
"country": "Country",
|
| 31 |
+
"language": "Language",
|
| 32 |
+
"sort_by": "Sort By",
|
| 33 |
+
"period": "Period",
|
| 34 |
+
"max_results": "Max Results",
|
| 35 |
+
"tab_search": "🔍 Search",
|
| 36 |
+
"tab_ai_pick": "⭐ AI Pick",
|
| 37 |
+
"tab_trending": "🔥 Trending",
|
| 38 |
+
"tab_growing": "📈 Top Growing",
|
| 39 |
+
"tab_ai_tools": "🤖 AI Tools",
|
| 40 |
+
"tab_history": "🕐 History",
|
| 41 |
+
"ai_pick_desc": "### 🤖 AI-Curated Top Recommendations (⭐⭐ and above)",
|
| 42 |
+
"trending_desc": "### 🔥 Sudden Surge Detection (20%+ view increase)",
|
| 43 |
+
"growing_desc": "### 📈 48-Hour Growth Champions (Highest growth rate)",
|
| 44 |
+
"ai_tools_desc": "### 🧠 LLM-Powered Analysis Tools (GPT-OSS-120B)",
|
| 45 |
+
"keyword_suggest": "🏷️ Keyword Suggest",
|
| 46 |
+
"trend_prediction": "🔮 Trend Prediction",
|
| 47 |
+
"content_ideas": "💡 Content Ideas",
|
| 48 |
+
"channel_analysis": "📊 Channel Analysis",
|
| 49 |
+
"competitor_analysis": "⚔️ Competitor Analysis",
|
| 50 |
+
"enter_base_keyword": "Enter base keyword",
|
| 51 |
+
"enter_topic": "Enter topic/keyword",
|
| 52 |
+
"enter_channel": "Enter channel name or niche",
|
| 53 |
+
"your_channel": "Your Channel/Topic",
|
| 54 |
+
"competitor": "Competitor",
|
| 55 |
+
"generate_keywords": "🔍 Generate Keywords",
|
| 56 |
+
"predict_trend": "🔮 Predict Trend",
|
| 57 |
+
"generate_ideas": "💡 Generate Ideas",
|
| 58 |
+
"analyze_channel": "📊 Analyze Channel",
|
| 59 |
+
"analyze_competition": "⚔️ Analyze Competition",
|
| 60 |
+
"no_data": "No data yet. Run a search first!",
|
| 61 |
+
"results": "results",
|
| 62 |
+
"total": "Total",
|
| 63 |
+
"views": "Views",
|
| 64 |
+
"likes": "Likes",
|
| 65 |
+
"subs": "Subs",
|
| 66 |
+
"date": "Date",
|
| 67 |
+
"rank": "Rank",
|
| 68 |
+
"thumb": "Thumb",
|
| 69 |
+
"channel": "Channel",
|
| 70 |
+
"comments": "Cmts",
|
| 71 |
+
"ai_pick_col": "AI Pick",
|
| 72 |
+
"sort_options": {"Most Viewed": "viewCount", "Latest": "date", "Relevance": "relevance", "Top Rated": "rating"},
|
| 73 |
+
"date_options": {"All Time": "", "Today": "today", "This Week": "thisWeek", "This Month": "thisMonth", "This Year": "thisYear"},
|
| 74 |
+
},
|
| 75 |
+
"ko": {
|
| 76 |
+
"title": "🎬 유튜브 트렌드 분석기 📊",
|
| 77 |
+
"trending_keywords": "### 🔥 실시간 인기 키워드",
|
| 78 |
+
"click_autofill": "클릭하면 자동 입력",
|
| 79 |
+
"search_keyword": "검색어",
|
| 80 |
+
"enter_keyword": "검색어 입력...",
|
| 81 |
+
"search": "🔍 검색",
|
| 82 |
+
"refresh": "🔄 새로고침",
|
| 83 |
+
"country": "국가",
|
| 84 |
+
"language": "언어",
|
| 85 |
+
"sort_by": "정렬",
|
| 86 |
+
"period": "기간",
|
| 87 |
+
"max_results": "최대 결과",
|
| 88 |
+
"tab_search": "🔍 검색",
|
| 89 |
+
"tab_ai_pick": "⭐ AI 추천",
|
| 90 |
+
"tab_trending": "🔥 급상승",
|
| 91 |
+
"tab_growing": "📈 급성장",
|
| 92 |
+
"tab_ai_tools": "🤖 AI 도구",
|
| 93 |
+
"tab_history": "🕐 기록",
|
| 94 |
+
"ai_pick_desc": "### 🤖 AI 추천 영상 (⭐⭐ 이상)",
|
| 95 |
+
"trending_desc": "### 🔥 급상승 감지 (조회수 20%+ 급증)",
|
| 96 |
+
"growing_desc": "### 📈 48시간 급성장 TOP (최고 성장률)",
|
| 97 |
+
"ai_tools_desc": "### 🧠 LLM 기반 분석 도구 (GPT-OSS-120B)",
|
| 98 |
+
"keyword_suggest": "🏷️ 키워드 추천",
|
| 99 |
+
"trend_prediction": "🔮 트렌드 예측",
|
| 100 |
+
"content_ideas": "💡 콘텐츠 아이디어",
|
| 101 |
+
"channel_analysis": "📊 채널 분석",
|
| 102 |
+
"competitor_analysis": "⚔️ 경쟁 분석",
|
| 103 |
+
"enter_base_keyword": "기본 키워드 입력",
|
| 104 |
+
"enter_topic": "주제/키워드 입력",
|
| 105 |
+
"enter_channel": "채널명 또는 주제 입력",
|
| 106 |
+
"your_channel": "내 채널/주제",
|
| 107 |
+
"competitor": "경쟁자",
|
| 108 |
+
"generate_keywords": "🔍 키워드 생성",
|
| 109 |
+
"predict_trend": "🔮 트렌드 예측",
|
| 110 |
+
"generate_ideas": "💡 아이디어 생성",
|
| 111 |
+
"analyze_channel": "📊 채널 분석",
|
| 112 |
+
"analyze_competition": "⚔️ 경쟁 분석",
|
| 113 |
+
"no_data": "데이터 없음. 먼저 검색하세요!",
|
| 114 |
+
"results": "개 결과",
|
| 115 |
+
"total": "총",
|
| 116 |
+
"views": "조회수",
|
| 117 |
+
"likes": "좋아요",
|
| 118 |
+
"subs": "구독자",
|
| 119 |
+
"date": "날짜",
|
| 120 |
+
"rank": "순위",
|
| 121 |
+
"thumb": "썸네일",
|
| 122 |
+
"channel": "채널",
|
| 123 |
+
"comments": "댓글",
|
| 124 |
+
"ai_pick_col": "AI 추천",
|
| 125 |
+
"sort_options": {"조회수 순": "viewCount", "최신순": "date", "관련성 순": "relevance", "평점 순": "rating"},
|
| 126 |
+
"date_options": {"전체 기간": "", "오늘": "today", "이번 주": "thisWeek", "이번 달": "thisMonth", "올해": "thisYear"},
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
# ============================================
|
| 131 |
+
# 🎨 Comic Classic Theme CSS + Hide HF Elements
|
| 132 |
# ============================================
|
| 133 |
css = """
|
| 134 |
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
|
| 135 |
|
| 136 |
+
/* ===== Hide Hugging Face Header/Footer/Buttons ===== */
|
| 137 |
+
#space-header,
|
| 138 |
+
.space-header,
|
| 139 |
+
header.svelte-1ax1toq,
|
| 140 |
+
.huggingface-space-header,
|
| 141 |
+
[data-testid="space-header"],
|
| 142 |
+
.svelte-1ed2p3z,
|
| 143 |
+
.svelte-kqij2n,
|
| 144 |
+
.gradio-container > header,
|
| 145 |
+
div.svelte-1kyws56,
|
| 146 |
+
.wrap.svelte-1kyws56,
|
| 147 |
+
button.svelte-1kyws56,
|
| 148 |
+
.duplicate-button,
|
| 149 |
+
.settings-button,
|
| 150 |
+
[class*="settings"],
|
| 151 |
+
[class*="duplicate"],
|
| 152 |
+
a[href*="huggingface.co/spaces"][target="_blank"],
|
| 153 |
+
.embed-buttons,
|
| 154 |
+
.buttons-container,
|
| 155 |
+
div[class*="header"] > button,
|
| 156 |
+
header button,
|
| 157 |
+
.gr-button-icon,
|
| 158 |
+
footer,
|
| 159 |
+
.footer,
|
| 160 |
+
.gradio-container footer,
|
| 161 |
+
.built-with,
|
| 162 |
+
[class*="footer"],
|
| 163 |
+
.built-with-gradio,
|
| 164 |
+
a[href*="gradio.app"] {
|
| 165 |
+
display: none !important;
|
| 166 |
+
visibility: hidden !important;
|
| 167 |
+
height: 0 !important;
|
| 168 |
+
width: 0 !important;
|
| 169 |
+
padding: 0 !important;
|
| 170 |
+
margin: 0 !important;
|
| 171 |
+
overflow: hidden !important;
|
| 172 |
+
opacity: 0 !important;
|
| 173 |
+
pointer-events: none !important;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/* Force hide top-right buttons */
|
| 177 |
+
.gradio-container > div:first-child button,
|
| 178 |
+
.gradio-container > div > button {
|
| 179 |
+
display: none !important;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* ===== Main Theme ===== */
|
| 183 |
.gradio-container {
|
| 184 |
background-color: #FEF9C3 !important;
|
| 185 |
background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
|
|
|
|
| 188 |
font-family: 'Comic Neue', cursive, sans-serif !important;
|
| 189 |
}
|
| 190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
.header-text h1 {
|
| 192 |
font-family: 'Bangers', cursive !important;
|
| 193 |
color: #1F2937 !important;
|
|
|
|
| 248 |
font-family: 'Courier New', monospace !important;
|
| 249 |
white-space: pre-wrap !important;
|
| 250 |
}
|
| 251 |
+
|
| 252 |
+
.lang-selector {
|
| 253 |
+
position: fixed !important;
|
| 254 |
+
top: 10px !important;
|
| 255 |
+
right: 10px !important;
|
| 256 |
+
z-index: 9999 !important;
|
| 257 |
+
background: #FACC15 !important;
|
| 258 |
+
border: 3px solid #1F2937 !important;
|
| 259 |
+
border-radius: 8px !important;
|
| 260 |
+
padding: 5px 10px !important;
|
| 261 |
+
font-family: 'Comic Neue', cursive !important;
|
| 262 |
+
font-weight: 700 !important;
|
| 263 |
+
box-shadow: 3px 3px 0 #1F2937 !important;
|
| 264 |
+
}
|
| 265 |
"""
|
| 266 |
|
| 267 |
# DB 초기화
|
|
|
|
| 316 |
}
|
| 317 |
|
| 318 |
LANGUAGES = {
|
| 319 |
+
"Auto (by Country)": "", "English": "en", "Korean": "ko", "Spanish": "es",
|
| 320 |
+
"Portuguese": "pt", "French": "fr", "German": "de", "Italian": "it",
|
| 321 |
+
"Russian": "ru", "Japanese": "ja", "Chinese": "zh", "Hindi": "hi",
|
| 322 |
"Arabic": "ar", "Turkish": "tr", "Indonesian": "id", "Vietnamese": "vi",
|
| 323 |
"Thai": "th", "Dutch": "nl", "Polish": "pl", "Swedish": "sv",
|
| 324 |
"Norwegian": "no", "Danish": "da", "Finnish": "fi", "Greek": "el",
|
|
|
|
| 326 |
"Hungarian": "hu", "Filipino": "tl", "Malay": "ms",
|
| 327 |
}
|
| 328 |
|
| 329 |
+
SORT_OPTIONS_MAP = {"viewCount": "viewCount", "date": "date", "relevance": "relevance", "rating": "rating"}
|
| 330 |
+
DATE_OPTIONS_MAP = {"": "", "today": "today", "thisWeek": "thisWeek", "thisMonth": "thisMonth", "thisYear": "thisYear"}
|
| 331 |
|
| 332 |
def format_count(count):
|
| 333 |
if count >= 1000000000: return f"{count/1000000000:.1f}B"
|
|
|
|
| 336 |
return str(count)
|
| 337 |
|
| 338 |
def call_llm(prompt, max_tokens=2000):
|
|
|
|
| 339 |
if not groq_client:
|
| 340 |
return None
|
| 341 |
try:
|
| 342 |
completion = groq_client.chat.completions.create(
|
| 343 |
model="openai/gpt-oss-120b",
|
| 344 |
messages=[{"role": "user", "content": prompt}],
|
| 345 |
+
temperature=0.7, max_completion_tokens=max_tokens, top_p=1, stream=True, stop=None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
)
|
| 347 |
result = ""
|
| 348 |
for chunk in completion:
|
|
|
|
| 353 |
return f"Error: {e}"
|
| 354 |
|
| 355 |
def get_ai_pick_rating(videos_data):
|
|
|
|
| 356 |
global ai_pick_storage
|
| 357 |
+
if not videos_data: return {}
|
|
|
|
| 358 |
|
| 359 |
if groq_client:
|
| 360 |
try:
|
| 361 |
sample = videos_data[:50]
|
| 362 |
+
video_info = "\n".join([f"#{i+1}. {v['title'][:40]}, Views:{v['views']}, Likes:{v['likes']}, Subs:{v.get('subs',0)}" for i, v in enumerate(sample)])
|
| 363 |
+
prompt = f"Rate YouTube videos 0-4. 0=None,1=♥,2=⭐,3=⭐⭐,4=⭐⭐⭐\nConsider engagement, viral potential. Format: 1:3,2:2,3:4\n\n{video_info}\n\nResponse (number:rating only):"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
result = call_llm(prompt, 1500)
|
| 365 |
if result and "Error" not in result:
|
| 366 |
ratings = {}
|
|
|
|
| 368 |
ratings[int(idx)-1] = int(rating)
|
| 369 |
if len(videos_data) > 50:
|
| 370 |
local = calculate_local_rating(videos_data[50:])
|
| 371 |
+
for k, v in local.items(): ratings[k + 50] = v
|
|
|
|
| 372 |
ai_pick_storage = {"videos": videos_data, "ratings": ratings, "timestamp": datetime.now().isoformat()}
|
| 373 |
return ratings
|
| 374 |
+
except: pass
|
|
|
|
| 375 |
|
| 376 |
ratings = calculate_local_rating(videos_data)
|
| 377 |
ai_pick_storage = {"videos": videos_data, "ratings": ratings, "timestamp": datetime.now().isoformat()}
|
|
|
|
| 390 |
if views > 0:
|
| 391 |
score += min(40, (views/max_views)*40)
|
| 392 |
score += min(30, ((likes+comments*2)/views)*300)
|
| 393 |
+
if subs > 0 and views > 0: score += min(30, (views/subs)*10)
|
| 394 |
+
elif views > avg_views: score += 15
|
|
|
|
|
|
|
| 395 |
|
| 396 |
if score >= 70: ratings[i] = 4
|
| 397 |
elif score >= 50: ratings[i] = 3
|
|
|
|
| 410 |
for item in response.get("items", []):
|
| 411 |
for tag in item["snippet"].get("tags", [])[:3]:
|
| 412 |
if tag.lower() not in seen and 2 <= len(tag) <= 20:
|
| 413 |
+
keywords.append(tag); seen.add(tag.lower())
|
|
|
|
| 414 |
channel = item["snippet"]["channelTitle"]
|
| 415 |
+
if channel.lower() not in seen: keywords.append(channel); seen.add(channel.lower())
|
|
|
|
|
|
|
| 416 |
if len(keywords) >= 20: break
|
| 417 |
return keywords[:20] if keywords else ["AI","gaming","music","vlog","shorts","news"]
|
| 418 |
+
except: return ["AI","ChatGPT","gaming","music","vlog","shorts","news","tech"]
|
|
|
|
| 419 |
|
| 420 |
def save_to_db(videos_data, channels_data, keyword, country, language, sort_by):
|
| 421 |
conn = sqlite3.connect("youtube_data.db")
|
|
|
|
| 434 |
c.execute('INSERT INTO trending_alerts VALUES (NULL,?,?,?,?,?,?)', (video['video_id'], 'views_surge', prev[0], video['views'], change, now))
|
| 435 |
for ch_id, subs in channels_data.items():
|
| 436 |
c.execute('INSERT OR IGNORE INTO channels VALUES (?,?,?)', (ch_id, '', now))
|
| 437 |
+
if isinstance(subs, int): c.execute('INSERT INTO channel_stats VALUES (NULL,?,?,?)', (ch_id, subs, now))
|
| 438 |
+
conn.commit(); conn.close()
|
|
|
|
|
|
|
| 439 |
|
| 440 |
def get_db_stats():
|
| 441 |
conn = sqlite3.connect("youtube_data.db")
|
| 442 |
c = conn.cursor()
|
| 443 |
stats = {}
|
| 444 |
for t, k in [("videos","videos"),("video_stats","stats"),("channels","channels"),("search_history","searches"),("trending_alerts","alerts")]:
|
| 445 |
+
c.execute(f"SELECT COUNT(*) FROM {t}"); stats[k] = c.fetchone()[0]
|
|
|
|
| 446 |
conn.close()
|
| 447 |
return stats
|
| 448 |
|
|
|
|
| 453 |
def use_trending_keyword(kw):
|
| 454 |
return kw if kw else ""
|
| 455 |
|
| 456 |
+
def search_videos(keyword, country, language, sort_by, date_filter, max_results, ui_lang):
|
| 457 |
+
L = UI_LANG.get(ui_lang, UI_LANG["en"])
|
| 458 |
if not keyword or not keyword.strip():
|
| 459 |
+
return f"⚠️ {'검색어를 입력하세요!' if ui_lang == 'ko' else 'Please enter a search keyword!'}", ""
|
| 460 |
|
| 461 |
max_results = int(max_results)
|
| 462 |
all_items, next_page = [], None
|
| 463 |
region_code, default_lang = COUNTRIES.get(country, ("", ""))
|
| 464 |
+
lang_code = default_lang if language in ["Auto (by Country)", "자동 (국가 기반)"] else LANGUAGES.get(language, "")
|
| 465 |
|
| 466 |
+
# Map sort/date options
|
| 467 |
+
sort_value = L["sort_options"].get(sort_by, "viewCount")
|
| 468 |
+
date_value = L["date_options"].get(date_filter, "")
|
| 469 |
+
|
| 470 |
+
params = {"q": keyword, "part": "snippet", "type": "video", "order": sort_value}
|
| 471 |
if region_code: params["regionCode"] = region_code
|
| 472 |
if lang_code: params["relevanceLanguage"] = lang_code
|
| 473 |
+
if date_value:
|
| 474 |
+
deltas = {"today": 1, "thisWeek": 7, "thisMonth": 30, "thisYear": 365}
|
| 475 |
+
params["publishedAfter"] = (datetime.utcnow() - timedelta(days=deltas.get(date_value, 0))).strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 476 |
|
| 477 |
while len(all_items) < max_results:
|
| 478 |
params["maxResults"] = min(50, max_results - len(all_items))
|
|
|
|
| 484 |
next_page = resp.get("nextPageToken")
|
| 485 |
if not next_page: break
|
| 486 |
|
| 487 |
+
if not all_items:
|
| 488 |
+
return f"{'검색 결과 없음' if ui_lang == 'ko' else 'No results found.'}", ""
|
| 489 |
|
| 490 |
video_ids = [item["id"]["videoId"] for item in all_items]
|
| 491 |
channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
|
|
|
|
| 500 |
for i in range(0, len(channel_ids), 50):
|
| 501 |
for ch in youtube.channels().list(id=",".join(channel_ids[i:i+50]), part="statistics").execute().get("items", []):
|
| 502 |
sub = ch["statistics"].get("subscriberCount", "0")
|
| 503 |
+
if sub: channel_subs_raw[ch["id"]] = int(sub); channel_subs[ch["id"]] = format_count(int(sub))
|
|
|
|
|
|
|
| 504 |
|
| 505 |
videos_data = []
|
| 506 |
for item in all_items:
|
|
|
|
| 536 |
.qsort button:hover{{background:#1F2937;color:#FACC15}}
|
| 537 |
</style>
|
| 538 |
<div class="res-hdr">
|
| 539 |
+
🎬 {L["total"]} <b>{len(all_items)}</b> {L["results"]} | 🔍 "{keyword}" | 🌍 {country}
|
| 540 |
+
<br><span style="font-size:0.9rem">🤖 AI Pick: ♥ ⭐ ⭐⭐ ⭐⭐⭐</span>
|
| 541 |
<div style="margin-top:10px"><span style="font-size:0.9rem">⚡ Sort:</span>
|
| 542 |
<span class="qsort">
|
| 543 |
+
<button onclick="sortT_{table_id}(5,'n')">{L["views"]}</button>
|
| 544 |
+
<button onclick="sortT_{table_id}(6,'n')">{L["likes"]}</button>
|
| 545 |
+
<button onclick="sortT_{table_id}(4,'n')">{L["subs"]}</button>
|
| 546 |
+
<button onclick="sortT_{table_id}(8,'n')">AI</button>
|
| 547 |
+
<button onclick="sortT_{table_id}(9,'s')">{L["date"]}</button>
|
| 548 |
</span></div>
|
| 549 |
</div>
|
| 550 |
<div style="max-height:700px;overflow-y:auto;border:3px solid #1F2937;border-radius:8px">
|
| 551 |
<table class="yt-tbl" id="{table_id}"><thead><tr>
|
| 552 |
+
<th onclick="sortT_{table_id}(0,'n')" style="width:45px">{L["rank"]}</th>
|
| 553 |
+
<th style="width:130px">{L["thumb"]}</th>
|
| 554 |
<th onclick="sortT_{table_id}(2,'s')">Title</th>
|
| 555 |
+
<th onclick="sortT_{table_id}(3,'s')" style="width:100px">{L["channel"]}</th>
|
| 556 |
+
<th onclick="sortT_{table_id}(4,'n')" style="width:65px">{L["subs"]}</th>
|
| 557 |
+
<th onclick="sortT_{table_id}(5,'n')" style="width:70px">{L["views"]}</th>
|
| 558 |
+
<th onclick="sortT_{table_id}(6,'n')" style="width:55px">{L["likes"]}</th>
|
| 559 |
+
<th onclick="sortT_{table_id}(7,'n')" style="width:50px">{L["comments"]}</th>
|
| 560 |
+
<th onclick="sortT_{table_id}(8,'n')" style="width:70px">{L["ai_pick_col"]}</th>
|
| 561 |
+
<th onclick="sortT_{table_id}(9,'s')" style="width:85px">{L["date"]}</th>
|
| 562 |
</tr></thead><tbody>'''
|
| 563 |
|
| 564 |
for i, v in enumerate(videos_data):
|
|
|
|
| 599 |
stats = get_db_stats()
|
| 600 |
return html, f"📊 DB: Videos {stats['videos']} | Records {stats['stats']} | Channels {stats['channels']} | Searches {stats['searches']}"
|
| 601 |
|
| 602 |
+
def show_trending_alerts(ui_lang):
|
| 603 |
+
is_ko = ui_lang == "ko"
|
| 604 |
conn = sqlite3.connect("youtube_data.db")
|
| 605 |
c = conn.cursor()
|
| 606 |
c.execute('''SELECT ta.video_id, v.title, v.channel_name, v.thumbnail, ta.old_value, ta.new_value, ta.change_percent, ta.detected_at
|
|
|
|
| 608 |
alerts = c.fetchall()
|
| 609 |
conn.close()
|
| 610 |
|
| 611 |
+
info_box = f'''
|
| 612 |
<div style="background:linear-gradient(135deg,#EF4444,#DC2626);color:#fff;padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
|
| 613 |
+
<h2 style="font-family:'Bangers',cursive;margin:0 0 10px 0">🔥 {"급상승 - 갑작스런 조회수 급증 감지" if is_ko else "TRENDING - Sudden Surge Detection"}</h2>
|
| 614 |
<table style="width:100%;color:#fff;font-size:14px">
|
| 615 |
+
<tr><td style="width:120px"><b>📌 {"이게 뭐야?" if is_ko else "What is this?"}</b></td><td>{"이전 대비 조회수 20% 이상 급증한 영상" if is_ko else "Videos with 20%+ sudden view increase"}</td></tr>
|
| 616 |
+
<tr><td><b>🎯 {"목적" if is_ko else "Purpose"}</b></td><td>{"실시간 바이럴 영상 및 핫이슈 포착" if is_ko else "Catch viral moments and breaking content"}</td></tr>
|
| 617 |
+
<tr><td><b>⏱️ {"작동방식" if is_ko else "Trigger"}</b></td><td>{"동일 영상 재검색시 조회수 20% 이상 증가 감지" if is_ko else "When same video is searched again and views jumped 20%+"}</td></tr>
|
| 618 |
+
<tr><td><b>💡 {"활용" if is_ko else "Best for"}</b></td><td>{"지금 바로 바이럴 중인 영상, 뉴스, 트렌드 발굴" if is_ko else "Finding videos going viral RIGHT NOW"}</td></tr>
|
| 619 |
</table>
|
| 620 |
</div>'''
|
| 621 |
|
| 622 |
if not alerts:
|
| 623 |
try:
|
| 624 |
resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="US", maxResults=20).execute()
|
| 625 |
+
html = info_box + f'<p style="font-family:Comic Neue,cursive;margin-bottom:15px">📢 {"아직 급상승 알림 없음. 검색을 여러 번 실행하세요!" if is_ko else "No surge alerts yet. Run searches multiple times!"}</p><div style="display:flex;flex-wrap:wrap;gap:15px">'
|
| 626 |
for i, item in enumerate(resp.get("items", [])[:20], 1):
|
| 627 |
snip, stats = item["snippet"], item["statistics"]
|
| 628 |
title = snip["title"][:35] + "..." if len(snip["title"]) > 35 else snip["title"]
|
|
|
|
| 647 |
</div>'''
|
| 648 |
return html + '</div>'
|
| 649 |
|
| 650 |
+
def show_top_growing(ui_lang):
|
| 651 |
+
is_ko = ui_lang == "ko"
|
| 652 |
conn = sqlite3.connect("youtube_data.db")
|
| 653 |
c = conn.cursor()
|
| 654 |
cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
|
|
|
|
| 661 |
results = c.fetchall()
|
| 662 |
conn.close()
|
| 663 |
|
| 664 |
+
info_box = f'''
|
| 665 |
<div style="background:linear-gradient(135deg,#3B82F6,#2563EB);color:#fff;padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:20px;font-family:'Comic Neue',cursive">
|
| 666 |
+
<h2 style="font-family:'Bangers',cursive;margin:0 0 10px 0">📈 {"급성장 - 48시간 성장률 TOP" if is_ko else "TOP GROWING - 48h Growth Champions"}</h2>
|
| 667 |
<table style="width:100%;color:#fff;font-size:14px">
|
| 668 |
+
<tr><td style="width:120px"><b>📌 {"이게 뭐야?" if is_ko else "What is this?"}</b></td><td>{"48시간 동안 가장 높은 성장률을 기록한 영상 순위" if is_ko else "Videos ranked by highest growth RATE over 48 hours"}</td></tr>
|
| 669 |
+
<tr><td><b>🎯 {"목적" if is_ko else "Purpose"}</b></td><td>{"꾸준히 성장하는 콘텐츠 발굴" if is_ko else "Find consistently rising content"}</td></tr>
|
| 670 |
+
<tr><td><b>📊 {"계산방식" if is_ko else "Calculation"}</b></td><td>({"최대 조회수" if is_ko else "Max Views"} - {"최소 조회수" if is_ko else "Min Views"}) / {"최소 조회수" if is_ko else "Min Views"} × 100%</td></tr>
|
| 671 |
+
<tr><td><b>💡 {"활용" if is_ko else "Best for"}</b></td><td>{"에버그린 콘텐츠, 안정적 트렌드 발굴" if is_ko else "Evergreen content, reliable trends"}</td></tr>
|
| 672 |
</table>
|
| 673 |
</div>'''
|
| 674 |
|
| 675 |
if not results:
|
| 676 |
try:
|
| 677 |
resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="US", maxResults=20).execute()
|
| 678 |
+
html = info_box + f'<p style="font-family:Comic Neue,cursive;margin-bottom:15px">📢 {"데이터 축적 중. 검색을 여러 번 실행하세요!" if is_ko else "No growth data yet. Run searches over time!"}</p><div style="display:flex;flex-wrap:wrap;gap:15px">'
|
| 679 |
for i, item in enumerate(resp.get("items", [])[:20], 1):
|
| 680 |
snip, stats = item["snippet"], item["statistics"]
|
| 681 |
views, likes = int(stats.get("viewCount", 0)), int(stats.get("likeCount", 0))
|
|
|
|
| 704 |
</div>'''
|
| 705 |
return html + '</div>'
|
| 706 |
|
| 707 |
+
def show_ai_picks(ui_lang):
|
| 708 |
+
is_ko = ui_lang == "ko"
|
| 709 |
global ai_pick_storage
|
| 710 |
if not ai_pick_storage["videos"]:
|
| 711 |
+
return f'<div style="background:#FACC15;padding:20px;border:3px solid #1F2937;border-radius:8px;text-align:center;font-family:Comic Neue,cursive"><h2 style="font-family:Bangers,cursive">⭐ {"AI 추천 - 데이터 없음" if is_ko else "AI PICK - No Data Yet"}</h2><p>{"먼저 검색을 실행하세요!" if is_ko else "Run a search first!"}</p></div>'
|
| 712 |
|
| 713 |
videos, ratings = ai_pick_storage["videos"], ai_pick_storage["ratings"]
|
| 714 |
top_picks = [(i, v, ratings.get(i, 0)) for i, v in enumerate(videos) if ratings.get(i, 0) >= 3]
|
|
|
|
| 717 |
analysis = ""
|
| 718 |
if groq_client and top_picks:
|
| 719 |
info = "\n".join([f"- {v['title'][:50]} (Views:{format_count(v['views'])})" for _, v, _ in top_picks[:5]])
|
| 720 |
+
lang_prompt = "한국어로 답변해주세요." if is_ko else ""
|
| 721 |
+
result = call_llm(f"Analyze top YouTube videos in 3-4 sentences:\n{info}\n\nProvide: 1) Common theme 2) Why popular 3) Content opportunity {lang_prompt}", 500)
|
| 722 |
if result and "Error" not in result:
|
| 723 |
+
analysis = f'<div style="background:#1F2937;color:#10B981;padding:15px;border:3px solid #10B981;border-radius:8px;margin:15px 0;font-family:Courier New,monospace"><h4 style="color:#FACC15;margin:0 0 10px">🤖 {"AI 분석" if is_ko else "AI ANALYSIS"}</h4><p style="margin:0;font-size:14px;line-height:1.6">{result}</p></div>'
|
| 724 |
|
| 725 |
html = f'''<div style="background:linear-gradient(135deg,#FACC15,#EF4444);padding:20px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
|
| 726 |
+
<h2 style="font-family:Bangers,cursive;color:#1F2937;margin:0;text-shadow:2px 2px 0 #FFF">⭐ {"AI 추천 - TOP 영상" if is_ko else "AI PICK - TOP RECOMMENDATIONS"}</h2>
|
| 727 |
+
<p style="color:#1F2937;margin:5px 0 0;font-family:Comic Neue,cursive;font-weight:700">{"⭐⭐ 이상 등급 영상" if is_ko else "⭐⭐+ rated videos"}: {len(top_picks)}{"개" if is_ko else ""}</p>
|
| 728 |
</div>{analysis}<div style="display:flex;flex-wrap:wrap;gap:20px">'''
|
| 729 |
|
| 730 |
for idx, (_, v, rating) in enumerate(top_picks[:30], 1):
|
|
|
|
| 740 |
<span style="color:#EF4444">❤️ {format_count(v['likes'])}</span>
|
| 741 |
</div></div>'''
|
| 742 |
|
| 743 |
+
return html + '</div>' if top_picks else html + f'<p style="text-align:center;padding:30px">{"⭐⭐+ 등급 영상 없음. 다른 키워드로 검색하세요!" if is_ko else "No ⭐⭐+ videos. Try different keywords!"}</p></div>'
|
| 744 |
+
|
| 745 |
+
# LLM Analysis Functions
|
| 746 |
+
def analyze_keyword_suggest(keyword, ui_lang):
|
| 747 |
+
if not keyword: return "⚠️ Please enter a keyword!" if ui_lang == "en" else "⚠️ 키워드를 입력하세요!"
|
| 748 |
+
if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
|
| 749 |
+
lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
|
| 750 |
+
return call_llm(f'YouTube SEO expert. For "{keyword}", suggest 15 related keywords with search volume, competition, content type. {lang}', 1500) or "Error"
|
| 751 |
+
|
| 752 |
+
def analyze_trend_prediction(keyword, ui_lang):
|
| 753 |
+
if not keyword: return "⚠️ Please enter a keyword!" if ui_lang == "en" else "⚠️ 키워드를 입력하세요!"
|
| 754 |
+
if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
|
| 755 |
+
lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
|
| 756 |
+
return call_llm(f'Trend analyst. For "{keyword}": 1) Current status 2) Peak season 3) 6-month forecast 4) Risk factors 5) Opportunity windows 6) Emerging topics. {lang}', 1500) or "Error"
|
| 757 |
+
|
| 758 |
+
def analyze_content_ideas(keyword, ui_lang):
|
| 759 |
+
if not keyword: return "⚠️ Please enter a topic!" if ui_lang == "en" else "⚠️ 주제를 입력하세요!"
|
| 760 |
+
if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
|
| 761 |
+
lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
|
| 762 |
+
return call_llm(f'YouTube strategist. For "{keyword}", generate 10 video ideas with: Title, Hook, Format, Length, Thumbnail concept, Viral score 1-10. {lang}', 2000) or "Error"
|
| 763 |
+
|
| 764 |
+
def analyze_channel(channel_name, ui_lang):
|
| 765 |
+
if not channel_name: return "⚠️ Please enter channel name!" if ui_lang == "en" else "⚠️ 채널명을 입력하세요!"
|
| 766 |
+
if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
|
| 767 |
+
lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
|
| 768 |
+
return call_llm(f'YouTube consultant. For "{channel_name}": 1) Niche assessment 2) Content strategy 3) Growth tactics 4) Monetization 5) Competitive advantages. {lang}', 2000) or "Error"
|
| 769 |
+
|
| 770 |
+
def analyze_competitor(my_channel, competitor, ui_lang):
|
| 771 |
+
if not my_channel or not competitor: return "⚠️ Please enter both channels!" if ui_lang == "en" else "⚠️ 둘 다 입력하세요!"
|
| 772 |
+
if not groq_client: return "⚠️ LLM API not configured." if ui_lang == "en" else "⚠️ LLM API 미설정"
|
| 773 |
+
lang = "한국어로 답변해주세요." if ui_lang == "ko" else ""
|
| 774 |
+
return call_llm(f'Compare "{my_channel}" vs "{competitor}": 1) Positioning 2) Content gap 3) Benchmarks 4) Advantages 5) Action plan 6) 5 video ideas to beat them. {lang}', 2000) or "Error"
|
| 775 |
+
|
| 776 |
+
def show_search_history(ui_lang):
|
| 777 |
+
is_ko = ui_lang == "ko"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 778 |
conn = sqlite3.connect("youtube_data.db")
|
| 779 |
c = conn.cursor()
|
| 780 |
c.execute('SELECT keyword,country,language,sort_by,results_count,searched_at FROM search_history ORDER BY searched_at DESC LIMIT 50')
|
|
|
|
| 782 |
conn.close()
|
| 783 |
|
| 784 |
if not history:
|
| 785 |
+
return f"<p style='font-family:Comic Neue,cursive'>{'검색 기록 없음' if is_ko else 'No search history yet.'}</p>"
|
| 786 |
|
| 787 |
+
html = f'''<div style="background:#1F2937;color:#FACC15;padding:15px;border:3px solid #1F2937;border-radius:8px;margin-bottom:15px">
|
| 788 |
+
<h3 style="font-family:Bangers,cursive;margin:0">🕐 {"검색 기록" if is_ko else "SEARCH HISTORY"}</h3></div>
|
| 789 |
<table style="width:100%;border-collapse:collapse;font-family:Comic Neue,cursive">
|
| 790 |
<thead><tr style="background:#FACC15;color:#1F2937">
|
| 791 |
+
<th style="padding:12px;border:2px solid #1F2937">{"검색어" if is_ko else "Keyword"}</th>
|
| 792 |
+
<th style="padding:12px">{"국가" if is_ko else "Country"}</th>
|
| 793 |
+
<th style="padding:12px">{"언어" if is_ko else "Language"}</th>
|
| 794 |
+
<th style="padding:12px">{"정렬" if is_ko else "Sort"}</th>
|
| 795 |
+
<th style="padding:12px">{"결과" if is_ko else "Results"}</th>
|
| 796 |
+
<th style="padding:12px">{"시간" if is_ko else "Time"}</th>
|
| 797 |
</tr></thead><tbody>'''
|
| 798 |
|
| 799 |
for kw, country, lang, sort_by, cnt, searched in history:
|
|
|
|
| 805 |
|
| 806 |
return html + '</tbody></table>'
|
| 807 |
|
| 808 |
+
# UI Language switch functions
|
| 809 |
+
def switch_language(lang):
|
| 810 |
+
L = UI_LANG.get(lang, UI_LANG["en"])
|
| 811 |
+
sort_opts = list(L["sort_options"].keys())
|
| 812 |
+
date_opts = list(L["date_options"].keys())
|
| 813 |
+
return (
|
| 814 |
+
gr.update(label=L["search_keyword"], placeholder=L["enter_keyword"]),
|
| 815 |
+
gr.update(value=L["search"]),
|
| 816 |
+
gr.update(value=L["refresh"]),
|
| 817 |
+
gr.update(label=L["country"]),
|
| 818 |
+
gr.update(label=L["language"]),
|
| 819 |
+
gr.update(choices=sort_opts, value=sort_opts[0], label=L["sort_by"]),
|
| 820 |
+
gr.update(choices=date_opts, value=date_opts[0], label=L["period"]),
|
| 821 |
+
gr.update(label=L["max_results"]),
|
| 822 |
+
gr.update(label=L["click_autofill"]),
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
# Initial trending
|
| 826 |
initial_trending = get_real_trending_keywords("US", "en")
|
| 827 |
|
|
|
|
| 829 |
# 🎨 Gradio UI
|
| 830 |
# ============================================
|
| 831 |
with gr.Blocks(css=css, title="YouTube Trend Analyzer") as demo:
|
| 832 |
+
# Hidden state for UI language
|
| 833 |
+
ui_lang_state = gr.State("en")
|
| 834 |
+
|
| 835 |
gr.HTML('''<div style="text-align:center;margin:20px 0">
|
| 836 |
<a href="https://www.humangen.ai" target="_blank">
|
| 837 |
<img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge">
|
| 838 |
</a></div>''')
|
| 839 |
|
| 840 |
+
# Language selector at top
|
| 841 |
+
with gr.Row():
|
| 842 |
+
gr.Markdown("# 🎬 YOUTUBE TREND ANALYZER 📊", elem_classes="header-text")
|
| 843 |
+
|
| 844 |
+
with gr.Row():
|
| 845 |
+
with gr.Column(scale=4):
|
| 846 |
+
db_stats = gr.Markdown("📊 Loading DB stats...")
|
| 847 |
+
with gr.Column(scale=1):
|
| 848 |
+
ui_lang_dropdown = gr.Dropdown(
|
| 849 |
+
choices=["English", "한국어"],
|
| 850 |
+
value="English",
|
| 851 |
+
label="🌐 UI Language",
|
| 852 |
+
interactive=True,
|
| 853 |
+
elem_classes="lang-selector"
|
| 854 |
+
)
|
| 855 |
|
| 856 |
with gr.Tabs():
|
| 857 |
# Tab 1: Search
|
|
|
|
| 865 |
with gr.Row():
|
| 866 |
country = gr.Dropdown(list(COUNTRIES.keys()), value="United States", label="Country")
|
| 867 |
language = gr.Dropdown(list(LANGUAGES.keys()), value="Auto (by Country)", label="Language")
|
| 868 |
+
sort_by = gr.Dropdown(list(UI_LANG["en"]["sort_options"].keys()), value="Most Viewed", label="Sort By")
|
| 869 |
+
date_filter = gr.Dropdown(list(UI_LANG["en"]["date_options"].keys()), value="All Time", label="Period")
|
| 870 |
max_results = gr.Slider(10, 1000, value=100, step=10, label="Max Results")
|
| 871 |
output = gr.HTML()
|
| 872 |
|
|
|
|
| 891 |
# Tab 5: AI Tools
|
| 892 |
with gr.Tab("🤖 AI Tools"):
|
| 893 |
gr.Markdown("### 🧠 LLM-Powered Analysis Tools (GPT-OSS-120B)")
|
|
|
|
| 894 |
with gr.Tabs():
|
|
|
|
| 895 |
with gr.Tab("🏷️ Keyword Suggest"):
|
| 896 |
gr.Markdown("**Generate related keywords with SEO insights**")
|
| 897 |
kw_input = gr.Textbox(label="Enter base keyword", placeholder="e.g., Python tutorial")
|
| 898 |
kw_btn = gr.Button("🔍 Generate Keywords", variant="primary")
|
| 899 |
kw_output = gr.Textbox(label="Suggested Keywords", lines=20, elem_classes="llm-result")
|
| 900 |
|
|
|
|
| 901 |
with gr.Tab("🔮 Trend Prediction"):
|
| 902 |
gr.Markdown("**Predict future trend trajectory**")
|
| 903 |
tp_input = gr.Textbox(label="Enter topic/keyword", placeholder="e.g., AI tools")
|
| 904 |
tp_btn = gr.Button("🔮 Predict Trend", variant="primary")
|
| 905 |
tp_output = gr.Textbox(label="Trend Analysis", lines=20, elem_classes="llm-result")
|
| 906 |
|
|
|
|
| 907 |
with gr.Tab("💡 Content Ideas"):
|
| 908 |
gr.Markdown("**Generate creative video ideas**")
|
| 909 |
ci_input = gr.Textbox(label="Enter topic", placeholder="e.g., Home workout")
|
| 910 |
ci_btn = gr.Button("💡 Generate Ideas", variant="primary")
|
| 911 |
ci_output = gr.Textbox(label="Content Ideas", lines=25, elem_classes="llm-result")
|
| 912 |
|
|
|
|
| 913 |
with gr.Tab("📊 Channel Analysis"):
|
| 914 |
gr.Markdown("**Get growth strategy for your channel**")
|
| 915 |
ca_input = gr.Textbox(label="Enter channel name or niche", placeholder="e.g., Tech reviews")
|
| 916 |
ca_btn = gr.Button("📊 Analyze Channel", variant="primary")
|
| 917 |
ca_output = gr.Textbox(label="Channel Analysis", lines=25, elem_classes="llm-result")
|
| 918 |
|
|
|
|
| 919 |
with gr.Tab("⚔️ Competitor Analysis"):
|
| 920 |
gr.Markdown("**Compare against competitors**")
|
| 921 |
with gr.Row():
|
|
|
|
| 929 |
history_btn = gr.Button("🔄 Refresh", variant="primary")
|
| 930 |
history_out = gr.HTML()
|
| 931 |
|
| 932 |
+
# Language switch handler
|
| 933 |
+
def on_lang_change(lang_choice):
|
| 934 |
+
lang_code = "ko" if lang_choice == "한국어" else "en"
|
| 935 |
+
return lang_code
|
| 936 |
+
|
| 937 |
+
ui_lang_dropdown.change(on_lang_change, ui_lang_dropdown, ui_lang_state)
|
| 938 |
+
ui_lang_dropdown.change(
|
| 939 |
+
switch_language,
|
| 940 |
+
gr.State("ko" if ui_lang_dropdown.value == "한국어" else "en"),
|
| 941 |
+
[keyword, btn, refresh, country, language, sort_by, date_filter, max_results, trending]
|
| 942 |
+
)
|
| 943 |
+
|
| 944 |
# Events
|
| 945 |
trending.change(use_trending_keyword, trending, keyword)
|
| 946 |
country.change(update_trending, country, trending)
|
| 947 |
+
btn.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results, ui_lang_state], [output, db_stats])
|
| 948 |
+
keyword.submit(search_videos, [keyword, country, language, sort_by, date_filter, max_results, ui_lang_state], [output, db_stats])
|
| 949 |
+
refresh.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results, ui_lang_state], [output, db_stats])
|
| 950 |
|
| 951 |
+
pick_btn.click(show_ai_picks, ui_lang_state, pick_out)
|
| 952 |
+
alerts_btn.click(show_trending_alerts, ui_lang_state, alerts_out)
|
| 953 |
+
growing_btn.click(show_top_growing, ui_lang_state, growing_out)
|
| 954 |
+
history_btn.click(show_search_history, ui_lang_state, history_out)
|
| 955 |
|
| 956 |
# AI Tools events
|
| 957 |
+
kw_btn.click(analyze_keyword_suggest, [kw_input, ui_lang_state], kw_output)
|
| 958 |
+
tp_btn.click(analyze_trend_prediction, [tp_input, ui_lang_state], tp_output)
|
| 959 |
+
ci_btn.click(analyze_content_ideas, [ci_input, ui_lang_state], ci_output)
|
| 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 |
demo.launch()
|
| 964 |
|