VNEWS / rewrite_slide.py
bep40's picture
Upload rewrite_slide.py
cf417d1 verified
Raw
History Blame
6.97 kB
"""Fast rewrite as slides - no AI needed, extracts key points + images from article."""
from main import app
from fastapi import Request
from fastapi.responses import JSONResponse
import requests, re, time, random, json, os
from bs4 import BeautifulSoup
from urllib.parse import quote
UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept-Language': 'vi-VN,vi;q=0.9'}
try:
from main import _load_wall, _save_wall
except:
_data_dir = "/data" if os.path.isdir("/data") else "/app/data"
_wall_file = os.path.join(_data_dir, "wall_posts.json")
def _load_wall():
try:
if os.path.exists(_wall_file):
with open(_wall_file, 'r', encoding='utf-8') as f: return json.load(f)
except: pass
return []
def _save_wall(posts):
try:
os.makedirs(os.path.dirname(_wall_file), exist_ok=True)
with open(_wall_file+'.tmp', 'w', encoding='utf-8') as f: json.dump(posts[:100], f, ensure_ascii=False)
os.replace(_wall_file+'.tmp', _wall_file)
except: pass
def _clean(s): return re.sub(r'\s+', ' ', str(s or '')).strip()
def _scrape_article_full(url):
"""Scrape article: extract paragraphs + ALL images."""
try:
r = requests.get(url, headers=UA, timeout=15, allow_redirects=True)
r.encoding = 'utf-8'
soup = BeautifulSoup(r.text, 'lxml')
for tag in soup.find_all(['script', 'style', 'nav', 'footer', 'aside', 'form']): tag.decompose()
# Title
h1 = soup.find('h1')
ogt = soup.find('meta', property='og:title')
title = (h1.get_text(strip=True) if h1 else '') or (ogt.get('content', '') if ogt else '')
# OG image
ogi = soup.find('meta', property='og:image')
og_img = ogi.get('content', '') if ogi else ''
if og_img and og_img.startswith('//'): og_img = 'https:' + og_img
# Find content block
block = None
for sel in ['article', '.singular-content', '.detail-content', '.fck_detail', '.content-detail', '.knc-content', 'main', '.cms-body', '.article__body']:
el = soup.select_one(sel)
if el and len(el.find_all('p')) >= 2: block = el; break
if not block: block = soup.body or soup
# Extract paragraphs and images IN ORDER
paragraphs = []
images = []
seen_imgs = set()
if og_img and og_img not in seen_imgs:
images.append(og_img)
seen_imgs.add(og_img)
for el in block.find_all(['p', 'h2', 'h3', 'figure', 'img'], recursive=True):
if el.name == 'p':
t = _clean(el.get_text(strip=True))
if t and len(t) > 40:
paragraphs.append(t)
elif el.name in ('figure', 'img'):
im = el if el.name == 'img' else el.find('img')
if im:
src = im.get('data-src') or im.get('src') or im.get('data-original') or ''
if src and 'base64' not in src:
if src.startswith('//'): src = 'https:' + src
if src not in seen_imgs:
images.append(src)
seen_imgs.add(src)
return {'title': _clean(title), 'paragraphs': paragraphs, 'images': images, 'og_img': og_img}
except Exception as e:
return None
def _extract_key_points(paragraphs, max_points=5):
"""Extract key points: take first sentence of each significant paragraph."""
points = []
for p in paragraphs:
if len(points) >= max_points: break
# Take first complete sentence (ends with . ! ?)
m = re.match(r'^(.+?[.!?])\s', p)
if m:
sentence = m.group(1)
else:
sentence = p[:150] + ('.' if not p.endswith('.') else '')
# Skip if too short or duplicate
if len(sentence) < 30: continue
if any(sentence[:50] in existing for existing in points): continue
points.append(sentence)
return points
@app.post("/api/rewrite_slide")
async def api_rewrite_slide(request: Request):
"""
Fast rewrite as SLIDES:
- Extract key points from article (1 sentence each, full and complete)
- Pair each point with an image from the article
- Return as slides array for frontend to display
- Save to Tường AI
NO AI NEEDED - instant response.
"""
body = await request.json()
url = _clean(body.get("url", ""))
context = body.get("context", "")
if not url and not context:
return JSONResponse({"error": "Cần URL hoặc nội dung"}, status_code=400)
# Scrape article
data = None
if url and url.startswith("http"):
data = _scrape_article_full(url)
if not data and context:
# Use context passed from frontend
paragraphs = [_clean(p) for p in context.split('\n') if len(_clean(p)) > 40]
data = {'title': paragraphs[0][:80] if paragraphs else 'Bài viết', 'paragraphs': paragraphs, 'images': [], 'og_img': ''}
if not data or not data.get('paragraphs'):
return JSONResponse({"error": "Không đọc được bài viết"}, status_code=422)
# Extract key points
points = _extract_key_points(data['paragraphs'], max_points=6)
if not points:
return JSONResponse({"error": "Không tìm được ý chính"}, status_code=422)
# Build slides: pair each point with an image
images = data.get('images', [])
slides = []
for i, point in enumerate(points):
img = images[i] if i < len(images) else (images[-1] if images else '')
# Proxy dantri images
if img and 'cdnphoto.dantri' in img:
img = '/api/proxy/img?url=' + quote(img, safe='')
slides.append({
'text': point,
'image': img,
'index': i + 1
})
# Create post for Tường AI
summary_text = '\n\n'.join([f"• {s['text']}" for s in slides])
# Auto voice + emotion based on topic (reuse ai_ext detector if available)
try:
from ai_ext import _detect_voice_emotion
_voice, _emotion = _detect_voice_emotion(data['title'], summary_text)
except Exception:
_voice, _emotion = "hoaimy", "trung_tinh"
post = {
"id": str(int(time.time() * 1000)) + str(random.randint(100, 999)),
"title": data['title'],
"text": summary_text,
"img": images[0] if images else '',
"url": url,
"kind": "slide_summary",
"slides": slides,
"images": images[:10],
"video": "",
"voice": _voice,
"emotion": _emotion,
"ts": int(time.time())
}
# Save to wall
posts = _load_wall()
posts.insert(0, post)
_save_wall(posts)
return JSONResponse({"post": post, "slides": slides})