Spaces:
Running
Running
Upload ai_runtime_final4.py
Browse files- ai_runtime_final4.py +172 -0
ai_runtime_final4.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Final4 runtime: fix topic button visibility, shorts home feed, AI asking for videos/articles."""
|
| 2 |
+
import re, time, json, os, requests
|
| 3 |
+
from urllib.parse import urlparse
|
| 4 |
+
import ai_runtime_final3 as f3
|
| 5 |
+
from ai_runtime_final3 import app, base, rt, HTMLResponse, JSONResponse, Request, Query
|
| 6 |
+
try:
|
| 7 |
+
import main as main_mod
|
| 8 |
+
except Exception:
|
| 9 |
+
main_mod=None
|
| 10 |
+
|
| 11 |
+
AI_INTERACTIONS_FILE=f3.AI_INTERACTIONS_FILE
|
| 12 |
+
_SHORTS_CACHE={"t":0,"d":[]}
|
| 13 |
+
SHORT_CHANNELS=f3.SHORT_CHANNELS
|
| 14 |
+
|
| 15 |
+
def clean(s):
|
| 16 |
+
import html as html_lib
|
| 17 |
+
return re.sub(r"\s+"," ",html_lib.unescape(s or "")).strip()
|
| 18 |
+
|
| 19 |
+
def _domain(u):
|
| 20 |
+
try:return urlparse(u or '').netloc.replace('www.','')
|
| 21 |
+
except Exception:return ''
|
| 22 |
+
|
| 23 |
+
def _load_json(path,default):
|
| 24 |
+
try:
|
| 25 |
+
if os.path.exists(path):
|
| 26 |
+
with open(path,'r',encoding='utf-8') as f:return json.load(f)
|
| 27 |
+
except Exception:pass
|
| 28 |
+
return default
|
| 29 |
+
|
| 30 |
+
def _save_json(path,data):
|
| 31 |
+
try:
|
| 32 |
+
os.makedirs(os.path.dirname(path),exist_ok=True);tmp=path+'.tmp'
|
| 33 |
+
with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
|
| 34 |
+
os.replace(tmp,path)
|
| 35 |
+
except Exception:pass
|
| 36 |
+
|
| 37 |
+
def _fallback_shorts():
|
| 38 |
+
out=[];seen=set()
|
| 39 |
+
candidates=[]
|
| 40 |
+
try:candidates+=(getattr(main_mod,'SHORTS_FALLBACK',[]) or [])
|
| 41 |
+
except Exception:pass
|
| 42 |
+
try:candidates+=(getattr(rt,'SHORTS_FALLBACK',[]) or [])
|
| 43 |
+
except Exception:pass
|
| 44 |
+
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')]
|
| 45 |
+
for vid,title,ch in hard:
|
| 46 |
+
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'})
|
| 47 |
+
for v in candidates:
|
| 48 |
+
vid=v.get('id') or ''
|
| 49 |
+
if vid and vid not in seen:
|
| 50 |
+
seen.add(vid)
|
| 51 |
+
if not v.get('link'):v['link']='https://www.youtube.com/watch?v='+vid
|
| 52 |
+
if not v.get('img'):v['img']='https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg'
|
| 53 |
+
v['source']='yt';out.append(v)
|
| 54 |
+
return out
|
| 55 |
+
|
| 56 |
+
def _fresh_shorts():
|
| 57 |
+
items=[];seen=set()
|
| 58 |
+
for ch in SHORT_CHANNELS:
|
| 59 |
+
got=f3._youtube_shorts_ytdlp(ch,24) or f3._youtube_shorts_html(ch,24)
|
| 60 |
+
for v in got:
|
| 61 |
+
vid=v.get('id')
|
| 62 |
+
if vid and vid not in seen:
|
| 63 |
+
seen.add(vid);items.append(v)
|
| 64 |
+
for v in _fallback_shorts():
|
| 65 |
+
vid=v.get('id')
|
| 66 |
+
if vid and vid not in seen:
|
| 67 |
+
seen.add(vid);items.append(v)
|
| 68 |
+
return items[:60]
|
| 69 |
+
|
| 70 |
+
_PATCH={('/api/shorts','GET'),('/api/ai/interact','POST'),('/api/article/ask','POST'),('/','GET')}
|
| 71 |
+
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)]
|
| 72 |
+
|
| 73 |
+
@app.get('/api/shorts')
|
| 74 |
+
def api_shorts_final4(refresh:int=Query(default=0)):
|
| 75 |
+
now=time.time()
|
| 76 |
+
if not refresh and _SHORTS_CACHE['d'] and now-_SHORTS_CACHE['t']<900:return JSONResponse(_SHORTS_CACHE['d'])
|
| 77 |
+
data=_fresh_shorts()
|
| 78 |
+
_SHORTS_CACHE.update({'t':now,'d':data})
|
| 79 |
+
return JSONResponse(data)
|
| 80 |
+
|
| 81 |
+
@app.post('/api/ai/interact')
|
| 82 |
+
async def ai_interact_final4(request:Request):
|
| 83 |
+
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',''))
|
| 84 |
+
if not pid:return JSONResponse({'error':'missing id'},status_code=400)
|
| 85 |
+
db=_load_json(AI_INTERACTIONS_FILE,{})
|
| 86 |
+
key=kind+':'+pid
|
| 87 |
+
st=db.get(key) or {'views':0,'likes':0,'comments':[],'asks':[]}
|
| 88 |
+
if action=='view':st['views']=int(st.get('views',0))+1
|
| 89 |
+
elif action=='like':st['likes']=int(st.get('likes',0))+1
|
| 90 |
+
elif action=='comment' and text:
|
| 91 |
+
st.setdefault('comments',[]).insert(0,{'text':text[:240],'ts':int(time.time())});st['comments']=st['comments'][:80]
|
| 92 |
+
elif action=='ask' and text:
|
| 93 |
+
if kind in ('ai','short','wall'):
|
| 94 |
+
posts=base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==pid),{})
|
| 95 |
+
title=title or p.get('title','');context=context or (p.get('text') or '')
|
| 96 |
+
if not context:context=title or pid
|
| 97 |
+
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.
|
| 98 |
+
|
| 99 |
+
Tiêu đề/ngữ cảnh: {title}
|
| 100 |
+
Nội dung mô tả: {context[:5000]}
|
| 101 |
+
|
| 102 |
+
Câu hỏi người dùng: {text}
|
| 103 |
+
|
| 104 |
+
Yêu cầu:
|
| 105 |
+
- 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.
|
| 106 |
+
- Trả lời cụ thể, có giải thích, không quá ngắn.
|
| 107 |
+
"""
|
| 108 |
+
ans=await base.qwen_generate(prompt,max_tokens=900)
|
| 109 |
+
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.'
|
| 110 |
+
st.setdefault('asks',[]).insert(0,{'q':text[:240],'a':ans[:1500],'ts':int(time.time())});st['asks']=st['asks'][:50]
|
| 111 |
+
db[key]=st;_save_json(AI_INTERACTIONS_FILE,db)
|
| 112 |
+
return JSONResponse({'stats':st})
|
| 113 |
+
|
| 114 |
+
@app.post('/api/article/ask')
|
| 115 |
+
async def article_ask(request:Request):
|
| 116 |
+
body=await request.json();url=clean(body.get('url',''));question=clean(body.get('question',''))
|
| 117 |
+
if not question:return JSONResponse({'error':'missing question'},status_code=400)
|
| 118 |
+
title='';raw=''
|
| 119 |
+
try:
|
| 120 |
+
data=None
|
| 121 |
+
if url and hasattr(f3.f2.f1,'_scrape_url_article_only'):
|
| 122 |
+
data=f3.f2.f1._scrape_url_article_only(url)
|
| 123 |
+
if not data and url:data=base.scrape_any_url(url)
|
| 124 |
+
if data:
|
| 125 |
+
title=data.get('title','');raw=(data.get('summary','')+'\n'+data.get('text','')).strip()
|
| 126 |
+
except Exception:pass
|
| 127 |
+
context=raw[:12000] if raw else clean(body.get('context',''))[:12000]
|
| 128 |
+
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.
|
| 129 |
+
|
| 130 |
+
Tiêu đề bài: {title}
|
| 131 |
+
Nội dung bài:
|
| 132 |
+
{context}
|
| 133 |
+
|
| 134 |
+
Câu hỏi: {question}
|
| 135 |
+
|
| 136 |
+
Yêu cầu:
|
| 137 |
+
- Trả lời bằng tiếng Việt.
|
| 138 |
+
- Dựa sát nội dung bài, nếu bài không có thông tin thì nói rõ.
|
| 139 |
+
- Giải thích chi tiết, có gạch đầu dòng khi hữu ích.
|
| 140 |
+
"""
|
| 141 |
+
ans=await base.qwen_generate(prompt,max_tokens=1200)
|
| 142 |
+
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.'
|
| 143 |
+
return JSONResponse({'answer':ans,'title':title})
|
| 144 |
+
|
| 145 |
+
FINAL4_INJECT = r'''
|
| 146 |
+
<style>
|
| 147 |
+
.topic-final4{display:flex!important;flex-direction:column!important;gap:8px!important;width:100%!important;margin-top:6px}.topic-final4 input,.topic-final4 button{display:block!important;width:100%!important;box-sizing:border-box!important}.topic-final4 button{background:#2d8659!important;color:#fff!important;border:0!important;border-radius:18px!important;padding:9px 12px!important;font-size:11px!important;font-weight:700!important}.article-ai-ask{margin-top:12px;background:#141414;border:1px solid #2a2a2a;border-radius:10px;padding:10px}.article-ai-ask textarea{width:100%;min-height:70px;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px}.article-ai-ask button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin-top:6px}.article-ai-answer{white-space:pre-wrap;color:#ccc;font-size:13px;line-height:1.55;margin-top:8px}.ai-compose-row:has(#ai-url-input){display:flex!important;flex-direction:column!important}.ai-compose-row:has(#ai-url-input) input,.ai-compose-row:has(#ai-url-input) button{width:100%!important}
|
| 148 |
+
</style>
|
| 149 |
+
<script>
|
| 150 |
+
(function(){
|
| 151 |
+
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 152 |
+
let shortsMap={};
|
| 153 |
+
function ensureTopicButtonFinal4(){let comp=document.querySelector('.ai-compose');if(!comp)return;if(!document.getElementById('ai-topic-input-final4')){let row=document.createElement('div');row.className='topic-final4';row.innerHTML='<input id="ai-topic-input-final4" placeholder="Nhập chủ đề để Qwen2.5VL tạo bài lên Tường AI..."><button id="ai-topic-btn-final4" onclick="createTopicPostFinal4()">✨ Tạo bài bằng Qwen</button>';comp.insertBefore(row,comp.firstChild.nextSibling);}let b=document.getElementById('ai-topic-btn-final4');if(b){b.style.display='block';b.textContent='✨ Tạo bài bằng Qwen';}}
|
| 154 |
+
window.createTopicPostFinal4=async function(){let inp=document.getElementById('ai-topic-input-final4');let topic=(inp&&inp.value||'').trim();if(!topic)return alert('Nhập chủ đề trước');let btn=document.getElementById('ai-topic-btn-final4');if(btn){btn.disabled=true;btn.textContent='Đang tạo...'}try{let r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');if(window.finalWall)window.finalWall.unshift(j.post);if(window.finalWall3)window.finalWall3.unshift(j.post);if(inp)inp.value='';if(window.renderWall)window.renderWall();alert('Đã tạo bài bằng Qwen và đăng lên Tường AI.');}catch(e){alert(e.message)}finally{if(btn){btn.disabled=false;btn.textContent='✨ Tạo bài bằng Qwen'}}};
|
| 155 |
+
async function ensureShortsHome(){let home=document.getElementById('view-home');if(!home||document.getElementById('shorts-final4'))return;let sh=await fetch('/api/shorts?refresh=1').then(r=>r.json()).catch(()=>[]);if(!sh.length)return;let wrap=document.createElement('div');wrap.id='shorts-final4';wrap.className='slider-wrap';let h='<div class="slider-header"><span class="slider-label">📱 Shorts Dân trí & SKĐS</span><span class="slider-note">Cập nhật YouTube</span></div><div class="slider-track">';sh.slice(0,30).forEach((a,i)=>{shortsMap[a.id]=a;h+=`<div class="slider-item shorts-item" onclick="openTikTok('shorts',${i})"><div class="slider-thumb shorts-thumb">${a.img?`<img src="${a.img}">`:''}<div class="card-play">▶</div></div><div class="slider-title">${esc(a.title)}</div></div>`});h+='</div>';wrap.innerHTML=h;let after=document.querySelector('.ai-compose')||home.firstChild;if(after)after.after(wrap);else home.prepend(wrap);}
|
| 156 |
+
let oldShortAct=window.shortAct;
|
| 157 |
+
window.shortAct=async function(kind,id,action,text=''){let meta=shortsMap[id]||{};let url='/api/ai/interact';let body={id,kind:kind==='yt'?'yt':kind,action,text,title:meta.title||'',context:meta.title?('Video Shorts YouTube từ kênh '+(meta.channel||'')+'. Tiêu đề: '+meta.title):''};let r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});let j=await r.json();let st=j.stats||j;let v=document.getElementById(`v-${kind}-${id}`),l=document.getElementById(`l-${kind}-${id}`);if(v&&st.views!=null)v.textContent=st.views;if(l&&st.likes!=null)l.textContent=st.likes;return st;};
|
| 158 |
+
window.submitShortAsk=async function(kind,id){let t=document.getElementById('short-ask-text').value.trim();if(!t)return;let st=await shortAct(kind,id,'ask',t);let a=(st.asks&&st.asks[0]&&st.asks[0].a)||'Chưa có trả lời';document.getElementById('short-answer').innerHTML='<p style="white-space:pre-wrap;color:#ccc">'+esc(a)+'</p>';};
|
| 159 |
+
let oldOpenTikTok=window.openTikTok;
|
| 160 |
+
window.openTikTok=async function(type,startIdx){if(type==='shorts'){let arts=await fetch('/api/shorts?refresh=1').then(r=>r.json()).catch(()=>[]);arts.forEach(a=>{if(a.id)shortsMap[a.id]=a});}return oldOpenTikTok?oldOpenTikTok(type,startIdx):null;};
|
| 161 |
+
function addArticleAskBox(){let view=document.getElementById('view-article');if(!view||document.getElementById('article-ai-ask'))return;let art=view.querySelector('.article-view');if(!art)return;let box=document.createElement('div');box.id='article-ai-ask';box.className='article-ai-ask';box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:6px">🤖 Hỏi AI về bài viết</h3><textarea id="article-ai-question" placeholder="Nhập câu hỏi cần AI trả lời chi tiết về bài viết..."></textarea><button onclick="askArticleAI()">Hỏi AI</button><div id="article-ai-answer" class="article-ai-answer"></div>';art.appendChild(box);}
|
| 162 |
+
window.askArticleAI=async function(){let q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi trước');let ans=document.getElementById('article-ai-answer');ans.textContent='Đang hỏi AI...';let url=(window._currentArticle&&window._currentArticle.url)||((typeof _currentArticle!=='undefined'&&_currentArticle.url)||'');let context=document.querySelector('.article-view')?.innerText||'';try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,question:q,context})});let j=await r.json();ans.textContent=j.answer||j.error||'Không có trả lời';}catch(e){ans.textContent='Lỗi hỏi AI: '+e.message}}
|
| 163 |
+
let oldReadArticle=window.readArticle;if(oldReadArticle){window.readArticle=async function(){let ret=await oldReadArticle.apply(this,arguments);setTimeout(addArticleAskBox,700);return ret;}}
|
| 164 |
+
setTimeout(()=>{ensureTopicButtonFinal4();ensureShortsHome();},1000);setInterval(()=>{ensureTopicButtonFinal4();if(document.getElementById('view-home')?.classList.contains('active'))ensureShortsHome();addArticleAskBox();},2000);
|
| 165 |
+
})();
|
| 166 |
+
</script>
|
| 167 |
+
'''
|
| 168 |
+
|
| 169 |
+
@app.get('/')
|
| 170 |
+
async def index_final4():
|
| 171 |
+
html=f3.f2.f1._load_index_html();body=getattr(rt.old,'PATCH_INJECT','')+f3.f2.f1.FINAL_INJECT+f3.FINAL3_INJECT+FINAL4_INJECT
|
| 172 |
+
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|