Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add duration for completed files #463

Merged
merged 4 commits into from
May 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down
90 changes: 2 additions & 88 deletions buzz/gui.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,8 +15,7 @@
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
Expand Down Expand Up @@ -45,6 +43,7 @@
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


Expand Down Expand Up @@ -722,91 +721,6 @@ def is_version_lower(version_a: str, version_b: str):
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
Expand Down
8 changes: 8 additions & 0 deletions buzz/transcriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ class Status(enum.Enum):
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):
Expand Down Expand Up @@ -769,9 +772,13 @@ def run(self):
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)
Expand Down Expand Up @@ -802,6 +809,7 @@ def on_task_completed(self, segments: List[Segment]):
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):
Expand Down
116 changes: 116 additions & 0 deletions buzz/widgets/transcription_tasks_table_widget.py
Original file line number Diff line number Diff line change
@@ -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)
35 changes: 4 additions & 31 deletions tests/gui_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
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
Expand Down Expand Up @@ -106,7 +106,7 @@ def get_test_asset(filename: str):
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'),
]


Expand Down Expand Up @@ -179,7 +179,7 @@ def test_should_load_tasks_from_cache(self, qtbot, tasks_cache):
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()
Expand Down Expand Up @@ -261,7 +261,7 @@ def _assert_task_status(table_widget: QTableWidget, row_index: int, expected_sta
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

Expand Down Expand Up @@ -355,33 +355,6 @@ def test_should_validate_temperature(self, text: str, state: QValidator.State):
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()
Expand Down
Loading