From 7fda977e9d1dc3294b2bde6e081b65fa9baaa70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Wed, 24 Jun 2026 11:58:08 +0800 Subject: [PATCH] fix: complete subtitle overlay pipeline --- video-ai-system/CURRENT.hdlp | 3 + video-ai-system/engines/subtitle-renderer.py | 116 +++++---- .../knowledge/D144-MODEL-COST-ROUTE.hdlp | 240 ++++++++++++++++++ .../memory/zai-fu-fei-xiu-xian/STATUS.hdlp | 6 + .../TEST-SUBTITLE-001-RUN-001-REPORT.hdlp | 71 ++++++ .../TEST-TTS-001-SUBAI-RUN-001-REPORT.hdlp | 48 ++++ video-ai-system/requirements-video-ai.txt | 4 + 7 files changed, 444 insertions(+), 44 deletions(-) create mode 100644 video-ai-system/knowledge/D144-MODEL-COST-ROUTE.hdlp create mode 100644 video-ai-system/outputs/tests/TEST-SUBTITLE-001-RUN-001-REPORT.hdlp create mode 100644 video-ai-system/outputs/tests/TEST-TTS-001-SUBAI-RUN-001-REPORT.hdlp create mode 100644 video-ai-system/requirements-video-ai.txt diff --git a/video-ai-system/CURRENT.hdlp b/video-ai-system/CURRENT.hdlp index 5d21905..655e045 100644 --- a/video-ai-system/CURRENT.hdlp +++ b/video-ai-system/CURRENT.hdlp @@ -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` | diff --git a/video-ai-system/engines/subtitle-renderer.py b/video-ai-system/engines/subtitle-renderer.py index 165cfc7..8e4288b 100644 --- a/video-ai-system/engines/subtitle-renderer.py +++ b/video-ai-system/engines/subtitle-renderer.py @@ -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)} 条字幕") diff --git a/video-ai-system/knowledge/D144-MODEL-COST-ROUTE.hdlp b/video-ai-system/knowledge/D144-MODEL-COST-ROUTE.hdlp new file mode 100644 index 0000000..4dc8f2a --- /dev/null +++ b/video-ai-system/knowledge/D144-MODEL-COST-ROUTE.hdlp @@ -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合成,已跑通。 +配乐: 先自建/免版权/本地生成,后续看质量再接商用模型。 +``` + diff --git a/video-ai-system/memory/zai-fu-fei-xiu-xian/STATUS.hdlp b/video-ai-system/memory/zai-fu-fei-xiu-xian/STATUS.hdlp index c42b41b..65f5d45 100644 --- a/video-ai-system/memory/zai-fu-fei-xiu-xian/STATUS.hdlp +++ b/video-ai-system/memory/zai-fu-fei-xiu-xian/STATUS.hdlp @@ -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 批准参考图 diff --git a/video-ai-system/outputs/tests/TEST-SUBTITLE-001-RUN-001-REPORT.hdlp b/video-ai-system/outputs/tests/TEST-SUBTITLE-001-RUN-001-REPORT.hdlp new file mode 100644 index 0000000..bd890e8 --- /dev/null +++ b/video-ai-system/outputs/tests/TEST-SUBTITLE-001-RUN-001-REPORT.hdlp @@ -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. 建立商用字幕样式: 字体、描边、底色、位置、安全区。 +``` diff --git a/video-ai-system/outputs/tests/TEST-TTS-001-SUBAI-RUN-001-REPORT.hdlp b/video-ai-system/outputs/tests/TEST-TTS-001-SUBAI-RUN-001-REPORT.hdlp new file mode 100644 index 0000000..f57fdaa --- /dev/null +++ b/video-ai-system/outputs/tests/TEST-TTS-001-SUBAI-RUN-001-REPORT.hdlp @@ -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测试。 +``` + diff --git a/video-ai-system/requirements-video-ai.txt b/video-ai-system/requirements-video-ai.txt new file mode 100644 index 0000000..8f3ab0b --- /dev/null +++ b/video-ai-system/requirements-video-ai.txt @@ -0,0 +1,4 @@ +edge-tts>=7.2.8 +pysrt>=1.1.2 +cairosvg>=2.9.0 +Pillow>=12.2.0