Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,9 +11,11 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 11 |
import uvicorn
|
| 12 |
from huggingface_hub import HfApi, hf_hub_download
|
| 13 |
from typing import Optional, List, Dict, Any
|
|
|
|
|
|
|
| 14 |
|
| 15 |
print("=" * 60)
|
| 16 |
-
print("🚀 TEXT STYLING API - ASS SUBTITLE METHOD")
|
| 17 |
print("=" * 60)
|
| 18 |
|
| 19 |
# =============================================
|
|
@@ -46,7 +48,7 @@ def download_all_fonts():
|
|
| 46 |
fonts = {}
|
| 47 |
|
| 48 |
for file in files:
|
| 49 |
-
if file.endswith(('.ttf', '.otf')):
|
| 50 |
font_name = os.path.basename(file)
|
| 51 |
local_path = os.path.join(FONTS_DIR, font_name)
|
| 52 |
|
|
@@ -66,7 +68,7 @@ def download_all_fonts():
|
|
| 66 |
fonts[font_key] = {
|
| 67 |
"path": local_path,
|
| 68 |
"name": font_name,
|
| 69 |
-
"display_name": font_name.replace('.ttf', '').replace('.otf', '').replace('-', ' ')
|
| 70 |
}
|
| 71 |
|
| 72 |
print(f"✅ Loaded {len(fonts)} fonts")
|
|
@@ -78,6 +80,25 @@ def download_all_fonts():
|
|
| 78 |
# Download fonts at startup
|
| 79 |
FONTS = download_all_fonts()
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
# =============================================
|
| 82 |
# CREATE FASTAPI APP
|
| 83 |
# =============================================
|
|
@@ -162,6 +183,7 @@ def create_text_overlay(input_video, output_video, text_style):
|
|
| 162 |
return False
|
| 163 |
|
| 164 |
print(f"✅ Using font: {font_path}")
|
|
|
|
| 165 |
|
| 166 |
# Create working directory for ASS file
|
| 167 |
work_dir = os.path.dirname(output_video)
|
|
@@ -171,7 +193,8 @@ def create_text_overlay(input_video, output_video, text_style):
|
|
| 171 |
color_map = {
|
| 172 |
"white": "FFFFFF", "black": "000000", "red": "FF0000",
|
| 173 |
"green": "00FF00", "blue": "0000FF", "yellow": "FFFF00",
|
| 174 |
-
"gold": "FFD700", "purple": "800080", "cyan": "00FFFF"
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
# Get font color
|
|
@@ -248,12 +271,174 @@ Dialogue: 0,0:00:00.00,0:00:10.00,Default,,0,0,0,,{text_style.text}"""
|
|
| 248 |
return True
|
| 249 |
|
| 250 |
# =============================================
|
| 251 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
# =============================================
|
| 253 |
|
| 254 |
@app.get("/health")
|
| 255 |
async def health():
|
| 256 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
@app.get("/fonts")
|
| 259 |
async def list_fonts():
|
|
@@ -278,6 +463,12 @@ async def style_video(request: StylingRequest):
|
|
| 278 |
titled_path = os.path.join(work_dir, "titled.mp4")
|
| 279 |
if create_text_overlay(current_video, titled_path, request.title_overlay):
|
| 280 |
current_video = titled_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
# Upload styled video
|
| 283 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
@@ -312,11 +503,16 @@ async def style_video(request: StylingRequest):
|
|
| 312 |
async def root():
|
| 313 |
return {
|
| 314 |
"name": "Text Styling API",
|
| 315 |
-
"version": "
|
| 316 |
"endpoints": {
|
| 317 |
"style": "POST /api/style",
|
| 318 |
"fonts": "GET /fonts",
|
| 319 |
-
"health": "GET /health"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
},
|
| 321 |
"fonts_loaded": len(FONTS)
|
| 322 |
}
|
|
|
|
| 11 |
import uvicorn
|
| 12 |
from huggingface_hub import HfApi, hf_hub_download
|
| 13 |
from typing import Optional, List, Dict, Any
|
| 14 |
+
import base64
|
| 15 |
+
from io import BytesIO
|
| 16 |
|
| 17 |
print("=" * 60)
|
| 18 |
+
print("🚀 TEXT STYLING API - ASS SUBTITLE METHOD WITH DEBUG")
|
| 19 |
print("=" * 60)
|
| 20 |
|
| 21 |
# =============================================
|
|
|
|
| 48 |
fonts = {}
|
| 49 |
|
| 50 |
for file in files:
|
| 51 |
+
if file.endswith(('.ttf', '.otf', '.ttc')):
|
| 52 |
font_name = os.path.basename(file)
|
| 53 |
local_path = os.path.join(FONTS_DIR, font_name)
|
| 54 |
|
|
|
|
| 68 |
fonts[font_key] = {
|
| 69 |
"path": local_path,
|
| 70 |
"name": font_name,
|
| 71 |
+
"display_name": font_name.replace('.ttf', '').replace('.otf', '').replace('.ttc', '').replace('-', ' ')
|
| 72 |
}
|
| 73 |
|
| 74 |
print(f"✅ Loaded {len(fonts)} fonts")
|
|
|
|
| 80 |
# Download fonts at startup
|
| 81 |
FONTS = download_all_fonts()
|
| 82 |
|
| 83 |
+
# =============================================
|
| 84 |
+
# TRY TO IMPORT PIL (for debug endpoints)
|
| 85 |
+
# =============================================
|
| 86 |
+
try:
|
| 87 |
+
from PIL import Image, ImageFont, ImageDraw
|
| 88 |
+
PIL_AVAILABLE = True
|
| 89 |
+
print("✅ PIL available for font testing")
|
| 90 |
+
except ImportError:
|
| 91 |
+
PIL_AVAILABLE = False
|
| 92 |
+
print("⚠️ PIL not available - font testing limited")
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
from fontTools import ttLib
|
| 96 |
+
FONTTOOLS_AVAILABLE = True
|
| 97 |
+
print("✅ fontTools available for font analysis")
|
| 98 |
+
except ImportError:
|
| 99 |
+
FONTTOOLS_AVAILABLE = False
|
| 100 |
+
print("⚠️ fontTools not available - font analysis limited")
|
| 101 |
+
|
| 102 |
# =============================================
|
| 103 |
# CREATE FASTAPI APP
|
| 104 |
# =============================================
|
|
|
|
| 183 |
return False
|
| 184 |
|
| 185 |
print(f"✅ Using font: {font_path}")
|
| 186 |
+
print(f"📝 Text to render: {text_style.text}")
|
| 187 |
|
| 188 |
# Create working directory for ASS file
|
| 189 |
work_dir = os.path.dirname(output_video)
|
|
|
|
| 193 |
color_map = {
|
| 194 |
"white": "FFFFFF", "black": "000000", "red": "FF0000",
|
| 195 |
"green": "00FF00", "blue": "0000FF", "yellow": "FFFF00",
|
| 196 |
+
"gold": "FFD700", "purple": "800080", "cyan": "00FFFF",
|
| 197 |
+
"pink": "FFC0CB", "orange": "FFA500", "brown": "A52A2A"
|
| 198 |
}
|
| 199 |
|
| 200 |
# Get font color
|
|
|
|
| 271 |
return True
|
| 272 |
|
| 273 |
# =============================================
|
| 274 |
+
# DEBUG ENDPOINTS
|
| 275 |
+
# =============================================
|
| 276 |
+
|
| 277 |
+
@app.get("/debug/characters/{font_name}")
|
| 278 |
+
async def debug_characters(font_name: str):
|
| 279 |
+
"""Test if font supports specific Chinese characters"""
|
| 280 |
+
if not PIL_AVAILABLE:
|
| 281 |
+
return {"error": "PIL not installed - cannot test characters"}
|
| 282 |
+
|
| 283 |
+
try:
|
| 284 |
+
font_path = None
|
| 285 |
+
for key, font_info in FONTS.items():
|
| 286 |
+
if font_name in key or font_name in font_info["name"]:
|
| 287 |
+
font_path = font_info["path"]
|
| 288 |
+
break
|
| 289 |
+
|
| 290 |
+
if not font_path:
|
| 291 |
+
return {"error": f"Font {font_name} not found"}
|
| 292 |
+
|
| 293 |
+
# Test different character sets
|
| 294 |
+
test_texts = [
|
| 295 |
+
("English", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
|
| 296 |
+
("Numbers", "0123456789"),
|
| 297 |
+
("Common Chinese", "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进等着部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞"),
|
| 298 |
+
("Test Phrase", "荆南麦圆体测试"),
|
| 299 |
+
("Your Text", font_name)
|
| 300 |
+
]
|
| 301 |
+
|
| 302 |
+
images = []
|
| 303 |
+
for label, text in test_texts:
|
| 304 |
+
# Create image
|
| 305 |
+
img = Image.new('RGB', (1200, 400), color='white')
|
| 306 |
+
d = ImageDraw.Draw(img)
|
| 307 |
+
|
| 308 |
+
try:
|
| 309 |
+
# Try to load font with size 24
|
| 310 |
+
font = ImageFont.truetype(font_path, 24)
|
| 311 |
+
# Draw label
|
| 312 |
+
d.text((10, 10), f"{label}:", fill='black', font=font)
|
| 313 |
+
# Draw text (wrap if too long)
|
| 314 |
+
d.text((10, 50), str(text)[:200], fill='black', font=font)
|
| 315 |
+
except Exception as e:
|
| 316 |
+
d.text((10, 10), f"Error: {str(e)}", fill='red', font=ImageFont.load_default())
|
| 317 |
+
|
| 318 |
+
# Convert to base64
|
| 319 |
+
buffered = BytesIO()
|
| 320 |
+
img.save(buffered, format="PNG")
|
| 321 |
+
img_base64 = base64.b64encode(buffered.getvalue()).decode()
|
| 322 |
+
images.append(img_base64)
|
| 323 |
+
|
| 324 |
+
return {
|
| 325 |
+
"font_name": font_name,
|
| 326 |
+
"font_path": font_path,
|
| 327 |
+
"font_size_bytes": os.path.getsize(font_path),
|
| 328 |
+
"test_images": images,
|
| 329 |
+
"message": "If Chinese characters appear as boxes, the font lacks those glyphs"
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
except Exception as e:
|
| 333 |
+
return {"error": str(e)}
|
| 334 |
+
|
| 335 |
+
@app.get("/debug/font-info/{font_name}")
|
| 336 |
+
async def font_info(font_name: str):
|
| 337 |
+
"""Get detailed font information"""
|
| 338 |
+
try:
|
| 339 |
+
font_path = None
|
| 340 |
+
for key, font_info in FONTS.items():
|
| 341 |
+
if font_name in key or font_name in font_info["name"]:
|
| 342 |
+
font_path = font_info["path"]
|
| 343 |
+
break
|
| 344 |
+
|
| 345 |
+
if not font_path:
|
| 346 |
+
return {"error": f"Font {font_name} not found"}
|
| 347 |
+
|
| 348 |
+
info = {
|
| 349 |
+
"font_name": font_name,
|
| 350 |
+
"path": font_path,
|
| 351 |
+
"size_bytes": os.path.getsize(font_path),
|
| 352 |
+
"pil_available": PIL_AVAILABLE,
|
| 353 |
+
"fonttools_available": FONTTOOLS_AVAILABLE
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
# Try to get basic info with PIL
|
| 357 |
+
if PIL_AVAILABLE:
|
| 358 |
+
try:
|
| 359 |
+
font = ImageFont.truetype(font_path, 20)
|
| 360 |
+
info["pil_loads"] = True
|
| 361 |
+
except Exception as e:
|
| 362 |
+
info["pil_loads"] = False
|
| 363 |
+
info["pil_error"] = str(e)
|
| 364 |
+
|
| 365 |
+
# Try to get detailed info with fontTools
|
| 366 |
+
if FONTTOOLS_AVAILABLE:
|
| 367 |
+
try:
|
| 368 |
+
from fontTools import ttLib
|
| 369 |
+
font = ttLib.TTFont(font_path)
|
| 370 |
+
|
| 371 |
+
# Get font name
|
| 372 |
+
name_records = {}
|
| 373 |
+
for record in font['name'].names:
|
| 374 |
+
try:
|
| 375 |
+
if record.nameID == 1:
|
| 376 |
+
name_records['family'] = record.string.decode('utf-16-be')
|
| 377 |
+
elif record.nameID == 2:
|
| 378 |
+
name_records['subfamily'] = record.string.decode('utf-16-be')
|
| 379 |
+
elif record.nameID == 4:
|
| 380 |
+
name_records['full'] = record.string.decode('utf-16-be')
|
| 381 |
+
elif record.nameID == 6:
|
| 382 |
+
name_records['postscript'] = record.string.decode('utf-16-be')
|
| 383 |
+
except:
|
| 384 |
+
pass
|
| 385 |
+
|
| 386 |
+
info["names"] = name_records
|
| 387 |
+
|
| 388 |
+
# Check for Chinese glyphs
|
| 389 |
+
cmap = font.getBestCmap()
|
| 390 |
+
chinese_ranges = [
|
| 391 |
+
(0x4E00, 0x9FFF), # CJK Unified Ideographs
|
| 392 |
+
(0x3400, 0x4DBF), # Extension A
|
| 393 |
+
(0x20000, 0x2A6DF), # Extension B
|
| 394 |
+
]
|
| 395 |
+
|
| 396 |
+
has_chinese = False
|
| 397 |
+
chinese_count = 0
|
| 398 |
+
for start, end in chinese_ranges:
|
| 399 |
+
for code in range(start, min(start+1000, end), 100):
|
| 400 |
+
if code in cmap:
|
| 401 |
+
has_chinese = True
|
| 402 |
+
chinese_count += 1
|
| 403 |
+
|
| 404 |
+
info["has_chinese_glyphs"] = has_chinese
|
| 405 |
+
info["approx_chinese_glyphs"] = chinese_count * 100
|
| 406 |
+
info["total_glyphs"] = len(font.getGlyphOrder())
|
| 407 |
+
|
| 408 |
+
except Exception as e:
|
| 409 |
+
info["fonttools_error"] = str(e)
|
| 410 |
+
|
| 411 |
+
return info
|
| 412 |
+
|
| 413 |
+
except Exception as e:
|
| 414 |
+
return {"error": str(e)}
|
| 415 |
+
|
| 416 |
+
@app.get("/debug/font-list")
|
| 417 |
+
async def debug_font_list():
|
| 418 |
+
"""List all fonts with basic info"""
|
| 419 |
+
font_list = []
|
| 420 |
+
for key, font_info in FONTS.items():
|
| 421 |
+
font_list.append({
|
| 422 |
+
"id": key,
|
| 423 |
+
"name": font_info["display_name"],
|
| 424 |
+
"file": font_info["name"],
|
| 425 |
+
"path": font_info["path"],
|
| 426 |
+
"size_kb": round(os.path.getsize(font_info["path"]) / 1024, 1)
|
| 427 |
+
})
|
| 428 |
+
return {"fonts": font_list}
|
| 429 |
+
|
| 430 |
+
# =============================================
|
| 431 |
+
# MAIN API ENDPOINTS
|
| 432 |
# =============================================
|
| 433 |
|
| 434 |
@app.get("/health")
|
| 435 |
async def health():
|
| 436 |
+
return {
|
| 437 |
+
"status": "healthy",
|
| 438 |
+
"fonts_loaded": len(FONTS),
|
| 439 |
+
"pil_available": PIL_AVAILABLE,
|
| 440 |
+
"fonttools_available": FONTTOOLS_AVAILABLE
|
| 441 |
+
}
|
| 442 |
|
| 443 |
@app.get("/fonts")
|
| 444 |
async def list_fonts():
|
|
|
|
| 463 |
titled_path = os.path.join(work_dir, "titled.mp4")
|
| 464 |
if create_text_overlay(current_video, titled_path, request.title_overlay):
|
| 465 |
current_video = titled_path
|
| 466 |
+
else:
|
| 467 |
+
return StylingResponse(
|
| 468 |
+
status="error",
|
| 469 |
+
project_id=request.project_id,
|
| 470 |
+
error="Failed to add text overlay"
|
| 471 |
+
)
|
| 472 |
|
| 473 |
# Upload styled video
|
| 474 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
| 503 |
async def root():
|
| 504 |
return {
|
| 505 |
"name": "Text Styling API",
|
| 506 |
+
"version": "2.0.0",
|
| 507 |
"endpoints": {
|
| 508 |
"style": "POST /api/style",
|
| 509 |
"fonts": "GET /fonts",
|
| 510 |
+
"health": "GET /health",
|
| 511 |
+
"debug": {
|
| 512 |
+
"font_list": "GET /debug/font-list",
|
| 513 |
+
"font_info": "GET /debug/font-info/{font_name}",
|
| 514 |
+
"characters": "GET /debug/characters/{font_name}"
|
| 515 |
+
}
|
| 516 |
},
|
| 517 |
"fonts_loaded": len(FONTS)
|
| 518 |
}
|