mirror of
https://github.com/chidiwilliams/buzz.git
synced 2026-03-14 14:45:46 +01:00
Add duration for completed files (#463)
This commit is contained in:
parent
806b691d02
commit
e907614e7a
6 changed files with 191 additions and 122 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry config experimental.new-installer false && poetry install
|
||||
run: poetry install
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
|
|
@ -122,7 +122,7 @@ jobs:
|
|||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry config experimental.new-installer false && poetry install
|
||||
run: poetry install
|
||||
|
||||
- name: Bundle
|
||||
run: |
|
||||
|
|
@ -237,7 +237,7 @@ jobs:
|
|||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry config experimental.new-installer false && poetry install
|
||||
run: poetry install
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
|
|
|
|||
90
buzz/gui.py
90
buzz/gui.py
|
|
@ -1,7 +1,6 @@
|
|||
import enum
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from enum import auto
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
|
@ -16,8 +15,7 @@ from PyQt6.QtGui import (QAction, QCloseEvent, QDesktopServices, QIcon,
|
|||
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
|
||||
from PyQt6.QtWidgets import (QApplication, QCheckBox, QComboBox, QDialog,
|
||||
QDialogButtonBox, QFileDialog, QLabel, QMainWindow, QMessageBox, QPlainTextEdit,
|
||||
QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QGroupBox, QTableWidget,
|
||||
QMenuBar, QFormLayout, QTableWidgetItem,
|
||||
QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QGroupBox, QMenuBar, QFormLayout,
|
||||
QAbstractItemView, QListWidget, QListWidgetItem, QSizePolicy)
|
||||
|
||||
from buzz.cache import TasksCache
|
||||
|
|
@ -45,6 +43,7 @@ from .widgets.model_type_combo_box import ModelTypeComboBox
|
|||
from .widgets.openai_api_key_line_edit import OpenAIAPIKeyLineEdit
|
||||
from .widgets.preferences_dialog import PreferencesDialog
|
||||
from .widgets.toolbar import ToolBar
|
||||
from .widgets.transcription_tasks_table_widget import TranscriptionTasksTableWidget
|
||||
from .widgets.transcription_viewer_widget import TranscriptionViewerWidget
|
||||
|
||||
|
||||
|
|
@ -722,91 +721,6 @@ class AboutDialog(QDialog):
|
|||
return version_a.replace('.', '') < version_b.replace('.', '')
|
||||
|
||||
|
||||
class TranscriptionTasksTableWidget(QTableWidget):
|
||||
class Column(enum.Enum):
|
||||
TASK_ID = 0
|
||||
FILE_NAME = auto()
|
||||
STATUS = auto()
|
||||
|
||||
return_clicked = pyqtSignal()
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setRowCount(0)
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
self.setColumnCount(3)
|
||||
self.setColumnHidden(0, True)
|
||||
|
||||
self.verticalHeader().hide()
|
||||
self.setHorizontalHeaderLabels([_('ID'), _('File Name'), _('Status')])
|
||||
self.setColumnWidth(self.Column.FILE_NAME.value, 250)
|
||||
self.setColumnWidth(self.Column.STATUS.value, 180)
|
||||
self.horizontalHeader().setMinimumSectionSize(180)
|
||||
|
||||
self.setSelectionBehavior(
|
||||
QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
|
||||
def upsert_task(self, task: FileTranscriptionTask):
|
||||
task_row_index = self.task_row_index(task.id)
|
||||
if task_row_index is None:
|
||||
self.insertRow(self.rowCount())
|
||||
|
||||
row_index = self.rowCount() - 1
|
||||
task_id_widget_item = QTableWidgetItem(str(task.id))
|
||||
self.setItem(row_index, self.Column.TASK_ID.value,
|
||||
task_id_widget_item)
|
||||
|
||||
file_name_widget_item = QTableWidgetItem(
|
||||
os.path.basename(task.file_path))
|
||||
file_name_widget_item.setFlags(
|
||||
file_name_widget_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.setItem(row_index, self.Column.FILE_NAME.value,
|
||||
file_name_widget_item)
|
||||
|
||||
status_widget_item = QTableWidgetItem(
|
||||
task.status.value.title() if task.status is not None else '')
|
||||
status_widget_item.setFlags(
|
||||
status_widget_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.setItem(row_index, self.Column.STATUS.value,
|
||||
status_widget_item)
|
||||
else:
|
||||
status_widget = self.item(task_row_index, self.Column.STATUS.value)
|
||||
|
||||
if task.status == FileTranscriptionTask.Status.IN_PROGRESS:
|
||||
status_widget.setText(
|
||||
f'{_("In Progress")} ({task.fraction_completed :.0%})')
|
||||
elif task.status == FileTranscriptionTask.Status.COMPLETED:
|
||||
status_widget.setText(_('Completed'))
|
||||
elif task.status == FileTranscriptionTask.Status.FAILED:
|
||||
status_widget.setText(f'{_("Failed")} ({task.error})')
|
||||
elif task.status == FileTranscriptionTask.Status.CANCELED:
|
||||
status_widget.setText(_('Canceled'))
|
||||
|
||||
def clear_task(self, task_id: int):
|
||||
task_row_index = self.task_row_index(task_id)
|
||||
if task_row_index is not None:
|
||||
self.removeRow(task_row_index)
|
||||
|
||||
def task_row_index(self, task_id: int) -> int | None:
|
||||
table_items_matching_task_id = [item for item in self.findItems(str(task_id), Qt.MatchFlag.MatchExactly) if
|
||||
item.column() == self.Column.TASK_ID.value]
|
||||
if len(table_items_matching_task_id) == 0:
|
||||
return None
|
||||
return table_items_matching_task_id[0].row()
|
||||
|
||||
@staticmethod
|
||||
def find_task_id(index: QModelIndex):
|
||||
sibling_index = index.siblingAtColumn(TranscriptionTasksTableWidget.Column.TASK_ID.value).data()
|
||||
return int(sibling_index) if sibling_index is not None else None
|
||||
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
|
||||
if event.key() == Qt.Key.Key_Return:
|
||||
self.return_clicked.emit()
|
||||
super().keyPressEvent(event)
|
||||
|
||||
|
||||
class MainWindowToolbar(ToolBar):
|
||||
new_transcription_action_triggered: pyqtSignal
|
||||
open_transcript_action_triggered: pyqtSignal
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ class FileTranscriptionTask:
|
|||
status: Optional[Status] = None
|
||||
fraction_completed = 0.0
|
||||
error: Optional[str] = None
|
||||
queued_at: Optional[datetime.datetime] = None
|
||||
started_at: Optional[datetime.datetime] = None
|
||||
completed_at: Optional[datetime.datetime] = None
|
||||
|
||||
|
||||
class RecordingTranscriber(QObject):
|
||||
|
|
@ -769,9 +772,13 @@ class FileTranscriberQueueWorker(QObject):
|
|||
self.current_transcriber.error.connect(self.run)
|
||||
self.current_transcriber.completed.connect(self.run)
|
||||
|
||||
self.current_task.started_at = datetime.datetime.now()
|
||||
self.current_transcriber_thread.start()
|
||||
|
||||
def add_task(self, task: FileTranscriptionTask):
|
||||
if task.queued_at is None:
|
||||
task.queued_at = datetime.datetime.now()
|
||||
|
||||
self.tasks_queue.put(task)
|
||||
task.status = FileTranscriptionTask.Status.QUEUED
|
||||
self.task_updated.emit(task)
|
||||
|
|
@ -802,6 +809,7 @@ class FileTranscriberQueueWorker(QObject):
|
|||
if self.current_task is not None:
|
||||
self.current_task.status = FileTranscriptionTask.Status.COMPLETED
|
||||
self.current_task.segments = segments
|
||||
self.current_task.completed_at = datetime.datetime.now()
|
||||
self.task_updated.emit(self.current_task)
|
||||
|
||||
def stop(self):
|
||||
|
|
|
|||
116
buzz/widgets/transcription_tasks_table_widget.py
Normal file
116
buzz/widgets/transcription_tasks_table_widget.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import datetime
|
||||
import enum
|
||||
import os
|
||||
from enum import auto
|
||||
from typing import Optional
|
||||
|
||||
from PyQt6 import QtGui
|
||||
from PyQt6.QtCore import pyqtSignal, Qt, QModelIndex
|
||||
from PyQt6.QtWidgets import QTableWidget, QWidget, QAbstractItemView, QTableWidgetItem
|
||||
|
||||
from buzz.locale import _
|
||||
from buzz.transcriber import FileTranscriptionTask
|
||||
|
||||
|
||||
class TranscriptionTasksTableWidget(QTableWidget):
|
||||
class Column(enum.Enum):
|
||||
TASK_ID = 0
|
||||
FILE_NAME = auto()
|
||||
STATUS = auto()
|
||||
|
||||
return_clicked = pyqtSignal()
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setRowCount(0)
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
self.setColumnCount(3)
|
||||
self.setColumnHidden(0, True)
|
||||
|
||||
self.verticalHeader().hide()
|
||||
self.setHorizontalHeaderLabels([_('ID'), _('File Name'), _('Status')])
|
||||
self.setColumnWidth(self.Column.FILE_NAME.value, 250)
|
||||
self.setColumnWidth(self.Column.STATUS.value, 180)
|
||||
self.horizontalHeader().setMinimumSectionSize(180)
|
||||
|
||||
self.setSelectionBehavior(
|
||||
QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
|
||||
def upsert_task(self, task: FileTranscriptionTask):
|
||||
task_row_index = self.task_row_index(task.id)
|
||||
if task_row_index is None:
|
||||
self.insertRow(self.rowCount())
|
||||
|
||||
row_index = self.rowCount() - 1
|
||||
task_id_widget_item = QTableWidgetItem(str(task.id))
|
||||
self.setItem(row_index, self.Column.TASK_ID.value,
|
||||
task_id_widget_item)
|
||||
|
||||
file_name_widget_item = QTableWidgetItem(
|
||||
os.path.basename(task.file_path))
|
||||
file_name_widget_item.setFlags(
|
||||
file_name_widget_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.setItem(row_index, self.Column.FILE_NAME.value,
|
||||
file_name_widget_item)
|
||||
|
||||
status_widget_item = QTableWidgetItem(self.get_status_text(task))
|
||||
status_widget_item.setFlags(
|
||||
status_widget_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||
self.setItem(row_index, self.Column.STATUS.value,
|
||||
status_widget_item)
|
||||
else:
|
||||
status_widget = self.item(task_row_index, self.Column.STATUS.value)
|
||||
status_widget.setText(self.get_status_text(task))
|
||||
|
||||
@staticmethod
|
||||
def format_timedelta(delta: datetime.timedelta):
|
||||
mm, ss = divmod(delta.seconds, 60)
|
||||
result = f'{ss}s'
|
||||
if mm == 0:
|
||||
return result
|
||||
hh, mm = divmod(mm, 60)
|
||||
result = f'{mm}m {result}'
|
||||
if hh == 0:
|
||||
return result
|
||||
return f'{hh}h {result}'
|
||||
|
||||
@staticmethod
|
||||
def get_status_text(task: FileTranscriptionTask):
|
||||
if task.status == FileTranscriptionTask.Status.IN_PROGRESS:
|
||||
return (
|
||||
f'{_("In Progress")} ({task.fraction_completed :.0%})')
|
||||
elif task.status == FileTranscriptionTask.Status.COMPLETED:
|
||||
status = _('Completed')
|
||||
if task.started_at is not None and task.completed_at is not None:
|
||||
status += f" ({TranscriptionTasksTableWidget.format_timedelta(task.completed_at - task.started_at)})"
|
||||
return status
|
||||
elif task.status == FileTranscriptionTask.Status.FAILED:
|
||||
return f'{_("Failed")} ({task.error})'
|
||||
elif task.status == FileTranscriptionTask.Status.CANCELED:
|
||||
return _('Canceled')
|
||||
elif task.status == FileTranscriptionTask.Status.QUEUED:
|
||||
return _('Queued')
|
||||
|
||||
def clear_task(self, task_id: int):
|
||||
task_row_index = self.task_row_index(task_id)
|
||||
if task_row_index is not None:
|
||||
self.removeRow(task_row_index)
|
||||
|
||||
def task_row_index(self, task_id: int) -> int | None:
|
||||
table_items_matching_task_id = [item for item in self.findItems(str(task_id), Qt.MatchFlag.MatchExactly) if
|
||||
item.column() == self.Column.TASK_ID.value]
|
||||
if len(table_items_matching_task_id) == 0:
|
||||
return None
|
||||
return table_items_matching_task_id[0].row()
|
||||
|
||||
@staticmethod
|
||||
def find_task_id(index: QModelIndex):
|
||||
sibling_index = index.siblingAtColumn(TranscriptionTasksTableWidget.Column.TASK_ID.value).data()
|
||||
return int(sibling_index) if sibling_index is not None else None
|
||||
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
|
||||
if event.key() == Qt.Key.Key_Return:
|
||||
self.return_clicked.emit()
|
||||
super().keyPressEvent(event)
|
||||
|
|
@ -17,7 +17,7 @@ from buzz.cache import TasksCache
|
|||
from buzz.gui import (AboutDialog, AdvancedSettingsDialog, AudioDevicesComboBox, FileTranscriberWidget,
|
||||
LanguagesComboBox, MainWindow,
|
||||
RecordingTranscriberWidget,
|
||||
TemperatureValidator, TranscriptionTasksTableWidget, HuggingFaceSearchLineEdit,
|
||||
TemperatureValidator, HuggingFaceSearchLineEdit,
|
||||
TranscriptionOptionsGroupBox)
|
||||
from buzz.model_loader import ModelType
|
||||
from buzz.settings.settings import Settings
|
||||
|
|
@ -106,7 +106,7 @@ mock_tasks = [
|
|||
status=FileTranscriptionTask.Status.CANCELED),
|
||||
FileTranscriptionTask(file_path='', transcription_options=TranscriptionOptions(),
|
||||
file_transcription_options=FileTranscriptionOptions(file_paths=[]), model_path='',
|
||||
status=FileTranscriptionTask.Status.FAILED),
|
||||
status=FileTranscriptionTask.Status.FAILED, error='Error'),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ class TestMainWindow:
|
|||
table_widget.selectRow(1)
|
||||
assert window.toolbar.open_transcript_action.isEnabled() is False
|
||||
|
||||
assert table_widget.item(2, 2).text() == 'Failed'
|
||||
assert table_widget.item(2, 2).text() == 'Failed (Error)'
|
||||
table_widget.selectRow(2)
|
||||
assert window.toolbar.open_transcript_action.isEnabled() is False
|
||||
window.close()
|
||||
|
|
@ -261,7 +261,7 @@ class TestMainWindow:
|
|||
def assert_task_canceled():
|
||||
assert table_widget.rowCount() > 0
|
||||
assert table_widget.item(row_index, 1).text() == 'whisper-french.mp3'
|
||||
assert table_widget.item(row_index, 2).text() == expected_status
|
||||
assert expected_status in table_widget.item(row_index, 2).text()
|
||||
|
||||
return assert_task_canceled
|
||||
|
||||
|
|
@ -355,33 +355,6 @@ class TestTemperatureValidator:
|
|||
assert self.validator.validate(text, 0)[0] == state
|
||||
|
||||
|
||||
class TestTranscriptionTasksTableWidget:
|
||||
|
||||
def test_upsert_task(self, qtbot: QtBot):
|
||||
widget = TranscriptionTasksTableWidget()
|
||||
qtbot.add_widget(widget)
|
||||
|
||||
task = FileTranscriptionTask(id=0, file_path='testdata/whisper-french.mp3',
|
||||
transcription_options=TranscriptionOptions(),
|
||||
file_transcription_options=FileTranscriptionOptions(
|
||||
file_paths=['testdata/whisper-french.mp3']), model_path='',
|
||||
status=FileTranscriptionTask.Status.QUEUED)
|
||||
|
||||
widget.upsert_task(task)
|
||||
|
||||
assert widget.rowCount() == 1
|
||||
assert widget.item(0, 1).text() == 'whisper-french.mp3'
|
||||
assert widget.item(0, 2).text() == 'Queued'
|
||||
|
||||
task.status = FileTranscriptionTask.Status.IN_PROGRESS
|
||||
task.fraction_completed = 0.3524
|
||||
widget.upsert_task(task)
|
||||
|
||||
assert widget.rowCount() == 1
|
||||
assert widget.item(0, 1).text() == 'whisper-french.mp3'
|
||||
assert widget.item(0, 2).text() == 'In Progress (35%)'
|
||||
|
||||
|
||||
class TestRecordingTranscriberWidget:
|
||||
def test_should_set_window_title(self, qtbot: QtBot):
|
||||
widget = RecordingTranscriberWidget()
|
||||
|
|
|
|||
58
tests/widgets/transcription_tasks_table_widget_test.py
Normal file
58
tests/widgets/transcription_tasks_table_widget_test.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import datetime
|
||||
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from buzz.transcriber import FileTranscriptionTask, TranscriptionOptions, FileTranscriptionOptions
|
||||
from buzz.widgets.transcription_tasks_table_widget import TranscriptionTasksTableWidget
|
||||
|
||||
|
||||
class TestTranscriptionTasksTableWidget:
|
||||
|
||||
def test_upsert_task(self, qtbot: QtBot):
|
||||
widget = TranscriptionTasksTableWidget()
|
||||
qtbot.add_widget(widget)
|
||||
|
||||
task = FileTranscriptionTask(id=0, file_path='testdata/whisper-french.mp3',
|
||||
transcription_options=TranscriptionOptions(),
|
||||
file_transcription_options=FileTranscriptionOptions(
|
||||
file_paths=['testdata/whisper-french.mp3']), model_path='',
|
||||
status=FileTranscriptionTask.Status.QUEUED)
|
||||
task.queued_at = datetime.datetime(2023, 4, 12, 0, 0, 0)
|
||||
task.started_at = datetime.datetime(2023, 4, 12, 0, 0, 5)
|
||||
|
||||
widget.upsert_task(task)
|
||||
|
||||
assert widget.rowCount() == 1
|
||||
assert widget.item(0, 1).text() == 'whisper-french.mp3'
|
||||
assert widget.item(0, 2).text() == 'Queued'
|
||||
|
||||
task.status = FileTranscriptionTask.Status.IN_PROGRESS
|
||||
task.fraction_completed = 0.3524
|
||||
widget.upsert_task(task)
|
||||
|
||||
assert widget.rowCount() == 1
|
||||
assert widget.item(0, 1).text() == 'whisper-french.mp3'
|
||||
assert widget.item(0, 2).text() == 'In Progress (35%)'
|
||||
|
||||
task.status = FileTranscriptionTask.Status.COMPLETED
|
||||
task.completed_at = datetime.datetime(2023, 4, 12, 0, 0, 10)
|
||||
widget.upsert_task(task)
|
||||
|
||||
assert widget.rowCount() == 1
|
||||
assert widget.item(0, 1).text() == 'whisper-french.mp3'
|
||||
assert widget.item(0, 2).text() == 'Completed (5s)'
|
||||
|
||||
def test_upsert_task_no_timings(self, qtbot: QtBot):
|
||||
widget = TranscriptionTasksTableWidget()
|
||||
qtbot.add_widget(widget)
|
||||
|
||||
task = FileTranscriptionTask(id=0, file_path='testdata/whisper-french.mp3',
|
||||
transcription_options=TranscriptionOptions(),
|
||||
file_transcription_options=FileTranscriptionOptions(
|
||||
file_paths=['testdata/whisper-french.mp3']), model_path='',
|
||||
status=FileTranscriptionTask.Status.COMPLETED)
|
||||
widget.upsert_task(task)
|
||||
|
||||
assert widget.rowCount() == 1
|
||||
assert widget.item(0, 1).text() == 'whisper-french.mp3'
|
||||
assert widget.item(0, 2).text() == 'Completed'
|
||||
Loading…
Add table
Add a link
Reference in a new issue