Phase 6: Add Deepgram remote transcription (managed + BYOK modes)
New files: - client/deepgram_transcription.py — DeepgramTranscriptionEngine with managed mode (proxy) and BYOK mode (direct Deepgram). Sends raw binary PCM audio over WebSocket, handles both proxy and Deepgram response formats. Modified files: - config/default_config.yaml — Replace remote_processing with new remote section (mode, server_url, auth_token, byok_api_key, deepgram_model, language) - client/config.py — Add migration from old remote_processing config - gui/settings_dialog_qt.py — Replace Remote Processing group with Transcription Mode section (Local/Managed/BYOK radio buttons, login/register dialogs, balance display, model selector) - gui/main_window_qt.py — Select engine based on remote.mode config, add error and credits_low handlers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||
QLabel, QLineEdit, QComboBox, QCheckBox, QSlider,
|
||||
QPushButton, QMessageBox, QGroupBox, QScrollArea, QWidget,
|
||||
QFileDialog, QColorDialog
|
||||
QFileDialog, QColorDialog, QRadioButton
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QScreen, QFontDatabase, QColor
|
||||
@@ -487,46 +487,91 @@ class SettingsDialog(QDialog):
|
||||
server_group.setLayout(server_layout)
|
||||
content_layout.addWidget(server_group)
|
||||
|
||||
# Remote Processing Group
|
||||
remote_group = QGroupBox("Remote Processing (GPU Offload)")
|
||||
remote_layout = QFormLayout()
|
||||
remote_layout.setSpacing(10)
|
||||
# Transcription Mode Group
|
||||
mode_group = QGroupBox("Transcription Mode")
|
||||
mode_layout = QVBoxLayout()
|
||||
mode_layout.setSpacing(10)
|
||||
|
||||
self.remote_enabled_check = QCheckBox()
|
||||
self.remote_enabled_check.setToolTip(
|
||||
"Enable remote transcription processing:\n"
|
||||
"• Offload transcription to a GPU-equipped server\n"
|
||||
"• Reduces local CPU/GPU usage\n"
|
||||
"• Requires running the remote transcription service"
|
||||
)
|
||||
remote_layout.addRow("Enable Remote Processing:", self.remote_enabled_check)
|
||||
# Radio buttons for mode selection
|
||||
self.mode_local_radio = QRadioButton("Local (Whisper)")
|
||||
self.mode_local_radio.setToolTip("Transcribe locally using Whisper models")
|
||||
self.mode_managed_radio = QRadioButton("Remote - Managed")
|
||||
self.mode_managed_radio.setToolTip("Use the transcription proxy service with prepaid credits")
|
||||
self.mode_byok_radio = QRadioButton("Remote - BYOK (Bring Your Own Key)")
|
||||
self.mode_byok_radio.setToolTip("Connect directly to Deepgram with your own API key")
|
||||
|
||||
self.remote_url_input = QLineEdit()
|
||||
self.remote_url_input.setPlaceholderText("ws://your-server:8765/ws/transcribe")
|
||||
self.remote_url_input.setToolTip(
|
||||
"WebSocket URL of the remote transcription service:\n"
|
||||
"• Format: ws://host:port/ws/transcribe\n"
|
||||
"• Use wss:// for secure connections"
|
||||
)
|
||||
remote_layout.addRow("Server URL:", self.remote_url_input)
|
||||
mode_layout.addWidget(self.mode_local_radio)
|
||||
mode_layout.addWidget(self.mode_managed_radio)
|
||||
mode_layout.addWidget(self.mode_byok_radio)
|
||||
|
||||
self.remote_api_key_input = QLineEdit()
|
||||
self.remote_api_key_input.setEchoMode(QLineEdit.Password)
|
||||
self.remote_api_key_input.setPlaceholderText("your-api-key")
|
||||
self.remote_api_key_input.setToolTip(
|
||||
"API key for authentication with the remote service"
|
||||
)
|
||||
remote_layout.addRow("API Key:", self.remote_api_key_input)
|
||||
# Managed mode fields (shown when managed radio selected)
|
||||
self.managed_widget = QWidget()
|
||||
managed_layout = QFormLayout()
|
||||
managed_layout.setSpacing(8)
|
||||
|
||||
self.remote_fallback_check = QCheckBox("Enable")
|
||||
self.remote_fallback_check.setChecked(True)
|
||||
self.remote_fallback_check.setToolTip(
|
||||
"Fall back to local transcription if remote service is unavailable"
|
||||
)
|
||||
remote_layout.addRow("Fallback to Local:", self.remote_fallback_check)
|
||||
self.managed_server_url = QLineEdit()
|
||||
self.managed_server_url.setPlaceholderText("wss://your-proxy-server.com")
|
||||
managed_layout.addRow("Server URL:", self.managed_server_url)
|
||||
|
||||
remote_group.setLayout(remote_layout)
|
||||
content_layout.addWidget(remote_group)
|
||||
# Login/Register buttons in a row
|
||||
auth_widget = QWidget()
|
||||
auth_layout = QHBoxLayout()
|
||||
auth_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.managed_login_btn = QPushButton("Login")
|
||||
self.managed_login_btn.clicked.connect(self._managed_login)
|
||||
self.managed_register_btn = QPushButton("Register")
|
||||
self.managed_register_btn.clicked.connect(self._managed_register)
|
||||
auth_layout.addWidget(self.managed_login_btn)
|
||||
auth_layout.addWidget(self.managed_register_btn)
|
||||
auth_layout.addStretch()
|
||||
auth_widget.setLayout(auth_layout)
|
||||
managed_layout.addRow("Account:", auth_widget)
|
||||
|
||||
self.managed_balance_label = QLabel("Not logged in")
|
||||
managed_layout.addRow("Balance:", self.managed_balance_label)
|
||||
|
||||
self.managed_fallback_check = QCheckBox("Enable")
|
||||
self.managed_fallback_check.setChecked(True)
|
||||
self.managed_fallback_check.setToolTip("Fall back to local Whisper if remote fails")
|
||||
managed_layout.addRow("Fallback to Local:", self.managed_fallback_check)
|
||||
|
||||
self.managed_widget.setLayout(managed_layout)
|
||||
mode_layout.addWidget(self.managed_widget)
|
||||
|
||||
# BYOK mode fields (shown when BYOK radio selected)
|
||||
self.byok_widget = QWidget()
|
||||
byok_layout = QFormLayout()
|
||||
byok_layout.setSpacing(8)
|
||||
|
||||
self.byok_api_key_input = QLineEdit()
|
||||
self.byok_api_key_input.setEchoMode(QLineEdit.Password)
|
||||
self.byok_api_key_input.setPlaceholderText("your-deepgram-api-key")
|
||||
byok_layout.addRow("Deepgram API Key:", self.byok_api_key_input)
|
||||
|
||||
self.byok_model_combo = QComboBox()
|
||||
self.byok_model_combo.addItems(["nova-2", "nova-2-general", "nova-2-meeting", "nova-2-phonecall", "whisper-large", "whisper-medium", "whisper-small"])
|
||||
byok_layout.addRow("Model:", self.byok_model_combo)
|
||||
|
||||
self.byok_language_input = QLineEdit()
|
||||
self.byok_language_input.setText("en-US")
|
||||
self.byok_language_input.setPlaceholderText("en-US")
|
||||
byok_layout.addRow("Language:", self.byok_language_input)
|
||||
|
||||
self.byok_fallback_check = QCheckBox("Enable")
|
||||
self.byok_fallback_check.setChecked(True)
|
||||
self.byok_fallback_check.setToolTip("Fall back to local Whisper if Deepgram fails")
|
||||
byok_layout.addRow("Fallback to Local:", self.byok_fallback_check)
|
||||
|
||||
self.byok_widget.setLayout(byok_layout)
|
||||
mode_layout.addWidget(self.byok_widget)
|
||||
|
||||
mode_group.setLayout(mode_layout)
|
||||
content_layout.addWidget(mode_group)
|
||||
|
||||
# Connect radio buttons to show/hide relevant widgets
|
||||
self.mode_local_radio.toggled.connect(self._on_mode_changed)
|
||||
self.mode_managed_radio.toggled.connect(self._on_mode_changed)
|
||||
self.mode_byok_radio.toggled.connect(self._on_mode_changed)
|
||||
|
||||
# Updates Group
|
||||
updates_group = QGroupBox("Software Updates")
|
||||
@@ -794,11 +839,28 @@ class SettingsDialog(QDialog):
|
||||
self.server_room_input.setText(self.config.get('server_sync.room', 'default'))
|
||||
self.server_passphrase_input.setText(self.config.get('server_sync.passphrase', ''))
|
||||
|
||||
# Remote processing settings
|
||||
self.remote_enabled_check.setChecked(self.config.get('remote_processing.enabled', False))
|
||||
self.remote_url_input.setText(self.config.get('remote_processing.server_url', ''))
|
||||
self.remote_api_key_input.setText(self.config.get('remote_processing.api_key', ''))
|
||||
self.remote_fallback_check.setChecked(self.config.get('remote_processing.fallback_to_local', True))
|
||||
# Transcription mode settings
|
||||
mode = self.config.get('remote.mode', 'local')
|
||||
if mode == 'managed':
|
||||
self.mode_managed_radio.setChecked(True)
|
||||
elif mode == 'byok':
|
||||
self.mode_byok_radio.setChecked(True)
|
||||
else:
|
||||
self.mode_local_radio.setChecked(True)
|
||||
|
||||
self.managed_server_url.setText(self.config.get('remote.server_url', ''))
|
||||
self.managed_fallback_check.setChecked(self.config.get('remote.fallback_to_local', True))
|
||||
self.byok_api_key_input.setText(self.config.get('remote.byok_api_key', ''))
|
||||
self.byok_model_combo.setCurrentText(self.config.get('remote.deepgram_model', 'nova-2'))
|
||||
self.byok_language_input.setText(self.config.get('remote.language', 'en-US'))
|
||||
self.byok_fallback_check.setChecked(self.config.get('remote.fallback_to_local', True))
|
||||
|
||||
# Trigger visibility update
|
||||
self._on_mode_changed()
|
||||
|
||||
# Update balance if managed mode and has token
|
||||
if self.config.get('remote.auth_token'):
|
||||
self._update_managed_balance()
|
||||
|
||||
# Update settings
|
||||
self.update_auto_check.setChecked(self.config.get('updates.auto_check', True))
|
||||
@@ -869,11 +931,21 @@ class SettingsDialog(QDialog):
|
||||
self.config.set('server_sync.room', self.server_room_input.text())
|
||||
self.config.set('server_sync.passphrase', self.server_passphrase_input.text())
|
||||
|
||||
# Remote processing settings
|
||||
self.config.set('remote_processing.enabled', self.remote_enabled_check.isChecked())
|
||||
self.config.set('remote_processing.server_url', self.remote_url_input.text())
|
||||
self.config.set('remote_processing.api_key', self.remote_api_key_input.text())
|
||||
self.config.set('remote_processing.fallback_to_local', self.remote_fallback_check.isChecked())
|
||||
# Transcription mode settings
|
||||
if self.mode_managed_radio.isChecked():
|
||||
self.config.set('remote.mode', 'managed')
|
||||
elif self.mode_byok_radio.isChecked():
|
||||
self.config.set('remote.mode', 'byok')
|
||||
else:
|
||||
self.config.set('remote.mode', 'local')
|
||||
|
||||
self.config.set('remote.server_url', self.managed_server_url.text())
|
||||
self.config.set('remote.fallback_to_local',
|
||||
self.managed_fallback_check.isChecked() if self.mode_managed_radio.isChecked()
|
||||
else self.byok_fallback_check.isChecked())
|
||||
self.config.set('remote.byok_api_key', self.byok_api_key_input.text())
|
||||
self.config.set('remote.deepgram_model', self.byok_model_combo.currentText())
|
||||
self.config.set('remote.language', self.byok_language_input.text())
|
||||
|
||||
# Update settings
|
||||
self.config.set('updates.auto_check', self.update_auto_check.isChecked())
|
||||
@@ -892,6 +964,194 @@ class SettingsDialog(QDialog):
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to save settings:\n{e}")
|
||||
|
||||
def _on_mode_changed(self):
|
||||
"""Show/hide mode-specific widgets based on selected radio button."""
|
||||
self.managed_widget.setVisible(self.mode_managed_radio.isChecked())
|
||||
self.byok_widget.setVisible(self.mode_byok_radio.isChecked())
|
||||
|
||||
def _managed_login(self):
|
||||
"""Open a login dialog and authenticate with the managed proxy server."""
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("Login")
|
||||
dialog.setMinimumWidth(350)
|
||||
layout = QFormLayout()
|
||||
|
||||
email_input = QLineEdit()
|
||||
email_input.setPlaceholderText("you@example.com")
|
||||
layout.addRow("Email:", email_input)
|
||||
|
||||
password_input = QLineEdit()
|
||||
password_input.setEchoMode(QLineEdit.Password)
|
||||
layout.addRow("Password:", password_input)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
login_btn = QPushButton("Login")
|
||||
login_btn.setDefault(True)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(cancel_btn)
|
||||
button_layout.addWidget(login_btn)
|
||||
layout.addRow("", button_layout)
|
||||
|
||||
dialog.setLayout(layout)
|
||||
|
||||
def do_login():
|
||||
server_url = self.managed_server_url.text().rstrip('/')
|
||||
if not server_url:
|
||||
QMessageBox.warning(dialog, "Error", "Please enter a Server URL first.")
|
||||
return
|
||||
payload = json.dumps({
|
||||
"email": email_input.text(),
|
||||
"password": password_input.text()
|
||||
}).encode('utf-8')
|
||||
req = urllib.request.Request(
|
||||
f"{server_url}/auth/login",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
token = data.get('token', '')
|
||||
if token:
|
||||
self.config.set('remote.auth_token', token)
|
||||
self._update_managed_balance()
|
||||
QMessageBox.information(dialog, "Success", "Logged in successfully.")
|
||||
dialog.accept()
|
||||
else:
|
||||
QMessageBox.warning(dialog, "Error", "Login succeeded but no token received.")
|
||||
except urllib.error.HTTPError as e:
|
||||
try:
|
||||
body = json.loads(e.read().decode('utf-8'))
|
||||
msg = body.get('detail', body.get('message', str(e)))
|
||||
except Exception:
|
||||
msg = str(e)
|
||||
QMessageBox.warning(dialog, "Login Failed", msg)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(dialog, "Error", f"Could not connect to server:\n{e}")
|
||||
|
||||
login_btn.clicked.connect(do_login)
|
||||
dialog.exec()
|
||||
|
||||
def _managed_register(self):
|
||||
"""Open a registration dialog and create an account on the managed proxy server."""
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("Register")
|
||||
dialog.setMinimumWidth(350)
|
||||
layout = QFormLayout()
|
||||
|
||||
email_input = QLineEdit()
|
||||
email_input.setPlaceholderText("you@example.com")
|
||||
layout.addRow("Email:", email_input)
|
||||
|
||||
password_input = QLineEdit()
|
||||
password_input.setEchoMode(QLineEdit.Password)
|
||||
layout.addRow("Password:", password_input)
|
||||
|
||||
confirm_input = QLineEdit()
|
||||
confirm_input.setEchoMode(QLineEdit.Password)
|
||||
layout.addRow("Confirm Password:", confirm_input)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
register_btn = QPushButton("Register")
|
||||
register_btn.setDefault(True)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(cancel_btn)
|
||||
button_layout.addWidget(register_btn)
|
||||
layout.addRow("", button_layout)
|
||||
|
||||
dialog.setLayout(layout)
|
||||
|
||||
def do_register():
|
||||
if password_input.text() != confirm_input.text():
|
||||
QMessageBox.warning(dialog, "Error", "Passwords do not match.")
|
||||
return
|
||||
server_url = self.managed_server_url.text().rstrip('/')
|
||||
if not server_url:
|
||||
QMessageBox.warning(dialog, "Error", "Please enter a Server URL first.")
|
||||
return
|
||||
payload = json.dumps({
|
||||
"email": email_input.text(),
|
||||
"password": password_input.text()
|
||||
}).encode('utf-8')
|
||||
req = urllib.request.Request(
|
||||
f"{server_url}/auth/register",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
token = data.get('token', '')
|
||||
if token:
|
||||
self.config.set('remote.auth_token', token)
|
||||
self._update_managed_balance()
|
||||
QMessageBox.information(dialog, "Success", "Account created and logged in.")
|
||||
dialog.accept()
|
||||
else:
|
||||
QMessageBox.information(dialog, "Success",
|
||||
"Account created. Please log in.")
|
||||
dialog.accept()
|
||||
except urllib.error.HTTPError as e:
|
||||
try:
|
||||
body = json.loads(e.read().decode('utf-8'))
|
||||
msg = body.get('detail', body.get('message', str(e)))
|
||||
except Exception:
|
||||
msg = str(e)
|
||||
QMessageBox.warning(dialog, "Registration Failed", msg)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(dialog, "Error", f"Could not connect to server:\n{e}")
|
||||
|
||||
register_btn.clicked.connect(do_register)
|
||||
dialog.exec()
|
||||
|
||||
def _update_managed_balance(self):
|
||||
"""Fetch and display the current account balance from the managed proxy server."""
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
server_url = self.managed_server_url.text().rstrip('/')
|
||||
token = self.config.get('remote.auth_token', '')
|
||||
if not server_url or not token:
|
||||
self.managed_balance_label.setText("Not logged in")
|
||||
return
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{server_url}/billing/balance",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method="GET"
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
balance = data.get('balance', data.get('credits', 'N/A'))
|
||||
self.managed_balance_label.setText(str(balance))
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 401:
|
||||
self.managed_balance_label.setText("Session expired - please login again")
|
||||
self.config.set('remote.auth_token', '')
|
||||
else:
|
||||
self.managed_balance_label.setText("Error fetching balance")
|
||||
except Exception:
|
||||
self.managed_balance_label.setText("Could not connect to server")
|
||||
|
||||
def _check_for_updates_now(self):
|
||||
"""Manually check for updates."""
|
||||
from version import __version__
|
||||
|
||||
Reference in New Issue
Block a user