bep40 commited on
Commit
ffa275a
·
verified ·
1 Parent(s): 0dabb0a

Restore ai_runtime_final.py from c93b544 - final runtime overrides with article-only images

Browse files
Files changed (1) hide show
  1. ai_runtime_final.py +1 -141
ai_runtime_final.py CHANGED
@@ -15,7 +15,6 @@ RESTORE_INDEX_URL = "https://huggingface.co/spaces/bep40/vnews/raw/restore-33c3d
15
  SPACE_URL = "https://bep40-vnews.hf.space"
16
  DEFAULT_IMG = "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg"
17
 
18
- # Only voices that support Vietnamese reliably. Extra labels map to these Vietnamese neural voices.
19
  VN_VOICES = {
20
  "nu": "vi-VN-HoaiMyNeural", "female": "vi-VN-HoaiMyNeural", "hoaimy": "vi-VN-HoaiMyNeural",
21
  "nu-tre": "vi-VN-HoaiMyNeural", "nu-truyen-cam": "vi-VN-HoaiMyNeural", "nu-tin-nhanh": "vi-VN-HoaiMyNeural",
@@ -35,7 +34,7 @@ def _domain(url):
35
 
36
 
37
  def _strip_bullet_prefix(s):
38
- return clean(re.sub(r'^[\s•\-\*·▪▫●○\d\.\)\(]+', '', s or ''))
39
 
40
 
41
  def _source_badge_url_first(post):
@@ -60,7 +59,6 @@ def _abs_url(src, base_url):
60
 
61
  def _article_content_block(soup):
62
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','iframe']):tag.decompose()
63
- # Aggressively remove related/ad/recommend containers before image collection.
64
  bad_re=re.compile(r'(related|relate|recommend|suggest|sidebar|ads|advert|popular|more|xem-them|xemthem|tin-lien-quan|tinlienquan|doc-them|docthem|other-news|news-other|article-related|box-tin|box_related|story-related|recommend-news|same-category|cate-list|news-list|most-view|banner|qc|quang-cao|sponsor)',re.I)
65
  for el in list(soup.find_all(True)):
66
  cls=' '.join(el.get('class',[])); eid=el.get('id',''); role=el.get('role','')
@@ -94,7 +92,6 @@ def _image_is_likely_article(im, src):
94
 
95
 
96
  def _article_only_images(url):
97
- """Collect images only inside main article content. If uncertain, return fewer/no images rather than related/ad images."""
98
  imgs=[]
99
  try:
100
  from bs4 import BeautifulSoup
@@ -102,7 +99,6 @@ def _article_only_images(url):
102
  soup=BeautifulSoup(r.text,'lxml')
103
  block=_article_content_block(soup)
104
  candidates=[]
105
- # Prefer figure/picture under article body; then direct img in body.
106
  for el in block.find_all(['figure','picture'],recursive=True):
107
  im=el.find('img')
108
  if im:candidates.append(im)
@@ -115,12 +111,10 @@ def _article_only_images(url):
115
  else:src=src.strip().split(' ')[0]
116
  src=_abs_url(src,url)
117
  if src in seen or not _image_is_likely_article(im,src):continue
118
- # parent text guard: skip images from any remaining related block
119
  parent_txt=' '.join((im.parent.get('class',[]) if im.parent else []))+' '+(im.parent.get('id','') if im.parent else '')
120
  if re.search(r'(related|recommend|tin-lien-quan|doc-them|xem-them|popular|ads|banner)',parent_txt,re.I):continue
121
  seen.add(src);imgs.append(src)
122
  if len(imgs)>=20:break
123
- # Use og:image ONLY as article main image fallback when no body image found.
124
  if not imgs:
125
  og=soup.find('meta',property='og:image') or soup.find('meta',attrs={'name':'twitter:image'})
126
  if og:
@@ -154,7 +148,6 @@ def _download_image_safe(url, fallback_title, out_path):
154
  r=requests.get(url,headers=getattr(base,'HEADERS',{}),timeout=18)
155
  if r.status_code==200 and len(r.content)>1200:
156
  with open(out_path,'wb') as f:f.write(r.content)
157
- # verify PIL opens it
158
  if Image:
159
  Image.open(out_path).verify()
160
  return out_path
@@ -167,7 +160,6 @@ def _download_image_safe(url, fallback_title, out_path):
167
 
168
  def final_make_tts(text,voice,out_path):
169
  text=_strip_bullet_prefix(text) or 'Bản tin VNEWS.'
170
- # Only Vietnamese voices. Unknown choices fall back to Vietnamese female.
171
  edge_voice=VN_VOICES.get(str(voice or '').lower().strip(), 'vi-VN-HoaiMyNeural')
172
  for ev in [edge_voice, 'vi-VN-HoaiMyNeural', 'vi-VN-NamMinhNeural']:
173
  try:
@@ -178,7 +170,6 @@ def final_make_tts(text,voice,out_path):
178
  base.gTTS(text,lang='vi',tld='com.vn',slow=False).save(out_path)
179
  if os.path.exists(out_path) and os.path.getsize(out_path)>1000:return out_path
180
  except Exception:pass
181
- # Last-resort silent audio guarantees short generation succeeds.
182
  subprocess.run(['ffmpeg','-y','-f','lavfi','-i','anullsrc=channel_layout=stereo:sample_rate=44100','-t','3','-q:a','9','-acodec','libmp3lame',out_path],stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=30)
183
  return out_path
184
 
@@ -219,138 +210,7 @@ def final_make_frame(post,seg,idx,total,img_path,out_path):
219
  title_lines=rt.wrap_text(draw,_strip_bullet_prefix(post.get('title','')),fs,W-120,3);y2=1640;draw.line((80,y2-26,W-80,y2-26),fill=(70,70,70),width=2);_draw_center(draw,title_lines,fs,y2,(220,220,220),W,42)
220
  bg.save(out_path,quality=92)
221
 
222
- # Monkey patches for old functions.
223
  rt.make_frame=final_make_frame;rt.make_tts=final_make_tts;rt._source_badge=_source_badge_url_first
224
 
225
- # Override endpoints.
226
  _PATCH={('/api/url_wall','POST'),('/api/rewrite_share','POST'),('/api/ai/url','POST'),('/api/ai/short/{post_id}','POST'),('/','GET'),('/aw','GET')}
227
  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)]
228
-
229
- @app.post('/api/url_wall')
230
- async def final_url_wall(request:Request):
231
- body=await request.json();url=base._clean_text(body.get('url',''))
232
- if not url.startswith('http'):return JSONResponse({'error':'missing url'},status_code=400)
233
- try:data=_scrape_url_article_only(url)
234
- except Exception as e:return JSONResponse({'error':'Không scrape được URL: '+str(e)[:180]},status_code=422)
235
- raw=(data.get('summary','')+'\n'+data.get('text','')).strip()
236
- if len(raw)<120:return JSONResponse({'error':'URL không có đủ nội dung để tóm tắt'},status_code=422)
237
- prompt=f"""Tóm tắt bài viết nguồn dưới đây để đăng lên Tường AI VNEWS.
238
-
239
- Yêu cầu:
240
- - Chỉ tóm tắt nội dung chính, không viết lại toàn bộ bài.
241
- - Ngắn gọn, cụ thể, dễ hiểu.
242
- - Không lặp ý, không thêm chi tiết ngoài nguồn.
243
- - Tối đa 5 ý chính hoặc 2 đoạn ngắn.
244
- - Hạn chế dùng dấu đầu dòng.
245
-
246
- Tiêu đề gốc: {data.get('title','')}
247
- Nguồn: {_domain(url)}
248
- Nội dung gốc:
249
- {raw[:16000]}"""
250
- text=await base.qwen_generate(prompt,image_url=(data.get('image') or None),max_tokens=900)
251
- if not text:text=rt.old._fallback_summary_from_prompt(prompt,max_units=5) if hasattr(rt.old,'_fallback_summary_from_prompt') else raw[:900]
252
- text=rt.postprocess(text) if hasattr(rt,'postprocess') else text
253
- src=[{'title':data.get('title'), 'url':url, 'excerpt':raw[:500], 'via':_domain(url)}]
254
- if 'Nguồn tham khảo:' not in text:text+='\n\n'+rt.source_line(src)
255
- imgs=data.get('images') or []
256
- post=base.make_post(data.get('title') or 'Bài viết',text,imgs[0] if imgs else '',url,'url',sources=src)
257
- post['images']=imgs
258
- posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
259
- return JSONResponse({'post':post})
260
-
261
- @app.post('/api/rewrite_share')
262
- async def final_rewrite_share(request:Request):return await final_url_wall(request)
263
- @app.post('/api/ai/url')
264
- async def final_ai_url(request:Request):return await final_url_wall(request)
265
-
266
- @app.post('/api/ai/short/{post_id}')
267
- async def final_short(post_id:str,request:Request):
268
- try:body=await request.json()
269
- except Exception:body={}
270
- voice=str(body.get('voice','nu')).lower().strip();emotion=str(body.get('emotion','neutral')).lower().strip();speed=max(0.85,min(1.35,float(body.get('speed',1.2) or 1.2)))
271
- posts=base._load_ai_wall();post=next((p for p in posts if str(p.get('id'))==str(post_id)),None)
272
- if not post:return JSONResponse({'error':'post not found'},status_code=404)
273
- segs=rt.split_segments(post,8) if hasattr(rt,'split_segments') else [_strip_bullet_prefix(post.get('text') or post.get('title') or 'VNEWS')]
274
- imgs=[u for u in (post.get('images') or []) if u] or ([post.get('img')] if post.get('img') else [])
275
- os.makedirs(base.SHORTS_DIR,exist_ok=True);suffix=f'_{voice}_{emotion}_{str(speed).replace(".","p")}_articleimgs_vivoice'
276
- out=os.path.join(base.SHORTS_DIR,base._safe_name(post_id+suffix)+'.mp4')
277
- if os.path.exists(out):
278
- post['video']='/api/ai/short-file/'+post_id+suffix;base._save_ai_wall(posts);return JSONResponse({'video':post['video'],'segments':len(segs),'subtitles':False})
279
- work=os.path.join(base.SHORTS_DIR,base._safe_name(post_id+suffix));os.makedirs(work,exist_ok=True)
280
- clips=[]
281
- try:
282
- for i,seg in enumerate(segs):
283
- img_url=imgs[i % len(imgs)] if imgs else ''
284
- img=os.path.join(work,f'image_{i}.jpg');frame=os.path.join(work,f'f{i}.jpg');aud=os.path.join(work,f'a{i}.mp3');aud2=os.path.join(work,f'a{i}_fast.mp3');clip=os.path.join(work,f'c{i}.mp4')
285
- _download_image_safe(img_url,post.get('title','AI news'),img)
286
- seg=_strip_bullet_prefix(seg);final_make_frame(post,seg,i,len(segs),img,frame)
287
- prefix={'urgent':'Tin nhanh.','warm':'Câu chuyện đáng chú ý.','serious':'Bản tin nghiêm túc.','energetic':'Cập nhật nổi bật.'}.get(emotion,'')
288
- spoken=(prefix+' '+seg).strip() if i==0 and prefix else seg
289
- final_make_tts(spoken,voice,aud)
290
- try:subprocess.run(['ffmpeg','-y','-i',aud,'-filter:a',f'atempo={speed}','-vn',aud2],check=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=120)
291
- except Exception:aud2=aud
292
- try:
293
- subprocess.run(['ffmpeg','-y','-loop','1','-i',frame,'-i',aud2,'-shortest','-c:v','libx264','-tune','stillimage','-pix_fmt','yuv420p','-c:a','aac','-b:a','128k','-vf','scale=1080:1920',clip],check=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=180)
294
- except Exception:
295
- # last-resort visual-only 4s clip
296
- subprocess.run(['ffmpeg','-y','-loop','1','-t','4','-i',frame,'-f','lavfi','-i','anullsrc=channel_layout=stereo:sample_rate=44100','-shortest','-c:v','libx264','-pix_fmt','yuv420p','-c:a','aac','-vf','scale=1080:1920',clip],check=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=120)
297
- clips.append(clip)
298
- lf=os.path.join(work,'list.txt')
299
- with open(lf,'w',encoding='utf-8') as f:
300
- for c in clips:f.write("file '"+c.replace("'","'\\''")+"'\n")
301
- subprocess.run(['ffmpeg','-y','-f','concat','-safe','0','-i',lf,'-c','copy',out],check=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=240)
302
- post['video']='/api/ai/short-file/'+post_id+suffix;post['short_subtitles']=False;post['short_segments']=segs;post['short_speed']=speed;base._save_ai_wall(posts)
303
- return JSONResponse({'video':post['video'],'segments':len(segs),'subtitles':False})
304
- except Exception as e:return JSONResponse({'error':'Không tạo được shorts: '+str(e)[:220]},status_code=500)
305
-
306
- @app.get('/aw')
307
- def ai_wall_share(post:str=Query(default=''), short:int=Query(default=0)):
308
- posts=base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==str(post)),None)
309
- if not p:return HTMLResponse(f'<script>location.href="{SPACE_URL}"</script>')
310
- title=p.get('title') or 'VNEWS AI';img=p.get('img') or DEFAULT_IMG
311
- desc=(p.get('text') or '')[:220]
312
- return HTMLResponse(f'''<!doctype html><html><head><meta charset="utf-8"><title>{title}</title><meta property="og:title" content="{title}"><meta property="og:description" content="{desc}"><meta property="og:image" content="{img}"><meta property="og:type" content="article"><meta name="twitter:card" content="summary_large_image"></head><body><script>localStorage.setItem('pending_ai_post','{post}');location.href='{SPACE_URL}'</script></body></html>''')
313
-
314
- FINAL_INJECT = r'''
315
- <style>
316
- #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-compose{width:calc(100% - 8px)!important}.ai-compose-row:has(#ai-url-input){display:flex!important;flex-direction:column!important;gap:8px!important;width:100%!important}.ai-compose-row:has(#ai-url-input) button{width:100%!important;display:block!important}.ai-compose-row:has(#ai-url-input) input,#ai-url-input{display:block!important;width:100%!important;max-width:none!important;flex:1 1 100%!important;box-sizing:border-box!important}.ai-url-only-note{font-size:11px;color:#888;margin:5px 0 8px;width:100%}.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}.ai-wall-final{margin:6px 4px;background:#1a1a1a;border:1px solid #2a2a2a;border-radius:8px;overflow:hidden}
317
- </style>
318
- <script>
319
- (function(){
320
- function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));}
321
- let finalWall=[];
322
- 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; ảnh chỉ lấy từ nội dung bài, không lấy bài liên quan/quảng cáo.';url.insertAdjacentElement('afterend',n);}}
323
- function voiceOptions(){return `<option value="nu">Nữ Việt - Hoài My</option><option value="nam">Nam Việt - Nam Minh</option><option value="nu-truyen-cam">Nữ Việt truyền cảm</option><option value="nu-tin-nhanh">Nữ Việt tin nhanh</option><option value="nam-tram">Nam Việt trầm</option><option value="nam-ban-tin">Nam Việt bản tin</option><option value="nam-nang-dong">Nam Việt năng động</option>`}
324
- function addVoiceOptions(){document.querySelectorAll('#ai-short-voice, select[id*=voice]').forEach(sel=>{let old=sel.value||'nu';sel.innerHTML=voiceOptions();sel.value=[...sel.options].some(o=>o.value===old)?old:'nu';});}
325
- function shareAI(p,isShort){let url=location.origin+'/aw?post='+encodeURIComponent(p.id)+(isShort?'&short=1':'');let title=(isShort?'🎬 Short AI: ':'🧱 Tường AI: ')+(p.title||'VNEWS');if(navigator.share)navigator.share({title,url}).catch(()=>{});else navigator.clipboard.writeText(url).then(()=>alert('Đã sao chép link chia sẻ!')).catch(()=>{});}
326
- 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>';}
327
- async function loadWall(){try{finalWall=(await (await fetch('/api/ai_wall')).json()).posts||[];renderWall();let pend=localStorage.getItem('pending_ai_post');if(pend){localStorage.removeItem('pending_ai_post');let idx=finalWall.findIndex(p=>String(p.id)===String(pend));if(idx>=0)setTimeout(()=>readFinalWall(idx),300);}}catch(e){}}
328
- function renderWall(){let home=document.getElementById('view-home');if(!home||!finalWall.length)return;document.getElementById('ai-wall-final')?.remove();let wrap=document.createElement('div');wrap.id='ai-wall-final';wrap.className='ai-wall-final';let h='<div class="slider-header"><span class="slider-label">🧱 Tường AI</span><span class="slider-note">URL summary</span></div><div class="slider-track">';finalWall.slice(0,30).forEach((p,i)=>{h+=`<div class="wall-item"><div class="wall-thumb">${p.img?`<img src="${esc(p.img)}">`:''}</div><div class="wall-title">${esc(p.title)}</div><div class="wall-text">${esc(p.text)}</div><div class="wall-actions"><button class="primary" onclick="readFinalWall(${i})">Xem</button><button onclick="shareFinalWall(${i})">📤 Chia sẻ</button>${p.video?`<button onclick="readFinalShort(${i})">Short</button>`:''}</div></div>`});h+='</div>';wrap.innerHTML=h;let compose=document.querySelector('.ai-compose');if(compose)compose.after(wrap);else home.prepend(wrap);}
329
- window.shareFinalWall=function(i){let p=finalWall[i];if(p)shareAI(p,false)};
330
- window.shareFinalShort=function(i){let p=finalWall[i];if(p)shareAI(p,true)};
331
- window.readFinalWall=window.aiReadWallPatched=window.aiReadWall=function(i){let p=finalWall[i]||(window.patchedWall||window.aiWall||[])[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 voiceBox=`<div class="article-actions"><select id="ai-short-voice">${voiceOptions()}</select><select id="ai-short-emotion"><option value="neutral">Trung tính</option><option value="urgent">Tin nhanh</option><option value="warm">Ấm áp</option><option value="serious">Nghiêm túc</option><option value="energetic">Sôi nổi</option></select><button onclick="makeFinalShort(${i})">🎬 Tạo short</button><button onclick="shareFinalWall(${i})">📤 Chia sẻ bài</button>${p.video?`<button onclick="shareFinalShort(${i})">📤 Chia sẻ short</button>`:''}</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>`:''}</div>${voiceBox}</div>`;document.getElementById('view-article').innerHTML=h;addVoiceOptions();window.scrollTo(0,0)};
332
- window.readFinalShort=function(i){let p=finalWall[i];if(!p||!p.video)return;showView('view-article');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">Short AI</span><h1 class="article-title">${esc(p.title)}</h1><video class="article-img" src="${p.video}" controls playsinline autoplay></video><div class="article-actions"><button onclick="shareFinalShort(${i})">📤 Chia sẻ short</button><button onclick="readFinalWall(${i})">Xem bài tường</button></div></div>`;window.scrollTo(0,0)};
333
- window.makeFinalShort=window.aiMakeShortPatched=async function(i){let p=finalWall[i]||(window.patchedWall||window.aiWall||[])[i];if(!p)return;let voice=document.getElementById('ai-short-voice')?.value||'nu';let emotion=document.getElementById('ai-short-emotion')?.value||'neutral';let btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tạo...'}try{let r=await fetch('/api/ai/short/'+p.id,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({voice,emotion,speed:1.2})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi tạo short');p.video=j.video;let idx=finalWall.findIndex(x=>x.id===p.id);if(idx<0){finalWall.unshift(p);idx=0}renderWall();readFinalWall(idx);alert('Đã tạo short AI và cập nhật ngay trên tường.');}catch(e){alert('Không tạo được short: '+e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🎬 Tạo short'}}};
334
- window.createTopicPost=function(){alert('Đã tắt ô nhập chủ đề. Vui lòng dán URL bài viết.');};
335
- window.createUrlPost=async 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://');let btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tóm tắt...'}try{let r=await fetch('/api/url_wall',{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 URL');finalWall.unshift(j.post);if(inp)inp.value='';renderWall();readFinalWall(0);alert('Đã đăng tường AI, không cần tải lại trang.');}catch(e){alert(e.message||'Lỗi URL')}finally{if(btn){btn.disabled=false;btn.textContent='Chèn URL'}}};
336
- setTimeout(()=>{hideTopicControls();addVoiceOptions();loadWall();},300);setInterval(()=>{hideTopicControls();addVoiceOptions();},1200);
337
- })();
338
- </script>
339
- '''
340
-
341
- def _load_index_html():
342
- try:
343
- with open('/app/static/index.html','r',encoding='utf-8') as f:html=f.read()
344
- except Exception:html=''
345
- if '<!DOCTYPE html>' not in html or '<div id="view-home"' not in html:
346
- try:
347
- r=requests.get(RESTORE_INDEX_URL,timeout=20)
348
- if r.status_code==200 and '<!DOCTYPE html>' in r.text:html=r.text
349
- except Exception:pass
350
- if '<!DOCTYPE html>' not in html: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>'
351
- return html
352
-
353
- @app.get('/')
354
- async def index_final():
355
- html=_load_index_html();body=getattr(rt.old,'PATCH_INJECT','') + FINAL_INJECT
356
- return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
 
15
  SPACE_URL = "https://bep40-vnews.hf.space"
16
  DEFAULT_IMG = "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg"
17
 
 
18
  VN_VOICES = {
19
  "nu": "vi-VN-HoaiMyNeural", "female": "vi-VN-HoaiMyNeural", "hoaimy": "vi-VN-HoaiMyNeural",
20
  "nu-tre": "vi-VN-HoaiMyNeural", "nu-truyen-cam": "vi-VN-HoaiMyNeural", "nu-tin-nhanh": "vi-VN-HoaiMyNeural",
 
34
 
35
 
36
  def _strip_bullet_prefix(s):
37
+ return clean(re.sub(r'^[\s\•\-\*·▪▫●○\d\.\)\(]+', '', s or ''))
38
 
39
 
40
  def _source_badge_url_first(post):
 
59
 
60
  def _article_content_block(soup):
61
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','iframe']):tag.decompose()
 
62
  bad_re=re.compile(r'(related|relate|recommend|suggest|sidebar|ads|advert|popular|more|xem-them|xemthem|tin-lien-quan|tinlienquan|doc-them|docthem|other-news|news-other|article-related|box-tin|box_related|story-related|recommend-news|same-category|cate-list|news-list|most-view|banner|qc|quang-cao|sponsor)',re.I)
63
  for el in list(soup.find_all(True)):
64
  cls=' '.join(el.get('class',[])); eid=el.get('id',''); role=el.get('role','')
 
92
 
93
 
94
  def _article_only_images(url):
 
95
  imgs=[]
96
  try:
97
  from bs4 import BeautifulSoup
 
99
  soup=BeautifulSoup(r.text,'lxml')
100
  block=_article_content_block(soup)
101
  candidates=[]
 
102
  for el in block.find_all(['figure','picture'],recursive=True):
103
  im=el.find('img')
104
  if im:candidates.append(im)
 
111
  else:src=src.strip().split(' ')[0]
112
  src=_abs_url(src,url)
113
  if src in seen or not _image_is_likely_article(im,src):continue
 
114
  parent_txt=' '.join((im.parent.get('class',[]) if im.parent else []))+' '+(im.parent.get('id','') if im.parent else '')
115
  if re.search(r'(related|recommend|tin-lien-quan|doc-them|xem-them|popular|ads|banner)',parent_txt,re.I):continue
116
  seen.add(src);imgs.append(src)
117
  if len(imgs)>=20:break
 
118
  if not imgs:
119
  og=soup.find('meta',property='og:image') or soup.find('meta',attrs={'name':'twitter:image'})
120
  if og:
 
148
  r=requests.get(url,headers=getattr(base,'HEADERS',{}),timeout=18)
149
  if r.status_code==200 and len(r.content)>1200:
150
  with open(out_path,'wb') as f:f.write(r.content)
 
151
  if Image:
152
  Image.open(out_path).verify()
153
  return out_path
 
160
 
161
  def final_make_tts(text,voice,out_path):
162
  text=_strip_bullet_prefix(text) or 'Bản tin VNEWS.'
 
163
  edge_voice=VN_VOICES.get(str(voice or '').lower().strip(), 'vi-VN-HoaiMyNeural')
164
  for ev in [edge_voice, 'vi-VN-HoaiMyNeural', 'vi-VN-NamMinhNeural']:
165
  try:
 
170
  base.gTTS(text,lang='vi',tld='com.vn',slow=False).save(out_path)
171
  if os.path.exists(out_path) and os.path.getsize(out_path)>1000:return out_path
172
  except Exception:pass
 
173
  subprocess.run(['ffmpeg','-y','-f','lavfi','-i','anullsrc=channel_layout=stereo:sample_rate=44100','-t','3','-q:a','9','-acodec','libmp3lame',out_path],stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=30)
174
  return out_path
175
 
 
210
  title_lines=rt.wrap_text(draw,_strip_bullet_prefix(post.get('title','')),fs,W-120,3);y2=1640;draw.line((80,y2-26,W-80,y2-26),fill=(70,70,70),width=2);_draw_center(draw,title_lines,fs,y2,(220,220,220),W,42)
211
  bg.save(out_path,quality=92)
212
 
 
213
  rt.make_frame=final_make_frame;rt.make_tts=final_make_tts;rt._source_badge=_source_badge_url_first
214
 
 
215
  _PATCH={('/api/url_wall','POST'),('/api/rewrite_share','POST'),('/api/ai/url','POST'),('/api/ai/short/{post_id}','POST'),('/','GET'),('/aw','GET')}
216
  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)]