Spaces:
Running
Running
Clean remaining topic instruction and add rich source cards with in-app viewing/rewrite
Browse files- ai_runtime_final11.py +149 -0
ai_runtime_final11.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Final11: remove remaining leaked sentence, rich source cards, in-app source reading + rewrite."""
|
| 2 |
+
import re, requests
|
| 3 |
+
from urllib.parse import quote
|
| 4 |
+
import ai_runtime_final10 as f10
|
| 5 |
+
from ai_runtime_final10 import app, base, rt, HTMLResponse, JSONResponse, Request, Query
|
| 6 |
+
|
| 7 |
+
EXACT_LEAKS = [
|
| 8 |
+
r'Bài cần có tiêu đề,?\s*sapo ngắn,?\s*các đoạn phân tích/bối cảnh/tác động,?\s*và mục Nguồn tham khảo ngắn\.?',
|
| 9 |
+
r'Bài cần có tiêu đề,?\s*sapo.*?Nguồn tham khảo ngắn\.?' ,
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
def clean(s):
|
| 13 |
+
import html as html_lib
|
| 14 |
+
return re.sub(r"\s+", " ", html_lib.unescape(s or "")).strip()
|
| 15 |
+
|
| 16 |
+
def _clean_more(text):
|
| 17 |
+
text=f10._clean_topic_text(text or '')
|
| 18 |
+
for p in EXACT_LEAKS:
|
| 19 |
+
text=re.sub(p,'',text,flags=re.I|re.S)
|
| 20 |
+
lines=[]
|
| 21 |
+
for ln in text.splitlines():
|
| 22 |
+
nb=re.sub(r'^[\s•\-\*\d\.\)]+','',ln.strip()).strip()
|
| 23 |
+
if any(re.search(p,nb,flags=re.I|re.S) for p in EXACT_LEAKS):continue
|
| 24 |
+
lines.append(ln.strip())
|
| 25 |
+
out='\n'.join(lines)
|
| 26 |
+
out=re.sub(r'\n{3,}','\n\n',out).strip()
|
| 27 |
+
if 'Nguồn tham khảo:' not in out:
|
| 28 |
+
out+='\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp.'
|
| 29 |
+
return out
|
| 30 |
+
|
| 31 |
+
def _source_meta(url, fallback=''):
|
| 32 |
+
meta={'img':'','desc':fallback or ''}
|
| 33 |
+
if not url:return meta
|
| 34 |
+
try:
|
| 35 |
+
from bs4 import BeautifulSoup
|
| 36 |
+
r=requests.get(url,headers=getattr(base,'HEADERS',{}),timeout=8);r.encoding='utf-8'
|
| 37 |
+
soup=BeautifulSoup(r.text,'lxml')
|
| 38 |
+
og=soup.find('meta',property='og:image') or soup.find('meta',attrs={'name':'twitter:image'})
|
| 39 |
+
if og:meta['img']=og.get('content','') or ''
|
| 40 |
+
desc=soup.find('meta',property='og:description') or soup.find('meta',attrs={'name':'description'})
|
| 41 |
+
if desc:meta['desc']=clean(desc.get('content',''))[:320]
|
| 42 |
+
except Exception:pass
|
| 43 |
+
if not meta['desc']:meta['desc']=clean(fallback)[:320]
|
| 44 |
+
return meta
|
| 45 |
+
|
| 46 |
+
def _enrich_details(details):
|
| 47 |
+
out=[]
|
| 48 |
+
for d in (details or [])[:10]:
|
| 49 |
+
nd=dict(d)
|
| 50 |
+
meta=_source_meta(nd.get('url',''),nd.get('content',''))
|
| 51 |
+
nd['img']=nd.get('img') or meta.get('img','')
|
| 52 |
+
nd['desc']=nd.get('desc') or meta.get('desc','') or clean(nd.get('content',''))[:260]
|
| 53 |
+
out.append(nd)
|
| 54 |
+
return out
|
| 55 |
+
|
| 56 |
+
# Remove routes to override.
|
| 57 |
+
_PATCH={('/api/topic_post','POST'),('/api/topic_rewrite','POST'),('/api/source_article','GET'),('/','GET')}
|
| 58 |
+
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)]
|
| 59 |
+
|
| 60 |
+
@app.post('/api/topic_post')
|
| 61 |
+
async def topic_post_links_rich(request:Request):
|
| 62 |
+
body=await request.json();topic=clean(body.get('topic',''))
|
| 63 |
+
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 64 |
+
try:img=f10.f9.f6._topic_image(topic)
|
| 65 |
+
except Exception:img=base.pollinations_image_url(topic)
|
| 66 |
+
context,sources,details=f10._topic_sources(topic)
|
| 67 |
+
details=_enrich_details(details)
|
| 68 |
+
source_brief='\n\n'.join([f"[{i+1}] {d.get('title','')} ({d.get('via','')})\n{d.get('content','')[:1400]}" for i,d in enumerate(details[:8])]) or 'Không có nguồn web đủ rõ; dùng kiến thức tổng hợp thận trọng.'
|
| 69 |
+
prompt=f"""Viết một bài báo tiếng Việt hoàn chỉnh về chủ đề: {topic}
|
| 70 |
+
|
| 71 |
+
Dữ liệu tham khảo:
|
| 72 |
+
{source_brief[:16000]}
|
| 73 |
+
|
| 74 |
+
Chỉ viết phần bài báo cuối cùng cho độc giả. Không nhắc lại yêu cầu. Không liệt kê chỉ dẫn. Không mở đầu bằng 'Dưới đây là'.
|
| 75 |
+
Bài gồm tiêu đề, sapo, phân tích/bối cảnh/tác động và nguồn tham khảo."""
|
| 76 |
+
text=await base.qwen_generate(prompt,image_url=img,max_tokens=1700)
|
| 77 |
+
if not text:text=f"{topic}: những điểm đáng chú ý\n\n{topic} là chủ đề cần được nhìn qua bối cảnh, tác động và các thông tin liên quan. Nội dung dưới đây tổng hợp các ý chính để người đọc nắm vấn đề nhanh hơn.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp."
|
| 78 |
+
text=_clean_more(text)
|
| 79 |
+
if details:
|
| 80 |
+
vias=', '.join([d.get('via') or '' for d in details[:5] if d.get('via')])
|
| 81 |
+
text=re.sub(r'Nguồn tham khảo:.*$', 'Nguồn tham khảo: '+(vias or 'Qwen2.5-VL / kiến thức tổng hợp.'), text, flags=re.I|re.S)
|
| 82 |
+
post=base.make_post(topic,text,img,'','topic_qwen_links',sources=[s for s in sources if s.get('url')])
|
| 83 |
+
post['images']=[img]
|
| 84 |
+
post['source_details']=details
|
| 85 |
+
posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
|
| 86 |
+
return JSONResponse({'post':post})
|
| 87 |
+
|
| 88 |
+
@app.post('/api/topic_rewrite')
|
| 89 |
+
async def topic_rewrite_clean(request:Request):
|
| 90 |
+
body=await request.json();pid=str(body.get('id',''));tone=clean(body.get('tone','mạch lạc, sắc gọn'))
|
| 91 |
+
posts=base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==pid),None)
|
| 92 |
+
if not p:return JSONResponse({'error':'post not found'},status_code=404)
|
| 93 |
+
details=_enrich_details(p.get('source_details') or [])
|
| 94 |
+
src='\n\n'.join([f"{i+1}. {d.get('title','')} ({d.get('via','')})\n{d.get('content','')[:1200]}" for i,d in enumerate(details[:8])])
|
| 95 |
+
prompt=f"""Viết lại bài chủ đề sau cho hay hơn, rõ hơn, bỏ câu thừa.
|
| 96 |
+
|
| 97 |
+
Phong cách: {tone}
|
| 98 |
+
|
| 99 |
+
Bài hiện tại:
|
| 100 |
+
{(p.get('text') or '')[:12000]}
|
| 101 |
+
|
| 102 |
+
Nguồn tham khảo:
|
| 103 |
+
{src[:12000]}
|
| 104 |
+
|
| 105 |
+
Chỉ trả về bài viết cuối cùng."""
|
| 106 |
+
text=await base.qwen_generate(prompt,image_url=(p.get('img') or None),max_tokens=1700)
|
| 107 |
+
if not text:text=p.get('text','')
|
| 108 |
+
p['text']=_clean_more(text)
|
| 109 |
+
p['source_details']=details
|
| 110 |
+
base._save_ai_wall(posts)
|
| 111 |
+
return JSONResponse({'post':p})
|
| 112 |
+
|
| 113 |
+
@app.get('/api/source_article')
|
| 114 |
+
def source_article(url:str=Query(...)):
|
| 115 |
+
if not url.startswith('http'):return JSONResponse({'error':'missing url'},status_code=400)
|
| 116 |
+
try:
|
| 117 |
+
data=base.scrape_any_url(url)
|
| 118 |
+
except Exception as e:
|
| 119 |
+
return JSONResponse({'error':'Không đọc được nguồn: '+str(e)[:160]},status_code=422)
|
| 120 |
+
text=data.get('text','') or data.get('summary','') or ''
|
| 121 |
+
body=[]
|
| 122 |
+
for para in [x.strip() for x in re.split(r'\n+',text) if len(x.strip())>30][:80]:
|
| 123 |
+
body.append({'type':'p','text':para})
|
| 124 |
+
return JSONResponse({'title':data.get('title') or url,'summary':data.get('summary',''),'og_image':data.get('image',''),'body':body,'url':url,'source':'generic'})
|
| 125 |
+
|
| 126 |
+
FINAL11_INJECT=r'''
|
| 127 |
+
<style>
|
| 128 |
+
.source-card-grid{display:grid;grid-template-columns:1fr;gap:8px;margin-top:12px}.source-card{display:flex;gap:9px;background:#181818;border:1px solid #2b2b2b;border-radius:10px;padding:8px;cursor:pointer}.source-card img{width:110px;aspect-ratio:16/9;object-fit:cover;border-radius:7px;background:#222}.source-card-title{font-size:12px;font-weight:800;color:#eee;line-height:1.35}.source-card-desc{font-size:11px;color:#aaa;line-height:1.35;margin-top:3px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.source-card-via{font-size:10px;color:#5cb87a;margin-top:3px}.source-rewrite-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}.source-rewrite-actions button,.source-rewrite-actions select{background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:8px}.source-rewrite-actions button{background:#2d8659;border:0;color:#fff}
|
| 129 |
+
</style>
|
| 130 |
+
<script>
|
| 131 |
+
(function(){
|
| 132 |
+
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 133 |
+
let lastTopicPost11=null;
|
| 134 |
+
function sourceCardsHtml(p){let arr=p.source_details||p.sources||[];if(!arr.length)return '';let h='<div class="source-links-box"><h3>🔗 Nguồn tham khảo</h3><div class="source-card-grid">';arr.slice(0,10).forEach((s,i)=>{let url=s.url||'';h+=`<div class="source-card" onclick="${url?`readSourceOnVnews('${esc(url).replace(/'/g,"\\'")}')`:'void(0)'}">${s.img?`<img src="${esc(s.img)}">`:'<img>'}<div><div class="source-card-title">${i+1}. ${esc(s.title||url||'Nguồn')}</div><div class="source-card-desc">${esc(s.desc||s.content||'')}</div><div class="source-card-via">${esc(s.via||'')}</div></div></div>`});h+='</div></div>';return h;}
|
| 135 |
+
function rewriteTopicBox(p){return `<div class="source-rewrite-actions"><select id="topic-rewrite-tone"><option value="mạch lạc, sắc gọn, báo điện tử">Mạch lạc</option><option value="phân tích sâu, chuyên gia">Phân tích sâu</option><option value="dễ hiểu, phổ thông">Dễ hiểu</option><option value="sôi nổi, thu hút">Thu hút</option></select><button onclick="rewriteTopicPost11('${p.id}')">✍️ Rewrite bài chủ đề</button></div>`}
|
| 136 |
+
function renderTopic11(p){lastTopicPost11=p;showView('view-article');let imgs=(p.images||[]).filter(Boolean);let gal=imgs.length?'<div class="ai-wall-gallery">'+imgs.slice(0,12).map(u=>`<img src="${esc(u)}" loading="lazy">`).join('')+'</div>':(p.img?`<img class="article-img" src="${esc(p.img)}">`:'');document.getElementById('view-article').innerHTML=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view"><span class="badge badge-ai">AI Chủ đề</span><h1 class="article-title">${esc(p.title)}</h1>${gal}<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>${sourceCardsHtml(p)}${rewriteTopicBox(p)}</div>`;window.scrollTo(0,0)}
|
| 137 |
+
window.rewriteTopicPost11=window.rewriteTopicPost=async function(id){let tone=document.getElementById('topic-rewrite-tone')?.value||'mạch lạc';let btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang rewrite...'}try{let r=await fetch('/api/topic_rewrite',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,tone})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi rewrite');renderTopic11(j.post);alert('Đã rewrite bài chủ đề.');}catch(e){alert(e.message)}finally{if(btn){btn.disabled=false;btn.textContent='✍️ Rewrite bài chủ đề'}}}
|
| 138 |
+
window.readSourceOnVnews=async function(url){showView('view-article');document.getElementById('view-article').innerHTML='<div class="loading">Đang tải nguồn...</div>';try{let d=await fetch('/api/source_article?url='+encodeURIComponent(url)).then(r=>r.json());if(d.error)throw new Error(d.error);let h=`<button class="back-btn" onclick="history.back();switchCat('home')">← Quay lại</button><div class="article-view"><h1 class="article-title">${esc(d.title)}</h1>${d.og_image?`<img class="article-img" src="${esc(d.og_image)}">`:''}${d.summary?`<div class="article-summary">${esc(d.summary)}</div>`:''}`;(d.body||[]).forEach(b=>{if(b.type==='p')h+=`<p class="article-p">${esc(b.text)}</p>`});h+=`<div class="article-actions"><button onclick="rewriteSourceOnWall('${esc(url).replace(/'/g,"\\'")}')">🤖 Rewrite nguồn này lên Tường AI</button><button onclick="window.open('${esc(url)}','_blank')">🔗 Mở gốc</button></div></div>`;document.getElementById('view-article').innerHTML=h;window.scrollTo(0,0)}catch(e){document.getElementById('view-article').innerHTML='<button class="back-btn" onclick="switchCat(\'home\')">← Quay lại</button><div class="loading">'+esc(e.message)+'</div>'}}
|
| 139 |
+
window.rewriteSourceOnWall=async function(url){try{let r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã rewrite nguồn lên Tường AI');}catch(e){alert(e.message)}}
|
| 140 |
+
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(inp)inp.value='';renderTopic11(j.post);alert('Đã tạo bài chủ đề kèm ảnh, mô tả và link nguồn.');}catch(e){alert(e.message)}finally{if(btn){btn.disabled=false;btn.textContent='✨ Tạo bài kiến thức bằng Qwen'}}}
|
| 141 |
+
})();
|
| 142 |
+
</script>
|
| 143 |
+
'''
|
| 144 |
+
|
| 145 |
+
@app.get('/')
|
| 146 |
+
async def index_final11():
|
| 147 |
+
html=f10.f9.f6.f5.f4.f3.f2.f1._load_index_html()
|
| 148 |
+
body=getattr(rt.old,'PATCH_INJECT','')+f10.f9.f6.f5.f4.f3.f2.f1.FINAL_INJECT+f10.f9.f6.f5.f4.f3.FINAL3_INJECT+f10.f9.f6.f5.f4.FINAL4_INJECT+f10.f9.f6.f5.FINAL5_INJECT+f10.f9.f6.FINAL6_INJECT+f10.FINAL10_INJECT+FINAL11_INJECT
|
| 149 |
+
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|