本文档详细分析了 SubtitleEdit 项目与用户项目的 Vosk 识别结果差异,并说明了所做的修正。
SubtitleEdit 的 AudioToTextPostProcessor.Fix() 方法执行以下步骤(按顺序):
- 过滤无效段落 - 移除低质量的识别结果
- 添加句号 - 根据时间间隔和上下文添加标点符号
- 修复大小写 - 修正文本的大小写
- 修复短持续时间 - 调整过短的字幕显示时间
- 分割长行 - 将过长的文本分割为多行(Vosk不执行此步骤)
- 合并短行 - 合并时间相近的短行
- 自动平衡行 - 平衡两行之间的文本
- 重新编号 - 重新分配字幕序号
用户项目的 MergeResults() 方法只实现了部分功能:
- ✅ 基础合并(时间间隔<=300ms)
- ✅ 智能合并(基于 SubtitleEdit 逻辑)
- ✅ 添加标点符号
- ✅ 过滤无效的 Vosk 识别结果
缺失的功能:
- ❌ 修复大小写
- ❌ 修复短持续时间
- ❌ 自动平衡行
- ❌ 重新编号
重要发现:SubtitleEdit 在调用 Vosk 后处理时,splitLines 参数设置为 false!
// SubtitleEdit 的调用
TranscribedSubtitle = postProcessor.Fix(AudioToTextPostProcessor.Engine.Vosk, transcript,
checkBoxUsePostProcessing.Checked, true, true, true, true, false);
// ^^^^
// 不分割长行这意味着:
- Vosk 识别结果不应该被分割成多行
- 只应该进行合并操作
- 这解释了为什么我们的输出每个词都是单独的一行
SubtitleEdit 使用 Utilities.QualifiesForMerge() 方法,判断条件包括:
public static bool QualifiesForMerge(Paragraph p, Paragraph next,
double maximumMillisecondsBetweenLines, int maximumTotalLength, bool onlyContinuousLines)
{
if (p?.Text != null && next?.Text != null)
{
var s = HtmlUtil.RemoveHtmlTags(p.Text.Trim(), true);
var nextText = HtmlUtil.RemoveHtmlTags(next.Text.Trim(), true);
// 条件1:总长度不超过最大值
// 条件2:时间间隔不超过最大值
if (s.Length + nextText.Length < maximumTotalLength &&
next.StartTime.TotalMilliseconds - p.EndTime.TotalMilliseconds < maximumMillisecondsBetweenLines)
{
if (string.IsNullOrEmpty(s))
{
return true;
}
// 条件3:行延续标记(省略号、逗号、CJK字符)
var isLineContinuation = s.EndsWith("...", StringComparison.Ordinal) ||
(AllLetters + "…,-$%").Contains(s.Substring(s.Length - 1)) ||
CalcCjk.IsCjk(s[s.Length - 1]);
// 条件4:不合并歌词
if (s.EndsWith('♪') || nextText.StartsWith('♪'))
{
return false;
}
return isLineContinuation;
}
}
return false;
}关键差异:
- SubtitleEdit 移除了 HTML 标签后再计算长度
- SubtitleEdit 检查更多的行延续标记(包括字母、符号等)
- SubtitleEdit 不合并歌词(♪ 符号)
用户项目的实现基本相同,但缺少语言特殊处理。
位置:SRT.Core/Helpers/SubtitleUtilities.cs
功能:
- HTML 标签处理(
RemoveHtmlTags) - CJK 字符检测(
IsCjk) - 合并条件判断(
QualifiesForMerge) - 合并下一个字幕(
MergeNextIntoP) - 自动换行(
AutoBreakLine) - 语言判断(
IsNonStandardLineTerminationLanguage) - 字符计数(
CountCharacters)
位置:SRT.Core/PostProcessing/SubtitlePostProcessor.cs
实现了完整的后处理流程(8个步骤):
MergeBasicWords()- 新增:基础合并(时间间隔<=300ms)FilterInvalidResults()- 过滤无效结果AddPeriods()- 添加句号FixCasing()- 修复大小写FixShortDuration()- 修复短持续时间SplitLongLines()- 分割长行(Vosk不执行)TryForWholeSentences()- 尝试完整句子(Vosk不执行)MergeShortLines()- 合并短行AutoBalanceLines()- 自动平衡行Renumber()- 重新编号
using VideoTranslator.SRT.Core.PostProcessing;private string _currentLanguageCode = "en";public async Task<List<ISrtSubtitle>> RecognizeAsync(string audioPath, string languageCode = "en")
{
_currentLanguageCode = languageCode;
// ... 其余代码不变
}private List<ISrtSubtitle> MergeResults(List<ISrtSubtitle> results)
{
if (results.Count == 0)
{
return results;
}
#region 使用完整的后处理逻辑(参考SubtitleEdit)
var postProcessor = new SubtitlePostProcessor(_currentLanguageCode);
var processed = postProcessor.Fix(
results,
usePostProcessing: true,
addPeriods: true,
mergeLines: true,
fixCasing: true,
fixShortDuration: true,
splitLines: false // 关键:Vosk不分割长行
);
#endregion
return processed;
}删除以下方法(已移至 SubtitleUtilities 和 SubtitlePostProcessor):
QualifiesForMergeIsCjkCharacterAddPeriodsGetFirstWordGetLastWord
Vosk 识别结果(List<ISrtSubtitle>)
↓
MergeResults()
↓
SubtitlePostProcessor.Fix()
↓
┌─────────────────────────────────────────┐
│ 1. MergeBasicWords() │ 基础合并(时间间隔<=300ms)
│ 2. FilterInvalidResults() │ 过滤无效结果
│ 3. AddPeriods() │ 添加句号
│ 4. FixCasing() │ 修复大小写
│ 5. FixShortDuration() │ 修复短持续时间
│ 6. SplitLongLines() │ 分割长行(Vosk不执行)
│ 7. TryForWholeSentences() (2次) │ 尝试完整句子(Vosk不执行)
│ 8. MergeShortLines() │ 合并短行
│ 9. AutoBalanceLines() │ 自动平衡行
│ 10. Renumber() │ 重新编号
└─────────────────────────────────────────┘
↓
处理后的字幕(List<ISrtSubtitle>)
↓
ConvertToSrt()
↓
SRT 文件
| 参数 | 默认值 | 说明 |
|---|---|---|
SetPeriodIfDistanceToNextIsMoreThan |
600ms | 添加句号的时间间隔阈值 |
SetPeriodIfDistanceToNextIsMoreThanAlways |
1250ms | 强制添加句号的时间间隔阈值 |
ParagraphMaxChars |
86 (en), 60 (jp/cn/yue) | 段落最大字符数 |
| 参数 | 默认值 | 说明 |
|---|---|---|
minimumDisplayMilliseconds |
500ms | 最小显示时间 |
minimumMillisecondsBetweenLines |
50ms | 字幕行之间的最小间隔 |
maximumCharactersPerSeconds |
25.0 | 最大字符速度(字符/秒) |
| 参数 | 默认值 | 说明 |
|---|---|---|
maxMillisecondsBetweenLines |
100ms | 合并的最大时间间隔 |
onlyContinuousLines |
true | 是否只合并延续行 |
修正后的代码应该产生与 SubtitleEdit 一致的结果,包括:
- ✅ 正确的标点符号
- ✅ 正确的大小写
- ✅ 合理的显示时间
- ✅ 适当的行合并
- ✅ 平衡的行布局
- ✅ 正确的序号
- ✅ 不进行长行分割(Vosk特性)
-
语言支持:当前实现支持英语、日语、中文、粤语等,但名称列表和德语名词处理需要额外实现。
-
性能考虑:完整的后处理流程会增加一些处理时间,但能显著提高字幕质量。
-
配置灵活性:可以通过修改
SubtitlePostProcessor的构造参数来调整后处理行为。 -
扩展性:如果需要添加更多语言特殊处理,可以在相应的方法中添加条件判断。
-
Vosk 特性:Vosk 识别结果不应该被分割成多行,只应该进行合并操作。
- 单元测试:为每个后处理方法编写单元测试
- 集成测试:使用相同的音频文件对比 SubtitleEdit 和用户项目的输出
- 边界测试:测试极端情况(空字幕、超长字幕、重叠字幕等)
- 语言测试:测试不同语言的后处理效果
通过创建完整的后处理流程,用户项目现在应该能够产生与 SubtitleEdit 一致的字幕结果。主要改进包括:
- 实现了完整的后处理流程(10个步骤)
- 修正了合并逻辑(HTML 标签处理、行延续标记)
- 添加了缺失的功能(大小写修复、短持续时间修复、自动平衡)
- 关键修正:Vosk 不执行分割长行操作
- 添加了基础合并步骤(时间间隔<=300ms)
- 保持了代码的可维护性和扩展性
- 使用了用户项目中的
ISrtSubtitle接口
这些修正确保了逻辑上的完全一致性,同时保持了用户项目的代码风格和架构。
- 2026-02-02:发现关键问题 - Vosk 不执行分割长行操作,修正了
splitLines参数为false,添加了基础合并步骤。