From 251499e03b8622cfe635bca2a3c061bc5894b855 Mon Sep 17 00:00:00 2001 From: Saeed Al Mansouri Date: Wed, 8 Apr 2026 13:49:41 +0400 Subject: [PATCH] fix: temp file leaks in auto_sync and API model, correct return type annotations - auto_sync() now cleans up the .ass and .srt temp files (previously only the extensionless NamedTemporaryFile bases were deleted) - WhisperAPIModel.transcribe() now removes the converted audio file in a finally block so it doesn't leak on success or failure - Changed transcribe() return type from -> str to -> SSAFile in all 6 model files to match the actual return value and the abstract base class - Replaced type(model) == str with isinstance(model, str) in 3 places Co-Authored-By: Claude Opus 4.6 (1M context) --- src/subsai/main.py | 11 +++- src/subsai/models/faster_whisper_model.py | 2 +- src/subsai/models/whisperX_model.py | 2 +- src/subsai/models/whisper_api_model.py | 58 ++++++++++--------- src/subsai/models/whisper_model.py | 3 +- .../models/whisper_timestamped_model.py | 2 +- src/subsai/models/whispercpp_model.py | 2 +- 7 files changed, 45 insertions(+), 35 deletions(-) diff --git a/src/subsai/main.py b/src/subsai/main.py index df7f66d..1dfb9f8 100644 --- a/src/subsai/main.py +++ b/src/subsai/main.py @@ -107,7 +107,7 @@ def transcribe(media_file: str, model: Union[AbstractModel, str], model_config: :return: SSAFile: list of subtitles """ - if type(model) == str: + if isinstance(model, str): stt_model = SubsAI.create_model(model, model_config) else: stt_model = model @@ -142,7 +142,7 @@ def available_translation_languages(model: Union[str, TranslationModel]) -> list :param model: the name of the model :return: list of available languages """ - if type(model) == str: + if isinstance(model, str): langs = Tools.create_translation_model(model).available_languages() else: langs = model.available_languages() @@ -180,7 +180,7 @@ def translate(subs: SSAFile, :return: returns an `SSAFile` subtitles translated to the target language """ - if type(model) == str: + if isinstance(model, str): translation_model = Tools.create_translation_model(model_name=model, model_family=model_family) else: translation_model = model @@ -240,6 +240,11 @@ def auto_sync(subs: SSAFile, os.unlink(srtin_file.name) srtout_file.close() os.unlink(srtout_file.name) + if os.path.exists(srtin): + os.unlink(srtin) + if os.path.exists(srtout): + os.unlink(srtout) + @staticmethod def merge_subs_with_video(subs: Dict[str, SSAFile], media_file: str, diff --git a/src/subsai/models/faster_whisper_model.py b/src/subsai/models/faster_whisper_model.py index 62c2db1..4de6c21 100644 --- a/src/subsai/models/faster_whisper_model.py +++ b/src/subsai/models/faster_whisper_model.py @@ -251,7 +251,7 @@ def __init__(self, model_config): logging.basicConfig() logging.getLogger("faster_whisper").setLevel(logging.DEBUG) - def transcribe(self, media_file) -> str: + def transcribe(self, media_file) -> SSAFile: segments, info = self.model.transcribe(media_file, **self.transcribe_configs) subs = SSAFile() total_duration = round(info.duration, 2) # Same precision as the Whisper timestamps. diff --git a/src/subsai/models/whisperX_model.py b/src/subsai/models/whisperX_model.py index 4ee2b10..27d5759 100644 --- a/src/subsai/models/whisperX_model.py +++ b/src/subsai/models/whisperX_model.py @@ -126,7 +126,7 @@ def __init__(self, model_config): download_root=self.download_root, language=self.language) - def transcribe(self, media_file) -> str: + def transcribe(self, media_file) -> SSAFile: audio = whisperx.load_audio(media_file) result = self.model.transcribe(audio, batch_size=self.batch_size) model_a, metadata = whisperx.load_align_model(language_code=result["language"], device=self.device) diff --git a/src/subsai/models/whisper_api_model.py b/src/subsai/models/whisper_api_model.py index 795364f..a68c139 100644 --- a/src/subsai/models/whisper_api_model.py +++ b/src/subsai/models/whisper_api_model.py @@ -173,36 +173,40 @@ def _transcribe_chunk(self, chunk_data): os.remove(chunk_path) raise e - def transcribe(self, media_file: str) -> str: + def transcribe(self, media_file: str) -> SSAFile: audio_file_path = convert_video_to_audio_ffmpeg(media_file) - chunks = self.chunk_audio(audio_file_path) - - print(f"Processing {len(chunks)} audio chunks with {self.n_jobs} parallel jobs") - - # Prepare chunk data for parallel processing - chunk_data = [(i, chunk, offset) for i, (chunk, offset) in enumerate(chunks)] - - # Use parallel processing if n_jobs > 1, otherwise process sequentially - if self.n_jobs > 1: - # Use threading backend since API calls are I/O-bound - parallel_results = Parallel(n_jobs=self.n_jobs, backend="threading")( - delayed(self._transcribe_chunk)(data) for data in chunk_data - ) - else: - # Sequential processing for n_jobs=1 - parallel_results = [self._transcribe_chunk(data) for data in chunk_data] + try: + chunks = self.chunk_audio(audio_file_path) - # Sort results by chunk index to maintain order - parallel_results.sort(key=lambda x: x[0]) + print(f"Processing {len(chunks)} audio chunks with {self.n_jobs} parallel jobs") - # Process results and apply time offsets - results = "" - for i, result_text, offset in parallel_results: - # Shift subtitles by offset - result = SSAFile.from_string(result_text) - result.shift(ms=offset) - results += result.to_string("srt") + # Prepare chunk data for parallel processing + chunk_data = [(i, chunk, offset) for i, (chunk, offset) in enumerate(chunks)] - return SSAFile.from_string(results) + # Use parallel processing if n_jobs > 1, otherwise process sequentially + if self.n_jobs > 1: + # Use threading backend since API calls are I/O-bound + parallel_results = Parallel(n_jobs=self.n_jobs, backend="threading")( + delayed(self._transcribe_chunk)(data) for data in chunk_data + ) + else: + # Sequential processing for n_jobs=1 + parallel_results = [self._transcribe_chunk(data) for data in chunk_data] + + # Sort results by chunk index to maintain order + parallel_results.sort(key=lambda x: x[0]) + + # Process results and apply time offsets + results = "" + for i, result_text, offset in parallel_results: + # Shift subtitles by offset + result = SSAFile.from_string(result_text) + result.shift(ms=offset) + results += result.to_string("srt") + + return SSAFile.from_string(results) + finally: + if os.path.exists(audio_file_path): + os.unlink(audio_file_path) diff --git a/src/subsai/models/whisper_model.py b/src/subsai/models/whisper_model.py index e1f6f42..91ab817 100644 --- a/src/subsai/models/whisper_model.py +++ b/src/subsai/models/whisper_model.py @@ -9,6 +9,7 @@ from typing import Tuple import pysubs2 +from pysubs2 import SSAFile from subsai.models.abstract_model import AbstractModel import whisper from subsai.utils import _load_config, get_available_devices @@ -203,7 +204,7 @@ def __init__(self, model_config): download_root=self.download_root, in_memory=self.in_memory) - def transcribe(self, media_file) -> str: + def transcribe(self, media_file) -> SSAFile: audio = whisper.load_audio(media_file) result = self.model.transcribe(audio, verbose=self.verbose, diff --git a/src/subsai/models/whisper_timestamped_model.py b/src/subsai/models/whisper_timestamped_model.py index be54ff9..9ab5ce6 100644 --- a/src/subsai/models/whisper_timestamped_model.py +++ b/src/subsai/models/whisper_timestamped_model.py @@ -252,7 +252,7 @@ def __init__(self, model_config={}): download_root=self.download_root, in_memory=self.in_memory) - def transcribe(self, media_file) -> str: + def transcribe(self, media_file) -> SSAFile: audio = whisper_timestamped.load_audio(media_file) results = whisper_timestamped.transcribe(self.model, audio, verbose=self.verbose, diff --git a/src/subsai/models/whispercpp_model.py b/src/subsai/models/whispercpp_model.py index 7bc58fc..6329a3e 100644 --- a/src/subsai/models/whispercpp_model.py +++ b/src/subsai/models/whispercpp_model.py @@ -235,7 +235,7 @@ def __init__(self, model_config): self.model = Model(model=self.model_type, **self.params) - def transcribe(self, media_file) -> str: + def transcribe(self, media_file) -> SSAFile: segments = self.model.transcribe(media=media_file, **self.params) subs = SSAFile() for seg in segments: