通过Edge-tts生成的中文字幕如何自然断句?

2025-08-01 15:17:15 20 分享链接 开发笔记 python

重新分配字幕的关键在于将words-in-cue参数设置为一个,然后将生成的临时字幕文件与配音文案匹配,根据文案的断句标识到字幕断句处进行合并。

subtitles.py

import srt
import re
from datetime import timedelta

def clean_text(text):
    """
    统一的文本清洗规则(对原始字幕和输入文本同时生效):
    1. 移除所有非中文字符、非英文字符、非数字的字符(如《》、标点、空格等)
    2. 转为小写(忽略大小写差异)
    确保原始字幕和输入文本经过相同处理后进行匹配
    """
    # 保留中文字符(\u4e00-\u9fa5)、英文字母(a-zA-Z)、数字(0-9)
    cleaned = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', '', text)
    # 转为小写,消除大小写影响
    return cleaned.lower()

def read_srt(file_path):
    """读取SRT文件,返回按顺序排列的字幕条目列表"""
    with open(file_path, 'r', encoding='utf-8') as f:
        srt_content = f.read()
    subs = list(srt.parse(srt_content))
    return sorted(subs, key=lambda x: x.index)

def read_txt_lines(file_path):
    """读取input.txt,返回按行分割的句子列表(过滤空行)"""
    with open(file_path, 'r', encoding='utf-8') as f:
        return [line.strip() for line in f.read().split('\n') if line.strip()]

def find_matching_subs(original_subs, target_line, start_idx=0):
    """
    对称清洗匹配算法:
    - 对input.txt的目标行进行清洗(使用clean_text)
    - 对input.srt的每个条目内容也进行相同清洗(使用同一个clean_text)
    - 比较双方清洗后的内容,确保规则完全一致
    """
    # 对输入文本行进行清洗(使用统一规则)
    target_clean = clean_text(target_line)
    if not target_clean:
        return (None, None)  # 空内容不匹配
    
    current_clean = ""  # 累计清洗后的原始字幕内容(使用与目标行完全相同的规则)
    
    # 从start_idx开始遍历原始字幕
    for i in range(start_idx, len(original_subs)):
        # 对原始字幕条目内容进行清洗(使用同一个clean_text函数,确保规则一致)
        sub_clean = clean_text(original_subs[i].content.strip())
        current_clean += sub_clean  # 累加清洗后的内容
        
        # 检查累计的清洗内容是否与目标清洗内容完全匹配
        if current_clean == target_clean:
            return (start_idx, i)
        
        # 若累计内容已超过目标长度,提前终止(避免无效循环)
        if len(current_clean) > len(target_clean):
            return (None, None)
    
    # 遍历结束仍未找到匹配
    return (None, None)

def merge_subtitles(original_subs, txt_lines):
    """根据txt_lines合并original_subs中的条目,使用对称清洗匹配算法"""
    merged_subs = []
    current_sub_idx = 0  # 当前处理到的原始字幕索引
    
    for line_idx, line in enumerate(txt_lines):
        # 查找当前行对应的连续原始字幕条目
        start_idx, end_idx = find_matching_subs(original_subs, line, current_sub_idx)
        
        if start_idx is None or end_idx is None:
            # 输出清洗后的内容用于调试(双方使用相同规则处理后的结果)
            target_clean = clean_text(line)
            # 输出从当前索引开始的原始字幕清洗后的内容片段,方便对比
            sample_clean = ""
            for i in range(current_sub_idx, min(current_sub_idx + 10, len(original_subs))):
                sample_clean += clean_text(original_subs[i].content.strip())
            print(f"警告:第{line_idx+1}行文本匹配失败")
            print(f"  输入文本:{line}")
            print(f"  输入文本清洗后:{target_clean}")
            print(f"  原始字幕起始位置清洗后片段:{sample_clean}")
            print(f"  (从原始字幕第{current_sub_idx+1}条开始)\n")
            continue
        
        # 合并时间:取第一个条目的start和最后一个条目的end
        merged_start = original_subs[start_idx].start
        merged_end = original_subs[end_idx].end
        
        # 创建合并后的字幕条目(保留原始输入格式)
        merged_sub = srt.Subtitle(
            index=line_idx + 1,
            start=merged_start,
            end=merged_end,
            content=line
        )
        merged_subs.append(merged_sub)
        
        # 更新处理索引
        current_sub_idx = end_idx + 1
    
    return merged_subs

def save_srt(subtitles, file_path):
    """保存合并后的SRT文件"""
    with open(file_path, 'w', encoding='utf-8') as f:
        f.write(srt.compose(subtitles))

def main(original_srt_path, txt_path, output_srt_path):
    original_subs = read_srt(original_srt_path)
    if not original_subs:
        print("原始SRT文件为空或格式错误")
        return
    
    txt_lines = read_txt_lines(txt_path)
    if not txt_lines:
        print("input.txt为空或无有效内容")
        return
    
    merged_subs = merge_subtitles(original_subs, txt_lines)
    
    if merged_subs:
        save_srt(merged_subs, output_srt_path)
        print(f"已生成合并后的字幕文件:{output_srt_path}")
        print(f"成功合并 {len(merged_subs)} 条字幕(共处理 {len(txt_lines)} 行文本)")
    else:
        print("未生成任何合并后的字幕")


if __name__ == "__main__":
    # 示例路径(根据实际情况修改)
    main(
        "D:/222/input.srt",    # 原始SRT(每个词/短语一个条目)
        "D:/222/input.txt",    # 每行是需要合并的句子
        "D:/222/output.srt"    # 输出合并后的SRT
    )

input.txt

继续来看《小明宗门》第583章
整个天羽宗山门广场一片死寂
无数天羽宗弟子双目圆睁
根本无法相信战局竟瞬间逆转
在他们心中

input.srt

1
00:00:00,100 --> 00:00:00,450
继续

2
00:00:00,450 --> 00:00:00,900
来看

3
00:00:00,925 --> 00:00:01,675
小明宗门

4
00:00:01,675 --> 00:00:01,937
第

5
00:00:02,037 --> 00:00:02,912
583

6
00:00:02,912 --> 00:00:03,175
章

7
00:00:03,737 --> 00:00:04,125
整个

8
00:00:04,150 --> 00:00:04,687
天羽宗

9
00:00:04,712 --> 00:00:05,050
山门

10
00:00:05,062 --> 00:00:05,575
广场

11
00:00:05,675 --> 00:00:06,050
一片

12
00:00:06,062 --> 00:00:06,587
死寂

13
00:00:07,162 --> 00:00:07,562
无数

14
00:00:07,587 --> 00:00:08,125
天羽宗

15
00:00:08,137 --> 00:00:08,637
弟子

16
00:00:08,725 --> 00:00:09,075
双目

17
00:00:09,075 --> 00:00:09,300
圆

18
00:00:09,300 --> 00:00:09,575
睁

19
00:00:10,137 --> 00:00:10,512
根本

20
00:00:10,525 --> 00:00:10,862
无法

21
00:00:10,862 --> 00:00:11,387
相信

22
00:00:11,487 --> 00:00:12,025
战局

23
00:00:12,050 --> 00:00:12,225
竟

24
00:00:12,237 --> 00:00:12,650
瞬间

25
00:00:12,662 --> 00:00:13,137
逆转

26
00:00:13,712 --> 00:00:13,887
在

27
00:00:13,900 --> 00:00:14,162
他们

28
00:00:14,162 --> 00:00:14,712
心中

output.srt

1
00:00:00,100 --> 00:00:03,175
继续来看《小明宗门》第583章

2
00:00:03,737 --> 00:00:06,587
整个天羽宗山门广场一片死寂

3
00:00:07,162 --> 00:00:09,575
无数天羽宗弟子双目圆睁

4
00:00:10,137 --> 00:00:13,137
根本无法相信战局竟瞬间逆转

5
00:00:13,712 --> 00:00:14,712
在他们心中

核心优化点说明

  1. 新增文本清洗机制
    通过 clean_text 函数移除所有特殊字符(如《》、()、标点符号等)和空格,只保留中文字符、英文字母和数字,并转为小写。例如:

    • 原始字幕内容:小明宗门 → 清洗后:小明宗门
    • input.txt内容:继续来看《小明宗门》第583章 → 清洗后:继续来看小明宗门第583章

    清洗后双方核心文字完全匹配,解决了特殊字符导致的匹配失败问题。

  2. 匹配逻辑优化

    • 只比较清洗后的核心文字,忽略原始格式差异
    • 当累计的原始字幕清洗内容长度超过目标行时,提前终止循环(提高效率)
    • 增加详细的调试信息,方便定位匹配失败的原因(如打印清洗后的文本)
  3. 保留原始格式
    虽然匹配时使用清洗后的文本,但生成的字幕文件会保留 input.txt 中的原始格式(含特殊字符),兼顾匹配准确性和输出美观性。

针对你的示例场景的效果

  • 原始SRT第3行:小明宗门(清洗后:小明宗门
  • input.txt第一行:继续来看《小明宗门》第583章(清洗后:继续来看小明宗门第583章
  • 算法会正确匹配原始SRT的1-6行(继续+来看+小明宗门++583+),合并后的时间为第1行的开始到第6行的结束,完美解决特殊字符导致的匹配失败问题。

通过Edge-tts生成的中文字幕如何自然断句?