Compare commits

...

2 commits

Author SHA1 Message Date
Raivis Dejus 8939447d58 Adding test fixes 2024-06-14 23:28:05 +03:00
Raivis Dejus 99e8dd20a7 Adding translations to transcription view 2024-06-14 19:14:13 +03:00
30 changed files with 539 additions and 120 deletions

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="anna_vital_language_icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
<path d="M62.4,101c-1.5-2.1-2.1-3.4-1.8-3.9c0.2-0.5,1.6-0.7,3.9-0.5c2.3,0.2,4.2,0.5,5.8,0.9c1.5,0.4,2.8,1,3.8,1.7
c1,0.7,1.8,1.5,2.3,2.6c0.6,1,1,2.3,1.4,3.7c0.7,2.8,0.5,4.7-0.5,5.7c-1.1,1-2.6,0.8-4.6-0.6c-2.1-1.4-3.9-2.8-5.5-4.2
C65.5,105.1,63.9,103.2,62.4,101z M40.7,190.1c4.8-2.1,9-4.2,12.6-6.4c3.5-2.1,6.6-4.4,9.3-6.8c2.6-2.3,5-4.9,7-7.7
c2-2.7,3.8-5.8,5.4-9.2c1.3,1.2,2.5,2.4,3.8,3.5c1.2,1.1,2.5,2.2,3.8,3.4c1.3,1.2,2.8,2.4,4.3,3.8c1.5,1.4,3.3,2.8,5.3,4.5
c0.7,0.5,1.4,0.9,2.1,1c0.7,0.1,1.7,0,3.1-0.6c1.3-0.5,3-1.4,5.1-2.8c2.1-1.3,4.7-3.1,7.9-5.4c1.6-1.1,2.4-2,2.3-2.7
c-0.1-0.7-1-1-2.7-0.9c-3.1,0.1-5.9,0.1-8.3-0.1c-2.5-0.2-5-0.6-7.4-1.4c-2.4-0.8-4.9-1.9-7.5-3.4c-2.6-1.5-5.6-3.6-9.1-6.2
c1-3.9,1.8-8,2.4-12.4c0.3-2.5,0.6-4.3,0.8-5.6c0.2-1.2,0.5-2.4,0.9-3.3c0.3-0.8,0.4-1.4,0.5-1.9c0.1-0.5-0.1-1-0.4-1.6
c-0.4-0.5-1-1.1-1.9-1.7c-0.9-0.6-2.2-1.4-3.9-2.3c2.4-0.9,5.1-1.7,7.9-2.6c2.7-0.9,5.7-1.8,8.8-2.7c3-0.9,4.5-1.9,4.6-3.1
c0.1-1.2-0.9-2.3-3.2-3.5c-1.5-0.8-2.9-1.1-4.3-0.9c-1.4,0.2-3.2,0.9-5.4,2.2c-0.6,0.4-1.8,0.9-3.4,1.6c-1.7,0.7-3.6,1.5-6,2.5
c-2.4,1-5,2-7.8,3.1c-2.9,1.1-5.8,2.2-8.7,3.2c-2.9,1.1-5.7,2-8.2,2.8c-2.6,0.8-4.6,1.4-6.1,1.6c-3.8,0.8-5.8,1.6-5.9,2.4
c0,0.8,1.5,1.6,4.4,2.4c1.2,0.3,2.3,0.6,3.1,0.6c0.8,0.1,1.7,0.1,2.5,0c0.8-0.1,1.6-0.3,2.4-0.5c0.8-0.3,1.7-0.7,2.8-1.1
c1.6-0.8,3.9-1.7,6.9-2.8c2.9-1,6.6-2.4,11.2-4c0.9,2.7,1.4,6,1.4,9.8c0,3.8-0.4,8.1-1.4,13c-1.3-1.1-2.7-2.3-4.2-3.6
c-1.5-1.3-2.9-2.6-4.3-3.9c-1.6-1.5-3.2-2.5-4.7-3c-1.6-0.5-3.4-0.5-5.5,0c-3.3,0.9-5,1.9-4.9,3.1c0,1.2,1.3,1.8,3.8,1.9
c0.9,0.1,1.8,0.3,2.7,0.6c0.9,0.3,1.9,0.9,3.2,1.8c1.3,0.9,2.9,2.2,4.7,3.8c1.8,1.6,4.2,3.7,7,6.3c-1.2,2.9-2.6,5.6-4.1,8
c-1.5,2.5-3.4,5-5.5,7.3c-2.2,2.4-4.7,4.8-7.7,7.2c-3,2.5-6.6,5.1-10.8,7.8c-4.3,2.8-6.5,4.7-6.5,5.6C35,192.1,37,191.7,40.7,190.1z
M250.5,81.8v165.3l-111.6-36.4L10.5,253.4V76.1l29.9-10V10.4l81.2,28.7L231.3,2.6v73.1L250.5,81.8z M124.2,50.6L22.3,84.6v152.2
l101.9-33.9V50.6L124.2,50.6z M219.4,71.9V19L138.1,46L219.4,71.9z M227,201.9L196.5,92L176,85.6l-30.9,90.8l18.9,5.9l5.8-18.7
l31.9,10l5.7,22.3L227,201.9z M174.8,147.7l22.2,6.9l-10.9-42.9L174.8,147.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -6,7 +6,7 @@ import platform
import sys import sys
from typing import TextIO from typing import TextIO
from platformdirs import user_log_dir, user_cache_dir from platformdirs import user_log_dir, user_cache_dir, user_data_dir
from buzz.assets import APP_BASE_DIR from buzz.assets import APP_BASE_DIR
@ -60,6 +60,7 @@ def main():
logging.debug("app_dir: %s", APP_BASE_DIR) logging.debug("app_dir: %s", APP_BASE_DIR)
logging.debug("log_dir: %s", log_dir) logging.debug("log_dir: %s", log_dir)
logging.debug("cache_dir: %s", user_cache_dir("Buzz")) logging.debug("cache_dir: %s", user_cache_dir("Buzz"))
logging.debug("data_dir: %s", user_data_dir("Buzz"))
app = Application(sys.argv) app = Application(sys.argv)
parse_command_line(app) parse_command_line(app)

View file

@ -24,3 +24,18 @@ class TranscriptionSegmentDAO(DAO[TranscriptionSegment]):
) )
query.bindValue(":transcription_id", str(transcription_id)) query.bindValue(":transcription_id", str(transcription_id))
return self._execute_all(query) return self._execute_all(query)
def update_segment_translation(self, segment_id: int, translation: str):
query = self._create_query()
query.prepare(
"""
UPDATE transcription_segment
SET translation = :translation
WHERE id = :id
"""
)
query.bindValue(":id", segment_id)
query.bindValue(":translation", translation)
if not query.exec():
raise Exception(query.lastError().text())

View file

@ -8,5 +8,6 @@ class TranscriptionSegment(Entity):
start_time: int start_time: int
end_time: int end_time: int
text: str text: str
translation: str
transcription_id: str transcription_id: str
id: int = -1 id: int = -1

View file

@ -53,13 +53,14 @@ def copy_transcriptions_from_json_to_sqlite(conn: Connection):
for segment in task.segments: for segment in task.segments:
cursor.execute( cursor.execute(
""" """
INSERT INTO transcription_segment (end_time, start_time, text, transcription_id) INSERT INTO transcription_segment (end_time, start_time, text, translation, transcription_id)
VALUES (?, ?, ?, ?); VALUES (?, ?, ?, ?, ?);
""", """,
( (
segment.end, segment.end,
segment.start, segment.start,
segment.text, segment.text,
segment.translation,
transcription_id, transcription_id,
), ),
) )

View file

@ -39,9 +39,13 @@ class TranscriptionService:
start_time=segment.start, start_time=segment.start,
end_time=segment.end, end_time=segment.end,
text=segment.text, text=segment.text,
translation='',
transcription_id=str(id), transcription_id=str(id),
) )
) )
def get_transcription_segments(self, transcription_id: UUID): def get_transcription_segments(self, transcription_id: UUID):
return self.transcription_segment_dao.get_segments(transcription_id) return self.transcription_segment_dao.get_segments(transcription_id)
def update_segment_translation(self, segment_id: int, translation: str):
return self.transcription_segment_dao.update_segment_translation(segment_id, translation)

View file

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-10 21:58+0300\n" "POT-Creation-Date: 2024-06-14 18:59+0300\n"
"PO-Revision-Date: 2024-06-10 21:59+0300\n" "PO-Revision-Date: 2024-06-14 19:01+0300\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: lv_LV\n" "Language: lv_LV\n"
@ -39,51 +39,63 @@ msgid "View Transcript Text"
msgstr "Aplūkot atpazīto tekstu" msgstr "Aplūkot atpazīto tekstu"
#: buzz/settings/shortcut.py:23 #: buzz/settings/shortcut.py:23
msgid "View Transcript Translation"
msgstr "Aplūkot tulkojumu"
#: buzz/settings/shortcut.py:24
msgid "View Transcript Timestamps" msgid "View Transcript Timestamps"
msgstr "Aplūkot atpazīšanas laikus" msgstr "Aplūkot atpazīšanas laikus"
#: buzz/settings/shortcut.py:25 buzz/widgets/main_window_toolbar.py:60 #: buzz/settings/shortcut.py:26 buzz/widgets/main_window_toolbar.py:60
#: buzz/widgets/main_window.py:222 #: buzz/widgets/main_window.py:222
msgid "Clear History" msgid "Clear History"
msgstr "Notīrīt vēsturi" msgstr "Notīrīt vēsturi"
#: buzz/settings/shortcut.py:26 buzz/widgets/main_window_toolbar.py:52 #: buzz/settings/shortcut.py:27 buzz/widgets/main_window_toolbar.py:52
msgid "Cancel Transcription" msgid "Cancel Transcription"
msgstr "Atcelt atpazīšanu" msgstr "Atcelt atpazīšanu"
#: buzz/widgets/transcription_tasks_table_widget.py:64 #: buzz/widgets/transcription_tasks_table_widget.py:62
msgid "In Progress"
msgstr "Apstrādā"
#: buzz/widgets/transcription_tasks_table_widget.py:65
msgid "Completed" msgid "Completed"
msgstr "Pabeigts" msgstr "Pabeigts"
#: buzz/widgets/transcription_tasks_table_widget.py:73 #: buzz/widgets/transcription_tasks_table_widget.py:72
msgid "Failed"
msgstr "Neizdevās"
#: buzz/widgets/transcription_tasks_table_widget.py:75
msgid "Canceled" msgid "Canceled"
msgstr "Atcelts" msgstr "Atcelts"
#: buzz/widgets/transcription_tasks_table_widget.py:75 #: buzz/widgets/transcription_tasks_table_widget.py:77
msgid "Queued" msgid "Queued"
msgstr "Ierindots" msgstr "Ierindots"
#: buzz/widgets/transcription_tasks_table_widget.py:83 #: buzz/widgets/transcription_tasks_table_widget.py:85
msgid "File Name / URL" msgid "File Name / URL"
msgstr "Fails / URL" msgstr "Fails / URL"
#: buzz/widgets/transcription_tasks_table_widget.py:95 #: buzz/widgets/transcription_tasks_table_widget.py:97
msgid "Model" msgid "Model"
msgstr "Modelis" msgstr "Modelis"
#: buzz/widgets/transcription_tasks_table_widget.py:104 #: buzz/widgets/transcription_tasks_table_widget.py:106
msgid "Task" msgid "Task"
msgstr "Uzdevums" msgstr "Uzdevums"
#: buzz/widgets/transcription_tasks_table_widget.py:113 #: buzz/widgets/transcription_tasks_table_widget.py:115
msgid "Status" msgid "Status"
msgstr "Statuss" msgstr "Statuss"
#: buzz/widgets/transcription_tasks_table_widget.py:121 #: buzz/widgets/transcription_tasks_table_widget.py:123
msgid "Date Added" msgid "Date Added"
msgstr "Pievienots" msgstr "Pievienots"
#: buzz/widgets/transcription_tasks_table_widget.py:132 #: buzz/widgets/transcription_tasks_table_widget.py:134
msgid "Date Completed" msgid "Date Completed"
msgstr "Pabeigts" msgstr "Pabeigts"
@ -140,11 +152,11 @@ msgstr "Gaida MI tulkojumu..."
msgid "Microphone:" msgid "Microphone:"
msgstr "Mikrofons:" msgstr "Mikrofons:"
#: buzz/widgets/recording_transcriber_widget.py:382 #: buzz/widgets/recording_transcriber_widget.py:391
msgid "An error occurred while starting a new recording:" msgid "An error occurred while starting a new recording:"
msgstr "Sākot jaunu ierakstu notikusi kļūda:" msgstr "Sākot jaunu ierakstu notikusi kļūda:"
#: buzz/widgets/recording_transcriber_widget.py:386 #: buzz/widgets/recording_transcriber_widget.py:395
msgid "" msgid ""
"Please check your audio devices or check the application logs for more " "Please check your audio devices or check the application logs for more "
"information." "information."
@ -161,8 +173,8 @@ msgid ""
"Detected missing permissions, please check that snap permissions have been " "Detected missing permissions, please check that snap permissions have been "
"granted" "granted"
msgstr "" msgstr ""
"Ne visi nepieciešamie moduļi darbojas korekti, iespējams nav piešķirtas " "Ne visi nepieciešamie moduļi darbojas korekti, iespējams nav piešķirtas snap "
"snap atļaujas" "atļaujas"
#: buzz/widgets/snap_notice.py:16 #: buzz/widgets/snap_notice.py:16
msgid "" msgid ""
@ -205,83 +217,111 @@ msgstr ""
msgid "Select audio file" msgid "Select audio file"
msgstr "Izvēlieties audio failu" msgstr "Izvēlieties audio failu"
#: buzz/widgets/main_window.py:278 #: buzz/widgets/main_window.py:280
#: buzz/widgets/preferences_dialog/models_preferences_widget.py:191 #: buzz/widgets/preferences_dialog/models_preferences_widget.py:191
msgid "Error" msgid "Error"
msgstr "Kļūda" msgstr "Kļūda"
#: buzz/widgets/main_window.py:278 #: buzz/widgets/main_window.py:280
msgid "Unable to save OpenAI API key to keyring" msgid "Unable to save OpenAI API key to keyring"
msgstr "Neizdevās saglabāt OpenAI API atslēgu atslēgu saišķī" msgstr "Neizdevās saglabāt OpenAI API atslēgu atslēgu saišķī"
#: buzz/widgets/transcription_viewer/export_transcription_menu.py:42 #: buzz/widgets/transcription_viewer/export_transcription_menu.py:42
msgid "Save File" #: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py:34
msgstr "Saglabāt failu" #: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py:95
#: buzz/widgets/transcription_viewer/export_transcription_menu.py:44
msgid "Text files"
msgstr "Teksta faili"
#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py:19
msgid "View"
msgstr "Skats"
#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py:27
#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py:68
msgid "Text" msgid "Text"
msgstr "Teksts" msgstr "Teksts"
#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py:33 #: buzz/widgets/transcription_viewer/export_transcription_menu.py:43
#: buzz/widgets/transcription_viewer/export_transcription_menu.py:65
#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py:40
#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py:96
msgid "Translation"
msgstr "Tulkojums"
#: buzz/widgets/transcription_viewer/export_transcription_menu.py:79
msgid "Save File"
msgstr "Saglabāt failu"
#: buzz/widgets/transcription_viewer/export_transcription_menu.py:81
msgid "Text files"
msgstr "Teksta faili"
#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py:26
msgid "View"
msgstr "Skats"
#: buzz/widgets/transcription_viewer/transcription_view_mode_tool_button.py:46
msgid "Timestamps" msgid "Timestamps"
msgstr "Laiks" msgstr "Laiks"
#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py:66 #: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py:93
msgid "Start" msgid "Start"
msgstr "Sākums" msgstr "Sākums"
#: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py:67 #: buzz/widgets/transcription_viewer/transcription_segments_editor_widget.py:94
msgid "End" msgid "End"
msgstr "Beigas" msgstr "Beigas"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:92 #: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:146
msgid "Export" msgid "Export"
msgstr "Eksportēt" msgstr "Eksportēt"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:160
msgid "Translate"
msgstr "Tulkot"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:250
msgid "API Key Required"
msgstr "API atslēgas kļūda"
#: buzz/widgets/transcription_viewer/transcription_viewer_widget.py:251
msgid "Please enter OpenAI API Key in preferences"
msgstr "Lūdzu ievadiet OpenAI API atslēgu iestatījumos"
#: buzz/widgets/record_button.py:21 #: buzz/widgets/record_button.py:21
msgid "Stop" msgid "Stop"
msgstr "Apturēt" msgstr "Apturēt"
#: buzz/widgets/transcriber/advanced_settings_dialog.py:32 #: buzz/widgets/transcriber/advanced_settings_dialog.py:33
msgid "Advanced Settings" msgid "Advanced Settings"
msgstr "Papildu iestatījumi" msgstr "Papildu iestatījumi"
#: buzz/widgets/transcriber/advanced_settings_dialog.py:41 #: buzz/widgets/transcriber/advanced_settings_dialog.py:37
msgid "Speech recognition settings"
msgstr "Runas atpazīšanas iestatījumi"
#: buzz/widgets/transcriber/advanced_settings_dialog.py:46
msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\"" msgid "Comma-separated, e.g. \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
msgstr "Atdalīti ar komatu, piemēram, \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\"" msgstr "Atdalīti ar komatu, piemēram, \"0.0, 0.2, 0.4, 0.6, 0.8, 1.0\""
#: buzz/widgets/transcriber/advanced_settings_dialog.py:50 #: buzz/widgets/transcriber/advanced_settings_dialog.py:55
msgid "Temperature:" msgid "Temperature:"
msgstr "Temperatūra:" msgstr "Temperatūra:"
#: buzz/widgets/transcriber/advanced_settings_dialog.py:61 #: buzz/widgets/transcriber/advanced_settings_dialog.py:66
msgid "Initial Prompt:" msgid "Initial Prompt:"
msgstr "" msgstr ""
"Sākotnējais\n" "Sākotnējais\n"
"vaicājums:" "vaicājums:"
#: buzz/widgets/transcriber/advanced_settings_dialog.py:63 #: buzz/widgets/transcriber/advanced_settings_dialog.py:68
msgid "Translation settings"
msgstr "Tulkojuma iestatījumi"
#: buzz/widgets/transcriber/advanced_settings_dialog.py:72
msgid "Enable AI translation" msgid "Enable AI translation"
msgstr "Tulkot ar MI" msgstr "Tulkot ar MI"
#: buzz/widgets/transcriber/advanced_settings_dialog.py:75 #: buzz/widgets/transcriber/advanced_settings_dialog.py:84
msgid "AI model:" msgid "AI model:"
msgstr "AI modelis:" msgstr "AI modelis:"
#: buzz/widgets/transcriber/advanced_settings_dialog.py:79 #: buzz/widgets/transcriber/advanced_settings_dialog.py:88
msgid "Enter instructions for AI on how to translate..." msgid "Enter instructions for AI on how to translate..."
msgstr "Ievadiet tulkošanas norādes mākslīgajam intelektam..." msgstr "Ievadiet tulkošanas norādes mākslīgajam intelektam..."
#: buzz/widgets/transcriber/advanced_settings_dialog.py:83 #: buzz/widgets/transcriber/advanced_settings_dialog.py:92
msgid "Instructions for AI:" msgid "Instructions for AI:"
msgstr "Norādes MI:" msgstr "Norādes MI:"
@ -301,20 +341,20 @@ msgstr "Papildu iestatījumi..."
msgid "Enter prompt..." msgid "Enter prompt..."
msgstr "Ievadiet vaicājumu..." msgstr "Ievadiet vaicājumu..."
#: buzz/widgets/transcriber/transcription_options_group_box.py:79 #: buzz/widgets/transcriber/transcription_options_group_box.py:86
msgid "Model:" msgid "Model:"
msgstr "Modelis:" msgstr "Modelis:"
#: buzz/widgets/transcriber/transcription_options_group_box.py:83 #: buzz/widgets/transcriber/transcription_options_group_box.py:90
msgid "Task:" msgid "Task:"
msgstr "Uzdevums:" msgstr "Uzdevums:"
#: buzz/widgets/transcriber/transcription_options_group_box.py:84 #: buzz/widgets/transcriber/transcription_options_group_box.py:91
msgid "Language:" msgid "Language:"
msgstr "Valoda:" msgstr "Valoda:"
#: buzz/widgets/transcriber/languages_combo_box.py:25 #: buzz/widgets/transcriber/languages_combo_box.py:25
#: buzz/transcriber/transcriber.py:152 #: buzz/transcriber/transcriber.py:153
msgid "Detect Language" msgid "Detect Language"
msgstr "Noteikt valodu" msgstr "Noteikt valodu"
@ -380,10 +420,10 @@ msgstr "OpenAI API atslēgas pārbaude"
#: buzz/widgets/preferences_dialog/general_preferences_widget.py:129 #: buzz/widgets/preferences_dialog/general_preferences_widget.py:129
msgid "" msgid ""
"Your API key is valid. Buzz will use this key to perform Whisper API " "Your API key is valid. Buzz will use this key to perform Whisper API "
"transcriptions." "transcriptions and AI translations with ChatGPT."
msgstr "" msgstr ""
"Jūsu API atslēga ir derīga. Buzz izmantos to runas atpazīšanai ar Whisper " "Jūsu API atslēga ir derīga. Buzz izmantos to runas atpazīšanai ar Whisper "
"API." "API un tulkošanai ar ChatGPT."
#: buzz/widgets/preferences_dialog/general_preferences_widget.py:156 #: buzz/widgets/preferences_dialog/general_preferences_widget.py:156
msgid "Select Export Folder" msgid "Select Export Folder"

View file

@ -23,6 +23,7 @@ CREATE TABLE transcription_segment (
end_time INT DEFAULT 0, end_time INT DEFAULT 0,
start_time INT DEFAULT 0, start_time INT DEFAULT 0,
text TEXT NOT NULL, text TEXT NOT NULL,
translation TEXT DEFAULT '',
transcription_id TEXT, transcription_id TEXT,
FOREIGN KEY (transcription_id) REFERENCES transcription(id) ON DELETE CASCADE FOREIGN KEY (transcription_id) REFERENCES transcription(id) ON DELETE CASCADE
); );

View file

@ -20,6 +20,7 @@ class Shortcut(str, enum.Enum):
OPEN_PREFERENCES_WINDOW = ("Ctrl+,", _("Open Preferences Window")) OPEN_PREFERENCES_WINDOW = ("Ctrl+,", _("Open Preferences Window"))
VIEW_TRANSCRIPT_TEXT = ("Ctrl+E", _("View Transcript Text")) VIEW_TRANSCRIPT_TEXT = ("Ctrl+E", _("View Transcript Text"))
VIEW_TRANSCRIPT_TRANSLATION = ("Ctrl+L", _("View Transcript Translation"))
VIEW_TRANSCRIPT_TIMESTAMPS = ("Ctrl+T", _("View Transcript Timestamps")) VIEW_TRANSCRIPT_TIMESTAMPS = ("Ctrl+T", _("View Transcript Timestamps"))
CLEAR_HISTORY = ("Ctrl+S", _("Clear History")) CLEAR_HISTORY = ("Ctrl+S", _("Clear History"))

View file

@ -103,7 +103,12 @@ class FileTranscriber(QObject):
# TODO: Move to transcription service # TODO: Move to transcription service
def write_output(path: str, segments: List[Segment], output_format: OutputFormat): def write_output(
path: str,
segments: List[Segment],
output_format: OutputFormat,
segment_key: str = 'text'
):
logging.debug( logging.debug(
"Writing transcription output, path = %s, output format = %s, number of segments = %s", "Writing transcription output, path = %s, output format = %s, number of segments = %s",
path, path,
@ -114,7 +119,7 @@ def write_output(path: str, segments: List[Segment], output_format: OutputFormat
with open(path, "w", encoding="utf-8") as file: with open(path, "w", encoding="utf-8") as file:
if output_format == OutputFormat.TXT: if output_format == OutputFormat.TXT:
for i, segment in enumerate(segments): for i, segment in enumerate(segments):
file.write(segment.text) file.write(getattr(segment, segment_key))
file.write("\n") file.write("\n")
elif output_format == OutputFormat.VTT: elif output_format == OutputFormat.VTT:
@ -123,7 +128,7 @@ def write_output(path: str, segments: List[Segment], output_format: OutputFormat
file.write( file.write(
f"{to_timestamp(segment.start)} --> {to_timestamp(segment.end)}\n" f"{to_timestamp(segment.start)} --> {to_timestamp(segment.end)}\n"
) )
file.write(f"{segment.text}\n\n") file.write(f"{getattr(segment, segment_key)}\n\n")
elif output_format == OutputFormat.SRT: elif output_format == OutputFormat.SRT:
for i, segment in enumerate(segments): for i, segment in enumerate(segments):
@ -131,7 +136,7 @@ def write_output(path: str, segments: List[Segment], output_format: OutputFormat
file.write( file.write(
f'{to_timestamp(segment.start, ms_separator=",")} --> {to_timestamp(segment.end, ms_separator=",")}\n' f'{to_timestamp(segment.start, ms_separator=",")} --> {to_timestamp(segment.end, ms_separator=",")}\n'
) )
file.write(f"{segment.text}\n\n") file.write(f"{getattr(segment, segment_key)}\n\n")
logging.debug("Written transcription output") logging.debug("Written transcription output")

View file

@ -125,7 +125,7 @@ class RecordingTranscriber(QObject):
whisper_segments, info = model.transcribe( whisper_segments, info = model.transcribe(
audio=samples, audio=samples,
language=self.transcription_options.language language=self.transcription_options.language
if self.transcription_options.language is not "" if self.transcription_options.language != ""
else None, else None,
task=self.transcription_options.task.value, task=self.transcription_options.task.value,
temperature=self.transcription_options.temperature, temperature=self.transcription_options.temperature,

View file

@ -25,6 +25,7 @@ class Segment:
start: int # start time in ms start: int # start time in ms
end: int # end time in ms end: int # end time in ms
text: str text: str
translation: str = ""
LANGUAGES = { LANGUAGES = {

View file

@ -121,6 +121,7 @@ class WhisperFileTranscriber(FileTranscriber):
start=int(segment.get("start") * 1000), start=int(segment.get("start") * 1000),
end=int(segment.get("end") * 1000), end=int(segment.get("end") * 1000),
text=segment.get("text"), text=segment.get("text"),
translation=""
) )
for segment in result.get("segments") for segment in result.get("segments")
] ]
@ -149,6 +150,7 @@ class WhisperFileTranscriber(FileTranscriber):
start=int(word.start * 1000), start=int(word.start * 1000),
end=int(word.end * 1000), end=int(word.end * 1000),
text=word.word, text=word.word,
translation=""
) )
) )
else: else:
@ -157,6 +159,7 @@ class WhisperFileTranscriber(FileTranscriber):
start=int(segment.start * 1000), start=int(segment.start * 1000),
end=int(segment.end * 1000), end=int(segment.end * 1000),
text=segment.text, text=segment.text,
translation=""
) )
) )
@ -181,6 +184,7 @@ class WhisperFileTranscriber(FileTranscriber):
start=int(word.start * 1000), start=int(word.start * 1000),
end=int(word.end * 1000), end=int(word.end * 1000),
text=word.word.strip(), text=word.word.strip(),
translation=""
) )
for segment in result.segments for segment in result.segments
for word in segment.words for word in segment.words
@ -200,6 +204,7 @@ class WhisperFileTranscriber(FileTranscriber):
start=int(segment.get("start") * 1000), start=int(segment.get("start") * 1000),
end=int(segment.get("end") * 1000), end=int(segment.get("end") * 1000),
text=segment.get("text"), text=segment.get("text"),
translation=""
) )
for segment in segments for segment in segments
] ]
@ -226,6 +231,7 @@ class WhisperFileTranscriber(FileTranscriber):
start=segment.get("start"), start=segment.get("start"),
end=segment.get("end"), end=segment.get("end"),
text=segment.get("text"), text=segment.get("text"),
translation=""
) )
for segment in segments_dict for segment in segments_dict
] ]

View file

@ -8,16 +8,18 @@ from PyQt6.QtCore import QObject, pyqtSignal
from buzz.settings.settings import Settings from buzz.settings.settings import Settings
from buzz.store.keyring_store import get_password, Key from buzz.store.keyring_store import get_password, Key
from buzz.transcriber.transcriber import TranscriptionOptions from buzz.transcriber.transcriber import TranscriptionOptions
from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
class Translator(QObject): class Translator(QObject):
translation = pyqtSignal(str) translation = pyqtSignal(str, int)
finished = pyqtSignal() finished = pyqtSignal()
is_running = False is_running = False
def __init__( def __init__(
self, self,
transcription_options: TranscriptionOptions, transcription_options: TranscriptionOptions,
advanced_settings_dialog: AdvancedSettingsDialog,
parent: Optional[QObject] = None, parent: Optional[QObject] = None,
) -> None: ) -> None:
super().__init__(parent) super().__init__(parent)
@ -25,6 +27,11 @@ class Translator(QObject):
logging.debug(f"Translator init: {transcription_options}") logging.debug(f"Translator init: {transcription_options}")
self.transcription_options = transcription_options self.transcription_options = transcription_options
self.advanced_settings_dialog = advanced_settings_dialog
self.advanced_settings_dialog.transcription_options_changed.connect(
self.on_transcription_options_changed
)
self.queue = queue.Queue() self.queue = queue.Queue()
settings = Settings() settings = Settings()
@ -44,7 +51,7 @@ class Translator(QObject):
while self.is_running: while self.is_running:
try: try:
transcript = self.queue.get(timeout=1) transcript, transcript_id = self.queue.get(timeout=1)
except queue.Empty: except queue.Empty:
continue continue
@ -64,13 +71,17 @@ class Translator(QObject):
logging.error(f"Translation error! Server response: {completion}") logging.error(f"Translation error! Server response: {completion}")
next_translation = "Translation error, see logs!" next_translation = "Translation error, see logs!"
self.translation.emit(next_translation) self.translation.emit(next_translation, transcript_id)
self.finished.emit() self.finished.emit()
def enqueue(self, transcript: str): def on_transcription_options_changed(
if self.is_running: self, transcription_options: TranscriptionOptions
self.queue.put(transcript) ):
self.transcription_options = transcription_options
def enqueue(self, transcript: str, transcript_id: Optional[int] = None):
self.queue.put((transcript, transcript_id))
def stop(self): def stop(self):
self.is_running = False self.is_running = False

View file

@ -74,6 +74,11 @@ class FileDownloadIcon(Icon):
super().__init__(get_path("assets/file_download_black_24dp.svg"), parent) super().__init__(get_path("assets/file_download_black_24dp.svg"), parent)
class TranslateIcon(Icon):
def __init__(self, parent: QWidget):
super().__init__(get_path("assets/translate_black.svg"), parent)
class VisibilityIcon(Icon): class VisibilityIcon(Icon):
def __init__(self, parent: QWidget): def __init__(self, parent: QWidget):
super().__init__( super().__init__(

View file

@ -141,6 +141,8 @@ class MainWindow(QMainWindow):
self.folder_watcher.task_found.connect(self.add_task) self.folder_watcher.task_found.connect(self.add_task)
self.folder_watcher.find_tasks() self.folder_watcher.find_tasks()
self.transcription_viewer_widget = None
if os.environ.get('SNAP_NAME', '') == 'buzz': if os.environ.get('SNAP_NAME', '') == 'buzz':
logging.debug("Running in a snap environment") logging.debug("Running in a snap environment")
self.check_linux_permissions() self.check_linux_permissions()
@ -267,6 +269,8 @@ class MainWindow(QMainWindow):
self.on_openai_access_token_changed self.on_openai_access_token_changed
) )
file_transcriber_window.show() file_transcriber_window.show()
file_transcriber_window.raise_()
file_transcriber_window.activateWindow()
@staticmethod @staticmethod
def on_openai_access_token_changed(access_token: str): def on_openai_access_token_changed(access_token: str):
@ -347,14 +351,14 @@ class MainWindow(QMainWindow):
self.open_transcription_viewer(transcription) self.open_transcription_viewer(transcription)
def open_transcription_viewer(self, transcription: Transcription): def open_transcription_viewer(self, transcription: Transcription):
transcription_viewer_widget = TranscriptionViewerWidget( self.transcription_viewer_widget = TranscriptionViewerWidget(
transcription=transcription, transcription=transcription,
transcription_service=self.transcription_service, transcription_service=self.transcription_service,
shortcuts=self.shortcuts, shortcuts=self.shortcuts,
parent=self, parent=self,
flags=Qt.WindowType.Window, flags=Qt.WindowType.Window,
) )
transcription_viewer_widget.show() self.transcription_viewer_widget.show()
def add_task(self, task: FileTranscriptionTask): def add_task(self, task: FileTranscriptionTask):
self.transcription_service.create_transcription(task) self.transcription_service.create_transcription(task)
@ -396,6 +400,10 @@ class MainWindow(QMainWindow):
self.transcriber_worker.stop() self.transcriber_worker.stop()
self.transcriber_thread.quit() self.transcriber_thread.quit()
self.transcriber_thread.wait() self.transcriber_thread.wait()
if self.transcription_viewer_widget is not None:
self.transcription_viewer_widget.close()
super().closeEvent(event) super().closeEvent(event)
def save_geometry(self): def save_geometry(self):

View file

@ -126,7 +126,7 @@ class GeneralPreferencesWidget(QWidget):
QMessageBox.information( QMessageBox.information(
self, self,
_("OpenAI API Key Test"), _("OpenAI API Key Test"),
_("Your API key is valid. Buzz will use this key to perform Whisper API transcriptions."), _("Your API key is valid. Buzz will use this key to perform Whisper API transcriptions and AI translations with ChatGPT."),
) )
def on_test_openai_api_key_failure(self, error: str): def on_test_openai_api_key_failure(self, error: str):

View file

@ -127,12 +127,12 @@ class RecordingTranscriberWidget(QWidget):
self.translation_text_box = TextDisplayBox(self) self.translation_text_box = TextDisplayBox(self)
self.translation_text_box.setPlaceholderText(_("Waiting for AI translation...")) self.translation_text_box.setPlaceholderText(_("Waiting for AI translation..."))
transcription_options_group_box = TranscriptionOptionsGroupBox( self.transcription_options_group_box = TranscriptionOptionsGroupBox(
default_transcription_options=self.transcription_options, default_transcription_options=self.transcription_options,
model_types=model_types, model_types=model_types,
parent=self, parent=self,
) )
transcription_options_group_box.transcription_options_changed.connect( self.transcription_options_group_box.transcription_options_changed.connect(
self.on_transcription_options_changed self.on_transcription_options_changed
) )
@ -145,7 +145,7 @@ class RecordingTranscriberWidget(QWidget):
record_button_layout.addWidget(self.audio_meter_widget) record_button_layout.addWidget(self.audio_meter_widget)
record_button_layout.addWidget(self.record_button) record_button_layout.addWidget(self.record_button)
layout.addWidget(transcription_options_group_box) layout.addWidget(self.transcription_options_group_box)
layout.addLayout(recording_options_layout) layout.addLayout(recording_options_layout)
layout.addLayout(record_button_layout) layout.addLayout(record_button_layout)
layout.addWidget(self.transcription_text_box) layout.addWidget(self.transcription_text_box)
@ -289,7 +289,10 @@ class RecordingTranscriberWidget(QWidget):
if self.transcription_options.enable_llm_translation: if self.transcription_options.enable_llm_translation:
self.translation_thread = QThread() self.translation_thread = QThread()
self.translator = Translator(self.transcription_options) self.translator = Translator(
self.transcription_options,
self.transcription_options_group_box.advanced_settings_dialog,
)
self.translator.moveToThread(self.translation_thread) self.translator.moveToThread(self.translation_thread)
@ -354,7 +357,7 @@ class RecordingTranscriberWidget(QWidget):
with open(self.transcript_export_file, "a") as f: with open(self.transcript_export_file, "a") as f:
f.write(text + "\n\n") f.write(text + "\n\n")
def on_next_translation(self, text: str): def on_next_translation(self, text: str, _: Optional[int] = None):
if len(text) > 0: if len(text) > 0:
self.translation_text_box.moveCursor(QTextCursor.MoveOperation.End) self.translation_text_box.moveCursor(QTextCursor.MoveOperation.End)
if len(self.translation_text_box.toPlainText()) > 0: if len(self.translation_text_box.toPlainText()) > 0:

View file

@ -6,6 +6,7 @@ from PyQt6.QtWidgets import (
QCheckBox, QCheckBox,
QPlainTextEdit, QPlainTextEdit,
QFormLayout, QFormLayout,
QLabel,
) )
from buzz.locale import _ from buzz.locale import _
@ -33,6 +34,10 @@ class AdvancedSettingsDialog(QDialog):
layout = QFormLayout(self) layout = QFormLayout(self)
transcription_settings_title= _("Speech recognition settings")
transcription_settings_title_label = QLabel(f"<h4>{transcription_settings_title}</h4>", self)
layout.addRow("", transcription_settings_title_label)
default_temperature_text = ", ".join( default_temperature_text = ", ".join(
[str(temp) for temp in transcription_options.temperature] [str(temp) for temp in transcription_options.temperature]
) )
@ -60,6 +65,10 @@ class AdvancedSettingsDialog(QDialog):
layout.addRow(_("Initial Prompt:"), self.initial_prompt_text_edit) layout.addRow(_("Initial Prompt:"), self.initial_prompt_text_edit)
translation_settings_title= _("Translation settings")
translation_settings_title_label = QLabel(f"<h4>{translation_settings_title}</h4>", self)
layout.addRow("", translation_settings_title_label)
self.enable_llm_translation_checkbox = QCheckBox(_("Enable AI translation")) self.enable_llm_translation_checkbox = QCheckBox(_("Enable AI translation"))
self.enable_llm_translation_checkbox.setChecked(self.transcription_options.enable_llm_translation) self.enable_llm_translation_checkbox.setChecked(self.transcription_options.enable_llm_translation)
self.enable_llm_translation_checkbox.stateChanged.connect(self.on_enable_llm_translation_changed) self.enable_llm_translation_checkbox.stateChanged.connect(self.on_enable_llm_translation_changed)

View file

@ -39,6 +39,13 @@ class TranscriptionOptionsGroupBox(QGroupBox):
) )
self.model_type_combo_box.changed.connect(self.on_model_type_changed) self.model_type_combo_box.changed.connect(self.on_model_type_changed)
self.advanced_settings_dialog = AdvancedSettingsDialog(
transcription_options=self.transcription_options, parent=self
)
self.advanced_settings_dialog.transcription_options_changed.connect(
self.on_transcription_options_changed
)
self.whisper_model_size_combo_box = QComboBox(self) self.whisper_model_size_combo_box = QComboBox(self)
self.whisper_model_size_combo_box.addItems( self.whisper_model_size_combo_box.addItems(
[size.value.title() for size in WhisperModelSize] [size.value.title() for size in WhisperModelSize]
@ -102,13 +109,7 @@ class TranscriptionOptionsGroupBox(QGroupBox):
self.transcription_options_changed.emit(self.transcription_options) self.transcription_options_changed.emit(self.transcription_options)
def open_advanced_settings(self): def open_advanced_settings(self):
dialog = AdvancedSettingsDialog( self.advanced_settings_dialog.exec()
transcription_options=self.transcription_options, parent=self
)
dialog.transcription_options_changed.connect(
self.on_transcription_options_changed
)
dialog.exec()
def on_transcription_options_changed( def on_transcription_options_changed(
self, transcription_options: TranscriptionOptions self, transcription_options: TranscriptionOptions

View file

@ -59,7 +59,8 @@ def format_record_status_text(record: QSqlRecord) -> str:
status = FileTranscriptionTask.Status(record.value("status")) status = FileTranscriptionTask.Status(record.value("status"))
match status: match status:
case FileTranscriptionTask.Status.IN_PROGRESS: case FileTranscriptionTask.Status.IN_PROGRESS:
return f'{_("In Progress")} ({record.value("progress") :.0%})' in_progress_label = _("In Progress")
return f'{in_progress_label} ({record.value("progress") :.0%})'
case FileTranscriptionTask.Status.COMPLETED: case FileTranscriptionTask.Status.COMPLETED:
status = _("Completed") status = _("Completed")
started_at = record.value("time_started") started_at = record.value("time_started")
@ -68,7 +69,8 @@ def format_record_status_text(record: QSqlRecord) -> str:
status += f" ({TranscriptionTasksTableWidget.format_timedelta(datetime.fromisoformat(completed_at) - datetime.fromisoformat(started_at))})" status += f" ({TranscriptionTasksTableWidget.format_timedelta(datetime.fromisoformat(completed_at) - datetime.fromisoformat(started_at))})"
return status return status
case FileTranscriptionTask.Status.FAILED: case FileTranscriptionTask.Status.FAILED:
return f'{_("Failed")} ({record.value("error_message")})' failed_label = _("Failed")
return f'{failed_label} ({record.value("error_message")})'
case FileTranscriptionTask.Status.CANCELED: case FileTranscriptionTask.Status.CANCELED:
return _("Canceled") return _("Canceled")
case FileTranscriptionTask.Status.QUEUED: case FileTranscriptionTask.Status.QUEUED:

View file

@ -1,3 +1,4 @@
import logging
from PyQt6.QtGui import QAction from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QWidget, QMenu, QFileDialog from PyQt6.QtWidgets import QWidget, QMenu, QFileDialog
@ -23,15 +24,48 @@ class ExportTranscriptionMenu(QMenu):
self.transcription = transcription self.transcription = transcription
self.transcription_service = transcription_service self.transcription_service = transcription_service
actions = [ self.segments = [
QAction(text=output_format.value.upper(), parent=self) Segment(
for output_format in OutputFormat 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
)
] ]
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
]
self.addActions(actions) self.addActions(actions)
self.triggered.connect(self.on_menu_triggered) self.triggered.connect(self.on_menu_triggered)
@staticmethod
def extract_format_and_segment_key(action_text: str):
parts = action_text.split('-')
output_format = parts[0].strip()
label = parts[1].strip() if len(parts) > 1 else None
segment_key = 'translation' if label == _('Translation') else 'text'
return output_format, segment_key
def on_menu_triggered(self, action: QAction): def on_menu_triggered(self, action: QAction):
output_format = OutputFormat[action.text()] output_format_value, segment_key = self.extract_format_and_segment_key(action.text())
output_format = OutputFormat[output_format_value]
default_path = self.transcription.get_output_file_path( default_path = self.transcription.get_output_file_path(
output_format=output_format output_format=output_format
@ -47,15 +81,9 @@ class ExportTranscriptionMenu(QMenu):
if output_file_path == "": if output_file_path == "":
return return
segments = [
Segment(start=segment.start_time, end=segment.end_time, text=segment.text)
for segment in self.transcription_service.get_transcription_segments(
transcription_id=self.transcription.id_as_uuid
)
]
write_output( write_output(
path=output_file_path, path=output_file_path,
segments=segments, segments=self.segments,
output_format=output_format, output_format=output_format,
segment_key=segment_key
) )

View file

@ -1,19 +1,22 @@
import enum import enum
import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from uuid import UUID from uuid import UUID
from PyQt6.QtCore import pyqtSignal, Qt, QModelIndex, QItemSelection from PyQt6.QtCore import pyqtSignal, Qt, QModelIndex, QItemSelection
from PyQt6.QtSql import QSqlTableModel, QSqlRecord from PyQt6.QtSql import QSqlTableModel, QSqlRecord
from PyQt6.QtGui import QFontMetrics from PyQt6.QtGui import QFontMetrics, QTextOption
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QWidget,
QTableView, QTableView,
QStyledItemDelegate, QStyledItemDelegate,
QAbstractItemView, QAbstractItemView,
QTextEdit,
) )
from buzz.locale import _ from buzz.locale import _
from buzz.translator import Translator
from buzz.transcriber.file_transcriber import to_timestamp from buzz.transcriber.file_transcriber import to_timestamp
@ -22,6 +25,7 @@ class Column(enum.Enum):
END = enum.auto() END = enum.auto()
START = enum.auto() START = enum.auto()
TEXT = enum.auto() TEXT = enum.auto()
TRANSLATION = enum.auto()
TRANSCRIPTION_ID = enum.auto() TRANSCRIPTION_ID = enum.auto()
@ -38,6 +42,18 @@ class TimeStampDelegate(QStyledItemDelegate):
return to_timestamp(value) return to_timestamp(value)
class WordWrapDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QTextEdit(parent)
editor.setWordWrapMode(QTextOption.WrapMode.WordWrap)
editor.setAcceptRichText(False)
return editor
def setModelData(self, editor, model, index):
model.setData(index, editor.toPlainText())
class TranscriptionSegmentModel(QSqlTableModel): class TranscriptionSegmentModel(QSqlTableModel):
def __init__(self, transcription_id: UUID): def __init__(self, transcription_id: UUID):
super().__init__() super().__init__()
@ -53,20 +69,31 @@ class TranscriptionSegmentModel(QSqlTableModel):
class TranscriptionSegmentsEditorWidget(QTableView): class TranscriptionSegmentsEditorWidget(QTableView):
PARENT_PADDINGS = 40
segment_selected = pyqtSignal(QSqlRecord) segment_selected = pyqtSignal(QSqlRecord)
def __init__(self, transcription_id: UUID, parent: Optional[QWidget]): def __init__(
self,
transcription_id: UUID,
translator: Translator,
parent: Optional[QWidget]
):
super().__init__(parent) super().__init__(parent)
self.translator = translator
self.translator.translation.connect(self.update_translation)
model = TranscriptionSegmentModel(transcription_id=transcription_id) model = TranscriptionSegmentModel(transcription_id=transcription_id)
self.setModel(model) self.setModel(model)
timestamp_delegate = TimeStampDelegate() timestamp_delegate = TimeStampDelegate()
word_wrap_delegate = WordWrapDelegate()
self.column_definitions: list[ColDef] = [ self.column_definitions: list[ColDef] = [
ColDef("start", _("Start"), Column.START, delegate=timestamp_delegate), ColDef("start", _("Start"), Column.START, delegate=timestamp_delegate),
ColDef("end", _("End"), Column.END, delegate=timestamp_delegate), ColDef("end", _("End"), Column.END, delegate=timestamp_delegate),
ColDef("text", _("Text"), Column.TEXT), ColDef("text", _("Text"), Column.TEXT, delegate=word_wrap_delegate),
ColDef("translation", _("Translation"), Column.TRANSLATION, delegate=word_wrap_delegate),
] ]
for i in range(model.columnCount()): for i in range(model.columnCount()):
@ -91,17 +118,52 @@ class TranscriptionSegmentsEditorWidget(QTableView):
self.selectionModel().selectionChanged.connect(self.on_selection_changed) self.selectionModel().selectionChanged.connect(self.on_selection_changed)
model.select() model.select()
self.has_translations = self.has_non_empty_translation()
# Show start before end # Show start before end
self.horizontalHeader().swapSections(1, 2) self.horizontalHeader().swapSections(1, 2)
font_metrics = QFontMetrics(self.font()) font_metrics = QFontMetrics(self.font())
max_row_height = font_metrics.height() * 3 max_row_height = font_metrics.height() * 4
for row in range(self.model().rowCount()): for row in range(self.model().rowCount()):
self.setRowHeight(row, max_row_height) self.setRowHeight(row, max_row_height)
self.horizontalHeader().setStretchLastSection(True) self.setColumnWidth(Column.START.value, 95)
self.setColumnWidth(Column.END.value, 95)
self.setWordWrap(True) self.setWordWrap(True)
self.resizeColumnsToContents()
def has_non_empty_translation(self) -> bool:
for i in range(self.model().rowCount()):
if self.model().record(i).value("translation").strip():
return True
return False
def resizeEvent(self, event):
super().resizeEvent(event)
if not self.has_translations:
self.hideColumn(Column.TRANSLATION.value)
else:
self.showColumn(Column.TRANSLATION.value)
text_column_count = 2 if self.has_translations else 1
time_column_widths = self.columnWidth(Column.START.value) + self.columnWidth(Column.END.value)
text_column_width = (
int((self.parent().width() - self.PARENT_PADDINGS - time_column_widths) / text_column_count))
self.setColumnWidth(Column.TEXT.value, text_column_width)
self.setColumnWidth(Column.TRANSLATION.value, text_column_width)
def update_translation(self, translation: str, segment_id: Optional[int] = None):
self.has_translations = True
self.resizeEvent(None)
for row in range(self.model().rowCount()):
if self.model().record(row).value("id") == segment_id:
self.model().setData(self.model().index(row, Column.TRANSLATION.value), translation)
break
def on_selection_changed( def on_selection_changed(
self, selected: QItemSelection, _deselected: QItemSelection self, selected: QItemSelection, _deselected: QItemSelection

View file

@ -1,3 +1,4 @@
from enum import Enum
from typing import Optional from typing import Optional
from PyQt6.QtCore import pyqtSignal, Qt from PyQt6.QtCore import pyqtSignal, Qt
@ -10,8 +11,14 @@ from buzz.settings.shortcuts import Shortcuts
from buzz.widgets.icon import VisibilityIcon from buzz.widgets.icon import VisibilityIcon
class ViewMode(Enum):
TEXT = "Text"
TRANSLATION = "Translation"
TIMESTAMPS = "Timestamps"
class TranscriptionViewModeToolButton(QToolButton): class TranscriptionViewModeToolButton(QToolButton):
view_mode_changed = pyqtSignal(bool) # is_timestamps? view_mode_changed = pyqtSignal(ViewMode)
def __init__(self, shortcuts: Shortcuts, parent: Optional[QWidget] = None): def __init__(self, shortcuts: Shortcuts, parent: Optional[QWidget] = None):
super().__init__(parent) super().__init__(parent)
@ -26,12 +33,19 @@ class TranscriptionViewModeToolButton(QToolButton):
menu.addAction( menu.addAction(
_("Text"), _("Text"),
QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TEXT)), QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TEXT)),
lambda: self.view_mode_changed.emit(False), lambda: self.view_mode_changed.emit(ViewMode.TEXT),
)
menu.addAction(
_("Translation"),
QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TRANSLATION)),
lambda: self.view_mode_changed.emit(ViewMode.TRANSLATION)
) )
menu.addAction( menu.addAction(
_("Timestamps"), _("Timestamps"),
QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TIMESTAMPS)), QKeySequence(shortcuts.get(Shortcut.VIEW_TRANSCRIPT_TIMESTAMPS)),
lambda: self.view_mode_changed.emit(True), lambda: self.view_mode_changed.emit(ViewMode.TIMESTAMPS),
) )
self.setMenu(menu) self.setMenu(menu)

View file

@ -1,8 +1,9 @@
import logging
import platform import platform
from typing import Optional from typing import Optional
from uuid import UUID from uuid import UUID
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt, QThread
from PyQt6.QtGui import QFont from PyQt6.QtGui import QFont
from PyQt6.QtMultimedia import QMediaPlayer from PyQt6.QtMultimedia import QMediaPlayer
from PyQt6.QtSql import QSqlRecord from PyQt6.QtSql import QSqlRecord
@ -11,6 +12,7 @@ from PyQt6.QtWidgets import (
QVBoxLayout, QVBoxLayout,
QToolButton, QToolButton,
QLabel, QLabel,
QMessageBox,
) )
from buzz.locale import _ from buzz.locale import _
@ -18,25 +20,36 @@ from buzz.db.entity.transcription import Transcription
from buzz.db.service.transcription_service import TranscriptionService from buzz.db.service.transcription_service import TranscriptionService
from buzz.paths import file_path_as_title from buzz.paths import file_path_as_title
from buzz.settings.shortcuts import Shortcuts from buzz.settings.shortcuts import Shortcuts
from buzz.settings.settings import Settings
from buzz.store.keyring_store import get_password, Key
from buzz.widgets.audio_player import AudioPlayer from buzz.widgets.audio_player import AudioPlayer
from buzz.widgets.icon import ( from buzz.widgets.icon import (
FileDownloadIcon, FileDownloadIcon,
TranslateIcon
) )
from buzz.translator import Translator
from buzz.widgets.text_display_box import TextDisplayBox from buzz.widgets.text_display_box import TextDisplayBox
from buzz.widgets.toolbar import ToolBar from buzz.widgets.toolbar import ToolBar
from buzz.transcriber.transcriber import TranscriptionOptions
from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
from buzz.widgets.transcription_viewer.export_transcription_menu import ( from buzz.widgets.transcription_viewer.export_transcription_menu import (
ExportTranscriptionMenu, ExportTranscriptionMenu,
) )
from buzz.widgets.preferences_dialog.models.file_transcription_preferences import (
FileTranscriptionPreferences,
)
from buzz.widgets.transcription_viewer.transcription_segments_editor_widget import ( from buzz.widgets.transcription_viewer.transcription_segments_editor_widget import (
TranscriptionSegmentsEditorWidget, TranscriptionSegmentsEditorWidget,
) )
from buzz.widgets.transcription_viewer.transcription_view_mode_tool_button import ( from buzz.widgets.transcription_viewer.transcription_view_mode_tool_button import (
TranscriptionViewModeToolButton, TranscriptionViewModeToolButton,
ViewMode
) )
class TranscriptionViewerWidget(QWidget): class TranscriptionViewerWidget(QWidget):
transcription: Transcription transcription: Transcription
settings = Settings()
def __init__( def __init__(
self, self,
@ -55,10 +68,51 @@ class TranscriptionViewerWidget(QWidget):
self.setWindowTitle(file_path_as_title(transcription.file)) self.setWindowTitle(file_path_as_title(transcription.file))
self.is_showing_timestamps = True self.translation_thread = None
self.translator = None
self.view_mode = ViewMode.TIMESTAMPS
self.openai_access_token = get_password(Key.OPENAI_API_KEY)
preferences = self.load_preferences()
(
self.transcription_options,
self.file_transcription_options,
) = preferences.to_transcription_options(
openai_access_token=self.openai_access_token,
)
self.transcription_options_dialog = AdvancedSettingsDialog(
transcription_options=self.transcription_options, parent=self
)
self.transcription_options_dialog.transcription_options_changed.connect(
self.on_transcription_options_changed
)
self.translator = Translator(
self.transcription_options,
self.transcription_options_dialog,
)
self.translation_thread = QThread()
self.translator.moveToThread(self.translation_thread)
self.translation_thread.started.connect(self.translator.start)
self.translation_thread.finished.connect(
self.translation_thread.deleteLater
)
self.translator.finished.connect(self.translation_thread.quit)
self.translator.finished.connect(self.translator.deleteLater)
self.translation_thread.start()
self.table_widget = TranscriptionSegmentsEditorWidget( self.table_widget = TranscriptionSegmentsEditorWidget(
transcription_id=UUID(hex=transcription.id), parent=self transcription_id=UUID(hex=transcription.id),
translator=self.translator,
parent=self
) )
self.table_widget.segment_selected.connect(self.on_segment_selected) self.table_widget.segment_selected.connect(self.on_segment_selected)
@ -102,6 +156,16 @@ class TranscriptionViewerWidget(QWidget):
export_tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) export_tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
toolbar.addWidget(export_tool_button) toolbar.addWidget(export_tool_button)
translate_button = QToolButton()
translate_button.setText(_("Translate"))
translate_button.setIcon(TranslateIcon(self))
translate_button.setToolButtonStyle(
Qt.ToolButtonStyle.ToolButtonTextBesideIcon
)
translate_button.clicked.connect(self.on_translate_button_clicked)
toolbar.addWidget(translate_button)
layout.setMenuBar(toolbar) layout.setMenuBar(toolbar)
layout.addWidget(self.table_widget) layout.addWidget(self.table_widget)
@ -114,10 +178,10 @@ class TranscriptionViewerWidget(QWidget):
self.reset_view() self.reset_view()
def reset_view(self): def reset_view(self):
if self.is_showing_timestamps: if self.view_mode == ViewMode.TIMESTAMPS:
self.text_display_box.hide() self.text_display_box.hide()
self.table_widget.show() self.table_widget.show()
else: elif self.view_mode == ViewMode.TEXT:
segments = self.transcription_service.get_transcription_segments( segments = self.transcription_service.get_transcription_segments(
transcription_id=self.transcription.id_as_uuid transcription_id=self.transcription.id_as_uuid
) )
@ -126,9 +190,19 @@ class TranscriptionViewerWidget(QWidget):
) )
self.text_display_box.show() self.text_display_box.show()
self.table_widget.hide() self.table_widget.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
)
self.text_display_box.setPlainText(
" ".join(segment.translation.strip() for segment in segments)
)
self.text_display_box.show()
self.table_widget.hide()
def on_view_mode_changed(self, is_timestamps: bool) -> None: def on_view_mode_changed(self, view_mode: ViewMode) -> None:
self.is_showing_timestamps = is_timestamps self.view_mode = view_mode
self.reset_view() self.reset_view()
def on_segment_selected(self, segment: QSqlRecord): def on_segment_selected(self, segment: QSqlRecord):
@ -154,3 +228,46 @@ class TranscriptionViewerWidget(QWidget):
) )
if current_segment is not None: if current_segment is not None:
self.current_segment_label.setText(current_segment.value("text")) self.current_segment_label.setText(current_segment.value("text"))
def load_preferences(self):
self.settings.settings.beginGroup("file_transcriber")
preferences = FileTranscriptionPreferences.load(settings=self.settings.settings)
self.settings.settings.endGroup()
return preferences
def open_advanced_settings(self):
self.transcription_options_dialog.show()
def on_transcription_options_changed(
self, transcription_options: TranscriptionOptions
):
self.transcription_options = transcription_options
def on_translate_button_clicked(self):
if len(self.openai_access_token) == 0:
QMessageBox.information(
self,
_("API Key Required"),
_("Please enter OpenAI API Key in preferences")
)
return
if self.transcription_options.llm_model == "" or self.transcription_options.llm_prompt == "":
self.transcription_options_dialog.show()
return
segments = self.table_widget.segments()
for segment in segments:
self.translator.enqueue(segment.value("text"), segment.value("id"))
def closeEvent(self, event):
self.hide()
self.translator.stop()
if self.translation_thread.isRunning():
self.translation_thread.quit()
self.translation_thread.wait()
super().closeEvent(event)

View file

@ -111,7 +111,11 @@ class TestAdvancedSettingsDialog:
def test_should_update_advanced_settings(self, qtbot: QtBot): def test_should_update_advanced_settings(self, qtbot: QtBot):
dialog = AdvancedSettingsDialog( dialog = AdvancedSettingsDialog(
transcription_options=TranscriptionOptions( transcription_options=TranscriptionOptions(
temperature=(0.0, 0.8), initial_prompt="prompt" temperature=(0.0, 0.8),
initial_prompt="prompt",
enable_llm_translation=False,
llm_model="",
llm_prompt=""
) )
) )
qtbot.add_widget(dialog) qtbot.add_widget(dialog)
@ -122,12 +126,21 @@ class TestAdvancedSettingsDialog:
assert dialog.windowTitle() == _("Advanced Settings") assert dialog.windowTitle() == _("Advanced Settings")
assert dialog.temperature_line_edit.text() == "0.0, 0.8" assert dialog.temperature_line_edit.text() == "0.0, 0.8"
assert dialog.initial_prompt_text_edit.toPlainText() == "prompt" assert dialog.initial_prompt_text_edit.toPlainText() == "prompt"
assert dialog.enable_llm_translation_checkbox.isChecked() is False
assert dialog.llm_model_line_edit.text() == ""
assert dialog.llm_prompt_text_edit.toPlainText() == ""
dialog.temperature_line_edit.setText("0.0, 0.8, 1.0") dialog.temperature_line_edit.setText("0.0, 0.8, 1.0")
dialog.initial_prompt_text_edit.setPlainText("new prompt") dialog.initial_prompt_text_edit.setPlainText("new prompt")
dialog.enable_llm_translation_checkbox.setChecked(True)
dialog.llm_model_line_edit.setText("model")
dialog.llm_prompt_text_edit.setPlainText("Please translate this text")
assert transcription_options_mock.call_args[0][0].temperature == (0.0, 0.8, 1.0) assert transcription_options_mock.call_args[0][0].temperature == (0.0, 0.8, 1.0)
assert transcription_options_mock.call_args[0][0].initial_prompt == "new prompt" assert transcription_options_mock.call_args[0][0].initial_prompt == "new prompt"
assert transcription_options_mock.call_args[0][0].enable_llm_translation is True
assert transcription_options_mock.call_args[0][0].llm_model == "model"
assert transcription_options_mock.call_args[0][0].llm_prompt == "Please translate this text"
class TestTemperatureValidator: class TestTemperatureValidator:

View file

@ -7,6 +7,7 @@ from PyQt6.QtCore import QThread
from buzz.translator import Translator from buzz.translator import Translator
from buzz.transcriber.transcriber import TranscriptionOptions from buzz.transcriber.transcriber import TranscriptionOptions
from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
class TestTranslator: class TestTranslator:
@ -21,7 +22,7 @@ class TestTranslator:
if side_effect.call_count < 3: if side_effect.call_count < 3:
raise Empty raise Empty
return 'Hello, how are you?' return "Hello, how are you?", None
side_effect.call_count = 0 side_effect.call_count = 0
@ -31,11 +32,18 @@ class TestTranslator:
mock_chat.completions.create.return_value = Mock( mock_chat.completions.create.return_value = Mock(
choices=[Mock(message=Mock(content="AI Translated: Hello, how are you?"))] choices=[Mock(message=Mock(content="AI Translated: Hello, how are you?"))]
) )
translator = Translator(TranscriptionOptions(
transcription_options = TranscriptionOptions(
enable_llm_translation=False, enable_llm_translation=False,
llm_model="llama3", llm_model="llama3",
llm_prompt="Please translate this text:", llm_prompt="Please translate this text:",
)) )
translator = Translator(
transcription_options,
AdvancedSettingsDialog(
transcription_options=transcription_options, parent=None
)
)
translator.queue = mock_queue translator.queue = mock_queue
translator.start() translator.start()
@ -59,12 +67,18 @@ class TestTranslator:
) )
self.translation_thread = QThread() self.translation_thread = QThread()
self.transcription_options = TranscriptionOptions(
self.translator = Translator(TranscriptionOptions(
enable_llm_translation=False, enable_llm_translation=False,
llm_model="llama3", llm_model="llama3",
llm_prompt="Please translate this text:", llm_prompt="Please translate this text:",
)) )
self.translator = Translator(
self.transcription_options,
AdvancedSettingsDialog(
transcription_options=self.transcription_options, parent=None
)
)
self.translator.moveToThread(self.translation_thread) self.translator.moveToThread(self.translation_thread)

View file

@ -30,9 +30,9 @@ class TestExportTranscriptionMenu:
whisper_model_size=WhisperModelSize.SMALL.value, whisper_model_size=WhisperModelSize.SMALL.value,
) )
) )
transcription_segment_dao.insert(TranscriptionSegment(40, 299, "Bien", str(id))) transcription_segment_dao.insert(TranscriptionSegment(40, 299, "Bien", "", str(id)))
transcription_segment_dao.insert( transcription_segment_dao.insert(
TranscriptionSegment(299, 329, "venue dans", str(id)) TranscriptionSegment(299, 329, "venue dans", "", str(id))
) )
return transcription_dao.find_by_id(str(id)) return transcription_dao.find_by_id(str(id))

View file

@ -35,6 +35,7 @@ class TestShortcutsEditorWidget:
(_("Import URL"), "Ctrl+U"), (_("Import URL"), "Ctrl+U"),
(_("Open Preferences Window"), "Ctrl+,"), (_("Open Preferences Window"), "Ctrl+,"),
(_("View Transcript Text"), "Ctrl+E"), (_("View Transcript Text"), "Ctrl+E"),
(_("View Transcript Translation"), "Ctrl+L"),
(_("View Transcript Timestamps"), "Ctrl+T"), (_("View Transcript Timestamps"), "Ctrl+T"),
(_("Clear History"), "Ctrl+S"), (_("Clear History"), "Ctrl+S"),
(_("Cancel Transcription"), "Ctrl+X"), (_("Cancel Transcription"), "Ctrl+X"),

View file

@ -1,12 +1,18 @@
import uuid import uuid
import time
import pytest import pytest
from pytestqt.qtbot import QtBot from pytestqt.qtbot import QtBot
from buzz.locale import _
from buzz.db.entity.transcription import Transcription from buzz.db.entity.transcription import Transcription
from buzz.db.entity.transcription_segment import TranscriptionSegment from buzz.db.entity.transcription_segment import TranscriptionSegment
from buzz.model_loader import ModelType, WhisperModelSize from buzz.model_loader import ModelType, WhisperModelSize
from buzz.transcriber.transcriber import Task from buzz.transcriber.transcriber import Task
from buzz.widgets.transcription_viewer.transcription_view_mode_tool_button import (
TranscriptionViewModeToolButton,
ViewMode
)
from buzz.widgets.transcription_viewer.transcription_segments_editor_widget import ( from buzz.widgets.transcription_viewer.transcription_segments_editor_widget import (
TranscriptionSegmentsEditorWidget, TranscriptionSegmentsEditorWidget,
) )
@ -32,9 +38,9 @@ class TestTranscriptionViewerWidget:
whisper_model_size=WhisperModelSize.SMALL.value, whisper_model_size=WhisperModelSize.SMALL.value,
) )
) )
transcription_segment_dao.insert(TranscriptionSegment(40, 299, "Bien", str(id))) transcription_segment_dao.insert(TranscriptionSegment(40, 299, "Bien", "", str(id)))
transcription_segment_dao.insert( transcription_segment_dao.insert(
TranscriptionSegment(299, 329, "venue dans", str(id)) TranscriptionSegment(299, 329, "venue dans", "", str(id))
) )
return transcription_dao.find_by_id(str(id)) return transcription_dao.find_by_id(str(id))
@ -55,6 +61,8 @@ class TestTranscriptionViewerWidget:
assert editor.model().index(0, 1).data() == 299 assert editor.model().index(0, 1).data() == 299
assert editor.model().index(0, 2).data() == 40 assert editor.model().index(0, 2).data() == 40
assert editor.model().index(0, 3).data() == "Bien" assert editor.model().index(0, 3).data() == "Bien"
widget.close()
time.sleep(3)
def test_should_update_segment_text( def test_should_update_segment_text(
self, qtbot, transcription, transcription_service, shortcuts self, qtbot, transcription, transcription_service, shortcuts
@ -68,3 +76,27 @@ class TestTranscriptionViewerWidget:
assert isinstance(editor, TranscriptionSegmentsEditorWidget) assert isinstance(editor, TranscriptionSegmentsEditorWidget)
editor.model().setData(editor.model().index(0, 3), "Biens") editor.model().setData(editor.model().index(0, 3), "Biens")
widget.close()
time.sleep(3)
def test_text_button_changes_view_mode(
self, qtbot, transcription, transcription_service, shortcuts
):
widget = TranscriptionViewerWidget(
transcription, transcription_service, shortcuts
)
qtbot.add_widget(widget)
view_mode_tool_button = widget.findChild(TranscriptionViewModeToolButton)
menu = view_mode_tool_button.menu()
text_action = next(action for action in menu.actions() if action.text() == _("Text"))
text_action.trigger()
assert widget.view_mode == ViewMode.TEXT
text_action = next(action for action in menu.actions() if action.text() == _("Translation"))
text_action.trigger()
assert widget.view_mode == ViewMode.TRANSLATION
widget.close()
time.sleep(3)