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