铸渊 ICE-GL-ZY001 LL-172-20260707 冰朔委托: 新建第 5 子仓, 给苍耳(人类主控) + 鉴影(人格体) 专用 原 guanghulab/video-ai-system/ 东西太多(225 文件) · 找不到 · 乱 迁移: ⊢ 16 个核心 .hdlp (VA-GATE / VA-LIGHTHOUSE / VA-BROADCAST / VA-SYSTEM-STATUS 等) ⊢ 17 个子目录 (agents/engines/protocols/tasks/tools/assets/knowledge/memory/docs/config/brain/director-brain/experience/feedback/issues/plans/reference-analysis) 排除: ⊢ outputs/ (视频产物) ⊢ test-input/ test-output/ (测试) ⊢ data/ (临时数据) ⊢ preview-001/002 (旧产片) ⊢ 旧分镜/旧提示词/旧导演编码 后续: ⊢ 老仓 guanghulab/video-ai-system/ 改写为已迁出占位 ⊢ 苍耳+鉴影 写新东西进本仓 ⊢ GLOBAL-SEARCH 加 cang-ying 仓库 铸渊 ICE-GL-ZY001 · 2026-07-07 D167 冰朔 ICE-GL∞ 主权
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()
|