fix: 短时重复伪影也整条删除(判据补重复次数上限)
真实产物里出现 3.7 秒的 cue 被同一个假名填满 111 次(`は`×111),此前判据要求 "时长 ≥15 秒"才删,于是它漏到翻译层,最终变成 56 个"哈"进中文成品字幕。 - 判据改为:重复段占正文 ≥70% 且重复 ≥6 次,并且**时长 ≥15 秒或重复 ≥20 次**。 - 短条走"极端重复"档:真实呻吟的重复次数实测 ≤15(ぇ×15 占 3.2 秒),不会误删。 - 测试数据取自真实产物(A 的日语转写里那条 112 字短伪影)。
This commit is contained in:
+18
-10
@@ -64,11 +64,13 @@ MOAN_CHARS = frozenset(
|
|||||||
# /んふふ)有效假名 ≤3;>3(如ああああ)或含非呻吟字符的一律保留。
|
# /んふふ)有效假名 ≤3;>3(如ああああ)或含非呻吟字符的一律保留。
|
||||||
DEFAULT_MOAN_MAX_CHARS = 3
|
DEFAULT_MOAN_MAX_CHARS = 3
|
||||||
|
|
||||||
# 重复伪影判据(whisper 窗口内重复循环会输出整条重复到 30 秒的 cue):
|
# 重复伪影判据(whisper 循环解码会把一个单元写满整条 cue):
|
||||||
# 单元最长 6 字(更长的重复单元在真实数据里未见),至少重复 6 次,
|
# 单元最长 6 字(更长的重复单元在真实数据里未见),重复段占正文 ≥70%。
|
||||||
# 且重复段占正文 ≥70%。真实呻吟重复次数也在 6–15 之间,靠时长区分。
|
# 长度维度两档:长条(≥15s)重复 ≥6 次即删;短条则要求极端重复(≥20 次)——
|
||||||
|
# 真实呻吟重复次数实测 ≤15(ぇ×15 / しゅ×7),而短时伪影实测 3.7 秒填了 111 次。
|
||||||
REPETITION_UNIT_MAX_CHARS = 6
|
REPETITION_UNIT_MAX_CHARS = 6
|
||||||
REPETITION_MIN_REPEATS = 6
|
REPETITION_MIN_REPEATS = 6
|
||||||
|
REPETITION_HARD_REPEATS = 20
|
||||||
REPETITION_MIN_COVERAGE = 0.7
|
REPETITION_MIN_COVERAGE = 0.7
|
||||||
|
|
||||||
|
|
||||||
@@ -92,20 +94,25 @@ def _is_repetition_artifact(text: str, duration: float, threshold_seconds: float
|
|||||||
min_repeats: int, min_coverage: float) -> bool:
|
min_repeats: int, min_coverage: float) -> bool:
|
||||||
"""判断一条 cue 是否为 whisper 重复循环伪影(整条删除判据)。
|
"""判断一条 cue 是否为 whisper 重复循环伪影(整条删除判据)。
|
||||||
|
|
||||||
判据(同时满足):展示时长 ≥ 阈值、同一 1–6 字单元连续重复 ≥ min_repeats 次、
|
同时满足:同一 1–6 字单元连续重复 ≥ min_repeats 次、重复段占正文
|
||||||
重复段占正文 ≥ min_coverage。短促重复(“ぇ”×15 只占 3 秒)是真实发声,靠
|
≥ min_coverage,且**时长 ≥ 阈值或重复次数 ≥ REPETITION_HARD_REPEATS**。
|
||||||
时长区分,与寒暄幻觉同一套“长条才删”思路。
|
长度维度分两档是必要的:只按"长条才删"会漏掉短时循环(实测 3.7 秒的 cue
|
||||||
|
被填了 111 个假名,翻译后变成 56 个"哈"进成品);而真实呻吟的重复次数实测
|
||||||
|
≤15(ぇ×15 占 3.2 秒),所以短条用"极端重复"判据即可区分,不会误删。
|
||||||
|
threshold_seconds ≤ 0 关闭过滤。
|
||||||
"""
|
"""
|
||||||
if threshold_seconds <= 0 or duration < threshold_seconds:
|
if threshold_seconds <= 0:
|
||||||
return False
|
return False
|
||||||
stripped = text.replace("\n", "")
|
stripped = text.replace("\n", "")
|
||||||
found = _longest_repeated_run(stripped, REPETITION_UNIT_MAX_CHARS)
|
found = _longest_repeated_run(stripped, REPETITION_UNIT_MAX_CHARS)
|
||||||
if found is None:
|
if found is None:
|
||||||
return False
|
return False
|
||||||
run, repeats = found
|
run, repeats = found
|
||||||
if repeats < min_repeats or not stripped:
|
if not stripped or repeats < min_repeats:
|
||||||
return False
|
return False
|
||||||
return len(run) / len(stripped) >= min_coverage
|
if len(run) / len(stripped) < min_coverage:
|
||||||
|
return False
|
||||||
|
return duration >= threshold_seconds or repeats >= REPETITION_HARD_REPEATS
|
||||||
|
|
||||||
def _moan_chars(text: str) -> int:
|
def _moan_chars(text: str) -> int:
|
||||||
"""返回 text 中'有效假名字符'数量(呻吟判据的一部分)。
|
"""返回 text 中'有效假名字符'数量(呻吟判据的一部分)。
|
||||||
@@ -196,7 +203,8 @@ def remove_repetition_entries(
|
|||||||
模型在窗口内卡住重复时会把同一单元写满整条 cue(实测 30 秒、重复 74–446
|
模型在窗口内卡住重复时会把同一单元写满整条 cue(实测 30 秒、重复 74–446
|
||||||
次):这种正文会让下游 LLM 跟着重复、把预算耗在思考上而报结构错误,也会
|
次):这种正文会让下游 LLM 跟着重复、把预算耗在思考上而报结构错误,也会
|
||||||
直接渲染成超长字幕行,因此在产生处整条删除。判据见 _is_repetition_artifact:
|
直接渲染成超长字幕行,因此在产生处整条删除。判据见 _is_repetition_artifact:
|
||||||
长条(时长 ≥ 阈值)+ 同一短单元高频重复且占满正文,真实短促呻吟不会命中。
|
长条(时长 ≥ 阈值)或极端重复(≥ REPETITION_HARD_REPEATS 次)+ 同一短单元
|
||||||
|
占满正文;真实短促呻吟(重复 ≤15 次)不会命中。
|
||||||
threshold_seconds ≤ 0 时关闭过滤。纯函数,不修改输入。
|
threshold_seconds ≤ 0 时关闭过滤。纯函数,不修改输入。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -49,5 +49,14 @@
|
|||||||
"start": "00:00:12,000",
|
"start": "00:00:12,000",
|
||||||
"end": "00:00:15,000",
|
"end": "00:00:15,000",
|
||||||
"text": "そんなにご褒美欲しかったの?"
|
"text": "そんなにご褒美欲しかったの?"
|
||||||
|
},
|
||||||
|
"short_artifacts": [
|
||||||
|
{
|
||||||
|
"start": "00:22:56,363",
|
||||||
|
"end": "00:23:00,093",
|
||||||
|
"text": "ははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははははは�",
|
||||||
|
"repeats": 111,
|
||||||
|
"unit": "は"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -330,3 +330,20 @@ def test_remove_repetition_entries_can_be_disabled() -> None:
|
|||||||
srt = _srt((artifact["start"], artifact["end"], artifact["text"]))
|
srt = _srt((artifact["start"], artifact["end"], artifact["text"]))
|
||||||
|
|
||||||
assert remove_repetition_entries(srt, threshold_seconds=0) == srt
|
assert remove_repetition_entries(srt, threshold_seconds=0) == srt
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_repetition_entries_deletes_short_but_extreme_repetition() -> None:
|
||||||
|
"""数据:真实产物里的短时伪影——3.7 秒的 cue 被同一个假名填了 111 次。
|
||||||
|
|
||||||
|
过程:调用 remove_repetition_entries。
|
||||||
|
|
||||||
|
验证:整条删除。判据不能只看时长:这种短条会一路翻成 56 个"哈"进成品字幕,
|
||||||
|
但真实呻吟的重复次数实测 ≤15,用重复次数上限即可区分。
|
||||||
|
"""
|
||||||
|
artifact = _REPETITION_CUES["short_artifacts"][0]
|
||||||
|
srt = _srt((artifact["start"], artifact["end"], artifact["text"]))
|
||||||
|
|
||||||
|
cleaned = remove_repetition_entries(srt)
|
||||||
|
|
||||||
|
assert artifact["text"] not in cleaned
|
||||||
|
assert parse_srt_entries(cleaned) == []
|
||||||
|
|||||||
Reference in New Issue
Block a user