393 lines
14 KiB
Python
393 lines
14 KiB
Python
|
|
#!/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-cjk(apt 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()
|