Spaces:
Running
Running
| import os, re, subprocess, json, time, hashlib | |
| import ai_patch as old | |
| from ai_patch import app | |
| import ai_ext as base | |
| from fastapi import Request | |
| from fastapi.responses import JSONResponse, HTMLResponse, FileResponse | |
| try: | |
| from PIL import Image, ImageDraw, ImageFont | |
| except Exception: | |
| Image = ImageDraw = ImageFont = None | |
| def clean(s): | |
| import html as html_lib | |
| return re.sub(r"\s+", " ", html_lib.unescape(s or "")).strip() | |
| def _domain(url): | |
| try: | |
| from urllib.parse import urlparse | |
| return urlparse(url or '').netloc.replace('www.','') | |
| except Exception: | |
| return '' | |
| def _strip_bullet_prefix(s): | |
| return clean(re.sub(r'^[\s\•\-\*·▪▫●○\d\.\)\(]+', '', s or '')) | |
| def source_line(sources): | |
| names=[] | |
| for s in (sources or [])[:5]: | |
| via=s.get('via') or _domain(s.get('url','')) or s.get('title','') | |
| if via and via not in names:names.append(via) | |
| return 'Nguồn tham khảo: '+', '.join(names[:5]) if names else 'Nguồn tham khảo: tổng hợp internet' | |
| def _source_badge(post): | |
| sources=post.get('sources') or [] | |
| for s in sources: | |
| via=s.get('via') or _domain(s.get('url','')) | |
| if via:return via | |
| return _domain(post.get('url','')) or post.get('source') or 'VNEWS' | |
| def _collect_all_images(data): | |
| imgs=[] | |
| def add(u): | |
| u=(u or '').strip() | |
| if not u or u.startswith('data:') or 'base64' in u:return | |
| if u.startswith('//'):u='https:'+u | |
| if u not in imgs:imgs.append(u) | |
| add(data.get('image') or data.get('og_image') or data.get('img')) | |
| for u in data.get('images') or []:add(u) | |
| for b in data.get('body') or []: | |
| if isinstance(b,dict) and b.get('type')=='img':add(b.get('src')) | |
| return imgs[:20] | |
| def _scrape_url_with_images(url): | |
| data=base.scrape_any_url(url) | |
| try: | |
| import requests | |
| from bs4 import BeautifulSoup | |
| r=requests.get(url,headers=base.HEADERS,timeout=18);r.encoding='utf-8' | |
| soup=BeautifulSoup(r.text,'lxml') | |
| extra=[] | |
| for im in soup.find_all('img'): | |
| src=im.get('data-src') or im.get('data-original') or im.get('data-lazy-src') or im.get('src') or '' | |
| if src.startswith('//'):src='https:'+src | |
| if src and 'base64' not in src and src not in extra: | |
| low=src.lower() | |
| if any(x in low for x in ['logo','icon','avatar','sprite']): | |
| continue | |
| extra.append(src) | |
| if len(extra)>=20:break | |
| data['images']=_collect_all_images(data)+[u for u in extra if u not in _collect_all_images(data)] | |
| except Exception: | |
| data['images']=_collect_all_images(data) | |
| data['images']=_collect_all_images(data) | |
| if data['images'] and not data.get('image'): | |
| data['image']=data['images'][0] | |
| return data | |
| def rich_context(topic, limit=5): | |
| try: ctx,sources=base.web_context(topic, limit=limit) | |
| except Exception: ctx,sources='',[] | |
| rich=[];rs=[];seen=set() | |
| for s in (sources or [])[:limit*2]: | |
| url=s.get('url') or '' | |
| if not url.startswith('http') or url in seen:continue | |
| seen.add(url) | |
| try: | |
| data=base.scrape_any_url(url) | |
| raw=(data.get('summary','')+'\n'+data.get('text','')).strip() | |
| if len(raw)<180:continue | |
| title=data.get('title') or s.get('title') or url | |
| via=data.get('via') or s.get('via') or _domain(url) | |
| rich.append(f"### {title} ({via})\n{raw[:2600]}") | |
| rs.append({'title':title,'url':url,'excerpt':raw[:700],'via':via}) | |
| if len(rich)>=limit:break | |
| except Exception:continue | |
| if rich:return '\n\n'.join(rich),rs | |
| return ctx or f'Chủ đề: {topic}', sources or [] | |
| def postprocess(text): | |
| if hasattr(old,'_postprocess_ai_text'): | |
| out=old._postprocess_ai_text(text, max_units=7) | |
| else: | |
| out=clean(text) | |
| return out | |
| _PATCH={('/api/topic_post','POST'),('/api/url_wall','POST'),('/api/rewrite_share','POST'),('/api/ai/url','POST'),('/api/ai/short/{post_id}','POST'),('/api/ai/short-file/{file_id}','GET'),('/','GET')} | |
| 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)] | |
| async def url_wall_only(request:Request): | |
| body=await request.json();url=base._clean_text(body.get('url','')) | |
| if not url.startswith('http'):return JSONResponse({'error':'missing url'},status_code=400) | |
| try:data=_scrape_url_with_images(url) | |
| except Exception as e:return JSONResponse({'error':'Không scrape được URL: '+str(e)[:180]},status_code=422) | |
| raw=(data.get('summary','')+'\n'+data.get('text','')).strip() | |
| if len(raw)<120:return JSONResponse({'error':'URL không có đủ nội dung để tóm tắt'},status_code=422) | |
| prompt=f"Tóm tắt bài viết nguồn dưới đây để đăng lên Tường AI VNEWS.\n\nYêu cầu bắt buộc:\n- Chỉ tóm tắt nội dung chính, không viết lại toàn bộ bài.\n- Ngắn gọn, cụ thể, dễ hiểu.\n- Không lặp lại ý và không thêm chi tiết ngoài nguồn.\n- Tối đa 5 ý chính hoặc 2 đoạn ngắn.\n- Tránh dùng dấu đầu dòng nếu không thật cần thiết.\n\nTiêu đề gốc: {data.get('title','')}\nNguồn: {data.get('via','') or _domain(url)}\nNội dung gốc:\n{raw[:16000]}" | |
| text=await base.qwen_generate(prompt,image_url=(data.get('image') or None),max_tokens=900) | |
| if not text:text=old._fallback_summary_from_prompt(prompt,max_units=5) if hasattr(old,'_fallback_summary_from_prompt') else raw[:900] | |
| text=postprocess(text) | |
| src=[{'title':data.get('title'), 'url':url, 'excerpt':raw[:500], 'via':data.get('via') or _domain(url)}] | |
| if 'Nguồn tham khảo:' not in text:text+='\n\n'+source_line(src) | |
| images=_collect_all_images(data) | |
| post=base.make_post(data.get('title') or 'Bài viết',text,images[0] if images else (data.get('image') or ''),url,'url',sources=src) | |
| post['images']=images | |
| posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts) | |
| return JSONResponse({'post':post}) | |
| async def rewrite_share_url_only(request:Request): | |
| return await url_wall_only(request) | |
| async def ai_url_compat(request:Request): | |
| return await url_wall_only(request) | |
| async def topic_disabled(request:Request): | |
| return JSONResponse({'error':'Đã tắt tạo bài theo chủ đề. Vui lòng dán URL bài viết để AI tóm tắt.'},status_code=410) | |