Cache website translations in Redis
This commit is contained in:
parent
a3b6c79664
commit
c0b3eeef83
|
|
@ -18,6 +18,147 @@ function clean_translate_text(string $value, int $maxLength = 4000): string {
|
|||
return $value;
|
||||
}
|
||||
|
||||
function redis_encode_command(array $parts): string {
|
||||
$command = '*' . count($parts) . "\r\n";
|
||||
foreach ($parts as $part) {
|
||||
$value = (string) $part;
|
||||
$command .= '$' . strlen($value) . "\r\n" . $value . "\r\n";
|
||||
}
|
||||
return $command;
|
||||
}
|
||||
|
||||
function redis_read_line($socket): ?string {
|
||||
$line = fgets($socket);
|
||||
if ($line === false) {
|
||||
return null;
|
||||
}
|
||||
return rtrim($line, "\r\n");
|
||||
}
|
||||
|
||||
function redis_read_response($socket): mixed {
|
||||
$prefix = fgetc($socket);
|
||||
if ($prefix === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($prefix === '+') {
|
||||
return redis_read_line($socket);
|
||||
}
|
||||
|
||||
if ($prefix === '-' || $prefix === ':') {
|
||||
redis_read_line($socket);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($prefix === '$') {
|
||||
$lengthLine = redis_read_line($socket);
|
||||
if ($lengthLine === null) {
|
||||
return null;
|
||||
}
|
||||
$length = (int) $lengthLine;
|
||||
if ($length < 0) {
|
||||
return null;
|
||||
}
|
||||
$value = '';
|
||||
while (strlen($value) < $length) {
|
||||
$chunk = fread($socket, $length - strlen($value));
|
||||
if ($chunk === false || $chunk === '') {
|
||||
return null;
|
||||
}
|
||||
$value .= $chunk;
|
||||
}
|
||||
fread($socket, 2);
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($prefix === '*') {
|
||||
$countLine = redis_read_line($socket);
|
||||
if ($countLine === null) {
|
||||
return null;
|
||||
}
|
||||
$count = (int) $countLine;
|
||||
if ($count < 0) {
|
||||
return null;
|
||||
}
|
||||
$items = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$items[] = redis_read_response($socket);
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function redis_connect(): mixed {
|
||||
$host = trim((string) getenv('TRANSLATION_REDIS_HOST'));
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = (int) (getenv('TRANSLATION_REDIS_PORT') ?: 6379);
|
||||
$socket = @fsockopen($host, $port, $errno, $errstr, 0.2);
|
||||
if (!$socket) {
|
||||
return null;
|
||||
}
|
||||
stream_set_timeout($socket, 1);
|
||||
return $socket;
|
||||
}
|
||||
|
||||
function translation_cache_key(string $prefix, string $model, string $targetLang, string $text): string {
|
||||
$safePrefix = preg_replace('/[^a-zA-Z0-9:_-]/', '', $prefix) ?: 'translation:v1';
|
||||
return $safePrefix . ':' . hash('sha256', $model) . ':' . $targetLang . ':' . hash('sha256', $text);
|
||||
}
|
||||
|
||||
function translation_cache_read(array $keys): array {
|
||||
if (!$keys) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$socket = redis_connect();
|
||||
if (!$socket) {
|
||||
return [];
|
||||
}
|
||||
|
||||
fwrite($socket, redis_encode_command(array_merge(['MGET'], $keys)));
|
||||
$values = redis_read_response($socket);
|
||||
fclose($socket);
|
||||
|
||||
if (!is_array($values) || count($values) !== count($keys)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$cached = [];
|
||||
foreach ($keys as $index => $key) {
|
||||
if (is_string($values[$index])) {
|
||||
$cached[$key] = clean_translate_text($values[$index]);
|
||||
}
|
||||
}
|
||||
return $cached;
|
||||
}
|
||||
|
||||
function translation_cache_write(array $items): void {
|
||||
if (!$items) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ttl = (int) (getenv('TRANSLATION_CACHE_TTL_SECONDS') ?: 7776000);
|
||||
if ($ttl < 300) {
|
||||
$ttl = 300;
|
||||
}
|
||||
|
||||
$socket = redis_connect();
|
||||
if (!$socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($items as $key => $value) {
|
||||
fwrite($socket, redis_encode_command(['SETEX', $key, (string) $ttl, $value]));
|
||||
redis_read_response($socket);
|
||||
}
|
||||
fclose($socket);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
translate_response(405, ['error' => 'Method not allowed']);
|
||||
}
|
||||
|
|
@ -52,14 +193,47 @@ if (!$texts || count($texts) > 240 || $totalLength > 80000) {
|
|||
translate_response(400, ['error' => 'Invalid translation batch']);
|
||||
}
|
||||
|
||||
$ollamaHost = rtrim(getenv('OLLAMA_HOST') ?: 'http://192.168.100.68:11434', '/');
|
||||
$ollamaModel = getenv('OLLAMA_MODEL') ?: 'llama3.2:3b';
|
||||
$cachePrefix = getenv('TRANSLATION_CACHE_PREFIX') ?: 'translation:v1';
|
||||
$cacheKeys = [];
|
||||
foreach ($texts as $index => $text) {
|
||||
$cacheKeys[$index] = translation_cache_key($cachePrefix, $ollamaModel, $targetLang, $text);
|
||||
}
|
||||
|
||||
$cachedTranslations = translation_cache_read(array_values(array_unique($cacheKeys)));
|
||||
$cleanTranslations = [];
|
||||
$missTexts = [];
|
||||
$missPositions = [];
|
||||
foreach ($texts as $index => $text) {
|
||||
$cacheKey = $cacheKeys[$index];
|
||||
if (array_key_exists($cacheKey, $cachedTranslations)) {
|
||||
$cleanTranslations[$index] = $cachedTranslations[$cacheKey];
|
||||
continue;
|
||||
}
|
||||
$missPositions[] = $index;
|
||||
$missTexts[] = $text;
|
||||
}
|
||||
|
||||
if (!$missTexts) {
|
||||
ksort($cleanTranslations);
|
||||
echo json_encode([
|
||||
'translations' => array_values($cleanTranslations),
|
||||
'model' => $ollamaModel,
|
||||
'cache' => [
|
||||
'hits' => count($texts),
|
||||
'misses' => 0,
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$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);
|
||||
. 'Input: ' . json_encode($missTexts, 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,
|
||||
|
|
@ -96,19 +270,30 @@ if (!is_array($ollamaResponse) || !isset($ollamaResponse['response']) || !is_str
|
|||
|
||||
$translationJson = trim(str_replace(['```json', '```'], '', $ollamaResponse['response']));
|
||||
$translations = json_decode($translationJson, true);
|
||||
if (!is_array($translations) || count($translations) !== count($texts)) {
|
||||
if (!is_array($translations) || count($translations) !== count($missTexts)) {
|
||||
translate_response(502, ['error' => 'Unexpected translation response']);
|
||||
}
|
||||
|
||||
$cleanTranslations = [];
|
||||
foreach ($translations as $translation) {
|
||||
foreach ($translations as $index => $translation) {
|
||||
if (!is_string($translation) && !is_numeric($translation)) {
|
||||
translate_response(502, ['error' => 'Unexpected translation item']);
|
||||
}
|
||||
$cleanTranslations[] = clean_translate_text((string) $translation);
|
||||
$originalPosition = $missPositions[$index];
|
||||
$cleanTranslations[$originalPosition] = clean_translate_text((string) $translation);
|
||||
}
|
||||
|
||||
$cacheWrites = [];
|
||||
foreach ($missPositions as $position) {
|
||||
$cacheWrites[$cacheKeys[$position]] = $cleanTranslations[$position];
|
||||
}
|
||||
translation_cache_write($cacheWrites);
|
||||
|
||||
ksort($cleanTranslations);
|
||||
echo json_encode([
|
||||
'translations' => $cleanTranslations,
|
||||
'translations' => array_values($cleanTranslations),
|
||||
'model' => $ollamaModel,
|
||||
'cache' => [
|
||||
'hits' => count($texts) - count($missTexts),
|
||||
'misses' => count($missTexts),
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
|
|
|||
Loading…
Reference in New Issue