Spaces:
Running
Running
Patch b9b6ba4 topic prompt, fast shorts, and topic article actions
Browse files- patch_b9b6ba4.py +168 -0
patch_b9b6ba4.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, json, requests
|
| 7 |
+
from urllib.parse import quote, urlparse
|
| 8 |
+
from fastapi import Request, Query
|
| 9 |
+
from fastapi.responses import JSONResponse, HTMLResponse
|
| 10 |
+
|
| 11 |
+
# This module is copied into the b9b6ba4 snapshot and imported by runner.
|
| 12 |
+
import ai_runtime_final6 as base_app
|
| 13 |
+
app = base_app.app
|
| 14 |
+
base = base_app.base
|
| 15 |
+
rt = base_app.rt
|
| 16 |
+
|
| 17 |
+
SHORT_CHANNELS=["baodantri7941","baosuckhoedoisongboyte"]
|
| 18 |
+
_SHORTS_CACHE={"t":0,"d":[]}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def clean(s):
|
| 22 |
+
import html as html_lib
|
| 23 |
+
return re.sub(r"\s+"," ",html_lib.unescape(s or "")).strip()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _domain(u):
|
| 27 |
+
try:return urlparse(u or '').netloc.replace('www.','')
|
| 28 |
+
except Exception:return ''
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _clean_ai_text(text):
|
| 32 |
+
"""Remove leaked prompt/instruction lines from topic posts."""
|
| 33 |
+
bad_patterns=[
|
| 34 |
+
r'Chỉ xuất bản bài viết cuối cùng.*',
|
| 35 |
+
r'không nhắc lại yêu cầu.*',
|
| 36 |
+
r'không liệt kê chỉ dẫn.*',
|
| 37 |
+
r'•\s*Không sao chép nguyên văn.*',
|
| 38 |
+
r'Không sao chép nguyên văn.*',
|
| 39 |
+
r'•\s*Bài có tiêu đề, sapo.*',
|
| 40 |
+
r'Bài có tiêu đề, sapo.*',
|
| 41 |
+
r'Nhiệm vụ:\s*viết một bài báo tiếng Việt hoàn chỉnh.*',
|
| 42 |
+
r'Yêu cầu:\s*$',
|
| 43 |
+
r'Bắt buộc:\s*$',
|
| 44 |
+
]
|
| 45 |
+
lines=[]
|
| 46 |
+
for ln in (text or '').splitlines():
|
| 47 |
+
raw=ln.strip()
|
| 48 |
+
if not raw:lines.append(ln);continue
|
| 49 |
+
if any(re.search(p,raw,flags=re.I) for p in bad_patterns):
|
| 50 |
+
continue
|
| 51 |
+
lines.append(ln)
|
| 52 |
+
out='\n'.join(lines)
|
| 53 |
+
out=re.sub(r'\n{3,}','\n\n',out).strip()
|
| 54 |
+
return out
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _yt_html(handle,count=20):
|
| 58 |
+
try:
|
| 59 |
+
html=requests.get(f"https://www.youtube.com/@{handle}/shorts",headers=getattr(base,'HEADERS',{}),timeout=8).text
|
| 60 |
+
ids=[];out=[]
|
| 61 |
+
for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"',html):
|
| 62 |
+
vid=m.group(1)
|
| 63 |
+
if vid in ids:continue
|
| 64 |
+
ids.append(vid)
|
| 65 |
+
snip=html[max(0,m.start()-900):m.start()+1600]
|
| 66 |
+
title='YouTube Short'
|
| 67 |
+
mt=re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"',snip) or re.search(r'"accessibilityText":"([^"]+)"',snip)
|
| 68 |
+
if mt:title=clean(mt.group(1).replace('\\n',' '))
|
| 69 |
+
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'})
|
| 70 |
+
if len(out)>=count:break
|
| 71 |
+
return out
|
| 72 |
+
except Exception:return []
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _yt_dlp(handle,count=20):
|
| 76 |
+
try:
|
| 77 |
+
import yt_dlp
|
| 78 |
+
opts={'quiet':True,'extract_flat':True,'skip_download':True,'playlistend':count,'ignoreerrors':True,'no_warnings':True,'socket_timeout':8}
|
| 79 |
+
with yt_dlp.YoutubeDL(opts) as ydl:
|
| 80 |
+
info=ydl.extract_info(f"https://www.youtube.com/@{handle}/shorts",download=False)
|
| 81 |
+
out=[]
|
| 82 |
+
for e in (info or {}).get('entries') or []:
|
| 83 |
+
vid=e.get('id') or ''
|
| 84 |
+
if re.match(r'^[A-Za-z0-9_-]{11}$',vid):
|
| 85 |
+
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'})
|
| 86 |
+
return out
|
| 87 |
+
except Exception:return []
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _fallback():
|
| 91 |
+
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')]
|
| 92 |
+
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]
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _fresh_shorts():
|
| 96 |
+
seen=set();out=[]
|
| 97 |
+
for ch in SHORT_CHANNELS:
|
| 98 |
+
# HTML first is much faster; yt-dlp as backup.
|
| 99 |
+
got=_yt_html(ch,24) or _yt_dlp(ch,24)
|
| 100 |
+
for v in got:
|
| 101 |
+
if v['id'] not in seen:
|
| 102 |
+
seen.add(v['id']);out.append(v)
|
| 103 |
+
for v in _fallback():
|
| 104 |
+
if v['id'] not in seen:
|
| 105 |
+
seen.add(v['id']);out.append(v)
|
| 106 |
+
return out[:50]
|
| 107 |
+
|
| 108 |
+
# Remove routes to override.
|
| 109 |
+
_PATCH={('/api/shorts','GET'),('/api/topic_post','POST'),('/','GET')}
|
| 110 |
+
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)]
|
| 111 |
+
|
| 112 |
+
@app.get('/api/shorts')
|
| 113 |
+
def api_shorts_fast(refresh:int=Query(default=0)):
|
| 114 |
+
now=time.time()
|
| 115 |
+
if _SHORTS_CACHE['d'] and now-_SHORTS_CACHE['t']<300 and not refresh:
|
| 116 |
+
return JSONResponse(_SHORTS_CACHE['d'])
|
| 117 |
+
data=_fresh_shorts()
|
| 118 |
+
_SHORTS_CACHE.update({'t':now,'d':data})
|
| 119 |
+
return JSONResponse(data)
|
| 120 |
+
|
| 121 |
+
@app.post('/api/topic_post')
|
| 122 |
+
async def topic_post_clean(request:Request):
|
| 123 |
+
body=await request.json();topic=clean(body.get('topic',''))
|
| 124 |
+
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 125 |
+
try:img=base.pollinations_image_url(topic)
|
| 126 |
+
except Exception:img='https://image.pollinations.ai/prompt/'+quote('Vietnamese news editorial '+topic)+'?width=1024&height=576&nologo=true'
|
| 127 |
+
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}
|
| 128 |
+
|
| 129 |
+
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.
|
| 130 |
+
|
| 131 |
+
Đầu ra cần là bài viết hoàn chỉnh:
|
| 132 |
+
- Tiêu đề rõ và hấp dẫn.
|
| 133 |
+
- Sapo ngắn mở vấn đề.
|
| 134 |
+
- 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.
|
| 135 |
+
- 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.
|
| 136 |
+
- 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.
|
| 137 |
+
- 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.
|
| 138 |
+
- Cuối bài có mục Nguồn tham khảo ngắn.
|
| 139 |
+
"""
|
| 140 |
+
text=await base.qwen_generate(prompt,image_url=img,max_tokens=1600)
|
| 141 |
+
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."
|
| 142 |
+
text=_clean_ai_text(text)
|
| 143 |
+
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'}])
|
| 144 |
+
post['images']=[img]
|
| 145 |
+
posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
|
| 146 |
+
return JSONResponse({'post':post})
|
| 147 |
+
|
| 148 |
+
PATCH_INJECT=r'''
|
| 149 |
+
<script>
|
| 150 |
+
(function(){
|
| 151 |
+
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 152 |
+
// clean leaked instruction text in already-rendered AI posts
|
| 153 |
+
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();});}
|
| 154 |
+
// add rewrite/link buttons to topic AI article view if missing
|
| 155 |
+
function addTopicActions(){let art=document.querySelector('#view-article .article-view');if(!art)return;let title=art.querySelector('.article-title')?.textContent||'';let isAI=!!art.querySelector('.badge-ai')||title;let actions=art.querySelector('.article-actions');if(!actions){actions=document.createElement('div');actions.className='article-actions';art.appendChild(actions)}if(isAI&&!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ể copy/chia sẻ hoặc tạo Short AI từ Tường AI.');};actions.appendChild(b)}if(isAI&&!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)}}
|
| 156 |
+
// faster shorts home refresh: replace or insert shorts slide
|
| 157 |
+
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)}
|
| 158 |
+
setTimeout(()=>{cleanInstructionText();addTopicActions();refreshShortsFast();},1200);setInterval(()=>{cleanInstructionText();addTopicActions();},2000);
|
| 159 |
+
})();
|
| 160 |
+
</script>
|
| 161 |
+
'''
|
| 162 |
+
|
| 163 |
+
@app.get('/')
|
| 164 |
+
async def index_patch():
|
| 165 |
+
# Let b9b6ba4 index route build from restored non-placeholder HTML, then append patch.
|
| 166 |
+
with open('/app/static/index.html','r',encoding='utf-8') as f:html=f.read()
|
| 167 |
+
extra=getattr(base_app.old,'PATCH_INJECT','')+PATCH_INJECT if hasattr(base_app,'old') else PATCH_INJECT
|
| 168 |
+
return HTMLResponse(html.replace('</body>',extra+'\n</body>') if '</body>' in html else html+extra)
|