Skip to content

Commit

Permalink
Add option to select TTS voice per venue
Browse files Browse the repository at this point in the history
  • Loading branch information
thordy committed Aug 16, 2024
1 parent ed3b6b0 commit e60a690
Show file tree
Hide file tree
Showing 14 changed files with 154 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions routes/players.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,49 @@
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,
has_smartboard: false,
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 = {
id: input.venue.id,
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;
},
Expand All @@ -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;
},
Expand Down Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion src/pages/offices/components/new-venue-form/new-venue-form.marko
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ $ var heading = input.heading || "Add Venue";
<label class="control-label">Description</label>
<input class="form-control" type="text" value=state.description on-input("descriptionChange")/>
</div>
<div class="form-group">
<label class="control-label">TTS Voice</label>
<div style="width: 95%; float: left;">
<select class="form-control" on-change("ttsChange")>
<option value=null>${"<Use Browser Default>"}</option>
<for|idx, voice| in=state.voices>
<option value=voice.name selected=(state.ttsVoice == voice.name)>${voice.name} (${voice.lang})</option>
</for>
</select>
</div>
<div style="width: 5%; float: right;">
<span class="play-button" on-click("playVoice")>
<i class="fas fa-play"/>
</span>
</div>
</div>
<div class="form-group">
<div style="width: 50%; float: left;">
<label class="control-label">Dual Monitor</label>
Expand All @@ -39,7 +55,7 @@ $ var heading = input.heading || "Add Venue";
<div style="width: 50%;">
<label class="control-label">WLED Lights</label>
<input class="form-control small-checkbox" type="checkbox" checked=state.has_wled_lights on-input("wledLightsChange")/>
</div>
</div>
</div>
<div class="form-group">
<div style="width: 10%; float: left;">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
.small-checkbox {
height: 1.5em;
}

.play-button {
&:hover {
color: #dbdbdb;
}
}
19 changes: 19 additions & 0 deletions src/pages/offices/components/offices/offices.component.js
Original file line number Diff line number Diff line change
@@ -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));
}
}
8 changes: 4 additions & 4 deletions src/pages/offices/components/offices/offices.marko
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<i class="fas fa-edit"/>
<span class="ml-10">Edit</span>
</button>
<new-office-form id=`${modalId}` office=office heading="Edit Office" isAdd=false/>
<new-office-form id=`${modalId}` office=office heading="Edit Office" isAdd=false />
</td>
<td>
<div class="table table-responsive">
Expand All @@ -60,12 +60,12 @@
<td><input type="checkbox" name="LED Lights" checked=venue.config.has_wled_lights disabled></td>
<td><input type="checkbox" name="Smartboard" checked=venue.config.has_smartboard disabled></td>
<td>
$ var modalId = `edit-venue-${venue.id}-modal`;
$ let modalId = `edit-venue-${venue.id}-modal`;
<button class="btn btn-primary" type="button" data-toggle="modal" data-target=`#${modalId}`>
<i class="fas fa-edit"/>
<span class="ml-10">Edit</span>
</button>
<new-venue-form id=`${modalId}` venue=venue offices=input.offices heading="Edit Venue" isAdd=false/>
<new-venue-form id=`${modalId}` venue=venue offices=input.offices heading="Edit Venue" isAdd=false voices=state.voices/>
</td>
</tr>
</if>
Expand All @@ -82,6 +82,6 @@
</div>
<div class='text-center'>
<new-office-form isAdd=true offices=input.offices venues=input.venues />
<new-venue-form isAdd=true offices=input.offices />
<new-venue-form isAdd=true offices=input.offices voices=state.voices/>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ $ var stat2PerLeg = input.stat2PerLeg;
<tr>
<td> (${stat1}) ${stat1PerLeg}</td>
<td>${input.label}</td>
<td>${stat2} (${stat2PerLeg})</td>
<td>${stat2PerLeg} (${stat2})</td>
</tr>
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ module.exports = {
speaker.speak( {text: vocalName } );
});
} else {
speaker.speak( {text: vocalName } );
speaker.speakWithVoice( {text: vocalName }, this.input.ttsVoice );
}
}
},
Expand Down
15 changes: 14 additions & 1 deletion src/pages/players/components/players/players.component.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/players/components/players/players.marko
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<players-table players=state.players offices=input.offices on-edit-player("editPlayer")/>
</div>
<div class='text-center'>
<new-player-form isAdd=true key="add-player-modal" offices=input.offices/>
<new-player-form isAdd=true key="add-player-modal" offices=input.offices ttsVoice=state.ttsVoice/>
</div>
</div>
</else>
2 changes: 1 addition & 1 deletion src/pages/players/players-template.marko
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import Layout from "../layout.marko"

<${Layout}>
<@body>
<players players=input.players offices=input.offices/>
<players players=input.players offices=input.offices venues=input.venues/>
</@body>
</>
11 changes: 8 additions & 3 deletions src/util/socket.io-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down Expand Up @@ -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");
});
}
Expand Down
55 changes: 54 additions & 1 deletion src/util/speaker.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
exports.VOICES = [];

exports.getUtterance = function (data, callback) {
const msg = new SpeechSynthesisUtterance();
msg.volume = 1;
Expand All @@ -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);
};

0 comments on commit e60a690

Please sign in to comment.