diff --git a/src/components/ControllerPage.vue b/src/components/ControllerPage.vue index e6bc1c6..54622d1 100644 --- a/src/components/ControllerPage.vue +++ b/src/components/ControllerPage.vue @@ -481,21 +481,29 @@ export default { }, speak() { - const msg = new SpeechSynthesisUtterance() + let text = '' if (this.state.sp.punt.home + this.state.sp.punt.guest === 0) { - msg.text = "zero a zero" + text = "zero a zero" } else if (this.state.sp.punt.home === this.state.sp.punt.guest) { - msg.text = this.state.sp.punt.home + " pari" + text = this.state.sp.punt.home + " pari" } else { if (this.state.sp.servHome) { - msg.text = this.state.sp.punt.home + " a " + this.state.sp.punt.guest + text = this.state.sp.punt.home + " a " + this.state.sp.punt.guest } else { - msg.text = this.state.sp.punt.guest + " a " + this.state.sp.punt.home + text = this.state.sp.punt.guest + " a " + this.state.sp.punt.home } } - const voices = window.speechSynthesis.getVoices() - msg.voice = voices.find(v => v.name === 'Google italiano') - window.speechSynthesis.speak(msg) + if (!this.wsConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.showErrorFeedback('Non connesso al server') + return + } + + try { + this.ws.send(JSON.stringify({ type: 'speak', text })) + } catch (err) { + console.error('[Controller] Failed to send speak command:', err) + this.showErrorFeedback('Errore invio comando voce') + } } } } diff --git a/src/components/DisplayPage.vue b/src/components/DisplayPage.vue index af90728..fdc6011 100644 --- a/src/components/DisplayPage.vue +++ b/src/components/DisplayPage.vue @@ -266,6 +266,8 @@ export default { if (msg.type === 'state') { this.state = msg.state + } else if (msg.type === 'speak') { + this.speakOnDisplay(msg.text) } else if (msg.type === 'error') { console.error('[Display] Server error:', msg.message) } @@ -314,6 +316,27 @@ export default { this.reconnectTimeout = null this.connectWebSocket() }, delay) + }, + speakOnDisplay(text) { + if (typeof text !== 'string' || !text.trim()) { + return + } + if (!('speechSynthesis' in window)) { + console.warn('[Display] speechSynthesis not supported') + return + } + + const utterance = new SpeechSynthesisUtterance(text.trim()) + const voices = window.speechSynthesis.getVoices() + const preferredVoice = voices.find((v) => v.name === 'Google italiano') + || voices.find((v) => v.lang && v.lang.toLowerCase().startsWith('it')) + if (preferredVoice) { + utterance.voice = preferredVoice + } + utterance.lang = 'it-IT' + + window.speechSynthesis.cancel() + window.speechSynthesis.speak(utterance) } } } diff --git a/src/websocket-handler.js b/src/websocket-handler.js index 5941bb8..ea2474f 100644 --- a/src/websocket-handler.js +++ b/src/websocket-handler.js @@ -28,6 +28,10 @@ export function setupWebSocketHandler(wss) { if (msg.type === 'action') { return handleAction(ws, msg) } + + if (msg.type === 'speak') { + return handleSpeak(ws, msg) + } } catch (err) { console.error('Error processing message:', err, 'data:', data) // Invia l'errore solo se la connessione e ancora aperta. @@ -98,6 +102,30 @@ export function setupWebSocketHandler(wss) { broadcastState() } + /** + * Gestisce una richiesta di sintesi vocale proveniente dal controller. + * Il messaggio viene inoltrato solo ai client registrati come display. + */ + function handleSpeak(ws, msg) { + const client = clients.get(ws) + if (!client || client.role !== 'controller') { + sendError(ws, 'Only controllers can request speech') + return + } + + if (typeof msg.text !== 'string' || msg.text.trim() === '') { + sendError(ws, 'Invalid speak payload') + return + } + + const speakMsg = JSON.stringify({ type: 'speak', text: msg.text.trim() }) + clients.forEach((meta, clientWs) => { + if (meta.role === 'display' && clientWs.readyState === 1) { + clientWs.send(speakMsg) + } + }) + } + /** * Invia un messaggio di errore al client */