feat: whisper 新增 decode_full 无VAD整段解码参数并改为整条删除式幻觉清洗
解决转写漏句(有人说话但没识别出来)问题:silero VAD 对呻吟/轻语/BGM 混叠声学切段能力天然不足,把真话当非语音剔除(实测 savr-1054 全片仅 召回 115 条)。新增 decode_full 参数(默认 false 保持 VAD 现状): - decode_full=true 时强制无 VAD 整段解码 + 跳过自动 VAD 分析,救回被 剔除的弱语音(savr-1054 全片 115 条 → 340 条) - 副作用是长时寒暄套话幻觉(おやすみなさい/ご視聴ありがとうございま した 等),whisper 转录后连带时间戳整条删除(clean_japanese_ha lllucinations),不留下 '-' 占位污染下游(占位会渲染进 ASS 成减号) - llm-translate 翻译后同样整条删除中文长时寒暄幻觉(clean_srt_text) - 短时(≤15s)相同词可能是剧情真实道晚安,保留(15s 阈值实测校准) - subtitle_cleanup 由 '-' 占位式改为整条删除式 + 剩余重编号,新增 JAPANESE_HALLUCINATION_TOKENS 词表 新增工作流 learn-translate(学习资料转译+翻译字幕)示范 decode_full 用法,并确立参数标注约定:params._note_<参数名> 存放设定理由与正反例、 _node_help 放节点参数手册(_ 前缀说明键,节点执行时忽略,零运行影响)。 调研记录见 docs/调研-whisper漏句与decode_full验证.md(A/B 实验、结论 修正与 5 个待决问题)。
This commit is contained in:
+103
-42
@@ -1,4 +1,4 @@
|
||||
"""Subtitle cleanup: mask long-duration closing/greeting hallucinations.
|
||||
"""Subtitle cleanup: remove long-duration closing/greeting hallucinations.
|
||||
|
||||
Background (real run 20260905115050): after fixing the timing alignment,
|
||||
subtitles still contain "closing/greeting hallucination words" - fixed
|
||||
@@ -7,13 +7,20 @@ phrases like 'wan an / gan xie guan kan / gan xie nin de guan kan'
|
||||
empty segments, filling a full 30s block, unrelated to video content.
|
||||
Some 2s 'good night' might be real dialogue, so it must be kept.
|
||||
|
||||
Plan (confirmed by user): after translation, mask subtitle entries whose
|
||||
*display duration* exceeds a threshold AND whose text contains a greeting
|
||||
hallucination token - replace the text with '-' so the downstream SRT/filter
|
||||
pipeline drops it. The duration threshold protects short real greetings.
|
||||
处理策略(2026-09 用户确认改为"整条剔除"):
|
||||
幻觉识别出后(展示时长 ≥ 阈值 且 文本命中套话词表),应**连带时间戳把整条
|
||||
字幕 cue 删除**(剩余条目重新编号),而不是把文本替换成 '-' 占位留给下游——
|
||||
占位会一路流到 ASS 渲染成可见的"减号"、在翻译/过滤阶段都要额外特殊处理,
|
||||
处理位置绕且不彻底。因此清洗统一为:**在幻觉产生处(whisper 转录后 / LLM
|
||||
翻译后)整条删除**,时间轴随之消失,字幕序号重新连续编号。
|
||||
|
||||
Threshold is derived from real run data: 30s hallucinations vs 2s real words,
|
||||
a clear gap; default 15s (>=15s masks, <15s keeps).
|
||||
两类词表:
|
||||
- HALLUCINATION_TOKENS:中文(LLM 翻译产物中的寒暄,如"晚安/感谢观看");
|
||||
- JAPANESE_HALLUCINATION_TOKENS:日文(whisper decode_full 无 VAD 解码在
|
||||
无语音段直接输出的套话,如"おやすみなさい/ご視聴ありがとうございました")。
|
||||
|
||||
阈值从真实运行实测数据判定:30s 幻觉占位 vs 2s 真实词,分界明显,
|
||||
默认 threshold=15s(≥15s 才删除;<15s 的相同词可能是剧情真实道晚安,保留)。
|
||||
|
||||
Pure functions, unit-testable (tests/test_hallucination_mask.py).
|
||||
"""
|
||||
@@ -29,32 +36,31 @@ HALLUCINATION_TOKENS = (
|
||||
"再见", "多谢观看", "观看愉快",
|
||||
)
|
||||
|
||||
# Display-duration threshold (seconds): only mask entries longer than this.
|
||||
# Display-duration threshold (seconds): only remove entries longer than this.
|
||||
DEFAULT_THRESHOLD_SECONDS = 15.0
|
||||
|
||||
_SRT_BLOCK = re.compile(
|
||||
r"(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})\s*\n(.*?)(?=\n\s*\d+\s*\n|\Z)",
|
||||
re.DOTALL,
|
||||
# 日文 ASR 直出(whisper 节点 decode_full 无 VAD 整段解码)的收尾/寒暄幻觉词。
|
||||
# 无 VAD 解码会把无语音/音乐/呻吟段当语音,whisper 常在这些段重复输出套话
|
||||
# (实测 savr-1054 全片 119-149s/600-630s 等出现 30s 长"おやすみなさい"
|
||||
# "ご視聴ありがとうございました")。短时(≤阈值)的相同词可能是剧情里真实
|
||||
# 互道晚安,须保留;仅删除展示时长 ≥ 阈值的条目(与中文表同一机制)。
|
||||
JAPANESE_HALLUCINATION_TOKENS = (
|
||||
"おやすみなさい", # 晚安
|
||||
"ご視聴ありがとうございました", # 感谢观看
|
||||
"ありがとうございました", # 感谢
|
||||
"ご視聴ありがとうございます", # 感谢观看(现在时)
|
||||
"また見てね", # 下次再见
|
||||
"お楽しみに", # 敬请期待
|
||||
"チャンネル登録よろしくお願いします", # 求订阅
|
||||
"さようなら", # 再见
|
||||
"音楽", # 音乐(自述)
|
||||
"Goodbye",
|
||||
"Thank you for watching",
|
||||
)
|
||||
|
||||
|
||||
def mask_hallucination_text(
|
||||
entries: list[dict],
|
||||
threshold_seconds: float = DEFAULT_THRESHOLD_SECONDS,
|
||||
) -> list[dict]:
|
||||
"""Return a new list where long-duration greeting entries have text='-'.
|
||||
|
||||
duration = end - start. Only entries whose duration >= threshold AND text
|
||||
contains any HALLUCINATION_TOKENS are masked. Input list is not mutated.
|
||||
"""
|
||||
cleaned = []
|
||||
for entry in entries:
|
||||
duration = entry.get("end", 0.0) - entry.get("start", 0.0)
|
||||
text = entry.get("text", "")
|
||||
if duration >= threshold_seconds and any(t in text for t in HALLUCINATION_TOKENS):
|
||||
entry = dict(entry, text="-")
|
||||
cleaned.append(entry)
|
||||
return cleaned
|
||||
# 解析 SRT:每个 cue 由 序号行 + 时间轴行 + 文本行(可能多行) + 空行 组成。
|
||||
# 采用逐行解析(不依赖可能粘连的跨 cue 正则),兼容文本多行。
|
||||
_TS_RE = re.compile(r"^(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})\s*$")
|
||||
|
||||
|
||||
def _ts_to_seconds(ts: str) -> float:
|
||||
@@ -64,22 +70,77 @@ def _ts_to_seconds(ts: str) -> float:
|
||||
return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + int(millis) / 1000
|
||||
|
||||
|
||||
def remove_hallucination_entries(
|
||||
srt_text: str,
|
||||
tokens: tuple[str, ...],
|
||||
threshold_seconds: float = DEFAULT_THRESHOLD_SECONDS,
|
||||
) -> str:
|
||||
"""删除 SRT 中展示时长 ≥ 阈值且文本命中 tokens 的**整条 cue**(连带时间戳)。
|
||||
|
||||
被删除的 cue 不再输出(序号、时间轴、文本全部消失),剩余 cue 按原顺序
|
||||
重新从 1 编号,保证产物是合法连续的 SRT。输入不被修改(纯函数)。
|
||||
|
||||
用途:幻觉在产生处直接剔除——whisper decode_full(日语词表)与 LLM 翻译后
|
||||
(中文词表)均调用本函数,避免 '-' 占位一路流到 ASS 渲染成可见减号。
|
||||
"""
|
||||
lines = srt_text.splitlines()
|
||||
kept: list[str] = []
|
||||
number = 1
|
||||
index = 0
|
||||
while index < len(lines):
|
||||
line = lines[index]
|
||||
if line.strip() and line.strip().isdigit() and index + 1 < len(lines):
|
||||
ts_match = _TS_RE.match(lines[index + 1].strip())
|
||||
if ts_match:
|
||||
# 收集本 cue 文本:时间轴后直到空行前的所有非空行(可多行)。
|
||||
text_lines: list[str] = []
|
||||
cursor = index + 2
|
||||
while cursor < len(lines) and lines[cursor].strip():
|
||||
text_lines.append(lines[cursor].strip())
|
||||
cursor += 1
|
||||
text = "\n".join(text_lines)
|
||||
start_sec = _ts_to_seconds(ts_match.group(1))
|
||||
end_sec = _ts_to_seconds(ts_match.group(2))
|
||||
duration = end_sec - start_sec
|
||||
is_hallucination = duration >= threshold_seconds and any(
|
||||
t in text for t in tokens
|
||||
)
|
||||
if not is_hallucination:
|
||||
# 非幻觉:输出 新序号+时间轴+文本+空行(重建标准 SRT)。
|
||||
kept.append(
|
||||
f"{number}\n{lines[index + 1].strip()}\n{text}\n"
|
||||
)
|
||||
number += 1
|
||||
# 幻觉 cue:整条跳过(序号/时间轴/文本都不输出)。
|
||||
index = cursor
|
||||
continue
|
||||
# 非 cue 行(文件头/尾部噪声)跳过,避免序号/空行残留。
|
||||
index += 1
|
||||
return "\n".join(kept).rstrip() + "\n"
|
||||
|
||||
|
||||
def clean_japanese_hallucinations(
|
||||
srt_text: str,
|
||||
threshold_seconds: float = DEFAULT_THRESHOLD_SECONDS,
|
||||
) -> str:
|
||||
"""从 SRT 中整条删除日文收尾/寒暄长时幻觉(ASR 无 VAD 解码兜底)。
|
||||
|
||||
whisper 节点 decode_full=true(无 VAD 整段解码)在无语音段会输出长时
|
||||
套话占位;本函数把展示时长 ≥ 阈值且文本含 JAPANESE_HALLUCINATION_TOKENS
|
||||
的**整条 cue 连带时间戳删除**、剩余重编号。短时相同词(剧情真实道晚安)
|
||||
保留。纯函数,不修改输入。
|
||||
"""
|
||||
return remove_hallucination_entries(srt_text, JAPANESE_HALLUCINATION_TOKENS, threshold_seconds)
|
||||
|
||||
|
||||
def clean_srt_text(
|
||||
srt_text: str,
|
||||
threshold_seconds: float = DEFAULT_THRESHOLD_SECONDS,
|
||||
) -> str:
|
||||
"""Mask long-duration greeting hallucinations in an SRT string.
|
||||
"""从 SRT 中整条删除中文长时寒暄幻觉(LLM 翻译产物清洗)。
|
||||
|
||||
Parses each cue's start/end/text, applies mask_hallucination_text, and
|
||||
rewrites the block keeping the original time line when not masked.
|
||||
对展示时长 ≥ 阈值且文本含 HALLUCINATION_TOKENS 的 cue 连带时间戳整条
|
||||
删除、剩余重编号;短时相同词(剧情真实互道晚安)保留。与
|
||||
clean_japanese_hallucinations 同机制,词表不同。纯函数,不修改输入。
|
||||
"""
|
||||
def _replace(match) -> str:
|
||||
start = _ts_to_seconds(match.group(1))
|
||||
end = _ts_to_seconds(match.group(2))
|
||||
text = match.group(3).strip()
|
||||
entry = {"start": start, "end": end, "text": text}
|
||||
cleaned = mask_hallucination_text([entry], threshold_seconds)
|
||||
new_text = cleaned[0]["text"]
|
||||
return f"{match.group(1)} --> {match.group(2)}\n{new_text}"
|
||||
|
||||
return _SRT_BLOCK.sub(_replace, srt_text)
|
||||
return remove_hallucination_entries(srt_text, HALLUCINATION_TOKENS, threshold_seconds)
|
||||
|
||||
Reference in New Issue
Block a user