fix: complete subtitle overlay pipeline
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled

This commit is contained in:
冰朔 2026-06-24 11:58:08 +08:00
parent 0be2b552ea
commit 7fda977e9d
7 changed files with 444 additions and 44 deletions

View File

@ -126,6 +126,9 @@ JZAO外置盘: 产物存放地,不是状态主控。
| 固定道具合成路线 | ✅ 已建立 | `video-ai-system/plans/script-to-screen/EP01-LOCKED-PROP-COMPOSITING-ROUTE.hdlp` |
| 苏白+牌匾合成小样 | 🟡 技术通过·位置失败 | `video-ai-system/outputs/tests/TEST-COMP-001-SUBAI-PLAQUE-OVERLAY-RUN-001-REPORT.hdlp` |
| 牌匾固定贴图 | ✅ PNG已生成 | `video-ai-system/assets/props/PROP-TDZ-PLAQUE/texture/tdz-plaque-texture.png` |
| 字幕渲染合成 | ✅ RUN-001通过 | `video-ai-system/outputs/tests/TEST-SUBTITLE-001-RUN-001-REPORT.hdlp` |
| 苏白TTS配音 | ✅ 工程测试通过 | `video-ai-system/outputs/tests/TEST-TTS-001-SUBAI-RUN-001-REPORT.hdlp` |
| 模型/成本路线 | ✅ 已建立 | `video-ai-system/knowledge/D144-MODEL-COST-ROUTE.hdlp` |
| 广告牌资产 | ✅ 草案 | `video-ai-system/assets/props/PROP-FREE-AD-BOARD/manifest.hdlp` |
| 广告牌文字贴图 | ✅ PNG已生成 | `video-ai-system/assets/props/PROP-FREE-AD-BOARD/texture/free-ad-board-texture.png` |
| 百宗会场景资产 | ✅ 草案 | `video-ai-system/assets/envs/ENV-002-Baizonghui/manifest.hdlp` |

View File

@ -38,6 +38,7 @@ Subtitle Renderer · 字幕渲染引擎
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
@ -71,6 +72,27 @@ DEFAULT_STYLE = {
"video_height": 1920,
}
CHINESE_FONT_CANDIDATES = [
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/Hiragino Sans GB.ttc",
"/System/Library/Fonts/STHeiti Medium.ttc",
"/System/Library/Fonts/Supplemental/Songti.ttc",
"/System/Library/Fonts/Supplemental/Kaiti.ttc",
"/Library/Fonts/Arial Unicode.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]
def load_font(font_size: int):
for font_path in CHINESE_FONT_CANDIDATES:
if not os.path.isfile(font_path):
continue
try:
return ImageFont.truetype(font_path, font_size)
except Exception:
continue
return ImageFont.load_default()
def render_subtitle_png(
text: str,
@ -99,13 +121,7 @@ def render_subtitle_png(
# 字体(使用系统字体)
font_size = style.get("font_size", 36)
try:
font = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", font_size)
except:
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", font_size)
except:
font = ImageFont.load_default()
font = load_font(font_size)
# 计算文本尺寸
bbox = draw.textbbox((0, 0), text, font=font)
@ -215,53 +231,63 @@ def burn_subtitles_to_video(
print("[ERROR] 没有字幕数据")
return False
# 生成 FFmpeg 复杂滤镜表达式
# 思路:为每个字幕 PNG 创建带有时序的 overlay
# 简化版:使用 subtitles 滤镜(需要 FFmpeg 编译时启用)
#
# 由于本机 FFmpeg 无 subtitles 滤镜,改用 drawtext 方案
# 如果 drawtext 也不可用,则生成带透明度的 PNG 序列,用 overlay 滤镜
if not os.path.isfile(video_path):
print(f"[ERROR] 视频文件不存在:{video_path}")
return False
print("[INFO] 生成字幕叠加滤镜脚本...")
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
# 简化方案:生成所有字幕 PNG 后,用 FFmpeg 的 overlay 滤镜
# 为每个时间段启用对应的 PNG
# 这需要用 FFmpeg 的 `enable` 表达式
ordered_subs = [subtitles[k] for k in sorted(subtitles.keys())]
cmd = ["ffmpeg", "-y", "-i", video_path]
for sub in ordered_subs:
cmd.extend(["-i", sub["png"]])
filter_complex = []
input_count = 1 # 第0个输入是视频
margin_bottom = DEFAULT_STYLE["margin_bottom"]
chains = []
previous = "[0:v]"
for idx, sub in subtitles.items():
png_path = sub["png"]
for i, sub in enumerate(ordered_subs, start=1):
start_sec = sub["start"] / 1000.0
end_sec = sub["end"] / 1000.0
scaled = f"[s{i}]"
out = f"[v{i}]"
chains.append(f"[{i}:v]scale={video_width}:-1{scaled}")
chains.append(
f"{previous}{scaled}"
f"overlay=x=0:y=H-h-{margin_bottom}:"
f"enable='between(t,{start_sec:.3f},{end_sec:.3f})'{out}"
)
previous = out
# 添加 PNG 输入
filter_complex.append(f"[1:v]scale={video_width}:-1[png{idx}]")
# 简化:直接用一条命令处理所有字幕(实际应该用动态叠加)
filter_complex = ";".join(chains)
cmd.extend([
"-filter_complex", filter_complex,
"-map", previous,
"-map", "0:a?",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-c:a", "copy",
"-shortest",
output_path,
])
input_count += 1
print("[INFO] 开始合成字幕到视频...")
try:
result = subprocess.run(cmd, check=False, capture_output=True, text=True)
except FileNotFoundError:
print("[ERROR] 未找到 ffmpeg")
return False
# 实际实现:用一条简单的 FFmpeg 命令测试
# 这里先输出一个简化版:把第一条字幕叠加上去
first_png = list(subtitles.values())[0]["png"]
if result.returncode != 0:
print("[ERROR] FFmpeg 合成失败")
print(result.stderr[-2000:])
return False
cmd = (
f'ffmpeg -i "{video_path}" -i "{first_png}" '
f'-filter_complex "[0:v][1:v] overlay=0:(H-h) [out]" '
f'-map "[out]" -c:a copy "{output_path}" -y'
)
print(f"[INFO] FFmpeg 命令:{cmd}")
# 实际应该用更准确的方案,这里先输出架构
print(f"\n[INFO] 字幕合成需要更复杂的 FFmpeg 滤镜表达式。")
print(f"[INFO] 建议方案:")
print(f" 1. 使用 FFmpeg 的 `drawtext` 滤镜(需要重新编译 FFmpeg")
print(f" 2. 或使用专业字幕工具(如 Aegisub生成 ASS再用 FFmpeg 烧录")
print(f"\n[INFO] 当前已生成字幕 PNG 序列在:{os.path.dirname(first_png)}")
print(f"[INFO] 可手动用 FFmpeg 或视频编辑软件合成。")
if os.path.isfile(output_path) and os.path.getsize(output_path) > 0:
print(f"[OK] 字幕已合成到视频:{output_path}")
return True
print(f"[ERROR] 输出视频未生成:{output_path}")
return False
@ -308,7 +334,9 @@ def main():
# 如果指定了视频,则合成
if args.video and args.output:
burn_subtitles_to_video(args.video, subtitles, args.output, args.video_width, args.video_height)
ok = burn_subtitles_to_video(args.video, subtitles, args.output, args.video_width, args.video_height)
if not ok:
sys.exit(1)
print(f"\n[OK] 字幕 PNG 序列已生成:{output_dir}")
print(f"[INFO] 共 {len(subtitles)} 条字幕")

View File

@ -0,0 +1,240 @@
# D144 · 视频AI模型与成本路线
> HLDP://video-ai-system/knowledge/D144-MODEL-COST-ROUTE
> 类型: 模型选型 · 成本路线 · 需持续更新
> 日期: 2026-06-24
> 铸渊 ICE-GL-ZY001
---
## 当前事实
```
用户观看TEST-LINE/TEST-PROP系列后反馈:
“这是少有的符合标准的质量了,还蛮有感觉的,现在就是差字幕渲染了。”
```
因此:
```
画面主线暂不换掉火山/Seedance。
```
---
## 当前出图/视频来源
已实测:
```
图片:
- 之前有火山Seedream生成/候选图。
- JZAO anchors里也有可用参考图。
视频:
- 当前高质量测试样本来自火山方舟 Seedance 2.0。
- TEST-LINE-001: 苏白说台词画面基础部分通过。
- TEST-PROP-001 RUN-001: 天道宗牌匾特写通过。
- TEST-PROP-001 RUN-002: 苏白+牌匾复合参考图同场失败。
```
结论:
```
火山/Seedance负责高质量短镜头生成。
文字道具不交给模型自由生成,改为固定贴图/合成层。
```
---
## 成本分层
### A层 · 正片候选镜头
用途:
```
角色近景
关键表演
牌匾特写
需要商用质感的镜头
```
模型:
```
火山 Seedance 2.0 Pro/当前JIMENG_MODEL
```
策略:
```
只在资产/字幕/音频/构图预检通过后生成。
每个技术路线最多2次不连续抽卡。
```
### B层 · 低成本测试镜头
用途:
```
构图试验
节奏试验
远景/过场
无角色口型镜头
```
候选:
```
Seedance 2.0 Mini
```
官方价格页/搜索索引显示:
```
720P: 0.5元/秒
1080P: 1元/秒
```
注意:
```
正式调用前必须在控制台/模型列表验证API是否开放。
低价不等于可替代正片主镜头。
```
来源:
```
https://www.volcengine.com/docs/82379/1544106
```
### C层 · 开源/本地后期
用途:
```
字幕
混音
简单音效
文字道具贴图
基础配音测试
```
工具:
```
subtitle-renderer.py
tts-engine.py / Edge-TTS
FFmpeg
svg-to-png.py
```
策略:
```
尽量本地完成,不花模型钱。
```
---
## 声音路线
### 角色对白
当前:
```
Edge-TTS可生成苏白测试对白。
免费、速度快、适合节奏/字幕/混音联调。
```
风险:
```
Edge-TTS声线未必达到最终商用角色声线。
不能解决口型,只解决音频。
```
升级候选:
```
豆包语音TTS / 声音复刻 / 其他商用TTS
```
豆包语音官方计费页显示:
```
豆包语音合成模型2.0: 约3元/万字符。
```
来源:
```
https://www.volcengine.com/docs/6561/1359370
https://www.volcengine.com/docs/6561/2528925
```
### 口型
当前:
```
Seedance可生成“看起来在开口”的画面基础。
但未由独立对白音频驱动,不算真正口型解决。
```
路线:
```
1. Edge-TTS/豆包TTS生成角色对白。
2. 视频改口型或本地Wav2Lip驱动画面嘴型。
3. 与字幕时间轴对齐。
```
来源:
```
https://www.volcengine.com/docs/86081/1804521
https://www.volcengine.com/docs/85128/1462623
```
---
## 配乐路线
当前:
```
BGM生成器尚未开发。
```
短期:
```
先用免版权/自建素材库 + FFmpeg混音。
第一集测试阶段不优先花钱买音乐API。
```
中期:
```
开发 bgm-generator.py:
- 开源MusicGen可做探索。
- 若本机无GPU或质量不够再评估商用音乐API。
```
---
## 当前推荐
```
画面: 火山Seedance继续主线Mini只做低成本测试。
出图: 继续保留Seedream/已有JZAO参考图最终角色资产要补多角度。
配音: Edge-TTS先跑通工程闭环商用声线再评估豆包语音。
口型: 不能靠旁白,必须接音频驱动口型路线。
字幕: 本地PNG+FFmpeg合成已跑通。
配乐: 先自建/免版权/本地生成,后续看质量再接商用模型。
```

View File

@ -112,6 +112,9 @@ preview-003: 暂不生成。
| 固定道具合成路线 | ✅ | `plans/script-to-screen/EP01-LOCKED-PROP-COMPOSITING-ROUTE.hdlp` |
| 苏白+牌匾合成小样 | 🟡 技术通过·位置失败 | `outputs/tests/TEST-COMP-001-SUBAI-PLAQUE-OVERLAY-RUN-001-REPORT.hdlp` |
| 牌匾固定贴图 | ✅ PNG已生成 | `assets/props/PROP-TDZ-PLAQUE/texture/tdz-plaque-texture.png` |
| 字幕渲染合成 | ✅ RUN-001通过 | `outputs/tests/TEST-SUBTITLE-001-RUN-001-REPORT.hdlp` |
| 苏白TTS配音 | ✅ 工程测试通过 | `outputs/tests/TEST-TTS-001-SUBAI-RUN-001-REPORT.hdlp` |
| 模型/成本路线 | ✅ 已建立 | `knowledge/D144-MODEL-COST-ROUTE.hdlp` |
| 广告牌资产 | ✅ 草案 | `assets/props/PROP-FREE-AD-BOARD/manifest.hdlp` |
| 广告牌文字贴图 | ✅ PNG已生成 | `assets/props/PROP-FREE-AD-BOARD/texture/free-ad-board-texture.png` |
| 百宗会场景资产 | ✅ 草案 | `assets/envs/ENV-002-Baizonghui/manifest.hdlp` |
@ -202,6 +205,9 @@ preview-003: 暂不生成。
- [x] 建立工程合成/固定贴图路线解决E1-SHOT03牌匾同场 → `plans/script-to-screen/EP01-LOCKED-PROP-COMPOSITING-ROUTE.hdlp`
- [x] 生成 PROP-TDZ-PLAQUE 固定文字贴图PNG → `assets/props/PROP-TDZ-PLAQUE/texture/tdz-plaque-texture.png`
- [x] 执行 TEST-COMP-001 RUN-001: 苏白底片+固定牌匾合成 → 技术通过但位置失败
- [x] 修复字幕管线: SRT→PNG→FFmpeg overlay合成 → `outputs/tests/TEST-SUBTITLE-001-RUN-001-REPORT.hdlp`
- [x] 测试苏白Edge-TTS工程配音 → `outputs/tests/TEST-TTS-001-SUBAI-RUN-001-REPORT.hdlp`
- [x] 建立模型/成本路线 → `knowledge/D144-MODEL-COST-ROUTE.hdlp`
- [ ] 生成E1-SHOT03专用苏白底片: 上方/身后预留牌匾空间
- [ ] 执行 TEST-PROP-001 RUN-003: 广告牌PNG文字贴图上镜追踪测试
- [ ] 生成或确认 CHAR-003-SuBai 批准参考图

View File

@ -0,0 +1,71 @@
# TEST-SUBTITLE-001 · 字幕渲染 RUN-001报告
> HLDP://video-ai-system/outputs/tests/TEST-SUBTITLE-001-RUN-001-REPORT
> 类型: 字幕PNG渲染 + FFmpeg合成测试
> 日期: D144 · 2026-06-24 11:54 CST
> 铸渊 ICE-GL-ZY001
---
## 输入
```
srt: /tmp/guanghulab-d144-test/subai.srt
base_video: /Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/视频/zai-fu-fei-xiu-xian/ep01/tests/TEST-COMP-001-SUBAI-PLAQUE-OVERLAY-RUN-001.mp4
output: /Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/视频/zai-fu-fei-xiu-xian/ep01/tests/TEST-SUBTITLE-001-SUBAI-RUN-001.mp4
```
实际输出:
```
/Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/视频/zai-fu-fei-xiu-xian/ep01/tests/TEST-SUBTITLE-001-SUBAI-RUN-001.mp4
```
---
## 修复内容
```
subtitle-renderer.py:
- 原 burn_subtitles_to_video 只是占位,未真正合成多条字幕。
- D144修复为 PNG输入 + overlay enable 分时叠加。
- 修复中文字体加载,优先使用 macOS 中文字体。
```
---
## 测试结果
```
PNG渲染: PASS
FFmpeg合成: PASS
中文字体: PASS
字幕可读: PASS
```
观察:
```
字幕位置位于底部偏上,适合竖屏短剧。
黑色半透明底可读性够。
当前样式可作为测试默认,不是最终商用字幕包装。
```
---
## 裁决
```
status: PASS_FOR_PIPELINE_TEST
approved_for: 后续EP01测试样片字幕烧录
not_approved_for: 最终商用字幕样式
```
下一步:
```
1. 把EP01每句原文台词转成正式SRT。
2. 对角色对白音频做时长校准。
3. 字幕按音频时间轴烧录。
4. 建立商用字幕样式: 字体、描边、底色、位置、安全区。
```

View File

@ -0,0 +1,48 @@
# TEST-TTS-001 · 苏白配音 RUN-001报告
> HLDP://video-ai-system/outputs/tests/TEST-TTS-001-SUBAI-RUN-001-REPORT
> 类型: Edge-TTS角色对白测试
> 日期: D144 · 2026-06-24 11:53 CST
> 铸渊 ICE-GL-ZY001
---
## 输入
```
text: 未来的天下第一宗!天道宗开门收徒啦!
character: 苏白
voice: zh-CN-XiaoxiaoNeural
rate: +5%
pitch: +0Hz
output: /tmp/guanghulab-d144-test/subai-line.mp3
```
---
## 探针
```
codec: mp3
sample_rate: 24000Hz
channels: 1
duration: 4.248s
```
---
## 裁决
```
status: PASS_FOR_PIPELINE_TEST
approved_for: 字幕/混音/对白节奏联调
not_approved_for: 最终商用声线
```
结论:
```
Edge-TTS可作为免费工程配音线先跑通角色对白、字幕、混音闭环。
最终商用声线仍需与豆包语音/声音复刻做A/B测试。
```

View File

@ -0,0 +1,4 @@
edge-tts>=7.2.8
pysrt>=1.1.2
cairosvg>=2.9.0
Pillow>=12.2.0