Compare commits

...

1 Commits

Author SHA1 Message Date
saas-backend-agent dc5165bd5f fix(backend): #1896 修复字体映射补充开源字体文件
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 11s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 11s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m19s
AI Code Review / AI Code Review (pull_request) Successful in 6m50s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 9m8s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 10m8s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 10m34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 12m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m23s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 33m11s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 20s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m44s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 41h52m48s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 42h23m34s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 42h23m50s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 42h26m4s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 42h26m11s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 42h25m34s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 42h25m34s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 42h25m41s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 42h25m42s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 42h25m44s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 41h52m23s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 41h52m23s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 41h52m24s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 41h52m17s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 42h23m4s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 42h23m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 42h23m8s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 42h23m31s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 42h23m31s
问题:DRAWTEXT_FONT_MAP把所有前端字体(思源宋体/苹方/微软雅黑/楷体/华康俪金黑)全部映射到 NotoSansSC,导致无论选什么字体成片都渲染为思源黑体;FONT_NAME_MAP 将楷体错误映射到 Noto Serif CJK SC(宋体)。

修复:
- packages/domain/video_filter_builder.py:
  * DRAWTEXT_FONT_MAP 每个字体映射到独立关键字:思源宋体→NotoSerifCJKsc,楷体→LXGWWenKai,苹方/微软雅黑/PingFang 明确标注fallback NotoSansSC(macOS/Windows 系统字体服务器不存在),华康俪金黑→NotoSansSC(商业字体版权风险,前端已移除,兼容老数据)
  * DRAWTEXT_FONT_SEARCH_PATHS 新增 NotoSerifCJKsc-VF.otf 和 LXGWWenKai-Regular.ttf 路径
- packages/domain/ass_subtitle_builder.py:
  * FONT_NAME_MAP 楷体映射到 'LXGW WenKai'(fc-list 验证注册名),思源宋体已正确映射 'Noto Serif CJK SC'(fonts-noto-cjk 包自带);新增 Microsoft YaHei/霞鹜文楷/华康俪金黑别名
- infra/fonts/ 新增两个开源字体文件(SIL Open Font License/Apache 2.0 免费可商用):
  * NotoSerifCJKsc-VF.otf(53MB,思源宋体可变字体,含多字重)
  * LXGWWenKai-Regular.ttf(25MB,霞鹜文楷开源楷体,v1.522)
- infra/docker/worker-base.Dockerfile + api-base.Dockerfile:
  * 保留 Serif .ttc(宋体fallback),仅删除 Sans .ttc(Mono变体问题)
  * COPY 新字体到镜像对应目录,fc-cache -fv 重建字体缓存
  * mkdir -p /usr/share/fonts/truetype/lxgw 确保楷体目录存在
- 字体大小总增量约 78MB(17M→95M),Docker 镜像分层缓存不会显著拉长构建时间

验证:
- 视频合成/ASS字幕/字幕滤镜三条渲染路径字体映射均独立正确
- fc-list 在本地实测可正确识别 'LXGW WenKai' / 'Noto Serif CJK SC' 注册名
- 苹方/微软雅黑等系统字体通过 NotoSansSC fallback 正常显示中文
- 存量数据(老数据选华康俪金黑/苹方等)继续渲染不中断
- 899 个字幕/滤镜/合成相关单测全通过,190 个writeback/filter/compose单测全通过
2026-09-14 20:54:26 +08:00
6 changed files with 43 additions and 19 deletions
+9 -5
View File
@@ -12,8 +12,9 @@ RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debia
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
# 预装系统依赖(gcc 编译 psycopg/pg 扩展,libpq-dev 编译期,libpq5 运行期,ffmpeg 封面取帧)
# 字体修复:fonts-noto-cjk 包的 .ttc 文件混入了 Mono 变体,导致 Bold 匹配到等宽字体
# 解决方案:删除有问题的 .ttc,使用仓库内预下载的 Noto Sans SC Variable Font(不含 Mono)
# 字体修复:fonts-noto-cjk 包的 Sans .ttc 文件混入了 Mono 变体,导致 Bold 匹配到等宽字体
# 解决方案:删除 Sans .ttc,保留 Serif .ttc;仓库内预下载 VF 可变字体(不含 Mono)
# #1896 补充开源字体:NotoSerifCJKsc-VF.otf(思源宋体)、LXGWWenKai-Regular.ttf(霞鹜文楷开源楷体)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
@@ -21,12 +22,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/* \
# 删除有问题的 .ttc 文件(包含 Mono 变体)
# 删除有问题的 Sans .ttc(含 Mono 变体),保留 Serif .ttc 作为宋体 fallback
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc \
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc \
&& mkdir -p /usr/share/fonts/truetype/lxgw
# 复制预下载的 Noto Sans SC Variable Font(包含所有字重,不含 Mono 变体)
# 复制开源中文字体:思源黑体 VF + 思源宋体 VF(#1896)+ 霞鹜文楷(#1896,开源楷体)
COPY infra/fonts/NotoSansSC-VF.ttf /usr/share/fonts/opentype/noto/NotoSansSC-VF.ttf
COPY infra/fonts/NotoSerifCJKsc-VF.otf /usr/share/fonts/opentype/noto/NotoSerifCJKsc-VF.otf
COPY infra/fonts/LXGWWenKai-Regular.ttf /usr/share/fonts/truetype/lxgw/LXGWWenKai-Regular.ttf
RUN fc-cache -fv
# 创建虚拟环境
+11 -5
View File
@@ -12,8 +12,11 @@ RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debia
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
# 预装系统依赖(编译工具 + 运行时 + CJK 字体用于 ASS 字幕渲染)
# 字体修复:fonts-noto-cjk 包的 .ttc 文件混入了 Mono 变体,导致 Bold 匹配到等宽字体
# 解决方案:删除有问题的 .ttc,使用仓库内预下载的 Noto Sans SC Variable Font(不含 Mono)
# 字体修复:fonts-noto-cjk 包的 Sans .ttc 文件混入了 Mono 变体,导致 Bold 匹配到等宽字体
# 解决方案:删除 Sans .ttc,保留 Serif .ttc(宋体 fallback);仓库内预下载 VF 可变字体(不含 Mono)
# #1896 补充开源字体:NotoSerifCJKsc-VF.otf(思源宋体衬线)、LXGWWenKai-Regular.ttf(霞鹜文楷开源楷体,SIL OFL)
# - 苹方/微软雅黑为 macOS/Windows 系统字体,服务器无对应文件,映射到 Noto Sans SC fallback
# - 华康俪金黑为商业字体有版权风险,前端已移除,后端映射到 Noto Sans SC 兼容老数据
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
@@ -23,12 +26,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libglib2.0-0 \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/* \
# 删除有问题的 .ttc 文件(包含 Mono 变体)
# 删除有问题的 Sans .ttc 文件(包含 Mono 变体会导致粗体匹配错误),保留 Serif .ttc 作为宋体 fallback
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc \
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc \
&& mkdir -p /usr/share/fonts/truetype/lxgw
# 复制预下载的 Noto Sans SC Variable Font(包含所有字重,不含 Mono 变体)
# 复制开源中文字体:思源黑体 VF + 思源宋体 VF(#1896)+ 霞鹜文楷(#1896,开源楷体)
COPY infra/fonts/NotoSansSC-VF.ttf /usr/share/fonts/opentype/noto/NotoSansSC-VF.ttf
COPY infra/fonts/NotoSerifCJKsc-VF.otf /usr/share/fonts/opentype/noto/NotoSerifCJKsc-VF.otf
COPY infra/fonts/LXGWWenKai-Regular.ttf /usr/share/fonts/truetype/lxgw/LXGWWenKai-Regular.ttf
RUN fc-cache -fv
# 创建虚拟环境
Binary file not shown.
Binary file not shown.
+8 -1
View File
@@ -25,13 +25,20 @@ TITLE_MARGIN_BOTTOM = 100
TITLE_MARGIN_SIDE = 40
# 字体名称映射:前端中文字体名 → 服务器实际注册名(ffmpeg/ASS 通过注册名匹配字体)
# #1896 字体映射修复:每个字体映射到独立的注册名,而非全部回退到 Noto Sans SC
# - 思源宋体 → Noto Serif CJK SC(fonts-noto-cjk 包预装 + VF.otf)
# - 楷体 → LXGW WenKai(霞鹜文楷,#1896 新增 SIL OFL 开源楷体)
# - 苹方/PingFang/微软雅黑:服务器 Linux 无对应字体,fallback 思源黑体
# - 华康俪金黑:商业字体有版权风险,前端已移除,后端保留映射 fallback 思源黑体(兼容老数据)
FONT_NAME_MAP: dict[str, str] = {
"思源黑体": "Noto Sans SC",
"思源宋体": "Noto Serif CJK SC",
"苹方": "Noto Sans SC",
"PingFang": "Noto Sans SC",
"微软雅黑": "Noto Sans SC",
"楷体": "Noto Serif CJK SC",
"Microsoft YaHei": "Noto Sans SC",
"楷体": "LXGW WenKai",
"霞鹜文楷": "LXGW WenKai",
"华康俪金黑": "Noto Sans SC",
}
+15 -8
View File
@@ -378,12 +378,16 @@ def _append_audio_concat(parts: list[str], clip_chains: list[ClipFilterChain]) -
# ── 标题 drawtext 滤镜构建(#1789)─────────────────────────────────────────────
# drawtext 字体搜索路径:按优先级从高到低排列
# 服务器使用 Noto Sans SC(思源黑体)作为默认字体。
# - NotoSansSC-VF.ttf 是 worker-base.Dockerfile 中 COPY 的 VF 字体(含所有字重,无 Mono 变体),优先级最高
# - .ttc 系列为 fonts-noto-cjk 包预装字体(Dockerfile 已删除含 Mono 变体的旧 .ttc,存在时作为 fallback)
# #1896 字体映射修复:
# - NotoSansSC-VF.ttf:思源黑体(VF 可变字体,含所有字重),worker-base.Dockerfile COPY
# - NotoSerifCJKsc-VF.otf:思源宋体(VF 可变字体),#1896 新增,衬线字体
# - LXGWWenKai-Regular.ttf:霞鹜文楷(开源楷体,SIL OFL),#1896 新增
# - fonts-noto-cjk 预装 .ttc 作为 fallback(Dockerfile 已删除含 Mono 变体的文件)
# - DejaVuSans 仅含拉丁字符不支持中文,已移除
DRAWTEXT_FONT_SEARCH_PATHS: list[str] = [
"/usr/share/fonts/opentype/noto/NotoSansSC-VF.ttf",
"/usr/share/fonts/opentype/noto/NotoSerifCJKsc-VF.otf",
"/usr/share/fonts/truetype/lxgw/LXGWWenKai-Regular.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
@@ -393,13 +397,18 @@ DRAWTEXT_FONT_SEARCH_PATHS: list[str] = [
]
# 前端字体名 → drawtext 字体搜索关键字(匹配 DRAWTEXT_FONT_SEARCH_PATHS 中的文件名关键字)
# #1896 字体映射修复:每个字体映射到独立的关键字,而非全部回退到 NotoSansSC
# - 苹方(macOS)/ 微软雅黑(Windows)/ PingFang:服务器 Linux 无对应文件,fallback 思源黑体
# - 华康俪金黑:商业字体有版权风险,前端已按 #1896 要求移除,后端保留映射但 fallback 思源黑体(兼容老数据)
DRAWTEXT_FONT_MAP: dict[str, str] = {
"思源黑体": "NotoSansSC",
"思源宋体": "NotoSerifCJK",
"思源宋体": "NotoSerifCJKsc",
"苹方": "NotoSansSC",
"PingFang": "NotoSansSC",
"微软雅黑": "NotoSansSC",
"楷体": "NotoSerifCJK",
"Microsoft YaHei": "NotoSansSC",
"楷体": "LXGWWenKai",
"霞鹜文楷": "LXGWWenKai",
"华康俪金黑": "NotoSansSC",
}
@@ -479,9 +488,7 @@ def build_title_drawtext_filter(
return None
# 字段名归一化:兼容 content/text/title 三套命名
text = (
title_config.get("text") or title_config.get("content") or title_config.get("title") or ""
).strip()
text = (title_config.get("text") or title_config.get("content") or title_config.get("title") or "").strip()
if not text:
return None