add transcript viewer (#686)

This commit is contained in:
Chidi Williams 2024-03-15 17:38:35 +00:00 committed by GitHub
parent 8ae8300afc
commit ba522a58cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 423 additions and 206 deletions

View file

@ -1,6 +1,6 @@
# Adapted from https://github.com/zhiyiYo/Groove
from abc import ABC
from typing import TypeVar, Generic, Any, Type
from typing import TypeVar, Generic, Any, Type, List
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlRecord
@ -11,6 +11,7 @@ T = TypeVar("T", bound=Entity)
class DAO(ABC, Generic[T]):
entity: Type[T]
ignore_fields = []
def __init__(self, table: str, db: QSqlDatabase):
self.db = db
@ -18,15 +19,18 @@ class DAO(ABC, Generic[T]):
def insert(self, record: T):
query = self._create_query()
keys = record.__dict__.keys()
fields = [
field for field in record.__dict__.keys() if field not in self.ignore_fields
]
query.prepare(
f"""
INSERT INTO {self.table} ({", ".join(keys)})
VALUES ({", ".join([f":{key}" for key in keys])})
INSERT INTO {self.table} ({", ".join(fields)})
VALUES ({", ".join([f":{key}" for key in fields])})
"""
)
for key, value in record.__dict__.items():
query.bindValue(f":{key}", value)
for field in fields:
query.bindValue(f":{field}", getattr(record, field))
if not query.exec():
raise Exception(query.lastError().text())
@ -37,10 +41,8 @@ class DAO(ABC, Generic[T]):
return self._execute(query)
def to_entity(self, record: QSqlRecord) -> T:
entity = self.entity()
for i in range(record.count()):
setattr(entity, record.fieldName(i), record.value(i))
return entity
kwargs = {record.fieldName(i): record.value(i) for i in range(record.count())}
return self.entity(**kwargs)
def _execute(self, query: QSqlQuery) -> T | None:
if not query.exec():
@ -49,5 +51,13 @@ class DAO(ABC, Generic[T]):
return None
return self.to_entity(query.record())
def _execute_all(self, query: QSqlQuery) -> List[T]:
if not query.exec():
raise Exception(query.lastError().text())
entities = []
while query.next():
entities.append(self.to_entity(query.record()))
return entities
def _create_query(self):
return QSqlQuery(self.db)

View file

@ -1,3 +1,6 @@
from typing import List
from uuid import UUID
from PyQt6.QtSql import QSqlDatabase
from buzz.db.dao.dao import DAO
@ -6,6 +9,18 @@ from buzz.db.entity.transcription_segment import TranscriptionSegment
class TranscriptionSegmentDAO(DAO[TranscriptionSegment]):
entity = TranscriptionSegment
ignore_fields = ["id"]
def __init__(self, db: QSqlDatabase):
super().__init__("transcription_segment", db)
def get_segments(self, transcription_id: UUID) -> List[TranscriptionSegment]:
query = self._create_query()
query.prepare(
f"""
SELECT * FROM {self.table}
WHERE transcription_id = :transcription_id
"""
)
query.bindValue(":transcription_id", str(transcription_id))
return self._execute_all(query)

View file

@ -20,6 +20,13 @@ class Transcription(Entity):
error_message: str | None = None
file: str | None = None
time_queued: str = datetime.datetime.now().isoformat()
progress: float = 0.0
time_ended: str | None = None
time_started: str | None = None
export_formats: str | None = None
output_folder: str | None = None
source: str | None = None
url: str | None = None
@property
def id_as_uuid(self):

View file

@ -9,3 +9,4 @@ class TranscriptionSegment(Entity):
end_time: int
text: str
transcription_id: str
id: int = -1

View file

@ -42,3 +42,6 @@ class TranscriptionService:
transcription_id=str(id),
)
)
def get_transcription_segments(self, transcription_id: UUID):
return self.transcription_segment_dao.get_segments(transcription_id)

View file

@ -7,8 +7,8 @@ APP_NAME = "Buzz"
class Settings:
def __init__(self):
self.settings = QSettings(APP_NAME)
def __init__(self, application=""):
self.settings = QSettings(APP_NAME, application)
self.settings.sync()
class Key(enum.Enum):

View file

@ -18,7 +18,9 @@ class Shortcut(str, enum.Enum):
OPEN_IMPORT_URL_WINDOW = ("Ctrl+U", "Import URL")
OPEN_PREFERENCES_WINDOW = ("Ctrl+,", "Open Preferences Window")
OPEN_TRANSCRIPT_EDITOR = ("Ctrl+E", "Open Transcript Viewer")
VIEW_TRANSCRIPT_TEXT = ("Ctrl+E", "View Transcript Text")
VIEW_TRANSCRIPT_TIMESTAMPS = ("Ctrl+T", "View Transcript Timestamps")
CLEAR_HISTORY = ("Ctrl+S", "Clear History")
STOP_TRANSCRIPTION = ("Ctrl+X", "Cancel Transcription")

View file

@ -1,21 +0,0 @@
import typing
from buzz.settings.settings import Settings
from buzz.settings.shortcut import Shortcut
class ShortcutSettings:
def __init__(self, settings: Settings):
self.settings = settings
def load(self) -> typing.Dict[str, str]:
shortcuts = Shortcut.get_default_shortcuts()
custom_shortcuts: typing.Dict[str, str] = self.settings.value(
Settings.Key.SHORTCUTS, {}
)
for shortcut_name in custom_shortcuts:
shortcuts[shortcut_name] = custom_shortcuts[shortcut_name]
return shortcuts
def save(self, shortcuts: typing.Dict[str, str]) -> None:
self.settings.set_value(Settings.Key.SHORTCUTS, shortcuts)

View file

@ -0,0 +1,24 @@
import typing
from buzz.settings.settings import Settings
from buzz.settings.shortcut import Shortcut
class Shortcuts:
def __init__(self, settings: Settings):
self.settings = settings
def get(self, shortcut: Shortcut) -> str:
custom_shortcuts = self.get_custom_shortcuts()
return custom_shortcuts.get(shortcut.name, shortcut.sequence)
def set(self, shortcut: Shortcut, sequence: str) -> None:
custom_shortcuts = self.get_custom_shortcuts()
custom_shortcuts[shortcut.name] = sequence
self.settings.set_value(Settings.Key.SHORTCUTS, custom_shortcuts)
def clear(self) -> None:
self.settings.set_value(Settings.Key.SHORTCUTS, {})
def get_custom_shortcuts(self) -> typing.Dict[str, str]:
return self.settings.value(Settings.Key.SHORTCUTS, {})

View file

@ -102,6 +102,7 @@ class FileTranscriber(QObject):
...
# TODO: Move to transcription service
def write_output(path: str, segments: List[Segment], output_format: OutputFormat):
logging.debug(
"Writing transcription output, path = %s, output format = %s, number of segments = %s",

View file

@ -1,5 +1,6 @@
import sys
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QApplication
from buzz.__version__ import VERSION
@ -22,7 +23,7 @@ class Application(QApplication):
self.setApplicationVersion(VERSION)
if sys.platform == "darwin":
self.setStyle("Fusion")
self.setFont(QFont("SF Pro", self.font().pointSize()))
db = setup_app_db()
transcription_service = TranscriptionService(

View file

@ -74,6 +74,13 @@ class FileDownloadIcon(Icon):
super().__init__(get_path("assets/file_download_black_24dp.svg"), parent)
class VisibilityIcon(Icon):
def __init__(self, parent: QWidget):
super().__init__(
get_path("assets/visibility_FILL0_wght700_GRAD0_opsz48.svg"), parent
)
BUZZ_ICON_PATH = get_path("assets/buzz.ico")
BUZZ_LARGE_ICON_PATH = get_path("assets/buzz-icon-1024.png")

View file

@ -19,7 +19,7 @@ from buzz.db.service.transcription_service import TranscriptionService
from buzz.file_transcriber_queue_worker import FileTranscriberQueueWorker
from buzz.locale import _
from buzz.settings.settings import APP_NAME, Settings
from buzz.settings.shortcut_settings import ShortcutSettings
from buzz.settings.shortcuts import Shortcuts
from buzz.store.keyring_store import set_password, Key
from buzz.transcriber.transcriber import (
FileTranscriptionTask,
@ -59,8 +59,7 @@ class MainWindow(QMainWindow):
self.settings = Settings()
self.shortcut_settings = ShortcutSettings(settings=self.settings)
self.shortcuts = self.shortcut_settings.load()
self.shortcuts = Shortcuts(settings=self.settings)
self.transcription_service = transcription_service
@ -326,7 +325,11 @@ class MainWindow(QMainWindow):
def open_transcription_viewer(self, transcription: Transcription):
transcription_viewer_widget = TranscriptionViewerWidget(
transcription=transcription, parent=self, flags=Qt.WindowType.Window
transcription=transcription,
transcription_service=self.transcription_service,
shortcuts=self.shortcuts,
parent=self,
flags=Qt.WindowType.Window,
)
transcription_viewer_widget.show()
@ -354,15 +357,12 @@ class MainWindow(QMainWindow):
self.table_widget.refresh_row(task.uid)
def on_task_error(self, task: FileTranscriptionTask, error: str):
logging.debug("FAILED!!!!")
self.transcription_service.update_transcription_as_failed(task.uid, error)
self.table_widget.refresh_row(task.uid)
def on_shortcuts_changed(self, shortcuts: dict):
self.shortcuts = shortcuts
self.menu_bar.set_shortcuts(shortcuts=self.shortcuts)
self.toolbar.set_shortcuts(shortcuts=self.shortcuts)
self.shortcut_settings.save(shortcuts=self.shortcuts)
def on_shortcuts_changed(self):
self.menu_bar.reset_shortcuts()
self.toolbar.reset_shortcuts()
def resizeEvent(self, event):
self.save_geometry()
@ -373,7 +373,6 @@ class MainWindow(QMainWindow):
self.transcriber_worker.stop()
self.transcriber_thread.quit()
self.transcriber_thread.wait()
self.shortcut_settings.save(shortcuts=self.shortcuts)
super().closeEvent(event)
def save_geometry(self):

View file

@ -1,10 +1,14 @@
from typing import Dict, Optional
from typing import Optional
from PyQt6.QtCore import pyqtSignal, Qt
from PyQt6.QtGui import QKeySequence
from PyQt6.QtWidgets import QWidget
from buzz.action import Action
from buzz.locale import _
from buzz.settings.shortcut import Shortcut
from buzz.settings.shortcuts import Shortcuts
from buzz.widgets.icon import Icon
from buzz.widgets.icon import (
RECORD_ICON_PATH,
ADD_ICON_PATH,
@ -12,9 +16,6 @@ from buzz.widgets.icon import (
CANCEL_ICON_PATH,
TRASH_ICON_PATH,
)
from buzz.locale import _
from buzz.settings.shortcut import Shortcut
from buzz.widgets.icon import Icon
from buzz.widgets.recording_transcriber_widget import RecordingTranscriberWidget
from buzz.widgets.toolbar import ToolBar
@ -26,9 +27,11 @@ class MainWindowToolbar(ToolBar):
ICON_LIGHT_THEME_BACKGROUND = "#555"
ICON_DARK_THEME_BACKGROUND = "#AAA"
def __init__(self, shortcuts: Dict[str, str], parent: Optional[QWidget]):
def __init__(self, shortcuts: Shortcuts, parent: Optional[QWidget]):
super().__init__(parent)
self.shortcuts = shortcuts
self.record_action = Action(Icon(RECORD_ICON_PATH, self), _("Record"), self)
self.record_action.triggered.connect(self.on_record_action_triggered)
@ -59,7 +62,7 @@ class MainWindowToolbar(ToolBar):
self.clear_history_action_triggered = self.clear_history_action.triggered
self.clear_history_action.setDisabled(True)
self.set_shortcuts(shortcuts)
self.reset_shortcuts()
self.addAction(self.record_action)
self.addSeparator()
@ -74,21 +77,18 @@ class MainWindowToolbar(ToolBar):
self.setMovable(False)
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
def set_shortcuts(self, shortcuts: Dict[str, str]):
def reset_shortcuts(self):
self.record_action.setShortcut(
QKeySequence.fromString(shortcuts[Shortcut.OPEN_RECORD_WINDOW.name])
QKeySequence.fromString(self.shortcuts.get(Shortcut.OPEN_RECORD_WINDOW))
)
self.new_transcription_action.setShortcut(
QKeySequence.fromString(shortcuts[Shortcut.OPEN_IMPORT_WINDOW.name])
)
self.open_transcript_action.setShortcut(
QKeySequence.fromString(shortcuts[Shortcut.OPEN_TRANSCRIPT_EDITOR.name])
QKeySequence.fromString(self.shortcuts.get(Shortcut.OPEN_IMPORT_WINDOW))
)
self.stop_transcription_action.setShortcut(
QKeySequence.fromString(shortcuts[Shortcut.STOP_TRANSCRIPTION.name])
QKeySequence.fromString(self.shortcuts.get(Shortcut.STOP_TRANSCRIPTION))
)
self.clear_history_action.setShortcut(
QKeySequence.fromString(shortcuts[Shortcut.CLEAR_HISTORY.name])
QKeySequence.fromString(self.shortcuts.get(Shortcut.CLEAR_HISTORY))
)
def on_record_action_triggered(self):

View file

@ -1,5 +1,5 @@
import webbrowser
from typing import Dict, Optional
from typing import Optional
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QAction, QKeySequence
@ -8,6 +8,7 @@ from PyQt6.QtWidgets import QMenuBar, QWidget
from buzz.locale import _
from buzz.settings.settings import APP_NAME
from buzz.settings.shortcut import Shortcut
from buzz.settings.shortcuts import Shortcuts
from buzz.widgets.about_dialog import AboutDialog
from buzz.widgets.preferences_dialog.models.preferences import Preferences
from buzz.widgets.preferences_dialog.preferences_dialog import (
@ -18,14 +19,14 @@ from buzz.widgets.preferences_dialog.preferences_dialog import (
class MenuBar(QMenuBar):
import_action_triggered = pyqtSignal()
import_url_action_triggered = pyqtSignal()
shortcuts_changed = pyqtSignal(dict)
shortcuts_changed = pyqtSignal()
openai_api_key_changed = pyqtSignal(str)
preferences_changed = pyqtSignal(Preferences)
preferences_dialog: Optional[PreferencesDialog] = None
def __init__(
self,
shortcuts: Dict[str, str],
shortcuts: Shortcuts,
preferences: Preferences,
parent: Optional[QWidget] = None,
):
@ -49,7 +50,7 @@ class MenuBar(QMenuBar):
help_action = QAction(f'{_("Help")}', self)
help_action.triggered.connect(self.on_help_action_triggered)
self.set_shortcuts(shortcuts)
self.reset_shortcuts()
file_menu = self.addMenu(_("File"))
file_menu.addAction(self.import_action)
@ -86,15 +87,15 @@ class MenuBar(QMenuBar):
def on_help_action_triggered(self):
webbrowser.open("https://chidiwilliams.github.io/buzz/docs")
def set_shortcuts(self, shortcuts: Dict[str, str]):
self.shortcuts = shortcuts
def reset_shortcuts(self):
self.import_action.setShortcut(
QKeySequence.fromString(shortcuts[Shortcut.OPEN_IMPORT_WINDOW.name])
QKeySequence.fromString(self.shortcuts.get(Shortcut.OPEN_IMPORT_WINDOW))
)
self.import_url_action.setShortcut(
QKeySequence.fromString(shortcuts[Shortcut.OPEN_IMPORT_URL_WINDOW.name])
QKeySequence.fromString(self.shortcuts.get(Shortcut.OPEN_IMPORT_URL_WINDOW))
)
self.preferences_action.setShortcut(
QKeySequence.fromString(shortcuts[Shortcut.OPEN_PREFERENCES_WINDOW.name])
QKeySequence.fromString(
self.shortcuts.get(Shortcut.OPEN_PREFERENCES_WINDOW)
)
)

View file

@ -4,7 +4,7 @@ from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QWidget, QLineEdit
from buzz.assets import get_path
from buzz.widgets.icon import Icon
from buzz.widgets.icon import Icon, VisibilityIcon
from buzz.widgets.line_edit import LineEdit
@ -16,9 +16,7 @@ class OpenAIAPIKeyLineEdit(LineEdit):
self.key = key
self.visible_on_icon = Icon(
get_path("assets/visibility_FILL0_wght700_GRAD0_opsz48.svg"), self
)
self.visible_on_icon = VisibilityIcon(self)
self.visible_off_icon = Icon(
get_path("assets/visibility_off_FILL0_wght700_GRAD0_opsz48.svg"), self
)

View file

@ -1,10 +1,11 @@
import copy
from typing import Dict, Optional
from typing import Optional
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QDialog, QWidget, QVBoxLayout, QTabWidget, QDialogButtonBox
from buzz.locale import _
from buzz.settings.shortcuts import Shortcuts
from buzz.widgets.preferences_dialog.folder_watch_preferences_widget import (
FolderWatchPreferencesWidget,
)
@ -24,15 +25,14 @@ from buzz.widgets.preferences_dialog.shortcuts_editor_preferences_widget import
class PreferencesDialog(QDialog):
shortcuts_changed = pyqtSignal(dict)
shortcuts_changed = pyqtSignal()
openai_api_key_changed = pyqtSignal(str)
folder_watch_config_changed = pyqtSignal(FolderWatchPreferences)
preferences_changed = pyqtSignal(Preferences)
def __init__(
self,
# TODO: move shortcuts and default export file name into preferences
shortcuts: Dict[str, str],
shortcuts: Shortcuts,
preferences: Preferences,
parent: Optional[QWidget] = None,
) -> None:

View file

@ -1,26 +1,27 @@
from typing import Optional, Dict
from typing import Optional
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QKeySequence
from PyQt6.QtWidgets import QWidget, QFormLayout, QPushButton
from buzz.settings.shortcut import Shortcut
from buzz.settings.shortcuts import Shortcuts
from buzz.widgets.sequence_edit import SequenceEdit
class ShortcutsEditorPreferencesWidget(QWidget):
shortcuts_changed = pyqtSignal(dict)
shortcuts_changed = pyqtSignal()
def __init__(self, shortcuts: Dict[str, str], parent: Optional[QWidget] = None):
def __init__(self, shortcuts: Shortcuts, parent: Optional[QWidget] = None):
super().__init__(parent)
self.shortcuts = shortcuts
self.layout = QFormLayout(self)
for shortcut in Shortcut:
sequence_edit = SequenceEdit(shortcuts.get(shortcut.name, ""), self)
sequence_edit = SequenceEdit(shortcuts.get(shortcut), self)
sequence_edit.keySequenceChanged.connect(
self.get_key_sequence_changed(shortcut.name)
self.get_key_sequence_changed(shortcut)
)
self.layout.addRow(shortcut.description, sequence_edit)
@ -31,21 +32,21 @@ class ShortcutsEditorPreferencesWidget(QWidget):
self.layout.addWidget(reset_to_defaults_button)
def get_key_sequence_changed(self, shortcut_name: str):
def get_key_sequence_changed(self, shortcut: Shortcut):
def key_sequence_changed(sequence: QKeySequence):
self.shortcuts[shortcut_name] = sequence.toString()
self.shortcuts_changed.emit(self.shortcuts)
self.shortcuts.set(shortcut, sequence.toString())
self.shortcuts_changed.emit()
return key_sequence_changed
def reset_to_defaults(self):
self.shortcuts = Shortcut.get_default_shortcuts()
self.shortcuts.clear()
for i, shortcut in enumerate(Shortcut):
sequence_edit = self.layout.itemAt(
i, QFormLayout.ItemRole.FieldRole
).widget()
assert isinstance(sequence_edit, SequenceEdit)
sequence_edit.setKeySequence(QKeySequence(self.shortcuts[shortcut.name]))
sequence_edit.setKeySequence(QKeySequence(self.shortcuts.get(shortcut)))
self.shortcuts_changed.emit(self.shortcuts)
self.shortcuts_changed.emit()

View file

@ -14,9 +14,10 @@ class ToolBar(QToolBar):
self.setStyleSheet("QToolButton{margin: 6px 3px;}")
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
def addAction(self, action: QtGui.QAction) -> None:
super().addAction(action)
def addAction(self, *args):
action = super().addAction(*args)
self.fix_spacing_on_mac()
return action
def addActions(self, actions: typing.Iterable[QtGui.QAction]) -> None:
super().addActions(actions)

View file

@ -187,6 +187,9 @@ class TranscriptionTasksTableWidget(QTableView):
self.verticalHeader().hide()
self.setAlternatingRowColors(True)
# Show date added before date completed
self.horizontalHeader().swapSections(11, 12)
def contextMenuEvent(self, event):
menu = QMenu(self)
for definition in column_definitions:

View file

@ -1,30 +0,0 @@
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QPushButton, QWidget, QMenu
from buzz.transcriber.transcriber import (
OutputFormat,
)
from buzz.widgets.icon import FileDownloadIcon
class ExportTranscriptionButton(QPushButton):
on_export_triggered = pyqtSignal(OutputFormat)
def __init__(self, parent: QWidget):
super().__init__(parent)
export_button_menu = QMenu()
actions = [
QAction(text=output_format.value.upper(), parent=self)
for output_format in OutputFormat
]
export_button_menu.addActions(actions)
export_button_menu.triggered.connect(self.on_menu_triggered)
self.setMenu(export_button_menu)
self.setIcon(FileDownloadIcon(self))
def on_menu_triggered(self, action: QAction):
output_format = OutputFormat[action.text()]
self.on_export_triggered.emit(output_format)

View file

@ -0,0 +1,61 @@
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QWidget, QMenu, QFileDialog
from buzz.db.entity.transcription import Transcription
from buzz.db.service.transcription_service import TranscriptionService
from buzz.locale import _
from buzz.transcriber.file_transcriber import write_output
from buzz.transcriber.transcriber import (
OutputFormat,
Segment,
)
class ExportTranscriptionMenu(QMenu):
def __init__(
self,
transcription: Transcription,
transcription_service: TranscriptionService,
parent: QWidget | None = None,
):
super().__init__(parent)
self.transcription = transcription
self.transcription_service = transcription_service
actions = [
QAction(text=output_format.value.upper(), parent=self)
for output_format in OutputFormat
]
self.addActions(actions)
self.triggered.connect(self.on_menu_triggered)
def on_menu_triggered(self, action: QAction):
output_format = OutputFormat[action.text()]
default_path = self.transcription.get_output_file_path(
output_format=output_format
)
(output_file_path, nil) = QFileDialog.getSaveFileName(
self,
_("Save File"),
default_path,
_("Text files") + f" (*.{output_format.value})",
)
if output_file_path == "":
return
segments = [
Segment(start=segment.start_time, end=segment.end_time, text=segment.text)
for segment in self.transcription_service.get_transcription_segments(
transcription_id=self.transcription.id_as_uuid
)
]
write_output(
path=output_file_path,
segments=segments,
output_format=output_format,
)

View file

@ -90,6 +90,8 @@ class TranscriptionSegmentsEditorWidget(QTableView):
self.selectionModel().selectionChanged.connect(self.on_selection_changed)
model.select()
# Show start before end
self.horizontalHeader().swapSections(1, 2)
self.resizeColumnsToContents()
def on_selection_changed(

View file

@ -0,0 +1,37 @@
from typing import Optional
from PyQt6.QtCore import pyqtSignal, Qt
from PyQt6.QtGui import QKeySequence
from PyQt6.QtWidgets import QToolButton, QWidget, QMenu
from buzz.locale import _
from buzz.settings.shortcut import Shortcut
from buzz.settings.shortcuts import Shortcuts
from buzz.widgets.icon import VisibilityIcon
class TranscriptionViewModeToolButton(QToolButton):
view_mode_changed = pyqtSignal(bool) # is_timestamps?
def __init__(self, shortcuts: Shortcuts, parent: Optional[QWidget] = None):
super().__init__(parent)
self.setText("View")
self.setIcon(VisibilityIcon(self))
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
menu = QMenu(self)
menu.addAction(
_("Text"),
QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TEXT)),
lambda: self.view_mode_changed.emit(False),
)
menu.addAction(
_("Timestamps"),
QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TIMESTAMPS)),
lambda: self.view_mode_changed.emit(True),
)
self.setMenu(menu)

View file

@ -3,28 +3,35 @@ from typing import Optional
from uuid import UUID
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
from PyQt6.QtMultimedia import QMediaPlayer
from PyQt6.QtSql import QSqlRecord
from PyQt6.QtWidgets import (
QWidget,
QHBoxLayout,
QVBoxLayout,
QToolButton,
QLabel,
QGridLayout,
QFileDialog,
)
from buzz.db.entity.transcription import Transcription
from buzz.locale import _
from buzz.db.service.transcription_service import TranscriptionService
from buzz.paths import file_path_as_title
from buzz.transcriber.file_transcriber import write_output
from buzz.transcriber.transcriber import OutputFormat, Segment
from buzz.settings.shortcuts import Shortcuts
from buzz.widgets.audio_player import AudioPlayer
from buzz.widgets.transcription_viewer.export_transcription_button import (
ExportTranscriptionButton,
from buzz.widgets.icon import (
FileDownloadIcon,
)
from buzz.widgets.text_display_box import TextDisplayBox
from buzz.widgets.toolbar import ToolBar
from buzz.widgets.transcription_viewer.export_transcription_menu import (
ExportTranscriptionMenu,
)
from buzz.widgets.transcription_viewer.transcription_segments_editor_widget import (
TranscriptionSegmentsEditorWidget,
)
from buzz.widgets.transcription_viewer.transcription_view_mode_tool_button import (
TranscriptionViewModeToolButton,
)
class TranscriptionViewerWidget(QWidget):
@ -33,22 +40,31 @@ class TranscriptionViewerWidget(QWidget):
def __init__(
self,
transcription: Transcription,
transcription_service: TranscriptionService,
shortcuts: Shortcuts,
parent: Optional["QWidget"] = None,
flags: Qt.WindowType = Qt.WindowType.Widget,
) -> None:
super().__init__(parent, flags)
self.transcription = transcription
self.transcription_service = transcription_service
self.setMinimumWidth(800)
self.setMinimumHeight(500)
self.setWindowTitle(file_path_as_title(transcription.file))
self.is_showing_timestamps = True
self.table_widget = TranscriptionSegmentsEditorWidget(
transcription_id=UUID(hex=transcription.id), parent=self
)
self.table_widget.segment_selected.connect(self.on_segment_selected)
self.text_display_box = TextDisplayBox(self)
font = QFont(self.text_display_box.font().family(), 14)
self.text_display_box.setFont(font)
self.audio_player: Optional[AudioPlayer] = None
if platform.system() != "Linux":
self.audio_player = AudioPlayer(file_path=transcription.file)
@ -56,56 +72,61 @@ class TranscriptionViewerWidget(QWidget):
self.on_audio_player_position_ms_changed
)
self.current_segment_label = QLabel()
self.current_segment_label.setText("")
self.current_segment_label = QLabel("", self)
self.current_segment_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.current_segment_label.setContentsMargins(0, 0, 0, 10)
buttons_layout = QHBoxLayout()
buttons_layout.addStretch()
layout = QVBoxLayout(self)
export_button = ExportTranscriptionButton(parent=self)
export_button.on_export_triggered.connect(self.on_export_triggered)
toolbar = ToolBar(self)
layout = QGridLayout(self)
layout.addWidget(self.table_widget, 0, 0, 1, 2)
view_mode_tool_button = TranscriptionViewModeToolButton(shortcuts, self)
view_mode_tool_button.view_mode_changed.connect(self.on_view_mode_changed)
toolbar.addWidget(view_mode_tool_button)
export_tool_button = QToolButton()
export_tool_button.setText("Export")
export_tool_button.setIcon(FileDownloadIcon(self))
export_tool_button.setToolButtonStyle(
Qt.ToolButtonStyle.ToolButtonTextBesideIcon
)
export_transcription_menu = ExportTranscriptionMenu(
transcription, transcription_service, self
)
export_tool_button.setMenu(export_transcription_menu)
export_tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
toolbar.addWidget(export_tool_button)
layout.setMenuBar(toolbar)
layout.addWidget(self.table_widget)
layout.addWidget(self.text_display_box)
if self.audio_player is not None:
layout.addWidget(self.audio_player, 1, 0, 1, 1)
layout.addWidget(export_button, 1, 1, 1, 1)
layout.addWidget(self.current_segment_label, 2, 0, 1, 2)
layout.addWidget(self.audio_player)
layout.addWidget(self.current_segment_label)
self.setLayout(layout)
def on_export_triggered(self, output_format: OutputFormat) -> None:
default_path = self.transcription.get_output_file_path(
output_format=output_format
)
self.reset_view()
(output_file_path, nil) = QFileDialog.getSaveFileName(
self,
_("Save File"),
default_path,
_("Text files") + f" (*.{output_format.value})",
)
if output_file_path == "":
return
segments = [
Segment(
start=segment.value("start_time"),
end=segment.value("end_time"),
text=segment.value("text"),
def reset_view(self):
if self.is_showing_timestamps:
self.text_display_box.hide()
self.table_widget.show()
else:
segments = self.transcription_service.get_transcription_segments(
transcription_id=self.transcription.id_as_uuid
)
for segment in self.table_widget.segments()
]
self.text_display_box.setPlainText(
" ".join(segment.text.strip() for segment in segments)
)
self.text_display_box.show()
self.table_widget.hide()
write_output(
path=output_file_path,
segments=segments,
output_format=output_format,
)
def on_view_mode_changed(self, is_timestamps: bool) -> None:
self.is_showing_timestamps = is_timestamps
self.reset_view()
def on_segment_selected(self, segment: QSqlRecord):
if self.audio_player is not None and (

View file

@ -1,4 +1,6 @@
import os
import random
import string
import pytest
from PyQt6.QtSql import QSqlDatabase
@ -8,6 +10,8 @@ from buzz.db.dao.transcription_dao import TranscriptionDAO
from buzz.db.dao.transcription_segment_dao import TranscriptionSegmentDAO
from buzz.db.db import setup_test_db
from buzz.db.service.transcription_service import TranscriptionService
from buzz.settings.settings import Settings
from buzz.settings.shortcuts import Shortcuts
from buzz.widgets.application import Application
@ -52,3 +56,19 @@ def qapp_args(request):
return []
return request.param
@pytest.fixture(scope="session")
def settings():
application = "".join(
random.choice(string.ascii_letters + string.digits) for _ in range(6)
)
settings = Settings(application=application)
yield settings
settings.clear()
@pytest.fixture(scope="session")
def shortcuts(settings):
return Shortcuts(settings)

View file

@ -0,0 +1,63 @@
import pathlib
import uuid
import pytest
from pytestqt.qtbot import QtBot
from buzz.db.entity.transcription import Transcription
from buzz.db.entity.transcription_segment import TranscriptionSegment
from buzz.model_loader import ModelType, WhisperModelSize
from buzz.transcriber.transcriber import Task
from buzz.widgets.transcription_viewer.export_transcription_menu import (
ExportTranscriptionMenu,
)
class TestExportTranscriptionMenu:
@pytest.fixture()
def transcription(
self, transcription_dao, transcription_segment_dao
) -> Transcription:
id = uuid.uuid4()
transcription_dao.insert(
Transcription(
id=str(id),
status="completed",
file="testdata/whisper-french.mp3",
task=Task.TRANSCRIBE.value,
model_type=ModelType.WHISPER.value,
whisper_model_size=WhisperModelSize.SMALL.value,
)
)
transcription_segment_dao.insert(TranscriptionSegment(40, 299, "Bien", str(id)))
transcription_segment_dao.insert(
TranscriptionSegment(299, 329, "venue dans", str(id))
)
return transcription_dao.find_by_id(str(id))
def test_should_export_segments(
self,
tmp_path: pathlib.Path,
qtbot: QtBot,
transcription,
transcription_service,
shortcuts,
mocker,
):
output_file_path = tmp_path / "whisper.txt"
mocker.patch(
"PyQt6.QtWidgets.QFileDialog.getSaveFileName",
return_value=(str(output_file_path), ""),
)
widget = ExportTranscriptionMenu(
transcription,
transcription_service,
)
qtbot.add_widget(widget)
widget.actions()[0].trigger()
with open(output_file_path, encoding="utf-8") as output_file:
assert "Bien\nvenue dans" in output_file.read()

View file

@ -1,17 +1,14 @@
from PyQt6.QtCore import QSettings
from buzz.settings.settings import Settings
from buzz.settings.shortcut_settings import ShortcutSettings
from buzz.widgets.menu_bar import MenuBar
from buzz.widgets.preferences_dialog.models.preferences import Preferences
from buzz.widgets.preferences_dialog.preferences_dialog import PreferencesDialog
class TestMenuBar:
def test_open_preferences_dialog(self, qtbot):
def test_open_preferences_dialog(self, qtbot, shortcuts):
menu_bar = MenuBar(
shortcuts=ShortcutSettings(Settings()).load(),
preferences=Preferences.load(QSettings()),
shortcuts=shortcuts, preferences=Preferences.load(QSettings())
)
qtbot.add_widget(menu_bar)

View file

@ -7,10 +7,9 @@ from buzz.widgets.preferences_dialog.preferences_dialog import PreferencesDialog
class TestPreferencesDialog:
def test_create(self, qtbot: QtBot):
def test_create(self, qtbot: QtBot, shortcuts):
dialog = PreferencesDialog(
shortcuts={},
preferences=Preferences.load(QSettings()),
shortcuts=shortcuts, preferences=Preferences.load(QSettings())
)
qtbot.add_widget(dialog)

View file

@ -1,6 +1,6 @@
from PyQt6.QtWidgets import QPushButton, QLabel
from pytestqt.qtbot import QtBot
from buzz.settings.shortcut import Shortcut
from buzz.widgets.preferences_dialog.shortcuts_editor_preferences_widget import (
ShortcutsEditorPreferencesWidget,
)
@ -8,10 +8,17 @@ from buzz.widgets.sequence_edit import SequenceEdit
class TestShortcutsEditorWidget:
def test_should_reset_to_defaults(self, qtbot):
widget = ShortcutsEditorPreferencesWidget(
shortcuts=Shortcut.get_default_shortcuts()
)
def test_should_update_shortcuts(self, qtbot: QtBot, shortcuts):
widget = ShortcutsEditorPreferencesWidget(shortcuts=shortcuts)
qtbot.add_widget(widget)
sequence_edit = widget.findChild(SequenceEdit)
assert sequence_edit.keySequence().toString() == "Ctrl+R"
with qtbot.wait_signal(widget.shortcuts_changed, timeout=1000):
sequence_edit.setKeySequence("Ctrl+Shift+R")
def test_should_reset_to_defaults(self, qtbot, shortcuts):
widget = ShortcutsEditorPreferencesWidget(shortcuts=shortcuts)
qtbot.add_widget(widget)
reset_button = widget.findChild(QPushButton)
@ -26,7 +33,8 @@ class TestShortcutsEditorWidget:
("Import File", "Ctrl+O"),
("Import URL", "Ctrl+U"),
("Open Preferences Window", "Ctrl+,"),
("Open Transcript Viewer", "Ctrl+E"),
("View Transcript Text", "Ctrl+E"),
("View Transcript Timestamps", "Ctrl+T"),
("Clear History", "Ctrl+S"),
("Cancel Transcription", "Ctrl+X"),
)

View file

@ -1,9 +1,6 @@
import pathlib
import uuid
from unittest.mock import patch
import pytest
from PyQt6.QtWidgets import QPushButton
from pytestqt.qtbot import QtBot
from buzz.db.entity.transcription import Transcription
@ -41,8 +38,12 @@ class TestTranscriptionViewerWidget:
return transcription_dao.find_by_id(str(id))
def test_should_display_segments(self, qtbot: QtBot, transcription):
widget = TranscriptionViewerWidget(transcription)
def test_should_display_segments(
self, qtbot: QtBot, transcription, transcription_service, shortcuts
):
widget = TranscriptionViewerWidget(
transcription, transcription_service, shortcuts
)
qtbot.add_widget(widget)
assert widget.windowTitle() == "whisper-french.mp3"
@ -54,30 +55,15 @@ class TestTranscriptionViewerWidget:
assert editor.model().index(0, 2).data() == 40
assert editor.model().index(0, 3).data() == "Bien"
def test_should_update_segment_text(self, qtbot, transcription):
widget = TranscriptionViewerWidget(transcription)
def test_should_update_segment_text(
self, qtbot, transcription, transcription_service, shortcuts
):
widget = TranscriptionViewerWidget(
transcription, transcription_service, shortcuts
)
qtbot.add_widget(widget)
editor = widget.findChild(TranscriptionSegmentsEditorWidget)
assert isinstance(editor, TranscriptionSegmentsEditorWidget)
editor.model().setData(editor.model().index(0, 3), "Biens")
def test_should_export_segments(
self, tmp_path: pathlib.Path, qtbot: QtBot, transcription
):
widget = TranscriptionViewerWidget(transcription)
qtbot.add_widget(widget)
export_button = widget.findChild(QPushButton)
assert isinstance(export_button, QPushButton)
output_file_path = tmp_path / "whisper.txt"
with patch(
"PyQt6.QtWidgets.QFileDialog.getSaveFileName"
) as save_file_name_mock:
save_file_name_mock.return_value = (str(output_file_path), "")
export_button.menu().actions()[0].trigger()
with open(output_file_path, encoding="utf-8") as output_file:
assert "Bien\nvenue dans" in output_file.read()