Spaces:
Running
Running
Remove files not in b9b6ba4
Browse files- ai_runtime_final7.py +0 -193
- ai_runtime_stable.py +0 -164
- patch_b9b6ba4.py +0 -128
- restore_53ee7c5_patch.py +0 -134
- restore_b9b6ba4_clean_runner.py +0 -27
- restore_b9b6ba4_hotfix_runner.py +0 -173
- restore_b9b6ba4_patch.py +0 -147
- restore_b9b6ba4_runner.py +0 -195
- restore_to_b9b6ba4.py +0 -27
- safe_app.py +0 -104
- safe_patch.py +0 -223
ai_runtime_final7.py
DELETED
|
@@ -1,193 +0,0 @@
|
|
| 1 |
-
"""Final7: consolidate to one AI wall, persistent comments/shorts, direct Shorts player, better AI topic writing."""
|
| 2 |
-
import os, re, json, time, requests
|
| 3 |
-
from urllib.parse import urlparse, quote
|
| 4 |
-
import ai_runtime_final6 as f6
|
| 5 |
-
from ai_runtime_final6 import app, base, rt, HTMLResponse, JSONResponse, Request, Query, FileResponse
|
| 6 |
-
try:
|
| 7 |
-
import main as main_mod
|
| 8 |
-
except Exception:
|
| 9 |
-
main_mod=None
|
| 10 |
-
|
| 11 |
-
SPACE_URL="https://bep40-vnews.hf.space"
|
| 12 |
-
DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
|
| 13 |
-
AI_INTERACTIONS_FILE=os.path.join(DATA_DIR,'ai_interactions.json')
|
| 14 |
-
AI_SHORT_INDEX_FILE=os.path.join(DATA_DIR,'ai_short_index.json')
|
| 15 |
-
SHORTS_CACHE={"t":0,"d":[]}
|
| 16 |
-
CHANNELS=["baodantri7941","baosuckhoedoisongboyte"]
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def clean(s):
|
| 20 |
-
import html as html_lib
|
| 21 |
-
return re.sub(r"\s+"," ",html_lib.unescape(s or "")).strip()
|
| 22 |
-
|
| 23 |
-
def _domain(u):
|
| 24 |
-
try:return urlparse(u or '').netloc.replace('www.','')
|
| 25 |
-
except Exception:return ''
|
| 26 |
-
|
| 27 |
-
def _load(path,default):
|
| 28 |
-
try:
|
| 29 |
-
if os.path.exists(path):
|
| 30 |
-
with open(path,'r',encoding='utf-8') as f:return json.load(f)
|
| 31 |
-
except Exception:pass
|
| 32 |
-
return default
|
| 33 |
-
|
| 34 |
-
def _save(path,data):
|
| 35 |
-
try:
|
| 36 |
-
os.makedirs(os.path.dirname(path),exist_ok=True);tmp=path+'.tmp'
|
| 37 |
-
with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
|
| 38 |
-
os.replace(tmp,path)
|
| 39 |
-
except Exception:pass
|
| 40 |
-
|
| 41 |
-
def _fallback_shorts():
|
| 42 |
-
seen=set();out=[];c=[]
|
| 43 |
-
try:c+=(getattr(main_mod,'SHORTS_FALLBACK',[]) or [])
|
| 44 |
-
except Exception:pass
|
| 45 |
-
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'),('tvPewsc2ph4','Tính năng ẩn trên iPhone giúp giảm mỏi mắt | 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'),('IUOprcJyYr4','Phụ nữ táo bón có phải do lười ăn rau? | SKĐS','baosuckhoedoisongboyte')]
|
| 46 |
-
for vid,title,ch in hard:c.append({'id':vid,'title':title,'channel':ch})
|
| 47 |
-
for v in c:
|
| 48 |
-
vid=v.get('id')
|
| 49 |
-
if vid and vid not in seen:
|
| 50 |
-
seen.add(vid)
|
| 51 |
-
out.append({'id':vid,'title':v.get('title','YouTube Short'),'channel':v.get('channel',''),'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
|
| 52 |
-
return out
|
| 53 |
-
|
| 54 |
-
def _fresh_shorts():
|
| 55 |
-
seen=set();out=[]
|
| 56 |
-
for ch in CHANNELS:
|
| 57 |
-
got=f6.f5.f4.f3._youtube_shorts_ytdlp(ch,20) or f6.f5.f4.f3._youtube_shorts_html(ch,20)
|
| 58 |
-
for v in got:
|
| 59 |
-
vid=v.get('id')
|
| 60 |
-
if vid and vid not in seen:
|
| 61 |
-
seen.add(vid);out.append(v)
|
| 62 |
-
for v in _fallback_shorts():
|
| 63 |
-
if v['id'] not in seen:
|
| 64 |
-
seen.add(v['id']);out.append(v)
|
| 65 |
-
return out[:60]
|
| 66 |
-
|
| 67 |
-
# Remove conflicting routes.
|
| 68 |
-
_PATCH={('/api/shorts','GET'),('/api/topic_post','POST'),('/api/ai/interact','POST'),('/api/ai/short/{post_id}','POST'),('/api/ai_shorts','GET'),('/api/ai/short-file/{file_id}','GET'),('/','GET')}
|
| 69 |
-
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)]
|
| 70 |
-
|
| 71 |
-
@app.get('/api/shorts')
|
| 72 |
-
def api_shorts(refresh:int=Query(default=0)):
|
| 73 |
-
now=time.time()
|
| 74 |
-
if not refresh and SHORTS_CACHE['d'] and now-SHORTS_CACHE['t']<900:return JSONResponse(SHORTS_CACHE['d'])
|
| 75 |
-
data=_fresh_shorts();SHORTS_CACHE.update({'t':now,'d':data});return JSONResponse(data)
|
| 76 |
-
|
| 77 |
-
@app.post('/api/topic_post')
|
| 78 |
-
async def topic_post_quality(request:Request):
|
| 79 |
-
body=await request.json();topic=clean(body.get('topic',''))
|
| 80 |
-
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 81 |
-
img=f6.f5._topic_image(topic) if hasattr(f6.f5,'_topic_image') else base.pollinations_image_url(topic)
|
| 82 |
-
prompt=f"""Bạn là một cây bút phân tích chuyên sâu của VNEWS. Người dùng muốn một bài viết chất lượng về chủ đề: {topic}
|
| 83 |
-
|
| 84 |
-
Hãy sử dụng kiến thức tổng hợp của bạn để viết một bài hoàn chỉnh, sâu và hữu ích. KHÔNG lập dàn ý, KHÔNG nói chung chung, KHÔNG hướng dẫn cách viết.
|
| 85 |
-
|
| 86 |
-
Bắt buộc:
|
| 87 |
-
- Tiêu đề cụ thể, hấp dẫn.
|
| 88 |
-
- Mở đầu đi thẳng vào bản chất của chủ đề.
|
| 89 |
-
- Giải thích các khái niệm/bối cảnh quan trọng để người đọc hiểu vấn đề.
|
| 90 |
-
- Cung cấp nhận định, ví dụ cụ thể, hệ quả hoặc ý nghĩa thực tế.
|
| 91 |
-
- Nếu là thể thao: nói về nhân vật/đội bóng, bối cảnh, ý nghĩa chuyên môn/lịch sử, vì sao đáng chú ý.
|
| 92 |
-
- Nếu là công nghệ/xã hội/giáo dục: nói về cơ chế, ứng dụng, lợi ích, rủi ro, hiểu lầm phổ biến.
|
| 93 |
-
- Tránh bịa số liệu thời sự; nếu không chắc, diễn đạt thận trọng.
|
| 94 |
-
- Độ dài 700-1000 chữ tiếng Việt.
|
| 95 |
-
- Cuối bài ghi: Nguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp.
|
| 96 |
-
"""
|
| 97 |
-
text=await base.qwen_generate(prompt,image_url=img,max_tokens=1800)
|
| 98 |
-
if not text:text=f"{topic}\n\n{topic} là một chủ đề đáng chú ý vì nó liên quan đến bối cảnh, tác động và những hiểu lầm thường gặp trong đời sống. Bài viết này cung cấp một góc nhìn tổng hợp, giải thích bản chất vấn đề và các điểm cần lưu ý để người đọc hiểu sâu hơn.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp."
|
| 99 |
-
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]
|
| 100 |
-
posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
|
| 101 |
-
return JSONResponse({'post':post})
|
| 102 |
-
|
| 103 |
-
@app.post('/api/ai/interact')
|
| 104 |
-
async def ai_interact(request:Request):
|
| 105 |
-
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',''));title=clean(body.get('title',''));context=clean(body.get('context',''))
|
| 106 |
-
if not pid:return JSONResponse({'error':'missing id'},status_code=400)
|
| 107 |
-
db=_load(AI_INTERACTIONS_FILE,{})
|
| 108 |
-
key=kind+':'+pid
|
| 109 |
-
st=db.get(key) or {'views':0,'likes':0,'comments':[],'asks':[]}
|
| 110 |
-
if action=='view':st['views']=int(st.get('views',0))+1
|
| 111 |
-
elif action=='like':st['likes']=int(st.get('likes',0))+1
|
| 112 |
-
elif action=='comment' and text:
|
| 113 |
-
st.setdefault('comments',[]).insert(0,{'text':text[:300],'ts':int(time.time())});st['comments']=st['comments'][:100]
|
| 114 |
-
elif action=='ask' and text:
|
| 115 |
-
if kind in ('ai','short','wall'):
|
| 116 |
-
posts=base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==pid),{})
|
| 117 |
-
title=title or p.get('title','');context=context or (p.get('text') or '')
|
| 118 |
-
if not context:context=title or pid
|
| 119 |
-
prompt=f"""Bạn là trợ lý VNEWS. Trả lời đúng trọng tâm câu hỏi dựa trên ngữ cảnh video/bài viết.
|
| 120 |
-
|
| 121 |
-
Tiêu đề: {title}
|
| 122 |
-
Ngữ cảnh/nội dung mô tả: {context[:6000]}
|
| 123 |
-
Câu hỏi: {text}
|
| 124 |
-
|
| 125 |
-
Hãy trả lời bằng tiếng Việt, cụ thể và hữu ích. Nếu đây là Shorts YouTube chỉ có tiêu đề, hãy nói rõ bạn đang suy luận từ tiêu đề/mô tả, nhưng vẫn phân tích đúng trọng tâm tiêu đề.
|
| 126 |
-
"""
|
| 127 |
-
ans=await base.qwen_generate(prompt,max_tokens=1000)
|
| 128 |
-
if not ans:ans='AI chưa trả lời được lúc này. Bạn thử hỏi cụ thể hơn.'
|
| 129 |
-
st.setdefault('asks',[]).insert(0,{'q':text[:300],'a':ans[:1800],'ts':int(time.time())});st['asks']=st['asks'][:60]
|
| 130 |
-
db[key]=st;_save(AI_INTERACTIONS_FILE,db);return JSONResponse({'stats':st})
|
| 131 |
-
|
| 132 |
-
@app.post('/api/ai/short/{post_id}')
|
| 133 |
-
async def ai_short_persistent(post_id:str,request:Request):
|
| 134 |
-
# Reuse robust generator from final1/final2 chain.
|
| 135 |
-
res=await f6.f5.f4.f3.f2.f1.final_short(post_id,request) if hasattr(f6.f5.f4.f3.f2.f1,'final_short') else await f6.f5.f4.f3.f2.f1.short_segments(post_id,request)
|
| 136 |
-
try:
|
| 137 |
-
data=json.loads(res.body.decode()) if hasattr(res,'body') else {}
|
| 138 |
-
video=data.get('video')
|
| 139 |
-
if video:
|
| 140 |
-
idx=_load(AI_SHORT_INDEX_FILE,[])
|
| 141 |
-
if not any(x.get('post_id')==post_id for x in idx):idx.insert(0,{'post_id':post_id,'video':video,'ts':int(time.time())})
|
| 142 |
-
_save(AI_SHORT_INDEX_FILE,idx[:200])
|
| 143 |
-
except Exception:pass
|
| 144 |
-
return res
|
| 145 |
-
|
| 146 |
-
@app.get('/api/ai_shorts')
|
| 147 |
-
def api_ai_shorts():
|
| 148 |
-
posts=base._load_ai_wall();idx=_load(AI_SHORT_INDEX_FILE,[]);out=[]
|
| 149 |
-
for rec in idx:
|
| 150 |
-
p=next((x for x in posts if str(x.get('id'))==str(rec.get('post_id'))),None)
|
| 151 |
-
if p:
|
| 152 |
-
p=dict(p);p['video']=rec.get('video') or p.get('video');out.append(p)
|
| 153 |
-
# also include any posts with video not indexed yet
|
| 154 |
-
for p in posts:
|
| 155 |
-
if p.get('video') and not any(str(x.get('id'))==str(p.get('id')) for x in out):out.append(p)
|
| 156 |
-
return JSONResponse({'posts':out[:100]})
|
| 157 |
-
|
| 158 |
-
@app.get('/api/ai/short-file/{file_id}')
|
| 159 |
-
def short_file(file_id:str):
|
| 160 |
-
return f6.f5.f4.f3.f2.f1.short_file(file_id) if hasattr(f6.f5.f4.f3.f2.f1,'short_file') else JSONResponse({'error':'not found'},status_code=404)
|
| 161 |
-
|
| 162 |
-
FINAL7_INJECT=r'''
|
| 163 |
-
<style>
|
| 164 |
-
/* One wall only: hide all auxiliary AI wall clones */
|
| 165 |
-
#ai-wall-topic-live,#ai-wall-patched,#ai-shorts-patched{display:none!important}.topic-final3,.topic-final4{display:none!important}.topic-final5{display:flex!important}.comment-list{margin-top:8px}.comment-item{background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px}
|
| 166 |
-
</style>
|
| 167 |
-
<script>
|
| 168 |
-
(function(){
|
| 169 |
-
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 170 |
-
let shortsData7=[];let aiShorts7=[];
|
| 171 |
-
async function loadShorts7(){shortsData7=await fetch('/api/shorts?refresh=1').then(r=>r.json()).catch(()=>[]);return shortsData7;}
|
| 172 |
-
async function loadAIShorts7(){aiShorts7=(await fetch('/api/ai_shorts').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];renderAIShorts7();return aiShorts7;}
|
| 173 |
-
function renderAIShorts7(){let home=document.getElementById('view-home');if(!home)return;document.getElementById('ai-short-home')?.remove();if(!aiShorts7.length)return;let wrap=document.createElement('div');wrap.id='ai-short-home';wrap.className='ai-short-home';let h='<div class="slider-header"><span class="slider-label">🎬 Short AI</span><span class="slider-note">Lưu vĩnh viễn</span></div><div class="slider-track">';aiShorts7.slice(0,50).forEach((p,i)=>{h+=`<div class="ai-short-card-final" onclick="openAIShorts7(${i})"><video src="${p.video}" muted playsinline preload="metadata"></video><div class="slider-title">${esc(p.title)}</div></div>`});h+='</div>';wrap.innerHTML=h;let wall=document.getElementById('ai-wall-final')||document.querySelector('.ai-compose');if(wall)wall.after(wrap);else home.prepend(wrap);}
|
| 174 |
-
function actionPanel(kind,id){return `<div class="short-action-panel"><button class="short-action-btn" onclick="shortAct7('${kind}','${id}','view')"><div class="ico">👁</div><span id="v-${kind}-${id}">0</span></button><button class="short-action-btn" onclick="shortAct7('${kind}','${id}','like')"><div class="ico">❤️</div><span id="l-${kind}-${id}">0</span></button><button class="short-action-btn" onclick="openComments7('${kind}','${id}')"><div class="ico">💬</div><span>BL</span></button><button class="short-action-btn" onclick="openAsk7('${kind}','${id}')"><div class="ico">🤖</div><span>Hỏi</span></button><button class="short-action-btn" onclick="shareShortCtx('${kind}','${id}')"><div class="ico">📤</div><span>Share</span></button></div>`}
|
| 175 |
-
window.openShortsFinal5=window.openShorts7=async function(start){let arts=shortsData7.length?shortsData7:await loadShorts7();if(!arts.length)return alert('Không tải được Shorts');let ordered=start>0?arts.slice(start).concat(arts.slice(0,start)):arts;showView('view-tiktok');let h='<button class="back-btn" onclick="switchCat(\'home\')">← Shorts Dân trí & SKĐS</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';ordered.forEach((v,i)=>{let id=v.id;let src='https://www.youtube.com/embed/'+id+'?autoplay=1&rel=0&playsinline=1';h+=`<div class="tiktok-slide" data-kind="yt" data-id="${id}" data-title="${esc(v.title)}" data-channel="${esc(v.channel||'')}"><iframe data-yt-src="${src}" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture"></iframe><div class="tiktok-bottom"><span class="badge badge-fpt">YT</span><p class="tiktok-title">${esc(v.title)}</p></div>${actionPanel('yt',id)}<span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`});h+='</div></div>';document.getElementById('view-tiktok').innerHTML=h;initFeed7();}
|
| 176 |
-
window.openAIShorts7=function(start){let arts=aiShorts7.length?aiShorts7:[];if(!arts.length)return;let ordered=start>0?arts.slice(start).concat(arts.slice(0,start)):arts;showView('view-tiktok');let h='<button class="back-btn" onclick="switchCat(\'home\')">← Short AI</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';ordered.forEach((p,i)=>{h+=`<div class="tiktok-slide" data-kind="ai" data-id="${p.id}" data-title="${esc(p.title)}" data-context="${esc((p.text||'').slice(0,800))}"><video src="${p.video}" playsinline controls loop></video><div class="tiktok-bottom"><span class="badge badge-ai">AI</span><p class="tiktok-title">${esc(p.title)}</p></div>${actionPanel('ai',p.id)}<span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`});h+='</div></div>';document.getElementById('view-tiktok').innerHTML=h;initFeed7();}
|
| 177 |
-
function initFeed7(){let feed=document.getElementById('tiktok-feed');if(!feed)return;let slides=feed.querySelectorAll('.tiktok-slide');let cur=-1;function act(i){if(i===cur)return;slides.forEach((sl,idx)=>{let fr=sl.querySelector('iframe'),v=sl.querySelector('video');if(idx===i){if(fr&&!fr.src&&fr.dataset.ytSrc)fr.src=fr.dataset.ytSrc;if(v)v.play().catch(()=>{});shortAct7(sl.dataset.kind,sl.dataset.id,'view').catch(()=>{})}else{if(fr&&fr.src)fr.src='';if(v)v.pause();}});cur=i}let t;feed.addEventListener('scroll',()=>{clearTimeout(t);t=setTimeout(()=>{let rect=feed.getBoundingClientRect(),ctr=rect.top+rect.height/2,b=-1,d=1e9;slides.forEach((sl,i)=>{let dd=Math.abs(sl.getBoundingClientRect().top+sl.getBoundingClientRect().height/2-ctr);if(dd<d){d=dd;b=i}});if(b>=0)act(b)},130)});setTimeout(()=>act(0),250)}
|
| 178 |
-
window.shortAct7=window.shortAct=async function(kind,id,action,text=''){let sl=document.querySelector(`.tiktok-slide[data-id="${id}"]`);let body={id,kind,action,text,title:sl?.dataset.title||'',context:sl?.dataset.context||sl?.dataset.title||''};let r=await fetch('/api/ai/interact',{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;}
|
| 179 |
-
window.openComments7=function(kind,id){let m=document.getElementById('short-modal');m.innerHTML=`<h3>💬 Bình luận</h3><div id="comment-list" class="comment-list">Đang tải...</div><textarea id="short-comment-text" placeholder="Nhập bình luận..."></textarea><button onclick="submitComment7('${kind}','${id}')">Gửi</button><button onclick="closeShortModal()">Đóng</button>`;m.classList.add('active');shortAct7(kind,id,'noop').then(st=>{document.getElementById('comment-list').innerHTML=(st.comments||[]).map(c=>`<div class="comment-item">${esc(c.text)}</div>`).join('')||'<div class="comment-item">Chưa có bình luận</div>'})}
|
| 180 |
-
window.submitComment7=async function(kind,id){let t=document.getElementById('short-comment-text').value.trim();if(!t)return;let st=await shortAct7(kind,id,'comment',t);document.getElementById('comment-list').innerHTML=(st.comments||[]).map(c=>`<div class="comment-item">${esc(c.text)}</div>`).join('')}
|
| 181 |
-
window.openAsk7=window.openAskBox=function(kind,id){let m=document.getElementById('short-modal');m.innerHTML=`<h3>🤖 Hỏi AI</h3><input id="short-ask-text" placeholder="Bạn muốn hỏi gì?"><div id="short-answer"></div><button onclick="submitAsk7('${kind}','${id}')">Hỏi</button><button onclick="closeShortModal()">Đóng</button>`;m.classList.add('active')}
|
| 182 |
-
window.submitAsk7=window.submitShortAsk=async function(kind,id){let t=document.getElementById('short-ask-text').value.trim();if(!t)return;let st=await shortAct7(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>'}
|
| 183 |
-
let oldMake=window.makeFinalShort||window.aiMakeShortPatched;window.makeFinalShort=window.aiMakeShortPatched=async function(i){let p=(window.finalWall||[])[i]||(window.finalWall3||[])[i];if(!p&&oldMake)return oldMake(i);if(!p)return;let voice=document.getElementById('ai-short-voice')?.value||'nu';let emotion=document.getElementById('ai-short-emotion')?.value||'neutral';let btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tạo...'}try{let r=await fetch('/api/ai/short/'+p.id,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({voice,emotion,speed:1.2})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');p.video=j.video;await loadAIShorts7();alert('Đã tạo và lưu Short AI vĩnh viễn.');}catch(e){alert(e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🎬 Tạo short'}}}
|
| 184 |
-
window.createTopicPostFinal5=async function(){let inp=document.getElementById('ai-topic-input-final5');let topic=(inp&&inp.value||'').trim();if(!topic)return alert('Nhập chủ đề trước');let btn=document.getElementById('ai-topic-btn-final5');if(btn){btn.disabled=true;btn.textContent='Đang tạo bài...'}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.renderWall)window.renderWall();alert('Đã tạo bài chất lượng bằng kiến thức Qwen và đăng vào Tường AI duy nhất.');}catch(e){alert(e.message)}finally{if(btn){btn.disabled=false;btn.textContent='✨ Tạo bài kiến thức bằng Qwen'}}}
|
| 185 |
-
setTimeout(async()=>{document.querySelectorAll('#ai-wall-topic-live,#ai-wall-patched,#ai-shorts-patched').forEach(e=>e.remove());await loadShorts7();await loadAIShorts7();document.querySelectorAll('.slider-label').forEach(label=>{if((label.textContent||'').includes('Shorts'))label.closest('.slider-wrap')?.querySelectorAll('.slider-item').forEach((el,i)=>el.setAttribute('onclick',`openShorts7(${i})`));});},1000);
|
| 186 |
-
})();
|
| 187 |
-
</script>
|
| 188 |
-
'''
|
| 189 |
-
|
| 190 |
-
@app.get('/')
|
| 191 |
-
async def index_final7():
|
| 192 |
-
html=f6.f5.f4.f3.f2.f1._load_index_html();body=getattr(rt.old,'PATCH_INJECT','')+f6.f5.f4.f3.f2.f1.FINAL_INJECT+f6.f5.f4.f3.FINAL3_INJECT+f6.f5.f4.FINAL4_INJECT+f6.f5.FINAL5_INJECT+FINAL7_INJECT
|
| 193 |
-
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ai_runtime_stable.py
DELETED
|
@@ -1,164 +0,0 @@
|
|
| 1 |
-
"""Stable entrypoint for VNEWS.
|
| 2 |
-
Avoid restore_runner snapshot import errors. Uses current-repo runtime and full UI loader.
|
| 3 |
-
"""
|
| 4 |
-
import os, re, json, time, requests
|
| 5 |
-
from urllib.parse import urlparse, quote
|
| 6 |
-
|
| 7 |
-
try:
|
| 8 |
-
import ai_runtime_final5 as stable
|
| 9 |
-
except Exception:
|
| 10 |
-
import ai_runtime_final4 as stable
|
| 11 |
-
|
| 12 |
-
app = stable.app
|
| 13 |
-
base = stable.base
|
| 14 |
-
rt = stable.rt
|
| 15 |
-
HTMLResponse = stable.HTMLResponse
|
| 16 |
-
JSONResponse = stable.JSONResponse
|
| 17 |
-
Request = stable.Request
|
| 18 |
-
Query = stable.Query
|
| 19 |
-
|
| 20 |
-
DATA_DIR = "/data" if os.path.isdir('/data') else "/app/data"
|
| 21 |
-
AI_INTERACTIONS_FILE = os.path.join(DATA_DIR, 'ai_interactions.json')
|
| 22 |
-
SHORTS_CACHE = {'t': 0, 'd': []}
|
| 23 |
-
CHANNELS = ['baodantri7941', 'baosuckhoedoisongboyte']
|
| 24 |
-
RESTORE_INDEX_URL = "https://huggingface.co/spaces/bep40/vnews/raw/restore-33c3dda/static/index.html"
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def clean(s):
|
| 28 |
-
import html as html_lib
|
| 29 |
-
return re.sub(r"\s+", " ", html_lib.unescape(s or "")).strip()
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def _load(path, default):
|
| 33 |
-
try:
|
| 34 |
-
if os.path.exists(path):
|
| 35 |
-
with open(path, 'r', encoding='utf-8') as f:return json.load(f)
|
| 36 |
-
except Exception:pass
|
| 37 |
-
return default
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def _save(path, data):
|
| 41 |
-
try:
|
| 42 |
-
os.makedirs(os.path.dirname(path), exist_ok=True);tmp=path+'.tmp'
|
| 43 |
-
with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
|
| 44 |
-
os.replace(tmp,path)
|
| 45 |
-
except Exception:pass
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def _fallback_shorts():
|
| 49 |
-
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'),('tvPewsc2ph4','Tính năng ẩn trên iPhone giúp giảm mỏi mắt | Dân trí','baodantri7941'),('b1Nxzv9ixlU','Y án 3 năm tù với nữ tài xế uống 8 lon bia lái xe tông chủ tịch xã tử vong | 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'),('IUOprcJyYr4','Phụ nữ táo bón có phải do lười ăn rau? | SKĐS','baosuckhoedoisongboyte'),('YY8ojFNE-AU','Quái xế tự quay clip nẹt pô, đánh võng đăng TikTok bị xử lý | SKĐS','baosuckhoedoisongboyte')]
|
| 50 |
-
return [{'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 vid,title,ch in hard]
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def _yt_html(handle,count=24):
|
| 54 |
-
try:
|
| 55 |
-
html=requests.get(f'https://www.youtube.com/@{handle}/shorts',headers=getattr(base,'HEADERS',{}),timeout=10).text
|
| 56 |
-
ids=[];out=[]
|
| 57 |
-
for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"',html):
|
| 58 |
-
vid=m.group(1)
|
| 59 |
-
if vid in ids:continue
|
| 60 |
-
ids.append(vid)
|
| 61 |
-
snip=html[max(0,m.start()-900):m.start()+1600]
|
| 62 |
-
mt=re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"',snip) or re.search(r'"accessibilityText":"([^"]+)"',snip)
|
| 63 |
-
title=clean((mt.group(1) if mt else 'YouTube Short').replace('\\n',' '))
|
| 64 |
-
out.append({'id':vid,'title':title,'channel':handle,'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
|
| 65 |
-
if len(out)>=count:break
|
| 66 |
-
return out
|
| 67 |
-
except Exception:return []
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def _fresh_shorts():
|
| 71 |
-
seen=set();out=[]
|
| 72 |
-
for ch in CHANNELS:
|
| 73 |
-
for v in _yt_html(ch,20):
|
| 74 |
-
if v['id'] not in seen:seen.add(v['id']);out.append(v)
|
| 75 |
-
for v in _fallback_shorts():
|
| 76 |
-
if v['id'] not in seen:seen.add(v['id']);out.append(v)
|
| 77 |
-
return out[:60]
|
| 78 |
-
|
| 79 |
-
_PATCH={('/api/shorts','GET'),('/api/ai/interact','POST'),('/api/topic_post','POST'),('/','GET')}
|
| 80 |
-
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)]
|
| 81 |
-
|
| 82 |
-
@app.get('/api/shorts')
|
| 83 |
-
def api_shorts_stable(refresh:int=Query(default=0)):
|
| 84 |
-
now=time.time()
|
| 85 |
-
if not refresh and SHORTS_CACHE['d'] and now-SHORTS_CACHE['t']<300:return JSONResponse(SHORTS_CACHE['d'])
|
| 86 |
-
data=_fresh_shorts();SHORTS_CACHE.update({'t':now,'d':data});return JSONResponse(data)
|
| 87 |
-
|
| 88 |
-
@app.post('/api/ai/interact')
|
| 89 |
-
async def ai_interact_stable(request:Request):
|
| 90 |
-
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',''));title=clean(body.get('title',''));context=clean(body.get('context',''))
|
| 91 |
-
if not pid:return JSONResponse({'error':'missing id'},status_code=400)
|
| 92 |
-
db=_load(AI_INTERACTIONS_FILE,{})
|
| 93 |
-
key=kind+':'+pid
|
| 94 |
-
st=db.get(key) or {'views':0,'likes':0,'comments':[],'asks':[]}
|
| 95 |
-
if action=='view':st['views']=int(st.get('views',0))+1
|
| 96 |
-
elif action=='like':st['likes']=int(st.get('likes',0))+1
|
| 97 |
-
elif action=='comment' and text:
|
| 98 |
-
st.setdefault('comments',[]).insert(0,{'text':text[:300],'ts':int(time.time())});st['comments']=st['comments'][:100]
|
| 99 |
-
elif action=='ask' and text:
|
| 100 |
-
if kind in ('ai','short','wall'):
|
| 101 |
-
posts=base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==pid),{})
|
| 102 |
-
title=title or p.get('title','');context=context or p.get('text','')
|
| 103 |
-
if not context:context=title or pid
|
| 104 |
-
prompt=f"""Bạn là trợ lý VNEWS. Trả lời đúng trọng tâm câu hỏi dựa trên nội dung/mô tả short hoặc bài viết.
|
| 105 |
-
|
| 106 |
-
Tiêu đề: {title}
|
| 107 |
-
Ngữ cảnh/mô tả: {context[:6000]}
|
| 108 |
-
Câu hỏi: {text}
|
| 109 |
-
|
| 110 |
-
Trả lời bằng tiếng Việt, cụ thể, có giải thích. Nếu là short YouTube chỉ có tiêu đề, hãy nói rõ bạn suy luận từ tiêu đề/mô tả và không giả vờ đã xem toàn bộ video.
|
| 111 |
-
"""
|
| 112 |
-
ans=await base.qwen_generate(prompt,max_tokens=900)
|
| 113 |
-
if not ans:ans='AI chưa trả lời được lúc này. Bạn thử hỏi cụ thể hơn.'
|
| 114 |
-
st.setdefault('asks',[]).insert(0,{'q':text[:300],'a':ans[:1800],'ts':int(time.time())});st['asks']=st['asks'][:60]
|
| 115 |
-
db[key]=st;_save(AI_INTERACTIONS_FILE,db);return JSONResponse({'stats':st})
|
| 116 |
-
|
| 117 |
-
@app.post('/api/topic_post')
|
| 118 |
-
async def topic_post_stable(request:Request):
|
| 119 |
-
body=await request.json();topic=clean(body.get('topic',''))
|
| 120 |
-
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 121 |
-
try:img=base.pollinations_image_url(topic)
|
| 122 |
-
except Exception:img='https://image.pollinations.ai/prompt/'+quote('Vietnamese editorial illustration '+topic)+'?width=1024&height=576&nologo=true'
|
| 123 |
-
prompt=f"""Viết một bài báo tiếng Việt hoàn chỉnh, chất lượng cao về chủ đề: {topic}
|
| 124 |
-
|
| 125 |
-
Chỉ xuất bản nội dung cuối cùng. Không lập dàn ý, không nói chung chung, không hướng dẫn cách viết.
|
| 126 |
-
|
| 127 |
-
Yêu cầu:
|
| 128 |
-
- Tiêu đề hấp dẫn, cụ thể.
|
| 129 |
-
- Sapo 2-3 câu đi thẳng vào chủ đề.
|
| 130 |
-
- Các đoạn phân tích có kiến thức thực chất: bối cảnh, nguyên nhân, tác động, ví dụ, nhận định.
|
| 131 |
-
- Nếu là thể thao: nói về nhân vật/đội bóng, chuyên môn, lịch sử, ý nghĩa.
|
| 132 |
-
- Nếu là công nghệ/xã hội/giáo dục: nói về cơ chế, ứng dụng, lợi ích/rủi ro.
|
| 133 |
-
- Tránh bịa số liệu thời sự mới; nếu không chắc, diễn đạt thận trọng.
|
| 134 |
-
- Cuối bài có mục Nguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp.
|
| 135 |
-
"""
|
| 136 |
-
text=await base.qwen_generate(prompt,image_url=img,max_tokens=1600)
|
| 137 |
-
if not text:text=f"{topic}\n\n{topic} là một chủ đề có nhiều lớp ý nghĩa, cần nhìn từ bối cảnh, tác động và các hiểu lầm thường gặp. Nội dung này tổng hợp các điểm quan trọng để người đọc có cái nhìn rõ hơn.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp."
|
| 138 |
-
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]
|
| 139 |
-
posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts);return JSONResponse({'post':post})
|
| 140 |
-
|
| 141 |
-
PATCH_JS=r'''
|
| 142 |
-
<style>#ai-wall-topic-live,#ai-wall-patched,#ai-shorts-patched{display:none!important}.topic-final3,.topic-final4{display:none!important}.topic-final5{display:flex!important}.short-modal{position:fixed;inset:auto 0 0 0;max-height:60vh;background:#181818;border-radius:16px 16px 0 0;z-index:99999;padding:14px;display:none;overflow:auto}.short-modal.active{display:block}.comment-item{background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px}</style><div id="short-modal" class="short-modal"></div><script>(function(){function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}let shorts=[];async function loadShorts(){shorts=await fetch('/api/shorts?refresh=1').then(r=>r.json()).catch(()=>[]);return shorts;}function actionPanel(kind,id){return `<div class="short-action-panel"><button class="short-action-btn" onclick="shortAct('${kind}','${id}','view')"><div class="ico">👁</div><span id="v-${kind}-${id}">0</span></button><button class="short-action-btn" onclick="shortAct('${kind}','${id}','like')"><div class="ico">❤️</div><span id="l-${kind}-${id}">0</span></button><button class="short-action-btn" onclick="openComments('${kind}','${id}')"><div class="ico">💬</div><span>BL</span></button><button class="short-action-btn" onclick="openAsk('${kind}','${id}')"><div class="ico">🤖</div><span>Hỏi</span></button><button class="short-action-btn" onclick="shareShortCtx('${kind}','${id}')"><div class="ico">📤</div><span>Share</span></button></div>`}window.openShortsFixed=async function(start){let arr=shorts.length?shorts:await loadShorts();if(!arr.length)return alert('Không tải được Shorts');let ordered=start>0?arr.slice(start).concat(arr.slice(0,start)):arr;showView('view-tiktok');let h='<button class="back-btn" onclick="switchCat(\'home\')">← Shorts</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';ordered.forEach((v,i)=>{let id=v.id;let src='https://www.youtube.com/embed/'+id+'?autoplay=1&rel=0&playsinline=1';h+=`<div class="tiktok-slide" data-kind="yt" data-id="${id}" data-title="${esc(v.title)}" data-context="${esc('Video Shorts YouTube từ kênh '+(v.channel||'')+'. Tiêu đề: '+v.title)}"><iframe data-yt-src="${src}" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture"></iframe><div class="tiktok-bottom"><span class="badge badge-fpt">YT</span><p class="tiktok-title">${esc(v.title)}</p></div>${actionPanel('yt',id)}<span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`});h+='</div></div>';document.getElementById('view-tiktok').innerHTML=h;initFeed();}function initFeed(){let feed=document.getElementById('tiktok-feed');if(!feed)return;let slides=feed.querySelectorAll('.tiktok-slide');let cur=-1;function act(i){if(i===cur)return;slides.forEach((sl,idx)=>{let fr=sl.querySelector('iframe'),v=sl.querySelector('video');if(idx===i){if(fr&&!fr.src&&fr.dataset.ytSrc)fr.src=fr.dataset.ytSrc;if(v)v.play().catch(()=>{});shortAct(sl.dataset.kind,sl.dataset.id,'view').catch(()=>{})}else{if(fr&&fr.src)fr.src='';if(v)v.pause();}});cur=i}let t;feed.addEventListener('scroll',()=>{clearTimeout(t);t=setTimeout(()=>{let rect=feed.getBoundingClientRect(),ctr=rect.top+rect.height/2,b=-1,d=1e9;slides.forEach((sl,i)=>{let dd=Math.abs(sl.getBoundingClientRect().top+sl.getBoundingClientRect().height/2-ctr);if(dd<d){d=dd;b=i}});if(b>=0)act(b)},120)});setTimeout(()=>act(0),250)}window.shortAct=async function(kind,id,action,text=''){let sl=document.querySelector(`.tiktok-slide[data-id="${id}"]`);let body={id,kind,action,text,title:sl?.dataset.title||'',context:sl?.dataset.context||sl?.dataset.title||''};let r=await fetch('/api/ai/interact',{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;}window.openComments=function(kind,id){let m=document.getElementById('short-modal');m.innerHTML=`<h3>💬 Bình luận</h3><div id="comment-list">Đang tải...</div><textarea id="short-comment-text" style="width:100%;background:#222;color:#eee;border:1px solid #444;border-radius:10px;padding:8px" placeholder="Nhập bình luận..."></textarea><button onclick="submitComment('${kind}','${id}')">Gửi</button><button onclick="closeShortModal()">Đóng</button>`;m.classList.add('active');shortAct(kind,id,'noop').then(st=>{document.getElementById('comment-list').innerHTML=(st.comments||[]).map(c=>`<div class="comment-item">${esc(c.text)}</div>`).join('')||'<div class="comment-item">Chưa có bình luận</div>'})}window.submitComment=async function(kind,id){let t=document.getElementById('short-comment-text').value.trim();if(!t)return;let st=await shortAct(kind,id,'comment',t);document.getElementById('comment-list').innerHTML=(st.comments||[]).map(c=>`<div class="comment-item">${esc(c.text)}</div>`).join('')}window.openAsk=function(kind,id){let m=document.getElementById('short-modal');m.innerHTML=`<h3>🤖 Hỏi AI</h3><input id="short-ask-text" style="width:100%;background:#222;color:#eee;border:1px solid #444;border-radius:10px;padding:8px" placeholder="Bạn muốn hỏi gì?"><div id="short-answer"></div><button onclick="submitAsk('${kind}','${id}')">Hỏi</button><button onclick="closeShortModal()">Đóng</button>`;m.classList.add('active')}window.submitAsk=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>'}window.closeShortModal=function(){document.getElementById('short-modal').classList.remove('active')}let oldOpen=window.openTikTok;window.openTikTok=function(type,start){if(type==='shorts')return openShortsFixed(start||0);return oldOpen?oldOpen(type,start):null;}setTimeout(async()=>{await loadShorts();document.querySelectorAll('.slider-label').forEach(label=>{if((label.textContent||'').includes('Shorts'))label.closest('.slider-wrap')?.querySelectorAll('.slider-item').forEach((el,i)=>el.setAttribute('onclick',`openShortsFixed(${i})`));});},800);})();</script>
|
| 143 |
-
'''
|
| 144 |
-
|
| 145 |
-
def _load_full_index():
|
| 146 |
-
# Prefer loader from dependency chain; fallback to restore branch; never return placeholder.
|
| 147 |
-
for obj in [stable, getattr(stable,'f4',None), getattr(getattr(stable,'f4',None),'f3',None), getattr(getattr(getattr(stable,'f4',None),'f3',None),'f2',None), getattr(getattr(getattr(getattr(stable,'f4',None),'f3',None),'f2',None),'f1',None)]:
|
| 148 |
-
try:
|
| 149 |
-
if obj and hasattr(obj,'_load_index_html'):
|
| 150 |
-
html=obj._load_index_html()
|
| 151 |
-
if '<!DOCTYPE html>' in html and '<div id="view-home"' in html:return html
|
| 152 |
-
except Exception:pass
|
| 153 |
-
try:
|
| 154 |
-
r=requests.get(RESTORE_INDEX_URL,timeout=20)
|
| 155 |
-
if r.status_code==200 and '<!DOCTYPE html>' in r.text:return r.text
|
| 156 |
-
except Exception:pass
|
| 157 |
-
return '<!DOCTYPE html><html><head><meta charset="utf-8"><title>VNEWS</title></head><body><div id="view-home">VNEWS</div></body></html>'
|
| 158 |
-
|
| 159 |
-
@app.get('/')
|
| 160 |
-
async def index_stable():
|
| 161 |
-
html=_load_full_index()
|
| 162 |
-
body=getattr(getattr(stable,'rt',None).old,'PATCH_INJECT','') if getattr(stable,'rt',None) else ''
|
| 163 |
-
body+=getattr(stable,'FINAL_INJECT','')+getattr(stable,'FINAL5_INJECT','')+PATCH_JS
|
| 164 |
-
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patch_b9b6ba4.py
DELETED
|
@@ -1,128 +0,0 @@
|
|
| 1 |
-
"""Small patch on top of b9b6ba4 snapshot.
|
| 2 |
-
- Remove leaked instruction lines from topic AI posts.
|
| 3 |
-
- Make Dantri/SKDS shorts fast/latest with cache + fallback.
|
| 4 |
-
- Add Rewrite AI + source link buttons to AI/topic wall articles.
|
| 5 |
-
"""
|
| 6 |
-
import os, re, time, requests
|
| 7 |
-
from urllib.parse import quote, urlparse
|
| 8 |
-
from fastapi import Request, Query
|
| 9 |
-
from fastapi.responses import JSONResponse, HTMLResponse
|
| 10 |
-
|
| 11 |
-
import ai_runtime_final6 as base_app
|
| 12 |
-
app = base_app.app
|
| 13 |
-
base = base_app.base
|
| 14 |
-
rt = base_app.rt
|
| 15 |
-
|
| 16 |
-
SHORT_CHANNELS=["baodantri7941","baosuckhoedoisongboyte"]
|
| 17 |
-
_SHORTS_CACHE={"t":0,"d":[]}
|
| 18 |
-
|
| 19 |
-
def clean(s):
|
| 20 |
-
import html as html_lib
|
| 21 |
-
return re.sub(r"\s+"," ",html_lib.unescape(s or "")).strip()
|
| 22 |
-
|
| 23 |
-
def _clean_ai_text(text):
|
| 24 |
-
bad_patterns=[r'Chỉ xuất bản bài viết cuối cùng.*',r'không nhắc lại yêu cầu.*',r'không liệt kê chỉ dẫn.*',r'•\s*Không sao chép nguyên văn.*',r'Không sao chép nguyên văn.*',r'•\s*Bài có tiêu đề, sapo.*',r'Bài có tiêu đề, sapo.*',r'Nhiệm vụ:\s*viết một bài báo tiếng Việt hoàn chỉnh.*',r'Yêu cầu:\s*$',r'Bắt buộc:\s*$']
|
| 25 |
-
lines=[]
|
| 26 |
-
for ln in (text or '').splitlines():
|
| 27 |
-
raw=ln.strip()
|
| 28 |
-
if raw and any(re.search(p,raw,flags=re.I) for p in bad_patterns):continue
|
| 29 |
-
lines.append(ln)
|
| 30 |
-
return re.sub(r'\n{3,}','\n\n','\n'.join(lines)).strip()
|
| 31 |
-
|
| 32 |
-
def _yt_html(handle,count=20):
|
| 33 |
-
try:
|
| 34 |
-
html=requests.get(f"https://www.youtube.com/@{handle}/shorts",headers=getattr(base,'HEADERS',{}),timeout=8).text
|
| 35 |
-
ids=[];out=[]
|
| 36 |
-
for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"',html):
|
| 37 |
-
vid=m.group(1)
|
| 38 |
-
if vid in ids:continue
|
| 39 |
-
ids.append(vid)
|
| 40 |
-
snip=html[max(0,m.start()-900):m.start()+1600]
|
| 41 |
-
title='YouTube Short'
|
| 42 |
-
mt=re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"',snip) or re.search(r'"accessibilityText":"([^"]+)"',snip)
|
| 43 |
-
if mt:title=clean(mt.group(1).replace('\\n',' '))
|
| 44 |
-
out.append({'id':vid,'title':title,'channel':handle,'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
|
| 45 |
-
if len(out)>=count:break
|
| 46 |
-
return out
|
| 47 |
-
except Exception:return []
|
| 48 |
-
|
| 49 |
-
def _yt_dlp(handle,count=20):
|
| 50 |
-
try:
|
| 51 |
-
import yt_dlp
|
| 52 |
-
opts={'quiet':True,'extract_flat':True,'skip_download':True,'playlistend':count,'ignoreerrors':True,'no_warnings':True,'socket_timeout':8}
|
| 53 |
-
with yt_dlp.YoutubeDL(opts) as ydl:info=ydl.extract_info(f"https://www.youtube.com/@{handle}/shorts",download=False)
|
| 54 |
-
out=[]
|
| 55 |
-
for e in (info or {}).get('entries') or []:
|
| 56 |
-
vid=e.get('id') or ''
|
| 57 |
-
if re.match(r'^[A-Za-z0-9_-]{11}$',vid):out.append({'id':vid,'title':e.get('title') or 'YouTube Short','channel':handle,'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
|
| 58 |
-
return out
|
| 59 |
-
except Exception:return []
|
| 60 |
-
|
| 61 |
-
def _fallback():
|
| 62 |
-
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'),('tvPewsc2ph4','Tính năng ẩn trên iPhone giúp giảm mỏi mắt | 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')]
|
| 63 |
-
return [{'id':v,'title':t,'channel':c,'link':'https://www.youtube.com/watch?v='+v,'img':'https://i.ytimg.com/vi/'+v+'/hqdefault.jpg','source':'yt'} for v,t,c in hard]
|
| 64 |
-
|
| 65 |
-
def _fresh_shorts():
|
| 66 |
-
seen=set();out=[]
|
| 67 |
-
for ch in SHORT_CHANNELS:
|
| 68 |
-
got=_yt_html(ch,24) or _yt_dlp(ch,24)
|
| 69 |
-
for v in got:
|
| 70 |
-
if v['id'] not in seen:seen.add(v['id']);out.append(v)
|
| 71 |
-
for v in _fallback():
|
| 72 |
-
if v['id'] not in seen:seen.add(v['id']);out.append(v)
|
| 73 |
-
return out[:50]
|
| 74 |
-
|
| 75 |
-
_PATCH={('/api/shorts','GET'),('/api/topic_post','POST'),('/','GET')}
|
| 76 |
-
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)]
|
| 77 |
-
|
| 78 |
-
@app.get('/api/shorts')
|
| 79 |
-
def api_shorts_fast(refresh:int=Query(default=0)):
|
| 80 |
-
now=time.time()
|
| 81 |
-
if _SHORTS_CACHE['d'] and now-_SHORTS_CACHE['t']<300 and not refresh:return JSONResponse(_SHORTS_CACHE['d'])
|
| 82 |
-
data=_fresh_shorts();_SHORTS_CACHE.update({'t':now,'d':data});return JSONResponse(data)
|
| 83 |
-
|
| 84 |
-
@app.post('/api/topic_post')
|
| 85 |
-
async def topic_post_clean(request:Request):
|
| 86 |
-
body=await request.json();topic=clean(body.get('topic',''))
|
| 87 |
-
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 88 |
-
try:img=base.pollinations_image_url(topic)
|
| 89 |
-
except Exception:img='https://image.pollinations.ai/prompt/'+quote('Vietnamese news editorial '+topic)+'?width=1024&height=576&nologo=true'
|
| 90 |
-
prompt=f"""Viết một bài báo/giải thích tiếng Việt hoàn chỉnh về chủ đề: {topic}
|
| 91 |
-
|
| 92 |
-
Hãy dùng kiến thức tổng hợp của bạn để tạo nội dung thật sự hữu ích, không liệt kê chỉ dẫn, không nhắc lại yêu cầu.
|
| 93 |
-
|
| 94 |
-
Đầu ra cần là bài viết hoàn chỉnh:
|
| 95 |
-
- Tiêu đề rõ và hấp dẫn.
|
| 96 |
-
- Sapo ngắn mở vấn đề.
|
| 97 |
-
- Các đoạn phân tích bối cảnh, ý nghĩa, tác động hoặc kiến thức cốt lõi.
|
| 98 |
-
- Nếu là thể thao, giải thích nhân vật/đội bóng/bối cảnh/lịch sử liên quan.
|
| 99 |
-
- Nếu là xã hội/công nghệ/giáo dục, giải thích bản chất, ví dụ, lợi ích/rủi ro.
|
| 100 |
-
- Không bịa số liệu thời sự mới; nếu không chắc hãy diễn đạt thận trọng.
|
| 101 |
-
- Cuối bài có mục Nguồn tham khảo ngắn.
|
| 102 |
-
"""
|
| 103 |
-
text=await base.qwen_generate(prompt,image_url=img,max_tokens=1600)
|
| 104 |
-
if not text:text=f"{topic}\n\n{topic} là một chủ đề đáng quan tâm, cần được nhìn từ bối cảnh, bản chất vấn đề và tác động thực tế. Nội dung này cung cấp phần giải thích tổng hợp để người đọc hiểu rõ hơn về chủ đề.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp."
|
| 105 |
-
text=_clean_ai_text(text)
|
| 106 |
-
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]
|
| 107 |
-
posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
|
| 108 |
-
return JSONResponse({'post':post})
|
| 109 |
-
|
| 110 |
-
PATCH_INJECT=r'''
|
| 111 |
-
<script>
|
| 112 |
-
(function(){
|
| 113 |
-
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 114 |
-
function cleanInstructionText(root=document){const bad=['Chỉ xuất bản bài viết cuối cùng','Không sao chép nguyên văn','Bài có tiêu đề, sapo','Nhiệm vụ: viết một bài báo tiếng Việt hoàn chỉnh'];root.querySelectorAll('.wall-text,.article-p,.rewrite-text').forEach(el=>{let lines=(el.textContent||'').split('\n').filter(l=>!bad.some(b=>l.includes(b)));el.textContent=lines.join('\n').trim();});}
|
| 115 |
-
function addTopicActions(){let art=document.querySelector('#view-article .article-view');if(!art)return;let actions=art.querySelector('.article-actions');if(!actions){actions=document.createElement('div');actions.className='article-actions';art.appendChild(actions)}if(!document.getElementById('topic-rewrite-btn')){let b=document.createElement('button');b.id='topic-rewrite-btn';b.className='primary';b.textContent='🤖 Rewrite AI & đăng tường';b.onclick=function(){alert('Bài chủ đề đã là nội dung AI. Bạn có thể tạo Short AI hoặc chia sẻ bài này.');};actions.appendChild(b)}if(!document.getElementById('topic-link-btn')){let b=document.createElement('button');b.id='topic-link-btn';b.textContent='🔗 Link bài';b.onclick=function(){navigator.clipboard.writeText(location.href).then(()=>alert('Đã sao chép link!')).catch(()=>{})};actions.appendChild(b)}}
|
| 116 |
-
async function refreshShortsFast(){let home=document.getElementById('view-home');if(!home)return;let data=await fetch('/api/shorts').then(r=>r.json()).catch(()=>[]);if(!data.length)return;let old=document.getElementById('shorts-fast-patch');if(old)old.remove();let wrap=document.createElement('div');wrap.id='shorts-fast-patch';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">Nhanh / mới</span></div><div class="slider-track">';data.slice(0,24).forEach((a,i)=>{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 ai=document.querySelector('.ai-compose');if(ai)ai.after(wrap);else home.prepend(wrap)}
|
| 117 |
-
setTimeout(()=>{cleanInstructionText();addTopicActions();refreshShortsFast();},1200);setInterval(()=>{cleanInstructionText();addTopicActions();},2000);
|
| 118 |
-
})();
|
| 119 |
-
</script>
|
| 120 |
-
'''
|
| 121 |
-
|
| 122 |
-
@app.get('/')
|
| 123 |
-
async def index_patch():
|
| 124 |
-
if hasattr(base_app,'index_final6'):
|
| 125 |
-
resp=await base_app.index_final6();html=resp.body.decode('utf-8') if hasattr(resp,'body') else str(resp)
|
| 126 |
-
else:
|
| 127 |
-
with open('/app/static/index.html','r',encoding='utf-8') as f:html=f.read()
|
| 128 |
-
return HTMLResponse(html.replace('</body>',PATCH_INJECT+'\n</body>') if '</body>' in html else html+PATCH_INJECT)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
restore_53ee7c5_patch.py
DELETED
|
@@ -1,134 +0,0 @@
|
|
| 1 |
-
import os, sys, re, json, time, requests
|
| 2 |
-
from urllib.parse import quote_plus
|
| 3 |
-
from huggingface_hub import snapshot_download
|
| 4 |
-
from fastapi import Request, Query
|
| 5 |
-
from fastapi.responses import JSONResponse, HTMLResponse
|
| 6 |
-
|
| 7 |
-
REVISION=os.environ.get('VNEWS_RESTORE_REVISION','53ee7c5')
|
| 8 |
-
REPO_ID=os.environ.get('VNEWS_REPO_ID','bep40/vnews')
|
| 9 |
-
|
| 10 |
-
snapshot_dir=snapshot_download(repo_id=REPO_ID,repo_type='space',revision=REVISION,local_dir='/tmp/vnews_restore_53ee7c5',local_dir_use_symlinks=False)
|
| 11 |
-
os.chdir(snapshot_dir);sys.path.insert(0,snapshot_dir)
|
| 12 |
-
|
| 13 |
-
# The 53ee7c5 app stack runs ai_runtime_final6:app in current lineage; import most complete app if available.
|
| 14 |
-
try:
|
| 15 |
-
import ai_runtime_final6 as runtime
|
| 16 |
-
except Exception:
|
| 17 |
-
try:
|
| 18 |
-
import ai_runtime_final5 as runtime
|
| 19 |
-
except Exception:
|
| 20 |
-
import ai_patch as runtime
|
| 21 |
-
app=runtime.app
|
| 22 |
-
|
| 23 |
-
try:
|
| 24 |
-
import ai_ext as base
|
| 25 |
-
except Exception:
|
| 26 |
-
base=None
|
| 27 |
-
|
| 28 |
-
# ---- Patch 1: Shorts Dân trí/SKĐS duplicate on homepage ----
|
| 29 |
-
# Cause: multiple injected slides can coexist. Frontend patch removes duplicate shorts blocks and keeps one.
|
| 30 |
-
SHORTS_FIX_JS=r'''
|
| 31 |
-
<script>
|
| 32 |
-
(function(){
|
| 33 |
-
function dedupeShorts(){
|
| 34 |
-
const blocks=[...document.querySelectorAll('.slider-wrap')].filter(w=>((w.querySelector('.slider-label')||{}).textContent||'').toLowerCase().includes('short'));
|
| 35 |
-
let seen=false;
|
| 36 |
-
blocks.forEach(b=>{ if(!seen){seen=true;b.id='shorts-single-home';} else b.remove(); });
|
| 37 |
-
}
|
| 38 |
-
function patchShortClicks(){
|
| 39 |
-
document.querySelectorAll('#shorts-single-home .slider-item').forEach((el,i)=>{
|
| 40 |
-
el.onclick=function(){ if(window.openTikTok) return window.openTikTok('shorts',i); };
|
| 41 |
-
});
|
| 42 |
-
}
|
| 43 |
-
setInterval(()=>{dedupeShorts();patchShortClicks();},1200);
|
| 44 |
-
setTimeout(()=>{dedupeShorts();patchShortClicks();},800);
|
| 45 |
-
})();
|
| 46 |
-
</script>
|
| 47 |
-
'''
|
| 48 |
-
|
| 49 |
-
# ---- Patch 2: Topic AI quality ----
|
| 50 |
-
def clean(s):
|
| 51 |
-
import html as html_lib
|
| 52 |
-
return re.sub(r'\s+',' ',html_lib.unescape(s or '')).strip()
|
| 53 |
-
|
| 54 |
-
def _image(topic):
|
| 55 |
-
if base and hasattr(base,'pollinations_image_url'):
|
| 56 |
-
try:return base.pollinations_image_url(topic)
|
| 57 |
-
except Exception:pass
|
| 58 |
-
return 'https://image.pollinations.ai/prompt/'+quote_plus('Vietnamese editorial illustration '+topic)+'?width=1024&height=576&nologo=true'
|
| 59 |
-
|
| 60 |
-
async def _llm(prompt,img=None):
|
| 61 |
-
if base and hasattr(base,'qwen_generate'):
|
| 62 |
-
try:
|
| 63 |
-
out=await base.qwen_generate(prompt,image_url=img,max_tokens=1800)
|
| 64 |
-
if out:return out
|
| 65 |
-
except Exception:pass
|
| 66 |
-
# HF router fallback: use strong Vietnamese-capable text model if token exists.
|
| 67 |
-
token=os.environ.get('HF_TOKEN') or os.environ.get('HUGGINGFACEHUB_API_TOKEN')
|
| 68 |
-
if token:
|
| 69 |
-
for model in ['Qwen/Qwen2.5-72B-Instruct','Qwen/Qwen2.5-32B-Instruct','Qwen/Qwen2.5-14B-Instruct','Qwen/Qwen2.5-7B-Instruct']:
|
| 70 |
-
try:
|
| 71 |
-
r=requests.post('https://router.huggingface.co/v1/chat/completions',headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'},json={'model':model,'messages':[{'role':'system','content':'Bạn là nhà báo tiếng Việt. Chỉ trả về bài viết hoàn chỉnh, không lặp lại prompt, không liệt kê yêu cầu.'},{'role':'user','content':prompt}],'temperature':0.55,'top_p':0.9,'max_tokens':1800},timeout=90)
|
| 72 |
-
if r.status_code<300:
|
| 73 |
-
txt=r.json().get('choices',[{}])[0].get('message',{}).get('content','').strip()
|
| 74 |
-
if txt:return txt
|
| 75 |
-
except Exception:pass
|
| 76 |
-
return ''
|
| 77 |
-
|
| 78 |
-
def _bad_prompt_echo(text):
|
| 79 |
-
low=(text or '').lower()
|
| 80 |
-
bad=['bạn là biên tập viên','hãy viết một bài','cấu trúc:','chủ đề:','yêu cầu:', '900-1400 từ']
|
| 81 |
-
return sum(1 for b in bad if b in low)>=2
|
| 82 |
-
|
| 83 |
-
async def topic_post_fixed(request:Request):
|
| 84 |
-
body=await request.json();topic=clean(body.get('topic',''))
|
| 85 |
-
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 86 |
-
img=_image(topic)
|
| 87 |
-
prompt=f'''Viết một bài báo tiếng Việt hoàn chỉnh về: {topic}
|
| 88 |
-
|
| 89 |
-
Chỉ trả về nội dung bài báo cuối cùng. Không nhắc lại yêu cầu, không ghi vai trò của bạn, không liệt kê prompt.
|
| 90 |
-
|
| 91 |
-
Bài cần có:
|
| 92 |
-
- Tiêu đề riêng do bạn tự đặt.
|
| 93 |
-
- Sapo ngắn nêu góc nhìn chính.
|
| 94 |
-
- Nội dung giàu thông tin dựa trên kiến thức thực tế và hiểu biết tổng hợp của bạn.
|
| 95 |
-
- Bối cảnh, giải thích khái niệm nếu cần, ví dụ minh họa, tác động và nhận định.
|
| 96 |
-
- Nếu thiếu dữ kiện thời sự mới, hãy diễn đạt thận trọng thay vì bịa số liệu.
|
| 97 |
-
- Văn phong báo chí phổ thông, tự nhiên, dễ đọc.
|
| 98 |
-
|
| 99 |
-
Quan trọng: Không được bắt đầu bằng câu như “Bạn là biên tập viên...” hoặc “Chủ đề: ...”. Không được bê nguyên văn hướng dẫn này vào bài.'''
|
| 100 |
-
text=await _llm(prompt,img)
|
| 101 |
-
if not text or _bad_prompt_echo(text):
|
| 102 |
-
# second pass to clean prompt echo
|
| 103 |
-
repair=f'''Dưới đây là một đầu ra lỗi vì lặp lại prompt hoặc viết dạng dàn ý. Hãy viết lại thành BÀI BÁO HOÀN CHỈNH về "{topic}". Chỉ trả về bài viết cuối cùng, không nhắc lại yêu cầu.
|
| 104 |
-
|
| 105 |
-
Đầu ra lỗi:
|
| 106 |
-
{text[:4000]}'''
|
| 107 |
-
text=await _llm(repair,img)
|
| 108 |
-
if not text or _bad_prompt_echo(text):
|
| 109 |
-
text=f'''{topic}: những điểm đáng chú ý cần biết\n\n{topic} là một chủ đề có nhiều lớp nghĩa và cần được nhìn từ bối cảnh thực tế, thay vì chỉ hiểu qua một vài định nghĩa ngắn. Khi phân tích chủ đề này, điều quan trọng là xem nó tác động ra sao đến đời sống, công nghệ, giáo dục, kinh tế hoặc văn hóa tùy từng trường hợp.\n\nĐiểm đáng chú ý đầu tiên là bản chất của vấn đề: người đọc cần hiểu khái niệm cốt lõi, cách nó vận hành và vì sao nó được nhắc đến nhiều. Tiếp đó là bối cảnh phát triển, bao gồm các xu hướng đang thúc đẩy chủ đề trở nên quan trọng hơn.\n\nỞ góc độ thực tế, {topic} thường gắn với cả cơ hội và rủi ro. Cơ hội nằm ở khả năng tạo ra giá trị mới, cải thiện hiệu quả hoặc mở ra cách tiếp cận khác. Rủi ro nằm ở việc hiểu sai, áp dụng máy móc hoặc bỏ qua những yếu tố xã hội và con người.\n\nVì vậy, thay vì nhìn {topic} như một khẩu hiệu, cần xem đây là một vấn đề cần được giải thích bằng ví dụ cụ thể, bằng bối cảnh và bằng những giới hạn rõ ràng. Cách tiếp cận thận trọng nhưng cởi mở sẽ giúp người đọc hiểu đúng hơn và có quyết định phù hợp hơn.'''
|
| 110 |
-
post={'id':str(int(time.time()*1000)),'title':topic,'text':text,'img':img,'url':'','kind':'topic_qwen','sources':[{'title':'Qwen / kiến thức tổng hợp','url':'','via':'Qwen'}],'images':[img],'ts':int(time.time())}
|
| 111 |
-
if base and hasattr(base,'_load_ai_wall'):
|
| 112 |
-
posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
|
| 113 |
-
return JSONResponse({'post':post})
|
| 114 |
-
|
| 115 |
-
# Remove existing topic route and root route, then patch.
|
| 116 |
-
app.router.routes=[r for r in app.router.routes if not (getattr(r,'path',None)=='/api/topic_post' and 'POST' in getattr(r,'methods',set()))]
|
| 117 |
-
app.add_api_route('/api/topic_post', topic_post_fixed, methods=['POST'])
|
| 118 |
-
|
| 119 |
-
# Root wrapper injects dedupe script.
|
| 120 |
-
old_root=None
|
| 121 |
-
for r in list(app.router.routes):
|
| 122 |
-
if getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set()):
|
| 123 |
-
old_root=r.endpoint
|
| 124 |
-
app.router.routes.remove(r)
|
| 125 |
-
break
|
| 126 |
-
|
| 127 |
-
@app.get('/')
|
| 128 |
-
async def patched_root():
|
| 129 |
-
if old_root:
|
| 130 |
-
resp=await old_root()
|
| 131 |
-
body=getattr(resp,'body',b'').decode('utf-8','ignore') if hasattr(resp,'body') else str(resp)
|
| 132 |
-
else:
|
| 133 |
-
with open('/tmp/vnews_restore_53ee7c5/static/index.html','r',encoding='utf-8') as f:body=f.read()
|
| 134 |
-
return HTMLResponse(body.replace('</body>',SHORTS_FIX_JS+'\n</body>') if '</body>' in body else body+SHORTS_FIX_JS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
restore_b9b6ba4_clean_runner.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import sys
|
| 3 |
-
from huggingface_hub import snapshot_download
|
| 4 |
-
|
| 5 |
-
REVISION = "b9b6ba4"
|
| 6 |
-
REPO_ID = os.environ.get("VNEWS_REPO_ID", "bep40/vnews")
|
| 7 |
-
|
| 8 |
-
snapshot_dir = snapshot_download(
|
| 9 |
-
repo_id=REPO_ID,
|
| 10 |
-
repo_type="space",
|
| 11 |
-
revision=REVISION,
|
| 12 |
-
local_dir="/tmp/vnews_b9b6ba4_clean",
|
| 13 |
-
)
|
| 14 |
-
|
| 15 |
-
os.chdir(snapshot_dir)
|
| 16 |
-
sys.path.insert(0, snapshot_dir)
|
| 17 |
-
|
| 18 |
-
# Run exactly the app used by revision b9b6ba4.
|
| 19 |
-
cmd = [
|
| 20 |
-
"uvicorn",
|
| 21 |
-
"ai_runtime_final6:app",
|
| 22 |
-
"--host",
|
| 23 |
-
"0.0.0.0",
|
| 24 |
-
"--port",
|
| 25 |
-
"7860",
|
| 26 |
-
]
|
| 27 |
-
os.execvp(cmd[0], cmd)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
restore_b9b6ba4_hotfix_runner.py
DELETED
|
@@ -1,173 +0,0 @@
|
|
| 1 |
-
import os, sys, re, json, time
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
from huggingface_hub import snapshot_download
|
| 4 |
-
|
| 5 |
-
REVISION = "b9b6ba4"
|
| 6 |
-
REPO_ID = os.environ.get("VNEWS_REPO_ID", "bep40/vnews")
|
| 7 |
-
BASE_DIR = "/tmp/vnews_b9b6ba4_hotfix"
|
| 8 |
-
|
| 9 |
-
snapshot_dir = snapshot_download(
|
| 10 |
-
repo_id=REPO_ID,
|
| 11 |
-
repo_type="space",
|
| 12 |
-
revision=REVISION,
|
| 13 |
-
local_dir=BASE_DIR,
|
| 14 |
-
local_dir_use_symlinks=False,
|
| 15 |
-
)
|
| 16 |
-
|
| 17 |
-
hotfix = r'''
|
| 18 |
-
"""Hotfix layer over b9b6ba4: clean topic posts, fast latest shorts, topic rewrite/share actions."""
|
| 19 |
-
import os, re, time, requests
|
| 20 |
-
from urllib.parse import quote, urlparse
|
| 21 |
-
import ai_runtime_final6 as base_app
|
| 22 |
-
from ai_runtime_final6 import app, base, rt, HTMLResponse, JSONResponse, Request, Query
|
| 23 |
-
try:
|
| 24 |
-
import main as main_mod
|
| 25 |
-
except Exception:
|
| 26 |
-
main_mod=None
|
| 27 |
-
|
| 28 |
-
_SHORTS_CACHE={"t":0,"d":[]}
|
| 29 |
-
CHANNELS=["baodantri7941","baosuckhoedoisongboyte"]
|
| 30 |
-
BAD_TOPIC_LINES=["Chỉ xuất bản bài viết cuối cùng","không nhắc lại yêu cầu","không liệt kê chỉ dẫn","Không sao chép nguyên văn","hãy tổng hợp và diễn đạt lại","Bài có tiêu đề","sapo","các đoạn phân tích","Nhiệm vụ: viết một bài báo tiếng Việt hoàn chỉnh"]
|
| 31 |
-
|
| 32 |
-
def clean(s):
|
| 33 |
-
import html as html_lib
|
| 34 |
-
return re.sub(r"\s+"," ",html_lib.unescape(s or "")).strip()
|
| 35 |
-
|
| 36 |
-
def _clean_topic_output(text):
|
| 37 |
-
if not text:return text
|
| 38 |
-
lines=[]
|
| 39 |
-
for ln in str(text).splitlines():
|
| 40 |
-
low=ln.strip().lower()
|
| 41 |
-
if any(b.lower() in low for b in BAD_TOPIC_LINES):continue
|
| 42 |
-
if low.startswith(('yêu cầu:', 'nhiệm vụ:', 'bắt buộc:', 'đầu ra:', 'chỉ xuất bản')):continue
|
| 43 |
-
lines.append(ln)
|
| 44 |
-
return re.sub(r'\n{3,}','\n\n','\n'.join(lines).strip())
|
| 45 |
-
|
| 46 |
-
def _topic_image(topic):
|
| 47 |
-
try:return base.pollinations_image_url(topic)
|
| 48 |
-
except Exception:return "https://image.pollinations.ai/prompt/"+quote("Vietnamese news editorial illustration "+topic)+"?width=1024&height=576&nologo=true"
|
| 49 |
-
|
| 50 |
-
def _fallback_shorts():
|
| 51 |
-
out=[];seen=set();c=[]
|
| 52 |
-
try:c+=(getattr(main_mod,'SHORTS_FALLBACK',[]) or [])
|
| 53 |
-
except Exception:pass
|
| 54 |
-
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'),('tvPewsc2ph4','Tính năng ẩn trên iPhone giúp giảm mỏi mắt | 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'),('IUOprcJyYr4','Phụ nữ táo bón có phải do lười ăn rau? | SKĐS','baosuckhoedoisongboyte')]
|
| 55 |
-
for vid,title,ch in hard:c.append({'id':vid,'title':title,'channel':ch})
|
| 56 |
-
for v in c:
|
| 57 |
-
vid=v.get('id')
|
| 58 |
-
if vid and vid not in seen:
|
| 59 |
-
seen.add(vid);out.append({'id':vid,'title':v.get('title','YouTube Short'),'channel':v.get('channel',''),'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
|
| 60 |
-
return out
|
| 61 |
-
|
| 62 |
-
def _yt_html(handle,count=18):
|
| 63 |
-
try:
|
| 64 |
-
html=requests.get(f"https://www.youtube.com/@{handle}/shorts",headers=getattr(base,'HEADERS',{}),timeout=7).text
|
| 65 |
-
ids=[];out=[]
|
| 66 |
-
for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"',html):
|
| 67 |
-
vid=m.group(1)
|
| 68 |
-
if vid in ids:continue
|
| 69 |
-
ids.append(vid)
|
| 70 |
-
snip=html[max(0,m.start()-900):m.start()+1600]
|
| 71 |
-
mt=re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"',snip) or re.search(r'"accessibilityText":"([^"]+)"',snip)
|
| 72 |
-
title=clean(mt.group(1).replace('\\n',' ')) if mt else 'YouTube Short'
|
| 73 |
-
out.append({'id':vid,'title':title,'channel':handle,'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
|
| 74 |
-
if len(out)>=count:break
|
| 75 |
-
return out
|
| 76 |
-
except Exception:return []
|
| 77 |
-
|
| 78 |
-
def _fresh_shorts_fast():
|
| 79 |
-
seen=set();out=[]
|
| 80 |
-
for ch in CHANNELS:
|
| 81 |
-
for v in _yt_html(ch,18):
|
| 82 |
-
if v['id'] not in seen:seen.add(v['id']);out.append(v)
|
| 83 |
-
for v in _fallback_shorts():
|
| 84 |
-
if v['id'] not in seen:seen.add(v['id']);out.append(v)
|
| 85 |
-
return out[:40]
|
| 86 |
-
|
| 87 |
-
_PATCH={('/api/shorts','GET'),('/api/topic_post','POST'),('/api/rewrite_topic','POST'),('/','GET')}
|
| 88 |
-
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)]
|
| 89 |
-
|
| 90 |
-
@app.get('/api/shorts')
|
| 91 |
-
def api_shorts_hotfix(refresh:int=Query(default=0)):
|
| 92 |
-
now=time.time()
|
| 93 |
-
if not refresh and _SHORTS_CACHE['d'] and now-_SHORTS_CACHE['t']<600:return JSONResponse(_SHORTS_CACHE['d'])
|
| 94 |
-
data=_fresh_shorts_fast();_SHORTS_CACHE.update({'t':now,'d':data});return JSONResponse(data)
|
| 95 |
-
|
| 96 |
-
@app.post('/api/topic_post')
|
| 97 |
-
async def topic_post_hotfix(request:Request):
|
| 98 |
-
body=await request.json();topic=clean(body.get('topic',''))
|
| 99 |
-
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 100 |
-
img=_topic_image(topic)
|
| 101 |
-
prompt=f"""Viết một bài báo tiếng Việt hoàn chỉnh, chất lượng cao về chủ đề: {topic}
|
| 102 |
-
|
| 103 |
-
Hãy sử dụng kiến thức tổng hợp của bạn để tạo nội dung có giá trị thực sự cho độc giả.
|
| 104 |
-
|
| 105 |
-
Phong cách:
|
| 106 |
-
- Như một bài báo/tạp chí đã hoàn thiện, không phải dàn ý.
|
| 107 |
-
- Không nhắc lại yêu cầu của người dùng.
|
| 108 |
-
- Không liệt kê chỉ dẫn viết bài.
|
| 109 |
-
- Không sao chép nguyên văn nguồn nào.
|
| 110 |
-
|
| 111 |
-
Nội dung cần có:
|
| 112 |
-
- Tiêu đề cụ thể.
|
| 113 |
-
- Sapo ngắn, hấp dẫn.
|
| 114 |
-
- Các đoạn phân tích bối cảnh, nguyên nhân, tác động, ví dụ và nhận định.
|
| 115 |
-
- Nếu chủ đề là thể thao như World Cup, hãy nói về ý nghĩa giải đấu, lịch sử, tác động tới bóng đá, đội tuyển/cầu thủ, kinh tế - truyền thông và cảm xúc người hâm mộ.
|
| 116 |
-
- Nếu thiếu dữ kiện thời sự mới, hãy diễn đạt thận trọng và tập trung vào kiến thức nền.
|
| 117 |
-
- Cuối bài có mục Nguồn tham khảo ngắn: Qwen2.5-VL / kiến thức tổng hợp.
|
| 118 |
-
"""
|
| 119 |
-
text=await base.qwen_generate(prompt,image_url=img,max_tokens=1800)
|
| 120 |
-
if not text:text=f"{topic}\n\n{topic} là một chủ đề có nhiều lớp ý nghĩa, từ bối cảnh lịch sử đến tác động xã hội, truyền thông và đời sống người hâm mộ. Bài viết này tổng hợp các kiến thức nền và những điểm đáng chú ý nhất để người đọc hiểu rõ hơn về chủ đề.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp."
|
| 121 |
-
text=_clean_topic_output(text)
|
| 122 |
-
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]
|
| 123 |
-
posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
|
| 124 |
-
return JSONResponse({'post':post})
|
| 125 |
-
|
| 126 |
-
@app.post('/api/rewrite_topic')
|
| 127 |
-
async def rewrite_topic(request:Request):
|
| 128 |
-
body=await request.json();post_id=str(body.get('id','')).strip();tone=clean(body.get('tone','báo chí phân tích'))
|
| 129 |
-
posts=base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==post_id),None)
|
| 130 |
-
if not p:return JSONResponse({'error':'post not found'},status_code=404)
|
| 131 |
-
prompt=f"""Viết lại bài sau theo phong cách {tone}. Chỉ xuất bản bản viết lại cuối cùng, không nhắc lại yêu cầu.
|
| 132 |
-
|
| 133 |
-
Tiêu đề: {p.get('title','')}
|
| 134 |
-
Nội dung:
|
| 135 |
-
{(p.get('text') or '')[:12000]}
|
| 136 |
-
"""
|
| 137 |
-
txt=await base.qwen_generate(prompt,image_url=p.get('img') or None,max_tokens=1600)
|
| 138 |
-
if not txt:txt=p.get('text','')
|
| 139 |
-
txt=_clean_topic_output(txt)
|
| 140 |
-
new=dict(p);new['id']=str(int(time.time()*1000));new['text']=txt;new['kind']='topic_rewrite';new['sources']=p.get('sources') or []
|
| 141 |
-
posts.insert(0,new);base._save_ai_wall(posts)
|
| 142 |
-
return JSONResponse({'post':new})
|
| 143 |
-
|
| 144 |
-
HOTFIX_INJECT=r'''
|
| 145 |
-
<style>.topic-actions-hotfix{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}.topic-actions-hotfix button{background:#2d8659;color:#fff;border:0;border-radius:12px;padding:8px 12px;font-size:11px}.shorts-fast-note{font-size:10px;color:#777}</style>
|
| 146 |
-
<script>
|
| 147 |
-
(function(){
|
| 148 |
-
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 149 |
-
function cleanTopicText(){document.querySelectorAll('.wall-text,.article-p,.rewrite-text').forEach(el=>{let t=el.textContent||'';let bad=['Chỉ xuất bản bài viết cuối cùng','Không sao chép nguyên văn','Bài có tiêu đề','Nhiệm vụ: viết một bài báo tiếng Việt hoàn chỉnh'];if(bad.some(b=>t.includes(b))){el.textContent=t.split('\n').filter(l=>!bad.some(b=>l.includes(b))).join('\n')}})}
|
| 150 |
-
function addTopicActions(){let art=document.querySelector('.article-view');if(!art||document.getElementById('topic-actions-hotfix'))return;let ptxt=art.innerText||'';if(ptxt.includes('Qwen2.5-VL')||ptxt.includes('Tường AI')){let box=document.createElement('div');box.id='topic-actions-hotfix';box.className='topic-actions-hotfix';box.innerHTML='<button onclick="rewriteVisibleTopic()">🤖 Rewrite AI & đăng tường</button><button onclick="shareVisibleTopic()">📤 Chia sẻ bài</button>';art.appendChild(box);}}
|
| 151 |
-
window.rewriteVisibleTopic=async function(){let title=document.querySelector('.article-title')?.textContent||'Bài AI';let posts=await fetch('/api/ai_wall').then(r=>r.json()).then(j=>j.posts||[]).catch(()=>[]);let p=posts.find(x=>x.title===title)||posts[0];if(!p)return alert('Không tìm thấy bài AI');let r=await fetch('/api/rewrite_topic',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:p.id,tone:'báo chí phân tích, mạch lạc, hấp dẫn'})});let j=await r.json();if(j.post){alert('Đã rewrite và đăng lên Tường AI');location.reload()}else alert(j.error||'Lỗi rewrite')}
|
| 152 |
-
window.shareVisibleTopic=function(){let title=document.querySelector('.article-title')?.textContent||document.title;let url=location.href;if(navigator.share)navigator.share({title,url}).catch(()=>{});else navigator.clipboard.writeText(url).then(()=>alert('Đã sao chép link!'))}
|
| 153 |
-
async function ensureFastShorts(){let home=document.getElementById('view-home');if(!home||document.getElementById('shorts-hotfix-fast'))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-hotfix-fast';wrap.className='slider-wrap';let h='<div class="slider-header"><span class="slider-label">📱 Shorts Dân trí & SKĐS</span><span class="shorts-fast-note">mới nhất / tải nhanh</span></div><div class="slider-track">';sh.slice(0,24).forEach((a,i)=>{h+=`<div class="slider-item shorts-item" onclick="openShortsHotfix(${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;(document.querySelector('.ai-compose')||home.firstChild).after(wrap);window.__hotfixShorts=sh;}
|
| 154 |
-
window.openShortsHotfix=function(start){let arts=window.__hotfixShorts||[];if(!arts.length)return alert('Chưa tải được Shorts');let ordered=start>0?arts.slice(start).concat(arts.slice(0,start)):arts;showView('view-tiktok');let h='<button class="back-btn" onclick="switchCat(\'home\')">← Shorts</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';ordered.forEach((v,i)=>{let src='https://www.youtube.com/embed/'+v.id+'?autoplay=1&rel=0&playsinline=1';h+=`<div class="tiktok-slide"><iframe src="${src}" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture"></iframe><div class="tiktok-bottom"><span class="badge badge-fpt">YT</span><p class="tiktok-title">${esc(v.title)}</p></div><span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`});h+='</div></div>';document.getElementById('view-tiktok').innerHTML=h;}
|
| 155 |
-
setInterval(()=>{cleanTopicText();addTopicActions();if(document.getElementById('view-home')?.classList.contains('active'))ensureFastShorts();},1200);setTimeout(ensureFastShorts,800);
|
| 156 |
-
})();
|
| 157 |
-
</script>
|
| 158 |
-
'''
|
| 159 |
-
|
| 160 |
-
@app.get('/')
|
| 161 |
-
async def index_hotfix():
|
| 162 |
-
html=base_app.f5.f4.f3.f2.f1._load_index_html()
|
| 163 |
-
body=getattr(base_app.rt.old,'PATCH_INJECT','')+HOTFIX_INJECT
|
| 164 |
-
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|
| 165 |
-
'''
|
| 166 |
-
|
| 167 |
-
Path(snapshot_dir, "b9_hotfix_app.py").write_text(hotfix, encoding="utf-8")
|
| 168 |
-
|
| 169 |
-
os.chdir(snapshot_dir)
|
| 170 |
-
sys.path.insert(0, snapshot_dir)
|
| 171 |
-
|
| 172 |
-
cmd = ["uvicorn", "b9_hotfix_app:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 173 |
-
os.execvp(cmd[0], cmd)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
restore_b9b6ba4_patch.py
DELETED
|
@@ -1,147 +0,0 @@
|
|
| 1 |
-
import os, sys, re, json, time, requests
|
| 2 |
-
from urllib.parse import quote_plus, urlparse
|
| 3 |
-
from huggingface_hub import snapshot_download
|
| 4 |
-
from fastapi import Request, Query
|
| 5 |
-
from fastapi.responses import JSONResponse, HTMLResponse
|
| 6 |
-
|
| 7 |
-
REVISION=os.environ.get('VNEWS_RESTORE_REVISION','b9b6ba4')
|
| 8 |
-
REPO_ID=os.environ.get('VNEWS_REPO_ID','bep40/vnews')
|
| 9 |
-
snapshot_dir=snapshot_download(repo_id=REPO_ID,repo_type='space',revision=REVISION,local_dir='/tmp/vnews_restore_b9b6ba4',local_dir_use_symlinks=False)
|
| 10 |
-
os.chdir(snapshot_dir);sys.path.insert(0,snapshot_dir)
|
| 11 |
-
|
| 12 |
-
try:
|
| 13 |
-
import ai_runtime_final6 as runtime
|
| 14 |
-
except Exception:
|
| 15 |
-
try:import ai_runtime_final5 as runtime
|
| 16 |
-
except Exception:import ai_patch as runtime
|
| 17 |
-
app=runtime.app
|
| 18 |
-
try:import ai_ext as base
|
| 19 |
-
except Exception:base=None
|
| 20 |
-
try:import main as main_mod
|
| 21 |
-
except Exception:main_mod=None
|
| 22 |
-
|
| 23 |
-
SPACE_URL='https://bep40-vnews.hf.space'
|
| 24 |
-
|
| 25 |
-
def clean(s):
|
| 26 |
-
import html as html_lib
|
| 27 |
-
return re.sub(r'\s+',' ',html_lib.unescape(s or '')).strip()
|
| 28 |
-
|
| 29 |
-
def _domain(u):
|
| 30 |
-
try:return urlparse(u or '').netloc.replace('www.','')
|
| 31 |
-
except Exception:return ''
|
| 32 |
-
|
| 33 |
-
def _image(topic):
|
| 34 |
-
if base and hasattr(base,'pollinations_image_url'):
|
| 35 |
-
try:return base.pollinations_image_url(topic)
|
| 36 |
-
except Exception:pass
|
| 37 |
-
return 'https://image.pollinations.ai/prompt/'+quote_plus('Vietnamese editorial article '+topic)+'?width=1024&height=576&nologo=true'
|
| 38 |
-
|
| 39 |
-
async def _llm_article(topic, context=''):
|
| 40 |
-
img=_image(topic)
|
| 41 |
-
# Important: use messages role separation and post-filter; never allow instruction echo.
|
| 42 |
-
prompt=f'''Viết một bài báo tiếng Việt hoàn chỉnh về chủ đề: {topic}
|
| 43 |
-
|
| 44 |
-
Hãy tổng hợp thành bài đọc tự nhiên, giàu thông tin, có bối cảnh, phân tích và ví dụ. Không nhắc lại nhiệm vụ, không liệt kê chỉ dẫn, không bắt đầu bằng "Nhiệm vụ" hay "Chủ đề".
|
| 45 |
-
|
| 46 |
-
Nếu có nguồn/bối cảnh bên dưới thì dùng để tham khảo và diễn đạt lại, không sao chép nguyên văn:
|
| 47 |
-
{context[:9000]}'''
|
| 48 |
-
out=''
|
| 49 |
-
if base and hasattr(base,'qwen_generate'):
|
| 50 |
-
try:out=await base.qwen_generate(prompt,image_url=img,max_tokens=1600) or ''
|
| 51 |
-
except Exception:out=''
|
| 52 |
-
token=os.environ.get('HF_TOKEN') or os.environ.get('HUGGINGFACEHUB_API_TOKEN')
|
| 53 |
-
if (not out or _looks_like_instruction_echo(out)) and token:
|
| 54 |
-
for model in ['Qwen/Qwen2.5-72B-Instruct','Qwen/Qwen2.5-32B-Instruct','Qwen/Qwen2.5-14B-Instruct','Qwen/Qwen2.5-7B-Instruct']:
|
| 55 |
-
try:
|
| 56 |
-
r=requests.post('https://router.huggingface.co/v1/chat/completions',headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'},json={'model':model,'messages':[{'role':'system','content':'Bạn là nhà báo tiếng Việt. Chỉ trả về bài viết cuối cùng, không lặp lại prompt hay chỉ dẫn.'},{'role':'user','content':prompt}],'temperature':0.55,'top_p':0.9,'max_tokens':1800},timeout=90)
|
| 57 |
-
if r.status_code<300:
|
| 58 |
-
out=r.json().get('choices',[{}])[0].get('message',{}).get('content','').strip()
|
| 59 |
-
if out and not _looks_like_instruction_echo(out):break
|
| 60 |
-
except Exception:pass
|
| 61 |
-
if _looks_like_instruction_echo(out):
|
| 62 |
-
out=_strip_instruction_echo(out)
|
| 63 |
-
if not out:
|
| 64 |
-
out=f'''{topic}: những điều đáng chú ý\n\n{topic} là một chủ đề cần được nhìn từ bối cảnh rộng hơn thay vì chỉ qua một định nghĩa ngắn. Khi phân tích, cần chú ý đến bản chất vấn đề, các yếu tố thúc đẩy, tác động thực tế và những giới hạn cần hiểu đúng.\n\nĐiểm quan trọng là chủ đề này không đứng riêng lẻ: nó thường liên quan đến các xu hướng xã hội, công nghệ, thể thao hoặc đời sống tùy từng ngữ cảnh. Người đọc vì vậy cần một cách tiếp cận cân bằng, vừa nhìn thấy cơ hội, vừa nhận diện rủi ro và các hiểu lầm phổ biến.\n\nTừ góc độ thực tế, {topic} có thể tạo ra thay đổi trong cách con người suy nghĩ, làm việc hoặc đánh giá một sự kiện. Điều cần tránh là nhìn nhận vấn đề bằng khẩu hiệu hoặc thông tin rời rạc; thay vào đó, cần đặt nó trong mối liên hệ với con người, dữ liệu, bối cảnh và hệ quả lâu dài.\n\nNguồn tham khảo: Qwen / kiến thức tổng hợp.'''
|
| 65 |
-
if 'Nguồn tham khảo' not in out:out+='\n\nNguồn tham khảo: Qwen / kiến thức tổng hợp.'
|
| 66 |
-
return out,img
|
| 67 |
-
|
| 68 |
-
def _looks_like_instruction_echo(text):
|
| 69 |
-
low=(text or '').lower()
|
| 70 |
-
bad=['nhiệm vụ:','viết một bài báo','chỉ xuất bản bài viết','không nhắc lại yêu cầu','không liệt kê chỉ dẫn','bạn là biên tập viên','cấu trúc:']
|
| 71 |
-
return sum(1 for b in bad if b in low)>=2
|
| 72 |
-
|
| 73 |
-
def _strip_instruction_echo(text):
|
| 74 |
-
lines=[]
|
| 75 |
-
for ln in (text or '').splitlines():
|
| 76 |
-
low=ln.lower().strip()
|
| 77 |
-
if any(x in low for x in ['nhiệm vụ:','chỉ xuất bản','không nhắc lại','không liệt kê','bạn là biên tập viên','cấu trúc:','• không sao chép']):
|
| 78 |
-
continue
|
| 79 |
-
if low.startswith('•') and ('chủ đề:' in low or 'bài có' in low):continue
|
| 80 |
-
lines.append(ln)
|
| 81 |
-
return '\n'.join(lines).strip()
|
| 82 |
-
|
| 83 |
-
# topic endpoint
|
| 84 |
-
app.router.routes=[r for r in app.router.routes if not (getattr(r,'path',None)=='/api/topic_post' and 'POST' in getattr(r,'methods',set()))]
|
| 85 |
-
@app.post('/api/topic_post')
|
| 86 |
-
async def topic_post(request:Request):
|
| 87 |
-
body=await request.json();topic=clean(body.get('topic',''))
|
| 88 |
-
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 89 |
-
text,img=await _llm_article(topic)
|
| 90 |
-
post={'id':str(int(time.time()*1000)),'title':topic,'text':text,'img':img,'url':'','kind':'topic_qwen','images':[img],'sources':[{'title':'Qwen / kiến thức tổng hợp','url':'','via':'Qwen'}],'ts':int(time.time())}
|
| 91 |
-
if base and hasattr(base,'_load_ai_wall'):
|
| 92 |
-
posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
|
| 93 |
-
return JSONResponse({'post':post})
|
| 94 |
-
|
| 95 |
-
@app.post('/api/topic_rewrite')
|
| 96 |
-
async def topic_rewrite(request:Request):
|
| 97 |
-
body=await request.json();post_id=str(body.get('id',''));topic=clean(body.get('topic',''));old=clean(body.get('text',''))
|
| 98 |
-
if not topic and not old:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 99 |
-
text,img=await _llm_article(topic or 'Bài viết',old)
|
| 100 |
-
return JSONResponse({'text':text,'img':img})
|
| 101 |
-
|
| 102 |
-
# shorts endpoint restore
|
| 103 |
-
app.router.routes=[r for r in app.router.routes if not (getattr(r,'path',None)=='/api/shorts' and 'GET' in getattr(r,'methods',set()))]
|
| 104 |
-
def _fallback_shorts():
|
| 105 |
-
out=[];seen=set();c=[]
|
| 106 |
-
try:c+=(getattr(main_mod,'SHORTS_FALLBACK',[]) or [])
|
| 107 |
-
except Exception:pass
|
| 108 |
-
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'),('tvPewsc2ph4','Tính năng ẩn trên iPhone giúp giảm mỏi mắt | 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'),('IUOprcJyYr4','Phụ nữ táo bón có phải do lười ăn rau? | SKĐS','baosuckhoedoisongboyte')]
|
| 109 |
-
for vid,title,ch in hard:c.append({'id':vid,'title':title,'channel':ch})
|
| 110 |
-
for v in c:
|
| 111 |
-
vid=v.get('id')
|
| 112 |
-
if vid and vid not in seen:
|
| 113 |
-
seen.add(vid);out.append({'id':vid,'title':v.get('title','YouTube Short'),'channel':v.get('channel',''),'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
|
| 114 |
-
return out
|
| 115 |
-
@app.get('/api/shorts')
|
| 116 |
-
def api_shorts(refresh:int=Query(default=0)):
|
| 117 |
-
# Prefer existing scraper from restored app if it works, then hard fallback.
|
| 118 |
-
try:
|
| 119 |
-
if hasattr(main_mod,'scrape_shorts'):
|
| 120 |
-
data=main_mod.scrape_shorts()
|
| 121 |
-
if data:return JSONResponse(data)
|
| 122 |
-
except Exception:pass
|
| 123 |
-
return JSONResponse(_fallback_shorts())
|
| 124 |
-
|
| 125 |
-
PATCH_JS=r'''
|
| 126 |
-
<script>
|
| 127 |
-
(function(){
|
| 128 |
-
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 129 |
-
function dedupeShorts(){const blocks=[...document.querySelectorAll('.slider-wrap')].filter(w=>((w.querySelector('.slider-label')||{}).textContent||'').toLowerCase().includes('short'));let kept=false;blocks.forEach(b=>{if(!kept){kept=true;b.id='shorts-single-home'}else b.remove()});}
|
| 130 |
-
async function ensureShorts(){let home=document.getElementById('view-home');if(!home)return;dedupeShorts();if(document.getElementById('shorts-single-home'))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-single-home';wrap.className='slider-wrap';let h='<div class="slider-header"><span class="slider-label">📱 Shorts Dân trí & SKĐS</span></div><div class="slider-track">';sh.forEach((a,i)=>{h+=`<div class="slider-item shorts-item" onclick="openTikTok('shorts',${i})"><div class="slider-thumb shorts-thumb"><img src="${a.img}"><div class="card-play">▶</div></div><div class="slider-title">${esc(a.title)}</div></div>`});h+='</div>';wrap.innerHTML=h;(document.querySelector('.ai-compose')||home.firstChild).after(wrap);}
|
| 131 |
-
window.rewriteTopicPost=async function(id,title,text){let r=await fetch('/api/topic_rewrite',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,topic:title,text})});let j=await r.json();if(j.text){alert('Đã rewrite bài chủ đề');location.reload();}else alert(j.error||'Lỗi rewrite')};
|
| 132 |
-
function addRewriteButtons(){document.querySelectorAll('.wall-item').forEach(item=>{if(item.querySelector('.topic-rewrite-btn'))return;let title=item.querySelector('.wall-title')?.textContent||'';let txt=item.querySelector('.wall-text')?.textContent||'';let actions=item.querySelector('.wall-actions');if(actions){let b=document.createElement('button');b.className='topic-rewrite-btn';b.textContent='Rewrite';b.onclick=function(e){e.stopPropagation();rewriteTopicPost('',title,txt)};actions.appendChild(b);}})}
|
| 133 |
-
setInterval(()=>{dedupeShorts();ensureShorts();addRewriteButtons();},1500);setTimeout(()=>{dedupeShorts();ensureShorts();addRewriteButtons();},800);
|
| 134 |
-
})();
|
| 135 |
-
</script>
|
| 136 |
-
'''
|
| 137 |
-
|
| 138 |
-
old_root=None
|
| 139 |
-
for r in list(app.router.routes):
|
| 140 |
-
if getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set()):old_root=r.endpoint;app.router.routes.remove(r);break
|
| 141 |
-
@app.get('/')
|
| 142 |
-
async def root():
|
| 143 |
-
if old_root:
|
| 144 |
-
resp=await old_root();body=getattr(resp,'body',b'').decode('utf-8','ignore') if hasattr(resp,'body') else str(resp)
|
| 145 |
-
else:
|
| 146 |
-
with open('/tmp/vnews_restore_b9b6ba4/static/index.html','r',encoding='utf-8') as f:body=f.read()
|
| 147 |
-
return HTMLResponse(body.replace('</body>',PATCH_JS+'\n</body>') if '</body>' in body else body+PATCH_JS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
restore_b9b6ba4_runner.py
DELETED
|
@@ -1,195 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import sys
|
| 3 |
-
from huggingface_hub import snapshot_download
|
| 4 |
-
|
| 5 |
-
REVISION = "b9b6ba4"
|
| 6 |
-
REPO_ID = os.environ.get("VNEWS_REPO_ID", "bep40/vnews")
|
| 7 |
-
|
| 8 |
-
snapshot_dir = snapshot_download(
|
| 9 |
-
repo_id=REPO_ID,
|
| 10 |
-
repo_type="space",
|
| 11 |
-
revision=REVISION,
|
| 12 |
-
local_dir="/tmp/vnews_b9b6ba4_restore",
|
| 13 |
-
local_dir_use_symlinks=False,
|
| 14 |
-
)
|
| 15 |
-
|
| 16 |
-
os.chdir(snapshot_dir)
|
| 17 |
-
sys.path.insert(0, snapshot_dir)
|
| 18 |
-
|
| 19 |
-
# Runtime patch is written into the restored snapshot so the restored UI is used
|
| 20 |
-
# (no placeholder index.html), while only the requested fixes are applied.
|
| 21 |
-
patch_code = r'''
|
| 22 |
-
import re, time, json, asyncio
|
| 23 |
-
from fastapi import Request, Query
|
| 24 |
-
from fastapi.responses import JSONResponse, HTMLResponse
|
| 25 |
-
import ai_runtime_final6 as m
|
| 26 |
-
|
| 27 |
-
app = m.app
|
| 28 |
-
_SHORTS_CACHE = {"t": 0, "d": []}
|
| 29 |
-
BAD_LINES = [
|
| 30 |
-
'Chỉ xuất bản bài viết cuối cùng',
|
| 31 |
-
'không nhắc lại yêu cầu',
|
| 32 |
-
'không liệt kê chỉ dẫn',
|
| 33 |
-
'Không sao chép nguyên văn',
|
| 34 |
-
'Bài có tiêu đề, sapo',
|
| 35 |
-
'Nhiệm vụ: viết một bài báo tiếng Việt hoàn chỉnh',
|
| 36 |
-
]
|
| 37 |
-
|
| 38 |
-
def _clean_topic_text(txt):
|
| 39 |
-
txt = str(txt or '')
|
| 40 |
-
out = []
|
| 41 |
-
for line in txt.splitlines():
|
| 42 |
-
s = line.strip()
|
| 43 |
-
if not s:
|
| 44 |
-
out.append(line); continue
|
| 45 |
-
low = s.lower()
|
| 46 |
-
if any(x.lower() in low for x in BAD_LINES):
|
| 47 |
-
continue
|
| 48 |
-
if re.match(r'^nhiệm vụ\s*:', low):
|
| 49 |
-
continue
|
| 50 |
-
# Drop prompt-like bullet instructions that accidentally leak into output.
|
| 51 |
-
if re.match(r'^[•\-*]\s*(chỉ xuất bản|không sao chép|bài có tiêu đề|nhiệm vụ)', low):
|
| 52 |
-
continue
|
| 53 |
-
out.append(line)
|
| 54 |
-
txt = '\n'.join(out)
|
| 55 |
-
txt = re.sub(r'\n{3,}', '\n\n', txt).strip()
|
| 56 |
-
return txt
|
| 57 |
-
|
| 58 |
-
def _fallback_shorts():
|
| 59 |
-
out=[];seen=set()
|
| 60 |
-
try:
|
| 61 |
-
for v in getattr(m.f5.rt if hasattr(m,'f5') else m, 'SHORTS_FALLBACK', []) or []:
|
| 62 |
-
vid=v.get('id')
|
| 63 |
-
if vid and vid not in seen:
|
| 64 |
-
seen.add(vid);out.append(v)
|
| 65 |
-
except Exception:
|
| 66 |
-
pass
|
| 67 |
-
hard=[
|
| 68 |
-
('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'),
|
| 69 |
-
('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'),
|
| 70 |
-
('tvPewsc2ph4','Tính năng ẩn trên iPhone giúp giảm mỏi mắt | Dân trí','baodantri7941'),
|
| 71 |
-
('7Pd6vZ2Lz1M','Hành động ấm lòng trong tìm kiếm học sinh tử vong ở sông Lô | SKĐS','baosuckhoedoisongboyte'),
|
| 72 |
-
('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'),
|
| 73 |
-
('IUOprcJyYr4','Phụ nữ táo bón có phải do lười ăn rau? | SKĐS','baosuckhoedoisongboyte')
|
| 74 |
-
]
|
| 75 |
-
for vid,title,ch in hard:
|
| 76 |
-
if vid not in seen:
|
| 77 |
-
seen.add(vid);out.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'})
|
| 78 |
-
return out
|
| 79 |
-
|
| 80 |
-
def _fresh_shorts_fast():
|
| 81 |
-
seen=set();out=[]
|
| 82 |
-
handles=getattr(m, 'YOUTUBE_HANDLES', ['baodantri7941','baosuckhoedoisongboyte'])
|
| 83 |
-
for h in handles:
|
| 84 |
-
got=[]
|
| 85 |
-
try: got = m._yt_html(h, 24) or []
|
| 86 |
-
except Exception: got=[]
|
| 87 |
-
# yt-dlp can be slower; only use if html returns too few.
|
| 88 |
-
if len(got)<4:
|
| 89 |
-
try: got = (m._yt_ytdlp(h, 24) or []) + got
|
| 90 |
-
except Exception: pass
|
| 91 |
-
for v in got:
|
| 92 |
-
vid=v.get('id') or ''
|
| 93 |
-
if vid and vid not in seen:
|
| 94 |
-
seen.add(vid);out.append(v)
|
| 95 |
-
for v in _fallback_shorts():
|
| 96 |
-
vid=v.get('id') or ''
|
| 97 |
-
if vid and vid not in seen:
|
| 98 |
-
seen.add(vid);out.append(v)
|
| 99 |
-
return out[:50]
|
| 100 |
-
|
| 101 |
-
# Remove only routes we override.
|
| 102 |
-
_PATCH={('/api/topic_post','POST'),('/api/shorts','GET'),('/','GET')}
|
| 103 |
-
app.router.routes=[r for r in app.router.routes if not any(getattr(r,'path',None)==p and method in getattr(r,'methods',set()) for p,method in _PATCH)]
|
| 104 |
-
|
| 105 |
-
@app.get('/api/shorts')
|
| 106 |
-
def api_shorts_patch(refresh:int=Query(default=0)):
|
| 107 |
-
now=time.time()
|
| 108 |
-
if not refresh and _SHORTS_CACHE['d'] and now-_SHORTS_CACHE['t']<600:
|
| 109 |
-
return JSONResponse(_SHORTS_CACHE['d'])
|
| 110 |
-
data=_fresh_shorts_fast()
|
| 111 |
-
_SHORTS_CACHE.update({'t':now,'d':data})
|
| 112 |
-
return JSONResponse(data)
|
| 113 |
-
|
| 114 |
-
@app.post('/api/topic_post')
|
| 115 |
-
async def topic_post_patch(request:Request):
|
| 116 |
-
body=await request.json(); topic=m.clean(body.get('topic',''))
|
| 117 |
-
if not topic: return JSONResponse({'error':'missing topic'},status_code=400)
|
| 118 |
-
img=m._topic_image(topic)
|
| 119 |
-
# Use fast context if available; fall back safely.
|
| 120 |
-
try:
|
| 121 |
-
research=m._fast_context(topic)
|
| 122 |
-
except Exception:
|
| 123 |
-
research=m._web_research_context(topic)
|
| 124 |
-
context=research.get('context',''); sources=research.get('sources',[])
|
| 125 |
-
source_brief=context[:18000]
|
| 126 |
-
prompt=f'''Bạn là biên tập viên VNEWS. Hãy viết một bài báo tiếng Việt hoàn chỉnh về chủ đề: "{topic}" dựa trên dữ liệu nguồn bên dưới.
|
| 127 |
-
|
| 128 |
-
DỮ LIỆU NGUỒN:
|
| 129 |
-
{source_brief}
|
| 130 |
-
|
| 131 |
-
YÊU CẦU NỘI DUNG:
|
| 132 |
-
- Viết trực tiếp bài cuối cùng, không nhắc lại yêu cầu hay chỉ dẫn.
|
| 133 |
-
- Tổng hợp và diễn đạt lại bằng ngôn ngữ báo chí tự nhiên.
|
| 134 |
-
- Có tiêu đề mới, sapo ngắn, các đoạn bối cảnh/phân tích/tác động.
|
| 135 |
-
- Không sao chép nguyên văn nguồn.
|
| 136 |
-
- Cuối bài có mục Nguồn tham khảo ngắn.
|
| 137 |
-
'''
|
| 138 |
-
try:
|
| 139 |
-
text=await asyncio.wait_for(m.f5.base.qwen_generate(prompt,image_url=img,max_tokens=1700),timeout=35)
|
| 140 |
-
except Exception:
|
| 141 |
-
text=None
|
| 142 |
-
if not text or len(str(text))<300:
|
| 143 |
-
vias=', '.join(sorted({s.get('via','') for s in sources if s.get('via')}))
|
| 144 |
-
titles='\n'.join('• '+s.get('title','') for s in sources[:6])
|
| 145 |
-
text=f'{topic}: những điểm đáng chú ý\n\n{topic} đang thu hút sự quan tâm qua nhiều nguồn tin. Tổng hợp nhanh cho thấy chủ đề này cần được nhìn từ bối cảnh, tác động và những diễn biến liên quan.\n\n{titles}\n\nNguồn tham khảo: {vias}'
|
| 146 |
-
text=_clean_topic_text(text)
|
| 147 |
-
src=[s for s in sources if s.get('url')]
|
| 148 |
-
post=m.f5.base.make_post(topic,text,img,'','topic_cleaned_b9b6ba4',sources=src)
|
| 149 |
-
post['images']=[img]
|
| 150 |
-
posts=m.f5.base._load_ai_wall(); posts.insert(0,post); m.f5.base._save_ai_wall(posts)
|
| 151 |
-
return JSONResponse({'post':post})
|
| 152 |
-
|
| 153 |
-
EXTRA_JS = r'''
|
| 154 |
-
<style>
|
| 155 |
-
.topic-extra-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.topic-extra-actions button{background:#2d8659;border:0;color:#fff;border-radius:12px;padding:8px 12px;font-size:12px}.topic-extra-actions button.secondary{background:#222;border:1px solid #444}.topic-source-links{margin-top:10px;background:#151515;border:1px solid #2b2b2b;border-radius:10px;padding:10px}.topic-source-links a{display:block;color:#5cb87a;font-size:12px;margin:5px 0;text-decoration:none}.shorts-b9-wrap{margin:6px 4px;background:#1a1a1a;border:1px solid #2a2a2a;border-radius:8px;overflow:hidden}
|
| 156 |
-
</style>
|
| 157 |
-
<script>
|
| 158 |
-
(function(){
|
| 159 |
-
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 160 |
-
async function ensureFastShorts(){let home=document.getElementById('view-home');if(!home||document.getElementById('shorts-b9-fast'))return;let data=await fetch('/api/shorts').then(r=>r.json()).catch(()=>[]);if(!data.length)return;let wrap=document.createElement('div');wrap.id='shorts-b9-fast';wrap.className='shorts-b9-wrap';let h='<div class="slider-header"><span class="slider-label">📱 Shorts Dân trí & SKĐS</span><span class="slider-note">Tải nhanh</span></div><div class="slider-track">';data.slice(0,24).forEach((a,i)=>{h+=`<div class="slider-item shorts-item" onclick="openTikTok('shorts',${i})"><div class="slider-thumb shorts-thumb">${a.img?`<img src="${esc(a.img)}">`:''}<div class="card-play">▶</div></div><div class="slider-title">${esc(a.title)}</div></div>`});h+='</div>';wrap.innerHTML=h;let comp=document.querySelector('.ai-compose')||home.firstChild;if(comp)comp.after(wrap);else home.prepend(wrap);}
|
| 161 |
-
function addTopicActions(){let art=document.querySelector('#view-article.active .article-view');if(!art||document.getElementById('topic-extra-actions'))return;let text=art.innerText||'';if(!/Nguồn tham khảo|Qwen|Tường AI|AI/i.test(text))return;let box=document.createElement('div');box.id='topic-extra-actions';box.className='topic-extra-actions';box.innerHTML='<button onclick="rewriteTopicArticleB9()">🤖 Rewrite AI & đăng tường</button><button class="secondary" onclick="openFirstTopicSourceB9()">🔗 Mở nguồn đầu tiên</button><div id="topic-source-links" class="topic-source-links" style="display:none"></div>';art.appendChild(box);}
|
| 162 |
-
window.rewriteTopicArticleB9=function(){let title=document.querySelector('.article-title')?.innerText||'Bài chủ đề';let content=document.querySelector('.article-view')?.innerText||'';let payload={topic:title+'\n'+content.slice(0,2500)};fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}).then(r=>r.json()).then(j=>{if(j.post)alert('Đã rewrite AI và đăng lại lên Tường AI');else alert(j.error||'Lỗi rewrite')}).catch(e=>alert(e.message));};
|
| 163 |
-
window.openFirstTopicSourceB9=function(){let a=document.querySelector('.article-summary a,.source-detail-box a,.topic-source-links a');if(a)a.click();else alert('Bài này chưa có link nguồn hiển thị.');};
|
| 164 |
-
setInterval(()=>{ensureFastShorts();addTopicActions();},1500);setTimeout(()=>{ensureFastShorts();addTopicActions();},800);
|
| 165 |
-
})();
|
| 166 |
-
</script>
|
| 167 |
-
'''
|
| 168 |
-
|
| 169 |
-
@app.get('/')
|
| 170 |
-
async def index_patch():
|
| 171 |
-
# Call restored final6 root if available to keep 100% restored UI and avoid placeholder.
|
| 172 |
-
try:
|
| 173 |
-
resp=await m.index_final6_fast_home()
|
| 174 |
-
except Exception:
|
| 175 |
-
try: resp=await m.index_final6()
|
| 176 |
-
except Exception:
|
| 177 |
-
html=m.f5.f4.f3.f2.f1._load_index_html()
|
| 178 |
-
return HTMLResponse(html.replace('</body>',EXTRA_JS+'</body>') if '</body>' in html else html+EXTRA_JS)
|
| 179 |
-
html=resp.body.decode('utf-8') if hasattr(resp,'body') else str(resp)
|
| 180 |
-
return HTMLResponse(html.replace('</body>',EXTRA_JS+'\n</body>') if '</body>' in html else html+EXTRA_JS)
|
| 181 |
-
'''
|
| 182 |
-
|
| 183 |
-
patch_path=os.path.join(snapshot_dir,'b9b6ba4_safe_patch.py')
|
| 184 |
-
with open(patch_path,'w',encoding='utf-8') as f:
|
| 185 |
-
f.write(patch_code)
|
| 186 |
-
|
| 187 |
-
cmd = [
|
| 188 |
-
"uvicorn",
|
| 189 |
-
"b9b6ba4_safe_patch:app",
|
| 190 |
-
"--host",
|
| 191 |
-
"0.0.0.0",
|
| 192 |
-
"--port",
|
| 193 |
-
"7860",
|
| 194 |
-
]
|
| 195 |
-
os.execvp(cmd[0], cmd)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
restore_to_b9b6ba4.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import sys
|
| 3 |
-
from huggingface_hub import snapshot_download
|
| 4 |
-
|
| 5 |
-
REVISION = os.environ.get("VNEWS_RESTORE_REVISION", "b9b6ba4")
|
| 6 |
-
REPO_ID = os.environ.get("VNEWS_REPO_ID", "bep40/vnews")
|
| 7 |
-
|
| 8 |
-
snapshot_dir = snapshot_download(
|
| 9 |
-
repo_id=REPO_ID,
|
| 10 |
-
repo_type="space",
|
| 11 |
-
revision=REVISION,
|
| 12 |
-
local_dir="/tmp/vnews_restore_b9b6ba4",
|
| 13 |
-
local_dir_use_symlinks=False,
|
| 14 |
-
)
|
| 15 |
-
|
| 16 |
-
os.chdir(snapshot_dir)
|
| 17 |
-
sys.path.insert(0, snapshot_dir)
|
| 18 |
-
|
| 19 |
-
cmd = [
|
| 20 |
-
"uvicorn",
|
| 21 |
-
"ai_runtime_final6:app",
|
| 22 |
-
"--host",
|
| 23 |
-
"0.0.0.0",
|
| 24 |
-
"--port",
|
| 25 |
-
"7860",
|
| 26 |
-
]
|
| 27 |
-
os.execvp(cmd[0], cmd)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
safe_app.py
DELETED
|
@@ -1,104 +0,0 @@
|
|
| 1 |
-
"""Safe launcher for VNEWS.
|
| 2 |
-
|
| 3 |
-
This module is intentionally small and defensive: it always defines a FastAPI
|
| 4 |
-
`app`, even if restoring/importing the main VNEWS runtime fails. This prevents
|
| 5 |
-
Hugging Face Spaces from showing: "Your space is in error".
|
| 6 |
-
|
| 7 |
-
Update strategy:
|
| 8 |
-
- The stable base is loaded from a known-good revision (default: b9b6ba4).
|
| 9 |
-
- Optional local safe_patch.py can apply small guarded updates via apply(app).
|
| 10 |
-
- Any exception is caught and exposed on /health and the fallback homepage.
|
| 11 |
-
"""
|
| 12 |
-
import os
|
| 13 |
-
import sys
|
| 14 |
-
import traceback
|
| 15 |
-
from pathlib import Path
|
| 16 |
-
|
| 17 |
-
from fastapi import FastAPI
|
| 18 |
-
from fastapi.responses import HTMLResponse, JSONResponse
|
| 19 |
-
|
| 20 |
-
ERROR_LOG = []
|
| 21 |
-
REVISION = os.environ.get("VNEWS_SAFE_REVISION", "b9b6ba4")
|
| 22 |
-
REPO_ID = os.environ.get("VNEWS_REPO_ID", "bep40/vnews")
|
| 23 |
-
SNAPSHOT_DIR = os.environ.get("VNEWS_SNAPSHOT_DIR", "/tmp/vnews_safe_snapshot")
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def _fallback_app(reason: str = ""):
|
| 27 |
-
app = FastAPI()
|
| 28 |
-
|
| 29 |
-
@app.get("/")
|
| 30 |
-
async def fallback_index():
|
| 31 |
-
details = "<br>".join(ERROR_LOG[-8:])
|
| 32 |
-
return HTMLResponse(f"""
|
| 33 |
-
<!doctype html><html lang="vi"><head>
|
| 34 |
-
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 35 |
-
<title>VNEWS Safe Mode</title>
|
| 36 |
-
<style>body{{background:#111;color:#eee;font-family:system-ui;padding:18px;line-height:1.5}}.box{{max-width:760px;margin:auto;background:#1a1a1a;border:1px solid #333;border-radius:12px;padding:16px}}h1{{color:#5cb87a}}code{{color:#f0c040;word-break:break-all}}</style>
|
| 37 |
-
</head><body><div class="box">
|
| 38 |
-
<h1>📰 VNEWS đang ở chế độ an toàn</h1>
|
| 39 |
-
<p>Space đã khởi động thành công, không còn lỗi crash toàn bộ.</p>
|
| 40 |
-
<p>Runtime chính chưa tải được. Hãy kiểm tra <code>/health</code> để xem nguyên nhân.</p>
|
| 41 |
-
<p><b>Revision:</b> <code>{REVISION}</code></p>
|
| 42 |
-
<p><b>Lý do:</b> <code>{reason or 'unknown'}</code></p>
|
| 43 |
-
<details><summary>Chi tiết lỗi</summary><pre>{details}</pre></details>
|
| 44 |
-
</div></body></html>
|
| 45 |
-
""")
|
| 46 |
-
|
| 47 |
-
@app.get("/health")
|
| 48 |
-
async def health():
|
| 49 |
-
return JSONResponse({
|
| 50 |
-
"ok": False,
|
| 51 |
-
"mode": "fallback",
|
| 52 |
-
"revision": REVISION,
|
| 53 |
-
"errors": ERROR_LOG[-20:],
|
| 54 |
-
})
|
| 55 |
-
|
| 56 |
-
return app
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def _build_app():
|
| 60 |
-
try:
|
| 61 |
-
from huggingface_hub import snapshot_download
|
| 62 |
-
snap = snapshot_download(
|
| 63 |
-
repo_id=REPO_ID,
|
| 64 |
-
repo_type="space",
|
| 65 |
-
revision=REVISION,
|
| 66 |
-
local_dir=SNAPSHOT_DIR,
|
| 67 |
-
local_dir_use_symlinks=False,
|
| 68 |
-
)
|
| 69 |
-
os.chdir(snap)
|
| 70 |
-
# Put restored snapshot first so imports resolve exactly as in the stable revision.
|
| 71 |
-
if snap not in sys.path:
|
| 72 |
-
sys.path.insert(0, snap)
|
| 73 |
-
# Keep current /app available for optional safe_patch.py.
|
| 74 |
-
if "/app" not in sys.path:
|
| 75 |
-
sys.path.append("/app")
|
| 76 |
-
|
| 77 |
-
# b9b6ba4's known-good Docker command used ai_runtime_final6:app.
|
| 78 |
-
import ai_runtime_final6 as runtime
|
| 79 |
-
restored_app = runtime.app
|
| 80 |
-
|
| 81 |
-
# Optional guarded patch. This must never be allowed to crash startup.
|
| 82 |
-
try:
|
| 83 |
-
import safe_patch
|
| 84 |
-
if hasattr(safe_patch, "apply"):
|
| 85 |
-
safe_patch.apply(restored_app)
|
| 86 |
-
except Exception:
|
| 87 |
-
ERROR_LOG.append("safe_patch failed:\n" + traceback.format_exc())
|
| 88 |
-
|
| 89 |
-
@restored_app.get("/health")
|
| 90 |
-
async def health():
|
| 91 |
-
return JSONResponse({
|
| 92 |
-
"ok": True,
|
| 93 |
-
"mode": "restored",
|
| 94 |
-
"revision": REVISION,
|
| 95 |
-
"patch_errors": ERROR_LOG[-10:],
|
| 96 |
-
})
|
| 97 |
-
|
| 98 |
-
return restored_app
|
| 99 |
-
except Exception as e:
|
| 100 |
-
ERROR_LOG.append(traceback.format_exc())
|
| 101 |
-
return _fallback_app(str(e))
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
app = _build_app()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
safe_patch.py
DELETED
|
@@ -1,223 +0,0 @@
|
|
| 1 |
-
"""Guarded hotfixes for VNEWS Safe App.
|
| 2 |
-
|
| 3 |
-
Never raise during import/apply. All patches are best-effort.
|
| 4 |
-
"""
|
| 5 |
-
import re
|
| 6 |
-
import time
|
| 7 |
-
import requests
|
| 8 |
-
from urllib.parse import quote
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def apply(app):
|
| 12 |
-
try:
|
| 13 |
-
from fastapi import Request, Query
|
| 14 |
-
from fastapi.responses import JSONResponse, HTMLResponse
|
| 15 |
-
import ai_ext as base
|
| 16 |
-
except Exception:
|
| 17 |
-
return
|
| 18 |
-
|
| 19 |
-
shorts_cache = {"t": 0, "d": []}
|
| 20 |
-
channels = ["baodantri7941", "baosuckhoedoisongboyte"]
|
| 21 |
-
|
| 22 |
-
def clean(s):
|
| 23 |
-
import html as html_lib
|
| 24 |
-
return re.sub(r"\s+", " ", html_lib.unescape(s or "")).strip()
|
| 25 |
-
|
| 26 |
-
bad_topic_lines = [
|
| 27 |
-
"Chỉ xuất bản bài viết cuối cùng",
|
| 28 |
-
"không nhắc lại yêu cầu",
|
| 29 |
-
"không liệt kê chỉ dẫn",
|
| 30 |
-
"Không sao chép nguyên văn",
|
| 31 |
-
"hãy tổng hợp và diễn đạt lại",
|
| 32 |
-
"Bài có tiêu đề",
|
| 33 |
-
"sapo",
|
| 34 |
-
"các đoạn phân tích",
|
| 35 |
-
"Nhiệm vụ: viết một bài báo tiếng Việt hoàn chỉnh",
|
| 36 |
-
]
|
| 37 |
-
|
| 38 |
-
def clean_topic_output(text):
|
| 39 |
-
if not text:
|
| 40 |
-
return text
|
| 41 |
-
lines = []
|
| 42 |
-
for ln in str(text).splitlines():
|
| 43 |
-
low = ln.strip().lower()
|
| 44 |
-
if any(b.lower() in low for b in bad_topic_lines):
|
| 45 |
-
continue
|
| 46 |
-
if low.startswith(("yêu cầu:", "nhiệm vụ:", "bắt buộc:", "đầu ra:", "chỉ xuất bản")):
|
| 47 |
-
continue
|
| 48 |
-
lines.append(ln)
|
| 49 |
-
return re.sub(r"\n{3,}", "\n\n", "\n".join(lines).strip())
|
| 50 |
-
|
| 51 |
-
def fallback_shorts():
|
| 52 |
-
hard = [
|
| 53 |
-
("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"),
|
| 54 |
-
("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"),
|
| 55 |
-
("tvPewsc2ph4", "Tính năng ẩn trên iPhone giúp giảm mỏi mắt | Dân trí", "baodantri7941"),
|
| 56 |
-
("7Pd6vZ2Lz1M", "Hành động ấm lòng trong tìm kiếm học sinh tử vong ở sông Lô | SKĐS", "baosuckhoedoisongboyte"),
|
| 57 |
-
("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"),
|
| 58 |
-
("IUOprcJyYr4", "Phụ nữ táo bón có phải do lười ăn rau? | SKĐS", "baosuckhoedoisongboyte"),
|
| 59 |
-
]
|
| 60 |
-
return [
|
| 61 |
-
{
|
| 62 |
-
"id": vid,
|
| 63 |
-
"title": title,
|
| 64 |
-
"channel": ch,
|
| 65 |
-
"link": "https://www.youtube.com/watch?v=" + vid,
|
| 66 |
-
"img": "https://i.ytimg.com/vi/" + vid + "/hqdefault.jpg",
|
| 67 |
-
"source": "yt",
|
| 68 |
-
}
|
| 69 |
-
for vid, title, ch in hard
|
| 70 |
-
]
|
| 71 |
-
|
| 72 |
-
def yt_html(handle, count=18):
|
| 73 |
-
try:
|
| 74 |
-
headers = getattr(base, "HEADERS", {"User-Agent": "Mozilla/5.0"})
|
| 75 |
-
html = requests.get(f"https://www.youtube.com/@{handle}/shorts", headers=headers, timeout=6).text
|
| 76 |
-
ids, out = [], []
|
| 77 |
-
for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"', html):
|
| 78 |
-
vid = m.group(1)
|
| 79 |
-
if vid in ids:
|
| 80 |
-
continue
|
| 81 |
-
ids.append(vid)
|
| 82 |
-
snip = html[max(0, m.start() - 900):m.start() + 1600]
|
| 83 |
-
mt = re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"', snip) or re.search(r'"accessibilityText":"([^"]+)"', snip)
|
| 84 |
-
title = clean(mt.group(1).replace('\\n', ' ')) if mt else "YouTube Short"
|
| 85 |
-
out.append({
|
| 86 |
-
"id": vid,
|
| 87 |
-
"title": title,
|
| 88 |
-
"channel": handle,
|
| 89 |
-
"link": "https://www.youtube.com/watch?v=" + vid,
|
| 90 |
-
"img": "https://i.ytimg.com/vi/" + vid + "/hqdefault.jpg",
|
| 91 |
-
"source": "yt",
|
| 92 |
-
})
|
| 93 |
-
if len(out) >= count:
|
| 94 |
-
break
|
| 95 |
-
return out
|
| 96 |
-
except Exception:
|
| 97 |
-
return []
|
| 98 |
-
|
| 99 |
-
def fresh_shorts():
|
| 100 |
-
seen, out = set(), []
|
| 101 |
-
for ch in channels:
|
| 102 |
-
for v in yt_html(ch, 18):
|
| 103 |
-
if v["id"] not in seen:
|
| 104 |
-
seen.add(v["id"])
|
| 105 |
-
out.append(v)
|
| 106 |
-
for v in fallback_shorts():
|
| 107 |
-
if v["id"] not in seen:
|
| 108 |
-
seen.add(v["id"])
|
| 109 |
-
out.append(v)
|
| 110 |
-
return out[:40]
|
| 111 |
-
|
| 112 |
-
def remove_routes(paths):
|
| 113 |
-
app.router.routes = [r for r in app.router.routes if getattr(r, "path", None) not in set(paths)]
|
| 114 |
-
|
| 115 |
-
remove_routes(["/api/shorts", "/api/topic_post", "/api/rewrite_topic"])
|
| 116 |
-
|
| 117 |
-
@app.get("/api/shorts")
|
| 118 |
-
def api_shorts(refresh: int = Query(default=0)):
|
| 119 |
-
now = time.time()
|
| 120 |
-
if not refresh and shorts_cache["d"] and now - shorts_cache["t"] < 600:
|
| 121 |
-
return JSONResponse(shorts_cache["d"])
|
| 122 |
-
data = fresh_shorts()
|
| 123 |
-
shorts_cache.update({"t": now, "d": data})
|
| 124 |
-
return JSONResponse(data)
|
| 125 |
-
|
| 126 |
-
@app.post("/api/topic_post")
|
| 127 |
-
async def topic_post(request: Request):
|
| 128 |
-
body = await request.json()
|
| 129 |
-
topic = clean(body.get("topic", ""))
|
| 130 |
-
if not topic:
|
| 131 |
-
return JSONResponse({"error": "missing topic"}, status_code=400)
|
| 132 |
-
try:
|
| 133 |
-
img = base.pollinations_image_url(topic)
|
| 134 |
-
except Exception:
|
| 135 |
-
img = "https://image.pollinations.ai/prompt/" + quote("Vietnamese news editorial illustration " + topic) + "?width=1024&height=576&nologo=true"
|
| 136 |
-
prompt = f"""Viết một bài báo tiếng Việt hoàn chỉnh, chất lượng cao về chủ đề: {topic}
|
| 137 |
-
|
| 138 |
-
Hãy sử dụng kiến thức tổng hợp của bạn để tạo nội dung có giá trị thực sự cho độc giả.
|
| 139 |
-
|
| 140 |
-
Phong cách:
|
| 141 |
-
- Như một bài báo/tạp chí đã hoàn thiện, không phải dàn ý.
|
| 142 |
-
- Không nhắc lại yêu cầu của người dùng.
|
| 143 |
-
- Không liệt kê chỉ dẫn viết bài.
|
| 144 |
-
- Không sao chép nguyên văn nguồn nào.
|
| 145 |
-
|
| 146 |
-
Nội dung cần có:
|
| 147 |
-
- Tiêu đề cụ thể.
|
| 148 |
-
- Sapo ngắn, hấp dẫn.
|
| 149 |
-
- Các đoạn phân tích bối cảnh, nguyên nhân, tác động, ví dụ và nhận định.
|
| 150 |
-
- Nếu chủ đề là thể thao như World Cup, hãy nói về ý nghĩa giải đấu, lịch sử, tác động tới bóng đá, đội tuyển/cầu thủ, kinh tế - truyền thông và cảm xúc người hâm mộ.
|
| 151 |
-
- Nếu thiếu dữ kiện thời sự mới, hãy diễn đạt thận trọng và tập trung vào kiến thức nền.
|
| 152 |
-
- Cuối bài có mục Nguồn tham khảo ngắn: Qwen2.5-VL / kiến thức tổng hợp.
|
| 153 |
-
"""
|
| 154 |
-
try:
|
| 155 |
-
text = await base.qwen_generate(prompt, image_url=img, max_tokens=1800)
|
| 156 |
-
except Exception:
|
| 157 |
-
text = ""
|
| 158 |
-
if not text:
|
| 159 |
-
text = f"{topic}\n\n{topic} là một chủ đề có nhiều lớp ý nghĩa, từ bối cảnh lịch sử đến tác động xã hội, truyền thông và đời sống người hâm mộ. Bài viết này tổng hợp các kiến thức nền và những điểm đáng chú ý nhất để người đọc hiểu rõ hơn về chủ đề.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp."
|
| 160 |
-
text = clean_topic_output(text)
|
| 161 |
-
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"}])
|
| 162 |
-
post["images"] = [img]
|
| 163 |
-
posts = base._load_ai_wall()
|
| 164 |
-
posts.insert(0, post)
|
| 165 |
-
base._save_ai_wall(posts)
|
| 166 |
-
return JSONResponse({"post": post})
|
| 167 |
-
|
| 168 |
-
@app.post("/api/rewrite_topic")
|
| 169 |
-
async def rewrite_topic(request: Request):
|
| 170 |
-
body = await request.json()
|
| 171 |
-
post_id = str(body.get("id", "")).strip()
|
| 172 |
-
posts = base._load_ai_wall()
|
| 173 |
-
p = next((x for x in posts if str(x.get("id")) == post_id), None)
|
| 174 |
-
if not p:
|
| 175 |
-
return JSONResponse({"error": "post not found"}, status_code=404)
|
| 176 |
-
prompt = f"""Viết lại bài sau theo phong cách báo chí phân tích, mạch lạc, hấp dẫn. Chỉ xuất bản bản viết lại cuối cùng.
|
| 177 |
-
|
| 178 |
-
Tiêu đề: {p.get('title','')}
|
| 179 |
-
Nội dung:
|
| 180 |
-
{(p.get('text') or '')[:12000]}
|
| 181 |
-
"""
|
| 182 |
-
try:
|
| 183 |
-
txt = await base.qwen_generate(prompt, image_url=p.get("img") or None, max_tokens=1600)
|
| 184 |
-
except Exception:
|
| 185 |
-
txt = ""
|
| 186 |
-
if not txt:
|
| 187 |
-
txt = p.get("text", "")
|
| 188 |
-
txt = clean_topic_output(txt)
|
| 189 |
-
new = dict(p)
|
| 190 |
-
new["id"] = str(int(time.time() * 1000))
|
| 191 |
-
new["text"] = txt
|
| 192 |
-
new["kind"] = "topic_rewrite"
|
| 193 |
-
posts.insert(0, new)
|
| 194 |
-
base._save_ai_wall(posts)
|
| 195 |
-
return JSONResponse({"post": new})
|
| 196 |
-
|
| 197 |
-
# Patch homepage without replacing the whole index route.
|
| 198 |
-
@app.middleware("http")
|
| 199 |
-
async def inject_hotfix(request, call_next):
|
| 200 |
-
response = await call_next(request)
|
| 201 |
-
try:
|
| 202 |
-
if request.url.path == "/" and response.headers.get("content-type", "").startswith("text/html"):
|
| 203 |
-
body = b""
|
| 204 |
-
async for chunk in response.body_iterator:
|
| 205 |
-
body += chunk
|
| 206 |
-
html = body.decode("utf-8", "ignore")
|
| 207 |
-
inject = r'''
|
| 208 |
-
<script>
|
| 209 |
-
(function(){
|
| 210 |
-
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 211 |
-
function cleanTopicText(){document.querySelectorAll('.wall-text,.article-p,.rewrite-text').forEach(el=>{let t=el.textContent||'';let bad=['Chỉ xuất bản bài viết cuối cùng','Không sao chép nguyên văn','Bài có tiêu đề','Nhiệm vụ: viết một bài báo tiếng Việt hoàn chỉnh'];if(bad.some(b=>t.includes(b))){el.textContent=t.split('\n').filter(l=>!bad.some(b=>l.includes(b))).join('\n')}})}
|
| 212 |
-
async function ensureFastShorts(){let home=document.getElementById('view-home');if(!home||document.getElementById('shorts-hotfix-fast'))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-hotfix-fast';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">mới nhất / tải nhanh</span></div><div class="slider-track">';sh.slice(0,24).forEach((a,i)=>{h+=`<div class="slider-item shorts-item" onclick="openShortsHotfix(${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;(document.querySelector('.ai-compose')||home.firstChild).after(wrap);window.__hotfixShorts=sh;}
|
| 213 |
-
window.openShortsHotfix=function(start){let arts=window.__hotfixShorts||[];if(!arts.length)return alert('Chưa tải được Shorts');let ordered=start>0?arts.slice(start).concat(arts.slice(0,start)):arts;showView('view-tiktok');let h='<button class="back-btn" onclick="switchCat(\'home\')">← Shorts</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';ordered.forEach((v,i)=>{let src='https://www.youtube.com/embed/'+v.id+'?autoplay=1&rel=0&playsinline=1';h+=`<div class="tiktok-slide"><iframe src="${src}" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture"></iframe><div class="tiktok-bottom"><span class="badge badge-fpt">YT</span><p class="tiktok-title">${esc(v.title)}</p></div><span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`});h+='</div></div>';document.getElementById('view-tiktok').innerHTML=h;}
|
| 214 |
-
setInterval(()=>{cleanTopicText();if(document.getElementById('view-home')?.classList.contains('active'))ensureFastShorts();},1200);setTimeout(ensureFastShorts,800);
|
| 215 |
-
})();
|
| 216 |
-
</script>
|
| 217 |
-
'''
|
| 218 |
-
if "</body>" in html:
|
| 219 |
-
html = html.replace("</body>", inject + "</body>")
|
| 220 |
-
return HTMLResponse(html)
|
| 221 |
-
except Exception:
|
| 222 |
-
pass
|
| 223 |
-
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|