Add unified per-speaker font support and remote transcription service

Font changes:
- Consolidate font settings into single Display Settings section
- Support Web-Safe, Google Fonts, and Custom File uploads for both displays
- Fix Google Fonts URL encoding (use + instead of %2B for spaces)
- Fix per-speaker font inline style quote escaping in Node.js display
- Add font debug logging to help diagnose font issues
- Update web server to sync all font settings on settings change
- Remove deprecated PHP server documentation files

New features:
- Add remote transcription service for GPU offloading
- Add instance lock to prevent multiple app instances
- Add version tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 18:56:12 -08:00
parent f035bdb927
commit ff067b3368
23 changed files with 2486 additions and 1160 deletions

View File

@@ -1,7 +1,7 @@
"""PySide6 transcription display widget for showing real-time transcriptions."""
from PySide6.QtWidgets import QTextEdit
from PySide6.QtGui import QFont, QTextCursor
from PySide6.QtGui import QFont, QTextCursor, QTextCharFormat, QColor
from PySide6.QtCore import Qt, Slot
from datetime import datetime
@@ -28,6 +28,10 @@ class TranscriptionDisplay(QTextEdit):
self.font_family = font_family
self.font_size = font_size
# Track the current preview line for two-stage transcription
self.preview_line_index = -1 # -1 means no active preview
self.preview_block_number = -1 # Block number for the preview line
# Configure text widget
self.setReadOnly(True)
self.setFont(QFont(font_family, font_size))
@@ -43,6 +47,36 @@ class TranscriptionDisplay(QTextEdit):
}
""")
def _format_line(self, text: str, user_name: str, timestamp: datetime, is_preview: bool = False) -> str:
"""
Format a transcription line.
Args:
text: Transcription text
user_name: User/speaker name
timestamp: Timestamp of transcription
is_preview: Whether this is a preview line
Returns:
Formatted line string
"""
line_parts = []
if self.show_timestamps:
time_str = timestamp.strftime("%H:%M:%S")
line_parts.append(f"[{time_str}]")
if user_name and user_name.strip():
line_parts.append(f"{user_name}:")
# Add preview indicator for visual distinction
if is_preview:
line_parts.append(f"[...] {text}")
else:
line_parts.append(text)
return " ".join(line_parts)
@Slot(str, str)
def add_transcription(self, text: str, user_name: str = "", timestamp: datetime = None):
"""
@@ -56,35 +90,130 @@ class TranscriptionDisplay(QTextEdit):
if timestamp is None:
timestamp = datetime.now()
# Build the display line
line_parts = []
line = self._format_line(text, user_name, timestamp, is_preview=False)
if self.show_timestamps:
time_str = timestamp.strftime("%H:%M:%S")
line_parts.append(f"[{time_str}]")
if user_name:
line_parts.append(f"{user_name}:")
line_parts.append(text)
line = " ".join(line_parts)
# Add to display
self.append(line)
# If there's an active preview, replace it instead of appending
if self.preview_line_index >= 0:
self._replace_preview_with_final(line)
else:
# Add to display normally
self.append(line)
self.line_count += 1
# Auto-scroll to bottom
cursor = self.textCursor()
cursor.movePosition(QTextCursor.End)
self.setTextCursor(cursor)
# Track line count
self.line_count += 1
# Remove old lines if exceeding max
if self.line_count > self.max_lines:
self._remove_oldest_lines(self.line_count - self.max_lines)
@Slot(str, str)
def add_preview(self, text: str, user_name: str = "", timestamp: datetime = None):
"""
Add a preview transcription that will be replaced by the final transcription.
Args:
text: Preview transcription text
user_name: User/speaker name
timestamp: Timestamp of transcription
"""
if timestamp is None:
timestamp = datetime.now()
line = self._format_line(text, user_name, timestamp, is_preview=True)
# If there's already a preview, replace it
if self.preview_line_index >= 0:
self._replace_preview_line(line)
else:
# Add new preview line
cursor = self.textCursor()
cursor.movePosition(QTextCursor.End)
# Apply italic formatting for preview
fmt = QTextCharFormat()
fmt.setFontItalic(True)
if self.line_count > 0:
cursor.insertText("\n")
cursor.insertText(line, fmt)
self.preview_line_index = self.line_count
self.preview_block_number = self.document().blockCount() - 1
self.line_count += 1
# Auto-scroll to bottom
cursor = self.textCursor()
cursor.movePosition(QTextCursor.End)
self.setTextCursor(cursor)
def _replace_preview_line(self, new_text: str):
"""Replace the current preview line with new preview text."""
if self.preview_block_number < 0:
return
doc = self.document()
block = doc.findBlockByNumber(self.preview_block_number)
if block.isValid():
cursor = QTextCursor(block)
cursor.select(QTextCursor.BlockUnderCursor)
# Apply italic formatting for preview
fmt = QTextCharFormat()
fmt.setFontItalic(True)
cursor.removeSelectedText()
cursor.insertText(new_text, fmt)
def _replace_preview_with_final(self, final_text: str):
"""Replace the preview line with final transcription."""
if self.preview_block_number < 0:
# No preview to replace, just add normally
self.append(final_text)
self.line_count += 1
self.preview_line_index = -1
self.preview_block_number = -1
return
doc = self.document()
block = doc.findBlockByNumber(self.preview_block_number)
if block.isValid():
cursor = QTextCursor(block)
cursor.select(QTextCursor.BlockUnderCursor)
# Apply normal formatting for final text
fmt = QTextCharFormat()
fmt.setFontItalic(False)
fmt.setForeground(QColor(255, 255, 255)) # White for final
cursor.removeSelectedText()
cursor.insertText(final_text, fmt)
# Clear preview tracking
self.preview_line_index = -1
self.preview_block_number = -1
def clear_preview(self):
"""Clear the current preview without adding a final transcription."""
if self.preview_block_number >= 0:
doc = self.document()
block = doc.findBlockByNumber(self.preview_block_number)
if block.isValid():
cursor = QTextCursor(block)
cursor.select(QTextCursor.BlockUnderCursor)
cursor.removeSelectedText()
cursor.deleteChar() # Remove newline
self.line_count -= 1
self.preview_line_index = -1
self.preview_block_number = -1
def _remove_oldest_lines(self, num_lines: int):
"""
Remove oldest lines from the display.
@@ -102,10 +231,20 @@ class TranscriptionDisplay(QTextEdit):
self.line_count -= num_lines
# Adjust preview tracking if lines were removed
if self.preview_line_index >= 0:
self.preview_line_index -= num_lines
self.preview_block_number -= num_lines
if self.preview_line_index < 0:
self.preview_line_index = -1
self.preview_block_number = -1
def clear_all(self):
"""Clear all transcriptions."""
self.clear()
self.line_count = 0
self.preview_line_index = -1
self.preview_block_number = -1
def get_all_text(self) -> str:
"""