Spaces:
Running
Running
Final runtime overrides for URL-only AI, image gallery, and short layout
Browse files- ai_runtime_final.py +137 -0
ai_runtime_final.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Final runtime overrides for VNEWS AI UI and short-video rendering."""
|
| 2 |
+
import os, re
|
| 3 |
+
import ai_runtime as rt
|
| 4 |
+
from ai_runtime import app
|
| 5 |
+
from fastapi.responses import HTMLResponse
|
| 6 |
+
try:
|
| 7 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 8 |
+
except Exception:
|
| 9 |
+
Image = ImageDraw = ImageFont = None
|
| 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 |
+
|
| 17 |
+
def _strip_bullet_prefix(s):
|
| 18 |
+
return clean(re.sub(r'^[\s•\-\*·▪▫●○\d\.\)\(]+', '', s or ''))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _draw_center(draw, lines, font, y, fill, W, line_h):
|
| 22 |
+
for ln in lines:
|
| 23 |
+
try:
|
| 24 |
+
box=draw.textbbox((0,0),ln,font=font);tw=box[2]-box[0]
|
| 25 |
+
except Exception:
|
| 26 |
+
tw=len(ln)*24
|
| 27 |
+
draw.text((max(30,(W-tw)//2),y),ln,fill=fill,font=font)
|
| 28 |
+
y+=line_h
|
| 29 |
+
return y
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def final_make_frame(post,seg,idx,total,img_path,out_path):
|
| 33 |
+
"""Frame layout requested by user:
|
| 34 |
+
- image at top
|
| 35 |
+
- source badge at image corner
|
| 36 |
+
- text centered in lower half
|
| 37 |
+
- no bullet/dot prefix in scene text
|
| 38 |
+
"""
|
| 39 |
+
if Image is None:
|
| 40 |
+
return rt.make_frame(post,seg,idx,total,img_path,out_path)
|
| 41 |
+
W,H=1080,1920
|
| 42 |
+
hero_h=760
|
| 43 |
+
bg=Image.new('RGB',(W,H),(12,12,12))
|
| 44 |
+
try:
|
| 45 |
+
im=Image.open(img_path).convert('RGB')
|
| 46 |
+
ratio=im.width/max(1,im.height);tr=W/hero_h
|
| 47 |
+
if ratio>tr:
|
| 48 |
+
nh=hero_h;nw=int(nh*ratio)
|
| 49 |
+
else:
|
| 50 |
+
nw=W;nh=int(nw/ratio)
|
| 51 |
+
im=im.resize((nw,nh));left=(nw-W)//2;top=(nh-hero_h)//2
|
| 52 |
+
bg.paste(im.crop((left,top,left+W,top+hero_h)),(0,0))
|
| 53 |
+
except Exception:
|
| 54 |
+
pass
|
| 55 |
+
draw=ImageDraw.Draw(bg)
|
| 56 |
+
try:
|
| 57 |
+
fb=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',58)
|
| 58 |
+
ft=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',38)
|
| 59 |
+
fs=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',30)
|
| 60 |
+
fsmall=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',28)
|
| 61 |
+
except Exception:
|
| 62 |
+
fb=ft=fs=fsmall=None
|
| 63 |
+
# source badge, black rounded rectangle, no RGBA tuple to avoid Pillow RGB errors
|
| 64 |
+
badge='Nguồn: '+rt._source_badge(post)
|
| 65 |
+
try:
|
| 66 |
+
b=draw.textbbox((0,0),badge,font=fsmall);bw=b[2]-b[0];bh=b[3]-b[1]
|
| 67 |
+
except Exception:
|
| 68 |
+
bw=len(badge)*16;bh=34
|
| 69 |
+
bx=W-bw-42;by=24
|
| 70 |
+
draw.rounded_rectangle((bx-16,by-8,W-24,by+bh+14),radius=18,fill=(0,0,0))
|
| 71 |
+
draw.text((bx,by),badge,fill=(255,255,255),font=fsmall)
|
| 72 |
+
|
| 73 |
+
draw.rectangle((0,hero_h-20,W,H),fill=(12,12,12))
|
| 74 |
+
total=max(1,total)
|
| 75 |
+
total_w=total*38-14;start=(W-total_w)//2
|
| 76 |
+
for i in range(total):
|
| 77 |
+
fill=(92,184,122) if i==idx else (70,70,70)
|
| 78 |
+
draw.rounded_rectangle((start+i*38,820,start+i*38+24,832),radius=6,fill=fill)
|
| 79 |
+
brand='VNEWS AI SHORT'
|
| 80 |
+
try:
|
| 81 |
+
bb=draw.textbbox((0,0),brand,font=ft);tx=(W-(bb[2]-bb[0]))//2
|
| 82 |
+
except Exception:
|
| 83 |
+
tx=360
|
| 84 |
+
draw.text((tx,870),brand,fill=(110,231,143),font=ft)
|
| 85 |
+
|
| 86 |
+
seg=_strip_bullet_prefix(seg)
|
| 87 |
+
lines=rt.wrap_text(draw,seg,fb,W-120,8)
|
| 88 |
+
block_h=len(lines)*74
|
| 89 |
+
y=max(980,1250-block_h//2)
|
| 90 |
+
_draw_center(draw,lines,fb,y,(255,255,255),W,74)
|
| 91 |
+
|
| 92 |
+
title_lines=rt.wrap_text(draw,_strip_bullet_prefix(post.get('title','')),fs,W-120,3)
|
| 93 |
+
y2=1640
|
| 94 |
+
draw.line((80,y2-26,W-80,y2-26),fill=(70,70,70),width=2)
|
| 95 |
+
_draw_center(draw,title_lines,fs,y2,(220,220,220),W,42)
|
| 96 |
+
bg.save(out_path,quality=92)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# Monkey-patch function used by ai_runtime.short_segments
|
| 100 |
+
rt.make_frame = final_make_frame
|
| 101 |
+
|
| 102 |
+
# Replace final index route to guarantee UI changes after all previous injections.
|
| 103 |
+
app.router.routes=[r for r in app.router.routes if not (getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set()))]
|
| 104 |
+
|
| 105 |
+
FINAL_INJECT = r'''
|
| 106 |
+
<style>
|
| 107 |
+
/* URL-only AI compose: remove topic controls completely */
|
| 108 |
+
#ai-topic-input{display:none!important}
|
| 109 |
+
button[onclick*="createTopicPost"],*[onclick*="createTopicPost"]{display:none!important}
|
| 110 |
+
.ai-topic-row,.topic-row,.ai-compose-topic{display:none!important}
|
| 111 |
+
.ai-url-only-note{font-size:11px;color:#888;margin:5px 0 8px}.ai-wall-gallery{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin:10px 0}.ai-wall-gallery img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:8px;background:#222}.ai-wall-gallery img:first-child{grid-column:1/-1}
|
| 112 |
+
</style>
|
| 113 |
+
<script>
|
| 114 |
+
(function(){
|
| 115 |
+
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 116 |
+
function hideTopicControls(){
|
| 117 |
+
document.querySelectorAll('#ai-topic-input').forEach(e=>{let row=e.closest('.ai-compose-topic,.topic-row,.ai-topic-row')||e.parentElement;if(row&&!row.querySelector('#ai-url-input'))row.style.display='none';else e.style.display='none';});
|
| 118 |
+
document.querySelectorAll('button,a').forEach(b=>{let t=(b.textContent||'').toLowerCase();let oc=b.getAttribute('onclick')||'';if(oc.includes('createTopicPost')||t.includes('chủ đề'))b.style.display='none';});
|
| 119 |
+
let url=document.getElementById('ai-url-input');if(url&&!document.getElementById('ai-url-only-note')){let n=document.createElement('div');n.id='ai-url-only-note';n.className='ai-url-only-note';n.textContent='Dán URL bài viết để AI tóm tắt và lấy tất cả ảnh trong bài.';url.insertAdjacentElement('afterend',n);}
|
| 120 |
+
}
|
| 121 |
+
window.createTopicPost=function(){alert('Đã tắt ô nhập chủ đề. Vui lòng dán URL bài viết.');};
|
| 122 |
+
window.createUrlPost=function(){let inp=document.getElementById('ai-url-input');let url=(inp&&inp.value||'').trim();if(!url)return alert('Dán URL trước');if(!/^https?:\/\//i.test(url))return alert('URL cần bắt đầu bằng http:// hoặc https://');fetch('/api/url_wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})}).then(r=>r.json().then(j=>({ok:r.ok,j}))).then(({ok,j})=>{if(ok&&j.post){if(inp)inp.value='';alert('Đã tóm tắt URL, lấy ảnh trong bài và đăng lên Tường AI');location.reload();}else alert(j.error||'Lỗi URL')}).catch(e=>alert(e.message||'Lỗi URL'));};
|
| 123 |
+
function galleryHtml(p){let imgs=(p.images||[]).filter(Boolean);if(!imgs.length&&p.img)imgs=[p.img];if(!imgs.length)return '';return '<div class="ai-wall-gallery">'+imgs.slice(0,12).map(u=>`<img src="${esc(u)}" loading="lazy">`).join('')+'</div>';}
|
| 124 |
+
async function getWall(){try{return (await (await fetch('/api/ai_wall')).json()).posts||[]}catch(e){return []}}
|
| 125 |
+
window.aiReadWallPatched=window.aiReadWall=async function(i){let arr=window.patchedWall||window.aiWall||[];if(!arr.length)arr=await getWall();let p=arr[i];if(!p)return;showView('view-article');let sources='';if(p.sources&&p.sources.length){sources='<div class="article-summary"><b>Nguồn tham khảo:</b><br>'+p.sources.slice(0,5).map(s=>`• ${esc(s.title||s.url||'Nguồn')} ${s.url?`(${esc(new URL(s.url).hostname.replace('www.',''))})`:''}`).join('<br>')+'</div>'}let h=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view"><span class="badge badge-ai">AI</span><h1 class="article-title">${esc(p.title)}</h1>${galleryHtml(p)}${sources}<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>${p.video?`<video class="article-img" src="${p.video}" controls playsinline></video>`:''}<div class="article-actions">${p.url?`<button onclick="window.open('${p.url}','_blank')">🔗 Nguồn</button>`:''}<button onclick="aiMakeShortPatched?aiMakeShortPatched(${i}):aiMakeShort(${i})">🎬 Tạo video shorts</button></div></div>`;document.getElementById('view-article').innerHTML=h;window.scrollTo(0,0);};
|
| 126 |
+
setTimeout(hideTopicControls,200);setInterval(hideTopicControls,1000);
|
| 127 |
+
})();
|
| 128 |
+
</script>
|
| 129 |
+
'''
|
| 130 |
+
|
| 131 |
+
@app.get('/')
|
| 132 |
+
async def index_final():
|
| 133 |
+
with open('/app/static/index.html','r',encoding='utf-8') as f:
|
| 134 |
+
html=f.read()
|
| 135 |
+
old_inject=getattr(rt.old,'PATCH_INJECT','')
|
| 136 |
+
body=old_inject + getattr(rt,'FINAL_PATCH','') + FINAL_INJECT
|
| 137 |
+
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|