Skip to content

Commit 4b2833c

Browse files
authored
Merge pull request #189 from chughts/ttsupdate
TTS Dynamic Voice Selection
2 parents ea9a73d + f639ac2 commit 4b2833c

File tree

3 files changed

+306
-54
lines changed

3 files changed

+306
-54
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Node-RED Watson Nodes for IBM Bluemix
88
<a href="https://cla-assistant.io/watson-developer-cloud/node-red-node-watson"><img src="https://cla-assistant.io/readme/badge/watson-developer-cloud/node-red-node-watson" alt="CLA assistant" /></a>
99

1010
### New in version 0.4.14
11+
- The dialog for the Test to Speech service now loads available voices dynamically. This allows
12+
new voices and languages to be identified without requiring a further code change.
1113

1214
### New in version 0.4.13
1315
- Emergency fix for Watson Language Translation node. Bluemix credentials read too late.

services/text_to_speech/v1.html

+266-47
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
<i class="fa fa-question-circle"></i><b> Please wait: </b> Checking for bound service credentials...
2121
</div>
2222
</div>
23+
24+
<div>
25+
<label id="node-label-message"><i class="fa fa-exclamation-triangle"></i></label>
26+
</div>
27+
2328
<div class="form-row">
2429
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
2530
<input type="text" id="node-input-name" placeholder="Name">
@@ -35,28 +40,19 @@
3540
<div class="form-row">
3641
<label for="node-input-lang"><i class="fa fa-language"></i> Language</label>
3742
<select type="text" id="node-input-lang" style="display: inline-block; width: 70%;" >
38-
<option value="english">English</option>
39-
<option value="german">German</option>
40-
<option value="french">French</option>
41-
<option value="spanish">Spanish</option>
42-
<option value="italian">Italian</option>
43+
4344
</select>
4445
</div>
46+
<div class="form-row">
47+
<input type="hidden" id="node-input-langhidden"/>
48+
</div>
4549
<div class="form-row">
4650
<label for="node-input-voice"><i class="fa fa-comment"></i> Voice</label>
4751
<select type="text" id="node-input-voice" style="display: inline-block; width: 70%;">
48-
<option class="english" value="en-US_MichaelVoice">Michael (Male)</option>
49-
<option class="english" value="en-US_LisaVoice">Lisa (Female)</option>
50-
<option class="english" value="en-US_AllisonVoice">Allison (Female)</option>
51-
<option class="spanish" value="es-ES_EnriqueVoice">Enrique (Male)</option>
52-
<option class="spanish" value="es-ES_LauraVoice">Laura (Female)</option>
53-
<option class="spanish" value="es-US_SofiaVoice">Sofia (Female)</option>
54-
<option class="french" value="fr-FR_ReneeVoice">Renee(Female)</option>
55-
<option class="italian" value="it-IT_FrancescaVoice">Francesca (Female)</option>
56-
<option class="english" value="en-GB_KateVoice">Kate (Female)</option>
57-
<option class="german" value="de-DE_DieterVoice">Dieter (Male)</option>
58-
<option class="german" value="de-DE_BirgitVoice">Birgit (Female)</option>
59-
</select>
52+
</select>
53+
</div>
54+
<div class="form-row">
55+
<input type="hidden" id="node-input-voicehidden"/>
6056
</div>
6157
<div class="form-row">
6258
<label for="node-input-format"><i class="fa fa-file-audio-o"></i> Format</label>
@@ -71,14 +67,7 @@
7167
<script type="text/x-red" data-help-name="watson-text-to-speech">
7268
<p>The Text To Speech service understands text and natural language to generate synthesized audio output complete
7369
with appropriate cadence and intonation.</p>
74-
<p>You can choose from eleven different voices from three languages:</b>.</p>
75-
<ul>
76-
<li><b>English</b> Male and Female.</li>
77-
<li><b>Spanish</b> Male and Female.</li>
78-
<li><b>German</b> Male and Female.</li>
79-
<li><b>French</b> Female.</li>
80-
<li><b>Italian</b> Female.</li>
81-
</ul>
70+
<p>You can choose different voices for a range of languages:</b>.</p>
8271
<p>The text to be converted should be passed in on <code>msg.payload</code>.</p>
8372
<p><b>The source text must be in the language which matches the chosen voice, i.e. you cannot choose to a Spanish
8473
voice with English text.</b>.</p>
@@ -87,18 +76,264 @@
8776
</script>
8877

8978
<script type="text/javascript">
79+
80+
// Need to simulate a namespace, so that some of the variables don't leak across nodes
81+
function TTS () {
82+
}
83+
84+
// This is the namespace for tts.
85+
var tts = new TTS();
86+
//tts.language_selected = '';
87+
//tts.voice_selected = '';
88+
tts.LANGUAGES = { 'en-US': 'US English',
89+
'en-GB': 'UK English',
90+
'pt-BR': 'Portuguese Brazilian',
91+
'fr-FR': 'French',
92+
'it-IT': 'Italian',
93+
'de-DE': 'German',
94+
'zh-CN': 'Mandarin',
95+
'es-ES': 'Spanish',
96+
'es-US': 'US Spanish',
97+
'ar-AR': 'Arablic',
98+
'ja-JP': 'Japanese'
99+
};
100+
101+
// Called to complete the languages selection table
102+
function processLanguages() {
103+
if (!tts.languages && tts.voices) {
104+
tts.languages = tts.voices.map(function(m) {
105+
return m.language;
106+
});
107+
}
108+
if (tts.languages) {
109+
$('select#node-input-lang').empty();
110+
var unique_langs = tts.languages.filter(onlyUnique);
111+
112+
unique_langs.forEach(function(l) {
113+
var selectedText = '';
114+
if (tts.language_selected === l) {
115+
selectedText = 'selected="selected"';
116+
}
117+
$('select#node-input-lang')
118+
.append('<option value='
119+
+ '"' + l + '"'
120+
+ selectedText
121+
+ '>'
122+
+ (tts.LANGUAGES[l] ? tts.LANGUAGES[l] : l)
123+
+ '</option>');
124+
});
125+
126+
}
127+
}
128+
129+
// Populate the Voices selection field
130+
function populateVoices() {
131+
if (!tts.voicenames && tts.voices) {
132+
tts.voicenames = tts.voices.map(function(m) {
133+
//return m.name.split('_')[1];
134+
return m.name;
135+
});
136+
var unique_voices = tts.voicenames.filter(onlyUnique);
137+
tts.voicenames = unique_voices;
138+
}
139+
if (!tts.voicedata && tts.voicenames){
140+
tts.voicedata = [];
141+
tts.voicenames.forEach(function(a){
142+
var element = {};
143+
var bits = a.split('_');
144+
element.full = a;
145+
element.language = bits[0];
146+
element.person = bits[1].replace('Voice','');;
147+
tts.voicedata.push(element);
148+
});
149+
}
150+
151+
if (tts.voicedata) {
152+
$('select#node-input-voice').empty();
153+
154+
tts.voicedata.forEach(function(b) {
155+
var selectedText = '';
156+
if (tts.voice_selected === b.full) {
157+
selectedText = 'selected="selected"';
158+
}
159+
if (tts.language_selected === b.language) {
160+
$('select#node-input-voice')
161+
.append('<option value='
162+
+ '"' + b.full + '"'
163+
+ selectedText
164+
+ '>'
165+
+ b.person
166+
+ '</option>');
167+
}
168+
});
169+
}
170+
}
171+
172+
173+
// Called to work through the voices, completing the dyanmic selection fields.
174+
function processVoices() {
175+
if (tts.voices) {
176+
processLanguages();
177+
populateVoices();
178+
}
179+
}
180+
181+
function visibilityCheck()
182+
{
183+
if (tts.voices) {
184+
$('label#node-label-message').parent().hide();
185+
$('select#node-input-lang').parent().show();
186+
$('select#node-input-voice').parent().show();
187+
} else {
188+
$('label#node-label-message').parent().hide();
189+
$('select#node-input-lang').parent().hide();
190+
$('select#node-input-voice').parent().hide();
191+
}
192+
193+
}
194+
195+
// Function called when either when the voices have been retrieved, or
196+
// on dialog load, if the voices has already been retrieved
197+
function postVoiceCheck(){
198+
processVoices();
199+
visibilityCheck();
200+
}
201+
202+
// Retrieve the available voices from the server, if data is returned, then
203+
// can enable the dynamic selection fields.
204+
function getVoices(){
205+
var u = $('#node-input-username').val();
206+
var p = $('#node-input-password').val();
207+
208+
$.getJSON('watson-text-to-speech/voices/', {un: u, pwd: p}).done(function (data) {
209+
if (data.error) {
210+
$('label#node-label-message').parent().show();
211+
$('label#node-label-message').text(data.error);
212+
} else if (data.voices) {
213+
tts.voices = data.voices;
214+
postVoiceCheck();
215+
}
216+
}).fail(function (err) {
217+
$('label#node-label-message').parent().show();
218+
$('label#node-label-message').text('Error trying to determine available service voices');
219+
220+
}).always(function () {});
221+
}
222+
223+
224+
// The dynamic nature of the selection fields in this node has caused problems.
225+
// Whenever there is a fetch for the models, on a page refresh or applicaiton
226+
// restart, the settings for the dynamic fields are lost.
227+
// So hidden (text) fields are being used to squirrel away the values, so that
228+
// they can be restored.
229+
function restoreFromHidden() {
230+
tts.language_selected = $('#node-input-langhidden').val();
231+
$('select#node-input-lang').val(tts.language_selected);
232+
233+
tts.voice_selected = $('#node-input-voicehidden').val();
234+
$('select#node-input-voice').val(tts.voice_selected);
235+
}
236+
237+
// Simple check that is only invoked if the service is not bound into bluemix. In this case the
238+
// user has to provide credentials. Once there are credentials, then the tts.voices are retrieved.
239+
function checkCredentials() {
240+
var u = $('#node-input-username').val();
241+
var p = $('#node-input-password').val();
242+
243+
if (u && u.length && p) {
244+
if (!tts.voices) {
245+
getVoices();
246+
}
247+
}
248+
}
249+
250+
// Language Setting has changed, modofy voice options appropriately
251+
function checkLanguage(){
252+
//var lang = $('#node-input-lang').val();
253+
//$('#node-input-voice option.' + lang).show();
254+
//$('#node-input-voice option:not(.' + lang + ')').hide();
255+
//var first = $('#node-input-voice option.' + lang + ':first').val();
256+
//$('#node-input-voice').val(first);
257+
258+
tts.language_selected = $('#node-input-lang').val();
259+
}
260+
261+
// Voice Setting has changed, modofy voice options appropriately
262+
function checkVoice(){
263+
tts.voice_selected = $('#node-input-voice').val();
264+
}
265+
266+
267+
// Register the onchange handlers
268+
function registerHandlers() {
269+
$('#node-input-username').change(function(val){
270+
checkCredentials();
271+
});
272+
$('#node-input-password').change(function(val){
273+
checkCredentials();
274+
});
275+
$('#node-input-lang').change(function () {
276+
checkLanguage();
277+
populateVoices();
278+
});
279+
$('#node-input-voice').change(function () {
280+
checkVoice();
281+
});
282+
283+
}
284+
285+
// Function to be used at the start, as don't want to expose any fields, unless the models are
286+
// available. The models can only be fetched if the credentials are available.
287+
function hideEverything() {
288+
if (!stt.models) {
289+
$('label#node-label-message').parent().hide();
290+
$('select#node-input-lang').parent().hide();
291+
$('select#node-input-voice').parent().hide();
292+
}
293+
}
294+
295+
// This is the on edit prepare function, which will be invoked everytime the dialog
296+
// is shown.
297+
function oneditprepare() {
298+
hideEverything();
299+
restoreFromHidden();
300+
registerHandlers();
301+
302+
$.getJSON('watson-text-to-speech/vcap/')
303+
.done(function (service) {
304+
restoreFromHidden();
305+
$('.credentials').toggle(!service);
306+
if (!tts.voices) {getVoices();}
307+
else {postVoiceCheck();}
308+
})
309+
.fail(function () {
310+
$('.credentials').show();
311+
}).always(function () {
312+
$('#credentials-check').hide();
313+
})
314+
}
315+
316+
// Save the values in the dyanmic lists to the hidden fields.
317+
function oneditsave(){
318+
$('#node-input-langhidden').val(tts.language_selected);
319+
$('#node-input-voicehidden').val(tts.voice_selected);
320+
}
321+
90322
(function() {
91323
RED.nodes.registerType('watson-text-to-speech', {
92324
category: 'IBM Watson',
93325
defaults: {
94326
name: {value: ""},
95-
lang: {value: "english"},
96-
voice: {value: "en-US_MichaelVoice"},
97-
format: {value: "audio/wav"}
327+
lang: {value: ""},
328+
langhidden: {value: ""},
329+
voice: {value: ""},
330+
voicehidden: {value: ""},
331+
format: {value: "audio/wav"},
332+
password: {value: ''}
98333
},
99334
credentials: {
100335
username: {type:"text"},
101-
password: {type:"password"}
336+
// password: {type:"password"} - // Taken out because, was not being restored on dialog open.
102337
},
103338
color: "rgb(140, 198, 63)",
104339
inputs: 1,
@@ -111,24 +346,8 @@
111346
labelStyle: function() {
112347
return this.name ? "node_label_italic" : "";
113348
},
114-
oneditprepare: function() {
115-
$('#node-input-lang').change(function () {
116-
var lang = $('#node-input-lang').val();
117-
$('#node-input-voice option.' + lang).show();
118-
$('#node-input-voice option:not(.' + lang + ')').hide();
119-
var first = $('#node-input-voice option.' + lang + ':first').val();
120-
$('#node-input-voice').val(first);
121-
})
122-
$.getJSON('watson-text-to-speech/vcap/')
123-
.done(function (service) {
124-
$('.credentials').toggle(!service);
125-
})
126-
.fail(function () {
127-
$('.credentials').show();
128-
}).always(function () {
129-
$('#credentials-check').hide();
130-
})
131-
}
349+
oneditsave: oneditsave,
350+
oneditprepare: oneditprepare
132351
});
133352
})();
134353
</script>

0 commit comments

Comments
 (0)