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

feat: add folder watch #655

Merged
merged 8 commits into from
Dec 27, 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
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 @@
)

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

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

Check warning on line 136 in buzz/widgets/main_window.py

View check run for this annotation

Codecov / codecov/patch

buzz/widgets/main_window.py#L133-L136

Added lines #L133 - L136 were not covered by tests

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

Check warning on line 141 in buzz/widgets/main_window.py

View check run for this annotation

Codecov / codecov/patch

buzz/widgets/main_window.py#L139-L141

Added lines #L139 - L141 were not covered by tests

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

Check warning on line 250 in buzz/widgets/main_window.py

View check run for this annotation

Codecov / codecov/patch

buzz/widgets/main_window.py#L250

Added line #L250 was not covered by tests

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

Check warning on line 329 in buzz/widgets/main_window.py

View check run for this annotation

Codecov / codecov/patch

buzz/widgets/main_window.py#L329

Added line #L329 was not covered by tests
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
Loading