From e60a6902df178cd9a302390e215d7c3dc0fbeadc Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 16 Aug 2024 10:02:51 +0200 Subject: [PATCH] Add option to select TTS voice per venue --- CHANGELOG.md | 1 + routes/players.js | 7 ++- .../new-venue-form.component.js | 26 ++++++++- .../new-venue-form/new-venue-form.marko | 18 +++++- .../new-venue-form/new-venue-form.style.less | 6 ++ .../components/offices/offices.component.js | 19 +++++++ .../offices/components/offices/offices.marko | 8 +-- .../score-statistic-row.marko | 2 +- .../new-player-form.component.js | 2 +- .../components/players/players.component.js | 15 ++++- .../players/components/players/players.marko | 2 +- src/pages/players/players-template.marko | 2 +- src/util/socket.io-helper.js | 11 +++- src/util/speaker.js | 55 ++++++++++++++++++- 14 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 src/pages/offices/components/offices/offices.component.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d627f3ce..3fa2f6fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ A preview of major changes can be found in the Wiki ([Latest Changes](https://gi - "Badges" page showing overview of all badges and how many players have unlocked them - New Darts Per Leg `DPL` metric added to tournament overview - Convenience method for scoring a user checkout by pressing `55` on numpad +- Option to select which TTS voice to use per venue - Tournament Predictor - Ability to configure bots from Tablet Controller - New set of larger Compact buttons diff --git a/routes/players.js b/routes/players.js index 6856d44b..5cabde08 100644 --- a/routes/players.js +++ b/routes/players.js @@ -16,11 +16,12 @@ var head2headTemplate = template.load(require.resolve('../src/pages/player-head2 router.get('/', function (req, res, next) { axios.all([ axios.get(`${req.app.locals.kcapp.api}/player/active`), - axios.get(`${req.app.locals.kcapp.api}/office`) - ]).then(axios.spread((playersResponse, officesResponse) => { + axios.get(`${req.app.locals.kcapp.api}/office`), + axios.get(`${req.app.locals.kcapp.api}/venue`) + ]).then(axios.spread((playersResponse, officesResponse, venueResponse) => { var players = playersResponse.data; players = _.sortBy(players, (player) => player.name) - res.marko(playersTemplate, { players: players, offices: officesResponse.data }); + res.marko(playersTemplate, { players: players, offices: officesResponse.data, venues: venueResponse.data }); })).catch(error => { debug(`Error when getting players: ${error}`); next(error); diff --git a/src/pages/offices/components/new-venue-form/new-venue-form.component.js b/src/pages/offices/components/new-venue-form/new-venue-form.component.js index 8b410a38..c914b2a4 100644 --- a/src/pages/offices/components/new-venue-form/new-venue-form.component.js +++ b/src/pages/offices/components/new-venue-form/new-venue-form.component.js @@ -1,11 +1,13 @@ const axios = require('axios'); +const speaker = require("../../../../util/speaker"); module.exports = { - onInput(input) { + onCreate(input) { var office = input.offices[Object.keys(input.offices)[0]]; this.state = { name: undefined, description: undefined, + ttsVoice: undefined, has_dual_monitor: false, has_led_lights: false, has_wled_lights: false, @@ -13,7 +15,9 @@ module.exports = { smartboard_uuid: undefined, smartboard_button_number: undefined, office_id: office ? office.id : undefined, - isAdd: input.isAdd + isAdd: input.isAdd, + + voices: input.voices } if (input.venue) { this.state = { @@ -21,16 +25,25 @@ module.exports = { office_id: input.venue.office_id, name: input.venue.name, description: input.venue.description, + ttsVoice: input.venue.config.tts_voice, has_dual_monitor: input.venue.config.has_dual_monitor, has_led_lights: input.venue.config.has_led_lights, has_wled_lights: input.venue.config.has_wled_lights, has_smartboard: input.venue.config.has_smartboard, smartboard_uuid: input.venue.config.smartboard_uuid, smartboard_button_number: input.venue.config.smartboard_button_number, - isAdd: input.isAdd + isAdd: input.isAdd, + + voices: input.voices } } }, + onInput(input) { + if (input.voices) { + this.state.voices = input.voices; + } + }, + officeChanged(event) { this.state.office_id = event.target.value; }, @@ -40,6 +53,12 @@ module.exports = { descriptionChange(event) { this.state.description = event.target.value; }, + ttsChange(event) { + this.state.ttsVoice = event.target.value; + }, + playVoice(event) { + speaker.speakWithVoice({ text: "Welcome to k capp!" }, this.state.ttsVoice); + }, dualMonitorChange(event) { this.state.has_dual_monitor = event.target.checked; }, @@ -75,6 +94,7 @@ module.exports = { has_dual_monitor: this.state.has_dual_monitor, has_led_lights: this.state.has_led_lights, has_wled_lights: this.state.has_wled_lights, + tts_voice: this.state.ttsVoice, has_smartboard: this.state.has_smartboard, smartboard_uuid: this.state.smartboard_uuid, smartboard_button_number: this.state.smartboard_button_number diff --git a/src/pages/offices/components/new-venue-form/new-venue-form.marko b/src/pages/offices/components/new-venue-form/new-venue-form.marko index 13842cb5..ab84a046 100644 --- a/src/pages/offices/components/new-venue-form/new-venue-form.marko +++ b/src/pages/offices/components/new-venue-form/new-venue-form.marko @@ -27,6 +27,22 @@ $ var heading = input.heading || "Add Venue"; +
+ +
+ +
+
+ + + +
+
@@ -39,7 +55,7 @@ $ var heading = input.heading || "Add Venue";
-
+
diff --git a/src/pages/offices/components/new-venue-form/new-venue-form.style.less b/src/pages/offices/components/new-venue-form/new-venue-form.style.less index 188ed0ba..73ab7b44 100644 --- a/src/pages/offices/components/new-venue-form/new-venue-form.style.less +++ b/src/pages/offices/components/new-venue-form/new-venue-form.style.less @@ -1,3 +1,9 @@ .small-checkbox { height: 1.5em; +} + +.play-button { + &:hover { + color: #dbdbdb; + } } \ No newline at end of file diff --git a/src/pages/offices/components/offices/offices.component.js b/src/pages/offices/components/offices/offices.component.js new file mode 100644 index 00000000..70f7d704 --- /dev/null +++ b/src/pages/offices/components/offices/offices.component.js @@ -0,0 +1,19 @@ +const speaker = require("../../../../util/speaker"); + +module.exports = { + onCreate(input) { + this.state = { + voices: [] + }; + }, + + onMount() { + $(function () { + speaker.loadVoices((voices) => { + if (voices && voices.length > 0) { + this.state.voices = voices; + } + }); + }.bind(this)); + } +} \ No newline at end of file diff --git a/src/pages/offices/components/offices/offices.marko b/src/pages/offices/components/offices/offices.marko index 2e1d1027..04e260bd 100644 --- a/src/pages/offices/components/offices/offices.marko +++ b/src/pages/offices/components/offices/offices.marko @@ -33,7 +33,7 @@ Edit - +
@@ -60,12 +60,12 @@ - $ var modalId = `edit-venue-${venue.id}-modal`; + $ let modalId = `edit-venue-${venue.id}-modal`; - + @@ -82,6 +82,6 @@
- +
diff --git a/src/pages/player-head2head/components/versus-table/components/score-statistic-row/score-statistic-row.marko b/src/pages/player-head2head/components/versus-table/components/score-statistic-row/score-statistic-row.marko index 414236c0..2e46357d 100644 --- a/src/pages/player-head2head/components/versus-table/components/score-statistic-row/score-statistic-row.marko +++ b/src/pages/player-head2head/components/versus-table/components/score-statistic-row/score-statistic-row.marko @@ -5,5 +5,5 @@ $ var stat2PerLeg = input.stat2PerLeg; (${stat1}) ${stat1PerLeg} ${input.label} - ${stat2} (${stat2PerLeg}) + ${stat2PerLeg} (${stat2}) \ No newline at end of file diff --git a/src/pages/players/components/new-player-form/new-player-form.component.js b/src/pages/players/components/new-player-form/new-player-form.component.js index b45ab657..d48d5052 100644 --- a/src/pages/players/components/new-player-form/new-player-form.component.js +++ b/src/pages/players/components/new-player-form/new-player-form.component.js @@ -55,7 +55,7 @@ module.exports = { speaker.speak( {text: vocalName } ); }); } else { - speaker.speak( {text: vocalName } ); + speaker.speakWithVoice( {text: vocalName }, this.input.ttsVoice ); } } }, diff --git a/src/pages/players/components/players/players.component.js b/src/pages/players/components/players/players.component.js index 8323e2a4..44be93fc 100644 --- a/src/pages/players/components/players/players.component.js +++ b/src/pages/players/components/players/players.component.js @@ -1,11 +1,14 @@ const io = require(`../../../../util/socket.io-helper`); +const localStorage = require("../../../../util/localstorage"); const _ = require("underscore"); +const speaker = require('../../../../util/speaker'); module.exports = { onCreate(input) { this.state = { players: input.players, - smartcardReadhFnc: (data) => { } + smartcardReadhFnc: (data) => { }, + ttsVoice: undefined } }, onMount() { @@ -17,6 +20,16 @@ module.exports = { const comp = this.getComponent('add-player-modal'); this.state.smartcardReadhFnc = comp.smartcardRead.bind(comp); }.bind(this)); + + const venueId = localStorage.get('venue_id'); + if (venueId) { + let venues = this.input.venues.filter((venue) => venue.id == venueId); + if (venues.length > 0) { + this.state.ttsVoice = venues[0].config.tts_voice; + } + } + // Initialize voices + speaker.loadVoices(() => {}); }, officeChanged(id) { const officeId = parseInt(id); diff --git a/src/pages/players/components/players/players.marko b/src/pages/players/components/players/players.marko index 55891a3d..a179762b 100644 --- a/src/pages/players/components/players/players.marko +++ b/src/pages/players/components/players/players.marko @@ -17,7 +17,7 @@
- +
\ No newline at end of file diff --git a/src/pages/players/players-template.marko b/src/pages/players/players-template.marko index 05a93295..6099640e 100644 --- a/src/pages/players/players-template.marko +++ b/src/pages/players/players-template.marko @@ -2,6 +2,6 @@ import Layout from "../layout.marko" <${Layout}> <@body> - + diff --git a/src/util/socket.io-helper.js b/src/util/socket.io-helper.js index 327a7abd..93a2dfcf 100644 --- a/src/util/socket.io-helper.js +++ b/src/util/socket.io-helper.js @@ -85,12 +85,17 @@ exports.say = (data, thiz) => { return; } + let voice = undefined; + if (thiz.state.venueConfig) { + voice = thiz.state.venueConfig.tts_voice; + } + const oldPlayer = thiz.state.audioAnnouncer; const isAudioAnnouncement = (oldPlayer.duration > 0 && !oldPlayer.paused) || (!isNaN(oldPlayer.duration) && !oldPlayer.ended && oldPlayer.paused); if (data.audios) { const audioPlayers = [ ]; for (const file of data.audios) { - audioPlayers.push(file.file ? new Audio(file.file) : speaker.getUtterance(file)); + audioPlayers.push(file.file ? new Audio(file.file) : speaker.getUtteranceWithVoice(file, voice)); } for (let i = 0; i < audioPlayers.length; i++) { @@ -124,13 +129,13 @@ exports.say = (data, thiz) => { } else { if (isAudioAnnouncement) { oldPlayer.addEventListener("ended", () => { - speaker.speak(data, () => { + speaker.speakWithVoice(data, voice, () => { thiz.state.socket.emit("speak_finish"); }); }, false); } else { - speaker.speak(data, () => { + speaker.speakWithVoice(data, voice, () => { thiz.state.socket.emit("speak_finish"); }); } diff --git a/src/util/speaker.js b/src/util/speaker.js index d3dae16f..ff36961b 100644 --- a/src/util/speaker.js +++ b/src/util/speaker.js @@ -1,3 +1,5 @@ +exports.VOICES = []; + exports.getUtterance = function (data, callback) { const msg = new SpeechSynthesisUtterance(); msg.volume = 1; @@ -16,9 +18,60 @@ exports.getUtterance = function (data, callback) { return msg; } +exports.getUtteranceWithVoice = function (data, voiceName, readyCallback, endCallback) { + const msg = new SpeechSynthesisUtterance(); + msg.volume = 1; + msg.rate = 1.0; + msg.pitch = 1; + msg.text = data.text; + msg.onend = () => { + if (endCallback) { + endCallback(); + } + }; + msg.play = () => { + // Adding custom play method to allow easier chaining with audio elements + speechSynthesis.speak(msg); + } + this.getVoice(voiceName, (voice) => { + msg.voice = voice; + if (readyCallback) { + readyCallback(msg); + } + }); + return msg; +} + +exports.loadVoices = function(callback) { + function getVoices() { + this.VOICES = speechSynthesis.getVoices(); + callback(this.VOICES); + } + if (this.VOICES.length === 0) { + if ('onvoiceschanged' in speechSynthesis) { + speechSynthesis.onvoiceschanged = getVoices.bind(this); + } + getVoices.bind(this)(); + } else { + callback(this.VOICES); + } +} + +exports.getVoice = function(name, callback) { + this.loadVoices((voices) => { + let voice = voices.filter(voice => voice.name === name)[0]; + callback(voice); + }); +} + exports.speak = function (data, callback) { const msg = this.getUtterance(data, callback); + speechSynthesis.cancel(); // Sometimes it gets stuck and refuses to speak any more, so cancel any existing before speaking again speechSynthesis.speak(msg); - return msg; }; +exports.speakWithVoice = function (data, voiceName, endCallback) { + this.getUtteranceWithVoice(data, voiceName, (msg) => { + speechSynthesis.speak(msg); + }, endCallback); +};