Spaces:
Running
Running
File size: 11,411 Bytes
16ee98b 4b6e868 89e1bbf 4b6e868 89e1bbf 4b6e868 16ee98b 4b6e868 89e1bbf 4b6e868 311576b 4b6e868 311576b 0b2c8ee f900d99 87fe551 f900d99 0b2c8ee f900d99 89e1bbf f900d99 89e1bbf 4b6e868 89e1bbf 4b6e868 f900d99 89e1bbf 4b6e868 89e1bbf 311576b 89e1bbf 311576b 4b6e868 84814ec 89e1bbf 4b6e868 89e1bbf 84814ec 89e1bbf 84814ec 89e1bbf 84814ec 89e1bbf 84814ec 89e1bbf 84814ec 89e1bbf 4b6e868 89e1bbf 7fb7fe0 89e1bbf 84814ec 89e1bbf 4b6e868 89e1bbf 84814ec 89e1bbf 4b6e868 89e1bbf 311576b 89e1bbf 311576b 89e1bbf 16ee98b 89e1bbf 16ee98b 89e1bbf 4b6e868 16ee98b 89e1bbf 16ee98b 89e1bbf 16ee98b 89e1bbf 16ee98b 89e1bbf 16ee98b 89e1bbf 16ee98b 89e1bbf 4b6e868 89e1bbf 16ee98b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 | """ai_patch.py — Helper functions for rewrite, summary, short frame creation.
FIX v2: Vietnamese font auto-detection (not hardcoded DejaVu paths).
"""
import os
import re
import time
import random
import json
import html as html_lib
import subprocess
import requests
import hashlib
from datetime import datetime, timezone
import ai_ext as base
from ai_ext import app
from fastapi import Request
from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
from bs4 import BeautifulSoup
from urllib.parse import quote_plus
try:
from PIL import Image, ImageDraw, ImageFont
except Exception:
Image = ImageDraw = ImageFont = None
_STOP = set('và của các những một được trong với cho tại sau trước khi không người vietnam hôm nay mới nhất nóng tin tức cập nhật theo từ đến là có thì này đã để'.split())
# ===== VIETNAMESE FONT DETECTION =====
_VN_FONT_REG = None
_VN_FONT_BOLD = None
def _get_vn_fonts():
"""Auto-detect fonts that support Vietnamese diacritics."""
global _VN_FONT_REG, _VN_FONT_BOLD
if _VN_FONT_REG is not None:
return _VN_FONT_REG, _VN_FONT_BOLD
# Try system fonts in priority order (Noto first for full Vietnamese)
reg_paths = [
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Variable.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
]
bold_paths = [
"/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Variable.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
]
# Also search broadly
try:
result = subprocess.run(
['find', '/usr/share/fonts', '-name', '*.ttf', '-type', 'f'],
capture_output=True, text=True, timeout=5
)
all_ttf = result.stdout.strip().split('\n')
for key in ['noto', 'dejavu', 'liberation', 'freefont', 'vn']:
for path in all_ttf:
pl = path.lower()
if key in pl:
if ('bold' in pl or 'variable' in pl) and _VN_FONT_BOLD is None:
try:
_VN_FONT_BOLD = ImageFont.truetype(path, 56)
except:
pass
elif ('regular' in pl or path == '/usr/share/fonts/truetype/vn/VNFont.ttf') and _VN_FONT_REG is None:
try:
_VN_FONT_REG = ImageFont.truetype(path, 48)
except:
pass
except:
pass
# Try regular paths
for path in reg_paths:
if os.path.exists(path) and _VN_FONT_REG is None:
try:
_VN_FONT_REG = ImageFont.truetype(path, 48)
except:
continue
for path in bold_paths:
if os.path.exists(path) and _VN_FONT_BOLD is None:
try:
_VN_FONT_BOLD = ImageFont.truetype(path, 56)
except:
continue
if _VN_FONT_REG is None:
_VN_FONT_REG = ImageFont.load_default()
if _VN_FONT_BOLD is None:
_VN_FONT_BOLD = _VN_FONT_REG
return _VN_FONT_REG, _VN_FONT_BOLD
def _clean(s):
"""Clean text - replace None with empty string, collapse whitespace."""
s = s or ''
s = s.strip()
s = re.sub(r'\s+', ' ', s)
return s
def _postprocess_ai_text(text, max_units=20):
"""Clean AI-generated text: remove HTML artifacts, deduplicate, return string with bullet points."""
text = _clean(text)
if not text:
return ""
# Remove malformed HTML with both href and src attributes (e.g., <a href="..." src="...">)
text = re.sub(r'<a[^>]*href\s*=\s*"[^"]*"[^>]*\s*src\s*=\s*"[^"]*"[^>]*>', '', text, flags=re.I)
# Remove any remaining HTML artifacts
text = re.sub(r'<a\s+[^>]*>', '', text, flags=re.I)
text = re.sub(r'<img\s+[^>]*>', '', text, flags=re.I)
text = re.sub(r'<[^>]+>', '', text)
text = re.sub(r'</a>', '', text, flags=re.I)
# Fix truncated HTML tags (missing closing >)
text = text.replace('<a href=" src="', '').replace("<a href=' src='", '')
text = text.replace('<a href=" src=', '').replace("<a href=' src=", '')
# Remove leading bullet/dash
text = re.sub(r'^\s*[-•]\s*', '', text)
# Split by double-space (AI often separates items with double space)
units = text.split(' ')
out = []
seen = set()
for u in units:
u = _clean(u)
if not u:
continue
# Deduplicate by word signature
w = tuple(re.findall(r"[\w']+", u.lower()))
if w in seen:
continue
seen.add(w)
out.append("• " + u)
if len(out) >= max_units:
break
return "\n".join(out) if out else ""
def _scrape_article_images(url):
"""Use the comprehensive scraper from ai_ext for images + paragraphs."""
sents = []; imgs = []; og = ''
try:
if url:
# Use the comprehensive scraper first
try:
data = base.scrape_any_url(url)
if data and data.get('text'):
sents = [p.strip() for p in data['text'].split('\n') if len(p.strip()) > 40]
imgs = data.get('images', [])
og = data.get('og_image', '')
if not imgs and og:
imgs = [og]
if sents or imgs:
return {'paragraphs': sents, 'images': imgs, 'og_img': og}
except:
pass
# Fallback: basic scrape
r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=8)
r.encoding = 'utf-8'
soup = BeautifulSoup(r.text, 'lxml')
og = soup.find('meta', property='og:image')
og = og.get('content', '') if og else ''
if og and og.startswith('//'):
og = 'https:' + og
for p in soup.find_all('p'):
t = _clean(p.get_text())
if len(t) > 30:
sents.append(t)
seen = set()
for im in soup.find_all('img'):
src = im.get('data-src') or im.get('src') or im.get('data-lazy') or ''
if src and 'base64' not in src and src not in seen:
seen.add(src)
if src.startswith('//'):
src = 'https:' + src
imgs.append(src)
# Filter: prefer large images
imgs = [im for im in imgs if not any(x in im.lower() for x in ['logo', 'icon', 'avatar', '.svg', 'banner'])]
return {'paragraphs': sents, 'images': imgs, 'og_img': og}
except Exception as e:
return {'paragraphs': [], 'images': [], 'og_img': ''}
def _extract_key_points_for_slides(paragraphs, max_points=8):
"""Extract key points from paragraphs, splitting long ones into sentence-level points."""
pts = []
for p in paragraphs:
if len(p) < 40:
continue
s = _clean(p)
# Split long paragraphs into sentence-level points
if len(s) > 200:
sentences = re.split(r'(?<=[.!?])\s+(?=[A-Z0-9À-Ỹ])', s)
for sent in sentences:
sent = _clean(sent)
if len(sent) > 60 and sent not in pts:
pts.append(sent)
if len(pts) >= max_points:
break
else:
if s and s not in pts:
pts.append(s)
if len(pts) >= max_points:
break
return pts
def _make_summary_prompt(title, body, source):
topic = source or "báo chính"
return f"""Tóm tắt tin tức: {title}
Nguồn: {topic}
---
{body[:1200] if body else ''}
---
Hãy viết tóm tắt ngắn gọn, súc tích bằng tiếng Việt. Dùng bullet point (•) để liệt kê điểm chính. Không dùng định dạng HTML. Không lặp lại tiêu đề. Không viết "Bản tin VNEWS" hay tiêu đề phụ."""
def _source_line(sources=None):
if not sources:
return "Nguồn tham khảo: VNEWS"
src = sources[0] if sources else {}
title = src.get('title', '')
url = src.get('url', '')
via = src.get('via', '')
if url and via:
return f"Nguồn: [{via}]({url})"
if url:
return f"Nguồn: {url}"
if via:
return f"Nguồn: {via}"
return "Nguồn tham khảo: VNEWS"
def _summary_segments_from_post(post, max_segments=15):
"""Split post text into segments for short video. Uses multi-strategy like main generator."""
text = post.get('text', '')
segs = []
if not text:
return segs
# Strategy 1: split by newlines (bullet points)
for line in text.split('\n'):
line = _clean(line)
line_bare = re.sub(r'^[•\-\*\d\.\)\s]+', '', line).strip()
if len(line_bare) > 25:
segs.append(line_bare)
# Strategy 2: split by sentences
if len(segs) < 3:
segs = []
for sent in re.split(r'(?<=[.!?])\s+(?=[A-Z0-9À-ỸĐ])', text):
sent = _clean(sent)
if len(sent) > 25:
segs.append(sent)
# Strategy 3: chunk by size
if len(segs) < 2:
segs = []
words = text.split()
chunk = []
ccount = 0
for w in words:
chunk.append(w)
ccount += len(w) + 1
if ccount > 150:
segs.append(' '.join(chunk))
chunk = []
ccount = 0
if chunk:
segs.append(' '.join(chunk))
return segs[:max_segments]
def _make_scene_frame(post, segment, idx, total, img_path, out_path, emotion='neutral'):
"""Create a scene frame with Vietnamese font support."""
from PIL import Image, ImageDraw, ImageFont
f, fb = _get_vn_fonts()
sz = (1080, 1920)
bg = Image.new('RGB', sz, (15, 23, 38))
d = ImageDraw.Draw(bg)
# Draw title bar
d.rectangle([0, 0, sz[0], 120], fill=(25, 118, 210))
title = post.get('title', '')[:60]
d.text((sz[0]//2, 60), title, fill='white', font=fb, anchor='mm')
# Draw segment text with word wrapping
max_width = 920
words = (segment or '')[:200].split()
lines = []
cur = []
for w in words:
test = ' '.join(cur + [w])
try:
tw = d.textbbox((0, 0), test, font=f)[2]
except:
tw = len(test) * 22
if tw <= max_width:
cur.append(w)
else:
if cur:
lines.append(' '.join(cur))
cur = [w]
if len(lines) >= 10:
break
if cur and len(lines) < 10:
lines.append(' '.join(cur))
y = 200
for line in lines:
d.text((80, y), line, fill='white', font=f)
y += 56
if y > sz[1] - 80:
break
bg.save(out_path, quality=85)
@app.get('/health')
async def health():
return JSONResponse({'status': 'ok'})
|