cang-ying/engines/subtitle-pipeline/font-manager/subtitle-font-manager.py
Guanghu Domestic Migration a2fa7d57d8 chore: import sanitized domestic snapshot for REPO-005
Source snapshot: 23037fa3a2e9b0597162735755e92fc763bf4d94
2026-07-17 15:55:48 +08:00

393 lines
14 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()