Spaces:
Running
Running
Use restored full UI if local static index is placeholder
Browse files- ai_runtime_final.py +29 -36
ai_runtime_final.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 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
|
|
@@ -8,6 +8,8 @@ try:
|
|
| 8 |
except Exception:
|
| 9 |
Image = ImageDraw = ImageFont = None
|
| 10 |
|
|
|
|
|
|
|
| 11 |
|
| 12 |
def clean(s):
|
| 13 |
import html as html_lib
|
|
@@ -30,16 +32,9 @@ def _draw_center(draw, lines, font, y, fill, W, line_h):
|
|
| 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')
|
|
@@ -60,7 +55,6 @@ def final_make_frame(post,seg,idx,total,img_path,out_path):
|
|
| 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]
|
|
@@ -69,55 +63,38 @@ def final_make_frame(post,seg,idx,total,img_path,out_path):
|
|
| 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 |
-
|
| 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>';}
|
|
@@ -128,10 +105,26 @@ setTimeout(hideTopicControls,200);setInterval(hideTopicControls,1000);
|
|
| 128 |
</script>
|
| 129 |
'''
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
@app.get('/')
|
| 132 |
async def index_final():
|
| 133 |
-
|
| 134 |
-
|
| 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)
|
|
|
|
| 1 |
"""Final runtime overrides for VNEWS AI UI and short-video rendering."""
|
| 2 |
+
import os, re, requests
|
| 3 |
import ai_runtime as rt
|
| 4 |
from ai_runtime import app
|
| 5 |
from fastapi.responses import HTMLResponse
|
|
|
|
| 8 |
except Exception:
|
| 9 |
Image = ImageDraw = ImageFont = None
|
| 10 |
|
| 11 |
+
RESTORE_INDEX_URL = "https://huggingface.co/spaces/bep40/vnews/raw/restore-33c3dda/static/index.html"
|
| 12 |
+
|
| 13 |
|
| 14 |
def clean(s):
|
| 15 |
import html as html_lib
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
def final_make_frame(post,seg,idx,total,img_path,out_path):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
if Image is None:
|
| 36 |
return rt.make_frame(post,seg,idx,total,img_path,out_path)
|
| 37 |
+
W,H=1080,1920;hero_h=760
|
|
|
|
| 38 |
bg=Image.new('RGB',(W,H),(12,12,12))
|
| 39 |
try:
|
| 40 |
im=Image.open(img_path).convert('RGB')
|
|
|
|
| 55 |
fsmall=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',28)
|
| 56 |
except Exception:
|
| 57 |
fb=ft=fs=fsmall=None
|
|
|
|
| 58 |
badge='Nguồn: '+rt._source_badge(post)
|
| 59 |
try:
|
| 60 |
b=draw.textbbox((0,0),badge,font=fsmall);bw=b[2]-b[0];bh=b[3]-b[1]
|
|
|
|
| 63 |
bx=W-bw-42;by=24
|
| 64 |
draw.rounded_rectangle((bx-16,by-8,W-24,by+bh+14),radius=18,fill=(0,0,0))
|
| 65 |
draw.text((bx,by),badge,fill=(255,255,255),font=fsmall)
|
|
|
|
| 66 |
draw.rectangle((0,hero_h-20,W,H),fill=(12,12,12))
|
| 67 |
+
total=max(1,total);total_w=total*38-14;start=(W-total_w)//2
|
|
|
|
| 68 |
for i in range(total):
|
| 69 |
fill=(92,184,122) if i==idx else (70,70,70)
|
| 70 |
draw.rounded_rectangle((start+i*38,820,start+i*38+24,832),radius=6,fill=fill)
|
| 71 |
brand='VNEWS AI SHORT'
|
| 72 |
try:
|
| 73 |
bb=draw.textbbox((0,0),brand,font=ft);tx=(W-(bb[2]-bb[0]))//2
|
| 74 |
+
except Exception:tx=360
|
|
|
|
| 75 |
draw.text((tx,870),brand,fill=(110,231,143),font=ft)
|
|
|
|
| 76 |
seg=_strip_bullet_prefix(seg)
|
| 77 |
lines=rt.wrap_text(draw,seg,fb,W-120,8)
|
| 78 |
+
block_h=len(lines)*74;y=max(980,1250-block_h//2)
|
|
|
|
| 79 |
_draw_center(draw,lines,fb,y,(255,255,255),W,74)
|
|
|
|
| 80 |
title_lines=rt.wrap_text(draw,_strip_bullet_prefix(post.get('title','')),fs,W-120,3)
|
| 81 |
y2=1640
|
| 82 |
draw.line((80,y2-26,W-80,y2-26),fill=(70,70,70),width=2)
|
| 83 |
_draw_center(draw,title_lines,fs,y2,(220,220,220),W,42)
|
| 84 |
bg.save(out_path,quality=92)
|
| 85 |
|
|
|
|
|
|
|
| 86 |
rt.make_frame = final_make_frame
|
| 87 |
|
|
|
|
| 88 |
app.router.routes=[r for r in app.router.routes if not (getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set()))]
|
| 89 |
|
| 90 |
FINAL_INJECT = r'''
|
| 91 |
<style>
|
| 92 |
+
#ai-topic-input{display:none!important}button[onclick*="createTopicPost"],*[onclick*="createTopicPost"]{display:none!important}.ai-topic-row,.topic-row,.ai-compose-topic{display:none!important}.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}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
</style>
|
| 94 |
<script>
|
| 95 |
(function(){
|
| 96 |
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 97 |
+
function hideTopicControls(){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';});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';});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);}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
window.createTopicPost=function(){alert('Đã tắt ô nhập chủ đề. Vui lòng dán URL bài viết.');};
|
| 99 |
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'));};
|
| 100 |
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>';}
|
|
|
|
| 105 |
</script>
|
| 106 |
'''
|
| 107 |
|
| 108 |
+
def _load_index_html():
|
| 109 |
+
try:
|
| 110 |
+
with open('/app/static/index.html','r',encoding='utf-8') as f:
|
| 111 |
+
html=f.read()
|
| 112 |
+
except Exception:
|
| 113 |
+
html=''
|
| 114 |
+
# main currently has a placeholder in some deployments; restore full UI from branch if needed
|
| 115 |
+
if '<!DOCTYPE html>' not in html or '<div id="view-home"' not in html:
|
| 116 |
+
try:
|
| 117 |
+
r=requests.get(RESTORE_INDEX_URL,timeout=20)
|
| 118 |
+
if r.status_code==200 and '<!DOCTYPE html>' in r.text:
|
| 119 |
+
html=r.text
|
| 120 |
+
except Exception:
|
| 121 |
+
pass
|
| 122 |
+
if '<!DOCTYPE html>' not in html:
|
| 123 |
+
html='<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>VNEWS</title></head><body><div id="view-home">VNEWS</div></body></html>'
|
| 124 |
+
return html
|
| 125 |
+
|
| 126 |
@app.get('/')
|
| 127 |
async def index_final():
|
| 128 |
+
html=_load_index_html()
|
| 129 |
+
body=getattr(rt.old,'PATCH_INJECT','') + FINAL_INJECT
|
|
|
|
|
|
|
| 130 |
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|