bep40 commited on
Commit
c53c3ff
·
verified ·
1 Parent(s): a27cf19

Final runtime overrides for URL-only AI, image gallery, and short layout

Browse files
Files changed (1) hide show
  1. 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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)