mirror of
https://github.com/chidiwilliams/buzz.git
synced 2024-06-29 05:00:21 +02:00
Fix recording window shutting down (#326)
This commit is contained in:
parent
70ad8f9b1d
commit
abf30a34df
|
@ -7,4 +7,4 @@ omit =
|
||||||
directory = coverage/html
|
directory = coverage/html
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
fail_under = 72
|
fail_under = 78
|
||||||
|
|
16
buzz/gui.py
16
buzz/gui.py
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue