Adding option to export live recordings to csv

This commit is contained in:
Raivis Dejus 2026-02-27 09:07:15 +02:00
commit bf48eff215
6 changed files with 264 additions and 20 deletions

View file

@ -27,6 +27,9 @@ class Settings:
RECORDING_TRANSCRIBER_MODE = "recording-transcriber/mode"
RECORDING_TRANSCRIBER_SILENCE_THRESHOLD = "recording-transcriber/silence-threshold"
RECORDING_TRANSCRIBER_LINE_SEPARATOR = "recording-transcriber/line-separator"
RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE = "recording-transcriber/export-file-type"
RECORDING_TRANSCRIBER_EXPORT_MAX_ENTRIES = "recording-transcriber/export-max-entries"
RECORDING_TRANSCRIBER_EXPORT_FILE_NAME = "recording-transcriber/export-file-name"
PRESENTATION_WINDOW_TEXT_COLOR = "presentation-window/text-color"
PRESENTATION_WINDOW_BACKGROUND_COLOR = "presentation-window/background-color"

View file

@ -406,8 +406,6 @@ class RecordingTranscriber(QObject):
"--threads", str(os.getenv("BUZZ_WHISPERCPP_N_THREADS", (os.cpu_count() or 8) // 2)),
"--model", self.model_path,
"--no-timestamps",
# on Windows context causes duplications of last message
"--no-context",
# Protections against hallucinated repetition. Seems to be problem on macOS
# https://github.com/ggml-org/whisper.cpp/issues/1507
"--max-context", "64",

View file

@ -188,6 +188,13 @@ class GeneralPreferencesWidget(QWidget):
layout.addRow(_("Live recording mode"), self.recording_transcriber_mode)
export_note_label = QLabel(
_("Note: Live recording export settings will be moved to the Advanced Settings in the Live Recording screen in a future version."),
self,
)
export_note_label.setWordWrap(True)
layout.addRow("", export_note_label)
self.reduce_gpu_memory_enabled = self.settings.value(
key=Settings.Key.REDUCE_GPU_MEMORY, default_value=False
)

View file

@ -1,3 +1,5 @@
import csv
import io
import os
import re
import enum
@ -451,7 +453,17 @@ class RecordingTranscriberWidget(QWidget):
date_time_now = datetime.datetime.now().strftime("%d-%b-%Y %H-%M-%S")
export_file_name_template = Settings().get_default_export_file_template()
custom_template = self.settings.value(
key=Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_NAME,
default_value="",
)
export_file_name_template = custom_template if custom_template else Settings().get_default_export_file_template()
export_file_type = self.settings.value(
key=Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE,
default_value="txt",
)
ext = ".csv" if export_file_type == "csv" else ".txt"
export_file_name = (
export_file_name_template.replace("{{ input_file_name }}", "live recording")
@ -460,14 +472,21 @@ class RecordingTranscriberWidget(QWidget):
.replace("{{ model_type }}", self.transcription_options.model.model_type.value)
.replace("{{ model_size }}", self.transcription_options.model.whisper_model_size or "")
.replace("{{ date_time }}", date_time_now)
+ ".txt"
+ ext
)
translated_ext = ".translated" + ext
if not os.path.isdir(export_folder):
self.export_enabled = False
self.transcript_export_file = os.path.join(export_folder, export_file_name)
self.translation_export_file = self.transcript_export_file.replace(".txt", ".translated.txt")
self.translation_export_file = self.transcript_export_file.replace(ext, translated_ext)
# Clear export files at the start of each recording session
for path in (self.transcript_export_file, self.translation_export_file):
if os.path.isfile(path):
self.write_to_export_file(path, "", mode="w")
def on_transcription_options_changed(
self, transcription_options: TranscriptionOptions
@ -706,6 +725,64 @@ class RecordingTranscriberWidget(QWidget):
logging.warning("Export write failed: %s", e)
return
@staticmethod
def write_csv_export(file_path: str, text: str, max_entries: int):
"""Append a new column to a single-row CSV export file, applying max_entries limit."""
existing_columns = []
if os.path.isfile(file_path):
raw = RecordingTranscriberWidget.read_export_file(file_path)
if raw.strip():
reader = csv.reader(io.StringIO(raw))
for row in reader:
existing_columns = row
break
existing_columns.append(text)
if max_entries > 0:
existing_columns = existing_columns[-max_entries:]
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(existing_columns)
for attempt in range(5):
try:
with open(file_path, "w", encoding='utf-8-sig') as f:
f.write(buf.getvalue())
return
except PermissionError:
if attempt < 4:
time.sleep(0.2)
else:
logging.warning("CSV export write failed after retries: %s", file_path)
except OSError as e:
logging.warning("CSV export write failed: %s", e)
return
@staticmethod
def write_txt_export(file_path: str, text: str, mode: str, max_entries: int, line_separator: str):
"""Write to a TXT export file, applying max_entries limit when needed."""
if mode == "a":
RecordingTranscriberWidget.write_to_export_file(file_path, text + line_separator)
if max_entries > 0 and os.path.isfile(file_path):
raw = RecordingTranscriberWidget.read_export_file(file_path)
parts = [p for p in raw.split(line_separator) if p]
if len(parts) > max_entries:
parts = parts[-max_entries:]
RecordingTranscriberWidget.write_to_export_file(
file_path, line_separator.join(parts) + line_separator, mode="w"
)
elif mode == "prepend":
existing_content = ""
if os.path.isfile(file_path):
existing_content = RecordingTranscriberWidget.read_export_file(file_path)
new_content = text + line_separator + existing_content
if max_entries > 0:
parts = [p for p in new_content.split(line_separator) if p]
if len(parts) > max_entries:
parts = parts[:max_entries]
new_content = line_separator.join(parts) + line_separator
RecordingTranscriberWidget.write_to_export_file(file_path, new_content, mode="w")
else:
RecordingTranscriberWidget.write_to_export_file(file_path, text, mode=mode)
@staticmethod
def read_export_file(file_path: str, retries: int = 5, delay: float = 0.2) -> str:
"""Read an export file with retry logic for Windows file locking."""
@ -777,7 +854,15 @@ class RecordingTranscriberWidget(QWidget):
text_box.moveCursor(QTextCursor.MoveOperation.End)
if self.export_enabled and export_file:
self.write_to_export_file(export_file, merged_texts, mode="w")
export_file_type = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE, "txt"
)
if export_file_type == "csv":
# For APPEND_AND_CORRECT mode, rewrite the whole CSV with all merged text as a single entry
self.write_to_export_file(export_file, "", mode="w")
self.write_csv_export(export_file, merged_texts, 0)
else:
self.write_to_export_file(export_file, merged_texts, mode="w")
def on_next_transcription(self, text: str):
text = self.filter_text(text)
@ -788,6 +873,13 @@ class RecordingTranscriberWidget(QWidget):
if self.translator is not None:
self.translator.enqueue(text)
export_file_type = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE, "txt"
)
max_entries = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_MAX_ENTRIES, 0, int
)
if self.transcriber_mode == RecordingTranscriberMode.APPEND_BELOW:
self.transcription_text_box.moveCursor(QTextCursor.MoveOperation.End)
if len(self.transcription_text_box.toPlainText()) > 0:
@ -796,7 +888,10 @@ class RecordingTranscriberWidget(QWidget):
self.transcription_text_box.moveCursor(QTextCursor.MoveOperation.End)
if self.export_enabled and self.transcript_export_file:
self.write_to_export_file(self.transcript_export_file, text + self.transcription_options.line_separator)
if export_file_type == "csv":
self.write_csv_export(self.transcript_export_file, text, max_entries)
else:
self.write_txt_export(self.transcript_export_file, text, "a", max_entries, self.transcription_options.line_separator)
elif self.transcriber_mode == RecordingTranscriberMode.APPEND_ABOVE:
self.transcription_text_box.moveCursor(QTextCursor.MoveOperation.Start)
@ -805,11 +900,25 @@ class RecordingTranscriberWidget(QWidget):
self.transcription_text_box.moveCursor(QTextCursor.MoveOperation.Start)
if self.export_enabled and self.transcript_export_file:
existing_content = ""
if os.path.isfile(self.transcript_export_file):
existing_content = self.read_export_file(self.transcript_export_file)
new_content = text + self.transcription_options.line_separator + existing_content
self.write_to_export_file(self.transcript_export_file, new_content, mode="w")
if export_file_type == "csv":
# For APPEND_ABOVE, prepend in CSV means inserting at beginning of columns
existing_columns = []
if os.path.isfile(self.transcript_export_file):
raw = self.read_export_file(self.transcript_export_file)
if raw.strip():
reader = csv.reader(io.StringIO(raw))
for row in reader:
existing_columns = row
break
new_columns = [text] + existing_columns
if max_entries > 0:
new_columns = new_columns[:max_entries]
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(new_columns)
self.write_to_export_file(self.transcript_export_file, buf.getvalue(), mode="w")
else:
self.write_txt_export(self.transcript_export_file, text, "prepend", max_entries, self.transcription_options.line_separator)
elif self.transcriber_mode == RecordingTranscriberMode.APPEND_AND_CORRECT:
self.process_transcription_merge(text, self.transcripts, self.transcription_text_box, self.transcript_export_file)
@ -836,6 +945,13 @@ class RecordingTranscriberWidget(QWidget):
if len(text) == 0:
return
export_file_type = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE, "txt"
)
max_entries = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_MAX_ENTRIES, 0, int
)
if self.transcriber_mode == RecordingTranscriberMode.APPEND_BELOW:
self.translation_text_box.moveCursor(QTextCursor.MoveOperation.End)
if len(self.translation_text_box.toPlainText()) > 0:
@ -844,7 +960,10 @@ class RecordingTranscriberWidget(QWidget):
self.translation_text_box.moveCursor(QTextCursor.MoveOperation.End)
if self.export_enabled and self.translation_export_file:
self.write_to_export_file(self.translation_export_file, text + self.transcription_options.line_separator)
if export_file_type == "csv":
self.write_csv_export(self.translation_export_file, text, max_entries)
else:
self.write_txt_export(self.translation_export_file, text, "a", max_entries, self.transcription_options.line_separator)
elif self.transcriber_mode == RecordingTranscriberMode.APPEND_ABOVE:
self.translation_text_box.moveCursor(QTextCursor.MoveOperation.Start)
@ -853,11 +972,24 @@ class RecordingTranscriberWidget(QWidget):
self.translation_text_box.moveCursor(QTextCursor.MoveOperation.Start)
if self.export_enabled and self.translation_export_file:
existing_content = ""
if os.path.isfile(self.translation_export_file):
existing_content = self.read_export_file(self.translation_export_file)
new_content = text + self.transcription_options.line_separator + existing_content
self.write_to_export_file(self.translation_export_file, new_content, mode="w")
if export_file_type == "csv":
existing_columns = []
if os.path.isfile(self.translation_export_file):
raw = self.read_export_file(self.translation_export_file)
if raw.strip():
reader = csv.reader(io.StringIO(raw))
for row in reader:
existing_columns = row
break
new_columns = [text] + existing_columns
if max_entries > 0:
new_columns = new_columns[:max_entries]
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(new_columns)
self.write_to_export_file(self.translation_export_file, buf.getvalue(), mode="w")
else:
self.write_txt_export(self.translation_export_file, text, "prepend", max_entries, self.transcription_options.line_separator)
elif self.transcriber_mode == RecordingTranscriberMode.APPEND_AND_CORRECT:
self.process_transcription_merge(text, self.translations, self.translation_text_box, self.translation_export_file)

View file

@ -9,12 +9,18 @@ from PyQt6.QtWidgets import (
QLabel,
QDoubleSpinBox,
QLineEdit,
QComboBox,
QHBoxLayout,
QPushButton,
QSpinBox,
QFileDialog,
)
from buzz.locale import _
from buzz.model_loader import ModelType
from buzz.transcriber.transcriber import TranscriptionOptions
from buzz.settings.settings import Settings
from buzz.settings.recording_transcriber_mode import RecordingTranscriberMode
from buzz.widgets.line_edit import LineEdit
from buzz.widgets.transcriber.initial_prompt_text_edit import InitialPromptTextEdit
from buzz.widgets.transcriber.temperature_validator import TemperatureValidator
@ -94,7 +100,7 @@ class AdvancedSettingsDialog(QDialog):
self.llm_prompt_text_edit = QPlainTextEdit(default_llm_prompt)
self.llm_prompt_text_edit.setEnabled(self.transcription_options.enable_llm_translation)
self.llm_prompt_text_edit.setMinimumWidth(170)
self.llm_prompt_text_edit.setFixedHeight(115)
self.llm_prompt_text_edit.setFixedHeight(80)
self.llm_prompt_text_edit.textChanged.connect(self.on_llm_prompt_changed)
layout.addRow(_("Instructions for AI:"), self.llm_prompt_text_edit)
@ -117,6 +123,74 @@ class AdvancedSettingsDialog(QDialog):
self.line_separator_line_edit.textChanged.connect(self.on_line_separator_changed)
layout.addRow(_("Line separator:"), self.line_separator_line_edit)
# Live recording mode
self.recording_mode_combo = QComboBox(self)
for mode in RecordingTranscriberMode:
self.recording_mode_combo.addItem(mode.value)
self.recording_mode_combo.setCurrentIndex(
self.settings.value(Settings.Key.RECORDING_TRANSCRIBER_MODE, 0)
)
self.recording_mode_combo.currentIndexChanged.connect(self.on_recording_mode_changed)
layout.addRow(_("Live recording mode:"), self.recording_mode_combo)
# Export enabled checkbox
self._export_enabled = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_ENABLED, False
)
self.export_enabled_checkbox = QCheckBox(_("Enable live recording export"))
self.export_enabled_checkbox.setChecked(self._export_enabled)
self.export_enabled_checkbox.stateChanged.connect(self.on_export_enabled_changed)
layout.addRow("", self.export_enabled_checkbox)
# Export folder
export_folder = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FOLDER, ""
)
self.export_folder_line_edit = LineEdit(export_folder, self)
self.export_folder_line_edit.setEnabled(self._export_enabled)
self.export_folder_line_edit.textChanged.connect(self.on_export_folder_changed)
self.export_folder_browse_button = QPushButton(_("Browse"), self)
self.export_folder_browse_button.setEnabled(self._export_enabled)
self.export_folder_browse_button.clicked.connect(self.on_browse_export_folder)
export_folder_row = QHBoxLayout()
export_folder_row.addWidget(self.export_folder_line_edit)
export_folder_row.addWidget(self.export_folder_browse_button)
layout.addRow(_("Export folder:"), export_folder_row)
# Export file name template
export_file_name = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_NAME, ""
)
self.export_file_name_line_edit = LineEdit(export_file_name, self)
self.export_file_name_line_edit.setEnabled(self._export_enabled)
self.export_file_name_line_edit.textChanged.connect(self.on_export_file_name_changed)
layout.addRow(_("Export file name:"), self.export_file_name_line_edit)
# Export file type
self.export_file_type_combo = QComboBox(self)
self.export_file_type_combo.addItem(_("Text file (.txt)"), "txt")
self.export_file_type_combo.addItem(_("CSV (.csv)"), "csv")
current_type = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE, "txt"
)
type_index = self.export_file_type_combo.findData(current_type)
if type_index >= 0:
self.export_file_type_combo.setCurrentIndex(type_index)
self.export_file_type_combo.setEnabled(self._export_enabled)
self.export_file_type_combo.currentIndexChanged.connect(self.on_export_file_type_changed)
layout.addRow(_("Export file type:"), self.export_file_type_combo)
# Max entries
max_entries = self.settings.value(
Settings.Key.RECORDING_TRANSCRIBER_EXPORT_MAX_ENTRIES, 0, int
)
self.export_max_entries_spin = QSpinBox(self)
self.export_max_entries_spin.setRange(0, 99)
self.export_max_entries_spin.setValue(max_entries)
self.export_max_entries_spin.setEnabled(self._export_enabled)
self.export_max_entries_spin.valueChanged.connect(self.on_export_max_entries_changed)
layout.addRow(_("Limit export entries\n(0 = export all):"), self.export_max_entries_spin)
button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton(QDialogButtonBox.StandardButton.Ok), self
)
@ -168,3 +242,33 @@ class AdvancedSettingsDialog(QDialog):
except UnicodeDecodeError:
return
self.transcription_options_changed.emit(self.transcription_options)
def on_recording_mode_changed(self, index: int):
self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_MODE, index)
def on_export_enabled_changed(self, state: int):
self._export_enabled = state == 2
self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_ENABLED, self._export_enabled)
self.export_folder_line_edit.setEnabled(self._export_enabled)
self.export_folder_browse_button.setEnabled(self._export_enabled)
self.export_file_name_line_edit.setEnabled(self._export_enabled)
self.export_file_type_combo.setEnabled(self._export_enabled)
self.export_max_entries_spin.setEnabled(self._export_enabled)
def on_export_folder_changed(self, text: str):
self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FOLDER, text)
def on_browse_export_folder(self):
folder = QFileDialog.getExistingDirectory(self, _("Select Export Folder"))
if folder:
self.export_folder_line_edit.setText(folder)
def on_export_file_name_changed(self, text: str):
self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_NAME, text)
def on_export_file_type_changed(self, index: int):
file_type = self.export_file_type_combo.itemData(index)
self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_FILE_TYPE, file_type)
def on_export_max_entries_changed(self, value: int):
self.settings.set_value(Settings.Key.RECORDING_TRANSCRIBER_EXPORT_MAX_ENTRIES, value)

View file

@ -10,4 +10,4 @@ class InitialPromptTextEdit(QPlainTextEdit):
self.setPlaceholderText(_("Enter prompt..."))
self.setEnabled(model_type.supports_initial_prompt)
self.setMinimumWidth(350)
self.setFixedHeight(115)
self.setFixedHeight(80)