Files
local-transcription/client/config.py
Developer 9ff883e2e3 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>
2026-04-05 11:45:30 -07:00

193 lines
6.1 KiB
Python

"""Configuration management for the local transcription application."""
import os
import yaml
from pathlib import Path
from typing import Any, Dict, Optional
class Config:
"""Manages application configuration with YAML file storage."""
def __init__(self, config_path: Optional[str] = None):
"""
Initialize configuration.
Args:
config_path: Path to configuration file. If None, uses default location.
"""
self.app_dir = Path.home() / ".local-transcription"
self.app_dir.mkdir(parents=True, exist_ok=True)
# Fonts directory for custom font files
self.fonts_dir = self.app_dir / "fonts"
self.fonts_dir.mkdir(parents=True, exist_ok=True)
if config_path is None:
self.config_path = self.app_dir / "config.yaml"
else:
self.config_path = Path(config_path)
self.config: Dict[str, Any] = {}
self.load()
def load(self) -> None:
"""Load configuration from file or create default if not exists."""
if self.config_path.exists():
with open(self.config_path, 'r') as f:
self.config = yaml.safe_load(f) or {}
else:
# Load default configuration
default_config_path = Path(__file__).resolve().parent.parent / "config" / "default_config.yaml"
if default_config_path.exists():
with open(default_config_path, 'r') as f:
self.config = yaml.safe_load(f) or {}
else:
self.config = self._get_default_config()
# Save the default configuration
self.save()
# Migrate remote_processing -> remote
self._migrate_remote_config()
def _migrate_remote_config(self):
"""Migrate old remote_processing config to new remote config."""
if 'remote_processing' in self.config and 'remote' not in self.config:
old = self.config['remote_processing']
self.config['remote'] = {
'mode': 'managed' if old.get('enabled', False) else 'local',
'server_url': old.get('server_url', ''),
'auth_token': '',
'byok_api_key': old.get('api_key', ''),
'deepgram_model': 'nova-2',
'language': 'en-US',
'fallback_to_local': old.get('fallback_to_local', True),
}
del self.config['remote_processing']
self.save()
def save(self) -> None:
"""Save current configuration to file."""
with open(self.config_path, 'w') as f:
yaml.dump(self.config, f, default_flow_style=False, indent=2)
def get(self, key_path: str, default: Any = None) -> Any:
"""
Get configuration value using dot notation.
Args:
key_path: Dot-separated path to config value (e.g., "audio.sample_rate")
default: Default value if key not found
Returns:
Configuration value or default
"""
keys = key_path.split('.')
value = self.config
for key in keys:
if isinstance(value, dict) and key in value:
value = value[key]
else:
return default
return value
def set(self, key_path: str, value: Any) -> None:
"""
Set configuration value using dot notation.
Args:
key_path: Dot-separated path to config value (e.g., "audio.sample_rate")
value: Value to set
"""
keys = key_path.split('.')
config = self.config
# Navigate to the parent dict
for key in keys[:-1]:
if key not in config:
config[key] = {}
config = config[key]
# Set the value
config[keys[-1]] = value
self.save()
def _get_default_config(self) -> Dict[str, Any]:
"""Get hardcoded default configuration."""
return {
'user': {
'name': 'User',
'id': ''
},
'audio': {
'input_device': 'default',
'sample_rate': 16000,
'chunk_duration': 3.0
},
'noise_suppression': {
'enabled': True,
'strength': 0.7,
'method': 'noisereduce'
},
'transcription': {
'model': 'base',
'device': 'auto',
'language': 'en',
'task': 'transcribe'
},
'processing': {
'use_vad': True,
'min_confidence': 0.5
},
'server_sync': {
'enabled': False,
'url': 'ws://localhost:8000',
'api_key': ''
},
'display': {
'show_timestamps': True,
'max_lines': 100,
'font_size': 12,
'theme': 'dark'
},
'updates': {
'auto_check': True,
'gitea_url': 'https://repo.anhonesthost.net',
'owner': 'streamer-tools',
'repo': 'local-transcription',
'skipped_versions': [],
'last_check': '',
'check_interval_hours': 24
}
}
def reset_to_default(self) -> None:
"""Reset configuration to default values."""
self.config = self._get_default_config()
self.save()
def get_custom_fonts(self) -> list:
"""
Get list of custom font files in the fonts directory.
Returns:
List of (font_name, font_path) tuples
"""
fonts = []
font_extensions = {'.ttf', '.otf', '.woff', '.woff2'}
if self.fonts_dir.exists():
for font_file in self.fonts_dir.iterdir():
if font_file.suffix.lower() in font_extensions:
# Use filename without extension as font name
font_name = font_file.stem
fonts.append((font_name, font_file))
return sorted(fonts, key=lambda x: x[0].lower())
def __repr__(self) -> str:
return f"Config(path={self.config_path})"