cang-ying/engines/subtitle-pipeline/font-manager/subtitle-font-manager.py

393 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Subtitle Font Manager · 字幕字体管理器
======================================
固定项目字幕字体不能依赖系统默认字体
功能
1. 检查项目字体是否已安装
2. 如果未安装自动下载/复制到项目字体目录
3. 生成字体配置文件 ASS 渲染器使用
4. 验证字体文件是否有效支持中文
依赖
pip install fonttools # 可选,用于验证字体
# macOS: 系统字体通常在 /System/Library/Fonts/
# Ubuntu: 系统字体通常在 /usr/share/fonts/
用法
# 检查并安装项目字体
python subtitle-font-manager.py --check --install
# 列出可用的中文字体
python subtitle-font-manager.py --list
# 设置项目字体
python subtitle-font-manager.py --set-font "PingFang SC" --output project-font.json
# 验证字体文件是否有效
python subtitle-font-manager.py --validate ./assets/fonts/PingFangSC-Regular.otf
# 生成 FFmpeg 字体路径配置
python subtitle-font-manager.py --generate-ffmpeg-config --output ffmpeg-font.conf
输出
- project-font.json: 项目字体配置
- ./assets/fonts/: 项目字体文件如果需要下载
- ffmpeg-font.conf: FFmpeg 字体配置
路径
video-ai-system/engines/subtitle-pipeline/font-manager/subtitle-font-manager.py
"""
import argparse
import json
import os
import shutil
import sys
from pathlib import Path
# 尝试导入 fonttools可选
try:
from fonttools import TTFont
from fonttools.unicode import Unicode
FONTTOOLS_AVAILABLE = True
except ImportError:
FONTTOOLS_AVAILABLE = False
print("[WARN] 未安装 fonttools将无法验证字体是否支持中文")
# 项目中文字体候选列表(按优先级排序)
CHINESE_FONT_CANDIDATES = [
{
"name": "PingFang SC",
"mac_path": "/System/Library/Fonts/PingFang.ttc",
"windows_path": "C:/Windows/Fonts/pingfang.ttc",
"linux_path": "/usr/share/fonts/truetype/pingfang/PingFangSC-Regular.otf",
"download_url": None, # macOS 系统自带,无需下载
"weight": "Regular"
},
{
"name": "Hiragino Sans GB",
"mac_path": "/System/Library/Fonts/Hiragino Sans GB.ttc",
"windows_path": "C:/Windows/Fonts/hiragino.ttc",
"linux_path": "/usr/share/fonts/truetype/hiragino/HiraginoSansGB-Regular.otf",
"download_url": None,
"weight": "Regular"
},
{
"name": "Noto Sans CJK SC",
"mac_path": "/System/Library/Fonts/Supplemental/Noto Sans CJK SC.ttc",
"windows_path": "C:/Windows/Fonts/NotoSansCJK-Regular.ttc",
"linux_path": "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"download_url": "https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf",
"weight": "Regular"
},
{
"name": "Source Han Sans SC",
"mac_path": "/System/Library/Fonts/Supplemental/Source Han Sans SC.ttc",
"windows_path": "C:/Windows/Fonts/SourceHanSansSC-Regular.otf",
"linux_path": "/usr/share/fonts/opentype/source-han-sans/SourceHanSansSC-Regular.otf",
"download_url": "https://github.com/adobe-fonts/source-han-sans/raw/release/OTF/SimplifiedChinese/SourceHanSansSC-Regular.otf",
"weight": "Regular"
},
{
"name": "WenQuanYi Micro Hei",
"mac_path": None,
"windows_path": None,
"linux_path": "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"download_url": "https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc",
"weight": "Regular"
}
]
def find_font_file(font_info: dict) -> str:
"""
查找字体文件跨平台
:param font_info: 字体信息字典
:return: 字体文件路径如果未找到返回 None
"""
import platform
system = platform.system()
if system == "Darwin": # macOS
font_path = font_info.get("mac_path")
elif system == "Windows":
font_path = font_info.get("windows_path")
elif system == "Linux":
font_path = font_info.get("linux_path")
else:
font_path = None
if font_path and os.path.isfile(font_path):
return font_path
return None
def check_chinese_support(font_path: str) -> bool:
"""
检查字体是否支持中文简体
:param font_path: 字体文件路径
:return: 是否支持中文
"""
if not FONTTOOLS_AVAILABLE:
print("[WARN] 未安装 fonttools跳过中文支持检查")
return True
try:
font = TTFont(font_path)
# 检查是否包含常用简体中文字符GB2312 范围)
# GB2312 一级汉字0x4E00-0x9FA5
chinese_chars = [chr(code) for code in range(0x4E00, 0x9FA5)]
# 抽样检查检查前100个常用汉字
sample_chars = chinese_chars[:100]
supported = 0
for char in sample_chars:
if char in font:
supported += 1
support_ratio = supported / len(sample_chars)
print(f"[INFO] 字体中文支持率:{support_ratio*100:.1f}%")
return support_ratio > 0.8 # 支持率 > 80% 认为支持中文
except Exception as e:
print(f"[ERROR] 检查字体中文支持失败:{e}")
return False
def install_font_to_project(font_info: dict, project_fonts_dir: str) -> str:
"""
安装字体到项目字体目录
:param font_info: 字体信息字典
:param project_fonts_dir: 项目字体目录
:return: 安装后的字体文件路径
"""
os.makedirs(project_fonts_dir, exist_ok=True)
# 查找系统字体文件
system_font_path = find_font_file(font_info)
if not system_font_path:
print(f"[ERROR] 未找到系统字体:{font_info['name']}")
if font_info.get("download_url"):
print(f"[INFO] 尝试从以下地址下载:{font_info['download_url']}")
# TODO: 实现下载逻辑
return None
else:
print(f"[ERROR] 该字体无下载地址,请手动安装")
return None
# 复制到项目字体目录
font_filename = os.path.basename(system_font_path)
project_font_path = os.path.join(project_fonts_dir, font_filename)
if not os.path.isfile(project_font_path):
try:
shutil.copy2(system_font_path, project_font_path)
print(f"[OK] 字体已复制到项目目录:{project_font_path}")
except Exception as e:
print(f"[ERROR] 复制字体失败:{e}")
return None
else:
print(f"[INFO] 字体已存在于项目目录:{project_font_path}")
return project_font_path
def generate_project_font_config(font_path: str, output_path: str, font_name: str = None):
"""
生成项目字体配置文件
:param font_path: 字体文件路径
:param output_path: 输出配置文件路径
:param font_name: 字体名称如果不指定从文件读取
"""
# 如果未指定字体名称,尝试从文件读取
if not font_name and FONTTOOLS_AVAILABLE:
try:
font = TTFont(font_path)
font_name = font.get("name").getDebugName(4) # 完整字体名称
except Exception:
font_name = os.path.splitext(os.path.basename(font_path))[0]
config = {
"_comment": "项目字幕字体配置",
"font_name": font_name or "Unknown",
"font_path": os.path.abspath(font_path),
"font_path_relative": os.path.relpath(font_path, os.path.dirname(output_path)),
"install_date": __import__("datetime").datetime.now().isoformat(),
"supports_chinese": check_chinese_support(font_path) if FONTTOOLS_AVAILABLE else None,
"ass_fontname": font_name, # ASS 格式使用的字体名称
"ffmpeg_font_path": font_path, # FFmpeg 使用的字体路径
}
with open(output_path, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
print(f"[OK] 项目字体配置已生成:{output_path}")
def list_available_fonts():
"""
列出系统中可用的中文字体
"""
print("\n[INFO] 查找系统中可用的中文字体...")
available = []
for font_info in CHINESE_FONT_CANDIDATES:
font_path = find_font_file(font_info)
if font_path:
available.append({
"name": font_info["name"],
"path": font_path,
"weight": font_info["weight"]
})
print(f"{font_info['name']} ({font_info['weight']})")
print(f" 路径:{font_path}")
else:
print(f"{font_info['name']} (未找到)")
if not available:
print("\n[WARN] 未找到任何中文字体,请手动安装中文字体")
print("推荐字体:")
print(" - macOS: PingFang SC系统自带")
print(" - Ubuntu: fonts-noto-cjkapt install fonts-noto-cjk")
print(" - Windows: 微软雅黑(系统自带)")
return available
def main():
parser = argparse.ArgumentParser(
description="Subtitle Font Manager · 字幕字体管理器",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例
python subtitle-font-manager.py --list
python subtitle-font-manager.py --check --install
python subtitle-font-manager.py --set-font "PingFang SC" --output project-font.json
python subtitle-font-manager.py --validate ./assets/fonts/PingFangSC-Regular.otf
"""
)
parser.add_argument("--list", action="store_true", help="列出可用的中文字体")
parser.add_argument("--check", action="store_true", help="检查项目字体是否已安装")
parser.add_argument("--install", action="store_true", help="如果未安装,自动安装字体到项目目录")
parser.add_argument("--set-font", help="设置项目字体(字体名称)")
parser.add_argument("--output", help="输出配置文件路径")
parser.add_argument("--validate", help="验证字体文件是否有效")
parser.add_argument("--generate-ffmpeg-config", action="store_true", help="生成 FFmpeg 字体配置")
parser.add_argument("--project-fonts-dir", default="./assets/fonts/", help="项目字体目录")
args = parser.parse_args()
if args.list:
list_available_fonts()
sys.exit(0)
if args.validate:
if not os.path.isfile(args.validate):
print(f"[ERROR] 字体文件不存在:{args.validate}")
sys.exit(1)
print(f"[INFO] 验证字体文件:{args.validate}")
supports_chinese = check_chinese_support(args.validate)
if supports_chinese:
print(f"[OK] 字体有效,支持中文")
else:
print(f"[WARN] 字体可能不支持中文,请手动检查")
sys.exit(0)
if args.check or args.install:
# 检查项目字体是否已安装
project_font_config = os.path.join(args.project_fonts_dir, "../subtitle-styles/project-font.json")
project_font_config = os.path.abspath(project_font_config)
if os.path.isfile(project_font_config):
with open(project_font_config, "r", encoding="utf-8") as f:
config = json.load(f)
print(f"[INFO] 项目字体已配置:{config['font_name']}")
print(f"[INFO] 字体路径:{config['font_path']}")
if not os.path.isfile(config["font_path"]):
print(f"[WARN] 字体文件不存在,需要重新安装")
args.install = True
else:
sys.exit(0)
if args.install:
# 安装字体
available = list_available_fonts()
if not available:
sys.exit(1)
# 使用第一个可用的字体
font_info = available[0]
print(f"\n[INFO] 安装字体:{font_info['name']}")
# 查找字体信息
font_info_full = next((f for f in CHINESE_FONT_CANDIDATES if f["name"] == font_info["name"]), None)
if font_info_full:
project_font_path = install_font_to_project(font_info_full, args.project_fonts_dir)
if project_font_path and args.output:
generate_project_font_config(project_font_path, args.output, font_info["name"])
if args.set_font:
# 设置项目字体
font_info = next((f for f in CHINESE_FONT_CANDIDATES if f["name"] == args.set_font), None)
if not font_info:
print(f"[ERROR] 未找到字体:{args.set_font}")
print(f"[INFO] 可用字体:{[f['name'] for f in CHINESE_FONT_CANDIDATES]}")
sys.exit(1)
project_font_path = install_font_to_project(font_info, args.project_fonts_dir)
if project_font_path and args.output:
generate_project_font_config(project_font_path, args.output, args.set_font)
if args.generate_ffmpeg_config:
# 生成 FFmpeg 字体配置
# FFmpeg 可以使用 fontfile 滤镜指定字体文件
output = args.output or "ffmpeg-font.conf"
# 读取项目字体配置
project_font_config = os.path.join(args.project_fonts_dir, "../subtitle-styles/project-font.json")
project_font_config = os.path.abspath(project_font_config)
if not os.path.isfile(project_font_config):
print(f"[ERROR] 项目字体未配置,请先运行 --check --install")
sys.exit(1)
with open(project_font_config, "r", encoding="utf-8") as f:
config = json.load(f)
# 生成 FFmpeg 滤镜字符串
font_path = config["font_path"]
ffmpeg_filter = f"subtitles=subtitles.ass:force_style='FontName={config['font_name']},FontFile={font_path}'"
with open(output, "w", encoding="utf-8") as f:
f.write(f"# FFmpeg 字体配置\n")
f.write(f"# 使用方法ffmpeg -i input.mp4 -vf \"{ffmpeg_filter}\" output.mp4\n\n")
f.write(f"font_path={font_path}\n")
f.write(f"font_name={config['font_name']}\n")
f.write(f"ffmpeg_filter={ffmpeg_filter}\n")
print(f"[OK] FFmpeg 字体配置已生成:{output}")
if __name__ == "__main__":
main()