Spaces:
Running
Running
Upload ai_runtime_patch_fast.py
Browse files- ai_runtime_patch_fast.py +489 -24
ai_runtime_patch_fast.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
| 1 |
-
"""Final patch
|
| 2 |
-
import re, threading, time, json, os, asyncio
|
| 3 |
import ai_runtime_final6 as f6
|
| 4 |
from ai_runtime_final6 import app, rt, f5, HTMLResponse, JSONResponse, Request, Query
|
|
|
|
| 5 |
import html as html_lib
|
| 6 |
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
def clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
|
| 9 |
def _domain(u):
|
|
@@ -13,6 +20,8 @@ DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
|
|
| 13 |
os.makedirs(DATA_DIR,exist_ok=True)
|
| 14 |
SHORT_COMMENTS_FILE=os.path.join(DATA_DIR,'short_comments.json')
|
| 15 |
TTL_24H=86400;HAS_PERSISTENT=os.path.isdir('/data')
|
|
|
|
|
|
|
| 16 |
def _lj(p,d):
|
| 17 |
try:
|
| 18 |
if os.path.exists(p):return json.load(open(p,'r',encoding='utf-8'))
|
|
@@ -27,6 +36,413 @@ def _cleanup():
|
|
| 27 |
def _scrape(url,mc=8000):
|
| 28 |
try:d=f5.base.scrape_any_url(url);return(d.get('title',''),((d.get('summary','')+'\n'+d.get('text','')).strip())[:mc],d.get('image') or d.get('og_image') or '')
|
| 29 |
except:return('','','')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
_bg_home={"t":0,"d":[]};_bg_shorts={"t":0,"d":[]};_bg_lock=False
|
| 31 |
def _bg():
|
| 32 |
global _bg_lock
|
|
@@ -43,7 +459,7 @@ def _bg():
|
|
| 43 |
@app.on_event("startup")
|
| 44 |
async def _s():threading.Thread(target=_bg,daemon=True).start()
|
| 45 |
threading.Thread(target=lambda:[time.sleep(600) or _bg() for _ in iter(int,1)],daemon=True).start()
|
| 46 |
-
app.router.routes=[r for r in app.router.routes if not (getattr(r,'path',None) in ('/api/homepage','/api/shorts','/api/ai_wall','/api/topic_post','/api/article/ask','/api/topic/rewrite','/api/rewrite_share','/api/url_wall','/api/short/comments','/api/short/comment','/api/storage_status','/') and any(m in getattr(r,'methods',set()) for m in ('GET','POST')))]
|
| 47 |
@app.get('/api/homepage')
|
| 48 |
def _h():
|
| 49 |
n=time.time()
|
|
@@ -75,6 +491,48 @@ async def _ask(request:Request):
|
|
| 75 |
if not raw:raw=ctx[:12000]
|
| 76 |
ans=await f5.base.qwen_generate(f'Bạn là VNEWS AI. Nội dung: "{title}"\n{raw[:9000]}\n\nHỏi: "{q}"\n\nTrả lời tự nhiên bằng tiếng Việt.',max_tokens=1200)
|
| 77 |
return JSONResponse({'answer':ans or 'Chưa trả lời được.','title':title})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
@app.post('/api/rewrite_share')
|
| 79 |
@app.post('/api/url_wall')
|
| 80 |
async def _rw(request:Request):
|
|
@@ -83,12 +541,33 @@ async def _rw(request:Request):
|
|
| 83 |
title,raw,img=_scrape(url,14000)
|
| 84 |
if len(raw)<50:raw=ctx[:14000]
|
| 85 |
if len(raw)<50:return JSONResponse({'error':'Không đọc được bài'},status_code=422)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
text=None
|
| 87 |
-
try:text=await asyncio.wait_for(f5.base.qwen_generate(f'Tóm tắt đăng Tường AI:\nTiêu đề: {title}\n{raw[:14000]}\n\n4-6 ý chính. Cuối ghi nguồn.',image_url=
|
| 88 |
except:pass
|
| 89 |
if not text or len(text)<80:text=f"Tóm tắt: {title}\n\n{raw[:1200]}\n\nNguồn: {_domain(url)}"
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
@app.post('/api/topic/rewrite')
|
| 93 |
async def _tr(request:Request):
|
| 94 |
b=await request.json();pid=str(b.get('post_id','')).strip()
|
|
@@ -128,7 +607,6 @@ PATCH_INJECT=r'''
|
|
| 128 |
.article-ai-ask{margin-top:12px;background:#141414;border:1px solid #2a2a2a;border-radius:10px;padding:10px}.article-ai-ask textarea{width:100%;min-height:60px;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px}.article-ai-ask button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin-top:6px}.article-ai-answer{white-space:pre-wrap;color:#ccc;font-size:13px;line-height:1.55;margin-top:8px}
|
| 129 |
.storage-warn{background:#332200;border:1px solid #664400;color:#ffcc00;padding:8px 12px;border-radius:8px;font-size:11px;margin:6px 4px}
|
| 130 |
button[onclick*="rewriteCurrentArticle"]{display:none!important}
|
| 131 |
-
/* Hide ALL old Short AI slides from previous layers */
|
| 132 |
#ai-short-home,.ai-short-home,.ai-short-card-final{display:none!important}
|
| 133 |
.source-detail-box a[target="_blank"]{display:none!important}
|
| 134 |
</style>
|
|
@@ -137,40 +615,27 @@ button[onclick*="rewriteCurrentArticle"]{display:none!important}
|
|
| 137 |
(function(){
|
| 138 |
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 139 |
fetch('/api/storage_status').then(r=>r.json()).then(j=>{if(!j.persistent){let h=document.getElementById('view-home');if(h){let w=document.createElement('div');w.className='storage-warn';w.innerHTML='⚠️ <b>Persistent Storage chưa bật.</b> Bật: Space Settings → Persistent Storage → Small.';h.prepend(w);}}});
|
| 140 |
-
|
| 141 |
-
// === Short AI Slide on homepage (same as Dantri shorts) ===
|
| 142 |
async function renderShortAISlide(){let home=document.getElementById('view-home');if(!home)return;document.getElementById('short-ai-final-slide')?.remove();let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let vids=wall.filter(p=>p.video);if(!vids.length)return;let wrap=document.createElement('div');wrap.id='short-ai-final-slide';wrap.className='slider-wrap';wrap.innerHTML='<div class="slider-header"><span class="slider-label">🎬 Short AI</span></div><div class="slider-track">'+vids.slice(0,30).map((p,i)=>`<div class="slider-item shorts-item" onclick="openAIShortFeed(${i})"><div class="slider-thumb shorts-thumb"><video src="${p.video}" muted preload="metadata" style="width:100%;height:100%;object-fit:cover"></video><div class="card-play">▶</div></div><div class="slider-title">${esc(p.title)}</div></div>`).join('')+'</div>';let comp=home.querySelector('.ai-compose');if(comp&&comp.nextSibling)comp.parentNode.insertBefore(wrap,comp.nextSibling);else home.prepend(wrap);}
|
| 143 |
-
setTimeout(renderShortAISlide,2500);
|
| 144 |
-
|
| 145 |
-
// === Source Details ===
|
| 146 |
function renderSourceDetails(post,container){let det=post.source_details||[];if(!det.length)return;container.querySelectorAll('.source-detail-box').forEach(e=>e.remove());let box=document.createElement('div');box.className='source-detail-box';box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:8px">📚 Bài nguồn</h3>'+det.map((s,i)=>`<div class="source-detail-item" data-url="${esc(s.url||'')}"><div class="source-detail-title">${i+1}. ${esc(s.title)}</div><div class="source-detail-content">${esc((s.content||'').slice(0,300))}</div><span class="source-vnews-btn">📖 Xem trên VNEWS</span></div>`).join('');container.appendChild(box);box.querySelectorAll('.source-detail-item').forEach(el=>{el.onclick=function(){let u=el.dataset.url;if(u&&typeof readArticle==='function')readArticle(u);}});det.forEach((s,i)=>{if(!s.url)return;fetch('/api/article?url='+encodeURIComponent(s.url)).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){let items=box.querySelectorAll('.source-detail-item');if(items[i]){let img=document.createElement('img');img.src=d.og_image||d.img;img.loading='lazy';img.onerror=function(){this.style.display='none'};items[i].prepend(img);}}}).catch(()=>{});});}
|
| 147 |
-
|
| 148 |
-
// === AI Wall Post View ===
|
| 149 |
async function readAIWallPost(i){let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let p=wall[i];if(!p)return;showView('view-article');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>${p.img?`<img class="article-img" src="${p.img}">`:''}`;h+=`<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>`;h+=`<div class="article-actions"><button class="primary" onclick="doRewriteTopic(this,'${esc(p.id)}')">🤖 Rewrite AI đăng tường</button>${p.video?`<button onclick="openAIShortFeed(${i})">🎬 Xem Short</button>`:''}<button onclick="doShare('${esc(p.title)}','${location.origin}','${esc(p.img||'')}')">📤</button></div>`;h+=`<div class="article-ai-ask"><h3 style="font-size:14px;color:#5cb87a">🤖 Hỏi AI</h3><textarea id="article-ai-q" placeholder="Hỏi về nội dung..."></textarea><button onclick="askAIWall(${i})">Hỏi</button><div id="article-ai-ans" class="article-ai-answer"></div></div></div>`;document.getElementById('view-article').innerHTML=h;let art=document.querySelector('.article-view');if(art)renderSourceDetails(p,art);window.scrollTo(0,0);}
|
| 150 |
window.readAIWallPost=readAIWallPost;window.aiReadWallPatched=window.aiReadWall=window.readWallPost=function(i){readAIWallPost(i)};
|
| 151 |
-
|
| 152 |
-
// === Short AI Feed: FULL interaction buttons like Dantri Shorts ===
|
| 153 |
window.openAIShortFeed=async function(startIdx){let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let vids=wall.filter(p=>p.video);if(!vids.length)return alert('Chưa có Short AI');let ordered=startIdx>0?vids.slice(startIdx).concat(vids.slice(0,startIdx)):vids;showView('view-tiktok');let h='<button class="back-btn" onclick="switchCat(\'home\')">← Short AI</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';ordered.forEach((p,i)=>{h+=`<div class="tiktok-slide" data-id="${p.id}"><video src="${p.video}" playsinline loop></video><div class="tiktok-bottom"><span class="badge badge-ai">AI Short</span><p class="tiktok-title">${esc(p.title)}</p></div><div class="tiktok-right"><button class="tiktok-right-btn" onclick="event.stopPropagation()"><div class="icon">👁</div><div class="count">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();likeShort('${p.id}',this)"><div class="icon">❤️</div><div class="count">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();openShortComments('${p.id}')"><div class="icon">💬</div><div class="count" id="cc-${p.id}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();shareShort('${esc(p.title)}')"><div class="icon">📤</div><div class="count">Share</div></button></div><span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`});h+='</div></div>';document.getElementById('view-tiktok').innerHTML=h;initShortFeed();ordered.forEach(p=>{fetch('/api/short/comments?id='+encodeURIComponent(p.id)).then(r=>r.json()).then(j=>{let el=document.getElementById('cc-'+p.id);if(el)el.textContent=(j.comments||[]).length}).catch(()=>{});});}
|
| 154 |
window.likeShort=function(id,btn){let c=btn.querySelector('.count');c.textContent=parseInt(c.textContent||0)+1;}
|
| 155 |
window.shareShort=function(title){if(navigator.share)navigator.share({title,url:location.href}).catch(()=>{});else{navigator.clipboard.writeText(location.href);alert('Đã sao chép link!');}}
|
| 156 |
function initShortFeed(){let feed=document.getElementById('tiktok-feed');if(!feed)return;let slides=feed.querySelectorAll('.tiktok-slide');let cur=-1;function act(i){if(i===cur)return;slides.forEach((sl,idx)=>{let v=sl.querySelector('video');let fr=sl.querySelector('iframe');if(idx===i){if(v)v.play().catch(()=>{});if(fr&&!fr.src&&fr.dataset.ytSrc)fr.src=fr.dataset.ytSrc;}else{if(v)v.pause();if(fr&&fr.src)fr.src='';}});cur=i}let t;feed.addEventListener('scroll',()=>{clearTimeout(t);t=setTimeout(()=>{let rect=feed.getBoundingClientRect(),ctr=rect.top+rect.height/2,b=-1,d=1e9;slides.forEach((sl,i)=>{let dd=Math.abs(sl.getBoundingClientRect().top+sl.getBoundingClientRect().height/2-ctr);if(dd<d){d=dd;b=i}});if(b>=0)act(b)},130)});setTimeout(()=>act(0),300);slides.forEach(sl=>{let v=sl.querySelector('video');if(v)v.addEventListener('click',e=>{e.preventDefault();v.paused?v.play().catch(()=>{}):v.pause()})});}
|
| 157 |
-
|
| 158 |
-
// === Handlers ===
|
| 159 |
window.doRewriteTopic=async function(btn,pid){btn.disabled=true;btn.textContent='Đang rewrite...';try{let r=await fetch('/api/topic/rewrite',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({post_id:pid})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Rewrite thành công!');showRewriteResult(j.post);}catch(e){alert(e.message)}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
|
| 160 |
window.doRewriteArticle=async function(btn){let url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){let a=document.querySelector('#view-article a[href*="://"]');if(a)url=a.href;}if(!url){let text=document.querySelector('.article-view')?.innerText?.slice(0,14000)||'';if(text.length<100){alert('Không tìm được nội dung để rewrite');return;}btn.disabled=true;btn.textContent='Đang rewrite...';try{let r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:'https://vnews.local/inline',context:text})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error);alert('Rewrite thành công!');showRewriteResult(j.post);}catch(e){alert(e.message)}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}return;}btn.disabled=true;btn.textContent='Đang rewrite...';try{let ctx=document.querySelector('.article-view')?.innerText?.slice(0,14000)||'';let r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,context:ctx})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Rewrite thành công!');showRewriteResult(j.post);}catch(e){alert(e.message)}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
|
| 161 |
function showRewriteResult(post){if(!post)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">Rewrite</span><h1 class="article-title">${esc(post.title)}</h1>${post.img?`<img class="article-img" src="${post.img}">`:''}` +`<p class="article-p" style="white-space:pre-wrap">${esc(post.text)}</p><div class="article-actions"><button class="primary" onclick="makeShortFromPost('${esc(post.id)}',this)">🎬 Tạo Short AI</button><button onclick="doShare('${esc(post.title)}','${location.origin}','${esc(post.img||'')}')">📤</button></div></div>`;window.scrollTo(0,0);}
|
| 162 |
-
window.makeShortFromPost=async function(pid,btn){if(btn){btn.disabled=true;btn.textContent='Đang tạo...';}try{let r=await fetch('/api/ai/short/'+pid,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({voice:'nu',emotion:'neutral',speed:1.2})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã tạo Short AI!');renderShortAISlide();}catch(e){alert(e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🎬 Tạo Short AI';}}};
|
| 163 |
window.rewriteCurrentArticle=function(){let btn=document.querySelector('[data-rw-article]');if(btn)doRewriteArticle(btn);};
|
| 164 |
window.askAIWall=async function(i){let q=document.getElementById('article-ai-q')?.value.trim();if(!q)return alert('Nhập câu hỏi');document.getElementById('article-ai-ans').textContent='Đang hỏi...';let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let p=wall[i]||{};let ctx=(p.text||'');for(let s of (p.source_details||[]))ctx+='\n'+(s.content||'');try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question:q,context:ctx.slice(0,12000)})});let j=await r.json();document.getElementById('article-ai-ans').textContent=j.answer||'Không trả lời được';}catch(e){document.getElementById('article-ai-ans').textContent='Lỗi: '+e.message}};
|
| 165 |
window.askArticleAI=async function(){let q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');let a=document.getElementById('article-ai-answer');a.textContent='Đang hỏi...';let url=(window._currentArticle&&window._currentArticle.url)||'';let ctx=document.querySelector('.article-view')?.innerText?.slice(0,12000)||'';try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,question:q,context:ctx})});let j=await r.json();a.textContent=j.answer||'Không trả lời được';}catch(e){a.textContent='Lỗi: '+e.message}};
|
| 166 |
window.openShortComments=async function(id){let panel=document.getElementById('short-cmt-panel');let j=await fetch('/api/short/comments?id='+encodeURIComponent(id)).then(r=>r.json()).catch(()=>({comments:[]}));panel.innerHTML=`<h3 style="color:#5cb87a">💬 Bình luận</h3><div id="cmt-list">${(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('')||'<div class="cmt-item" style="color:#777">Chưa có</div>'}</div><textarea id="cmt-text" placeholder="Bình luận..."></textarea><button onclick="submitShortCmt('${esc(id)}')">Gửi</button><button onclick="document.getElementById('short-cmt-panel').classList.remove('active')">Đóng</button>`;panel.classList.add('active');}
|
| 167 |
window.submitShortCmt=async function(id){let t=document.getElementById('cmt-text')?.value.trim();if(!t)return;let j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,text:t})}).then(r=>r.json()).catch(()=>({comments:[]}));document.getElementById('cmt-list').innerHTML=(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('');document.getElementById('cmt-text').value='';let el=document.getElementById('cc-'+id);if(el)el.textContent=(j.comments||[]).length;}
|
| 168 |
-
|
| 169 |
-
// === Patch regular articles ===
|
| 170 |
function patchArticle(){let art=document.querySelector('#view-article .article-view');if(!art)return;art.querySelectorAll('button[onclick*="rewriteCurrentArticle"],[data-rewrite],.rewrite-injected').forEach(e=>e.remove());art.querySelectorAll('.article-ai-ask').forEach((e,i)=>{if(i>0)e.remove();});if(!art.querySelector('[data-rw-article]')){let a=art.querySelector('.article-actions');if(a){let b=document.createElement('button');b.className='primary';b.setAttribute('data-rw-article','1');b.textContent='🤖 Rewrite AI đăng tường';b.onclick=function(){doRewriteArticle(b)};a.insertBefore(b,a.firstChild);}}if(!art.querySelector('.article-ai-ask')){let box=document.createElement('div');box.className='article-ai-ask';box.innerHTML='<h3 style="font-size:14px;color:#5cb87a">🤖 Hỏi AI</h3><textarea id="article-ai-question" placeholder="Hỏi..."></textarea><button onclick="askArticleAI()">Hỏi</button><div id="article-ai-answer" class="article-ai-answer"></div>';art.appendChild(box);}}
|
| 171 |
function patchShortBtns(){document.querySelectorAll('.tiktok-slide').forEach(sl=>{if(sl.dataset.cmtDone)return;sl.dataset.cmtDone='1';let id=sl.dataset.id||'';if(!id)return;let r=sl.querySelector('.tiktok-right');if(!r||r.querySelector('[data-cmt]'))return;let b=document.createElement('button');b.className='tiktok-right-btn';b.setAttribute('data-cmt','1');b.innerHTML='<div class="icon">💬</div><div class="count">0</div>';b.onclick=function(e){e.stopPropagation();openShortComments(id);};r.appendChild(b);fetch('/api/short/comments?id='+encodeURIComponent(id)).then(r=>r.json()).then(j=>{b.querySelector('.count').textContent=(j.comments||[]).length}).catch(()=>{});});}
|
| 172 |
function patchOldSourceLinks(){document.querySelectorAll('.source-detail-item a[target="_blank"],.source-detail-item a[href]').forEach(a=>{if(a.dataset.p7)return;a.dataset.p7='1';let url=a.href||'';a.removeAttribute('target');a.removeAttribute('href');a.textContent='📖 Xem trên VNEWS';a.className='source-vnews-btn';a.style.cursor='pointer';a.onclick=function(e){e.preventDefault();e.stopPropagation();if(url&&typeof readArticle==='function')readArticle(url);}});}
|
| 173 |
-
|
| 174 |
let oldRA=window.readArticle;if(oldRA){window.readArticle=async function(){let ret=await oldRA.apply(this,arguments);setTimeout(patchArticle,500);return ret;}}
|
| 175 |
let _hl=false;function dH(){if(_hl)return;_hl=true;setTimeout(()=>{if(typeof ensureHotTopics==='function')ensureHotTopics();if(typeof ensureNewsShortsHome==='function')ensureNewsShortsHome();},4000);}
|
| 176 |
if(document.readyState==='complete')dH();else window.addEventListener('load',dH);
|
|
@@ -181,8 +646,8 @@ setInterval(()=>{patchArticle();patchShortBtns();patchOldSourceLinks();},1500);
|
|
| 181 |
|
| 182 |
@app.get('/')
|
| 183 |
async def _index():
|
| 184 |
-
html=
|
| 185 |
body=getattr(rt.old,'PATCH_INJECT','')+f5.f4.f3.f2.f1.FINAL_INJECT+f5.f4.f3.FINAL3_INJECT+f5.f4.FINAL4_INJECT+f5.FINAL5_INJECT
|
| 186 |
body+=getattr(f6,'FINAL6_INJECT','');body+=getattr(f6,'FINAL6_FAST_HOME_INJECT','');body+=getattr(f6,'FINAL6E_INJECT','')
|
| 187 |
body+=PATCH_INJECT
|
| 188 |
-
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|
|
|
|
| 1 |
+
"""Final patch v6: FIXED short video generation — Vietnamese fonts, robust text splitting, no silent errors."""
|
| 2 |
+
import re, threading, time, json, os, asyncio, hashlib, subprocess, requests, sys, logging
|
| 3 |
import ai_runtime_final6 as f6
|
| 4 |
from ai_runtime_final6 import app, rt, f5, HTMLResponse, JSONResponse, Request, Query
|
| 5 |
+
from fastapi.responses import FileResponse
|
| 6 |
import html as html_lib
|
| 7 |
from urllib.parse import urlparse
|
| 8 |
+
from datetime import datetime, timezone, timedelta
|
| 9 |
+
|
| 10 |
+
_log = logging.getLogger("patch_fast")
|
| 11 |
+
_log.setLevel(logging.INFO)
|
| 12 |
+
if not _log.handlers:
|
| 13 |
+
_log.addHandler(logging.StreamHandler(sys.stderr))
|
| 14 |
|
| 15 |
def clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
|
| 16 |
def _domain(u):
|
|
|
|
| 20 |
os.makedirs(DATA_DIR,exist_ok=True)
|
| 21 |
SHORT_COMMENTS_FILE=os.path.join(DATA_DIR,'short_comments.json')
|
| 22 |
TTL_24H=86400;HAS_PERSISTENT=os.path.isdir('/data')
|
| 23 |
+
SHORTS_DIR = os.path.join(DATA_DIR, 'ai_shorts')
|
| 24 |
+
os.makedirs(SHORTS_DIR, exist_ok=True)
|
| 25 |
def _lj(p,d):
|
| 26 |
try:
|
| 27 |
if os.path.exists(p):return json.load(open(p,'r',encoding='utf-8'))
|
|
|
|
| 36 |
def _scrape(url,mc=8000):
|
| 37 |
try:d=f5.base.scrape_any_url(url);return(d.get('title',''),((d.get('summary','')+'\n'+d.get('text','')).strip())[:mc],d.get('image') or d.get('og_image') or '')
|
| 38 |
except:return('','','')
|
| 39 |
+
|
| 40 |
+
# ======================================================================
|
| 41 |
+
# VIETNAMESE FONT DETECTION — find ANY font that supports Vietnamese
|
| 42 |
+
# ======================================================================
|
| 43 |
+
_FONT_REGULAR = None
|
| 44 |
+
_FONT_BOLD = None
|
| 45 |
+
_FONT_REGULAR_PATH = None
|
| 46 |
+
_FONT_BOLD_PATH = None
|
| 47 |
+
|
| 48 |
+
def _find_vn_fonts():
|
| 49 |
+
"""Find fonts that support Vietnamese diacritics (ấ, ắ, ỗ, ệ, ỷ, etc.)."""
|
| 50 |
+
global _FONT_REGULAR, _FONT_BOLD, _FONT_REGULAR_PATH, _FONT_BOLD_PATH
|
| 51 |
+
if _FONT_REGULAR is not None:
|
| 52 |
+
return True
|
| 53 |
+
|
| 54 |
+
from PIL import ImageFont
|
| 55 |
+
|
| 56 |
+
# Priority 1: Noto Sans (full Vietnamese support)
|
| 57 |
+
candidates_regular = [
|
| 58 |
+
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
|
| 59 |
+
"/usr/share/fonts/truetype/noto/NotoSans-Variable.ttf",
|
| 60 |
+
"/usr/share/fonts/truetype/vn/VNFont.ttf",
|
| 61 |
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
| 62 |
+
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
| 63 |
+
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
|
| 64 |
+
]
|
| 65 |
+
candidates_bold = [
|
| 66 |
+
"/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
|
| 67 |
+
"/usr/share/fonts/truetype/noto/NotoSans-Variable.ttf",
|
| 68 |
+
"/usr/share/fonts/truetype/vn/VNFont-Bold.ttf",
|
| 69 |
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
| 70 |
+
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
|
| 71 |
+
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
result = subprocess.run(
|
| 76 |
+
['find', '/usr/share/fonts', '-name', '*.ttf', '-type', 'f'],
|
| 77 |
+
capture_output=True, text=True, timeout=10
|
| 78 |
+
)
|
| 79 |
+
all_ttf = result.stdout.strip().split('\n')
|
| 80 |
+
for key in ['noto', 'dejavu', 'liberation', 'freefont', 'vn']:
|
| 81 |
+
for path in all_ttf:
|
| 82 |
+
if key in path.lower():
|
| 83 |
+
if 'bold' in path.lower() and _FONT_BOLD_PATH is None:
|
| 84 |
+
_FONT_BOLD_PATH = path
|
| 85 |
+
elif 'regular' in path.lower() and _FONT_REGULAR_PATH is None:
|
| 86 |
+
_FONT_REGULAR_PATH = path
|
| 87 |
+
except:
|
| 88 |
+
pass
|
| 89 |
+
|
| 90 |
+
for path in candidates_regular:
|
| 91 |
+
if os.path.exists(path):
|
| 92 |
+
try:
|
| 93 |
+
_FONT_REGULAR = ImageFont.truetype(path, 40)
|
| 94 |
+
_FONT_REGULAR_PATH = path
|
| 95 |
+
_log.info(f"Font regular: {path}")
|
| 96 |
+
break
|
| 97 |
+
except:
|
| 98 |
+
continue
|
| 99 |
+
|
| 100 |
+
for path in candidates_bold:
|
| 101 |
+
if os.path.exists(path):
|
| 102 |
+
try:
|
| 103 |
+
_FONT_BOLD = ImageFont.truetype(path, 52)
|
| 104 |
+
_FONT_BOLD_PATH = path
|
| 105 |
+
_log.info(f"Font bold: {path}")
|
| 106 |
+
break
|
| 107 |
+
except:
|
| 108 |
+
continue
|
| 109 |
+
|
| 110 |
+
if _FONT_REGULAR is None:
|
| 111 |
+
_FONT_REGULAR = ImageFont.load_default()
|
| 112 |
+
_FONT_BOLD = _FONT_REGULAR
|
| 113 |
+
_log.warning("No TrueType font found! Using PIL default (may not show Vietnamese)")
|
| 114 |
+
if _FONT_BOLD is None:
|
| 115 |
+
_FONT_BOLD = _FONT_REGULAR
|
| 116 |
+
|
| 117 |
+
return True
|
| 118 |
+
|
| 119 |
+
def _split_into_segments(text, max_segments=10, min_len=30):
|
| 120 |
+
"""Split text into segments for short video, handling any format."""
|
| 121 |
+
text = clean(text)
|
| 122 |
+
if not text:
|
| 123 |
+
return []
|
| 124 |
+
|
| 125 |
+
# Strategy 1: Try bullet points
|
| 126 |
+
lines = text.split('\n')
|
| 127 |
+
segmented = []
|
| 128 |
+
for line in lines:
|
| 129 |
+
line = clean(line)
|
| 130 |
+
if not line:
|
| 131 |
+
continue
|
| 132 |
+
line_clean = re.sub(r'^[•\-\*\d\.\)\s]+', '', line).strip()
|
| 133 |
+
if len(line_clean) > min_len:
|
| 134 |
+
segmented.append(line_clean)
|
| 135 |
+
elif len(line) > min_len:
|
| 136 |
+
segmented.append(line)
|
| 137 |
+
|
| 138 |
+
# Strategy 2: If no bullet points found, split by sentences
|
| 139 |
+
if len(segmented) < 2:
|
| 140 |
+
sents = re.split(r'(?<=[.!?])\s+(?=[A-Z0-9À-ỸĐ])', text)
|
| 141 |
+
for s in sents:
|
| 142 |
+
s = clean(s)
|
| 143 |
+
if len(s) > min_len:
|
| 144 |
+
segmented.append(s)
|
| 145 |
+
|
| 146 |
+
# Strategy 3: If still no good segments, split by character chunks
|
| 147 |
+
if not segmented:
|
| 148 |
+
words = text.split()
|
| 149 |
+
chunk = []
|
| 150 |
+
char_count = 0
|
| 151 |
+
for w in words:
|
| 152 |
+
chunk.append(w)
|
| 153 |
+
char_count += len(w) + 1
|
| 154 |
+
if char_count > 150:
|
| 155 |
+
segmented.append(' '.join(chunk))
|
| 156 |
+
chunk = []
|
| 157 |
+
char_count = 0
|
| 158 |
+
if chunk:
|
| 159 |
+
segmented.append(' '.join(chunk))
|
| 160 |
+
|
| 161 |
+
# Strategy 4: Last resort
|
| 162 |
+
if not segmented:
|
| 163 |
+
segmented = [text[:300]]
|
| 164 |
+
|
| 165 |
+
return segmented[:max_segments]
|
| 166 |
+
|
| 167 |
+
def _draw_vn_text(draw, text, font, x, y, max_width, max_lines=6, fill=(255,255,255)):
|
| 168 |
+
"""Draw text with word wrapping, handling Vietnamese characters."""
|
| 169 |
+
if font is None:
|
| 170 |
+
return y
|
| 171 |
+
|
| 172 |
+
words = text.split()
|
| 173 |
+
lines = []
|
| 174 |
+
current_line = []
|
| 175 |
+
|
| 176 |
+
for w in words:
|
| 177 |
+
test_line = ' '.join(current_line + [w])
|
| 178 |
+
try:
|
| 179 |
+
w_px = font.getlength(test_line)
|
| 180 |
+
except:
|
| 181 |
+
try:
|
| 182 |
+
w_px = draw.textbbox((0,0), test_line, font=font)[2]
|
| 183 |
+
except:
|
| 184 |
+
w_px = len(test_line) * 22
|
| 185 |
+
|
| 186 |
+
if w_px <= max_width:
|
| 187 |
+
current_line.append(w)
|
| 188 |
+
else:
|
| 189 |
+
if current_line:
|
| 190 |
+
lines.append(' '.join(current_line))
|
| 191 |
+
current_line = [w]
|
| 192 |
+
if len(lines) >= max_lines:
|
| 193 |
+
break
|
| 194 |
+
|
| 195 |
+
if current_line and len(lines) < max_lines:
|
| 196 |
+
lines.append(' '.join(current_line))
|
| 197 |
+
|
| 198 |
+
for ln in lines[:max_lines]:
|
| 199 |
+
draw.text((x, y), ln, fill=fill, font=font)
|
| 200 |
+
y += 55
|
| 201 |
+
|
| 202 |
+
return y
|
| 203 |
+
|
| 204 |
+
def _make_frame(post, seg, idx, img_path, downloaded, frame_path):
|
| 205 |
+
"""Create a single frame with Vietnamese text support."""
|
| 206 |
+
from PIL import Image, ImageDraw
|
| 207 |
+
_find_vn_fonts()
|
| 208 |
+
|
| 209 |
+
sz = (1080, 1920)
|
| 210 |
+
bg = Image.new('RGB', sz, (15, 23, 38))
|
| 211 |
+
draw = ImageDraw.Draw(bg)
|
| 212 |
+
|
| 213 |
+
# Background image
|
| 214 |
+
if downloaded:
|
| 215 |
+
try:
|
| 216 |
+
im = Image.open(img_path).convert('RGB')
|
| 217 |
+
im = im.resize((1080, 760))
|
| 218 |
+
bg.paste(im, (0, 0))
|
| 219 |
+
overlay = Image.new('RGBA', (1080, 400), (0, 0, 0, 180))
|
| 220 |
+
bg.paste(overlay, (0, 720), overlay)
|
| 221 |
+
except Exception as e:
|
| 222 |
+
_log.warning(f"paste img: {e}")
|
| 223 |
+
|
| 224 |
+
# Title bar
|
| 225 |
+
draw.rectangle([0, 0, sz[0], 100], fill=(25, 118, 210))
|
| 226 |
+
ttl = post.get('title', '')[:50]
|
| 227 |
+
if _FONT_BOLD:
|
| 228 |
+
draw.text((sz[0]//2, 50), ttl, fill='white', font=_FONT_BOLD, anchor='mm')
|
| 229 |
+
|
| 230 |
+
# Segment text
|
| 231 |
+
seg_text = seg[:200]
|
| 232 |
+
_draw_vn_text(draw, seg_text, _FONT_REGULAR, 80, 820, 920, max_lines=8)
|
| 233 |
+
|
| 234 |
+
bg.save(frame_path, quality=88)
|
| 235 |
+
return True
|
| 236 |
+
|
| 237 |
+
def _gen_short_sync(post) -> str:
|
| 238 |
+
"""Generate short video NOW. Returns video URL or empty string on failure."""
|
| 239 |
+
post_id = post.get('id', '')
|
| 240 |
+
if not post_id:
|
| 241 |
+
_log.error("_gen_short_sync: no post_id")
|
| 242 |
+
return ''
|
| 243 |
+
|
| 244 |
+
text = post.get('text', '') or post.get('title', '')
|
| 245 |
+
if len(text) < 100:
|
| 246 |
+
_log.warning(f"_gen_short_sync: text too short ({len(text)})")
|
| 247 |
+
return ''
|
| 248 |
+
|
| 249 |
+
segments = _split_into_segments(text, max_segments=10, min_len=30)
|
| 250 |
+
if not segments:
|
| 251 |
+
_log.error("_gen_short_sync: no segments after splitting")
|
| 252 |
+
return ''
|
| 253 |
+
|
| 254 |
+
_log.info(f"Segments: {len(segments)} (first: '{segments[0][:50]}...')")
|
| 255 |
+
|
| 256 |
+
seg_hash = hashlib.md5(('|'.join(segments) + 'nu' + 'neutral' + '1.0').encode('utf-8')).hexdigest()[:8]
|
| 257 |
+
suffix = f"_nu_neutral_1p0_{seg_hash}_scenes_nosub"
|
| 258 |
+
out_name = f5.base._safe_name(post_id + suffix) + '.mp4'
|
| 259 |
+
out_mp4 = os.path.join(SHORTS_DIR, out_name)
|
| 260 |
+
video_url = '/api/ai/short-file/' + post_id + suffix
|
| 261 |
+
|
| 262 |
+
if os.path.exists(out_mp4):
|
| 263 |
+
_log.info(f"_gen_short_sync: already exists {out_mp4}")
|
| 264 |
+
return video_url
|
| 265 |
+
|
| 266 |
+
work = os.path.join(SHORTS_DIR, f5.base._safe_name(post_id + suffix))
|
| 267 |
+
os.makedirs(work, exist_ok=True)
|
| 268 |
+
|
| 269 |
+
# Download image
|
| 270 |
+
img_path = os.path.join(work, 'image.jpg')
|
| 271 |
+
downloaded = False
|
| 272 |
+
try:
|
| 273 |
+
img_url = post.get('img', '')
|
| 274 |
+
if img_url and img_url.startswith('http'):
|
| 275 |
+
r = requests.get(img_url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=15)
|
| 276 |
+
if r.status_code == 200:
|
| 277 |
+
with open(img_path, 'wb') as f:
|
| 278 |
+
f.write(r.content)
|
| 279 |
+
downloaded = True
|
| 280 |
+
except Exception as e:
|
| 281 |
+
_log.warning(f"download image: {e}")
|
| 282 |
+
|
| 283 |
+
try:
|
| 284 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 285 |
+
has_pil = True
|
| 286 |
+
except Exception as e:
|
| 287 |
+
_log.warning(f"PIL import: {e}")
|
| 288 |
+
has_pil = False
|
| 289 |
+
|
| 290 |
+
try:
|
| 291 |
+
subprocess.run(['ffmpeg', '-version'], capture_output=True, timeout=5)
|
| 292 |
+
except:
|
| 293 |
+
_log.error("ffmpeg not found!")
|
| 294 |
+
return ''
|
| 295 |
+
|
| 296 |
+
try:
|
| 297 |
+
from gtts import gTTS
|
| 298 |
+
has_tts = True
|
| 299 |
+
except Exception as e:
|
| 300 |
+
_log.warning(f"gTTS: {e}, using silent video")
|
| 301 |
+
has_tts = False
|
| 302 |
+
|
| 303 |
+
part_files = []
|
| 304 |
+
errors = []
|
| 305 |
+
|
| 306 |
+
for idx, seg in enumerate(segments[:10]):
|
| 307 |
+
frame_path = os.path.join(work, f'frame_{idx:02d}.jpg')
|
| 308 |
+
audio_path = os.path.join(work, f'voice_{idx:02d}.mp3')
|
| 309 |
+
audio_fast = os.path.join(work, f'voice_{idx:02d}_fast.mp3')
|
| 310 |
+
part_path = os.path.join(work, f'part_{idx:02d}.mp4')
|
| 311 |
+
|
| 312 |
+
# ----- Frame -----
|
| 313 |
+
frame_ok = False
|
| 314 |
+
if has_pil:
|
| 315 |
+
try:
|
| 316 |
+
_make_frame(post, seg, idx, img_path, downloaded, frame_path)
|
| 317 |
+
if os.path.exists(frame_path) and os.path.getsize(frame_path) > 1000:
|
| 318 |
+
frame_ok = True
|
| 319 |
+
else:
|
| 320 |
+
_log.warning(f"frame {idx}: file too small or missing")
|
| 321 |
+
except Exception as e:
|
| 322 |
+
_log.error(f"frame {idx} creation: {e}")
|
| 323 |
+
errors.append(f"frame{idx}: {e}")
|
| 324 |
+
|
| 325 |
+
if not frame_ok:
|
| 326 |
+
try:
|
| 327 |
+
subprocess.run(['ffmpeg', '-y', '-f', 'lavfi', '-i', 'color=c=0x0f1726:s=1080x1920:d=1',
|
| 328 |
+
'-frames:v', '1', frame_path], capture_output=True, timeout=30)
|
| 329 |
+
frame_ok = os.path.exists(frame_path)
|
| 330 |
+
except:
|
| 331 |
+
continue
|
| 332 |
+
|
| 333 |
+
# ----- Audio -----
|
| 334 |
+
dur = 10.0
|
| 335 |
+
if has_tts:
|
| 336 |
+
try:
|
| 337 |
+
tts_text = re.sub(r'^[•\-\*\d\.\)\s]+', '', seg).strip()[:300]
|
| 338 |
+
if tts_text:
|
| 339 |
+
gTTS(tts_text, lang='vi', slow=False).save(audio_path)
|
| 340 |
+
subprocess.run(['ffmpeg', '-y', '-i', audio_path, '-vn', '-c:a', 'libmp3lame', audio_fast],
|
| 341 |
+
check=True, capture_output=True, timeout=90)
|
| 342 |
+
pr = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
|
| 343 |
+
'-of', 'default=noprint_wrappers=1:no_key=1', audio_fast],
|
| 344 |
+
capture_output=True, timeout=20)
|
| 345 |
+
dur = max(6.0, float((pr.stdout or b'').decode().strip() or 10.0)) + 1.0
|
| 346 |
+
else:
|
| 347 |
+
dur = 8.0
|
| 348 |
+
except Exception as e:
|
| 349 |
+
_log.warning(f"TTS part {idx}: {e}")
|
| 350 |
+
errors.append(f"tts{idx}: {e}")
|
| 351 |
+
dur = 8.0
|
| 352 |
+
|
| 353 |
+
# ----- Combine frame + audio -----
|
| 354 |
+
try:
|
| 355 |
+
cmd = ['ffmpeg', '-y', '-loop', '1', '-t', str(dur), '-i', frame_path]
|
| 356 |
+
if has_tts and os.path.exists(audio_fast):
|
| 357 |
+
cmd += ['-i', audio_fast, '-shortest']
|
| 358 |
+
else:
|
| 359 |
+
cmd += ['-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=mono', '-shortest']
|
| 360 |
+
cmd += ['-c:v', 'libx264', '-tune', 'stillimage', '-pix_fmt', 'yuv420p',
|
| 361 |
+
'-c:a', 'aac', '-b:a', '128k',
|
| 362 |
+
'-preset', 'fast', '-crf', '23',
|
| 363 |
+
part_path]
|
| 364 |
+
subprocess.run(cmd, check=True, capture_output=True, timeout=150)
|
| 365 |
+
if os.path.exists(part_path) and os.path.getsize(part_path) > 5000:
|
| 366 |
+
part_files.append(part_path)
|
| 367 |
+
_log.info(f"Part {idx}: {os.path.getsize(part_path)} bytes")
|
| 368 |
+
else:
|
| 369 |
+
_log.warning(f"Part {idx}: file too small or missing")
|
| 370 |
+
except Exception as e:
|
| 371 |
+
_log.warning(f"ffmpeg part {idx}: {e}")
|
| 372 |
+
errors.append(f"part{idx}: {e}")
|
| 373 |
+
|
| 374 |
+
if part_files:
|
| 375 |
+
try:
|
| 376 |
+
concat_file = os.path.join(work, 'concat.txt')
|
| 377 |
+
with open(concat_file, 'w', encoding='utf-8') as f:
|
| 378 |
+
for p in part_files:
|
| 379 |
+
f.write("file '" + p.replace("'", "'\\''") + "'\n")
|
| 380 |
+
subprocess.run(['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', concat_file, '-c', 'copy', out_mp4],
|
| 381 |
+
check=True, capture_output=True, timeout=180)
|
| 382 |
+
_log.info(f"Short video generated: {out_mp4} ({len(part_files)} parts, {os.path.getsize(out_mp4)} bytes)")
|
| 383 |
+
|
| 384 |
+
post['video'] = video_url
|
| 385 |
+
post['short_voice'] = 'nu'
|
| 386 |
+
post['short_emotion'] = 'neutral'
|
| 387 |
+
post['short_speed'] = 1.0
|
| 388 |
+
post['short_segments'] = segments
|
| 389 |
+
post['short_subtitles'] = False
|
| 390 |
+
|
| 391 |
+
wall = f5.base._load_ai_wall()
|
| 392 |
+
for i, p in enumerate(wall):
|
| 393 |
+
if p.get('id') == post_id:
|
| 394 |
+
wall[i] = post
|
| 395 |
+
break
|
| 396 |
+
f5.base._save_ai_wall(wall)
|
| 397 |
+
return video_url
|
| 398 |
+
except Exception as e:
|
| 399 |
+
_log.error(f"concat: {e}")
|
| 400 |
+
errors.append(f"concat: {e}")
|
| 401 |
+
else:
|
| 402 |
+
_log.error(f"No part files generated! Errors: {'; '.join(errors)}")
|
| 403 |
+
return ''
|
| 404 |
+
|
| 405 |
+
# ===== Auto Scheduler Trigger =====
|
| 406 |
+
_AUTO_SCHEDULER_RUNNING = False
|
| 407 |
+
_AUTO_SCHEDULER_LAST = 0
|
| 408 |
+
_AUTO_SCHEDULER_RESULTS = []
|
| 409 |
+
|
| 410 |
+
def _run_scheduler_now():
|
| 411 |
+
"""Trigger auto_scheduler._run_scheduled_posting() safely."""
|
| 412 |
+
global _AUTO_SCHEDULER_RUNNING, _AUTO_SCHEDULER_LAST, _AUTO_SCHEDULER_RESULTS
|
| 413 |
+
if _AUTO_SCHEDULER_RUNNING:
|
| 414 |
+
_log.warning("Scheduler already running!")
|
| 415 |
+
return {"status": "running", "message": "Scheduler đang chạy, vui lòng đợi."}
|
| 416 |
+
_AUTO_SCHEDULER_RUNNING = True
|
| 417 |
+
try:
|
| 418 |
+
import auto_scheduler as _as
|
| 419 |
+
_as.LOG.info("Triggered manually via API")
|
| 420 |
+
_as._run_scheduled_posting()
|
| 421 |
+
_AUTO_SCHEDULER_LAST = time.time()
|
| 422 |
+
_AUTO_SCHEDULER_RESULTS = [{'time': datetime.now().strftime('%H:%M %d/%m/%Y'), 'status': 'done'}]
|
| 423 |
+
_log.info("Auto scheduler completed successfully")
|
| 424 |
+
return {"status": "done", "message": "Đã đăng 3 bài tự động thành công!"}
|
| 425 |
+
except Exception as e:
|
| 426 |
+
_log.error(f"Auto scheduler failed: {e}", exc_info=True)
|
| 427 |
+
return {"status": "error", "message": f"Lỗi: {e}"}
|
| 428 |
+
finally:
|
| 429 |
+
_AUTO_SCHEDULER_RUNNING = False
|
| 430 |
+
|
| 431 |
+
@app.post('/api/auto/schedule')
|
| 432 |
+
async def _api_auto_schedule():
|
| 433 |
+
_log.info("POST /api/auto/schedule triggered")
|
| 434 |
+
result = await asyncio.get_event_loop().run_in_executor(None, _run_scheduler_now)
|
| 435 |
+
return JSONResponse(result)
|
| 436 |
+
|
| 437 |
+
@app.get('/api/auto/status')
|
| 438 |
+
async def _api_auto_status():
|
| 439 |
+
return JSONResponse({
|
| 440 |
+
'running': _AUTO_SCHEDULER_RUNNING,
|
| 441 |
+
'last_run': _AUTO_SCHEDULER_LAST,
|
| 442 |
+
'last_run_str': datetime.fromtimestamp(_AUTO_SCHEDULER_LAST).strftime('%H:%M %d/%m/%Y') if _AUTO_SCHEDULER_LAST else 'never',
|
| 443 |
+
'results': _AUTO_SCHEDULER_RESULTS
|
| 444 |
+
})
|
| 445 |
+
|
| 446 |
_bg_home={"t":0,"d":[]};_bg_shorts={"t":0,"d":[]};_bg_lock=False
|
| 447 |
def _bg():
|
| 448 |
global _bg_lock
|
|
|
|
| 459 |
@app.on_event("startup")
|
| 460 |
async def _s():threading.Thread(target=_bg,daemon=True).start()
|
| 461 |
threading.Thread(target=lambda:[time.sleep(600) or _bg() for _ in iter(int,1)],daemon=True).start()
|
| 462 |
+
app.router.routes=[r for r in app.router.routes if not (getattr(r,'path',None) in ('/api/homepage','/api/shorts','/api/ai_wall','/api/topic_post','/api/article/ask','/api/topic/rewrite','/api/rewrite_share','/api/url_wall','/api/short/comments','/api/short/comment','/api/storage_status','/api/ai/short/{post_id}','/api/ai/short-file/{file_id}','/api/auto/schedule','/api/auto/status','/') and any(m in getattr(r,'methods',set()) for m in ('GET','POST')))]
|
| 463 |
@app.get('/api/homepage')
|
| 464 |
def _h():
|
| 465 |
n=time.time()
|
|
|
|
| 491 |
if not raw:raw=ctx[:12000]
|
| 492 |
ans=await f5.base.qwen_generate(f'Bạn là VNEWS AI. Nội dung: "{title}"\n{raw[:9000]}\n\nHỏi: "{q}"\n\nTrả lời tự nhiên bằng tiếng Việt.',max_tokens=1200)
|
| 493 |
return JSONResponse({'answer':ans or 'Chưa trả lời được.','title':title})
|
| 494 |
+
|
| 495 |
+
# ===== SHORT VIDEO ENDPOINT (SYNCHRONOUS) =====
|
| 496 |
+
@app.post('/api/ai/short/{post_id}')
|
| 497 |
+
async def api_ai_short_generate(post_id: str, request: Request):
|
| 498 |
+
wall = f5.base._load_ai_wall()
|
| 499 |
+
post = None
|
| 500 |
+
for p in wall:
|
| 501 |
+
if p.get('id') == post_id:
|
| 502 |
+
post = p
|
| 503 |
+
break
|
| 504 |
+
if not post:
|
| 505 |
+
return JSONResponse({'error': 'Post not found'}, status_code=404)
|
| 506 |
+
if post.get('video'):
|
| 507 |
+
return JSONResponse({'post': post, 'video': post['video'], 'status': 'done'})
|
| 508 |
+
_log.info(f"Generating short for post {post_id}...")
|
| 509 |
+
loop = asyncio.get_event_loop()
|
| 510 |
+
video_url = await loop.run_in_executor(None, _gen_short_sync, post)
|
| 511 |
+
if video_url:
|
| 512 |
+
_log.info(f"Short generated: {video_url}")
|
| 513 |
+
return JSONResponse({'post': post, 'video': video_url, 'status': 'done'})
|
| 514 |
+
else:
|
| 515 |
+
_log.error(f"Short generation FAILED for {post_id}")
|
| 516 |
+
return JSONResponse({'error': 'Không thể tạo video. Kiểm tra logs Space để biết chi tiết.'}, status_code=500)
|
| 517 |
+
|
| 518 |
+
@app.get('/api/ai/short-file/{file_id:path}')
|
| 519 |
+
async def api_ai_short_file(file_id: str):
|
| 520 |
+
safe_name = f5.base._safe_name(file_id)
|
| 521 |
+
fpath = os.path.join(SHORTS_DIR, safe_name + '.mp4')
|
| 522 |
+
if os.path.exists(fpath):
|
| 523 |
+
return FileResponse(fpath, media_type='video/mp4')
|
| 524 |
+
for fname in os.listdir(SHORTS_DIR):
|
| 525 |
+
if fname.endswith('.mp4') and (safe_name in fname or file_id in fname):
|
| 526 |
+
fpath = os.path.join(SHORTS_DIR, fname)
|
| 527 |
+
if os.path.exists(fpath):
|
| 528 |
+
return FileResponse(fpath, media_type='video/mp4')
|
| 529 |
+
base_id = f5.base._safe_name((file_id.split('_')[0] if '_' in file_id else file_id))
|
| 530 |
+
for fname in os.listdir(SHORTS_DIR):
|
| 531 |
+
if fname.endswith('.mp4') and fname.startswith(base_id):
|
| 532 |
+
return FileResponse(os.path.join(SHORTS_DIR, fname), media_type='video/mp4')
|
| 533 |
+
return JSONResponse({'error': 'File not found'}, status_code=404)
|
| 534 |
+
|
| 535 |
+
# ===== REWRITE =====
|
| 536 |
@app.post('/api/rewrite_share')
|
| 537 |
@app.post('/api/url_wall')
|
| 538 |
async def _rw(request:Request):
|
|
|
|
| 541 |
title,raw,img=_scrape(url,14000)
|
| 542 |
if len(raw)<50:raw=ctx[:14000]
|
| 543 |
if len(raw)<50:return JSONResponse({'error':'Không đọc được bài'},status_code=422)
|
| 544 |
+
og_img = img if img and img.startswith('http') else ''
|
| 545 |
+
if not og_img:
|
| 546 |
+
try:
|
| 547 |
+
d = f5.base.scrape_any_url(url)
|
| 548 |
+
if d: og_img = d.get('og_image', '') or d.get('image', '') or ''
|
| 549 |
+
except: pass
|
| 550 |
+
if not og_img:
|
| 551 |
+
og_img = 'https://image.pollinations.ai/prompt/Vietnamese%20news%20update'
|
| 552 |
text=None
|
| 553 |
+
try:text=await asyncio.wait_for(f5.base.qwen_generate(f'Tóm tắt đăng Tường AI:\nTiêu đề: {title}\n{raw[:14000]}\n\n4-6 ý chính. Cuối ghi nguồn.',image_url=og_img or None,max_tokens=1000),timeout=30)
|
| 554 |
except:pass
|
| 555 |
if not text or len(text)<80:text=f"Tóm tắt: {title}\n\n{raw[:1200]}\n\nNguồn: {_domain(url)}"
|
| 556 |
+
paragraphs = [p.strip() for p in raw.split('\n') if len(p.strip()) > 60]
|
| 557 |
+
if not paragraphs:
|
| 558 |
+
paragraphs = [p.strip() for p in raw.split('\n') if len(p.strip()) > 20]
|
| 559 |
+
slides = []
|
| 560 |
+
for i, pt in enumerate(paragraphs[:8]):
|
| 561 |
+
slides.append({'text': pt, 'image': og_img, 'index': i + 1})
|
| 562 |
+
post=f5.base.make_post(title or 'Bài viết',text,og_img,url,'rewrite',sources=[{'title':title,'url':url,'via':_domain(url)}])
|
| 563 |
+
post['images'] = [og_img] if og_img else []
|
| 564 |
+
post['slides'] = slides
|
| 565 |
+
ps=f5.base._load_ai_wall();ps.insert(0,post);f5.base._save_ai_wall(ps)
|
| 566 |
+
if len(text) > 100:
|
| 567 |
+
t = threading.Thread(target=_gen_short_sync, args=(post,), daemon=True)
|
| 568 |
+
t.start()
|
| 569 |
+
return JSONResponse({'post':post})
|
| 570 |
+
|
| 571 |
@app.post('/api/topic/rewrite')
|
| 572 |
async def _tr(request:Request):
|
| 573 |
b=await request.json();pid=str(b.get('post_id','')).strip()
|
|
|
|
| 607 |
.article-ai-ask{margin-top:12px;background:#141414;border:1px solid #2a2a2a;border-radius:10px;padding:10px}.article-ai-ask textarea{width:100%;min-height:60px;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px}.article-ai-ask button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin-top:6px}.article-ai-answer{white-space:pre-wrap;color:#ccc;font-size:13px;line-height:1.55;margin-top:8px}
|
| 608 |
.storage-warn{background:#332200;border:1px solid #664400;color:#ffcc00;padding:8px 12px;border-radius:8px;font-size:11px;margin:6px 4px}
|
| 609 |
button[onclick*="rewriteCurrentArticle"]{display:none!important}
|
|
|
|
| 610 |
#ai-short-home,.ai-short-home,.ai-short-card-final{display:none!important}
|
| 611 |
.source-detail-box a[target="_blank"]{display:none!important}
|
| 612 |
</style>
|
|
|
|
| 615 |
(function(){
|
| 616 |
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 617 |
fetch('/api/storage_status').then(r=>r.json()).then(j=>{if(!j.persistent){let h=document.getElementById('view-home');if(h){let w=document.createElement('div');w.className='storage-warn';w.innerHTML='⚠️ <b>Persistent Storage chưa bật.</b> Bật: Space Settings → Persistent Storage → Small.';h.prepend(w);}}});
|
|
|
|
|
|
|
| 618 |
async function renderShortAISlide(){let home=document.getElementById('view-home');if(!home)return;document.getElementById('short-ai-final-slide')?.remove();let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let vids=wall.filter(p=>p.video);if(!vids.length)return;let wrap=document.createElement('div');wrap.id='short-ai-final-slide';wrap.className='slider-wrap';wrap.innerHTML='<div class="slider-header"><span class="slider-label">🎬 Short AI</span></div><div class="slider-track">'+vids.slice(0,30).map((p,i)=>`<div class="slider-item shorts-item" onclick="openAIShortFeed(${i})"><div class="slider-thumb shorts-thumb"><video src="${p.video}" muted preload="metadata" style="width:100%;height:100%;object-fit:cover"></video><div class="card-play">▶</div></div><div class="slider-title">${esc(p.title)}</div></div>`).join('')+'</div>';let comp=home.querySelector('.ai-compose');if(comp&&comp.nextSibling)comp.parentNode.insertBefore(wrap,comp.nextSibling);else home.prepend(wrap);}
|
| 619 |
+
setTimeout(renderShortAISlide,2500);setInterval(renderShortAISlide,15000);
|
|
|
|
|
|
|
| 620 |
function renderSourceDetails(post,container){let det=post.source_details||[];if(!det.length)return;container.querySelectorAll('.source-detail-box').forEach(e=>e.remove());let box=document.createElement('div');box.className='source-detail-box';box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:8px">📚 Bài nguồn</h3>'+det.map((s,i)=>`<div class="source-detail-item" data-url="${esc(s.url||'')}"><div class="source-detail-title">${i+1}. ${esc(s.title)}</div><div class="source-detail-content">${esc((s.content||'').slice(0,300))}</div><span class="source-vnews-btn">📖 Xem trên VNEWS</span></div>`).join('');container.appendChild(box);box.querySelectorAll('.source-detail-item').forEach(el=>{el.onclick=function(){let u=el.dataset.url;if(u&&typeof readArticle==='function')readArticle(u);}});det.forEach((s,i)=>{if(!s.url)return;fetch('/api/article?url='+encodeURIComponent(s.url)).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){let items=box.querySelectorAll('.source-detail-item');if(items[i]){let img=document.createElement('img');img.src=d.og_image||d.img;img.loading='lazy';img.onerror=function(){this.style.display='none'};items[i].prepend(img);}}}).catch(()=>{});});}
|
|
|
|
|
|
|
| 621 |
async function readAIWallPost(i){let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let p=wall[i];if(!p)return;showView('view-article');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>${p.img?`<img class="article-img" src="${p.img}">`:''}`;h+=`<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>`;h+=`<div class="article-actions"><button class="primary" onclick="doRewriteTopic(this,'${esc(p.id)}')">🤖 Rewrite AI đăng tường</button>${p.video?`<button onclick="openAIShortFeed(${i})">🎬 Xem Short</button>`:''}<button onclick="doShare('${esc(p.title)}','${location.origin}','${esc(p.img||'')}')">📤</button></div>`;h+=`<div class="article-ai-ask"><h3 style="font-size:14px;color:#5cb87a">🤖 Hỏi AI</h3><textarea id="article-ai-q" placeholder="Hỏi về nội dung..."></textarea><button onclick="askAIWall(${i})">Hỏi</button><div id="article-ai-ans" class="article-ai-answer"></div></div></div>`;document.getElementById('view-article').innerHTML=h;let art=document.querySelector('.article-view');if(art)renderSourceDetails(p,art);window.scrollTo(0,0);}
|
| 622 |
window.readAIWallPost=readAIWallPost;window.aiReadWallPatched=window.aiReadWall=window.readWallPost=function(i){readAIWallPost(i)};
|
|
|
|
|
|
|
| 623 |
window.openAIShortFeed=async function(startIdx){let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let vids=wall.filter(p=>p.video);if(!vids.length)return alert('Chưa có Short AI');let ordered=startIdx>0?vids.slice(startIdx).concat(vids.slice(0,startIdx)):vids;showView('view-tiktok');let h='<button class="back-btn" onclick="switchCat(\'home\')">← Short AI</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';ordered.forEach((p,i)=>{h+=`<div class="tiktok-slide" data-id="${p.id}"><video src="${p.video}" playsinline loop></video><div class="tiktok-bottom"><span class="badge badge-ai">AI Short</span><p class="tiktok-title">${esc(p.title)}</p></div><div class="tiktok-right"><button class="tiktok-right-btn" onclick="event.stopPropagation()"><div class="icon">👁</div><div class="count">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();likeShort('${p.id}',this)"><div class="icon">❤️</div><div class="count">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();openShortComments('${p.id}')"><div class="icon">💬</div><div class="count" id="cc-${p.id}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();shareShort('${esc(p.title)}')"><div class="icon">📤</div><div class="count">Share</div></button></div><span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`});h+='</div></div>';document.getElementById('view-tiktok').innerHTML=h;initShortFeed();ordered.forEach(p=>{fetch('/api/short/comments?id='+encodeURIComponent(p.id)).then(r=>r.json()).then(j=>{let el=document.getElementById('cc-'+p.id);if(el)el.textContent=(j.comments||[]).length}).catch(()=>{});});}
|
| 624 |
window.likeShort=function(id,btn){let c=btn.querySelector('.count');c.textContent=parseInt(c.textContent||0)+1;}
|
| 625 |
window.shareShort=function(title){if(navigator.share)navigator.share({title,url:location.href}).catch(()=>{});else{navigator.clipboard.writeText(location.href);alert('Đã sao chép link!');}}
|
| 626 |
function initShortFeed(){let feed=document.getElementById('tiktok-feed');if(!feed)return;let slides=feed.querySelectorAll('.tiktok-slide');let cur=-1;function act(i){if(i===cur)return;slides.forEach((sl,idx)=>{let v=sl.querySelector('video');let fr=sl.querySelector('iframe');if(idx===i){if(v)v.play().catch(()=>{});if(fr&&!fr.src&&fr.dataset.ytSrc)fr.src=fr.dataset.ytSrc;}else{if(v)v.pause();if(fr&&fr.src)fr.src='';}});cur=i}let t;feed.addEventListener('scroll',()=>{clearTimeout(t);t=setTimeout(()=>{let rect=feed.getBoundingClientRect(),ctr=rect.top+rect.height/2,b=-1,d=1e9;slides.forEach((sl,i)=>{let dd=Math.abs(sl.getBoundingClientRect().top+sl.getBoundingClientRect().height/2-ctr);if(dd<d){d=dd;b=i}});if(b>=0)act(b)},130)});setTimeout(()=>act(0),300);slides.forEach(sl=>{let v=sl.querySelector('video');if(v)v.addEventListener('click',e=>{e.preventDefault();v.paused?v.play().catch(()=>{}):v.pause()})});}
|
|
|
|
|
|
|
| 627 |
window.doRewriteTopic=async function(btn,pid){btn.disabled=true;btn.textContent='Đang rewrite...';try{let r=await fetch('/api/topic/rewrite',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({post_id:pid})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Rewrite thành công!');showRewriteResult(j.post);}catch(e){alert(e.message)}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
|
| 628 |
window.doRewriteArticle=async function(btn){let url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){let a=document.querySelector('#view-article a[href*="://"]');if(a)url=a.href;}if(!url){let text=document.querySelector('.article-view')?.innerText?.slice(0,14000)||'';if(text.length<100){alert('Không tìm được nội dung để rewrite');return;}btn.disabled=true;btn.textContent='Đang rewrite...';try{let r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:'https://vnews.local/inline',context:text})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error);alert('Rewrite thành công!');showRewriteResult(j.post);}catch(e){alert(e.message)}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}return;}btn.disabled=true;btn.textContent='Đang rewrite...';try{let ctx=document.querySelector('.article-view')?.innerText?.slice(0,14000)||'';let r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,context:ctx})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Rewrite thành công!');showRewriteResult(j.post);}catch(e){alert(e.message)}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
|
| 629 |
function showRewriteResult(post){if(!post)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">Rewrite</span><h1 class="article-title">${esc(post.title)}</h1>${post.img?`<img class="article-img" src="${post.img}">`:''}` +`<p class="article-p" style="white-space:pre-wrap">${esc(post.text)}</p><div class="article-actions"><button class="primary" onclick="makeShortFromPost('${esc(post.id)}',this)">🎬 Tạo Short AI</button><button onclick="doShare('${esc(post.title)}','${location.origin}','${esc(post.img||'')}')">📤</button></div></div>`;window.scrollTo(0,0);}
|
| 630 |
+
window.makeShortFromPost=async function(pid,btn){if(btn){btn.disabled=true;btn.textContent='⏳ Đang tạo video...';}try{let r=await fetch('/api/ai/short/'+pid,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({voice:'nu',emotion:'neutral',speed:1.2})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');if(j.video){alert('✅ Đã tạo Short AI thành công!');renderShortAISlide();}else{alert(j.message||'Đã tạo!');renderShortAISlide();}}catch(e){alert('Lỗi: '+e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🎬 Tạo Short AI';}}};
|
| 631 |
window.rewriteCurrentArticle=function(){let btn=document.querySelector('[data-rw-article]');if(btn)doRewriteArticle(btn);};
|
| 632 |
window.askAIWall=async function(i){let q=document.getElementById('article-ai-q')?.value.trim();if(!q)return alert('Nhập câu hỏi');document.getElementById('article-ai-ans').textContent='Đang hỏi...';let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let p=wall[i]||{};let ctx=(p.text||'');for(let s of (p.source_details||[]))ctx+='\n'+(s.content||'');try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question:q,context:ctx.slice(0,12000)})});let j=await r.json();document.getElementById('article-ai-ans').textContent=j.answer||'Không trả lời được';}catch(e){document.getElementById('article-ai-ans').textContent='Lỗi: '+e.message}};
|
| 633 |
window.askArticleAI=async function(){let q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');let a=document.getElementById('article-ai-answer');a.textContent='Đang hỏi...';let url=(window._currentArticle&&window._currentArticle.url)||'';let ctx=document.querySelector('.article-view')?.innerText?.slice(0,12000)||'';try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,question:q,context:ctx})});let j=await r.json();a.textContent=j.answer||'Không trả lời được';}catch(e){a.textContent='Lỗi: '+e.message}};
|
| 634 |
window.openShortComments=async function(id){let panel=document.getElementById('short-cmt-panel');let j=await fetch('/api/short/comments?id='+encodeURIComponent(id)).then(r=>r.json()).catch(()=>({comments:[]}));panel.innerHTML=`<h3 style="color:#5cb87a">💬 Bình luận</h3><div id="cmt-list">${(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('')||'<div class="cmt-item" style="color:#777">Chưa có</div>'}</div><textarea id="cmt-text" placeholder="Bình luận..."></textarea><button onclick="submitShortCmt('${esc(id)}')">Gửi</button><button onclick="document.getElementById('short-cmt-panel').classList.remove('active')">Đóng</button>`;panel.classList.add('active');}
|
| 635 |
window.submitShortCmt=async function(id){let t=document.getElementById('cmt-text')?.value.trim();if(!t)return;let j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,text:t})}).then(r=>r.json()).catch(()=>({comments:[]}));document.getElementById('cmt-list').innerHTML=(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('');document.getElementById('cmt-text').value='';let el=document.getElementById('cc-'+id);if(el)el.textContent=(j.comments||[]).length;}
|
|
|
|
|
|
|
| 636 |
function patchArticle(){let art=document.querySelector('#view-article .article-view');if(!art)return;art.querySelectorAll('button[onclick*="rewriteCurrentArticle"],[data-rewrite],.rewrite-injected').forEach(e=>e.remove());art.querySelectorAll('.article-ai-ask').forEach((e,i)=>{if(i>0)e.remove();});if(!art.querySelector('[data-rw-article]')){let a=art.querySelector('.article-actions');if(a){let b=document.createElement('button');b.className='primary';b.setAttribute('data-rw-article','1');b.textContent='🤖 Rewrite AI đăng tường';b.onclick=function(){doRewriteArticle(b)};a.insertBefore(b,a.firstChild);}}if(!art.querySelector('.article-ai-ask')){let box=document.createElement('div');box.className='article-ai-ask';box.innerHTML='<h3 style="font-size:14px;color:#5cb87a">🤖 Hỏi AI</h3><textarea id="article-ai-question" placeholder="Hỏi..."></textarea><button onclick="askArticleAI()">Hỏi</button><div id="article-ai-answer" class="article-ai-answer"></div>';art.appendChild(box);}}
|
| 637 |
function patchShortBtns(){document.querySelectorAll('.tiktok-slide').forEach(sl=>{if(sl.dataset.cmtDone)return;sl.dataset.cmtDone='1';let id=sl.dataset.id||'';if(!id)return;let r=sl.querySelector('.tiktok-right');if(!r||r.querySelector('[data-cmt]'))return;let b=document.createElement('button');b.className='tiktok-right-btn';b.setAttribute('data-cmt','1');b.innerHTML='<div class="icon">💬</div><div class="count">0</div>';b.onclick=function(e){e.stopPropagation();openShortComments(id);};r.appendChild(b);fetch('/api/short/comments?id='+encodeURIComponent(id)).then(r=>r.json()).then(j=>{b.querySelector('.count').textContent=(j.comments||[]).length}).catch(()=>{});});}
|
| 638 |
function patchOldSourceLinks(){document.querySelectorAll('.source-detail-item a[target="_blank"],.source-detail-item a[href]').forEach(a=>{if(a.dataset.p7)return;a.dataset.p7='1';let url=a.href||'';a.removeAttribute('target');a.removeAttribute('href');a.textContent='📖 Xem trên VNEWS';a.className='source-vnews-btn';a.style.cursor='pointer';a.onclick=function(e){e.preventDefault();e.stopPropagation();if(url&&typeof readArticle==='function')readArticle(url);}});}
|
|
|
|
| 639 |
let oldRA=window.readArticle;if(oldRA){window.readArticle=async function(){let ret=await oldRA.apply(this,arguments);setTimeout(patchArticle,500);return ret;}}
|
| 640 |
let _hl=false;function dH(){if(_hl)return;_hl=true;setTimeout(()=>{if(typeof ensureHotTopics==='function')ensureHotTopics();if(typeof ensureNewsShortsHome==='function')ensureNewsShortsHome();},4000);}
|
| 641 |
if(document.readyState==='complete')dH();else window.addEventListener('load',dH);
|
|
|
|
| 646 |
|
| 647 |
@app.get('/')
|
| 648 |
async def _index():
|
| 649 |
+
html=open('/app/static/index_v2.html','r',encoding='utf-8').read() if os.path.exists('/app/static/index_v2.html') else ('<html><body>VNEWS</body></html>' if os.path.exists('/app/static/index.html') else '')
|
| 650 |
body=getattr(rt.old,'PATCH_INJECT','')+f5.f4.f3.f2.f1.FINAL_INJECT+f5.f4.f3.FINAL3_INJECT+f5.f4.FINAL4_INJECT+f5.FINAL5_INJECT
|
| 651 |
body+=getattr(f6,'FINAL6_INJECT','');body+=getattr(f6,'FINAL6_FAST_HOME_INJECT','');body+=getattr(f6,'FINAL6E_INJECT','')
|
| 652 |
body+=PATCH_INJECT
|
| 653 |
+
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|