diff --git a/buzz/transcriber/local_whisper_cpp_server_transcriber.py b/buzz/transcriber/local_whisper_cpp_server_transcriber.py index c58553d9..d57252fe 100644 --- a/buzz/transcriber/local_whisper_cpp_server_transcriber.py +++ b/buzz/transcriber/local_whisper_cpp_server_transcriber.py @@ -64,7 +64,8 @@ class LocalWhisperCppServerTranscriber(OpenAIWhisperAPIFileTranscriber): self.openai_client = OpenAI( api_key="not-used", - base_url="http://127.0.0.1:3000" + base_url="http://127.0.0.1:3000", + max_retries=0 ) def transcribe(self) -> List[Segment]: diff --git a/buzz/transcriber/openai_whisper_api_file_transcriber.py b/buzz/transcriber/openai_whisper_api_file_transcriber.py index 21a6652f..b2f02898 100644 --- a/buzz/transcriber/openai_whisper_api_file_transcriber.py +++ b/buzz/transcriber/openai_whisper_api_file_transcriber.py @@ -46,7 +46,8 @@ class OpenAIWhisperAPIFileTranscriber(FileTranscriber): self.task = task.transcription_options.task self.openai_client = OpenAI( api_key=self.transcription_task.transcription_options.openai_access_token, - base_url=custom_openai_base_url if custom_openai_base_url else None + base_url=custom_openai_base_url if custom_openai_base_url else None, + max_retries=0 ) self.whisper_api_model = get_custom_api_whisper_model(custom_openai_base_url) self.word_level_timings = self.transcription_task.transcription_options.word_level_timings diff --git a/buzz/transcriber/recording_transcriber.py b/buzz/transcriber/recording_transcriber.py index 8e5cc3d1..7867e50e 100644 --- a/buzz/transcriber/recording_transcriber.py +++ b/buzz/transcriber/recording_transcriber.py @@ -126,7 +126,8 @@ class RecordingTranscriber(QObject): self.whisper_api_model = get_custom_api_whisper_model(custom_openai_base_url) self.openai_client = OpenAI( api_key=self.transcription_options.openai_access_token, - base_url=custom_openai_base_url if custom_openai_base_url else None + base_url=custom_openai_base_url if custom_openai_base_url else None, + max_retries=0 ) logging.debug("Will use whisper API on %s, %s", custom_openai_base_url, self.whisper_api_model) diff --git a/buzz/translator.py b/buzz/translator.py index 56a816ea..ffeecf7b 100644 --- a/buzz/translator.py +++ b/buzz/translator.py @@ -3,7 +3,7 @@ import logging import queue from typing import Optional -from openai import OpenAI +from openai import OpenAI, max_retries from PyQt6.QtCore import QObject, pyqtSignal from buzz.settings.settings import Settings @@ -15,7 +15,6 @@ from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDi class Translator(QObject): translation = pyqtSignal(str, int) finished = pyqtSignal() - is_running = False def __init__( self, @@ -48,19 +47,22 @@ class Translator(QObject): ) self.openai_client = OpenAI( api_key=openai_api_key, - base_url=custom_openai_base_url if custom_openai_base_url else None + base_url=custom_openai_base_url if custom_openai_base_url else None, + max_retries=0 ) def start(self): logging.debug("Starting translation queue") - self.is_running = True + while True: + item = self.queue.get() # Block until item available - while self.is_running: - try: - transcript, transcript_id = self.queue.get(timeout=1) - except queue.Empty: - continue + # Check for sentinel value (None means stop) + if item is None: + logging.debug("Translation queue received stop signal") + break + + transcript, transcript_id = item try: completion = self.openai_client.chat.completions.create( @@ -69,7 +71,8 @@ class Translator(QObject): {"role": "system", "content": self.transcription_options.llm_prompt}, {"role": "user", "content": transcript} ], - timeout=30.0 + timeout=30.0, + ) except Exception as e: completion = None @@ -84,6 +87,7 @@ class Translator(QObject): self.translation.emit(next_translation, transcript_id) + logging.debug("Translation queue stopped") self.finished.emit() def on_transcription_options_changed( @@ -95,4 +99,5 @@ class Translator(QObject): self.queue.put((transcript, transcript_id)) def stop(self): - self.is_running = False + # Send sentinel value to unblock and stop the worker thread + self.queue.put(None) diff --git a/buzz/widgets/preferences_dialog/general_preferences_widget.py b/buzz/widgets/preferences_dialog/general_preferences_widget.py index 5cefcdaa..b7bdfc74 100644 --- a/buzz/widgets/preferences_dialog/general_preferences_widget.py +++ b/buzz/widgets/preferences_dialog/general_preferences_widget.py @@ -328,7 +328,7 @@ class ValidateOpenAIApiKeyJob(QRunnable): client = OpenAI( api_key=self.api_key, base_url=custom_openai_base_url if custom_openai_base_url else None, - timeout=5, + timeout=15, ) client.models.list() self.signals.success.emit() diff --git a/buzz/widgets/transcription_viewer/transcription_viewer_widget.py b/buzz/widgets/transcription_viewer/transcription_viewer_widget.py index bf4400b3..e77c2179 100644 --- a/buzz/widgets/transcription_viewer/transcription_viewer_widget.py +++ b/buzz/widgets/transcription_viewer/transcription_viewer_widget.py @@ -1351,8 +1351,15 @@ class TranscriptionViewerWidget(QWidget): # Only wait if thread is actually running if self.translation_thread.isRunning(): - if not self.translation_thread.wait(45_000): - logging.warning("Translation thread did not finish within timeout") + # Wait up to 35 seconds for graceful shutdown + # (30s max API call timeout + 5s buffer) + if not self.translation_thread.wait(35_000): + logging.warning("Translation thread did not finish gracefully, terminating") + # Force terminate the thread if it doesn't stop + self.translation_thread.terminate() + # Give it a brief moment to terminate + if not self.translation_thread.wait(1_000): + logging.error("Translation thread could not be terminated") super().closeEvent(event) diff --git a/tests/translator_test.py b/tests/translator_test.py index 6c0f87d6..c9b4d8e3 100644 --- a/tests/translator_test.py +++ b/tests/translator_test.py @@ -15,14 +15,12 @@ class TestTranslator: @patch('buzz.translator.queue.Queue', autospec=True) def test_start(self, mock_queue, mock_openai, qtbot): def side_effect(*args, **kwargs): - side_effect.call_count += 1 + if side_effect.call_count <= 1: + side_effect.call_count += 1 + return ("Hello, how are you?", 1) - if side_effect.call_count >= 5: - translator.is_running = False - - if side_effect.call_count < 3: - raise Empty - return "Hello, how are you?", None + # Finally return sentinel to stop + return None side_effect.call_count = 0 @@ -51,6 +49,8 @@ class TestTranslator: mock_queue.get.assert_called() mock_chat.completions.create.assert_called() + translator.stop() + @patch('buzz.translator.OpenAI', autospec=True) def test_translator(self, mock_openai, qtbot): @@ -94,8 +94,7 @@ class TestTranslator: self.translation_thread.start() - time.sleep(3) - assert self.translator.is_running + time.sleep(1) # Give thread time to start self.translator.enqueue("Hello, how are you?") diff --git a/tests/widgets/transcription_viewer/transcription_viewer_widget_additional_test.py b/tests/widgets/transcription_viewer/transcription_viewer_widget_additional_test.py index c007caf4..9e716e7a 100644 --- a/tests/widgets/transcription_viewer/transcription_viewer_widget_additional_test.py +++ b/tests/widgets/transcription_viewer/transcription_viewer_widget_additional_test.py @@ -778,24 +778,24 @@ class TestTranscriptionViewerWidgetAdditional: widget.close() - # Skipped as it seems it is sending actual requests and maybe failing on CI - # def test_run_translation(self, qtbot: QtBot, transcription, transcription_service, shortcuts): - # """Test run_translation method""" - # widget = TranscriptionViewerWidget( - # transcription, transcription_service, shortcuts - # ) - # qtbot.add_widget(widget) - # - # # Set required options - # widget.transcription_options.llm_model = "gpt-4" - # widget.transcription_options.llm_prompt = "Translate" - # - # widget.run_translation() - # - # # Should enqueue translation tasks - # assert hasattr(widget, 'run_translation') - # - # widget.close() + # TODO - it is sending actual requests, should mock + def test_run_translation(self, qtbot: QtBot, transcription, transcription_service, shortcuts): + """Test run_translation method""" + widget = TranscriptionViewerWidget( + transcription, transcription_service, shortcuts + ) + qtbot.add_widget(widget) + + # Set required options + widget.transcription_options.llm_model = "gpt-4" + widget.transcription_options.llm_prompt = "Translate" + + widget.run_translation() + + # Should enqueue translation tasks + assert hasattr(widget, 'run_translation') + + widget.close() def test_restore_ui_state(self, qtbot: QtBot, transcription, transcription_service, shortcuts): """Test restore_ui_state method"""