Skip to content
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
1 change: 0 additions & 1 deletion buzz/assets/redo_FILL0_wght700_GRAD0_opsz48.svg

This file was deleted.

1 change: 0 additions & 1 deletion buzz/assets/undo_FILL0_wght700_GRAD0_opsz48.svg

This file was deleted.

5 changes: 0 additions & 5 deletions buzz/pip_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ def is_flatpak() -> bool:
return "FLATPAK_ID" in os.environ


def _in_virtualenv() -> bool:
"""Returns True if running inside a virtualenv or uv venv."""
return sys.prefix != sys.base_prefix or "VIRTUAL_ENV" in os.environ


def subprocess_hide_window_kwargs() -> dict[str, Any]:
"""Return kwargs to hide the console window on Windows."""
if sys.platform == "win32":
Expand Down
5 changes: 0 additions & 5 deletions buzz/settings/shortcut.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import enum
import typing
from buzz.locale import _


Expand Down Expand Up @@ -37,7 +36,3 @@ def __new__(cls, sequence: str, description: str):

CLEAR_HISTORY = ("Ctrl+S", _("Clear History"))
STOP_TRANSCRIPTION = ("Ctrl+X", _("Cancel Transcription"))

@staticmethod
def get_default_shortcuts() -> typing.Dict[str, str]:
return {shortcut.name: shortcut.sequence for shortcut in Shortcut}
79 changes: 67 additions & 12 deletions buzz/store/keyring_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,42 @@ def _get_secrets_file_path() -> str:
return os.path.join(data_dir, ".secrets.json")


# The portal secret is fixed for the lifetime of the application, so it is
# fetched once per process. Each lookup is a D-Bus round trip that can block
# the UI thread, and get_password() is called from many widgets.
_PORTAL_SECRET_NOT_FETCHED = object()
_portal_secret_cache = _PORTAL_SECRET_NOT_FETCHED

# How long to wait for the portal to answer the RetrieveSecret request
PORTAL_RESPONSE_TIMEOUT_SECONDS = 5.0


def _get_portal_secret() -> bytes | None:
"""Get the application secret from XDG Desktop Portal.

The portal provides a per-application secret that can be used
for encrypting application-specific data. This works in sandboxed
environments (Snap/Flatpak) via the desktop plug.
"""
global _portal_secret_cache

if not _is_linux():
return None

if _portal_secret_cache is _PORTAL_SECRET_NOT_FETCHED:
_portal_secret_cache = _retrieve_portal_secret()

return _portal_secret_cache


def _retrieve_portal_secret() -> bytes | None:
try:
from jeepney import DBusAddress, new_method_call
from jeepney.io.blocking import open_dbus_connection
from jeepney import (
DBusAddress, HeaderFields, MatchRule, message_bus, new_method_call
)
from jeepney.io.blocking import Proxy, open_dbus_connection
import socket
import time

# Open connection with file descriptor support enabled
conn = open_dbus_connection(bus="SESSION", enable_fds=True)
Expand All @@ -52,25 +74,57 @@ def _get_portal_secret() -> bytes | None:
interface="org.freedesktop.portal.Secret",
)

# The portal reports the outcome with a Response signal on the
# returned request object. Subscribe before sending the request so
# the signal can not be missed.
response_rule = MatchRule(
type="signal",
interface="org.freedesktop.portal.Request",
member="Response",
)

# Create a socket pair for receiving the secret
sock_read, sock_write = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)

try:
# Build the method call with file descriptor
# RetrieveSecret(fd: h, options: a{sv}) -> (handle: o)
# Pass the socket object directly - jeepney handles fd passing
msg = new_method_call(portal, "RetrieveSecret", "ha{sv}", (sock_write, {}))
Proxy(message_bus, conn).AddMatch(response_rule)

with conn.filter(response_rule) as responses:
# Build the method call with file descriptor
# RetrieveSecret(fd: h, options: a{sv}) -> (handle: o)
# Pass the socket object directly - jeepney handles fd passing
msg = new_method_call(portal, "RetrieveSecret", "ha{sv}", (sock_write, {}))

# Send message and get reply
conn.send_and_get_reply(msg, timeout=10)
# Send message and get reply
reply = conn.send_and_get_reply(msg, timeout=10)
request_handle = reply.body[0]

# Close the write end - portal has it now
sock_write.close()
sock_write = None

# Wait for the Response of our request. A non zero code means
# the portal will not send a secret, e.g. when the app is not
# sandboxed, so there is no point waiting on the socket.
deadline = time.monotonic() + PORTAL_RESPONSE_TIMEOUT_SECONDS
while True:
signal = conn.recv_until_filtered(
responses, timeout=max(deadline - time.monotonic(), 0)
)
if signal.header.fields.get(HeaderFields.path) == request_handle:
response_code = signal.body[0]
break

# Close the write end - portal has it now
sock_write.close()
sock_write = None
if response_code != 0:
logging.debug(
"XDG Portal secret not available, response code %s",
response_code,
)
return None

# Read the secret from the read end
# The portal writes the secret and closes its end
sock_read.settimeout(5.0)
sock_read.settimeout(PORTAL_RESPONSE_TIMEOUT_SECONDS)
secret_data = b""
while True:
try:
Expand All @@ -90,6 +144,7 @@ def _get_portal_secret() -> bytes | None:
sock_read.close()
if sock_write is not None:
sock_write.close()
conn.close()

except Exception as exc:
logging.debug("XDG Portal secret not available: %s", exc)
Expand Down
10 changes: 0 additions & 10 deletions buzz/transcriber/transcriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,6 @@ class TranscriptionOptions:
transcription_step: float = 3.5


def humanize_language(language: str) -> str:
if language == "":
return _("Detect Language")
return LANGUAGES[language].title()


@dataclass()
class FileTranscriptionOptions:
file_paths: Optional[List[str]] = None
Expand Down Expand Up @@ -228,10 +222,6 @@ class OutputFormat(enum.Enum):
VTT = "vtt"


class Stopped(Exception):
pass


SUPPORTED_AUDIO_EXTENSIONS = (
".mp3", ".wav", ".m4a", ".m4b", ".aac", ".ogg", ".opus", ".flac",
)
Expand Down
3 changes: 0 additions & 3 deletions buzz/transcriber/whisper_file_transcriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@
import platform

from buzz.transcriber.cuda_device import cuda_works
import subprocess
from platformdirs import user_cache_dir
from multiprocessing.connection import Connection
from threading import Lock, Thread
from typing import Optional, List

import tqdm
import psutil
from PyQt6.QtCore import QObject

Expand Down Expand Up @@ -334,7 +332,6 @@ def transcribe_faster_whisper(cls, task: FileTranscriptionTask) -> List[Segment]
"Faster Whisper model is not available locally. "
"Check BUZZ_MODEL_ROOT and download the model into that cache first."
)
return []

model_root_dir = user_cache_dir("Buzz")
model_root_dir = os.path.join(model_root_dir, "models")
Expand Down
3 changes: 1 addition & 2 deletions buzz/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
import queue

from typing import Optional, List, Tuple
from openai import OpenAI, max_retries
from openai import OpenAI
from PyQt6.QtCore import QObject, pyqtSignal

from buzz.locale import _
from buzz.settings.settings import Settings
from buzz.store.keyring_store import get_password, Key
from buzz.transcriber.transcriber import TranscriptionOptions
Expand Down
16 changes: 9 additions & 7 deletions buzz/widgets/audio_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ def _init_sounddevice(self, file_path: str):
logging.warning("AudioPlayer: sounddevice init failed, using Qt fallback", exc_info=True)

def _init_media_player(self, file_path: str):
self.audio_output = QAudioOutput()
# Parent the multimedia objects to this widget so Qt destroys them along
# with it. Left parentless, they can outlive the widget until interpreter
# exit, where PyQt's cleanup segfaults on them.
self.audio_output = QAudioOutput(self)
self.audio_output.setVolume(100)
self.media_player = QMediaPlayer()
self.media_player = QMediaPlayer(self)
self.media_player.setSource(QUrl.fromLocalFile(file_path))
self.media_player.setAudioOutput(self.audio_output)

self.media_devices = QMediaDevices()
self.media_devices = QMediaDevices(self)
self.media_devices.audioOutputsChanged.connect(self.on_audio_outputs_changed)

if self.is_video:
Expand Down Expand Up @@ -258,9 +261,6 @@ def set_range(self, range_ms: Tuple[int, int]):
def clear_range(self):
self.range_ms = None

def _reset_looping_flag(self):
self.is_looping = False

def on_slider_moved(self, position_ms: int):
self.set_position(position_ms)
if self.range_ms is not None:
Expand Down Expand Up @@ -299,9 +299,11 @@ def on_audio_outputs_changed(self):
return
was_playing = self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState
position = self.media_player.position()
self.audio_output = QAudioOutput()
old_audio_output = self.audio_output
self.audio_output = QAudioOutput(self)
self.audio_output.setVolume(100)
self.media_player.setAudioOutput(self.audio_output)
old_audio_output.deleteLater()
self.media_player.setPosition(position)
if was_playing:
self.media_player.play()
Expand Down
15 changes: 0 additions & 15 deletions buzz/widgets/form_label.py

This file was deleted.

10 changes: 0 additions & 10 deletions buzz/widgets/icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,6 @@ def __init__(self, parent: QWidget):
super().__init__(get_path("assets/pause_black_24dp.svg"), parent)


class UndoIcon(Icon):
def __init__(self, parent: QWidget):
super().__init__(get_path("assets/undo_FILL0_wght700_GRAD0_opsz48.svg"), parent)


class RedoIcon(Icon):
def __init__(self, parent: QWidget):
super().__init__(get_path("assets/redo_FILL0_wght700_GRAD0_opsz48.svg"), parent)


class FileDownloadIcon(Icon):
def __init__(self, parent: QWidget):
super().__init__(get_path("assets/file_download_black_24dp.svg"), parent)
Expand Down
17 changes: 14 additions & 3 deletions buzz/widgets/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Tuple, List, Optional, Set
from uuid import UUID

from PyQt6 import QtGui
from PyQt6 import QtGui, sip
from PyQt6.QtCore import (
Qt,
QThread,
Expand Down Expand Up @@ -459,6 +459,12 @@ def open_transcription_viewer(self, transcription: Transcription):
flags=Qt.WindowType.Window,
transcriptions_updated_signal=self.transcriptions_updated,
)
# Free the viewer and its media player once closed. Otherwise every
# closed viewer lingers until exit, holding its decoded audio, and
# PyQt's cleanup at interpreter exit segfaults on its media player.
self.transcription_viewer_widget.setAttribute(
Qt.WidgetAttribute.WA_DeleteOnClose
)
self.transcription_viewer_widget.show()

def add_task(self, task: FileTranscriptionTask):
Expand Down Expand Up @@ -589,8 +595,13 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None:
if not self.transcriber_thread.wait(2000):
logging.error("Transcriber thread could not be terminated")

if self.transcription_viewer_widget is not None:
self.transcription_viewer_widget.close()
# Several viewers can be open at once, close them all. Delete them right
# away as the app may exit before their deferred deletion runs, and
# PyQt's cleanup at interpreter exit segfaults on their media players.
for viewer in self.findChildren(TranscriptionViewerWidget):
viewer.close()
sip.delete(viewer)
self.transcription_viewer_widget = None

try:
from buzz.widgets.application import Application
Expand Down
3 changes: 0 additions & 3 deletions buzz/widgets/transcriber/hugging_face_search_line_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,6 @@ def fetch_models(self):

return self.network_manager.get(QNetworkRequest(url))

def on_popup_selected(self):
self.timer.stop()

def on_request_response(self, network_reply: QNetworkReply):
if network_reply.error() != QNetworkReply.NetworkError.NoError:
logging.debug(
Expand Down
4 changes: 0 additions & 4 deletions buzz/widgets/transcriber/mms_language_line_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,3 @@ def _on_text_changed(self, text: str):
def language(self) -> str:
"""Get the current language code."""
return self.text().strip().lower()

def setLanguage(self, language: str):
"""Set the language code."""
self.setText(language.strip().lower() if language else "eng")
29 changes: 0 additions & 29 deletions buzz/widgets/transcription_viewer/speaker_identification_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,6 @@ def hide_cuda_from_torch(device):
torch.cuda.is_available = original_is_available


SENTENCE_END = re.compile(r'.*[.!?。!?]')

class IdentificationWorker(QObject):
finished = pyqtSignal(list)
progress_update = pyqtSignal(str)
Expand Down Expand Up @@ -188,33 +186,6 @@ def _get_language(self) -> Optional[str]:
return self.language
return self.transcription.language

def get_transcript(self, audio, **kwargs) -> dict:
buzz_segments = self._get_segments()

segments = []
words = []
text = ""
for buzz_segment in buzz_segments:
words.append({
'word': buzz_segment.text + " ",
'start': buzz_segment.start_time / 100,
'end': buzz_segment.end_time / 100,
})
text += buzz_segment.text + " "

if SENTENCE_END.match(buzz_segment.text):
segments.append({
'text': text,
'words': words
})
words = []
text = ""

return {
'language': self._get_language(),
'segments': segments
}

def _import_libraries(self):
from ctc_forced_aligner.ctc_forced_aligner import (
generate_emissions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,6 @@ def focusOutEvent(self, event):
super().focusOutEvent(event)


class TimeStampDelegate(QStyledItemDelegate):
def displayText(self, value, locale):
return to_timestamp(value)


class TimeStampEditorDelegate(QStyledItemDelegate):
"""Delegate for editing timestamps with overlap prevention"""

Expand Down
Loading
Loading