"""Final3 runtime: Qwen topic posts, robust YouTube shorts, TikTok-style actions for Shorts and Short AI.""" import os, re, time, json, hashlib, requests from urllib.parse import quote, urlparse import ai_runtime_final2 as f2 from ai_runtime_final2 import app, base, rt, HTMLResponse, JSONResponse, Request, Query SPACE_URL="https://bep40-vnews.hf.space" SHORT_CHANNELS=["baodantri7941","baosuckhoedoisongboyte"] _SHORTS_CACHE={"t":0,"d":[]} AI_INTERACTIONS_FILE="/data/ai_interactions.json" if os.path.isdir('/data') else "/app/data/ai_interactions.json" 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 _youtube_shorts_ytdlp(handle,count=20): try: import yt_dlp url=f"https://www.youtube.com/@{handle}/shorts" opts={'quiet':True,'extract_flat':True,'skip_download':True,'playlistend':count,'ignoreerrors':True,'no_warnings':True} with yt_dlp.YoutubeDL(opts) as ydl: info=ydl.extract_info(url,download=False) out=[] for e in (info or {}).get('entries') or []: vid=e.get('id') or '' if not re.match(r'^[A-Za-z0-9_-]{11}$',vid):continue title=e.get('title') or 'YouTube Short' out.append({'title':title,'link':f'https://www.youtube.com/watch?v={vid}','img':f'https://i.ytimg.com/vi/{vid}/hqdefault.jpg','source':'yt','id':vid,'channel':handle}) return out except Exception:return [] def _youtube_shorts_html(handle,count=20): try: html=requests.get(f"https://www.youtube.com/@{handle}/shorts",headers=getattr(base,'HEADERS',{}),timeout=15).text ids=[];out=[] for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"',html): vid=m.group(1) if vid in ids:continue ids.append(vid) snip=html[max(0,m.start()-1000):m.start()+1800] title='YouTube Short' mt=re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"',snip) or re.search(r'"accessibilityText":"([^"]+)"',snip) if mt:title=clean(mt.group(1).replace('\\n',' ')) out.append({'title':title,'link':f'https://www.youtube.com/watch?v={vid}','img':f'https://i.ytimg.com/vi/{vid}/hqdefault.jpg','source':'yt','id':vid,'channel':handle}) if len(out)>=count:break return out except Exception:return [] def _fresh_shorts(): items=[];seen=set() for ch in SHORT_CHANNELS: got=_youtube_shorts_ytdlp(ch,24) or _youtube_shorts_html(ch,24) for v in got: if v['id'] not in seen: seen.add(v['id']);items.append(v) # fallback from main if live scrape fails try: for v in getattr(rt.old.base if hasattr(rt.old,'base') else rt,'SHORTS_FALLBACK',[]) or []: vid=v.get('id') if vid and vid not in seen: seen.add(vid);items.append(v) except Exception:pass return items[:50] def _topic_image(topic): try:return base.pollinations_image_url(topic) except Exception:return "https://image.pollinations.ai/prompt/"+quote("Vietnamese news editorial illustration "+topic)+"?width=1024&height=576&nologo=true" # Remove old endpoints/root to override. _PATCH={('/api/shorts','GET'),('/api/topic_post','POST'),('/api/ai/interact','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_final3(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/topic_post') async def topic_post_qwen(request:Request): body=await request.json();topic=clean(body.get('topic','')) if not topic:return JSONResponse({'error':'missing topic'},status_code=400) img=_topic_image(topic) prompt=f"""Bạn là biên tập viên VNEWS. Dựa trên kiến thức tổng quát của bạn, hãy tạo một bài đăng Tường AI bằng tiếng Việt về chủ đề: {topic} Yêu cầu: - Viết như một bài tin/tạp chí ngắn, có tiêu đề hấp dẫn. - 1 đoạn mở đầu 2 câu. - 4-6 ý chính rõ ràng, không lan man. - Nếu chủ đề là thể thao/công nghệ/xã hội, hãy viết có bối cảnh và nhận định. - Không khẳng định số liệu thời sự mới nếu không chắc; dùng cách diễn đạt thận trọng. - Cuối bài thêm dòng: Nguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp. """ text=await base.qwen_generate(prompt,image_url=img,max_tokens=1100) if not text: text=f"{topic}\n\nĐây là bài gợi ý do AI tạo dựa trên kiến thức tổng hợp. Nội dung cung cấp bối cảnh, các điểm đáng chú ý và góc nhìn tham khảo về chủ đề này.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp." post=base.make_post(topic,text,img,'','topic_qwen',sources=[{'title':'Qwen2.5-VL / kiến thức tổng hợp','url':'','via':'Qwen2.5-VL'}]) post['images']=[img] posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts) return JSONResponse({'post':post}) @app.post('/api/ai/interact') async def ai_interact(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','')) 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: posts=base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==pid),{}) prompt=f"""Trả lời ngắn bằng tiếng Việt cho câu hỏi của người xem về nội dung này. Tiêu đề: {p.get('title','')} Nội dung: {(p.get('text') or '')[:4000]} Câu hỏi: {text} """ ans=await base.qwen_generate(prompt,max_tokens=500) if not ans:ans='AI chưa trả lời được lúc này. Bạn thử hỏi lại ngắn gọn hơn.' st.setdefault('asks',[]).insert(0,{'q':text[:240],'a':ans[:1000],'ts':int(time.time())});st['asks']=st['asks'][:50] db[key]=st;_save_json(AI_INTERACTIONS_FILE,db) return JSONResponse({'stats':st}) FINAL3_INJECT = r'''
''' @app.get('/') async def index_final3(): html=f2.f1._load_index_html();body=getattr(rt.old,'PATCH_INJECT','') + f2.f1.FINAL_INJECT + FINAL3_INJECT return HTMLResponse(html.replace('',body+'\n') if '' in html else html+body)