"""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})