"""Final4 runtime: fix topic button visibility, shorts home feed, AI asking for videos/articles."""
import re, time, json, os, requests
from urllib.parse import urlparse
import ai_runtime_final3 as f3
from ai_runtime_final3 import app, base, rt, HTMLResponse, JSONResponse, Request, Query
try:
import main as main_mod
except Exception:
main_mod=None
AI_INTERACTIONS_FILE=f3.AI_INTERACTIONS_FILE
_SHORTS_CACHE={"t":0,"d":[]}
SHORT_CHANNELS=f3.SHORT_CHANNELS
def clean(s):
import html as html_lib
return re.sub(r"\s+"," ",html_lib.unescape(s or "")).strip()
def _domain(u):
try:return urlparse(u or '').netloc.replace('www.','')
except Exception:return ''
def _load_json(path,default):
try:
if os.path.exists(path):
with open(path,'r',encoding='utf-8') as f:return json.load(f)
except Exception:pass
return default
def _save_json(path,data):
try:
os.makedirs(os.path.dirname(path),exist_ok=True);tmp=path+'.tmp'
with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
os.replace(tmp,path)
except Exception:pass
def _fallback_shorts():
out=[];seen=set()
candidates=[]
try:candidates+=(getattr(main_mod,'SHORTS_FALLBACK',[]) or [])
except Exception:pass
try:candidates+=(getattr(rt,'SHORTS_FALLBACK',[]) or [])
except Exception:pass
# hard fallback if imports fail
hard=[('Lu_iCQ5YwNM','Công an lập hồ sơ xử lý người phụ nữ chửi bới, tát tài xế ô tô | Dân trí','baodantri7941'),('CwWvijF8BOA','Chú rể bật khóc nhận món quà bí mật người cha quá cố gửi 26 năm trước | Dân trí','baodantri7941'),('7Pd6vZ2Lz1M','Hành động ấm lòng trong tìm kiếm học sinh tử vong ở sông Lô | SKĐS','baosuckhoedoisongboyte'),('SlHLt_ZyPiE','Xử phạt người đàn ông xóa số điện thoại cứu hộ trên cao tốc Bắc - Nam | SKĐS','baosuckhoedoisongboyte')]
for vid,title,ch in hard:
candidates.append({'id':vid,'title':title,'channel':ch,'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
for v in candidates:
vid=v.get('id') or ''
if vid and vid not in seen:
seen.add(vid)
if not v.get('link'):v['link']='https://www.youtube.com/watch?v='+vid
if not v.get('img'):v['img']='https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg'
v['source']='yt';out.append(v)
return out
def _fresh_shorts():
items=[];seen=set()
for ch in SHORT_CHANNELS:
got=f3._youtube_shorts_ytdlp(ch,24) or f3._youtube_shorts_html(ch,24)
for v in got:
vid=v.get('id')
if vid and vid not in seen:
seen.add(vid);items.append(v)
for v in _fallback_shorts():
vid=v.get('id')
if vid and vid not in seen:
seen.add(vid);items.append(v)
return items[:60]
# Remove endpoints/root to override.
_PATCH={('/api/shorts','GET'),('/api/ai/interact','POST'),('/api/article/ask','POST'),('/','GET')}
app.router.routes=[r for r in app.router.routes if not any(getattr(r,'path',None)==p and m in getattr(r,'methods',set()) for p,m in _PATCH)]
@app.get('/api/shorts')
def api_shorts_final4(refresh:int=Query(default=0)):
now=time.time()
if not refresh and _SHORTS_CACHE['d'] and now-_SHORTS_CACHE['t']<900:return JSONResponse(_SHORTS_CACHE['d'])
data=_fresh_shorts()
_SHORTS_CACHE.update({'t':now,'d':data})
return JSONResponse(data)
@app.post('/api/ai/interact')
async def ai_interact_final4(request:Request):
body=await request.json();pid=str(body.get('id','')).strip();kind=str(body.get('kind','wall')).strip();action=str(body.get('action','')).strip();text=clean(body.get('text',''));context=clean(body.get('context',''));title=clean(body.get('title',''))
if not pid:return JSONResponse({'error':'missing id'},status_code=400)
db=_load_json(AI_INTERACTIONS_FILE,{})
key=kind+':'+pid
st=db.get(key) or {'views':0,'likes':0,'comments':[],'asks':[]}
if action=='view':st['views']=int(st.get('views',0))+1
elif action=='like':st['likes']=int(st.get('likes',0))+1
elif action=='comment' and text:
st.setdefault('comments',[]).insert(0,{'text':text[:240],'ts':int(time.time())});st['comments']=st['comments'][:80]
elif action=='ask' and text:
if kind in ('ai','short','wall'):
posts=base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==pid),{})
title=title or p.get('title','');context=context or (p.get('text') or '')
# For YouTube shorts, frontend sends title/context because AI cannot watch video.
if not context:context=title or pid
prompt=f"""Bạn là trợ lý VNEWS. Trả lời chi tiết bằng tiếng Việt dựa trên thông tin có sẵn về video/bài viết.
Tiêu đề/ngữ cảnh: {title}
Nội dung mô tả: {context[:5000]}
Câu hỏi người dùng: {text}
Yêu cầu:
- Nếu là video YouTube/Shorts và chỉ có tiêu đề, hãy nói rõ rằng bạn suy luận từ tiêu đề/mô tả, không khẳng định đã xem video.
- Trả lời cụ thể, có giải thích, không quá ngắn.
"""
ans=await base.qwen_generate(prompt,max_tokens=900)
if not ans:ans='AI chưa trả lời được lúc này. Bạn thử hỏi lại cụ thể hơn.'
st.setdefault('asks',[]).insert(0,{'q':text[:240],'a':ans[:1500],'ts':int(time.time())});st['asks']=st['asks'][:50]
db[key]=st;_save_json(AI_INTERACTIONS_FILE,db)
return JSONResponse({'stats':st})
@app.post('/api/article/ask')
async def article_ask(request:Request):
body=await request.json();url=clean(body.get('url',''));question=clean(body.get('question',''))
if not question:return JSONResponse({'error':'missing question'},status_code=400)
title='';raw=''
try:
data=None
if url and hasattr(f3.f2.f1,'_scrape_url_article_only'):
data=f3.f2.f1._scrape_url_article_only(url)
if not data and url:data=base.scrape_any_url(url)
if data:
title=data.get('title','');raw=(data.get('summary','')+'\n'+data.get('text','')).strip()
except Exception:pass
context=raw[:12000] if raw else clean(body.get('context',''))[:12000]
prompt=f"""Bạn là trợ lý đọc hiểu bài viết của VNEWS. Hãy trả lời chi tiết câu hỏi của người dùng dựa trên bài viết.
Tiêu đề bài: {title}
Nội dung bài:
{context}
Câu hỏi: {question}
Yêu cầu:
- Trả lời bằng tiếng Việt.
- Dựa sát nội dung bài, nếu bài không có thông tin thì nói rõ.
- Giải thích chi tiết, có gạch đầu dòng khi hữu ích.
"""
ans=await base.qwen_generate(prompt,max_tokens=1200)
if not ans:ans='AI chưa trả lời được lúc này. Bạn thử hỏi lại hoặc rút gọn câu hỏi.'
return JSONResponse({'answer':ans,'title':title})
FINAL4_INJECT = r'''
'''
@app.get('/')
async def index_final4():
html=f3.f2.f1._load_index_html();body=getattr(rt.old,'PATCH_INJECT','')+f3.f2.f1.FINAL_INJECT+f3.FINAL3_INJECT+FINAL4_INJECT
return HTMLResponse(html.replace('