Skip to content

Commit

Permalink
feat: add folder watch (#655)
Browse files Browse the repository at this point in the history
  • Loading branch information
chidiwilliams authored Dec 27, 2023
1 parent 1120723 commit 2b839c3
Show file tree
Hide file tree
Showing 23 changed files with 952 additions and 194 deletions.
4 changes: 2 additions & 2 deletions buzz/settings/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ def value(
def clear(self):
self.settings.clear()

def begin_group(self, group: Key):
def begin_group(self, group: Key) -> None:
self.settings.beginGroup(group.value)

def end_group(self):
def end_group(self) -> None:
self.settings.endGroup()

def sync(self):
Expand Down
35 changes: 26 additions & 9 deletions buzz/transcriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import math
import multiprocessing
import os
import shutil
import subprocess
import sys
import tempfile
Expand Down Expand Up @@ -96,6 +97,10 @@ class Status(enum.Enum):
FAILED = "failed"
CANCELED = "canceled"

class Source(enum.Enum):
FILE_IMPORT = "file_import"
FOLDER_WATCH = "folder_watch"

file_path: str
transcription_options: TranscriptionOptions
file_transcription_options: FileTranscriptionOptions
Expand All @@ -108,6 +113,8 @@ class Status(enum.Enum):
queued_at: Optional[datetime.datetime] = None
started_at: Optional[datetime.datetime] = None
completed_at: Optional[datetime.datetime] = None
output_directory: Optional[str] = None
source: Source = Source.FILE_IMPORT

def status_text(self) -> str:
if self.status == FileTranscriptionTask.Status.IN_PROGRESS:
Expand Down Expand Up @@ -169,14 +176,23 @@ def run(self):
for (
output_format
) in self.transcription_task.file_transcription_options.output_formats:
default_path = get_default_output_file_path(
default_path = get_output_file_path(
task=self.transcription_task, output_format=output_format
)

write_output(
path=default_path, segments=segments, output_format=output_format
)

if self.transcription_task.source == FileTranscriptionTask.Source.FOLDER_WATCH:
shutil.move(
self.transcription_task.file_path,
os.path.join(
self.transcription_task.output_directory,
os.path.basename(self.transcription_task.file_path),
),
)

@abstractmethod
def transcribe(self) -> List[Segment]:
...
Expand Down Expand Up @@ -644,24 +660,22 @@ def segments_to_text(segments: List[Segment]) -> str:

def to_timestamp(ms: float, ms_separator=".") -> str:
hr = int(ms / (1000 * 60 * 60))
ms = ms - hr * (1000 * 60 * 60)
ms -= hr * (1000 * 60 * 60)
min = int(ms / (1000 * 60))
ms = ms - min * (1000 * 60)
ms -= min * (1000 * 60)
sec = int(ms / 1000)
ms = int(ms - sec * 1000)
return f"{hr:02d}:{min:02d}:{sec:02d}{ms_separator}{ms:03d}"


SUPPORTED_OUTPUT_FORMATS = "Audio files (*.mp3 *.wav *.m4a *.ogg);;\
SUPPORTED_AUDIO_FORMATS = "Audio files (*.mp3 *.wav *.m4a *.ogg);;\
Video files (*.mp4 *.webm *.ogm *.mov);;All files (*.*)"


def get_default_output_file_path(
task: FileTranscriptionTask, output_format: OutputFormat
):
input_file_name = os.path.splitext(task.file_path)[0]
def get_output_file_path(task: FileTranscriptionTask, output_format: OutputFormat):
input_file_name = os.path.splitext(os.path.basename(task.file_path))[0]
date_time_now = datetime.datetime.now().strftime("%d-%b-%Y %H-%M-%S")
return (
output_file_name = (
task.file_transcription_options.default_output_file_name.replace(
"{{ input_file_name }}", input_file_name
)
Expand All @@ -678,6 +692,9 @@ def get_default_output_file_path(
+ f".{output_format.value}"
)

output_directory = task.output_directory or os.path.dirname(task.file_path)
return os.path.join(output_directory, output_file_name)


def whisper_cpp_params(
language: str,
Expand Down
2 changes: 1 addition & 1 deletion buzz/widgets/audio_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def __init__(self, file_path: str):
self.media_player.playbackStateChanged.connect(self.on_playback_state_changed)
self.media_player.mediaStatusChanged.connect(self.on_media_status_changed)

self.update_time_label()
self.on_duration_changed(self.media_player.duration())

def on_duration_changed(self, duration_ms: int):
self.scrubber.setRange(0, duration_ms)
Expand Down
45 changes: 33 additions & 12 deletions buzz/widgets/icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,48 @@
from buzz.assets import get_asset_path


# TODO: move icons to Qt resources: https://stackoverflow.com/a/52341917/9830227
class Icon(QIcon):
LIGHT_THEME_BACKGROUND = "#555"
DARK_THEME_BACKGROUND = "#EEE"
LIGHT_THEME_COLOR = "#555"
DARK_THEME_COLOR = "#EEE"

def __init__(self, path: str, parent: QWidget):
# Adapted from https://stackoverflow.com/questions/15123544/change-the-color-of-an-svg-in-qt
is_dark_theme = parent.palette().window().color().black() > 127
color = self.get_color(is_dark_theme)

super().__init__()
self.path = path
self.parent = parent

self.color = self.get_color()
normal_pixmap = self.create_default_pixmap(self.path, self.color)
disabled_pixmap = self.create_disabled_pixmap(normal_pixmap, self.color)
self.addPixmap(normal_pixmap, QIcon.Mode.Normal)
self.addPixmap(disabled_pixmap, QIcon.Mode.Disabled)

# https://stackoverflow.com/questions/15123544/change-the-color-of-an-svg-in-qt
def create_default_pixmap(self, path, color):
pixmap = QPixmap(path)
painter = QPainter(pixmap)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
painter.fillRect(pixmap.rect(), QColor(color))
painter.fillRect(pixmap.rect(), color)
painter.end()
return pixmap

def create_disabled_pixmap(self, pixmap, color):
disabled_pixmap = QPixmap(pixmap.size())
disabled_pixmap.fill(QColor(0, 0, 0, 0))

super().__init__(pixmap)
painter = QPainter(disabled_pixmap)
painter.setOpacity(0.4)
painter.drawPixmap(0, 0, pixmap)
painter.setCompositionMode(
QPainter.CompositionMode.CompositionMode_DestinationIn
)
painter.fillRect(disabled_pixmap.rect(), color)
painter.end()
return disabled_pixmap

def get_color(self, is_dark_theme):
return (
self.DARK_THEME_BACKGROUND if is_dark_theme else self.LIGHT_THEME_BACKGROUND
def get_color(self) -> QColor:
is_dark_theme = self.parent.palette().window().color().black() > 127
return QColor(
self.DARK_THEME_COLOR if is_dark_theme else self.LIGHT_THEME_COLOR
)


Expand Down
65 changes: 50 additions & 15 deletions buzz/widgets/main_window.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from typing import Dict, Optional, Tuple, List
from typing import Dict, Tuple, List

from PyQt6 import QtGui
from PyQt6.QtCore import pyqtSignal, Qt, QThread, QModelIndex
from PyQt6.QtCore import (
Qt,
QThread,
QModelIndex,
)
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMainWindow, QMessageBox, QFileDialog

Expand All @@ -15,12 +19,16 @@
FileTranscriptionTask,
TranscriptionOptions,
FileTranscriptionOptions,
SUPPORTED_OUTPUT_FORMATS,
SUPPORTED_AUDIO_FORMATS,
)
from buzz.widgets.icon import BUZZ_ICON_PATH
from buzz.widgets.main_window_toolbar import MainWindowToolbar
from buzz.widgets.menu_bar import MenuBar
from buzz.widgets.preferences_dialog.models.preferences import Preferences
from buzz.widgets.transcriber.file_transcriber_widget import FileTranscriberWidget
from buzz.widgets.transcription_task_folder_watcher import (
TranscriptionTaskFolderWatcher,
)
from buzz.widgets.transcription_tasks_table_widget import TranscriptionTasksTableWidget
from buzz.widgets.transcription_viewer.transcription_viewer_widget import (
TranscriptionViewerWidget,
Expand All @@ -30,8 +38,6 @@
class MainWindow(QMainWindow):
table_widget: TranscriptionTasksTableWidget
tasks: Dict[int, "FileTranscriptionTask"]
tasks_changed = pyqtSignal()
openai_access_token: Optional[str]

def __init__(self, tasks_cache=TasksCache()):
super().__init__(flags=Qt.WindowType.Window)
Expand All @@ -54,7 +60,6 @@ def __init__(self, tasks_cache=TasksCache()):
)

self.tasks = {}
self.tasks_changed.connect(self.on_tasks_changed)

self.toolbar = MainWindowToolbar(shortcuts=self.shortcuts, parent=self)
self.toolbar.new_transcription_action_triggered.connect(
Expand All @@ -72,9 +77,11 @@ def __init__(self, tasks_cache=TasksCache()):
self.addToolBar(self.toolbar)
self.setUnifiedTitleAndToolBarOnMac(True)

self.preferences = self.load_preferences(settings=self.settings)
self.menu_bar = MenuBar(
shortcuts=self.shortcuts,
default_export_file_name=self.default_export_file_name,
preferences=self.preferences,
parent=self,
)
self.menu_bar.import_action_triggered.connect(
Expand All @@ -87,6 +94,7 @@ def __init__(self, tasks_cache=TasksCache()):
self.menu_bar.default_export_file_name_changed.connect(
self.default_export_file_name_changed
)
self.menu_bar.preferences_changed.connect(self.on_preferences_changed)
self.setMenuBar(self.menu_bar)

self.table_widget = TranscriptionTasksTableWidget(self)
Expand All @@ -113,6 +121,31 @@ def __init__(self, tasks_cache=TasksCache()):

self.load_geometry()

self.folder_watcher = TranscriptionTaskFolderWatcher(
tasks=self.tasks,
preferences=self.preferences.folder_watch,
default_export_file_name=self.default_export_file_name,
)
self.folder_watcher.task_found.connect(self.add_task)
self.folder_watcher.find_tasks()

def on_preferences_changed(self, preferences: Preferences):
self.preferences = preferences
self.save_preferences(preferences)
self.folder_watcher.set_preferences(preferences.folder_watch)
self.folder_watcher.find_tasks()

def save_preferences(self, preferences: Preferences):
self.settings.settings.beginGroup("preferences")
preferences.save(self.settings.settings)
self.settings.settings.endGroup()

def load_preferences(self, settings: Settings):
settings.settings.beginGroup("preferences")
preferences = Preferences.load(settings.settings)
settings.settings.endGroup()
return preferences

def dragEnterEvent(self, event):
# Accept file drag events
if event.mimeData().hasUrls():
Expand All @@ -134,13 +167,13 @@ def on_file_transcriber_triggered(
)
self.add_task(task)

def load_task(self, task: FileTranscriptionTask):
def upsert_task_in_table(self, task: FileTranscriptionTask):
self.table_widget.upsert_task(task)
self.tasks[task.id] = task

def update_task_table_row(self, task: FileTranscriptionTask):
self.load_task(task=task)
self.tasks_changed.emit()
self.upsert_task_in_table(task=task)
self.on_tasks_changed()

@staticmethod
def task_completed_or_errored(task: FileTranscriptionTask):
Expand All @@ -158,7 +191,8 @@ def on_clear_history_action_triggered(self):
self,
_("Clear History"),
_(
"Are you sure you want to delete the selected transcription(s)? This action cannot be undone."
"Are you sure you want to delete the selected transcription(s)? "
"This action cannot be undone."
),
)
if reply == QMessageBox.StandardButton.Yes:
Expand All @@ -169,7 +203,7 @@ def on_clear_history_action_triggered(self):
for task_id in task_ids:
self.table_widget.clear_task(task_id)
self.tasks.pop(task_id)
self.tasks_changed.emit()
self.on_tasks_changed()

def on_stop_transcription_action_triggered(self):
selected_rows = self.table_widget.selectionModel().selectedRows()
Expand All @@ -178,13 +212,13 @@ def on_stop_transcription_action_triggered(self):
task = self.tasks[task_id]

task.status = FileTranscriptionTask.Status.CANCELED
self.tasks_changed.emit()
self.on_tasks_changed()
self.transcriber_worker.cancel_task(task_id)
self.table_widget.upsert_task(task)

def on_new_transcription_action_triggered(self):
(file_paths, __) = QFileDialog.getOpenFileNames(
self, _("Select audio file"), "", SUPPORTED_OUTPUT_FORMATS
self, _("Select audio file"), "", SUPPORTED_AUDIO_FORMATS
)
if len(file_paths) == 0:
return
Expand Down Expand Up @@ -213,6 +247,7 @@ def default_export_file_name_changed(self, default_export_file_name: str):
self.settings.set_value(
Settings.Key.DEFAULT_EXPORT_FILE_NAME, default_export_file_name
)
self.folder_watcher.default_export_file_name = default_export_file_name

def open_transcript_viewer(self):
selected_rows = self.table_widget.selectionModel().selectedRows()
Expand Down Expand Up @@ -291,9 +326,9 @@ def load_tasks_from_cache(self):
or task.status == FileTranscriptionTask.Status.IN_PROGRESS
):
task.status = None
self.transcriber_worker.add_task(task)
self.add_task(task)
else:
self.load_task(task=task)
self.upsert_task_in_table(task=task)

def save_tasks_to_cache(self):
self.tasks_cache.save(list(self.tasks.values()))
Expand Down
Loading

0 comments on commit 2b839c3

Please sign in to comment.