my-homelab-configs/apps/website/translate.php

115 lines
4.1 KiB
PHP

<?php
header('Content-Type: application/json');
function translate_response(int $status, array $body): never {
http_response_code($status);
echo json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
function clean_translate_text(string $value, int $maxLength = 4000): string {
$value = strip_tags($value);
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value) ?? '';
$value = trim($value);
if (strlen($value) > $maxLength) {
$value = substr($value, 0, $maxLength);
}
return $value;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
translate_response(405, ['error' => 'Method not allowed']);
}
if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 131072) {
translate_response(413, ['error' => 'Request too large']);
}
$body = json_decode(file_get_contents('php://input'), true);
if (!is_array($body) || !isset($body['targetLang'], $body['texts']) || !is_array($body['texts'])) {
translate_response(400, ['error' => 'Missing target language or texts']);
}
$targetLang = preg_replace('/[^a-z]/', '', strtolower((string) $body['targetLang']));
if (strlen($targetLang) < 2 || strlen($targetLang) > 5) {
translate_response(400, ['error' => 'Invalid target language']);
}
$targetName = clean_translate_text((string) ($body['targetName'] ?? strtoupper($targetLang)), 80);
$texts = [];
$totalLength = 0;
foreach ($body['texts'] as $text) {
if (!is_string($text) && !is_numeric($text)) {
continue;
}
$cleanText = clean_translate_text((string) $text);
$totalLength += strlen($cleanText);
$texts[] = $cleanText;
}
if (!$texts || count($texts) > 240 || $totalLength > 80000) {
translate_response(400, ['error' => 'Invalid translation batch']);
}
$prompt = "Translate each item to {$targetName}.\n"
. "Return ONLY a valid JSON array of translated strings in the same order, no explanations, no markdown.\n"
. "Example input: [\"Hello\", \"How are you\"]\n"
. "Example output: [\"Bonjour\", \"Comment allez-vous\"]\n\n"
. 'Input: ' . json_encode($texts, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ollamaHost = rtrim(getenv('OLLAMA_HOST') ?: 'http://192.168.100.68:11434', '/');
$ollamaModel = getenv('OLLAMA_MODEL') ?: 'llama3.2:3b';
$ollamaPayload = json_encode([
'model' => $ollamaModel,
'prompt' => $prompt,
'stream' => false,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($ollamaPayload === false) {
translate_response(500, ['error' => 'Could not encode Ollama request']);
}
$curl = curl_init($ollamaHost . '/api/generate');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => $ollamaPayload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 130,
]);
$rawResponse = curl_exec($curl);
$curlError = curl_error($curl);
$statusCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($rawResponse === false || $statusCode < 200 || $statusCode >= 300) {
translate_response(502, ['error' => 'Ollama request failed', 'status' => $statusCode, 'detail' => $curlError]);
}
$ollamaResponse = json_decode($rawResponse, true);
if (!is_array($ollamaResponse) || !isset($ollamaResponse['response']) || !is_string($ollamaResponse['response'])) {
translate_response(502, ['error' => 'Invalid Ollama response']);
}
$translationJson = trim(str_replace(['```json', '```'], '', $ollamaResponse['response']));
$translations = json_decode($translationJson, true);
if (!is_array($translations) || count($translations) !== count($texts)) {
translate_response(502, ['error' => 'Unexpected translation response']);
}
$cleanTranslations = [];
foreach ($translations as $translation) {
if (!is_string($translation) && !is_numeric($translation)) {
translate_response(502, ['error' => 'Unexpected translation item']);
}
$cleanTranslations[] = clean_translate_text((string) $translation);
}
echo json_encode([
'translations' => $cleanTranslations,
'model' => $ollamaModel,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);