diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54e7158d..7cf8c250 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,8 +67,9 @@ jobs: ~/Library/Caches/Buzz ~/.cache/whisper ~/.cache/huggingface + ~/.cache/Buzz ~/AppData/Local/Buzz/Buzz/Cache - key: whisper-models + key: whisper-models-${{ runner.os }} - uses: AnimMouse/setup-ffmpeg@v1 id: setup-ffmpeg @@ -88,7 +89,13 @@ jobs: if [ "$(lsb_release -rs)" == "22.04" ]; then sudo apt-get install libegl1-mesa + + # Add ubuntu-toolchain-r PPA for newer libstdc++6 with GLIBCXX_3.4.32 + sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + sudo apt-get update + sudo apt-get install -y gcc-13 g++-13 libstdc++-13-dev fi + sudo apt-get install libyaml-dev libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 libportaudio2 gettext libpulse0 libgl1-mesa-dev libvulkan-dev ccache if: "startsWith(matrix.os, 'ubuntu-')" @@ -166,7 +173,13 @@ jobs: if [ "$(lsb_release -rs)" == "22.04" ]; then sudo apt-get install libegl1-mesa + + # Add ubuntu-toolchain-r PPA for newer libstdc++6 with GLIBCXX_3.4.32 + sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y + sudo apt-get update + sudo apt-get install -y gcc-13 g++-13 libstdc++-13-dev fi + sudo apt-get install libyaml-dev libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-shape0 libxcb-cursor0 libportaudio2 gettext libpulse0 libgl1-mesa-dev libvulkan-dev ccache if: "startsWith(matrix.os, 'ubuntu-')" diff --git a/buzz/buzz.py b/buzz/buzz.py index 6c4750d6..289304b3 100644 --- a/buzz/buzz.py +++ b/buzz/buzz.py @@ -8,6 +8,9 @@ from typing import TextIO from platformdirs import user_log_dir, user_cache_dir, user_data_dir +# Will download all Huggingface data to the app cache directory +os.environ.setdefault("HF_HOME", user_cache_dir("Buzz")) + from buzz.assets import APP_BASE_DIR # Check for segfaults if not running in frozen mode @@ -60,6 +63,7 @@ def main(): logging.getLogger("matplotlib").setLevel(logging.WARNING) logging.getLogger("graphviz").setLevel(logging.WARNING) logging.getLogger("nemo_logger").setLevel(logging.ERROR) + logging.getLogger("nemo_logging").setLevel(logging.ERROR) logging.getLogger("numba").setLevel(logging.WARNING) logging.getLogger("torio._extension.utils").setLevel(logging.WARNING) logging.getLogger("export_config_manager").setLevel(logging.WARNING) diff --git a/buzz/db/db.py b/buzz/db/db.py index 99b4c20b..05c038bf 100644 --- a/buzz/db/db.py +++ b/buzz/db/db.py @@ -41,3 +41,11 @@ def _setup_db(path: str) -> QSqlDatabase: db.exec('PRAGMA foreign_keys = ON') logging.debug("Database connection opened: %s", db.databaseName()) return db + + +def close_app_db(): + db = QSqlDatabase.database() + if db.isOpen(): + logging.debug("Closing database connection: %s", db.databaseName()) + db.close() + QSqlDatabase.removeDatabase(QSqlDatabase.defaultConnection) diff --git a/buzz/transcriber/whisper_file_transcriber.py b/buzz/transcriber/whisper_file_transcriber.py index c5533397..08a20426 100644 --- a/buzz/transcriber/whisper_file_transcriber.py +++ b/buzz/transcriber/whisper_file_transcriber.py @@ -273,27 +273,29 @@ class WhisperFileTranscriber(FileTranscriber): if self.started_process: self.current_process.terminate() - # Use timeout to avoid hanging indefinitely + + if self.read_line_thread and self.read_line_thread.is_alive(): + self.read_line_thread.join(timeout=5) + if self.read_line_thread.is_alive(): + logging.warning("Read line thread still alive after 5s") + self.current_process.join(timeout=10) if self.current_process.is_alive(): logging.warning("Process didn't terminate gracefully, force killing") self.current_process.kill() self.current_process.join(timeout=5) - - # Close pipes to unblock the read_line thread + try: - if hasattr(self, 'send_pipe'): + if hasattr(self, 'send_pipe') and self.send_pipe: self.send_pipe.close() - if hasattr(self, 'recv_pipe'): + except Exception as e: + logging.debug(f"Error closing send_pipe: {e}") + + try: + if hasattr(self, 'recv_pipe') and self.recv_pipe: self.recv_pipe.close() except Exception as e: - logging.debug(f"Error closing pipes: {e}") - - # Join read_line_thread with timeout to prevent hanging - if self.read_line_thread and self.read_line_thread.is_alive(): - self.read_line_thread.join(timeout=5) - if self.read_line_thread.is_alive(): - logging.warning("Read line thread didn't terminate gracefully") + logging.debug(f"Error closing recv_pipe: {e}") def read_line(self, pipe: Connection): while True: diff --git a/buzz/widgets/application.py b/buzz/widgets/application.py index 80c9c595..69af8f9c 100755 --- a/buzz/widgets/application.py +++ b/buzz/widgets/application.py @@ -56,9 +56,9 @@ class Application(QApplication): else: self.setFont(QFont(self.font().family(), font_size)) - db = setup_app_db() + self.db = setup_app_db() transcription_service = TranscriptionService( - TranscriptionDAO(db), TranscriptionSegmentDAO(db) + TranscriptionDAO(self.db), TranscriptionSegmentDAO(self.db) ) self.window = MainWindow(transcription_service) @@ -91,3 +91,7 @@ class Application(QApplication): def add_task(self, task: FileTranscriptionTask, quit_on_complete: bool = False): self.window.quit_on_complete = quit_on_complete self.window.add_task(task) + + def close_database(self): + from buzz.db.db import close_app_db + close_app_db() diff --git a/buzz/widgets/main_window.py b/buzz/widgets/main_window.py index 8c605f94..306bc3f8 100644 --- a/buzz/widgets/main_window.py +++ b/buzz/widgets/main_window.py @@ -421,19 +421,50 @@ class MainWindow(QMainWindow): self.save_geometry() def closeEvent(self, event: QtGui.QCloseEvent) -> None: + logging.debug("Starting MainWindow closeEvent") + self.save_geometry() + self.settings.settings.sync() + + if self.folder_watcher: + try: + self.folder_watcher.task_found.disconnect() + if len(self.folder_watcher.directories()) > 0: + self.folder_watcher.removePaths(self.folder_watcher.directories()) + except Exception as e: + logging.warning(f"Error cleaning up folder watcher: {e}") + + try: + self.transcriber_worker.task_started.disconnect() + self.transcriber_worker.task_progress.disconnect() + self.transcriber_worker.task_download_progress.disconnect() + self.transcriber_worker.task_error.disconnect() + self.transcriber_worker.task_completed.disconnect() + except Exception as e: + logging.warning(f"Error disconnecting signals: {e}") self.transcriber_worker.stop() self.transcriber_thread.quit() - # Only wait if thread is actually running + if self.transcriber_thread.isRunning(): - if not self.transcriber_thread.wait(5000): # Wait up to 5 seconds - logging.warning("Transcriber thread did not finish within timeout") + if not self.transcriber_thread.wait(10000): + logging.warning("Transcriber thread did not finish within 10s timeout, terminating") + self.transcriber_thread.terminate() + if not self.transcriber_thread.wait(2000): + logging.error("Transcriber thread could not be terminated") if self.transcription_viewer_widget is not None: self.transcription_viewer_widget.close() - logging.debug("Closing MainWindow") + try: + from buzz.widgets.application import Application + app = Application.instance() + if app and hasattr(app, 'close_database'): + app.close_database() + except Exception as e: + logging.warning(f"Error closing database: {e}") + + logging.debug("MainWindow closeEvent completed") super().closeEvent(event) diff --git a/buzz/widgets/transcription_viewer/speaker_identification_widget.py b/buzz/widgets/transcription_viewer/speaker_identification_widget.py index cbbe6216..97bdce3d 100644 --- a/buzz/widgets/transcription_viewer/speaker_identification_widget.py +++ b/buzz/widgets/transcription_viewer/speaker_identification_widget.py @@ -150,6 +150,15 @@ class IdentificationWorker(QObject): # Step 3 - Diarization self.progress_update.emit(_("6/8 Identifying speakers")) + # Silence NeMo's verbose logging + logging.getLogger("nemo_logging").setLevel(logging.ERROR) + try: + # Also try to silence NeMo's internal logging system + from nemo.utils import logging as nemo_logging + nemo_logging.setLevel(logging.ERROR) + except (ImportError, AttributeError): + pass + try: diarizer_model = MSDDDiarizer(device) speaker_ts = diarizer_model.diarize(torch.from_numpy(audio_waveform).unsqueeze(0)) @@ -228,6 +237,7 @@ class SpeakerIdentificationWidget(QWidget): self.thread = None self.worker = None + self.needs_layout_update = False self.setMinimumWidth(650) self.setMinimumHeight(400) @@ -403,6 +413,12 @@ class SpeakerIdentificationWidget(QWidget): self.speaker_preview_row.addLayout(speaker_layout) + # Trigger layout update to properly size the new widgets + self.layout().activate() + self.adjustSize() + # Schedule update if window is minimized + self.needs_layout_update = True + def on_speaker_preview(self, speaker_id): if self.player_timer: self.player_timer.stop() @@ -498,6 +514,15 @@ class SpeakerIdentificationWidget(QWidget): self.close() + def changeEvent(self, event): + super().changeEvent(event) + + # Handle window activation (restored from minimized or brought to front) + if self.needs_layout_update: + self.layout().activate() + self.adjustSize() + self.needs_layout_update = False + def closeEvent(self, event): self.hide()