"""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('',body+'\n') if '' in html else html+body)