diff --git a/client/server_sync.py b/client/server_sync.py index 4d3f218..ebaa056 100644 --- a/client/server_sync.py +++ b/client/server_sync.py @@ -19,7 +19,10 @@ class ServerSyncClient: font_source: str = "None", websafe_font: Optional[str] = None, google_font: Optional[str] = None, - custom_font_file: Optional[str] = None): + custom_font_file: Optional[str] = None, + user_color: str = "#4CAF50", + text_color: str = "#FFFFFF", + background_color: str = "#000000B3"): """ Initialize server sync client. @@ -33,6 +36,9 @@ class ServerSyncClient: websafe_font: Web-safe font name (e.g., "Arial", "Times New Roman") google_font: Google Font name (e.g., "Roboto", "Open Sans") custom_font_file: Path to a custom font file for this speaker + user_color: User name color (hex format) + text_color: Text color (hex format) + background_color: Background color (hex format with optional alpha) """ self.url = url self.room = room @@ -43,6 +49,9 @@ class ServerSyncClient: self.websafe_font = websafe_font self.google_font = google_font self.custom_font_file = custom_font_file + self.user_color = user_color + self.text_color = text_color + self.background_color = background_color # Font info to send with transcriptions self.font_family: Optional[str] = None @@ -303,7 +312,11 @@ class ServerSyncClient: 'user_name': self.user_name, 'text': trans_data['text'], 'timestamp': trans_data['timestamp'], - 'is_preview': trans_data.get('is_preview', False) + 'is_preview': trans_data.get('is_preview', False), + # Always include user's color settings + 'user_color': self.user_color, + 'text_color': self.text_color, + 'background_color': self.background_color } # Add font info if user has a custom font configured diff --git a/config/default_config.yaml b/config/default_config.yaml index 12f20b4..66ede0d 100644 --- a/config/default_config.yaml +++ b/config/default_config.yaml @@ -59,6 +59,10 @@ display: font_size: 12 theme: "dark" fade_after_seconds: 10 # Time before transcriptions fade out (0 = never fade) + # Color settings (used for both local display and server sync) + user_color: "#4CAF50" # User's name color (default green) + text_color: "#FFFFFF" # Text/font color (default white) + background_color: "#000000B3" # Background color with alpha (default semi-transparent black) web_server: port: 8080 diff --git a/gui/main_window_qt.py b/gui/main_window_qt.py index 60bfda7..e43701d 100644 --- a/gui/main_window_qt.py +++ b/gui/main_window_qt.py @@ -373,6 +373,11 @@ class MainWindow(QMainWindow): websafe_font = self.config.get('display.websafe_font', 'Arial') google_font = self.config.get('display.google_font', 'Roboto') + # Color settings + user_color = self.config.get('display.user_color', '#4CAF50') + text_color = self.config.get('display.text_color', '#FFFFFF') + background_color = self.config.get('display.background_color', '#000000B3') + # Try up to 5 ports if the default is in use ports_to_try = [port] + [port + i for i in range(1, 5)] server_started = False @@ -390,7 +395,10 @@ class MainWindow(QMainWindow): fonts_dir=fonts_dir, font_source=font_source, websafe_font=websafe_font, - google_font=google_font + google_font=google_font, + user_color=user_color, + text_color=text_color, + background_color=background_color ) self.web_server_thread = WebServerThread(self.web_server) self.web_server_thread.start() @@ -643,6 +651,10 @@ class MainWindow(QMainWindow): self.web_server.font_source = self.config.get('display.font_source', 'System Font') self.web_server.websafe_font = self.config.get('display.websafe_font', 'Arial') self.web_server.google_font = self.config.get('display.google_font', 'Roboto') + # Update color settings + self.web_server.user_color = self.config.get('display.user_color', '#4CAF50') + self.web_server.text_color = self.config.get('display.text_color', '#FFFFFF') + self.web_server.background_color = self.config.get('display.background_color', '#000000B3') # Update sync link visibility based on server sync settings self._update_sync_link() @@ -728,6 +740,11 @@ class MainWindow(QMainWindow): google_font = self.config.get('display.google_font', '') custom_font_file = self.config.get('display.custom_font_file', '') + # Color settings + user_color = self.config.get('display.user_color', '#4CAF50') + text_color = self.config.get('display.text_color', '#FFFFFF') + background_color = self.config.get('display.background_color', '#000000B3') + if not url: print("Server sync enabled but no URL configured") return @@ -743,7 +760,10 @@ class MainWindow(QMainWindow): font_source=font_source, websafe_font=websafe_font if websafe_font else None, google_font=google_font if google_font else None, - custom_font_file=custom_font_file if custom_font_file else None + custom_font_file=custom_font_file if custom_font_file else None, + user_color=user_color, + text_color=text_color, + background_color=background_color ) self.server_sync_client.start() diff --git a/gui/settings_dialog_qt.py b/gui/settings_dialog_qt.py index 73da05e..0b28bbc 100644 --- a/gui/settings_dialog_qt.py +++ b/gui/settings_dialog_qt.py @@ -4,10 +4,10 @@ from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QLineEdit, QComboBox, QCheckBox, QSlider, QPushButton, QMessageBox, QGroupBox, QScrollArea, QWidget, - QFileDialog + QFileDialog, QColorDialog ) from PySide6.QtCore import Qt -from PySide6.QtGui import QScreen, QFontDatabase +from PySide6.QtGui import QScreen, QFontDatabase, QColor from typing import Callable, List, Tuple @@ -388,6 +388,53 @@ class SettingsDialog(QDialog): ) display_layout.addRow("Fade After (seconds):", self.fade_seconds_input) + # Color settings + color_label = QLabel("Color Settings") + color_label.setStyleSheet("font-weight: bold; margin-top: 10px;") + display_layout.addRow("", color_label) + + # User name color picker + user_color_layout = QHBoxLayout() + self.user_color_button = QPushButton() + self.user_color_button.setFixedSize(100, 30) + self.user_color_button.setCursor(Qt.PointingHandCursor) + self.user_color_button.setToolTip("Click to change user name color") + self.user_color_button.clicked.connect(self._pick_user_color) + user_color_layout.addWidget(self.user_color_button) + self.user_color_hex = QLabel("#4CAF50") + self.user_color_hex.setStyleSheet("font-family: monospace;") + user_color_layout.addWidget(self.user_color_hex) + user_color_layout.addStretch() + display_layout.addRow("User Name Color:", user_color_layout) + + # Text color picker + text_color_layout = QHBoxLayout() + self.text_color_button = QPushButton() + self.text_color_button.setFixedSize(100, 30) + self.text_color_button.setCursor(Qt.PointingHandCursor) + self.text_color_button.setToolTip("Click to change text color") + self.text_color_button.clicked.connect(self._pick_text_color) + text_color_layout.addWidget(self.text_color_button) + self.text_color_hex = QLabel("#FFFFFF") + self.text_color_hex.setStyleSheet("font-family: monospace;") + text_color_layout.addWidget(self.text_color_hex) + text_color_layout.addStretch() + display_layout.addRow("Text Color:", text_color_layout) + + # Background color picker + bg_color_layout = QHBoxLayout() + self.bg_color_button = QPushButton() + self.bg_color_button.setFixedSize(100, 30) + self.bg_color_button.setCursor(Qt.PointingHandCursor) + self.bg_color_button.setToolTip("Click to change background color (with transparency)") + self.bg_color_button.clicked.connect(self._pick_background_color) + bg_color_layout.addWidget(self.bg_color_button) + self.bg_color_hex = QLabel("#000000B3") + self.bg_color_hex.setStyleSheet("font-family: monospace;") + bg_color_layout.addWidget(self.bg_color_hex) + bg_color_layout.addStretch() + display_layout.addRow("Background Color:", bg_color_layout) + display_group.setLayout(display_layout) content_layout.addWidget(display_group) @@ -577,6 +624,64 @@ class SettingsDialog(QDialog): if file_path: self.display_custom_font_input.setText(file_path) + def _update_color_button_style(self, button: QPushButton, color_hex: str): + """Update a color button's background to show the selected color.""" + # Handle colors with alpha (8-char hex) + if len(color_hex) == 9: # #RRGGBBAA + # For display, we just show the color (alpha is visible through the button style) + rgb = color_hex[:7] + alpha_hex = color_hex[7:9] + alpha = int(alpha_hex, 16) / 255 + button.setStyleSheet( + f"background-color: {rgb}; " + f"border: 2px solid #888; " + f"border-radius: 4px; " + f"opacity: {alpha};" + ) + else: + button.setStyleSheet( + f"background-color: {color_hex}; " + f"border: 2px solid #888; " + f"border-radius: 4px;" + ) + + def _pick_user_color(self): + """Open color dialog for user name color.""" + current_color = QColor(self.user_color_hex.text()) + color = QColorDialog.getColor(current_color, self, "Select User Name Color") + if color.isValid(): + hex_color = color.name() + self.user_color_hex.setText(hex_color) + self._update_color_button_style(self.user_color_button, hex_color) + + def _pick_text_color(self): + """Open color dialog for text color.""" + current_color = QColor(self.text_color_hex.text()) + color = QColorDialog.getColor(current_color, self, "Select Text Color") + if color.isValid(): + hex_color = color.name() + self.text_color_hex.setText(hex_color) + self._update_color_button_style(self.text_color_button, hex_color) + + def _pick_background_color(self): + """Open color dialog for background color (with alpha support).""" + current_hex = self.bg_color_hex.text() + current_color = QColor(current_hex[:7] if len(current_hex) > 7 else current_hex) + if len(current_hex) == 9: + current_color.setAlpha(int(current_hex[7:9], 16)) + + color = QColorDialog.getColor( + current_color, + self, + "Select Background Color", + QColorDialog.ShowAlphaChannel + ) + if color.isValid(): + # Include alpha in hex format: #RRGGBBAA + hex_color = f"{color.name()}{color.alpha():02X}" + self.bg_color_hex.setText(hex_color) + self._update_color_button_style(self.bg_color_button, hex_color) + def _load_current_settings(self): """Load current settings from config.""" # User settings @@ -649,6 +754,19 @@ class SettingsDialog(QDialog): self.font_size_input.setText(str(self.config.get('display.font_size', 12))) self.fade_seconds_input.setText(str(self.config.get('display.fade_after_seconds', 10))) + # Color settings + user_color = self.config.get('display.user_color', '#4CAF50') + self.user_color_hex.setText(user_color) + self._update_color_button_style(self.user_color_button, user_color) + + text_color = self.config.get('display.text_color', '#FFFFFF') + self.text_color_hex.setText(text_color) + self._update_color_button_style(self.text_color_button, text_color) + + bg_color = self.config.get('display.background_color', '#000000B3') + self.bg_color_hex.setText(bg_color) + self._update_color_button_style(self.bg_color_button, bg_color) + # Server sync settings self.server_enabled_check.setChecked(self.config.get('server_sync.enabled', False)) self.server_url_input.setText(self.config.get('server_sync.url', '')) @@ -716,6 +834,11 @@ class SettingsDialog(QDialog): fade_seconds = int(self.fade_seconds_input.text()) self.config.set('display.fade_after_seconds', fade_seconds) + # Color settings + self.config.set('display.user_color', self.user_color_hex.text()) + self.config.set('display.text_color', self.text_color_hex.text()) + self.config.set('display.background_color', self.bg_color_hex.text()) + # Server sync settings self.config.set('server_sync.enabled', self.server_enabled_check.isChecked()) self.config.set('server_sync.url', self.server_url_input.text()) diff --git a/server/nodejs/server.js b/server/nodejs/server.js index 2732c50..0b56431 100644 --- a/server/nodejs/server.js +++ b/server/nodejs/server.js @@ -374,13 +374,29 @@ app.get('/', (req, res) => { overflow-x: auto; margin-top: 10px; } + + .button { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 15px 30px; + border-radius: 10px; + cursor: pointer; + font-weight: bold; + transition: transform 0.2s, box-shadow 0.2s; + } + + .button:hover { + transform: translateY(-2px); + box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); + }

🎤 Local Transcription

-

Multi-User Server (Node.js)

+

Multi-User Transcription Server

🟢 Server Running
@@ -504,25 +520,59 @@ app.get('/', (req, res) => {
-

ℹ️ Server Information

-
-
-
Node.js
-
Runtime
-
-
-
v1.0.0
-
Version
-
-
-
<100ms
-
Latency
-
-
-
WebSocket
-
Protocol
-
-
+

📺 Display Options Reference

+

When creating a display URL for OBS, you can customize it with these URL parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionDefault
roomRoom name (required)-
fadeSeconds before text fades (0 = never)10
timestampsShow timestamps (true/false)true
maxlinesMaximum visible lines50
fontsizeFont size in pixels16
fontsourceFont source: websafe, google, or customwebsafe
websafefontWeb-safe font name (Arial, Courier New, etc.)Arial
googlefontGoogle Font name (Roboto, Open Sans, etc.)Roboto
+

+ Note: Per-user colors and fonts set in the desktop app will override these defaults. + Each user can customize their name color, text color, and background color in Settings. +

@@ -602,7 +652,8 @@ app.get('/', (req, res) => { app.post('/api/send', async (req, res) => { const requestStart = Date.now(); try { - const { room, passphrase, user_name, text, timestamp, is_preview, font_family, font_type } = req.body; + const { room, passphrase, user_name, text, timestamp, is_preview, font_family, font_type, + user_color, text_color, background_color } = req.body; if (!room || !passphrase || !user_name || !text) { return res.status(400).json({ error: 'Missing required fields' }); @@ -624,7 +675,11 @@ app.post('/api/send', async (req, res) => { created_at: Date.now(), is_preview: is_preview || false, font_family: font_family || null, // Per-speaker font name - font_type: font_type || null // Font type: "websafe", "google", or "custom" + font_type: font_type || null, // Font type: "websafe", "google", or "custom" + // Per-user color settings + user_color: user_color || null, // User name color (e.g., "#4CAF50") + text_color: text_color || null, // Text color (e.g., "#FFFFFF") + background_color: background_color || null // Background color (e.g., "#000000B3") }; const addStart = Date.now(); @@ -931,15 +986,36 @@ app.get('/display', (req, res) => { return userColors.get(userName); } + // Helper to convert hex color with alpha to rgba + function hexToRgba(hex) { + if (!hex) return null; + hex = hex.replace('#', ''); + if (hex.length === 8) { // RRGGBBAA + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + const a = parseInt(hex.substring(6, 8), 16) / 255; + return \`rgba(\${r}, \${g}, \${b}, \${a.toFixed(2)})\`; + } else if (hex.length === 6) { // RRGGBB + return '#' + hex; + } + return '#' + hex; + } + function addTranscription(data) { const isPreview = data.is_preview || false; const userName = data.user_name || ''; const fontFamily = data.font_family || null; // Per-speaker font name const fontType = data.font_type || null; // "websafe", "google", or "custom" + // Per-user color settings + const userColorSetting = data.user_color || null; // User name color + const textColorSetting = data.text_color || null; // Text color + const bgColorSetting = data.background_color || null; // Background color - // Debug: Log received font info - if (fontFamily) { - console.log('Received transcription with font:', fontFamily, '(' + fontType + ')'); + // Debug: Log received font/color info + if (fontFamily || userColorSetting) { + console.log('Received transcription with font:', fontFamily, '(' + fontType + ')', + 'colors:', userColorSetting, textColorSetting, bgColorSetting); } // Load Google Font if needed @@ -950,6 +1026,12 @@ app.get('/display', (req, res) => { // Build font style string if font is set // Use single quotes for font name to avoid conflict with style="" double quotes const fontStyle = fontFamily ? \`font-family: '\${fontFamily}', sans-serif;\` : ''; + // Build text color style + const textStyle = textColorSetting ? \`color: \${textColorSetting};\` : ''; + // Combine font and text color for text span + const combinedTextStyle = fontStyle + textStyle; + // Build background style for transcription div + const bgStyle = bgColorSetting ? \`background: \${hexToRgba(bgColorSetting)};\` : ''; // If this is a final transcription, remove any existing preview from this user if (!isPreview && userPreviews.has(userName)) { @@ -960,12 +1042,14 @@ app.get('/display', (req, res) => { userPreviews.delete(userName); } + // Determine user name color: use custom color if provided, otherwise auto-generated + const userColor = userColorSetting || getUserColor(userName); + // If this is a preview, update existing preview or create new one if (isPreview && userPreviews.has(userName)) { const previewEl = userPreviews.get(userName); if (previewEl && previewEl.parentNode) { // Update existing preview - const userColor = getUserColor(userName); let html = ''; if (showTimestamps && data.timestamp) { html += \`[\${data.timestamp}]\`; @@ -974,16 +1058,23 @@ app.get('/display', (req, res) => { html += \`\${userName}:\`; } html += \`[...]\`; - html += \`\${data.text}\`; + html += \`\${data.text}\`; previewEl.innerHTML = html; + // Update background color if provided + if (bgStyle) { + previewEl.style.background = hexToRgba(bgColorSetting); + } return; } } const div = document.createElement('div'); div.className = isPreview ? 'transcription preview' : 'transcription'; + // Apply per-user background color if provided + if (bgStyle) { + div.style.background = hexToRgba(bgColorSetting); + } - const userColor = getUserColor(userName); let html = ''; if (showTimestamps && data.timestamp) { html += \`[\${data.timestamp}]\`; @@ -994,7 +1085,7 @@ app.get('/display', (req, res) => { if (isPreview) { html += \`[...]\`; } - html += \`\${data.text}\`; + html += \`\${data.text}\`; div.innerHTML = html; container.appendChild(div); diff --git a/server/web_display.py b/server/web_display.py index 51e846d..a0002c5 100644 --- a/server/web_display.py +++ b/server/web_display.py @@ -16,7 +16,9 @@ class TranscriptionWebServer: fade_after_seconds: int = 10, max_lines: int = 50, font_family: str = "Arial", font_size: int = 16, fonts_dir: Optional[Path] = None, font_source: str = "System Font", websafe_font: str = "Arial", - google_font: str = "Roboto"): + google_font: str = "Roboto", + user_color: str = "#4CAF50", text_color: str = "#FFFFFF", + background_color: str = "#000000B3"): """ Initialize web server. @@ -32,6 +34,9 @@ class TranscriptionWebServer: font_source: Font source type ("System Font", "Web-Safe", "Google Font") websafe_font: Web-safe font name google_font: Google Font name + user_color: User name color (hex format) + text_color: Text color (hex format) + background_color: Background color (hex format with optional alpha, e.g., #RRGGBBAA) """ self.host = host self.port = port @@ -44,6 +49,9 @@ class TranscriptionWebServer: self.font_source = font_source self.websafe_font = websafe_font self.google_font = google_font + self.user_color = user_color + self.text_color = text_color + self.background_color = background_color self.app = FastAPI() self.active_connections: List[WebSocket] = [] self.transcriptions = [] # Store recent transcriptions @@ -138,6 +146,25 @@ class TranscriptionWebServer: return f'' return "" + def _hex_to_rgba(self, hex_color: str) -> str: + """Convert hex color (optionally with alpha) to CSS rgba() format.""" + # Remove # if present + hex_color = hex_color.lstrip('#') + + if len(hex_color) == 8: # RRGGBBAA + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + a = int(hex_color[6:8], 16) / 255 + return f"rgba({r}, {g}, {b}, {a:.2f})" + elif len(hex_color) == 6: # RRGGBB + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + return f"rgb({r}, {g}, {b})" + else: + return hex_color # Return as-is if format is unknown + def _get_html(self) -> str: """Generate HTML for transcription display.""" # Generate custom font CSS @@ -145,6 +172,9 @@ class TranscriptionWebServer: google_font_link = self._get_google_font_link() effective_font = self._get_effective_font() + # Convert background color to rgba for CSS + bg_color_css = self._hex_to_rgba(self.background_color) + return f""" @@ -168,7 +198,7 @@ class TranscriptionWebServer: .transcription {{ margin: 10px 0; padding: 10px; - background: rgba(0, 0, 0, 0.7); + background: {bg_color_css}; border-radius: 5px; animation: slideIn 0.3s ease-out; transition: opacity 1s ease-out; @@ -182,12 +212,12 @@ class TranscriptionWebServer: margin-right: 10px; }} .user {{ - color: #4CAF50; + color: {self.user_color}; font-weight: bold; margin-right: 10px; }} .text {{ - color: white; + color: {self.text_color}; }} .transcription.preview {{ font-style: italic; diff --git a/version.py b/version.py index c68045e..4a95f51 100644 --- a/version.py +++ b/version.py @@ -1,7 +1,7 @@ """Version information for Local Transcription.""" -__version__ = "1.2.4" -__version_info__ = (1, 2, 4) +__version__ = "1.3.1" +__version_info__ = (1, 3, 1) # Version history: # 1.0.0 - Initial release with: