Spaces:
Running
Running
Restore b9b6ba4 and patch topic rewrite/shorts
Browse files- restore_b9b6ba4_patch.py +147 -0
restore_b9b6ba4_patch.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|