Add duration for completed files (#463)

This commit is contained in:
Chidi Williams 2023-05-21 02:09:45 +01:00 committed by GitHub
commit e907614e7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 191 additions and 122 deletions

View file

@ -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: |

View file

@ -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

View file

@ -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):

View 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)

View file

@ -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()

View 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'