Fix recording window shutting down (#326)

This commit is contained in:
Chidi Williams 2023-01-09 10:05:38 +00:00 committed by GitHub
parent 70ad8f9b1d
commit abf30a34df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 68 deletions

View file

@ -7,4 +7,4 @@ omit =
directory = coverage/html directory = coverage/html
[report] [report]
fail_under = 72 fail_under = 78

View file

@ -459,7 +459,7 @@ class AudioMeterWidget(QWidget):
self.repaint() self.repaint()
class RecordingTranscriberWidget(QDialog): class RecordingTranscriberWidget(QWidget):
current_status: 'RecordingStatus' current_status: 'RecordingStatus'
transcription_options: TranscriptionOptions transcription_options: TranscriptionOptions
selected_device_id: Optional[int] selected_device_id: Optional[int]
@ -474,9 +474,12 @@ class RecordingTranscriberWidget(QDialog):
STOPPED = auto() STOPPED = auto()
RECORDING = 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) super().__init__(parent)
if flags is not None:
self.setWindowFlags(flags)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
self.current_status = self.RecordingStatus.STOPPED self.current_status = self.RecordingStatus.STOPPED
@ -532,7 +535,6 @@ class RecordingTranscriberWidget(QDialog):
def reset_recording_amplitude_listener(self): def reset_recording_amplitude_listener(self):
if self.recording_amplitude_listener is not None: if self.recording_amplitude_listener is not None:
self.recording_amplitude_listener.stop_recording() self.recording_amplitude_listener.stop_recording()
self.recording_amplitude_listener.deleteLater()
# Listening to audio will fail if there are no input devices # Listening to audio will fail if there are no input devices
if self.selected_device_id is None or self.selected_device_id == -1: if self.selected_device_id is None or self.selected_device_id == -1:
@ -777,7 +779,7 @@ class TranscriptionTasksTableWidget(QTableWidget):
self.verticalHeader().hide() self.verticalHeader().hide()
self.setHorizontalHeaderLabels([_('ID'), _('File Name'), _('Status')]) self.setHorizontalHeaderLabels([_('ID'), _('File Name'), _('Status')])
self.horizontalHeader().setMinimumSectionSize(140) self.horizontalHeader().setMinimumSectionSize(160)
self.horizontalHeader().setSectionResizeMode(self.FILE_NAME_COLUMN_INDEX, self.horizontalHeader().setSectionResizeMode(self.FILE_NAME_COLUMN_INDEX,
QHeaderView.ResizeMode.Stretch) QHeaderView.ResizeMode.Stretch)
@ -904,8 +906,8 @@ class MainWindowToolbar(QToolBar):
return QIcon(pixmap) return QIcon(pixmap)
def on_record_action_triggered(self): def on_record_action_triggered(self):
recording_transcriber_window = RecordingTranscriberWidget(self) recording_transcriber_window = RecordingTranscriberWidget(self, flags=Qt.WindowType.Window)
recording_transcriber_window.exec() recording_transcriber_window.show()
def set_stop_transcription_action_enabled(self, enabled: bool): def set_stop_transcription_action_enabled(self, enabled: bool):
self.stop_transcription_action.setEnabled(enabled) self.stop_transcription_action.setEnabled(enabled)
@ -927,7 +929,7 @@ class MainWindow(QMainWindow):
self.setWindowTitle(APP_NAME) self.setWindowTitle(APP_NAME)
self.setWindowIcon(QIcon(BUZZ_ICON_PATH)) self.setWindowIcon(QIcon(BUZZ_ICON_PATH))
self.setMinimumSize(400, 400) self.setMinimumSize(450, 400)
self.tasks_cache = tasks_cache self.tasks_cache = tasks_cache

View file

@ -99,6 +99,8 @@ class ModelLoader(QObject):
def download_model(self, url: str, file_path: str, expected_sha256: Optional[str]): def download_model(self, url: str, file_path: str, expected_sha256: Optional[str]):
try: try:
logging.debug(f'Downloading model from {url} to {file_path}')
os.makedirs(os.path.dirname(file_path), exist_ok=True) os.makedirs(os.path.dirname(file_path), exist_ok=True)
if os.path.exists(file_path) and not os.path.isfile(file_path): 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 has been downloaded but the SHA256 checksum does not match. Please retry loading the "
"model.") "model.")
logging.debug('Downloaded model')
return file_path return file_path
except RuntimeError as exc: except RuntimeError as exc:
self.error.emit(str(exc)) self.error.emit(str(exc))

View file

@ -588,7 +588,7 @@ class FileTranscriberQueueWorker(QObject):
@pyqtSlot() @pyqtSlot()
def run(self): 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() # Waiting for new tasks in a loop instead of with queue.wait()
# resolves a "No Python frame" crash when the thread is quit. # resolves a "No Python frame" crash when the thread is quit.
@ -608,6 +608,8 @@ class FileTranscriberQueueWorker(QObject):
except queue.Empty: except queue.Empty:
continue continue
logging.debug('Starting next transcription task')
if self.current_task.transcription_options.model.model_type == ModelType.WHISPER_CPP: if self.current_task.transcription_options.model.model_type == ModelType.WHISPER_CPP:
self.current_transcriber = WhisperCppFileTranscriber( self.current_transcriber = WhisperCppFileTranscriber(
task=self.current_task) task=self.current_task)

View file

@ -1,6 +1,7 @@
import logging import logging
import os.path import os.path
import pathlib import pathlib
import platform
from typing import List from typing import List
from unittest.mock import Mock, patch 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.model_loader import ModelType
from buzz.transcriber import (FileTranscriptionOptions, FileTranscriptionTask, from buzz.transcriber import (FileTranscriptionOptions, FileTranscriptionTask,
Segment, TranscriptionOptions) Segment, TranscriptionOptions)
from tests.mock_sounddevice import MockInputStream from tests.mock_sounddevice import MockInputStream, mock_query_devices
from .mock_qt import MockNetworkAccessManager, MockNetworkReply 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: class TestLanguagesComboBox:
def test_should_show_sorted_whisper_languages(self, qtbot): def test_should_show_sorted_whisper_languages(self, qtbot):
@ -51,64 +62,22 @@ class TestLanguagesComboBox:
class TestAudioDevicesComboBox: 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): def test_get_devices(self):
with patch('sounddevice.query_devices') as query_devices_mock: audio_devices_combo_box = AudioDevicesComboBox()
query_devices_mock.return_value = self.mock_query_devices
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.currentText() == 'MacBook Pro Microphone'
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'
def test_select_default_mic_when_no_default(self): def test_select_default_mic_when_no_default(self):
with patch('sounddevice.query_devices') as query_devices_mock: sounddevice.default.device = -1, 1
query_devices_mock.return_value = self.mock_query_devices
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: class TestDownloadModelProgressDialog:
@ -174,6 +143,7 @@ class TestMainWindow:
assert window.windowIcon().pixmap(QSize(64, 64)).isNull() is False assert window.windowIcon().pixmap(QSize(64, 64)).isNull() is False
window.close() window.close()
@pytest.mark.xfail(condition=platform.system() == 'Windows', reason='Timing out')
def test_should_run_transcription_task(self, qtbot: QtBot, tasks_cache): def test_should_run_transcription_task(self, qtbot: QtBot, tasks_cache):
window = MainWindow(tasks_cache=tasks_cache) window = MainWindow(tasks_cache=tasks_cache)
qtbot.add_widget(window) qtbot.add_widget(window)
@ -184,7 +154,7 @@ class TestMainWindow:
assert open_transcript_action.isEnabled() is False assert open_transcript_action.isEnabled() is False
table_widget: QTableWidget = window.findChild(QTableWidget) 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))) table_widget.setCurrentIndex(table_widget.indexFromItem(table_widget.item(0, 1)))
assert open_transcript_action.isEnabled() assert open_transcript_action.isEnabled()
@ -202,7 +172,7 @@ class TestMainWindow:
assert table_widget.item(0, 1).text() == 'whisper-french.mp3' assert table_widget.item(0, 1).text() == 'whisper-french.mp3'
assert 'In Progress' in table_widget.item(0, 2).text() 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 # Stop task in progress
table_widget.selectRow(0) table_widget.selectRow(0)
@ -420,7 +390,6 @@ class TestRecordingTranscriberWidget:
qtbot.add_widget(widget) qtbot.add_widget(widget)
assert widget.windowTitle() == 'Live Recording' assert widget.windowTitle() == 'Live Recording'
@pytest.mark.skip()
def test_should_transcribe(self, qtbot): def test_should_transcribe(self, qtbot):
widget = RecordingTranscriberWidget() widget = RecordingTranscriberWidget()
qtbot.add_widget(widget) qtbot.add_widget(widget)
@ -428,9 +397,8 @@ class TestRecordingTranscriberWidget:
def assert_text_box_contains_text(): def assert_text_box_contains_text():
assert len(widget.text_box.toPlainText()) > 0 assert len(widget.text_box.toPlainText()) > 0
with patch('sounddevice.InputStream', side_effect=MockInputStream), patch('sounddevice.check_input_settings'): widget.record_button.click()
widget.record_button.click() qtbot.wait_until(callback=assert_text_box_contains_text, timeout=60 * 1000)
qtbot.wait_until(callback=assert_text_box_contains_text, timeout=60 * 1000)
with qtbot.wait_signal(widget.transcription_thread.finished, timeout=60 * 1000): with qtbot.wait_signal(widget.transcription_thread.finished, timeout=60 * 1000):
widget.stop_recording() widget.stop_recording()

View file

@ -8,6 +8,37 @@ import numpy as np
import sounddevice import sounddevice
import whisper 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): class MockInputStream(MagicMock):
running = False running = False
@ -46,6 +77,9 @@ class MockInputStream(MagicMock):
self.running = False self.running = False
self.thread.join() self.thread.join()
def close(self):
pass
def __enter__(self): def __enter__(self):
self.start() self.start()