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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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"><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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user