铸渊 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∞ 主权
279 lines
8.8 KiB
Python
279 lines
8.8 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Script to SRT with Timing · 剧本到 SRT 时间轴生成器
|
||
===============================================
|
||
用剧本台词和配音时长生成正式 SRT,别手写测试时间轴。
|
||
|
||
功能:
|
||
1. 读取剧本台词(JSON/MD/TXT)
|
||
2. 读取配音文件(获取每条台词的实际时长)
|
||
3. 自动计算时间轴(考虑角色对话间隔)
|
||
4. 生成标准 SRT 字幕文件
|
||
5. 支持多角色、旁白、音效标注
|
||
|
||
依赖:
|
||
pip install pysrt
|
||
|
||
输入格式:
|
||
# 剧本 JSON 格式(推荐)
|
||
[
|
||
{"id": 1, "character": "苏白", "text": "付费才能修仙?", "audio_file": "./audio/subai_001.wav"},
|
||
{"id": 2, "character": "旁白", "text": "天道宗门口", "audio_file": "./audio/narrator_001.wav"}
|
||
]
|
||
|
||
# 剧本 MD 格式(兼容)
|
||
## E1-SHOT01
|
||
**苏白**:付费才能修仙?(audio: subai_001.wav)
|
||
**旁白**:天道宗门口(audio: narrator_001.wav)
|
||
|
||
输出 SRT 格式:
|
||
1
|
||
00:00:01,000 --> 00:00:03,500
|
||
付费才能修仙?
|
||
|
||
2
|
||
00:00:03,800 --> 00:00:05,200
|
||
天道宗门口
|
||
|
||
用法:
|
||
# 从剧本 JSON + 配音文件生成 SRT
|
||
python script-to-srt-with-timing.py --script script.json --output subtitles.srt
|
||
|
||
# 指定配音目录(自动匹配 audio_file)
|
||
python script-to-srt-with-timing.py --script script.json --audio-dir ./audio/ --output subtitles.srt
|
||
|
||
# 从 MD 剧本生成
|
||
python script-to-srt-with-timing.py --script script.md --output subtitles.srt
|
||
|
||
# 自定义对话间隔(秒)
|
||
python script-to-srt-with-timing.py --script script.json --gap 0.3 --output subtitles.srt
|
||
|
||
路径:
|
||
video-ai-system/engines/subtitle-pipeline/srt-tools/script-to-srt-with-timing.py
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import sys
|
||
import re
|
||
from pathlib import Path
|
||
|
||
try:
|
||
import pysrt
|
||
except ImportError:
|
||
print("[ERROR] 缺少依赖:pysrt")
|
||
print("请先安装:pip install pysrt")
|
||
sys.exit(1)
|
||
|
||
|
||
def parse_script_json(script_path: str) -> list:
|
||
"""
|
||
解析剧本 JSON 文件
|
||
|
||
:param script_path: 剧本 JSON 文件路径
|
||
:return: 台词列表 [{"id", "character", "text", "audio_file", "start", "end"}]
|
||
"""
|
||
with open(script_path, "r", encoding="utf-8") as f:
|
||
script = json.load(f)
|
||
|
||
print(f"[INFO] 读取剧本 JSON:{len(script)} 条台词")
|
||
return script
|
||
|
||
|
||
def parse_script_md(script_path: str) -> list:
|
||
"""
|
||
解析剧本 MD 文件(兼容格式)
|
||
|
||
:param script_path: 剧本 MD 文件路径
|
||
:return: 台词列表
|
||
"""
|
||
with open(script_path, "r", encoding="utf-8") as f:
|
||
lines = f.readlines()
|
||
|
||
script = []
|
||
current_id = 0
|
||
|
||
for line in lines:
|
||
# 匹配:**角色**:台词(audio: file.wav)
|
||
match = re.match(r"\*\*(.+?)\*\*[::]\s*(.+?)\s*\(audio:\s*(.+?)\)", line.strip())
|
||
if match:
|
||
character = match.group(1).strip()
|
||
text = match.group(2).strip()
|
||
audio_file = match.group(3).strip()
|
||
|
||
script.append({
|
||
"id": current_id + 1,
|
||
"character": character,
|
||
"text": text,
|
||
"audio_file": audio_file
|
||
})
|
||
current_id += 1
|
||
|
||
print(f"[INFO] 读取剧本 MD:{len(script)} 条台词")
|
||
return script
|
||
|
||
|
||
def get_audio_duration(audio_path: str) -> float:
|
||
"""
|
||
获取音频文件时长(秒)
|
||
|
||
:param audio_path: 音频文件路径
|
||
:return: 时长(秒)
|
||
"""
|
||
try:
|
||
import ffmpeg
|
||
probe = ffmpeg.probe(audio_path)
|
||
duration = float(probe['format']['duration'])
|
||
return duration
|
||
except ImportError:
|
||
# 如果没有 ffmpeg-python,使用 ffprobe 命令行
|
||
try:
|
||
import subprocess
|
||
cmd = [
|
||
"ffprobe", "-v", "quiet", "-print_format", "json",
|
||
"-show_format", "-show_streams", audio_path
|
||
]
|
||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||
probe = json.loads(result.stdout)
|
||
duration = float(probe['format']['duration'])
|
||
return duration
|
||
except Exception as e:
|
||
print(f"[WARN] 无法获取音频时长:{audio_path} ({e})")
|
||
# 估算:中文普通话约 4-6 字/秒
|
||
return 2.0 # 默认 2 秒
|
||
except Exception as e:
|
||
print(f"[WARN] 无法获取音频时长:{audio_path} ({e})")
|
||
return 2.0
|
||
|
||
|
||
def calculate_timings(script: list, audio_dir: str = None, gap: float = 0.2) -> list:
|
||
"""
|
||
计算时间轴
|
||
|
||
:param script: 台词列表
|
||
:param audio_dir: 配音文件目录(如果 audio_file 是相对路径)
|
||
:param gap: 对话间隔(秒)
|
||
:return: 带时间轴的台词列表
|
||
"""
|
||
current_time = 0.0
|
||
|
||
for item in script:
|
||
# 获取配音文件时长
|
||
audio_file = item.get("audio_file")
|
||
|
||
if audio_file and os.path.isfile(audio_file):
|
||
duration = get_audio_duration(audio_file)
|
||
elif audio_file and audio_dir:
|
||
audio_path = os.path.join(audio_dir, audio_file)
|
||
if os.path.isfile(audio_path):
|
||
duration = get_audio_duration(audio_path)
|
||
else:
|
||
# 估算时长
|
||
text = item.get("text", "")
|
||
duration = len(text) / 5.0 # 假设 5 字/秒
|
||
else:
|
||
# 没有配音文件,估算时长
|
||
text = item.get("text", "")
|
||
duration = len(text) / 5.0 # 假设 5 字/秒
|
||
|
||
# 设置开始和结束时间
|
||
item["start"] = current_time
|
||
item["end"] = current_time + duration
|
||
|
||
# 更新当前时间(加上间隔)
|
||
current_time = item["end"] + gap
|
||
|
||
print(f"[INFO] 时间轴计算完成:总时长 {current_time:.2f} 秒")
|
||
return script
|
||
|
||
|
||
def generate_srt(script: list, output_path: str):
|
||
"""
|
||
生成 SRT 字幕文件
|
||
|
||
:param script: 带时间轴的台词列表
|
||
:param output_path: 输出 SRT 文件路径
|
||
"""
|
||
# 创建 pysrt SubRipFile
|
||
subs = pysrt.SubRipFile()
|
||
|
||
for item in script:
|
||
# 创建字幕项
|
||
sub = pysrt.SubRipItem()
|
||
sub.index = item["id"]
|
||
sub.text = item["text"]
|
||
|
||
# 设置时间
|
||
start_seconds = item["start"]
|
||
end_seconds = item["end"]
|
||
|
||
sub.start.hours = int(start_seconds // 3600)
|
||
sub.start.minutes = int((start_seconds % 3600) // 60)
|
||
sub.start.seconds = int(start_seconds % 60)
|
||
sub.start.milliseconds = int((start_seconds % 1) * 1000)
|
||
|
||
sub.end.hours = int(end_seconds // 3600)
|
||
sub.end.minutes = int((end_seconds % 3600) // 60)
|
||
sub.end.seconds = int(end_seconds % 60)
|
||
sub.end.milliseconds = int((end_seconds % 1) * 1000)
|
||
|
||
subs.append(sub)
|
||
|
||
# 保存 SRT 文件
|
||
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
||
subs.save(output_path, encoding="utf-8")
|
||
|
||
print(f"[OK] SRT 字幕文件已生成:{output_path}")
|
||
print(f"[INFO] 共 {len(script)} 条字幕")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="Script to SRT with Timing · 剧本到 SRT 时间轴生成器",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
示例:
|
||
python script-to-srt-with-timing.py --script script.json --output subtitles.srt
|
||
python script-to-srt-with-timing.py --script script.md --audio-dir ./audio/ --output subtitles.srt
|
||
python script-to-srt-with-timing.py --script script.json --gap 0.3 --output subtitles.srt
|
||
"""
|
||
)
|
||
parser.add_argument("--script", required=True, help="剧本文件路径(JSON 或 MD 格式)")
|
||
parser.add_argument("--audio-dir", help="配音文件目录(如果 audio_file 是相对路径)")
|
||
parser.add_argument("--output", required=True, help="输出 SRT 文件路径")
|
||
parser.add_argument("--gap", type=float, default=0.2, help="对话间隔(秒,默认 0.2)")
|
||
|
||
args = parser.parse_args()
|
||
|
||
# 检查剧本文件是否存在
|
||
if not os.path.isfile(args.script):
|
||
print(f"[ERROR] 剧本文件不存在:{args.script}")
|
||
sys.exit(1)
|
||
|
||
# 解析剧本
|
||
if args.script.lower().endswith(".json"):
|
||
script = parse_script_json(args.script)
|
||
elif args.script.lower().endswith(".md"):
|
||
script = parse_script_md(args.script)
|
||
else:
|
||
print(f"[ERROR] 不支持的剧本格式:{args.script}")
|
||
print("支持格式:.json, .md")
|
||
sys.exit(1)
|
||
|
||
if not script:
|
||
print("[ERROR] 剧本为空,请检查格式")
|
||
sys.exit(1)
|
||
|
||
# 计算时间轴
|
||
script = calculate_timings(script, args.audio_dir, args.gap)
|
||
|
||
# 生成 SRT 文件
|
||
generate_srt(script, args.output)
|
||
|
||
print("\n[OK] 处理完成")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|