translate_vtt.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #!/usr/bin/env python3
  2. """
  3. Main script for translating Japanese VTT files to English using Ollama.
  4. This script orchestrates the entire translation pipeline:
  5. 1. Prompts user for input VTT file
  6. 2. Analyzes and chunks the file
  7. 3. Translates each chunk via Ollama
  8. 4. Validates translations
  9. 5. Reassembles into final output
  10. """
  11. import os
  12. import sys
  13. import tempfile
  14. import shutil
  15. from pathlib import Path
  16. from vtt_utils import VTTFile, Subtitle
  17. from chunker import VTTChunker
  18. from ollama_client import OllamaClient
  19. from translator import TranslationProcessor
  20. from reassembler import VTTReassembler
  21. from tui import ProgressDisplay
  22. # Configuration from environment / hardcoded defaults
  23. OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', 'http://localhost:11434/')
  24. OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'translategemma:12b')
  25. TEMP_DIR = '/tmp/'
  26. def get_input_file() -> str:
  27. """
  28. Prompt user for input VTT file path.
  29. Returns:
  30. Absolute path to the VTT file
  31. """
  32. display = ProgressDisplay()
  33. display.print_banner("Japanese VTT Translator")
  34. while True:
  35. display.print_info("Enter the path to your Japanese VTT file:")
  36. file_path = input(" > ").strip()
  37. if not file_path:
  38. display.print_warning("Please enter a valid path.")
  39. continue
  40. # Expand user home directory
  41. expanded_path = os.path.expanduser(file_path)
  42. # Convert to absolute path
  43. if not os.path.isabs(expanded_path):
  44. expanded_path = os.path.abspath(expanded_path)
  45. if not os.path.exists(expanded_path):
  46. display.print_error(f"File not found: {expanded_path}")
  47. continue
  48. if not expanded_path.lower().endswith('.vtt'):
  49. display.print_warning("File must be a .vtt file.")
  50. continue
  51. return expanded_path
  52. return ""
  53. def validate_ollama_connection() -> bool:
  54. """
  55. Validate that Ollama server is available and has the required model.
  56. Returns:
  57. True if connection is valid, False otherwise
  58. """
  59. display = ProgressDisplay()
  60. display.print_section("Validating Ollama Connection")
  61. display.print_info(f"Server URL: {OLLAMA_BASE_URL}")
  62. display.print_info(f"Model: {OLLAMA_MODEL}")
  63. client = OllamaClient(OLLAMA_BASE_URL, OLLAMA_MODEL)
  64. if not client.is_available():
  65. display.print_error("Cannot connect to Ollama server.")
  66. display.print_info(f"Make sure Ollama is running at {OLLAMA_BASE_URL}")
  67. return False
  68. display.print_success("✓ Connected to Ollama")
  69. # Try to get model info
  70. model_info = client.get_model_info()
  71. if model_info:
  72. display.print_success(f"✓ Model '{OLLAMA_MODEL}' is available")
  73. else:
  74. display.print_warning(f"Could not verify model '{OLLAMA_MODEL}' availability")
  75. display.print_info("Proceeding anyway - may fail during translation")
  76. return True
  77. def main():
  78. """Main execution flow."""
  79. display = ProgressDisplay()
  80. try:
  81. # Step 1: Get input file
  82. display.print_step(1, 6, "Select Input File")
  83. input_file = get_input_file()
  84. if not input_file:
  85. display.print_error("No valid file selected. Exiting.")
  86. return
  87. display.print_success(f"✓ Selected: {input_file}")
  88. # Step 2: Validate Ollama connection
  89. display.print_step(2, 6, "Validate Ollama Connection")
  90. if not validate_ollama_connection():
  91. display.print_error("Cannot proceed without Ollama connection. Exiting.")
  92. return
  93. # Step 3: Load and analyze input file
  94. display.print_step(3, 6, "Load and Analyze VTT File")
  95. display.print_info("Loading VTT file...")
  96. try:
  97. vtt_file = VTTFile(input_file)
  98. except Exception as e:
  99. display.print_error(f"Failed to parse VTT file: {e}")
  100. return
  101. display.print_success(f"✓ Loaded {len(vtt_file.subtitles)} subtitles")
  102. total_minutes, total_hours = vtt_file.get_duration()
  103. display.print_file_info(
  104. os.path.basename(input_file),
  105. total_minutes,
  106. total_hours,
  107. 0 # Will update after chunking
  108. )
  109. # Step 4: Chunk the file
  110. display.print_step(4, 6, "Chunk VTT File")
  111. display.print_info("Chunking file respecting token limits...")
  112. chunker = VTTChunker(vtt_file)
  113. chunks = chunker.chunk()
  114. display.print_success(f"✓ Created {len(chunks)} chunks")
  115. token_estimates = chunker.get_chunk_token_estimates()
  116. display.print_info(f"Average tokens per chunk: {sum(token_estimates) // len(token_estimates)}")
  117. # Step 5: Translate chunks
  118. display.print_step(5, 6, "Translate Chunks")
  119. display.print_info(
  120. f"Translating {len(chunks)} chunks via Ollama (this may take several minutes)..."
  121. )
  122. client = OllamaClient(OLLAMA_BASE_URL, OLLAMA_MODEL)
  123. processor = TranslationProcessor(client)
  124. translated_chunks = []
  125. failed_chunks = []
  126. for i, chunk in enumerate(chunks, 1):
  127. display.print_chunk_status(
  128. i, len(chunks), "⏳ Processing...",
  129. f"{len(chunk.subtitles)} subtitles"
  130. )
  131. # Create a custom processor that shows progress
  132. client = OllamaClient(OLLAMA_BASE_URL, OLLAMA_MODEL)
  133. # Translate subtitles with progress
  134. translated_subs = []
  135. for j, sub in enumerate(chunk.subtitles, 1):
  136. # Show progress bar for every subtitle (or every 10 if there are many)
  137. progress_interval = max(1, len(chunk.subtitles) // 20) if len(chunk.subtitles) > 50 else 1
  138. if j % progress_interval == 0 or j == 1 or j == len(chunk.subtitles):
  139. display.print_progress_bar(
  140. j, len(chunk.subtitles),
  141. label=f"Chunk {i}"
  142. )
  143. # Translate with feedback
  144. print(f" Translating subtitle {j}/{len(chunk.subtitles)}...", end="\r", flush=True)
  145. translated_text = client.translate(sub.text)
  146. if translated_text is None:
  147. translated_text = ""
  148. translated_subs.append(Subtitle(
  149. start_time=sub.start_time,
  150. end_time=sub.end_time,
  151. text=translated_text
  152. ))
  153. print() # Clear the progress line
  154. # Create translated chunk
  155. processed_chunk = VTTFile.__new__(VTTFile)
  156. processed_chunk.filepath = chunk.filepath
  157. processed_chunk.subtitles = translated_subs
  158. # Sanity check
  159. is_valid, reason = processor.sanity_check(processed_chunk)
  160. if is_valid:
  161. translated_chunks.append(processed_chunk)
  162. display.print_chunk_status(i, len(chunks), "✓ Translated")
  163. else:
  164. # Try once more
  165. display.print_warning(f" Sanity check failed: {reason}. Retrying chunk {i}...")
  166. translated_subs = []
  167. for j, sub in enumerate(chunk.subtitles, 1):
  168. print(f" Retrying subtitle {j}/{len(chunk.subtitles)}...", end="\r", flush=True)
  169. translated_text = client.translate(sub.text)
  170. if translated_text is None:
  171. translated_text = ""
  172. translated_subs.append(Subtitle(
  173. start_time=sub.start_time,
  174. end_time=sub.end_time,
  175. text=translated_text
  176. ))
  177. print() # Clear the progress line
  178. processed_chunk = VTTFile.__new__(VTTFile)
  179. processed_chunk.filepath = chunk.filepath
  180. processed_chunk.subtitles = translated_subs
  181. is_valid, reason = processor.sanity_check(processed_chunk)
  182. if is_valid:
  183. translated_chunks.append(processed_chunk)
  184. display.print_chunk_status(i, len(chunks), "✓ Translated (retry)")
  185. else:
  186. failed_chunks.append(i)
  187. display.print_chunk_status(i, len(chunks), f"✗ Failed: {reason}")
  188. if failed_chunks:
  189. display.print_warning(
  190. f"Failed to translate {len(failed_chunks)} chunk(s): {failed_chunks}"
  191. )
  192. if len(failed_chunks) == len(chunks):
  193. display.print_error("All chunks failed. Cannot proceed. Exiting.")
  194. return
  195. else:
  196. display.print_success(f"✓ All {len(chunks)} chunks translated successfully")
  197. # Step 6: Reassemble and finalize
  198. display.print_step(6, 6, "Reassemble and Finalize")
  199. display.print_info("Reassembling translated chunks...")
  200. if not translated_chunks:
  201. display.print_error("No translated chunks available. Exiting.")
  202. return
  203. output_dir = os.path.dirname(input_file)
  204. output_path = VTTReassembler.reassemble(
  205. translated_chunks,
  206. os.path.basename(input_file),
  207. output_dir
  208. )
  209. display.print_success(f"✓ Reassembled into single file")
  210. # Final summary
  211. display.print_banner("Translation Complete!")
  212. display.print_info(f"Output file: {output_path}")
  213. if failed_chunks:
  214. display.print_warning(
  215. f"Note: {len(failed_chunks)} chunk(s) could not be translated. "
  216. f"Output is incomplete."
  217. )
  218. display.print_success("Translation pipeline completed successfully!")
  219. except KeyboardInterrupt:
  220. display.print_warning("\nInterrupted by user.")
  221. sys.exit(1)
  222. except Exception as e:
  223. display.print_error(f"Unexpected error: {e}")
  224. import traceback
  225. traceback.print_exc()
  226. sys.exit(1)
  227. if __name__ == '__main__':
  228. main()