Spaces:
Running
Running
FIX: ai_patch.py - Vietnamese font auto-detection, robust text segmentation, word wrapping
Browse files- ai_patch.py +142 -17
ai_patch.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import re
|
| 3 |
import time
|
|
@@ -22,6 +25,77 @@ except Exception:
|
|
| 22 |
|
| 23 |
_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())
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
def _clean(s):
|
| 26 |
"""Clean text - replace None with empty string, collapse whitespace."""
|
| 27 |
s = s or ''
|
|
@@ -157,39 +231,90 @@ def _source_line(sources=None):
|
|
| 157 |
return "Nguồn tham khảo: VNEWS"
|
| 158 |
|
| 159 |
def _summary_segments_from_post(post, max_segments=15):
|
|
|
|
| 160 |
text = post.get('text', '')
|
| 161 |
segs = []
|
| 162 |
-
if text:
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
return segs[:max_segments]
|
| 168 |
|
| 169 |
def _make_scene_frame(post, segment, idx, total, img_path, out_path, emotion='neutral'):
|
|
|
|
| 170 |
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
|
|
| 171 |
sz = (1080, 1920)
|
| 172 |
bg = Image.new('RGB', sz, (15, 23, 38))
|
| 173 |
d = ImageDraw.Draw(bg)
|
| 174 |
-
|
| 175 |
-
f = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 48)
|
| 176 |
-
fb = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 56)
|
| 177 |
-
except:
|
| 178 |
-
f = ImageFont.load_default()
|
| 179 |
-
fb = f
|
| 180 |
# Draw title bar
|
| 181 |
d.rectangle([0, 0, sz[0], 120], fill=(25, 118, 210))
|
| 182 |
title = post.get('title', '')[:60]
|
| 183 |
d.text((sz[0]//2, 60), title, fill='white', font=fb, anchor='mm')
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
y += 56
|
| 188 |
-
if y > sz[1] -
|
| 189 |
break
|
| 190 |
-
|
| 191 |
bg.save(out_path, quality=85)
|
| 192 |
|
| 193 |
@app.get('/health')
|
| 194 |
async def health():
|
| 195 |
-
return JSONResponse({'status': 'ok'})
|
|
|
|
| 1 |
+
"""ai_patch.py — Helper functions for rewrite, summary, short frame creation.
|
| 2 |
+
FIX v2: Vietnamese font auto-detection (not hardcoded DejaVu paths).
|
| 3 |
+
"""
|
| 4 |
import os
|
| 5 |
import re
|
| 6 |
import time
|
|
|
|
| 25 |
|
| 26 |
_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())
|
| 27 |
|
| 28 |
+
# ===== VIETNAMESE FONT DETECTION =====
|
| 29 |
+
_VN_FONT_REG = None
|
| 30 |
+
_VN_FONT_BOLD = None
|
| 31 |
+
|
| 32 |
+
def _get_vn_fonts():
|
| 33 |
+
"""Auto-detect fonts that support Vietnamese diacritics."""
|
| 34 |
+
global _VN_FONT_REG, _VN_FONT_BOLD
|
| 35 |
+
if _VN_FONT_REG is not None:
|
| 36 |
+
return _VN_FONT_REG, _VN_FONT_BOLD
|
| 37 |
+
|
| 38 |
+
# Try system fonts in priority order (Noto first for full Vietnamese)
|
| 39 |
+
reg_paths = [
|
| 40 |
+
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
|
| 41 |
+
"/usr/share/fonts/truetype/noto/NotoSans-Variable.ttf",
|
| 42 |
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
| 43 |
+
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
|
| 44 |
+
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
| 45 |
+
]
|
| 46 |
+
bold_paths = [
|
| 47 |
+
"/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
|
| 48 |
+
"/usr/share/fonts/truetype/noto/NotoSans-Variable.ttf",
|
| 49 |
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
| 50 |
+
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
|
| 51 |
+
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
# Also search broadly
|
| 55 |
+
try:
|
| 56 |
+
result = subprocess.run(
|
| 57 |
+
['find', '/usr/share/fonts', '-name', '*.ttf', '-type', 'f'],
|
| 58 |
+
capture_output=True, text=True, timeout=5
|
| 59 |
+
)
|
| 60 |
+
all_ttf = result.stdout.strip().split('\n')
|
| 61 |
+
for key in ['noto', 'dejavu', 'liberation', 'freefont', 'vn']:
|
| 62 |
+
for path in all_ttf:
|
| 63 |
+
pl = path.lower()
|
| 64 |
+
if key in pl:
|
| 65 |
+
if ('bold' in pl or 'variable' in pl) and _VN_FONT_BOLD is None:
|
| 66 |
+
try:
|
| 67 |
+
_VN_FONT_BOLD = ImageFont.truetype(path, 56)
|
| 68 |
+
except:
|
| 69 |
+
pass
|
| 70 |
+
elif ('regular' in pl or path == '/usr/share/fonts/truetype/vn/VNFont.ttf') and _VN_FONT_REG is None:
|
| 71 |
+
try:
|
| 72 |
+
_VN_FONT_REG = ImageFont.truetype(path, 48)
|
| 73 |
+
except:
|
| 74 |
+
pass
|
| 75 |
+
except:
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
# Try regular paths
|
| 79 |
+
for path in reg_paths:
|
| 80 |
+
if os.path.exists(path) and _VN_FONT_REG is None:
|
| 81 |
+
try:
|
| 82 |
+
_VN_FONT_REG = ImageFont.truetype(path, 48)
|
| 83 |
+
except:
|
| 84 |
+
continue
|
| 85 |
+
for path in bold_paths:
|
| 86 |
+
if os.path.exists(path) and _VN_FONT_BOLD is None:
|
| 87 |
+
try:
|
| 88 |
+
_VN_FONT_BOLD = ImageFont.truetype(path, 56)
|
| 89 |
+
except:
|
| 90 |
+
continue
|
| 91 |
+
|
| 92 |
+
if _VN_FONT_REG is None:
|
| 93 |
+
_VN_FONT_REG = ImageFont.load_default()
|
| 94 |
+
if _VN_FONT_BOLD is None:
|
| 95 |
+
_VN_FONT_BOLD = _VN_FONT_REG
|
| 96 |
+
|
| 97 |
+
return _VN_FONT_REG, _VN_FONT_BOLD
|
| 98 |
+
|
| 99 |
def _clean(s):
|
| 100 |
"""Clean text - replace None with empty string, collapse whitespace."""
|
| 101 |
s = s or ''
|
|
|
|
| 231 |
return "Nguồn tham khảo: VNEWS"
|
| 232 |
|
| 233 |
def _summary_segments_from_post(post, max_segments=15):
|
| 234 |
+
"""Split post text into segments for short video. Uses multi-strategy like main generator."""
|
| 235 |
text = post.get('text', '')
|
| 236 |
segs = []
|
| 237 |
+
if not text:
|
| 238 |
+
return segs
|
| 239 |
+
|
| 240 |
+
# Strategy 1: split by newlines (bullet points)
|
| 241 |
+
for line in text.split('\n'):
|
| 242 |
+
line = _clean(line)
|
| 243 |
+
line_bare = re.sub(r'^[•\-\*\d\.\)\s]+', '', line).strip()
|
| 244 |
+
if len(line_bare) > 25:
|
| 245 |
+
segs.append(line_bare)
|
| 246 |
+
|
| 247 |
+
# Strategy 2: split by sentences
|
| 248 |
+
if len(segs) < 3:
|
| 249 |
+
segs = []
|
| 250 |
+
for sent in re.split(r'(?<=[.!?])\s+(?=[A-Z0-9À-ỸĐ])', text):
|
| 251 |
+
sent = _clean(sent)
|
| 252 |
+
if len(sent) > 25:
|
| 253 |
+
segs.append(sent)
|
| 254 |
+
|
| 255 |
+
# Strategy 3: chunk by size
|
| 256 |
+
if len(segs) < 2:
|
| 257 |
+
segs = []
|
| 258 |
+
words = text.split()
|
| 259 |
+
chunk = []
|
| 260 |
+
ccount = 0
|
| 261 |
+
for w in words:
|
| 262 |
+
chunk.append(w)
|
| 263 |
+
ccount += len(w) + 1
|
| 264 |
+
if ccount > 150:
|
| 265 |
+
segs.append(' '.join(chunk))
|
| 266 |
+
chunk = []
|
| 267 |
+
ccount = 0
|
| 268 |
+
if chunk:
|
| 269 |
+
segs.append(' '.join(chunk))
|
| 270 |
+
|
| 271 |
return segs[:max_segments]
|
| 272 |
|
| 273 |
def _make_scene_frame(post, segment, idx, total, img_path, out_path, emotion='neutral'):
|
| 274 |
+
"""Create a scene frame with Vietnamese font support."""
|
| 275 |
from PIL import Image, ImageDraw, ImageFont
|
| 276 |
+
f, fb = _get_vn_fonts()
|
| 277 |
+
|
| 278 |
sz = (1080, 1920)
|
| 279 |
bg = Image.new('RGB', sz, (15, 23, 38))
|
| 280 |
d = ImageDraw.Draw(bg)
|
| 281 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
# Draw title bar
|
| 283 |
d.rectangle([0, 0, sz[0], 120], fill=(25, 118, 210))
|
| 284 |
title = post.get('title', '')[:60]
|
| 285 |
d.text((sz[0]//2, 60), title, fill='white', font=fb, anchor='mm')
|
| 286 |
+
|
| 287 |
+
# Draw segment text with word wrapping
|
| 288 |
+
max_width = 920
|
| 289 |
+
words = (segment or '')[:200].split()
|
| 290 |
+
lines = []
|
| 291 |
+
cur = []
|
| 292 |
+
for w in words:
|
| 293 |
+
test = ' '.join(cur + [w])
|
| 294 |
+
try:
|
| 295 |
+
tw = d.textbbox((0, 0), test, font=f)[2]
|
| 296 |
+
except:
|
| 297 |
+
tw = len(test) * 22
|
| 298 |
+
if tw <= max_width:
|
| 299 |
+
cur.append(w)
|
| 300 |
+
else:
|
| 301 |
+
if cur:
|
| 302 |
+
lines.append(' '.join(cur))
|
| 303 |
+
cur = [w]
|
| 304 |
+
if len(lines) >= 10:
|
| 305 |
+
break
|
| 306 |
+
if cur and len(lines) < 10:
|
| 307 |
+
lines.append(' '.join(cur))
|
| 308 |
+
|
| 309 |
+
y = 200
|
| 310 |
+
for line in lines:
|
| 311 |
+
d.text((80, y), line, fill='white', font=f)
|
| 312 |
y += 56
|
| 313 |
+
if y > sz[1] - 80:
|
| 314 |
break
|
| 315 |
+
|
| 316 |
bg.save(out_path, quality=85)
|
| 317 |
|
| 318 |
@app.get('/health')
|
| 319 |
async def health():
|
| 320 |
+
return JSONResponse({'status': 'ok'})
|