From abf30a34df7a1d6e06077938f59bb104e810b2bc Mon Sep 17 00:00:00 2001 From: Chidi Williams Date: Mon, 9 Jan 2023 10:05:38 +0000 Subject: [PATCH] Fix recording window shutting down (#326) --- .coveragerc | 2 +- buzz/gui.py | 16 ++++---- buzz/model_loader.py | 4 ++ buzz/transcriber.py | 4 +- tests/gui_test.py | 86 ++++++++++++--------------------------- tests/mock_sounddevice.py | 34 ++++++++++++++++ 6 files changed, 78 insertions(+), 68 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6566e22..9843aa0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,4 +7,4 @@ omit = directory = coverage/html [report] -fail_under = 72 +fail_under = 78 diff --git a/buzz/gui.py b/buzz/gui.py index 9bb421d..df0b08f 100644 --- a/buzz/gui.py +++ b/buzz/gui.py @@ -459,7 +459,7 @@ class AudioMeterWidget(QWidget): self.repaint() -class RecordingTranscriberWidget(QDialog): +class RecordingTranscriberWidget(QWidget): current_status: 'RecordingStatus' transcription_options: TranscriptionOptions selected_device_id: Optional[int] @@ -474,9 +474,12 @@ class RecordingTranscriberWidget(QDialog): STOPPED = auto() RECORDING = auto() - def __init__(self, parent: Optional[QWidget] = None) -> None: + def __init__(self, parent: Optional[QWidget] = None, flags: Optional[Qt.WindowType] = None) -> None: super().__init__(parent) + if flags is not None: + self.setWindowFlags(flags) + layout = QVBoxLayout(self) self.current_status = self.RecordingStatus.STOPPED @@ -532,7 +535,6 @@ class RecordingTranscriberWidget(QDialog): def reset_recording_amplitude_listener(self): if self.recording_amplitude_listener is not None: self.recording_amplitude_listener.stop_recording() - self.recording_amplitude_listener.deleteLater() # Listening to audio will fail if there are no input devices if self.selected_device_id is None or self.selected_device_id == -1: @@ -777,7 +779,7 @@ class TranscriptionTasksTableWidget(QTableWidget): self.verticalHeader().hide() self.setHorizontalHeaderLabels([_('ID'), _('File Name'), _('Status')]) - self.horizontalHeader().setMinimumSectionSize(140) + self.horizontalHeader().setMinimumSectionSize(160) self.horizontalHeader().setSectionResizeMode(self.FILE_NAME_COLUMN_INDEX, QHeaderView.ResizeMode.Stretch) @@ -904,8 +906,8 @@ class MainWindowToolbar(QToolBar): return QIcon(pixmap) def on_record_action_triggered(self): - recording_transcriber_window = RecordingTranscriberWidget(self) - recording_transcriber_window.exec() + recording_transcriber_window = RecordingTranscriberWidget(self, flags=Qt.WindowType.Window) + recording_transcriber_window.show() def set_stop_transcription_action_enabled(self, enabled: bool): self.stop_transcription_action.setEnabled(enabled) @@ -927,7 +929,7 @@ class MainWindow(QMainWindow): self.setWindowTitle(APP_NAME) self.setWindowIcon(QIcon(BUZZ_ICON_PATH)) - self.setMinimumSize(400, 400) + self.setMinimumSize(450, 400) self.tasks_cache = tasks_cache diff --git a/buzz/model_loader.py b/buzz/model_loader.py index 821e9fc..c5b7b28 100644 --- a/buzz/model_loader.py +++ b/buzz/model_loader.py @@ -99,6 +99,8 @@ class ModelLoader(QObject): def download_model(self, url: str, file_path: str, expected_sha256: Optional[str]): try: + logging.debug(f'Downloading model from {url} to {file_path}') + os.makedirs(os.path.dirname(file_path), exist_ok=True) if os.path.exists(file_path) and not os.path.isfile(file_path): @@ -138,6 +140,8 @@ class ModelLoader(QObject): "Model has been downloaded but the SHA256 checksum does not match. Please retry loading the " "model.") + logging.debug('Downloaded model') + return file_path except RuntimeError as exc: self.error.emit(str(exc)) diff --git a/buzz/transcriber.py b/buzz/transcriber.py index 7b5e484..bd3362d 100644 --- a/buzz/transcriber.py +++ b/buzz/transcriber.py @@ -588,7 +588,7 @@ class FileTranscriberQueueWorker(QObject): @pyqtSlot() def run(self): - logging.debug('Waiting for next file transcription task') + logging.debug('Waiting for next transcription task') # Waiting for new tasks in a loop instead of with queue.wait() # resolves a "No Python frame" crash when the thread is quit. @@ -608,6 +608,8 @@ class FileTranscriberQueueWorker(QObject): except queue.Empty: continue + logging.debug('Starting next transcription task') + if self.current_task.transcription_options.model.model_type == ModelType.WHISPER_CPP: self.current_transcriber = WhisperCppFileTranscriber( task=self.current_task) diff --git a/tests/gui_test.py b/tests/gui_test.py index 01cd934..97d00c6 100644 --- a/tests/gui_test.py +++ b/tests/gui_test.py @@ -1,6 +1,7 @@ import logging import os.path import pathlib +import platform from typing import List from unittest.mock import Mock, patch @@ -23,10 +24,20 @@ from buzz.gui import (AboutDialog, AdvancedSettingsDialog, AudioDevicesComboBox, from buzz.model_loader import ModelType from buzz.transcriber import (FileTranscriptionOptions, FileTranscriptionTask, Segment, TranscriptionOptions) -from tests.mock_sounddevice import MockInputStream +from tests.mock_sounddevice import MockInputStream, mock_query_devices from .mock_qt import MockNetworkAccessManager, MockNetworkReply +@pytest.fixture(scope='module', autouse=True) +def audio_setup(): + with patch('sounddevice.query_devices') as query_devices_mock, \ + patch('sounddevice.InputStream', side_effect=MockInputStream), \ + patch('sounddevice.check_input_settings'): + query_devices_mock.return_value = mock_query_devices + sounddevice.default.device = 3, 4 + yield + + class TestLanguagesComboBox: def test_should_show_sorted_whisper_languages(self, qtbot): @@ -51,64 +62,22 @@ class TestLanguagesComboBox: class TestAudioDevicesComboBox: - mock_query_devices = [ - {'name': 'Background Music', 'index': 0, 'hostapi': 0, 'max_input_channels': 2, 'max_output_channels': 2, - 'default_low_input_latency': 0.01, - 'default_low_output_latency': 0.008, 'default_high_input_latency': 0.1, 'default_high_output_latency': 0.064, - 'default_samplerate': 8000.0}, - {'name': 'Background Music (UI Sounds)', 'index': 1, 'hostapi': 0, 'max_input_channels': 2, - 'max_output_channels': 2, 'default_low_input_latency': 0.01, - 'default_low_output_latency': 0.008, 'default_high_input_latency': 0.1, 'default_high_output_latency': 0.064, - 'default_samplerate': 8000.0}, - {'name': 'BlackHole 2ch', 'index': 2, 'hostapi': 0, 'max_input_channels': 2, 'max_output_channels': 2, - 'default_low_input_latency': 0.01, - 'default_low_output_latency': 0.0013333333333333333, 'default_high_input_latency': 0.1, - 'default_high_output_latency': 0.010666666666666666, 'default_samplerate': 48000.0}, - {'name': 'MacBook Pro Microphone', 'index': 3, 'hostapi': 0, 'max_input_channels': 1, 'max_output_channels': 0, - 'default_low_input_latency': 0.034520833333333334, - 'default_low_output_latency': 0.01, 'default_high_input_latency': 0.043854166666666666, - 'default_high_output_latency': 0.1, 'default_samplerate': 48000.0}, - {'name': 'MacBook Pro Speakers', 'index': 4, 'hostapi': 0, 'max_input_channels': 0, 'max_output_channels': 2, - 'default_low_input_latency': 0.01, - 'default_low_output_latency': 0.0070416666666666666, 'default_high_input_latency': 0.1, - 'default_high_output_latency': 0.016375, 'default_samplerate': 48000.0}, - {'name': 'Null Audio Device', 'index': 5, 'hostapi': 0, 'max_input_channels': 2, 'max_output_channels': 2, - 'default_low_input_latency': 0.01, - 'default_low_output_latency': 0.0014512471655328798, 'default_high_input_latency': 0.1, - 'default_high_output_latency': 0.011609977324263039, 'default_samplerate': 44100.0}, - {'name': 'Multi-Output Device', 'index': 6, 'hostapi': 0, 'max_input_channels': 0, 'max_output_channels': 2, - 'default_low_input_latency': 0.01, - 'default_low_output_latency': 0.0033333333333333335, 'default_high_input_latency': 0.1, - 'default_high_output_latency': 0.012666666666666666, 'default_samplerate': 48000.0}, - ] - def test_get_devices(self): - with patch('sounddevice.query_devices') as query_devices_mock: - query_devices_mock.return_value = self.mock_query_devices + audio_devices_combo_box = AudioDevicesComboBox() - sounddevice.default.device = 3, 4 + assert audio_devices_combo_box.itemText(0) == 'Background Music' + assert audio_devices_combo_box.itemText(1) == 'Background Music (UI Sounds)' + assert audio_devices_combo_box.itemText(2) == 'BlackHole 2ch' + assert audio_devices_combo_box.itemText(3) == 'MacBook Pro Microphone' + assert audio_devices_combo_box.itemText(4) == 'Null Audio Device' - audio_devices_combo_box = AudioDevicesComboBox() - - assert audio_devices_combo_box.itemText(0) == 'Background Music' - assert audio_devices_combo_box.itemText( - 1) == 'Background Music (UI Sounds)' - assert audio_devices_combo_box.itemText(2) == 'BlackHole 2ch' - assert audio_devices_combo_box.itemText( - 3) == 'MacBook Pro Microphone' - assert audio_devices_combo_box.itemText(4) == 'Null Audio Device' - - assert audio_devices_combo_box.currentText() == 'MacBook Pro Microphone' + assert audio_devices_combo_box.currentText() == 'MacBook Pro Microphone' def test_select_default_mic_when_no_default(self): - with patch('sounddevice.query_devices') as query_devices_mock: - query_devices_mock.return_value = self.mock_query_devices + sounddevice.default.device = -1, 1 - sounddevice.default.device = -1, 1 - - audio_devices_combo_box = AudioDevicesComboBox() - - assert audio_devices_combo_box.currentText() == 'Background Music' + audio_devices_combo_box = AudioDevicesComboBox() + assert audio_devices_combo_box.currentText() == 'Background Music' class TestDownloadModelProgressDialog: @@ -174,6 +143,7 @@ class TestMainWindow: assert window.windowIcon().pixmap(QSize(64, 64)).isNull() is False window.close() + @pytest.mark.xfail(condition=platform.system() == 'Windows', reason='Timing out') def test_should_run_transcription_task(self, qtbot: QtBot, tasks_cache): window = MainWindow(tasks_cache=tasks_cache) qtbot.add_widget(window) @@ -184,7 +154,7 @@ class TestMainWindow: assert open_transcript_action.isEnabled() is False table_widget: QTableWidget = window.findChild(QTableWidget) - qtbot.wait_until(self.assert_task_status(table_widget, 0, 'Completed'), timeout=60 * 1000) + qtbot.wait_until(self.assert_task_status(table_widget, 0, 'Completed'), timeout=2 * 60 * 1000) table_widget.setCurrentIndex(table_widget.indexFromItem(table_widget.item(0, 1))) assert open_transcript_action.isEnabled() @@ -202,7 +172,7 @@ class TestMainWindow: assert table_widget.item(0, 1).text() == 'whisper-french.mp3' assert 'In Progress' in table_widget.item(0, 2).text() - qtbot.wait_until(assert_task_in_progress, timeout=60 * 1000) + qtbot.wait_until(assert_task_in_progress, timeout=2 * 60 * 1000) # Stop task in progress table_widget.selectRow(0) @@ -420,7 +390,6 @@ class TestRecordingTranscriberWidget: qtbot.add_widget(widget) assert widget.windowTitle() == 'Live Recording' - @pytest.mark.skip() def test_should_transcribe(self, qtbot): widget = RecordingTranscriberWidget() qtbot.add_widget(widget) @@ -428,9 +397,8 @@ class TestRecordingTranscriberWidget: def assert_text_box_contains_text(): assert len(widget.text_box.toPlainText()) > 0 - with patch('sounddevice.InputStream', side_effect=MockInputStream), patch('sounddevice.check_input_settings'): - widget.record_button.click() - qtbot.wait_until(callback=assert_text_box_contains_text, timeout=60 * 1000) + widget.record_button.click() + qtbot.wait_until(callback=assert_text_box_contains_text, timeout=60 * 1000) with qtbot.wait_signal(widget.transcription_thread.finished, timeout=60 * 1000): widget.stop_recording() diff --git a/tests/mock_sounddevice.py b/tests/mock_sounddevice.py index 72859b9..299cb47 100644 --- a/tests/mock_sounddevice.py +++ b/tests/mock_sounddevice.py @@ -8,6 +8,37 @@ import numpy as np import sounddevice import whisper +mock_query_devices = [ + {'name': 'Background Music', 'index': 0, 'hostapi': 0, 'max_input_channels': 2, 'max_output_channels': 2, + 'default_low_input_latency': 0.01, + 'default_low_output_latency': 0.008, 'default_high_input_latency': 0.1, 'default_high_output_latency': 0.064, + 'default_samplerate': 8000.0}, + {'name': 'Background Music (UI Sounds)', 'index': 1, 'hostapi': 0, 'max_input_channels': 2, + 'max_output_channels': 2, 'default_low_input_latency': 0.01, + 'default_low_output_latency': 0.008, 'default_high_input_latency': 0.1, 'default_high_output_latency': 0.064, + 'default_samplerate': 8000.0}, + {'name': 'BlackHole 2ch', 'index': 2, 'hostapi': 0, 'max_input_channels': 2, 'max_output_channels': 2, + 'default_low_input_latency': 0.01, + 'default_low_output_latency': 0.0013333333333333333, 'default_high_input_latency': 0.1, + 'default_high_output_latency': 0.010666666666666666, 'default_samplerate': 48000.0}, + {'name': 'MacBook Pro Microphone', 'index': 3, 'hostapi': 0, 'max_input_channels': 1, 'max_output_channels': 0, + 'default_low_input_latency': 0.034520833333333334, + 'default_low_output_latency': 0.01, 'default_high_input_latency': 0.043854166666666666, + 'default_high_output_latency': 0.1, 'default_samplerate': 48000.0}, + {'name': 'MacBook Pro Speakers', 'index': 4, 'hostapi': 0, 'max_input_channels': 0, 'max_output_channels': 2, + 'default_low_input_latency': 0.01, + 'default_low_output_latency': 0.0070416666666666666, 'default_high_input_latency': 0.1, + 'default_high_output_latency': 0.016375, 'default_samplerate': 48000.0}, + {'name': 'Null Audio Device', 'index': 5, 'hostapi': 0, 'max_input_channels': 2, 'max_output_channels': 2, + 'default_low_input_latency': 0.01, + 'default_low_output_latency': 0.0014512471655328798, 'default_high_input_latency': 0.1, + 'default_high_output_latency': 0.011609977324263039, 'default_samplerate': 44100.0}, + {'name': 'Multi-Output Device', 'index': 6, 'hostapi': 0, 'max_input_channels': 0, 'max_output_channels': 2, + 'default_low_input_latency': 0.01, + 'default_low_output_latency': 0.0033333333333333335, 'default_high_input_latency': 0.1, + 'default_high_output_latency': 0.012666666666666666, 'default_samplerate': 48000.0}, +] + class MockInputStream(MagicMock): running = False @@ -46,6 +77,9 @@ class MockInputStream(MagicMock): self.running = False self.thread.join() + def close(self): + pass + def __enter__(self): self.start()