Add user-configurable colors for transcription display

- Add color settings (user_color, text_color, background_color) to config
- Add color picker buttons in Settings dialog with alpha support for backgrounds
- Update local web display to use configurable colors
- Send per-user colors with transcriptions to multi-user server
- Update Node.js server to apply per-user colors on display page
- Improve server landing page: replace tech details with display options reference
- Bump version to 1.3.2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 20:59:13 -08:00
parent ff067b3368
commit 89819f5d1b
7 changed files with 322 additions and 41 deletions

View File

@@ -19,7 +19,10 @@ class ServerSyncClient:
font_source: str = "None", font_source: str = "None",
websafe_font: Optional[str] = None, websafe_font: Optional[str] = None,
google_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. Initialize server sync client.
@@ -33,6 +36,9 @@ class ServerSyncClient:
websafe_font: Web-safe font name (e.g., "Arial", "Times New Roman") websafe_font: Web-safe font name (e.g., "Arial", "Times New Roman")
google_font: Google Font name (e.g., "Roboto", "Open Sans") google_font: Google Font name (e.g., "Roboto", "Open Sans")
custom_font_file: Path to a custom font file for this speaker 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.url = url
self.room = room self.room = room
@@ -43,6 +49,9 @@ class ServerSyncClient:
self.websafe_font = websafe_font self.websafe_font = websafe_font
self.google_font = google_font self.google_font = google_font
self.custom_font_file = custom_font_file 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 # Font info to send with transcriptions
self.font_family: Optional[str] = None self.font_family: Optional[str] = None
@@ -303,7 +312,11 @@ class ServerSyncClient:
'user_name': self.user_name, 'user_name': self.user_name,
'text': trans_data['text'], 'text': trans_data['text'],
'timestamp': trans_data['timestamp'], '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 # Add font info if user has a custom font configured

View File

@@ -59,6 +59,10 @@ display:
font_size: 12 font_size: 12
theme: "dark" theme: "dark"
fade_after_seconds: 10 # Time before transcriptions fade out (0 = never fade) 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: web_server:
port: 8080 port: 8080

View File

@@ -373,6 +373,11 @@ class MainWindow(QMainWindow):
websafe_font = self.config.get('display.websafe_font', 'Arial') websafe_font = self.config.get('display.websafe_font', 'Arial')
google_font = self.config.get('display.google_font', 'Roboto') 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 # Try up to 5 ports if the default is in use
ports_to_try = [port] + [port + i for i in range(1, 5)] ports_to_try = [port] + [port + i for i in range(1, 5)]
server_started = False server_started = False
@@ -390,7 +395,10 @@ class MainWindow(QMainWindow):
fonts_dir=fonts_dir, fonts_dir=fonts_dir,
font_source=font_source, font_source=font_source,
websafe_font=websafe_font, 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 = WebServerThread(self.web_server)
self.web_server_thread.start() 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.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.websafe_font = self.config.get('display.websafe_font', 'Arial')
self.web_server.google_font = self.config.get('display.google_font', 'Roboto') 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 # Update sync link visibility based on server sync settings
self._update_sync_link() self._update_sync_link()
@@ -728,6 +740,11 @@ class MainWindow(QMainWindow):
google_font = self.config.get('display.google_font', '') google_font = self.config.get('display.google_font', '')
custom_font_file = self.config.get('display.custom_font_file', '') 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: if not url:
print("Server sync enabled but no URL configured") print("Server sync enabled but no URL configured")
return return
@@ -743,7 +760,10 @@ class MainWindow(QMainWindow):
font_source=font_source, font_source=font_source,
websafe_font=websafe_font if websafe_font else None, websafe_font=websafe_font if websafe_font else None,
google_font=google_font if google_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() self.server_sync_client.start()

View File

@@ -4,10 +4,10 @@ from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
QLabel, QLineEdit, QComboBox, QCheckBox, QSlider, QLabel, QLineEdit, QComboBox, QCheckBox, QSlider,
QPushButton, QMessageBox, QGroupBox, QScrollArea, QWidget, QPushButton, QMessageBox, QGroupBox, QScrollArea, QWidget,
QFileDialog QFileDialog, QColorDialog
) )
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtGui import QScreen, QFontDatabase from PySide6.QtGui import QScreen, QFontDatabase, QColor
from typing import Callable, List, Tuple from typing import Callable, List, Tuple
@@ -388,6 +388,53 @@ class SettingsDialog(QDialog):
) )
display_layout.addRow("Fade After (seconds):", self.fade_seconds_input) 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) display_group.setLayout(display_layout)
content_layout.addWidget(display_group) content_layout.addWidget(display_group)
@@ -577,6 +624,64 @@ class SettingsDialog(QDialog):
if file_path: if file_path:
self.display_custom_font_input.setText(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): def _load_current_settings(self):
"""Load current settings from config.""" """Load current settings from config."""
# User settings # User settings
@@ -649,6 +754,19 @@ class SettingsDialog(QDialog):
self.font_size_input.setText(str(self.config.get('display.font_size', 12))) 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))) 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 # Server sync settings
self.server_enabled_check.setChecked(self.config.get('server_sync.enabled', False)) self.server_enabled_check.setChecked(self.config.get('server_sync.enabled', False))
self.server_url_input.setText(self.config.get('server_sync.url', '')) 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()) fade_seconds = int(self.fade_seconds_input.text())
self.config.set('display.fade_after_seconds', fade_seconds) 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 # Server sync settings
self.config.set('server_sync.enabled', self.server_enabled_check.isChecked()) self.config.set('server_sync.enabled', self.server_enabled_check.isChecked())
self.config.set('server_sync.url', self.server_url_input.text()) self.config.set('server_sync.url', self.server_url_input.text())

View File

@@ -374,13 +374,29 @@ app.get('/', (req, res) => {
overflow-x: auto; overflow-x: auto;
margin-top: 10px; 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);
}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>🎤 Local Transcription</h1> <h1>🎤 Local Transcription</h1>
<p>Multi-User Server (Node.js)</p> <p>Multi-User Transcription Server</p>
<div class="status">🟢 Server Running</div> <div class="status">🟢 Server Running</div>
</div> </div>
@@ -504,25 +520,59 @@ app.get('/', (req, res) => {
</div> </div>
<div class="card"> <div class="card">
<h2> Server Information</h2> <h2>📺 Display Options Reference</h2>
<div class="stats"> <p>When creating a display URL for OBS, you can customize it with these URL parameters:</p>
<div class="stat"> <table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
<div class="stat-value">Node.js</div> <tr style="background: #f5f5f5;">
<div class="stat-label">Runtime</div> <th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Parameter</th>
</div> <th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Description</th>
<div class="stat"> <th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Default</th>
<div class="stat-value">v1.0.0</div> </tr>
<div class="stat-label">Version</div> <tr>
</div> <td style="padding: 10px; border-bottom: 1px solid #eee;"><code>room</code></td>
<div class="stat"> <td style="padding: 10px; border-bottom: 1px solid #eee;">Room name (required)</td>
<div class="stat-value">&lt;100ms</div> <td style="padding: 10px; border-bottom: 1px solid #eee;">-</td>
<div class="stat-label">Latency</div> </tr>
</div> <tr>
<div class="stat"> <td style="padding: 10px; border-bottom: 1px solid #eee;"><code>fade</code></td>
<div class="stat-value">WebSocket</div> <td style="padding: 10px; border-bottom: 1px solid #eee;">Seconds before text fades (0 = never)</td>
<div class="stat-label">Protocol</div> <td style="padding: 10px; border-bottom: 1px solid #eee;">10</td>
</div> </tr>
</div> <tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;"><code>timestamps</code></td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Show timestamps (true/false)</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">true</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;"><code>maxlines</code></td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Maximum visible lines</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">50</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;"><code>fontsize</code></td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Font size in pixels</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">16</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;"><code>fontsource</code></td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Font source: websafe, google, or custom</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">websafe</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;"><code>websafefont</code></td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Web-safe font name (Arial, Courier New, etc.)</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Arial</td>
</tr>
<tr>
<td style="padding: 10px; border-bottom: 1px solid #eee;"><code>googlefont</code></td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Google Font name (Roboto, Open Sans, etc.)</td>
<td style="padding: 10px; border-bottom: 1px solid #eee;">Roboto</td>
</tr>
</table>
<p style="margin-top: 15px; font-size: 0.9em; color: #666;">
<strong>Note:</strong> 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.
</p>
</div> </div>
</div> </div>
@@ -602,7 +652,8 @@ app.get('/', (req, res) => {
app.post('/api/send', async (req, res) => { app.post('/api/send', async (req, res) => {
const requestStart = Date.now(); const requestStart = Date.now();
try { 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) { if (!room || !passphrase || !user_name || !text) {
return res.status(400).json({ error: 'Missing required fields' }); return res.status(400).json({ error: 'Missing required fields' });
@@ -624,7 +675,11 @@ app.post('/api/send', async (req, res) => {
created_at: Date.now(), created_at: Date.now(),
is_preview: is_preview || false, is_preview: is_preview || false,
font_family: font_family || null, // Per-speaker font name 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(); const addStart = Date.now();
@@ -931,15 +986,36 @@ app.get('/display', (req, res) => {
return userColors.get(userName); 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) { function addTranscription(data) {
const isPreview = data.is_preview || false; const isPreview = data.is_preview || false;
const userName = data.user_name || ''; const userName = data.user_name || '';
const fontFamily = data.font_family || null; // Per-speaker font name const fontFamily = data.font_family || null; // Per-speaker font name
const fontType = data.font_type || null; // "websafe", "google", or "custom" 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 // Debug: Log received font/color info
if (fontFamily) { if (fontFamily || userColorSetting) {
console.log('Received transcription with font:', fontFamily, '(' + fontType + ')'); console.log('Received transcription with font:', fontFamily, '(' + fontType + ')',
'colors:', userColorSetting, textColorSetting, bgColorSetting);
} }
// Load Google Font if needed // Load Google Font if needed
@@ -950,6 +1026,12 @@ app.get('/display', (req, res) => {
// Build font style string if font is set // Build font style string if font is set
// Use single quotes for font name to avoid conflict with style="" double quotes // Use single quotes for font name to avoid conflict with style="" double quotes
const fontStyle = fontFamily ? \`font-family: '\${fontFamily}', sans-serif;\` : ''; 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 this is a final transcription, remove any existing preview from this user
if (!isPreview && userPreviews.has(userName)) { if (!isPreview && userPreviews.has(userName)) {
@@ -960,12 +1042,14 @@ app.get('/display', (req, res) => {
userPreviews.delete(userName); 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 this is a preview, update existing preview or create new one
if (isPreview && userPreviews.has(userName)) { if (isPreview && userPreviews.has(userName)) {
const previewEl = userPreviews.get(userName); const previewEl = userPreviews.get(userName);
if (previewEl && previewEl.parentNode) { if (previewEl && previewEl.parentNode) {
// Update existing preview // Update existing preview
const userColor = getUserColor(userName);
let html = ''; let html = '';
if (showTimestamps && data.timestamp) { if (showTimestamps && data.timestamp) {
html += \`<span class="timestamp">[\${data.timestamp}]</span>\`; html += \`<span class="timestamp">[\${data.timestamp}]</span>\`;
@@ -974,16 +1058,23 @@ app.get('/display', (req, res) => {
html += \`<span class="user" style="color: \${userColor}">\${userName}:</span>\`; html += \`<span class="user" style="color: \${userColor}">\${userName}:</span>\`;
} }
html += \`<span class="preview-indicator">[...]</span>\`; html += \`<span class="preview-indicator">[...]</span>\`;
html += \`<span class="text" style="\${fontStyle}">\${data.text}</span>\`; html += \`<span class="text" style="\${combinedTextStyle}">\${data.text}</span>\`;
previewEl.innerHTML = html; previewEl.innerHTML = html;
// Update background color if provided
if (bgStyle) {
previewEl.style.background = hexToRgba(bgColorSetting);
}
return; return;
} }
} }
const div = document.createElement('div'); const div = document.createElement('div');
div.className = isPreview ? 'transcription preview' : 'transcription'; 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 = ''; let html = '';
if (showTimestamps && data.timestamp) { if (showTimestamps && data.timestamp) {
html += \`<span class="timestamp">[\${data.timestamp}]</span>\`; html += \`<span class="timestamp">[\${data.timestamp}]</span>\`;
@@ -994,7 +1085,7 @@ app.get('/display', (req, res) => {
if (isPreview) { if (isPreview) {
html += \`<span class="preview-indicator">[...]</span>\`; html += \`<span class="preview-indicator">[...]</span>\`;
} }
html += \`<span class="text" style="\${fontStyle}">\${data.text}</span>\`; html += \`<span class="text" style="\${combinedTextStyle}">\${data.text}</span>\`;
div.innerHTML = html; div.innerHTML = html;
container.appendChild(div); container.appendChild(div);

View File

@@ -16,7 +16,9 @@ class TranscriptionWebServer:
fade_after_seconds: int = 10, max_lines: int = 50, font_family: str = "Arial", fade_after_seconds: int = 10, max_lines: int = 50, font_family: str = "Arial",
font_size: int = 16, fonts_dir: Optional[Path] = None, font_size: int = 16, fonts_dir: Optional[Path] = None,
font_source: str = "System Font", websafe_font: str = "Arial", 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. Initialize web server.
@@ -32,6 +34,9 @@ class TranscriptionWebServer:
font_source: Font source type ("System Font", "Web-Safe", "Google Font") font_source: Font source type ("System Font", "Web-Safe", "Google Font")
websafe_font: Web-safe font name websafe_font: Web-safe font name
google_font: Google 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.host = host
self.port = port self.port = port
@@ -44,6 +49,9 @@ class TranscriptionWebServer:
self.font_source = font_source self.font_source = font_source
self.websafe_font = websafe_font self.websafe_font = websafe_font
self.google_font = google_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.app = FastAPI()
self.active_connections: List[WebSocket] = [] self.active_connections: List[WebSocket] = []
self.transcriptions = [] # Store recent transcriptions self.transcriptions = [] # Store recent transcriptions
@@ -138,6 +146,25 @@ class TranscriptionWebServer:
return f'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family={font_name}&display=swap">' return f'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family={font_name}&display=swap">'
return "" 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: def _get_html(self) -> str:
"""Generate HTML for transcription display.""" """Generate HTML for transcription display."""
# Generate custom font CSS # Generate custom font CSS
@@ -145,6 +172,9 @@ class TranscriptionWebServer:
google_font_link = self._get_google_font_link() google_font_link = self._get_google_font_link()
effective_font = self._get_effective_font() 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""" return f"""
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@@ -168,7 +198,7 @@ class TranscriptionWebServer:
.transcription {{ .transcription {{
margin: 10px 0; margin: 10px 0;
padding: 10px; padding: 10px;
background: rgba(0, 0, 0, 0.7); background: {bg_color_css};
border-radius: 5px; border-radius: 5px;
animation: slideIn 0.3s ease-out; animation: slideIn 0.3s ease-out;
transition: opacity 1s ease-out; transition: opacity 1s ease-out;
@@ -182,12 +212,12 @@ class TranscriptionWebServer:
margin-right: 10px; margin-right: 10px;
}} }}
.user {{ .user {{
color: #4CAF50; color: {self.user_color};
font-weight: bold; font-weight: bold;
margin-right: 10px; margin-right: 10px;
}} }}
.text {{ .text {{
color: white; color: {self.text_color};
}} }}
.transcription.preview {{ .transcription.preview {{
font-style: italic; font-style: italic;

View File

@@ -1,7 +1,7 @@
"""Version information for Local Transcription.""" """Version information for Local Transcription."""
__version__ = "1.2.4" __version__ = "1.3.1"
__version_info__ = (1, 2, 4) __version_info__ = (1, 3, 1)
# Version history: # Version history:
# 1.0.0 - Initial release with: # 1.0.0 - Initial release with: