[FEAT] ARIA voce naturale (proxy TTS nexus-voice-ms) + toggle voce — Fase C
Porting voce TRPG/AllTax (ElevenLabs "Sarah") sull'ARIA chat NIS2: - AiController::tts() + route POST /api/ai/tts: proxy same-origin a nexus-voice-ms (config VOICE_MS_URL=172.21.0.1:4215, voce TTS_VOICE_ID=EXAVITQu4vr4xnSDxMaL, eleven_turbo_v2_5). requireAuth, cache MP3 sha256 (sys_get_temp_dir/nis2-tts-cache), validazioni (text<=800, voice_id allowlist), stream audio/mpeg, 502 se upstream non-audio. - common.js: speak(answer) dopo ogni risposta ARIA (voce discreta vol .55, markdown strip), toggle 🔊/🔇 nell'header (muto persistito localStorage nis2_aria_voice), autoplay best-effort. Smoke: voice-ms da nis2-app HTTP 200 audio/mpeg 19KB; /api/ai/tts 401 senza auth. Reachability OK (no allowlist VOX necessaria). Cache-buster common.js -> 20260624g. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fe7d0555c6
commit
a982797fa2
@@ -97,4 +97,78 @@ class AiController extends BaseController
|
||||
$this->jsonError('Assistente AI non disponibile in questo momento', 503, 'AI_UNAVAILABLE');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/ai/tts — Voce naturale di ARIA.
|
||||
* Proxy same-origin verso nexus-voice-ms (ElevenLabs): il voice-ms non espone CORS
|
||||
* per l'origin NIS2 → il browser non può chiamarlo diretto. Cache MP3 per hash del
|
||||
* testo (stesse frasi → niente costo ripetuto). Stream audio/mpeg.
|
||||
*/
|
||||
public function tts(): void
|
||||
{
|
||||
$this->requireAuth();
|
||||
|
||||
$text = trim((string) $this->getParam('text', ''));
|
||||
if ($text === '') { $this->jsonError('Testo mancante', 422, 'TTS_EMPTY'); }
|
||||
if (mb_strlen($text) > 800) { $text = mb_substr($text, 0, 800); }
|
||||
|
||||
$lang = substr(strtolower(preg_replace('/[^a-z]/', '', (string) $this->getParam('lang', 'it')) ?: 'it'), 0, 5) ?: 'it';
|
||||
$voice = (string) $this->getParam('voice_id', defined('TTS_VOICE_ID') ? TTS_VOICE_ID : 'EXAVITQu4vr4xnSDxMaL');
|
||||
if (!preg_match('/^[A-Za-z0-9]{8,40}$/', $voice)) {
|
||||
$voice = defined('TTS_VOICE_ID') ? TTS_VOICE_ID : 'EXAVITQu4vr4xnSDxMaL';
|
||||
}
|
||||
|
||||
$cacheDir = sys_get_temp_dir() . '/nis2-tts-cache';
|
||||
$cacheFile = $cacheDir . '/' . hash('sha256', $voice . '|' . $lang . '|' . $text) . '.mp3';
|
||||
if (is_file($cacheFile) && filesize($cacheFile) > 256) {
|
||||
$this->streamMp3((string) file_get_contents($cacheFile), 'HIT');
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = json_encode([
|
||||
'text' => $text,
|
||||
'lang' => $lang,
|
||||
'voice_id' => $voice,
|
||||
'model' => 'eleven_turbo_v2_5',
|
||||
'stability' => 0.5,
|
||||
'similarity_boost' => 0.75,
|
||||
'style' => 0.0,
|
||||
'output_format' => 'mp3_44100_128',
|
||||
]);
|
||||
|
||||
$url = defined('VOICE_MS_URL') ? VOICE_MS_URL : 'http://172.21.0.1:4215/tts/speak';
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 6,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
$audio = curl_exec($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$ctype = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code !== 200 || !$audio || strlen($audio) < 256 || stripos($ctype, 'audio') === false) {
|
||||
$this->jsonError('Sintesi vocale non disponibile', 502, 'TTS_UPSTREAM');
|
||||
}
|
||||
|
||||
if (!is_dir($cacheDir)) { @mkdir($cacheDir, 0775, true); }
|
||||
@file_put_contents($cacheFile . '.tmp', $audio);
|
||||
@rename($cacheFile . '.tmp', $cacheFile);
|
||||
|
||||
$this->streamMp3($audio, 'MISS');
|
||||
}
|
||||
|
||||
private function streamMp3(string $audio, string $cache): void
|
||||
{
|
||||
header('Content-Type: audio/mpeg');
|
||||
header('Content-Length: ' . strlen($audio));
|
||||
header('Cache-Control: private, max-age=86400');
|
||||
header('X-TTS-Cache: ' . $cache);
|
||||
echo $audio;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user