From 806546282dd36dd2b98a0382403a7f463ba983df Mon Sep 17 00:00:00 2001 From: Raivis Dejus Date: Fri, 25 Apr 2025 10:10:52 +0300 Subject: [PATCH] Transcript viewer UX improvements for translations (#1151) --- .github/workflows/ci.yml | 13 ++++ .github/workflows/manual-build.yml | 4 ++ buzz/widgets/application.py | 24 ++++--- .../export_transcription_menu.py | 69 ++++++++++--------- .../transcription_view_mode_tool_button.py | 17 ++++- .../transcription_viewer_widget.py | 22 +++++- docs/docs/preferences.md | 2 + .../widgets/export_transcription_menu_test.py | 9 +++ 8 files changed, 111 insertions(+), 49 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba3519ff..df043976 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ concurrency: jobs: test: runs-on: ${{ matrix.os }} + env: + BUZZ_DISABLE_TELEMETRY: true strategy: fail-fast: false matrix: @@ -95,6 +97,9 @@ jobs: build: runs-on: ${{ matrix.os }} + timeout-minutes: 60 + env: + BUZZ_DISABLE_TELEMETRY: true strategy: fail-fast: false matrix: @@ -224,6 +229,8 @@ jobs: build_wheels: runs-on: ${{ matrix.os }} + env: + BUZZ_DISABLE_TELEMETRY: true strategy: fail-fast: false matrix: @@ -256,6 +263,8 @@ jobs: publish_pypi: needs: [build_wheels, test] runs-on: ubuntu-latest + env: + BUZZ_DISABLE_TELEMETRY: true environment: pypi permissions: id-token: write @@ -274,6 +283,8 @@ jobs: release: runs-on: ${{ matrix.os }} + env: + BUZZ_DISABLE_TELEMETRY: true strategy: fail-fast: false matrix: @@ -315,6 +326,8 @@ jobs: deploy_brew_cask: runs-on: macos-latest + env: + BUZZ_DISABLE_TELEMETRY: true needs: [release] if: startsWith(github.ref, 'refs/tags/') steps: diff --git a/.github/workflows/manual-build.yml b/.github/workflows/manual-build.yml index f1ea1c04..ecbae668 100644 --- a/.github/workflows/manual-build.yml +++ b/.github/workflows/manual-build.yml @@ -9,6 +9,8 @@ concurrency: jobs: build: runs-on: ${{ matrix.os }} + env: + BUZZ_DISABLE_TELEMETRY: true strategy: fail-fast: false matrix: @@ -69,6 +71,8 @@ jobs: build-snap: runs-on: ubuntu-latest + env: + BUZZ_DISABLE_TELEMETRY: true outputs: snap: ${{ steps.snapcraft.outputs.snap }} steps: diff --git a/buzz/widgets/application.py b/buzz/widgets/application.py index ca49ecb2..5cac4516 100755 --- a/buzz/widgets/application.py +++ b/buzz/widgets/application.py @@ -1,4 +1,5 @@ import logging +import os import sys import locale import platform @@ -180,16 +181,19 @@ class Application(QApplication): self.window = MainWindow(transcription_service) - posthog = Posthog(project_api_key='phc_NqZQUw8NcxfSXsbtk5eCFylmCQpp4FuNnd6ocPAzg2f', - host='https://us.i.posthog.com') - posthog.capture(distinct_id=self.settings.get_user_identifier(), event="app_launched", properties={ - "app": VERSION, - "locale": locale.getdefaultlocale(), - "system": platform.system(), - "release": platform.release(), - "machine": platform.machine(), - "version": platform.version(), - }) + disable_telemetry = os.getenv("BUZZ_DISABLE_TELEMETRY", None) + + if not disable_telemetry: + posthog = Posthog(project_api_key='phc_NqZQUw8NcxfSXsbtk5eCFylmCQpp4FuNnd6ocPAzg2f', + host='https://us.i.posthog.com') + posthog.capture(distinct_id=self.settings.get_user_identifier(), event="app_launched", properties={ + "app": VERSION, + "locale": locale.getdefaultlocale(), + "system": platform.system(), + "release": platform.release(), + "machine": platform.machine(), + "version": platform.version(), + }) logging.debug(f"Launching Buzz: {VERSION}, " f"locale: {locale.getdefaultlocale()}, " diff --git a/buzz/widgets/transcription_viewer/export_transcription_menu.py b/buzz/widgets/transcription_viewer/export_transcription_menu.py index eb450ab0..2ef41a7f 100644 --- a/buzz/widgets/transcription_viewer/export_transcription_menu.py +++ b/buzz/widgets/transcription_viewer/export_transcription_menu.py @@ -1,5 +1,6 @@ import logging from PyQt6.QtGui import QAction +from PyQt6.QtCore import pyqtSignal from PyQt6.QtWidgets import QWidget, QMenu, QFileDialog from buzz.db.entity.transcription import Transcription @@ -17,45 +18,33 @@ class ExportTranscriptionMenu(QMenu): self, transcription: Transcription, transcription_service: TranscriptionService, + has_translation: bool, + translation: pyqtSignal, parent: QWidget | None = None, ): super().__init__(parent) self.transcription = transcription self.transcription_service = transcription_service - self.segments = [] - self.load_segments() - if self.segments and len(self.segments[0].translation) > 0: - text_label = _("Text") - translation_label = _("Translation") - actions = [ - action - for output_format in OutputFormat - for action in [ - QAction(text=f"{output_format.value.upper()} - {text_label}", parent=self), - QAction(text=f"{output_format.value.upper()} - {translation_label}", parent=self) - ] - ] - else: - actions = [ - QAction(text=output_format.value.upper(), parent=self) - for output_format in OutputFormat - ] + translation.connect(self.on_translation_available) + + text_label = _("Text") + translation_label = _("Translation") + self.text_actions = [ + QAction(text=f"{output_format.value.upper()} - {text_label}", parent=self) + for output_format in OutputFormat + ] + self.translation_actions = [ + QAction(text=f"{output_format.value.upper()} - {translation_label}", parent=self) + for output_format in OutputFormat + ] + for action in self.translation_actions: + action.setVisible(has_translation) + actions = self.text_actions + self.translation_actions self.addActions(actions) self.triggered.connect(self.on_menu_triggered) - def load_segments(self): - self.segments = [ - Segment( - start=segment.start_time, - end=segment.end_time, - text=segment.text, - translation=segment.translation) - for segment in self.transcription_service.get_transcription_segments( - transcription_id=self.transcription.id_as_uuid - ) - ] @staticmethod def extract_format_and_segment_key(action_text: str): parts = action_text.split('-') @@ -65,9 +54,24 @@ class ExportTranscriptionMenu(QMenu): return output_format, segment_key + def on_translation_available(self): + for action in self.translation_actions: + action.setVisible(True) + def on_menu_triggered(self, action: QAction): + segments = [ + Segment( + start=segment.start_time, + end=segment.end_time, + text=segment.text, + translation=segment.translation) + for segment in self.transcription_service.get_transcription_segments( + transcription_id=self.transcription.id_as_uuid + ) + ] + output_format_value, segment_key = self.extract_format_and_segment_key(action.text()) - output_format = OutputFormat[output_format_value] + output_format = OutputFormat(output_format_value.lower()) default_path = self.transcription.get_output_file_path( output_format=output_format @@ -83,12 +87,9 @@ class ExportTranscriptionMenu(QMenu): if output_file_path == "": return - # Reload segments in case they were resized - self.load_segments() - write_output( path=output_file_path, - segments=self.segments, + segments=segments, output_format=output_format, segment_key=segment_key ) diff --git a/buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py b/buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py index 24f7c94c..a660e015 100644 --- a/buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py +++ b/buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py @@ -1,3 +1,4 @@ +import logging from enum import Enum from typing import Optional @@ -20,7 +21,13 @@ class ViewMode(Enum): class TranscriptionViewModeToolButton(QToolButton): view_mode_changed = pyqtSignal(ViewMode) - def __init__(self, shortcuts: Shortcuts, parent: Optional[QWidget] = None): + def __init__( + self, + shortcuts: Shortcuts, + has_translation: bool, + translation: pyqtSignal, + parent: Optional[QWidget] = None + ): super().__init__(parent) self.setText(_("View")) @@ -28,6 +35,8 @@ class TranscriptionViewModeToolButton(QToolButton): self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + translation.connect(self.on_translation_available) + menu = QMenu(self) menu.addAction( @@ -36,11 +45,12 @@ class TranscriptionViewModeToolButton(QToolButton): lambda: self.view_mode_changed.emit(ViewMode.TEXT), ) - menu.addAction( + self.translation_action = menu.addAction( _("Translation"), QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TRANSLATION)), lambda: self.view_mode_changed.emit(ViewMode.TRANSLATION) ) + self.translation_action.setVisible(has_translation) menu.addAction( _("Timestamps"), @@ -49,3 +59,6 @@ class TranscriptionViewModeToolButton(QToolButton): ) self.setMenu(menu) + + def on_translation_available(self): + self.translation_action.setVisible(True) diff --git a/buzz/widgets/transcription_viewer/transcription_viewer_widget.py b/buzz/widgets/transcription_viewer/transcription_viewer_widget.py index 11b6ba95..36f71fd0 100644 --- a/buzz/widgets/transcription_viewer/transcription_viewer_widget.py +++ b/buzz/widgets/transcription_viewer/transcription_viewer_widget.py @@ -78,6 +78,12 @@ class TranscriptionViewerWidget(QWidget): self.translator = None self.view_mode = ViewMode.TIMESTAMPS + # Can't reuse this globally, as transcripts may get translated, so need to get them each time + segments = self.transcription_service.get_transcription_segments( + transcription_id=self.transcription.id_as_uuid + ) + self.has_translations = any(segment.translation.strip() for segment in segments) + self.openai_access_token = get_password(Key.OPENAI_API_KEY) preferences = self.load_preferences() @@ -138,7 +144,11 @@ class TranscriptionViewerWidget(QWidget): toolbar = ToolBar(self) - view_mode_tool_button = TranscriptionViewModeToolButton(shortcuts, self) + view_mode_tool_button = TranscriptionViewModeToolButton( + shortcuts, + self.has_translations, + self.translator.translation, + ) view_mode_tool_button.view_mode_changed.connect(self.on_view_mode_changed) toolbar.addWidget(view_mode_tool_button) @@ -150,7 +160,11 @@ class TranscriptionViewerWidget(QWidget): ) export_transcription_menu = ExportTranscriptionMenu( - transcription, transcription_service, self + transcription, + transcription_service, + self.has_translations, + self.translator.translation, + self ) export_tool_button.setMenu(export_transcription_menu) export_tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) @@ -192,6 +206,7 @@ class TranscriptionViewerWidget(QWidget): if self.view_mode == ViewMode.TIMESTAMPS: self.text_display_box.hide() self.table_widget.show() + self.audio_player.show() elif self.view_mode == ViewMode.TEXT: segments = self.transcription_service.get_transcription_segments( transcription_id=self.transcription.id_as_uuid @@ -209,8 +224,8 @@ class TranscriptionViewerWidget(QWidget): self.text_display_box.setPlainText(combined_text.strip()) self.text_display_box.show() self.table_widget.hide() + self.audio_player.hide() else: # ViewMode.TRANSLATION - # TODO add check for if translation exists segments = self.transcription_service.get_transcription_segments( transcription_id=self.transcription.id_as_uuid ) @@ -219,6 +234,7 @@ class TranscriptionViewerWidget(QWidget): ) self.text_display_box.show() self.table_widget.hide() + self.audio_player.hide() def on_view_mode_changed(self, view_mode: ViewMode) -> None: self.view_mode = view_mode diff --git a/docs/docs/preferences.md b/docs/docs/preferences.md index 26397354..10cc3b1d 100644 --- a/docs/docs/preferences.md +++ b/docs/docs/preferences.md @@ -98,3 +98,5 @@ Defaults to [user_cache_dir](https://pypi.org/project/platformdirs/). **BUZZ_FORCE_CPU** - Will force Buzz to use CPU and not GPU, useful for setups with older GPU if that is slower than GPU or GPU has issues. Example usage `BUZZ_FORCE_CPU=true`. Available since `1.2.1` **BUZZ_MERGE_REGROUP_RULE** - Custom regroup merge rule to use when combining transcripts with word-level timings. More information on available options [in stable-ts repo](https://github.com/jianfch/stable-ts?tab=readme-ov-file#regrouping-methods). Available since `1.3.0` + +**BUZZ_DISABLE_TELEMETRY** - Buzz collects basic OS name and architecture usage statistics to better focus development efforts. This variable lets disable collection of these statistics. Example usage `BUZZ_DISABLE_TELEMETRY=true`. Available since `1.3.0` \ No newline at end of file diff --git a/tests/widgets/export_transcription_menu_test.py b/tests/widgets/export_transcription_menu_test.py index e326b809..7c15f1c4 100644 --- a/tests/widgets/export_transcription_menu_test.py +++ b/tests/widgets/export_transcription_menu_test.py @@ -2,6 +2,7 @@ import pathlib import uuid import pytest +from PyQt6.QtCore import QObject, pyqtSignal from pytestqt.qtbot import QtBot from buzz.db.entity.transcription import Transcription @@ -14,6 +15,10 @@ from buzz.widgets.transcription_viewer.export_transcription_menu import ( from tests.audio import test_audio_path +class TranslationSignal(QObject): + translation = pyqtSignal(str, int) + + class TestExportTranscriptionMenu: @pytest.fixture() def transcription( @@ -52,9 +57,13 @@ class TestExportTranscriptionMenu: return_value=(str(output_file_path), ""), ) + translation_signal = TranslationSignal() + widget = ExportTranscriptionMenu( transcription, transcription_service, + False, + translation_signal.translation ) qtbot.add_widget(widget)