backend/health_fixes.py
Rama master · réplica de lectura del repositorio original.
"""Subtitle auto-fix functions.
Extracted from health_checker.py. Each fix_* function mutates a
pysubs2.SSAFile in-place and returns the count of repairs made.
FIX_MAP wires detection-rule names to their fixer — apply_fixes() in
health_checker iterates it by name.
"""
def fix_duplicate_lines(subs) -> int:
"""Remove exact duplicate events. Returns count removed."""
seen = set()
to_remove = []
for idx, event in enumerate(subs.events):
if event.is_comment:
continue
key = (event.start, event.end, event.text, event.style)
if key in seen:
to_remove.append(idx)
else:
seen.add(key)
# Remove in reverse order to preserve indices
for idx in reversed(to_remove):
del subs.events[idx]
return len(to_remove)
def fix_timing_overlaps(subs) -> int:
"""Trim previous event end to current event start for same-style overlaps.
Returns count of overlaps fixed.
"""
groups = {}
for idx, event in enumerate(subs.events):
if event.is_comment:
continue
key = (event.style, getattr(event, "layer", 0))
if key not in groups:
groups[key] = []
groups[key].append((idx, event))
fixed = 0
for group_key, events in groups.items():
sorted_events = sorted(events, key=lambda x: x[1].start)
for i in range(1, len(sorted_events)):
prev_idx, prev = sorted_events[i - 1]
curr_idx, curr = sorted_events[i]
if curr.start < prev.end:
prev.end = curr.start
fixed += 1
return fixed
def fix_missing_styles(subs) -> int:
"""Change undefined style references to 'Default'. Returns count fixed."""
if not hasattr(subs, "styles") or not subs.styles:
return 0
defined_styles = set(subs.styles.keys())
fixed = 0
for event in subs.events:
if event.is_comment:
continue
if event.style not in defined_styles:
event.style = "Default"
fixed += 1
return fixed
def fix_empty_events(subs) -> int:
"""Remove events with empty plaintext. Returns count removed."""
to_remove = []
for idx, event in enumerate(subs.events):
if event.is_comment:
continue
if not event.plaintext.strip():
to_remove.append(idx)
for idx in reversed(to_remove):
del subs.events[idx]
return len(to_remove)
def fix_negative_timing(subs) -> int:
"""Swap start and end when end < start. Returns count fixed."""
fixed = 0
for event in subs.events:
if event.is_comment:
continue
if event.end < event.start:
event.start, event.end = event.end, event.start
fixed += 1
return fixed
def fix_zero_duration(subs) -> int:
"""Remove zero-duration events. Returns count removed."""
to_remove = []
for idx, event in enumerate(subs.events):
if event.is_comment:
continue
if event.start == event.end:
to_remove.append(idx)
for idx in reversed(to_remove):
del subs.events[idx]
return len(to_remove)
# Map check names to fix functions
FIX_MAP = {
"duplicate_lines": fix_duplicate_lines,
"timing_overlaps": fix_timing_overlaps,
"missing_styles": fix_missing_styles,
"empty_events": fix_empty_events,
"negative_timing": fix_negative_timing,
"zero_duration": fix_zero_duration,
}