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:
@@ -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);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎤 Local Transcription</h1>
|
||||
<p>Multi-User Server (Node.js)</p>
|
||||
<p>Multi-User Transcription Server</p>
|
||||
<div class="status">🟢 Server Running</div>
|
||||
</div>
|
||||
|
||||
@@ -504,25 +520,59 @@ app.get('/', (req, res) => {
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>ℹ️ Server Information</h2>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-value">Node.js</div>
|
||||
<div class="stat-label">Runtime</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">v1.0.0</div>
|
||||
<div class="stat-label">Version</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value"><100ms</div>
|
||||
<div class="stat-label">Latency</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">WebSocket</div>
|
||||
<div class="stat-label">Protocol</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>📺 Display Options Reference</h2>
|
||||
<p>When creating a display URL for OBS, you can customize it with these URL parameters:</p>
|
||||
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
|
||||
<tr style="background: #f5f5f5;">
|
||||
<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Parameter</th>
|
||||
<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Description</th>
|
||||
<th style="text-align: left; padding: 10px; border-bottom: 2px solid #ddd;">Default</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;"><code>room</code></td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;">Room name (required)</td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;"><code>fade</code></td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;">Seconds before text fades (0 = never)</td>
|
||||
<td style="padding: 10px; border-bottom: 1px solid #eee;">10</td>
|
||||
</tr>
|
||||
<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>
|
||||
|
||||
@@ -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 += \`<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="preview-indicator">[...]</span>\`;
|
||||
html += \`<span class="text" style="\${fontStyle}">\${data.text}</span>\`;
|
||||
html += \`<span class="text" style="\${combinedTextStyle}">\${data.text}</span>\`;
|
||||
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 += \`<span class="timestamp">[\${data.timestamp}]</span>\`;
|
||||
@@ -994,7 +1085,7 @@ app.get('/display', (req, res) => {
|
||||
if (isPreview) {
|
||||
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;
|
||||
container.appendChild(div);
|
||||
|
||||
Reference in New Issue
Block a user