Spaces:
Running
Running
Patch VN hot hashtags and reliable RSS topic sources
Browse files- ai_runtime_final6.py +148 -0
ai_runtime_final6.py
CHANGED
|
@@ -357,3 +357,151 @@ async def index_final6():
|
|
| 357 |
html=f5.f4.f3.f2.f1._load_index_html()
|
| 358 |
body=getattr(rt.old,'PATCH_INJECT','')+f5.f4.f3.f2.f1.FINAL_INJECT+f5.f4.f3.FINAL3_INJECT+f5.f4.FINAL4_INJECT+f5.FINAL5_INJECT+FINAL6_INJECT
|
| 359 |
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
html=f5.f4.f3.f2.f1._load_index_html()
|
| 358 |
body=getattr(rt.old,'PATCH_INJECT','')+f5.f4.f3.f2.f1.FINAL_INJECT+f5.f4.f3.FINAL3_INJECT+f5.f4.FINAL4_INJECT+f5.FINAL5_INJECT+FINAL6_INJECT
|
| 359 |
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
# ===== FINAL6B: Vietnam hot hashtags + reliable VN RSS/source retrieval =====
|
| 363 |
+
VN_RSS_FEEDS = [
|
| 364 |
+
('VnExpress Thời sự','https://vnexpress.net/rss/thoi-su.rss'),
|
| 365 |
+
('VnExpress Thế giới','https://vnexpress.net/rss/the-gioi.rss'),
|
| 366 |
+
('VnExpress Kinh doanh','https://vnexpress.net/rss/kinh-doanh.rss'),
|
| 367 |
+
('VnExpress Công nghệ','https://vnexpress.net/rss/so-hoa.rss'),
|
| 368 |
+
('VnExpress Thể thao','https://vnexpress.net/rss/the-thao.rss'),
|
| 369 |
+
('VnExpress Giải trí','https://vnexpress.net/rss/giai-tri.rss'),
|
| 370 |
+
('VnExpress Sức khỏe','https://vnexpress.net/rss/suc-khoe.rss'),
|
| 371 |
+
('VnExpress Giáo dục','https://vnexpress.net/rss/giao-duc.rss'),
|
| 372 |
+
('Dân trí Xã hội','https://dantri.com.vn/rss/xa-hoi.rss'),
|
| 373 |
+
('Dân trí Thế giới','https://dantri.com.vn/rss/the-gioi.rss'),
|
| 374 |
+
('Dân trí Kinh doanh','https://dantri.com.vn/rss/kinh-doanh.rss'),
|
| 375 |
+
('Dân trí Sức khỏe','https://dantri.com.vn/rss/suc-khoe.rss'),
|
| 376 |
+
('Dân trí Thể thao','https://dantri.com.vn/rss/the-thao.rss'),
|
| 377 |
+
('Dân trí Công nghệ','https://dantri.com.vn/rss/suc-manh-so.rss'),
|
| 378 |
+
('Vietnamnet Thời sự','https://vietnamnet.vn/thoi-su.rss'),
|
| 379 |
+
('Vietnamnet Kinh doanh','https://vietnamnet.vn/kinh-doanh.rss'),
|
| 380 |
+
('Vietnamnet Công nghệ','https://vietnamnet.vn/cong-nghe.rss'),
|
| 381 |
+
('Vietnamnet Thể thao','https://vietnamnet.vn/the-thao.rss'),
|
| 382 |
+
]
|
| 383 |
+
|
| 384 |
+
def _fetch_rss_items(feed_name, feed_url, max_items=15):
|
| 385 |
+
items=[]
|
| 386 |
+
try:
|
| 387 |
+
r=requests.get(feed_url,headers=UA,timeout=10);r.encoding='utf-8'
|
| 388 |
+
soup=BeautifulSoup(r.text,'xml')
|
| 389 |
+
for it in soup.find_all('item')[:max_items]:
|
| 390 |
+
title=clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
|
| 391 |
+
link=clean(it.find('link').get_text(strip=True) if it.find('link') else '')
|
| 392 |
+
desc=it.find('description').get_text(' ',strip=True) if it.find('description') else ''
|
| 393 |
+
desc_txt=clean(BeautifulSoup(desc,'lxml').get_text(' ',strip=True))
|
| 394 |
+
if title and link:
|
| 395 |
+
items.append({'title':title,'url':link,'source':feed_name,'snippet':desc_txt})
|
| 396 |
+
except Exception:pass
|
| 397 |
+
return items
|
| 398 |
+
|
| 399 |
+
def _vn_rss_pool():
|
| 400 |
+
now=time.time();key='vn_rss_pool'
|
| 401 |
+
if key in _TOPIC_CACHE and now-_TOPIC_CACHE[key]['t']<600:return _TOPIC_CACHE[key]['d']
|
| 402 |
+
pool=[];seen=set()
|
| 403 |
+
for name,url in VN_RSS_FEEDS:
|
| 404 |
+
for it in _fetch_rss_items(name,url,12):
|
| 405 |
+
if it['url'] not in seen:
|
| 406 |
+
seen.add(it['url']);pool.append(it)
|
| 407 |
+
_TOPIC_CACHE[key]={'t':now,'d':pool}
|
| 408 |
+
return pool
|
| 409 |
+
|
| 410 |
+
def _topic_tokens(topic):
|
| 411 |
+
toks=[w.lower() for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',topic or '') if len(w)>1]
|
| 412 |
+
return [t for t in toks if t not in STOP_WORDS]
|
| 413 |
+
|
| 414 |
+
def _score_topic_item(topic,item):
|
| 415 |
+
toks=_topic_tokens(topic)
|
| 416 |
+
hay=(item.get('title','')+' '+item.get('snippet','')+' '+item.get('source','')).lower()
|
| 417 |
+
if not toks:return 0
|
| 418 |
+
score=0
|
| 419 |
+
for t in toks:
|
| 420 |
+
if t in hay:score+=2 if len(t)>3 else 1
|
| 421 |
+
phrase=topic.lower().strip()
|
| 422 |
+
if phrase and phrase in hay:score+=8
|
| 423 |
+
return score
|
| 424 |
+
|
| 425 |
+
# Override: hashtags must be Việt Nam-focused, using VN news RSS directly.
|
| 426 |
+
def _hot_topics():
|
| 427 |
+
now=time.time()
|
| 428 |
+
if _HOT_CACHE['d'] and now-_HOT_CACHE['t']<600:return _HOT_CACHE['d']
|
| 429 |
+
pool=_vn_rss_pool()
|
| 430 |
+
freq={};display={}
|
| 431 |
+
for it in pool[:180]:
|
| 432 |
+
title=re.sub(r'\s+-\s+.*$','',it.get('title',''))
|
| 433 |
+
# Extract compact Vietnamese hot phrases from current VN headlines.
|
| 434 |
+
kws=[]
|
| 435 |
+
# quoted/name phrases first
|
| 436 |
+
for m in re.findall(r'([A-ZĐÀ-Ỹ][A-Za-zÀ-ỹ0-9]+(?:\s+[A-ZĐÀ-ỸA-Za-zÀ-ỹ0-9][A-Za-zÀ-ỹ0-9]+){1,4})',title):
|
| 437 |
+
if len(m)>=6:kws.append(m)
|
| 438 |
+
kws += _keywords_from_title(title)
|
| 439 |
+
for kw in kws[:5]:
|
| 440 |
+
kw=clean(kw)
|
| 441 |
+
words=[w for w in kw.split() if w.lower() not in STOP_WORDS]
|
| 442 |
+
if len(words)<2:continue
|
| 443 |
+
kw=' '.join(words[:5])
|
| 444 |
+
if len(kw)<6 or len(kw)>55:continue
|
| 445 |
+
key=kw.lower()
|
| 446 |
+
freq[key]=freq.get(key,0)+1
|
| 447 |
+
display[key]=kw
|
| 448 |
+
ranked=sorted(freq.items(),key=lambda x:x[1],reverse=True)
|
| 449 |
+
topics=[];seen=set()
|
| 450 |
+
for key,_ in ranked:
|
| 451 |
+
kw=display[key]
|
| 452 |
+
if key in seen:continue
|
| 453 |
+
seen.add(key)
|
| 454 |
+
label='#'+re.sub(r'\s+','',kw.title())
|
| 455 |
+
topics.append({'label':label,'topic':kw})
|
| 456 |
+
if len(topics)>=24:break
|
| 457 |
+
# VN fallback, not generic global.
|
| 458 |
+
for kw in ['Giá vàng trong nước','Bão và mưa lũ','Bóng đá Việt Nam','Kinh tế Việt Nam','AI tại Việt Nam','Giá xăng dầu','Thị trường chứng khoán Việt Nam','Tuyển Việt Nam','Sức khỏe cộng đồng','An ninh mạng Việt Nam']:
|
| 459 |
+
if kw.lower() not in seen:topics.append({'label':'#'+re.sub(r'\s+','',kw.title()),'topic':kw})
|
| 460 |
+
_HOT_CACHE.update({'t':now,'d':topics[:24]})
|
| 461 |
+
return _HOT_CACHE['d']
|
| 462 |
+
|
| 463 |
+
def _candidate_urls(topic):
|
| 464 |
+
seen=set();items=[]
|
| 465 |
+
# 1) VN RSS pool relevance is most reliable and has direct URLs.
|
| 466 |
+
scored=[]
|
| 467 |
+
for it in _vn_rss_pool():
|
| 468 |
+
sc=_score_topic_item(topic,it)
|
| 469 |
+
if sc>0:scored.append((sc,it))
|
| 470 |
+
for sc,it in sorted(scored,key=lambda x:x[0],reverse=True)[:12]:
|
| 471 |
+
if it['url'] not in seen:
|
| 472 |
+
seen.add(it['url']);items.append(it)
|
| 473 |
+
# 2) Search trusted web if RSS not enough.
|
| 474 |
+
queries=[topic+' Việt Nam tin tức',topic+' phân tích Việt Nam',topic+' mới nhất']
|
| 475 |
+
for q in queries:
|
| 476 |
+
for it in _ddg_search(q,8):
|
| 477 |
+
if it['url'] not in seen:
|
| 478 |
+
seen.add(it['url']);items.append(it)
|
| 479 |
+
if len(items)>=14:break
|
| 480 |
+
# 3) Google News as supplemental titles/direct links.
|
| 481 |
+
for it in _google_news_items(topic,10):
|
| 482 |
+
if it['url'] not in seen:
|
| 483 |
+
seen.add(it['url']);items.append(it)
|
| 484 |
+
return items[:24]
|
| 485 |
+
|
| 486 |
+
def _web_research_context(topic):
|
| 487 |
+
now=time.time();key='ctx2:'+topic.lower().strip()
|
| 488 |
+
if key in _TOPIC_CACHE and now-_TOPIC_CACHE[key]['t']<900:return _TOPIC_CACHE[key]['d']
|
| 489 |
+
items=_candidate_urls(topic)
|
| 490 |
+
crawled=[]
|
| 491 |
+
for it in items:
|
| 492 |
+
text=_scrape_article_text(it['url'],9000)
|
| 493 |
+
rel=_score_relevance(topic,it.get('title',''),text,it.get('snippet','')) or _score_topic_item(topic,it)
|
| 494 |
+
# If RSS item has good snippet, keep it even when full text blocks.
|
| 495 |
+
if text and len(text)>300 and rel>0:
|
| 496 |
+
crawled.append({**it,'text':text,'rel':rel})
|
| 497 |
+
elif it.get('snippet') and len(it['snippet'])>120 and rel>0:
|
| 498 |
+
crawled.append({**it,'text':it['snippet'],'rel':rel,'snippet_only':True})
|
| 499 |
+
crawled=sorted(crawled,key=lambda x:(x.get('rel',0),len(x.get('text',''))),reverse=True)[:7]
|
| 500 |
+
blocks=[];sources=[]
|
| 501 |
+
for it in crawled:
|
| 502 |
+
label='ĐOẠN MÔ TẢ TỪ RSS/TÌM KIẾM' if it.get('snippet_only') else 'NỘI DUNG BÀI VIẾT ĐÃ CRAWL'
|
| 503 |
+
blocks.append(f"NGUỒN: {it['source']}\nTIÊU ĐỀ: {it['title']}\n{label}:\n{it['text'][:8500]}")
|
| 504 |
+
sources.append({'title':it['title'],'url':it['url'],'via':it['source']})
|
| 505 |
+
data={'context':'\n\n---\n\n'.join(blocks),'sources':sources[:8],'count':len(blocks)}
|
| 506 |
+
_TOPIC_CACHE[key]={'t':now,'d':data}
|
| 507 |
+
return data
|