Add multi-user server sync (PHP server + client)

Phase 2 implementation: Multiple streamers can now merge their captions
into a single stream using a PHP server.

PHP Server (server/php/):
- server.php: API endpoint for sending/streaming transcriptions
- display.php: Web page for viewing merged captions in OBS
- config.php: Server configuration
- .htaccess: Security settings
- README.md: Comprehensive deployment guide

Features:
- Room-based isolation (multiple groups on same server)
- Passphrase authentication per room
- Real-time streaming via Server-Sent Events (SSE)
- Different colors for each user
- File-based storage (no database required)
- Auto-cleanup of old rooms
- Works on standard PHP hosting

Client-Side:
- client/server_sync.py: HTTP client for sending to PHP server
- Settings dialog updated with server sync options
- Config updated with server_sync section

Server Configuration:
- URL: Server endpoint (e.g., http://example.com/transcription/server.php)
- Room: Unique room name for your group
- Passphrase: Shared secret for authentication

OBS Integration:
Display URL format:
http://example.com/transcription/display.php?room=ROOM&passphrase=PASS&fade=10&timestamps=true

NOTE: Main window integration pending (client sends transcriptions)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-26 10:09:12 -08:00
parent aa8c294fdc
commit 9c3a0d7678
8 changed files with 982 additions and 2 deletions

163
client/server_sync.py Normal file
View File

@@ -0,0 +1,163 @@
"""Server sync client for multi-user transcription."""
import requests
import json
from typing import Optional
from datetime import datetime
import threading
import queue
class ServerSyncClient:
"""Client for syncing transcriptions to a PHP server."""
def __init__(self, url: str, room: str, passphrase: str, user_name: str):
"""
Initialize server sync client.
Args:
url: Server URL (e.g., http://example.com/transcription/server.php)
room: Room name
passphrase: Room passphrase
user_name: User's display name
"""
self.url = url
self.room = room
self.passphrase = passphrase
self.user_name = user_name
# Queue for sending transcriptions asynchronously
self.send_queue = queue.Queue()
self.is_running = False
self.send_thread: Optional[threading.Thread] = None
# Statistics
self.sent_count = 0
self.error_count = 0
self.last_error: Optional[str] = None
def start(self):
"""Start the sync client."""
if self.is_running:
return
self.is_running = True
self.send_thread = threading.Thread(target=self._send_loop, daemon=True)
self.send_thread.start()
print(f"Server sync started: room={self.room}")
def stop(self):
"""Stop the sync client."""
self.is_running = False
if self.send_thread:
self.send_thread.join(timeout=2.0)
print("Server sync stopped")
def send_transcription(self, text: str, timestamp: Optional[datetime] = None):
"""
Send a transcription to the server (non-blocking).
Args:
text: Transcription text
timestamp: Timestamp (defaults to now)
"""
if timestamp is None:
timestamp = datetime.now()
# Add to queue
self.send_queue.put({
'text': text,
'timestamp': timestamp.strftime("%H:%M:%S")
})
def _send_loop(self):
"""Background thread for sending transcriptions."""
while self.is_running:
try:
# Get transcription from queue (with timeout)
try:
trans_data = self.send_queue.get(timeout=1.0)
except queue.Empty:
continue
# Send to server
self._send_to_server(trans_data)
except Exception as e:
print(f"Error in server sync send loop: {e}")
self.error_count += 1
self.last_error = str(e)
def _send_to_server(self, trans_data: dict):
"""
Send a transcription to the PHP server.
Args:
trans_data: Dictionary with 'text' and 'timestamp'
"""
try:
# Prepare payload
payload = {
'room': self.room,
'passphrase': self.passphrase,
'user_name': self.user_name,
'text': trans_data['text'],
'timestamp': trans_data['timestamp']
}
# Send POST request
response = requests.post(
self.url,
params={'action': 'send'},
json=payload,
timeout=5.0
)
# Check response
if response.status_code == 200:
self.sent_count += 1
self.last_error = None
else:
error_msg = f"Server returned {response.status_code}"
try:
error_data = response.json()
if 'error' in error_data:
error_msg = error_data['error']
except:
pass
print(f"Server sync error: {error_msg}")
self.error_count += 1
self.last_error = error_msg
except requests.exceptions.Timeout:
print("Server sync timeout")
self.error_count += 1
self.last_error = "Request timeout"
except requests.exceptions.ConnectionError:
print("Server sync connection error")
self.error_count += 1
self.last_error = "Connection error"
except Exception as e:
print(f"Server sync error: {e}")
self.error_count += 1
self.last_error = str(e)
def get_stats(self) -> dict:
"""Get sync statistics."""
return {
'sent': self.sent_count,
'errors': self.error_count,
'last_error': self.last_error,
'queue_size': self.send_queue.qsize()
}
def is_healthy(self) -> bool:
"""Check if sync is working (no recent errors)."""
# Consider healthy if less than 10% error rate
total = self.sent_count + self.error_count
if total == 0:
return True
return (self.error_count / total) < 0.1