@@ -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+
475506def 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+
14491513def 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