bep40 commited on
Commit
ebe9d97
·
verified ·
1 Parent(s): 289b9e2

Upload ai_runtime_final4.py

Browse files
Files changed (1) hide show
  1. 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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)