Skip to content

Commit 937e39d

Browse files
FIX: Translate valid subtitles when SDH cleanup fails in ./Subtitles Translation with DeepL/main.py
1 parent f2d7f4b commit 937e39d

1 file changed

Lines changed: 88 additions & 14 deletions

File tree

  • Subtitles Translation with DeepL

Subtitles Translation with DeepL/main.py

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ def parse_srt_blocks(lines: List[str]) -> List[Tuple[str, str, List[str]]]:
449449

450450
blocks = [] # Store parsed subtitle blocks
451451
block = [] # Store current subtitle block
452+
pending_empty_block = None # Hold index/timing when text appears after extra blank lines
452453

453454
for line in lines + [""]: # Add sentinel blank line to flush final block
454455
stripped = line.strip().lstrip("\ufeff") # Normalize current line and ignore UTF-8 BOM
@@ -459,19 +460,49 @@ def parse_srt_blocks(lines: List[str]) -> List[Tuple[str, str, List[str]]]:
459460
if not block:
460461
continue
461462

462-
if len(block) < 3 or not block[0].isdigit() or "-->" not in block[1]: # Reject malformed SRT block
463+
if len(block) < 2 or not block[0].isdigit() or "-->" not in block[1]: # Reject malformed SRT block
464+
if pending_empty_block:
465+
index, timing = pending_empty_block # Recover text split from its timing by extra blank lines
466+
blocks.append((index, timing, [text_line.strip() for text_line in block if text_line.strip()])) # Store recovered block text
467+
pending_empty_block = None # Clear recovered pending block
468+
block = [] # Reset current subtitle block
469+
continue
470+
if blocks:
471+
blocks[-1][2].extend(text_line.strip() for text_line in block if text_line.strip()) # Recover orphan text split by extra blank lines
472+
block = [] # Reset current subtitle block
473+
continue
463474
return [] # Return empty result for malformed subtitles
464475

465476
text_lines = [text_line.strip() for text_line in block[2:] if text_line.strip()] # Normalize translatable text lines
477+
if pending_empty_block:
478+
index, timing = pending_empty_block # Restore earlier empty timed cue before current valid block
479+
blocks.append((index, timing, [])) # Preserve empty cue structure
480+
pending_empty_block = None # Clear restored pending block
466481
if not text_lines:
467-
return [] # Return empty result for blocks without text
468-
482+
pending_empty_block = (block[0], block[1]) # Wait for possible text split by extra blank lines
483+
block = [] # Reset current subtitle block
484+
continue
469485
blocks.append((block[0], block[1], text_lines)) # Store parsed block
470486
block = [] # Reset current subtitle block
471487

488+
if pending_empty_block:
489+
index, timing = pending_empty_block # Restore trailing empty timed cue
490+
blocks.append((index, timing, [])) # Preserve empty cue structure
491+
472492
return blocks # Return parsed subtitle blocks
473493

474494

495+
def has_valid_srt_structure(lines: List[str]) -> bool:
496+
"""
497+
Determines whether SRT lines are empty or structurally parseable.
498+
499+
:param lines: SRT lines to validate.
500+
:return: True when empty or valid, otherwise False.
501+
"""
502+
503+
return not lines or bool(parse_srt_blocks(lines)) # Preserve empty-file handling while rejecting malformed content
504+
505+
475506
def strip_html_tags(text: str) -> str:
476507
"""
477508
Removes supported SRT formatting tags from text.
@@ -788,7 +819,8 @@ def remove_descriptive_subtitles(file_path) -> Tuple[List[str], int, int]:
788819
original_lines = read_srt(file_path) # Read source SRT lines
789820
cleaned_lines, removed_entry_count, mixed_cleaned_entry_count = clean_descriptive_subtitle_lines(original_lines) # Clean without duplicating rules
790821
if cleaned_lines and not parse_srt_blocks(cleaned_lines): # Validate serialized cleaned subtitles before replacing
791-
raise ValueError(f"Invalid SRT structure after SDH cleanup: {file_path}") # Stop before source replacement
822+
print(f"{BackgroundColors.YELLOW}SDH cleanup failed structural validation. Original subtitle will be translated:{Style.RESET_ALL}\n{BackgroundColors.CYAN}{file_path}{Style.RESET_ALL}") # Preserve source on cleanup failure
823+
return original_lines, 0, 0 # Fallback to original valid source lines
792824

793825
write_srt_lines_atomic(Path(file_path), cleaned_lines) # Replace source file after successful write
794826

@@ -1060,7 +1092,7 @@ def build_translation_plan(srt_files: List[Path], input_dir: Path, output_dir: P
10601092
plan = [] # Store pending translation work
10611093
records = [] # Store valid discovered SRT metadata before source/output dedupe
10621094
records_by_path = {} # Map resolved paths to discovered records
1063-
summary = {"discovered": len(srt_files), "source_candidates": 0, "generated_skipped": 0, "existing_skipped": 0, "target_language_skipped": 0, "empty_skipped": 0, "invalid": 0, "invalid_language_outputs": 0, "mislabeled_source_files": 0, "other_skipped": 0, "total_characters": 0} # Store preflight counts
1095+
summary = {"discovered": len(srt_files), "source_candidates": 0, "generated_skipped": 0, "existing_skipped": 0, "target_language_skipped": 0, "empty_skipped": 0, "invalid": 0, "cleanup_fallbacks": 0, "cleanup_warnings": 0, "invalid_language_outputs": 0, "mislabeled_source_files": 0, "other_skipped": 0, "total_characters": 0} # Store preflight counts
10641096

10651097
for srt_file in srt_files: # Analyze each discovered SRT
10661098
current_srt_path = srt_file.resolve() # Resolve source path
@@ -1075,14 +1107,19 @@ def build_translation_plan(srt_files: List[Path], input_dir: Path, output_dir: P
10751107
srt_lines = source_lines # Default to source lines
10761108
removed_entry_count = 0 # Default cleanup counts
10771109
mixed_cleaned_entry_count = 0 # Default cleanup counts
1078-
if DESCRIPTIVE_SUBTITLES_REMOVAL:
1079-
srt_lines, removed_entry_count, mixed_cleaned_entry_count = clean_descriptive_subtitle_lines(source_lines) # Plan cleanup without writing
1080-
1081-
cleaned_blocks = parse_srt_blocks(srt_lines) # Validate cleaned SRT structure
1082-
if srt_lines and not cleaned_blocks:
1083-
summary["invalid"] += 1 # Count invalid preflight file
1084-
print(f"{BackgroundColors.RED}Invalid SRT structure after SDH cleanup: {BackgroundColors.CYAN}{current_srt_path}{Style.RESET_ALL}") # Log invalid source
1110+
cleanup_fallback = False # Track valid source translated without cleanup after cleanup validation failure
1111+
if not has_valid_srt_structure(source_lines):
1112+
summary["invalid"] += 1 # Count genuinely invalid source structure
1113+
print(f"{BackgroundColors.RED}Invalid SRT structure: {BackgroundColors.CYAN}{current_srt_path}{Style.RESET_ALL}") # Log invalid source
10851114
continue
1115+
if DESCRIPTIVE_SUBTITLES_REMOVAL:
1116+
cleaned_lines, cleaned_removed_entry_count, cleaned_mixed_entry_count = clean_descriptive_subtitle_lines(source_lines) # Plan cleanup without writing
1117+
if has_valid_srt_structure(cleaned_lines):
1118+
srt_lines = cleaned_lines # Use valid cleaned representation for all later processing
1119+
removed_entry_count = cleaned_removed_entry_count # Preserve cleanup metadata for valid cleanup
1120+
mixed_cleaned_entry_count = cleaned_mixed_entry_count # Preserve cleanup metadata for valid cleanup
1121+
else:
1122+
cleanup_fallback = True # Keep original valid source when cleanup breaks structure
10861123

10871124
translatable_character_count = count_translatable_characters(srt_lines) # Count exact DeepL text blocks
10881125
if translatable_character_count == 0:
@@ -1091,7 +1128,7 @@ def build_translation_plan(srt_files: List[Path], input_dir: Path, output_dir: P
10911128
continue
10921129

10931130
is_target_language, detection_conclusive, detected_language_label = detect_cleaned_subtitle_language(srt_lines, TARGET_LANG) # Offline language detection
1094-
record = {"source_path": current_srt_path, "output_file": output_file, "lines": srt_lines, "characters": translatable_character_count, "removed_entries": removed_entry_count, "mixed_cleaned_entries": mixed_cleaned_entry_count, "detection_conclusive": detection_conclusive, "detected_language_label": detected_language_label, "is_target_language": is_target_language, "filename_has_generated_marker": has_generated_filename_marker(current_srt_path), "family_key": get_srt_family_key(current_srt_path)} # Store content-based classification record
1131+
record = {"source_path": current_srt_path, "output_file": output_file, "lines": srt_lines, "characters": translatable_character_count, "removed_entries": removed_entry_count, "mixed_cleaned_entries": mixed_cleaned_entry_count, "cleanup_fallback": cleanup_fallback, "detection_conclusive": detection_conclusive, "detected_language_label": detected_language_label, "is_target_language": is_target_language, "filename_has_generated_marker": has_generated_filename_marker(current_srt_path), "family_key": get_srt_family_key(current_srt_path)} # Store content-based classification record
10951132
records.append(record) # Keep record for family dedupe
10961133
records_by_path[current_srt_path] = record # Map by resolved path
10971134
except Exception as e:
@@ -1135,6 +1172,9 @@ def build_translation_plan(srt_files: List[Path], input_dir: Path, output_dir: P
11351172

11361173
summary["generated_skipped"] += sum(1 for record in extra_records if record["is_target_language"]) # Count valid generated companions without planning them
11371174
summary["invalid_language_outputs"] += sum(1 for record in extra_records if not record["is_target_language"]) # Track invalid generated companions
1175+
if source_record["cleanup_fallback"]:
1176+
summary["cleanup_fallbacks"] += 1 # Count planned best-effort cleanup fallback separately from invalid files
1177+
print(f"{BackgroundColors.YELLOW}SDH cleanup failed structural validation. Original subtitle will be translated:{Style.RESET_ALL}\n{BackgroundColors.CYAN}{source_record['source_path']}{Style.RESET_ALL}") # Log cleanup fallback
11381178
plan.append(source_record) # Add pending translation work
11391179
summary["total_characters"] += source_record["characters"] # Add only planned translation characters
11401180
continue
@@ -1155,6 +1195,9 @@ def build_translation_plan(srt_files: List[Path], input_dir: Path, output_dir: P
11551195
else:
11561196
print(f"{BackgroundColors.YELLOW}Language detection was inconclusive despite the target-language filename. Keeping file eligible for translation:{Style.RESET_ALL}\n{BackgroundColors.CYAN}{source_record['source_path']}{Style.RESET_ALL}") # Log conservative classification
11571197

1198+
if source_record["cleanup_fallback"]:
1199+
summary["cleanup_fallbacks"] += 1 # Count planned best-effort cleanup fallback separately from invalid files
1200+
print(f"{BackgroundColors.YELLOW}SDH cleanup failed structural validation. Original subtitle will be translated:{Style.RESET_ALL}\n{BackgroundColors.CYAN}{source_record['source_path']}{Style.RESET_ALL}") # Log cleanup fallback
11581201
plan.append(source_record) # Add pending translation work
11591202
summary["total_characters"] += source_record["characters"] # Add only planned translation characters
11601203
for record in family_records:
@@ -1446,6 +1489,27 @@ def save_srt(lines, output_file, success_message: str = "Translated SRT saved as
14461489
) # Output the saved file message
14471490

14481491

1492+
def cleanup_saved_translation(output_file: Path) -> bool:
1493+
"""
1494+
Attempts SDH cleanup on a saved translated SRT without risking the valid output.
1495+
1496+
:param output_file: Saved translated SRT path.
1497+
:return: True when cleanup was skipped because it produced invalid structure.
1498+
"""
1499+
1500+
output_lines = output_file.read_text(encoding="utf-8").splitlines() # Read valid translated output
1501+
cleaned_lines, removed_entry_count, mixed_cleaned_entry_count = clean_descriptive_subtitle_lines(output_lines) # Clean translated output in memory
1502+
if not removed_entry_count and not mixed_cleaned_entry_count:
1503+
return False # No translated cleanup needed
1504+
1505+
if not has_valid_srt_structure(cleaned_lines) or count_translatable_characters(cleaned_lines) == 0:
1506+
print(f"{BackgroundColors.YELLOW}Translated SRT saved successfully, but SDH cleanup was skipped because it produced an invalid structure:{Style.RESET_ALL}\n{BackgroundColors.CYAN}{output_file}{Style.RESET_ALL}") # Preserve valid translated output
1507+
return True # Cleanup warning emitted
1508+
1509+
write_srt_lines_atomic(output_file, cleaned_lines) # Replace only with validated cleaned translation
1510+
return False # Cleanup succeeded
1511+
1512+
14491513
def calculate_execution_time(start_time, finish_time):
14501514
"""
14511515
Calculates the execution time between start and finish times and formats it as hh:mm:ss.
@@ -1521,7 +1585,7 @@ def main():
15211585
planned_files = len(translation_plan) # Count pending files
15221586
total_planned_characters = preflight_summary["total_characters"] # Count pending characters only
15231587
other_skipped_files = preflight_summary["empty_skipped"] + preflight_summary["other_skipped"] # Count non-language skipped files
1524-
print(f"{BackgroundColors.GREEN}Translation plan: {BackgroundColors.CYAN}{planned_files}{BackgroundColors.GREEN} files | {BackgroundColors.CYAN}{total_planned_characters:,}{BackgroundColors.GREEN} characters | {BackgroundColors.CYAN}{preflight_summary['existing_skipped']}{BackgroundColors.GREEN} existing translations skipped | {BackgroundColors.CYAN}{preflight_summary['target_language_skipped']}{BackgroundColors.GREEN} target-language files skipped | {BackgroundColors.CYAN}{preflight_summary['invalid_language_outputs']}{BackgroundColors.GREEN} invalid-language outputs | {BackgroundColors.CYAN}{preflight_summary['invalid']}{BackgroundColors.GREEN} invalid files{Style.RESET_ALL}") # Print preflight summary
1588+
print(f"{BackgroundColors.GREEN}Translation plan: {BackgroundColors.CYAN}{planned_files}{BackgroundColors.GREEN} files | {BackgroundColors.CYAN}{total_planned_characters:,}{BackgroundColors.GREEN} characters | {BackgroundColors.CYAN}{preflight_summary['existing_skipped']}{BackgroundColors.GREEN} existing translations skipped | {BackgroundColors.CYAN}{preflight_summary['target_language_skipped']}{BackgroundColors.GREEN} target-language files skipped | {BackgroundColors.CYAN}{preflight_summary['invalid_language_outputs']}{BackgroundColors.GREEN} invalid-language outputs | {BackgroundColors.CYAN}{preflight_summary['cleanup_fallbacks']}{BackgroundColors.GREEN} cleanup fallbacks | {BackgroundColors.CYAN}{preflight_summary['invalid']}{BackgroundColors.GREEN} invalid files{Style.RESET_ALL}") # Print preflight summary
15251589

15261590
translated_files = 0 # Count files translated in this run
15271591
failed_files = 0 # Count failed planned files
@@ -1554,6 +1618,8 @@ def main():
15541618
if planned_file["removed_entries"] or planned_file["mixed_cleaned_entries"]: # Log concise cleanup summary when cleanup changed content
15551619
write_srt_lines_atomic(current_srt_path, srt_lines) # Preserve existing source cleanup behavior after preflight
15561620
print(f"{BackgroundColors.YELLOW}SDH cleanup: {BackgroundColors.CYAN}{filename}{BackgroundColors.YELLOW} removed {BackgroundColors.CYAN}{planned_file['removed_entries']}{BackgroundColors.YELLOW} entries, cleaned {BackgroundColors.CYAN}{planned_file['mixed_cleaned_entries']}{BackgroundColors.YELLOW} mixed entries.{Style.RESET_ALL}") # Log cleanup summary
1621+
if planned_file["cleanup_fallback"]:
1622+
print(f"{BackgroundColors.YELLOW}Cleanup mode: Original subtitle content{Style.RESET_ALL}") # Identify fallback source representation once
15571623

15581624
if not planned_file["detection_conclusive"]: # Continue normally when language detection is not reliable
15591625
print(f"{BackgroundColors.YELLOW}Source language detection was inconclusive for {BackgroundColors.CYAN}{filename}{BackgroundColors.YELLOW}. DeepL will determine the source language during translation.{Style.RESET_ALL}") # Log inconclusive detection
@@ -1572,11 +1638,17 @@ def main():
15721638
print_progress_event(progress_state, f"{BackgroundColors.RED}{e}{Style.RESET_ALL}") # Log fatal quota exhaustion cleanly
15731639
break # Preserve existing stop-on-fatal-quota behavior
15741640

1641+
if translated_lines and not has_valid_srt_structure(translated_lines):
1642+
failed_files += 1 # Count invalid translated serialization as failed work
1643+
print_progress_event(progress_state, f"{BackgroundColors.RED}Translated SRT structure is invalid and was not saved: {BackgroundColors.CYAN}{output_file}{Style.RESET_ALL}") # Preserve output safety
1644+
continue
15751645
progress_state["completed_files"] += 1 # Count fully translated file
15761646
translated_files += 1 # Count successful file
15771647
translated_characters = progress_state["overall_translated_characters"] # Store successful character progress
15781648
render_translation_progress(progress_state, force=True) # Finalize successful file progress
15791649
save_srt(translated_lines, output_file) # Save the translated SRT to the output file
1650+
if DESCRIPTIVE_SUBTITLES_REMOVAL and cleanup_saved_translation(output_file):
1651+
preflight_summary["cleanup_warnings"] += 1 # Count skipped translated cleanup separately from translation failure
15801652

15811653
finish_time = datetime.datetime.now() # Get the finish time of the program
15821654
print(
@@ -1590,6 +1662,8 @@ def main():
15901662
f"{BackgroundColors.GREEN}Generated files skipped: {BackgroundColors.CYAN}{preflight_summary['generated_skipped']}{Style.RESET_ALL}\n"
15911663
f"{BackgroundColors.GREEN}Invalid-language outputs: {BackgroundColors.CYAN}{preflight_summary['invalid_language_outputs']}{Style.RESET_ALL}\n"
15921664
f"{BackgroundColors.GREEN}Mislabeled source files: {BackgroundColors.CYAN}{preflight_summary['mislabeled_source_files']}{Style.RESET_ALL}\n"
1665+
f"{BackgroundColors.GREEN}Cleanup fallbacks: {BackgroundColors.CYAN}{preflight_summary['cleanup_fallbacks']}{Style.RESET_ALL}\n"
1666+
f"{BackgroundColors.GREEN}Cleanup warnings: {BackgroundColors.CYAN}{preflight_summary['cleanup_warnings']}{Style.RESET_ALL}\n"
15931667
f"{BackgroundColors.GREEN}Other files skipped: {BackgroundColors.CYAN}{other_skipped_files}{Style.RESET_ALL}\n"
15941668
f"{BackgroundColors.GREEN}Invalid files: {BackgroundColors.CYAN}{preflight_summary['invalid']}{Style.RESET_ALL}\n"
15951669
f"{BackgroundColors.GREEN}Files failed: {BackgroundColors.CYAN}{failed_files}{Style.RESET_ALL}\n"

0 commit comments

Comments
 (0)