diff --git a/application/config/config.php b/application/config/config.php index 9cf661b..d658229 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -115,6 +115,12 @@ define('VOCEA_BASE_URL', Env::get('VOCEA_BASE_URL', 'https://api.vocea.cloud define('VOCEA_API_KEY', Env::get('VOCEA_API_KEY', '')); // wbk_... per-tenant (da vault) define('VOCEA_PORTAL_URL', Env::get('VOCEA_PORTAL_URL', 'https://app.vocea.cloud')); // portale embed via wb-link.js +// ═══════════════════════════════════════════════════════════════════════════ +// VOICE / TTS (ARIA voce naturale — proxy same-origin a nexus-voice-ms AgileHub) +// ═══════════════════════════════════════════════════════════════════════════ +define('VOICE_MS_URL', Env::get('VOICE_MS_URL', 'http://172.21.0.1:4215/tts/speak')); // gateway nis2-network → host:4215 +define('TTS_VOICE_ID', Env::get('TTS_VOICE_ID', 'EXAVITQu4vr4xnSDxMaL')); // voce "Sarah" (come AllTax/TRPG) + // ═══════════════════════════════════════════════════════════════════════════ // CERTISOURCE (atti-service.php) // ═══════════════════════════════════════════════════════════════════════════ diff --git a/application/controllers/AiController.php b/application/controllers/AiController.php index 82f4481..4ea209e 100644 --- a/application/controllers/AiController.php +++ b/application/controllers/AiController.php @@ -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; + } } diff --git a/public/_app-bi-demo.html b/public/_app-bi-demo.html index 9b0ec8e..4228f49 100644 --- a/public/_app-bi-demo.html +++ b/public/_app-bi-demo.html @@ -71,7 +71,7 @@ - + - + diff --git a/public/admin/organizations.html b/public/admin/organizations.html index 5370e4d..ab0744d 100644 --- a/public/admin/organizations.html +++ b/public/admin/organizations.html @@ -148,7 +148,7 @@ - + diff --git a/public/admin/users.html b/public/admin/users.html index e48b9a4..4913943 100644 --- a/public/admin/users.html +++ b/public/admin/users.html @@ -170,7 +170,7 @@ - + diff --git a/public/architecture.html b/public/architecture.html index 797a0ae..859837a 100644 --- a/public/architecture.html +++ b/public/architecture.html @@ -1091,7 +1091,7 @@ - + diff --git a/public/assessment.html b/public/assessment.html index 37f3a9e..4c65f65 100644 --- a/public/assessment.html +++ b/public/assessment.html @@ -155,7 +155,7 @@ - + diff --git a/public/assets.html b/public/assets.html index 7a47861..2c53264 100644 --- a/public/assets.html +++ b/public/assets.html @@ -362,7 +362,7 @@ - + diff --git a/public/calendario.html b/public/calendario.html index 5778220..278bbd7 100644 --- a/public/calendario.html +++ b/public/calendario.html @@ -152,7 +152,7 @@ - + diff --git a/public/companies.html b/public/companies.html index 3715536..7bf0132 100644 --- a/public/companies.html +++ b/public/companies.html @@ -350,7 +350,7 @@ - + diff --git a/public/competenze.html b/public/competenze.html index ae1d88b..0998913 100644 --- a/public/competenze.html +++ b/public/competenze.html @@ -184,7 +184,7 @@ - + diff --git a/public/controlli-periodici.html b/public/controlli-periodici.html index 8d9a6ee..491ad52 100644 --- a/public/controlli-periodici.html +++ b/public/controlli-periodici.html @@ -137,7 +137,7 @@ - + diff --git a/public/cross-analysis.html b/public/cross-analysis.html index e7c4a65..a2b96ae 100644 --- a/public/cross-analysis.html +++ b/public/cross-analysis.html @@ -383,7 +383,7 @@ - + diff --git a/public/dashboard.html b/public/dashboard.html index 1f6ed36..9fc9ba0 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -144,7 +144,7 @@ - + diff --git a/public/guida.html b/public/guida.html index 5287c81..339fde3 100644 --- a/public/guida.html +++ b/public/guida.html @@ -1143,7 +1143,7 @@ - + diff --git a/public/incidents.html b/public/incidents.html index fb5a440..598cbb5 100644 --- a/public/incidents.html +++ b/public/incidents.html @@ -352,7 +352,7 @@ - + diff --git a/public/index.php b/public/index.php index af8de1b..f4eb7d1 100644 --- a/public/index.php +++ b/public/index.php @@ -698,6 +698,7 @@ $actionMap = [ 'ai' => [ 'POST:ask' => 'ask', + 'POST:tts' => 'tts', // voce naturale ARIA (proxy nexus-voice-ms) ], // ── BrandingController — White-label firm (Fase 5 / G16) ── diff --git a/public/integrazioniext.html b/public/integrazioniext.html index 79d68f7..52b3d0a 100644 --- a/public/integrazioniext.html +++ b/public/integrazioniext.html @@ -912,7 +912,7 @@ curl -H "X-API-Key: nis2_TUA_CHIAVE" \ - + - + diff --git a/public/isms.html b/public/isms.html index 26d1f8b..4a48e72 100644 --- a/public/isms.html +++ b/public/isms.html @@ -185,7 +185,7 @@ - + diff --git a/public/js/common.js b/public/js/common.js index cbe5e8e..58cc981 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -1014,6 +1014,42 @@ function switchLang(lang) { + "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;"; document.body.appendChild(panel); + // ── Voce ARIA naturale (TTS via /api/ai/tts → nexus-voice-ms) ───────────── + var ttsMuted = localStorage.getItem('nis2_aria_voice') === 'off'; + var ttsAudio = null; + function _ttsPlain(s) { + return String(s || '') + .replace(/```[\s\S]*?```/g, ' ') + .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/[#*_>`|]/g, ' ') + .replace(/\s+/g, ' ').trim().slice(0, 800); + } + function speak(text) { + if (ttsMuted) return; + var clean = _ttsPlain(text); + if (!clean) return; + var tok = localStorage.getItem('nis2_access_token') || ''; + fetch('/api/ai/tts', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': tok ? ('Bearer ' + tok) : '' }, + body: JSON.stringify({ text: clean }) + }).then(function (r) { return r.ok ? r.blob() : null; }) + .then(function (b) { + if (!b || ttsMuted) return; + try { if (ttsAudio) ttsAudio.pause(); } catch (e) {} + ttsAudio = new Audio(URL.createObjectURL(b)); + ttsAudio.volume = 0.55; + ttsAudio.play().catch(function () {}); // autoplay bloccato → silenzioso + }).catch(function () {}); + } + function toggleVoice() { + ttsMuted = !ttsMuted; + localStorage.setItem('nis2_aria_voice', ttsMuted ? 'off' : 'on'); + if (ttsMuted && ttsAudio) { try { ttsAudio.pause(); } catch (e) {} } + var vb = document.getElementById('ai-chat-voice'); + if (vb) vb.textContent = ttsMuted ? '🔇' : '🔊'; + } + var STORAGE_KEY = 'nis2_ai_chat'; var history = []; var pending = false; // ARIA sta elaborando una risposta (mostra indicatore "sta scrivendo") @@ -1071,6 +1107,8 @@ function switchLang(lang) { + '
' + ' ARIA — Assistente NIS2' + '
' + + '' + '' + '' @@ -1106,6 +1144,8 @@ function switchLang(lang) { + '' + ''; document.getElementById('ai-chat-close').onclick = closePanel; + var _vb = document.getElementById('ai-chat-voice'); + if (_vb) _vb.onclick = toggleVoice; document.getElementById('ai-chat-form').onsubmit = onSubmit; var inputEl = document.getElementById('ai-chat-input'); if (inputEl) { @@ -1165,6 +1205,7 @@ function switchLang(lang) { || (resp && (resp.answer || resp.message || resp.text)) || 'ARIA non ha risposto. Riprova tra poco.'; history.push({ role: 'assistant', content: answer }); + try { speak(answer); } catch (e2) {} // voce naturale ARIA (se non mutata) } catch (e) { history.push({ role: 'assistant', diff --git a/public/kb.html b/public/kb.html index 43df1da..87843dd 100644 --- a/public/kb.html +++ b/public/kb.html @@ -151,7 +151,7 @@ - + diff --git a/public/login.html b/public/login.html index fa8157e..d7e7a2b 100644 --- a/public/login.html +++ b/public/login.html @@ -95,7 +95,7 @@ - + - + diff --git a/public/misure-requisiti.html b/public/misure-requisiti.html index 188eaf5..2710156 100644 --- a/public/misure-requisiti.html +++ b/public/misure-requisiti.html @@ -168,7 +168,7 @@ - + diff --git a/public/mktg-api-doc.html b/public/mktg-api-doc.html index 9e01367..ddf581b 100644 --- a/public/mktg-api-doc.html +++ b/public/mktg-api-doc.html @@ -426,6 +426,6 @@ curl https://nis2.agile.software/api/services/status - + diff --git a/public/normative.html b/public/normative.html index 11f935d..bf1f0c2 100644 --- a/public/normative.html +++ b/public/normative.html @@ -113,7 +113,7 @@ - + diff --git a/public/onboarding.html b/public/onboarding.html index 466026d..c090a81 100644 --- a/public/onboarding.html +++ b/public/onboarding.html @@ -495,7 +495,7 @@ - + - + diff --git a/public/policies.html b/public/policies.html index 174aeb7..fb80045 100644 --- a/public/policies.html +++ b/public/policies.html @@ -334,7 +334,7 @@ - + diff --git a/public/raci.html b/public/raci.html index 09db56b..6be2a6e 100644 --- a/public/raci.html +++ b/public/raci.html @@ -132,7 +132,7 @@ - + diff --git a/public/register.html b/public/register.html index 3f92cbc..6c263dc 100644 --- a/public/register.html +++ b/public/register.html @@ -269,7 +269,7 @@ - + - + diff --git a/public/review-schedule.html b/public/review-schedule.html index d68a218..ea6c178 100644 --- a/public/review-schedule.html +++ b/public/review-schedule.html @@ -142,7 +142,7 @@ - + diff --git a/public/risks.html b/public/risks.html index f2d01bb..c5e2a7c 100644 --- a/public/risks.html +++ b/public/risks.html @@ -495,7 +495,7 @@ - + diff --git a/public/settings.html b/public/settings.html index c2e7629..fe264bf 100644 --- a/public/settings.html +++ b/public/settings.html @@ -689,7 +689,7 @@ - + diff --git a/public/stakeholder-activities.html b/public/stakeholder-activities.html index 7eaf6c2..3895674 100644 --- a/public/stakeholder-activities.html +++ b/public/stakeholder-activities.html @@ -195,7 +195,7 @@ - + diff --git a/public/stakeholders.html b/public/stakeholders.html index 976bc6c..344430a 100644 --- a/public/stakeholders.html +++ b/public/stakeholders.html @@ -192,7 +192,7 @@ - + diff --git a/public/supply-chain.html b/public/supply-chain.html index 2a2355d..3acb03a 100644 --- a/public/supply-chain.html +++ b/public/supply-chain.html @@ -478,7 +478,7 @@ - + diff --git a/public/training.html b/public/training.html index 9d48c23..1176581 100644 --- a/public/training.html +++ b/public/training.html @@ -293,7 +293,7 @@ - + diff --git a/public/whistleblowing.html b/public/whistleblowing.html index a5658fa..6ac9f41 100644 --- a/public/whistleblowing.html +++ b/public/whistleblowing.html @@ -208,7 +208,7 @@ - + diff --git a/public/workflow.html b/public/workflow.html index ed56380..2facd8e 100644 --- a/public/workflow.html +++ b/public/workflow.html @@ -322,7 +322,7 @@ - +