Skip to content

Commit

Permalink
Merge pull request #63 from ieasybooks/feature/62/Integrate_the_UI_wi…
Browse files Browse the repository at this point in the history
…th_Tafrigh
  • Loading branch information
yshalsager authored Jul 31, 2023
2 parents a78f565 + 8140632 commit 14f52fb
Show file tree
Hide file tree
Showing 20 changed files with 529 additions and 149 deletions.
40 changes: 20 additions & 20 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# https://pre-commit.com
ci:
skip: [qmlformat, qmllint]
#ci:
# skip: [qmlformat, qmllint]
default_install_hook_types: [ commit-msg, pre-commit ]
default_stages: [ commit, manual ]
fail_fast: true
Expand Down Expand Up @@ -67,21 +67,21 @@ repos:
rev: 'v1.4.1' # Use the sha / tag you want to point at
hooks:
- id: mypy
- repo: local
hooks:
- id: qmlformat
name: qmlformat
entry: qmlformat -i
pass_filenames: true
require_serial: true
language: system
types: [ text ]
files: ^.*\.qml$
- id: qmllint
name: qmllint
entry: qmllint
pass_filenames: true
require_serial: true
language: system
types: [ text ]
files: ^.*\.qml$
# - repo: local
# hooks:
# - id: qmlformat
# name: qmlformat
# entry: qmlformat -i
# pass_filenames: true
# require_serial: true
# language: system
# types: [ text ]
# files: ^.*\.qml$
# - id: qmllint
# name: qmllint
# entry: qmllint
# pass_filenames: true
# require_serial: true
# language: system
# types: [ text ]
# files: ^.*\.qml$
18 changes: 0 additions & 18 deletions settings.ini

This file was deleted.

50 changes: 0 additions & 50 deletions src/controller.py

This file was deleted.

1 change: 1 addition & 0 deletions src/domain/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Domain package."""
101 changes: 101 additions & 0 deletions src/domain/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Backend that interacts with tafrigh."""
from platform import system
from subprocess import Popen
from typing import Any

from PySide6.QtCore import Property, QObject, QThreadPool, Signal, Slot
from tafrigh import Config, farrigh

from domain.config import AppConfig, CaseSensitiveConfigParser
from domain.progress import Progress
from domain.threadpool import Worker, WorkerSignals


# BACKEND
class Backend(QObject):

"""Backend object."""

result = Signal(dict)
progress = Signal(int, int)
error = Signal(tuple)
finish = Signal()

def __init__(self, parent: QObject | None = None) -> None:
"""Initialize backend object."""
super().__init__(parent=parent)
self.signals = WorkerSignals()
self.threadpool = QThreadPool()
self._is_running = False
self._urls: list[str] = []

def on_error(self, error: tuple[str, int, str]) -> None:
self.error.emit(error)
self._is_running = False

def on_result(self, result: dict[str, Any]) -> None:
self.result.emit(result)

def on_progress(self, progress: Progress) -> None:
self.progress.emit(progress.value, progress.remaining_time)

def on_finish(self) -> None:
self.finish.emit()

@Slot()
def start(self) -> None:
worker = Worker(func=self.run)
worker.signals.finished.connect(self.on_finish)
worker.signals.progress.connect(self.on_progress)
worker.signals.error.connect(self.on_error)
worker.signals.result.connect(self.on_result)
self.threadpool.start(worker)

@Property(list)
def urls(self) -> list[str]:
return self._urls

@urls.setter # type: ignore[no-redef]
def urls(self, value: list[str]):
self._urls = [x.replace("file:///", "") for x in value]

def run(self, *args: Any, **kwargs: Any) -> Any:
app_config: AppConfig = CaseSensitiveConfigParser.read_config()

config = Config(
urls_or_paths=self.urls,
playlist_items="",
verbose=False,
skip_if_output_exist=True,
model_name_or_path=app_config.whisper_model,
task="transcribe",
language=app_config.convert_language,
use_whisper_jax=False,
use_faster_whisper=True,
beam_size=5,
ct2_compute_type="default",
wit_client_access_token=app_config.wit_convert_key
if app_config.is_wit_engine
else None,
max_cutting_duration=int(app_config.max_part_length),
min_words_per_segment=app_config.word_count,
save_files_before_compact=False,
save_yt_dlp_responses=app_config.download_json,
output_sample=0,
output_formats=app_config.get_output_formats(),
output_dir=app_config.save_location.replace("file:///", ""),
)

return farrigh(config)

@Slot(str)
@staticmethod
def open_folder(path: str) -> None:
if system() == "Windows":
from os import startfile # type: ignore[attr-defined]

startfile(path) # noqa: S606
elif system() == "Darwin":
Popen(["open", path], shell=False) # noqa: S603, S607
else:
Popen(["xdg-open", path], shell=False) # noqa: S603, S607
File renamed without changes.
61 changes: 61 additions & 0 deletions src/domain/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""A module contains the configuration parser.
It is used to read the settings.ini file and convert it to a pydantic model.
"""
import re
from configparser import ConfigParser
from typing import TypeVar, cast

from pydantic import BaseModel

T = TypeVar("T", bound=BaseModel)


class AppConfig(BaseModel):

"""App configuration model."""

download_json: bool
convert_engine: str
save_location: str
word_count: int
is_wit_engine: bool
export_vtt: bool
drop_empty_parts: bool
max_part_length: float
wit_convert_key: str
whisper_model: str
convert_language: str
export_srt: bool
export_txt: bool

def get_output_formats(self) -> list[str]:
formats = {"srt": self.export_srt, "vtt": self.export_vtt, "txt": self.export_txt}
return [key for key, value in formats.items() if value]


class CaseSensitiveConfigParser(ConfigParser):

"""A case sensitive config parser."""

def optionxform(self, option_str: str) -> str:
return option_str

@staticmethod
def camel_to_snake(camel_case: str) -> str:
snake_case = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", camel_case)
return snake_case.lower()

@classmethod
def read_config(
cls,
model: type[T] = AppConfig,
filename: str = "settings.ini",
default_section: str = "config",
) -> T:
parser = cls(default_section=default_section)
parser.read(filename)

data = {cls.camel_to_snake(key): value for key, value in parser.defaults().items()}

return cast(T, model(**data))
11 changes: 11 additions & 0 deletions src/domain/progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Progress domain model."""
from dataclasses import dataclass


@dataclass
class Progress:

"""Progress data class."""

value: float = 0.0
remaining_time: float | None = None
58 changes: 58 additions & 0 deletions src/domain/threadpool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Custom thread class and signals emitted by worker threads."""
import sys
import traceback
from collections.abc import Callable, Generator
from typing import Any

from PySide6.QtCore import QObject, QRunnable, Signal

from domain.progress import Progress


class WorkerSignals(QObject):

"""Signals emitted by worker threads."""

finished = Signal()
error = Signal(tuple)
result = Signal(dict)
progress = Signal(Progress)


class Worker(QRunnable):

"""Custom thread class."""

def __init__(
self, func: Callable[..., Generator[dict[str, int], None, None]], *args: Any, **kwargs: Any
) -> None:
"""Initialize worker object."""
super().__init__()
self.func = func
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()

def run(self) -> None:
try:
results = self.func(args=self.args, kwargs=self.kwargs)
for result in results:
progress_value, remaining_time = result["progress"], result["remaining_time"]
progress = Progress(
value=progress_value,
remaining_time=remaining_time,
)
self.signals.progress.emit(progress)
self.signals.result.emit(result)
except Exception: # noqa: BLE001
traceback.print_exc()
exc_type, value = sys.exc_info()[:2]
self.signals.error.emit((exc_type, value, traceback.format_exc()))
finally:
try:
self.signals.finished.emit()
except RuntimeError:
return

def stop(self) -> None:
self.terminate()
12 changes: 7 additions & 5 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from PySide6.QtQuickControls2 import QQuickStyle
from PySide6.QtWidgets import QApplication

from clipboardproxy import ClipboardProxy
from controller import Controller
from domain.backend import Backend
from domain.clipboardproxy import ClipboardProxy


QQuickStyle.setStyle("Material")

Expand All @@ -23,12 +24,13 @@
app.setApplicationName("Almufaragh")

engine = QQmlApplicationEngine()
controller = Controller()
backend = Backend()
clipboard = QApplication.clipboard()
clipboard_proxy = ClipboardProxy(clipboard)

# Expose the Python object to QML
engine.rootContext().setContextProperty("controller", controller)
engine.quit.connect(app.quit)
engine.rootContext().setContextProperty("backend", backend)
engine.rootContext().setContextProperty("clipboard", clipboard_proxy)
path: Path = Path(__file__).parent / "qml"

Expand All @@ -40,7 +42,7 @@
app.exec()

# Load Main Window
engine.load(QUrl.fromLocalFile(path / "main.qml"))
engine.load(QUrl.fromLocalFile((path / "main.qml")))

if not engine.rootObjects():
sys.exit(-1)
Expand Down
Loading

0 comments on commit 14f52fb

Please sign in to comment.