2025-12-26 10:09:12 -08:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Multi-User Transcription Server - API Endpoint
|
|
|
|
|
*
|
|
|
|
|
* Endpoints:
|
|
|
|
|
* - POST /server.php?action=send - Send a transcription
|
|
|
|
|
* - GET /server.php?action=stream - Stream transcriptions via SSE
|
|
|
|
|
* - GET /server.php?action=list - List recent transcriptions
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
require_once 'config.php';
|
|
|
|
|
|
|
|
|
|
// Get action
|
|
|
|
|
$action = $_GET['action'] ?? 'info';
|
|
|
|
|
|
|
|
|
|
// Route to appropriate handler
|
|
|
|
|
switch ($action) {
|
|
|
|
|
case 'send':
|
|
|
|
|
handleSend();
|
|
|
|
|
break;
|
|
|
|
|
case 'stream':
|
|
|
|
|
handleStream();
|
|
|
|
|
break;
|
|
|
|
|
case 'list':
|
|
|
|
|
handleList();
|
|
|
|
|
break;
|
|
|
|
|
case 'info':
|
|
|
|
|
handleInfo();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
sendError('Invalid action', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle sending a transcription
|
|
|
|
|
*/
|
|
|
|
|
function handleSend() {
|
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
|
|
|
sendError('Method not allowed', 405);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get JSON body
|
|
|
|
|
$input = file_get_contents('php://input');
|
|
|
|
|
$data = json_decode($input, true);
|
|
|
|
|
|
|
|
|
|
if (!$data) {
|
|
|
|
|
sendError('Invalid JSON', 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate required fields
|
|
|
|
|
$required = ['room', 'passphrase', 'user_name', 'text'];
|
|
|
|
|
foreach ($required as $field) {
|
|
|
|
|
if (empty($data[$field])) {
|
|
|
|
|
sendError("Missing required field: $field", 400);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify passphrase
|
|
|
|
|
$room = sanitize($data['room']);
|
|
|
|
|
$passphrase = $data['passphrase'];
|
|
|
|
|
|
|
|
|
|
if (!verifyPassphrase($room, $passphrase)) {
|
|
|
|
|
sendError('Invalid passphrase', 401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create transcription entry
|
|
|
|
|
$transcription = [
|
|
|
|
|
'user_name' => sanitize($data['user_name']),
|
|
|
|
|
'text' => sanitize($data['text']),
|
|
|
|
|
'timestamp' => $data['timestamp'] ?? date('H:i:s'),
|
|
|
|
|
'created_at' => time()
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Add to room
|
|
|
|
|
addTranscription($room, $transcription);
|
|
|
|
|
|
|
|
|
|
// Cleanup old sessions
|
|
|
|
|
cleanupOldSessions();
|
|
|
|
|
|
|
|
|
|
// Success response
|
|
|
|
|
sendJson(['status' => 'ok', 'message' => 'Transcription added']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle streaming transcriptions via Server-Sent Events
|
2025-12-26 10:18:40 -08:00
|
|
|
* Note: Passphrase is optional for streaming (read-only access)
|
2025-12-26 10:09:12 -08:00
|
|
|
*/
|
|
|
|
|
function handleStream() {
|
|
|
|
|
// Get parameters
|
|
|
|
|
$room = sanitize($_GET['room'] ?? '');
|
|
|
|
|
|
2025-12-26 10:18:40 -08:00
|
|
|
if (empty($room)) {
|
|
|
|
|
sendError('Missing room name', 400);
|
2025-12-26 10:09:12 -08:00
|
|
|
}
|
|
|
|
|
|
2025-12-26 10:18:40 -08:00
|
|
|
// Passphrase is optional for streaming (read-only)
|
|
|
|
|
// If room doesn't exist yet, return empty stream
|
|
|
|
|
if (!roomExists($room)) {
|
|
|
|
|
// Return empty stream - room doesn't exist yet
|
|
|
|
|
header('Content-Type: text/event-stream');
|
|
|
|
|
header('Cache-Control: no-cache');
|
|
|
|
|
header('X-Accel-Buffering: no');
|
|
|
|
|
echo ": waiting for room to be created\n\n";
|
|
|
|
|
flush();
|
|
|
|
|
exit();
|
2025-12-26 10:09:12 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set SSE headers
|
|
|
|
|
header('Content-Type: text/event-stream');
|
|
|
|
|
header('Cache-Control: no-cache');
|
|
|
|
|
header('X-Accel-Buffering: no'); // Disable nginx buffering
|
|
|
|
|
|
|
|
|
|
// Track last known count
|
|
|
|
|
$lastCount = 0;
|
|
|
|
|
|
|
|
|
|
// Stream loop
|
|
|
|
|
while (true) {
|
|
|
|
|
$transcriptions = getTranscriptions($room);
|
|
|
|
|
$currentCount = count($transcriptions);
|
|
|
|
|
|
|
|
|
|
// If new transcriptions, send them
|
|
|
|
|
if ($currentCount > $lastCount) {
|
|
|
|
|
$newTranscriptions = array_slice($transcriptions, $lastCount);
|
|
|
|
|
foreach ($newTranscriptions as $trans) {
|
|
|
|
|
echo "data: " . json_encode($trans) . "\n\n";
|
|
|
|
|
flush();
|
|
|
|
|
}
|
|
|
|
|
$lastCount = $currentCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send keepalive comment every 15 seconds
|
|
|
|
|
echo ": keepalive\n\n";
|
|
|
|
|
flush();
|
|
|
|
|
|
|
|
|
|
// Check if client disconnected
|
|
|
|
|
if (connection_aborted()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait before next check
|
|
|
|
|
sleep(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle listing recent transcriptions
|
2025-12-26 10:18:40 -08:00
|
|
|
* Note: Passphrase is optional for listing (read-only access)
|
2025-12-26 10:09:12 -08:00
|
|
|
*/
|
|
|
|
|
function handleList() {
|
|
|
|
|
$room = sanitize($_GET['room'] ?? '');
|
|
|
|
|
|
2025-12-26 10:18:40 -08:00
|
|
|
if (empty($room)) {
|
|
|
|
|
sendError('Missing room name', 400);
|
2025-12-26 10:09:12 -08:00
|
|
|
}
|
|
|
|
|
|
2025-12-26 10:18:40 -08:00
|
|
|
// Passphrase is optional for read-only access
|
|
|
|
|
// If room doesn't exist, return empty array
|
2025-12-26 10:09:12 -08:00
|
|
|
$transcriptions = getTranscriptions($room);
|
|
|
|
|
sendJson(['transcriptions' => $transcriptions]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle info request
|
|
|
|
|
*/
|
|
|
|
|
function handleInfo() {
|
|
|
|
|
sendJson([
|
|
|
|
|
'service' => 'Local Transcription Multi-User Server',
|
|
|
|
|
'version' => '1.0.0',
|
|
|
|
|
'endpoints' => [
|
|
|
|
|
'POST ?action=send' => 'Send a transcription',
|
|
|
|
|
'GET ?action=stream' => 'Stream transcriptions (SSE)',
|
|
|
|
|
'GET ?action=list' => 'List recent transcriptions'
|
|
|
|
|
]
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Verify passphrase for a room
|
|
|
|
|
*/
|
|
|
|
|
function verifyPassphrase($room, $passphrase) {
|
|
|
|
|
$file = getRoomFile($room);
|
|
|
|
|
|
|
|
|
|
// If room doesn't exist, create it with this passphrase
|
|
|
|
|
if (!file_exists($file)) {
|
|
|
|
|
$roomData = [
|
|
|
|
|
'passphrase_hash' => password_hash($passphrase, PASSWORD_DEFAULT),
|
|
|
|
|
'created_at' => time(),
|
|
|
|
|
'transcriptions' => []
|
|
|
|
|
];
|
|
|
|
|
file_put_contents($file, json_encode($roomData));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify passphrase
|
|
|
|
|
$roomData = json_decode(file_get_contents($file), true);
|
|
|
|
|
return password_verify($passphrase, $roomData['passphrase_hash']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add transcription to room
|
|
|
|
|
*/
|
|
|
|
|
function addTranscription($room, $transcription) {
|
|
|
|
|
$file = getRoomFile($room);
|
|
|
|
|
$roomData = json_decode(file_get_contents($file), true);
|
|
|
|
|
|
|
|
|
|
// Add transcription
|
|
|
|
|
$roomData['transcriptions'][] = $transcription;
|
|
|
|
|
|
|
|
|
|
// Limit to max transcriptions
|
|
|
|
|
if (count($roomData['transcriptions']) > MAX_TRANSCRIPTIONS_PER_ROOM) {
|
|
|
|
|
$roomData['transcriptions'] = array_slice(
|
|
|
|
|
$roomData['transcriptions'],
|
|
|
|
|
-MAX_TRANSCRIPTIONS_PER_ROOM
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update last activity
|
|
|
|
|
$roomData['last_activity'] = time();
|
|
|
|
|
|
|
|
|
|
// Save
|
|
|
|
|
file_put_contents($file, json_encode($roomData));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get transcriptions for a room
|
|
|
|
|
*/
|
|
|
|
|
function getTranscriptions($room) {
|
|
|
|
|
$file = getRoomFile($room);
|
|
|
|
|
if (!file_exists($file)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$roomData = json_decode(file_get_contents($file), true);
|
|
|
|
|
return $roomData['transcriptions'] ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get room data file path
|
|
|
|
|
*/
|
|
|
|
|
function getRoomFile($room) {
|
|
|
|
|
return STORAGE_DIR . '/room_' . md5($room) . '.json';
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 10:18:40 -08:00
|
|
|
/**
|
|
|
|
|
* Check if room exists
|
|
|
|
|
*/
|
|
|
|
|
function roomExists($room) {
|
|
|
|
|
return file_exists(getRoomFile($room));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 10:09:12 -08:00
|
|
|
/**
|
|
|
|
|
* Cleanup old sessions
|
|
|
|
|
*/
|
|
|
|
|
function cleanupOldSessions() {
|
|
|
|
|
$files = glob(STORAGE_DIR . '/room_*.json');
|
|
|
|
|
$now = time();
|
|
|
|
|
|
|
|
|
|
foreach ($files as $file) {
|
|
|
|
|
$data = json_decode(file_get_contents($file), true);
|
|
|
|
|
$lastActivity = $data['last_activity'] ?? $data['created_at'];
|
|
|
|
|
|
|
|
|
|
if ($now - $lastActivity > CLEANUP_THRESHOLD) {
|
|
|
|
|
unlink($file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sanitize input
|
|
|
|
|
*/
|
|
|
|
|
function sanitize($input) {
|
|
|
|
|
return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send JSON response
|
|
|
|
|
*/
|
|
|
|
|
function sendJson($data, $code = 200) {
|
|
|
|
|
http_response_code($code);
|
|
|
|
|
header('Content-Type: application/json');
|
|
|
|
|
echo json_encode($data);
|
|
|
|
|
exit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send error response
|
|
|
|
|
*/
|
|
|
|
|
function sendError($message, $code = 400) {
|
|
|
|
|
sendJson(['error' => $message], $code);
|
|
|
|
|
}
|
|
|
|
|
?>
|