- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / common / extensions / docs / examples / extensions / ttsdebug / ttsdebug.js
1 /**
2  * Copyright (c) 2011 The Chromium Authors. All rights reserved.
3  * Use of this source code is governed by a BSD-style license that can be
4  * found in the LICENSE file.
5  */
6
7 var voiceArray;
8 var trials = 3;
9 var resultMap = {};
10 var updateDependencyFunctions = [];
11 var testRunIndex = 0;
12 var emergencyStop = false;
13
14 function $(id) {
15   return document.getElementById(id);
16 }
17
18 function isErrorEvent(evt) {
19   return (evt.type == 'error' ||
20           evt.type == 'interrupted' ||
21           evt.type == 'cancelled');
22 }
23
24 function logEvent(callTime, testRunName, evt) {
25   var elapsed = ((new Date() - callTime) / 1000).toFixed(3);
26   while (elapsed.length < 7) {
27     elapsed = ' ' + elapsed;
28   }
29   console.log(elapsed + ' ' + testRunName + ': ' + JSON.stringify(evt));
30 }
31
32 function logSpeakCall(utterance, options, callback) {
33   var optionsCopy = {};
34   for (var key in options) {
35     if (key != 'onEvent') {
36       optionsCopy[key] = options[key];
37     }
38   }
39   console.log('Calling chrome.tts.speak(\'' +
40               utterance + '\', ' +
41               JSON.stringify(optionsCopy) + ')');
42   if (callback)
43     chrome.tts.speak(utterance, options, callback);
44   else
45     chrome.tts.speak(utterance, options);
46 }
47
48 var tests = [
49   {
50     name: 'Baseline',
51     description: 'Ensures that the speech engine sends both start and ' +
52                  'end events, and establishes a baseline time to speak a ' +
53                  'key phrase, to compare other tests against.',
54     dependencies: [],
55     trials: 3,
56     run: function(testRunName, voiceName, callback) {
57       var callTime = new Date();
58       var startTime;
59       var warnings = [];
60       var errors = [];
61       logSpeakCall('Alpha Bravo Charlie Delta Echo', {
62         voiceName: voiceName,
63         onEvent: function(evt) {
64           logEvent(callTime, testRunName, evt);
65           if (isErrorEvent(evt)) {
66             callback(false, null, []);
67           } else if (evt.type == 'start') {
68             startTime = new Date();
69             if (evt.charIndex != 0) {
70               errors.push('Error: start event should have a charIndex of 0.');
71             }
72           } else if (evt.type == 'end') {
73             if (startTime == undefined) {
74               errors.push('Error: no "start" event received!');
75               startTime = callTime;
76             }
77             if (evt.charIndex != 30) {
78               errors.push('Error: end event should have a charIndex of 30.');
79             }
80             var endTime = new Date();
81             if (startTime - callTime > 1000) {
82               var delta = ((startTime - callTime) / 1000).toFixed(3);
83               warnings.push('Note: Delay of ' + delta +
84                             ' before speech started. ' +
85                             'Less than 1.0 s latency is recommended.');
86             }
87             var delta = (endTime - startTime) / 1000;
88             if (delta < 1.0) {
89               warnings.push('Warning: Default speech rate seems too fast.');
90             } else if (delta > 3.0) {
91               warnings.push('Warning: Default speech rate seems too slow.');
92             }
93             callback(errors.length == 0, delta, warnings.concat(errors));
94           }
95         }
96       });
97     }
98   },
99   {
100     name: 'Fast',
101     description: 'Speaks twice as fast and compares the time to the baseline.',
102     dependencies: ['Baseline'],
103     trials: 3,
104     run: function(testRunName, voiceName, callback) {
105       var callTime = new Date();
106       var startTime;
107       var errors = [];
108       logSpeakCall('Alpha Bravo Charlie Delta Echo', {
109         voiceName: voiceName,
110         rate: 2.0,
111         onEvent: function(evt) {
112           logEvent(callTime, testRunName, evt);
113           if (isErrorEvent(evt)) {
114             callback(false, null, []);
115           } else if (evt.type == 'start') {
116             startTime = new Date();
117           } else if (evt.type == 'end') {
118             if (startTime == undefined)
119               startTime = callTime;
120             var endTime = new Date();
121             var delta = (endTime - startTime) / 1000;
122             var relative = delta / resultMap['Baseline'];
123             if (relative < 0.35) {
124               errors.push('2x speech rate seems too fast.');
125             } else if (relative > 0.65) {
126               errors.push('2x speech rate seems too slow.');
127             }
128             callback(errors.length == 0, delta, errors);
129           }
130         }
131       });
132     }
133   },
134   {
135     name: 'Slow',
136     description: 'Speaks twice as slow and compares the time to the baseline.',
137     dependencies: ['Baseline'],
138     trials: 3,
139     run: function(testRunName, voiceName, callback) {
140       var callTime = new Date();
141       var startTime;
142       var errors = [];
143       logSpeakCall('Alpha Bravo Charlie Delta Echo', {
144         voiceName: voiceName,
145         rate: 0.5,
146         onEvent: function(evt) {
147           logEvent(callTime, testRunName, evt);
148           if (isErrorEvent(evt)) {
149             callback(false, null, []);
150           } else if (evt.type == 'start') {
151             startTime = new Date();
152           } else if (evt.type == 'end') {
153             if (startTime == undefined)
154               startTime = callTime;
155             var endTime = new Date();
156             var delta = (endTime - startTime) / 1000;
157             var relative = delta / resultMap['Baseline'];
158             if (relative < 1.6) {
159               errors.push('Half-speed speech rate seems too fast.');
160             } else if (relative > 2.4) {
161               errors.push('Half-speed speech rate seems too slow.');
162             }
163             callback(errors.length == 0, delta, errors);
164           }
165         }
166       });
167     }
168   },
169   {
170     name: 'Interrupt and restart',
171     description: 'Interrupts partway through a long sentence and then ' +
172                  'the baseline utterance, to make sure that speech after ' +
173                  'an interruption works correctly.',
174     dependencies: ['Baseline'],
175     trials: 1,
176     run: function(testRunName, voiceName, callback) {
177       var callTime = new Date();
178       var startTime;
179       var errors = [];
180       logSpeakCall('When in the course of human events it becomes ' +
181                        'necessary for one people to dissolve the political ' +
182                        'bands which have connected them ', {
183         voiceName: voiceName,
184         onEvent: function(evt) {
185           logEvent(callTime, testRunName, evt);
186         }
187       });
188       window.setTimeout(function() {
189         logSpeakCall('Alpha Bravo Charlie Delta Echo', {
190           voiceName: voiceName,
191           onEvent: function(evt) {
192             logEvent(callTime, testRunName, evt);
193             if (isErrorEvent(evt)) {
194               callback(false, null, []);
195             } else if (evt.type == 'start') {
196               startTime = new Date();
197             } else if (evt.type == 'end') {
198               if (startTime == undefined)
199                 startTime = callTime;
200               var endTime = new Date();
201               var delta = (endTime - startTime) / 1000;
202               var relative = delta / resultMap['Baseline'];
203               if (relative < 0.9) {
204                 errors.push('Interrupting speech seems too short.');
205               } else if (relative > 1.1) {
206                 errors.push('Interrupting speech seems too long.');
207               }
208               callback(errors.length == 0, delta, errors);
209             }
210           }
211         });
212       }, 4000);
213     }
214   },
215   {
216     name: 'Low volume',
217     description: '<b>Manual</b> test - verify that the volume is lower.',
218     dependencies: [],
219     trials: 1,
220     run: function(testRunName, voiceName, callback) {
221       var callTime = new Date();
222       logSpeakCall('Alpha Bravo Charlie Delta Echo', {
223         voiceName: voiceName,
224         volume: 0.5,
225         onEvent: function(evt) {
226           logEvent(callTime, testRunName, evt);
227           if (isErrorEvent(evt)) {
228             callback(false, null, []);
229           } else if (evt.type == 'end') {
230             callback(true, null, []);
231           }
232         }
233       });
234     }
235   },
236   {
237     name: 'High pitch',
238     description: '<b>Manual</b> test - verify that the pitch is ' +
239                  'moderately higher, but quite understandable.',
240     dependencies: [],
241     trials: 1,
242     run: function(testRunName, voiceName, callback) {
243       var callTime = new Date();
244       logSpeakCall('Alpha Bravo Charlie Delta Echo', {
245         voiceName: voiceName,
246         pitch: 1.2,
247         onEvent: function(evt) {
248           logEvent(callTime, testRunName, evt);
249           if (isErrorEvent(evt)) {
250             callback(false, null, []);
251           } else if (evt.type == 'end') {
252             callback(true, null, []);
253           }
254         }
255       });
256     }
257   },
258   {
259     name: 'Low pitch',
260     description: '<b>Manual</b> test - verify that the pitch is ' +
261                  'moderately lower, but quite understandable.',
262     dependencies: [],
263     trials: 1,
264     run: function(testRunName, voiceName, callback) {
265       var callTime = new Date();
266       logSpeakCall('Alpha Bravo Charlie Delta Echo', {
267         voiceName: voiceName,
268         pitch: 0.8,
269         onEvent: function(evt) {
270           logEvent(callTime, testRunName, evt);
271           if (isErrorEvent(evt)) {
272             callback(false, null, []);
273           } else if (evt.type == 'end') {
274             callback(true, null, []);
275           }
276         }
277       });
278     }
279   },
280   {
281     name: 'Word and sentence callbacks',
282     description: 'Checks to see if proper word and sentence callbacks ' +
283                  'are received.',
284     dependencies: ['Baseline'],
285     trials: 1,
286     run: function(testRunName, voiceName, callback) {
287       var callTime = new Date();
288       var startTime;
289       var errors = [];
290       var wordExpected = [{min: 5, max: 6},
291                           {min: 11, max: 12},
292                           {min: 19, max: 20},
293                           {min: 25, max: 26},
294                           {min: 30, max: 32},
295                           {min: 37, max: 38},
296                           {min: 43, max: 44},
297                           {min: 51, max: 52},
298                           {min: 57, max: 58}];
299       var sentenceExpected = [{min: 30, max: 32}]
300       var wordCount = 0;
301       var sentenceCount = 0;
302       var lastWordTime = callTime;
303       var lastSentenceTime = callTime;
304       var avgWordTime = resultMap['Baseline'] / 5;
305       logSpeakCall('Alpha Bravo Charlie Delta Echo. ' +
306                        'Alpha Bravo Charlie Delta Echo.', {
307         voiceName: voiceName,
308         onEvent: function(evt) {
309           logEvent(callTime, testRunName, evt);
310           if (isErrorEvent(evt)) {
311             callback(false, null, []);
312           } else if (evt.type == 'start') {
313             startTime = new Date();
314             lastWordTime = startTime;
315             lastSentenceTime = startTime;
316           } else if (evt.type == 'word') {
317             if (evt.charIndex > 0 && evt.charIndex < 62) {
318               var min = wordExpected[wordCount].min;
319               var max = wordExpected[wordCount].max;
320               if (evt.charIndex < min || evt.charIndex > max) {
321                 errors.push('Got word at charIndex ' + evt.charIndex + ', ' +
322                             'was expecting next word callback charIndex ' +
323                             'in the range ' + min + ':' + max + '.');
324               }
325               if (wordCount != 4) {
326                 var delta = (new Date() - lastWordTime) / 1000;
327                 if (delta < 0.6 * avgWordTime) {
328                   errors.push('Word at charIndex ' + evt.charIndex +
329                               ' came after only ' + delta.toFixed(3) +
330                               ' s, which seems too short.');
331                 } else if (delta > 1.3 * avgWordTime) {
332                   errors.push('Word at charIndex ' + evt.charIndex +
333                               ' came after ' + delta.toFixed(3) +
334                               ' s, which seems too long.');
335                 }
336               }
337               wordCount++;
338             }
339             lastWordTime = new Date();
340           } else if (evt.type == 'sentence') {
341             if (evt.charIndex > 0 && evt.charIndex < 62) {
342               var min = sentenceExpected[sentenceCount].min;
343               var max = sentenceExpected[sentenceCount].max;
344               if (evt.charIndex < min || evt.charIndex > max) {
345                 errors.push('Got sentence at charIndex ' + evt.charIndex +
346                             ', was expecting next callback charIndex ' +
347                             'in the range ' + min + ':' + max + '.');
348               }
349               var delta = (new Date() - lastSentenceTime) / 1000;
350               if (delta < 0.75 * resultMap['Baseline']) {
351                 errors.push('Sentence at charIndex ' + evt.charIndex +
352                             ' came after only ' + delta.toFixed(3) +
353                             ' s, which seems too short.');
354               } else if (delta > 1.25 * resultMap['Baseline']) {
355                 errors.push('Sentence at charIndex ' + evt.charIndex +
356                             ' came after ' + delta.toFixed(3) +
357                             ' s, which seems too long.');
358               }
359               sentenceCount++;
360             }
361             lastSentenceTime = new Date();
362           } else if (evt.type == 'end') {
363             if (wordCount == 0) {
364               errors.push('Didn\'t get any word callbacks.');
365             } else if (wordCount < wordExpected.length) {
366               errors.push('Not enough word callbacks.');
367             } else if (wordCount > wordExpected.length) {
368               errors.push('Too many word callbacks.');
369             }
370             if (sentenceCount == 0) {
371               errors.push('Didn\'t get any sentence callbacks.');
372             } else if (sentenceCount < sentenceExpected.length) {
373               errors.push('Not enough sentence callbacks.');
374             } else if (sentenceCount > sentenceExpected.length) {
375               errors.push('Too many sentence callbacks.');
376             }
377             if (startTime == undefined) {
378               errors.push('Error: no "start" event received!');
379               startTime = callTime;
380             }
381             var endTime = new Date();
382             var delta = (endTime - startTime) / 1000;
383             if (delta < 2.5) {
384               errors.push('Default speech rate seems too fast.');
385             } else if (delta > 7.0) {
386               errors.push('Default speech rate seems too slow.');
387             }
388             callback(errors.length == 0, delta, errors);
389           }
390         }
391       });
392     }
393   },
394   {
395     name: 'Baseline Queueing Test',
396     description: 'Establishes a baseline time to speak a ' +
397                  'sequence of three enqueued phrases, to compare ' +
398                  'other tests against.',
399     dependencies: [],
400     trials: 3,
401     run: function(testRunName, voiceName, callback) {
402       var callTime = new Date();
403       var startTime;
404       var errors = [];
405       logSpeakCall('Alpha Alpha', {
406         voiceName: voiceName,
407         onEvent: function(evt) {
408           logEvent(callTime, testRunName, evt);
409           if (isErrorEvent(evt)) {
410             callback(false, null, []);
411           } else if (evt.type == 'start') {
412             startTime = new Date();
413           }
414         }
415       });
416       logSpeakCall('Bravo bravo.', {
417         voiceName: voiceName,
418         enqueue: true,
419         onEvent: function(evt) {
420           logEvent(callTime, testRunName, evt);
421           if (isErrorEvent(evt)) {
422             callback(false, null, []);
423           }
424         }
425       });
426       logSpeakCall('Charlie charlie', {
427         voiceName: voiceName,
428         enqueue: true,
429         onEvent: function(evt) {
430           logEvent(callTime, testRunName, evt);
431           if (isErrorEvent(evt)) {
432             callback(false, null, []);
433           } else if (evt.type == 'end') {
434             if (startTime == undefined) {
435               errors.push('Error: no "start" event received!');
436               startTime = callTime;
437             }
438             var endTime = new Date();
439             var delta = (endTime - startTime) / 1000;
440             callback(errors.length == 0, delta, errors);
441           }
442         }
443       });
444     }
445   },
446   {
447     name: 'Interruption with Queueing',
448     description: 'Queue a sequence of three utterances, then before they ' +
449                  'are finished, interrupt and queue a sequence of three ' +
450                  'more utterances. Make sure that interrupting and ' +
451                  'cancelling the previous utterances doesn\'t interfere ' +
452                  'with the interrupting utterances.',
453     dependencies: ['Baseline Queueing Test'],
454     trials: 1,
455     run: function(testRunName, voiceName, callback) {
456       var callTime = new Date();
457       var startTime;
458       var errors = [];
459
460       logSpeakCall('Just when I\'m about to say something interesting,', {
461         voiceName: voiceName
462       });
463       logSpeakCall('it seems that I always get interrupted.', {
464         voiceName: voiceName,
465         enqueue: true,
466       });
467       logSpeakCall('How rude! Will you ever let me finish?', {
468         voiceName: voiceName,
469         enqueue: true,
470       });
471
472       window.setTimeout(function() {
473         logSpeakCall('Alpha Alpha', {
474           voiceName: voiceName,
475           onEvent: function(evt) {
476             logEvent(callTime, testRunName, evt);
477             if (isErrorEvent(evt)) {
478               callback(false, null, []);
479             } else if (evt.type == 'start') {
480               startTime = new Date();
481             }
482           }
483         });
484         logSpeakCall('Bravo bravo.', {
485           voiceName: voiceName,
486           enqueue: true,
487           onEvent: function(evt) {
488             logEvent(callTime, testRunName, evt);
489             if (isErrorEvent(evt)) {
490               callback(false, null, []);
491             }
492           }
493         });
494         logSpeakCall('Charlie charlie', {
495           voiceName: voiceName,
496           enqueue: true,
497           onEvent: function(evt) {
498             logEvent(callTime, testRunName, evt);
499             if (isErrorEvent(evt)) {
500               callback(false, null, []);
501             } else if (evt.type == 'end') {
502               if (startTime == undefined) {
503                 errors.push('Error: no "start" event received!');
504                 startTime = callTime;
505               }
506               var endTime = new Date();
507               var delta = (endTime - startTime) / 1000;
508               var relative = delta / resultMap['Baseline Queueing Test'];
509               if (relative < 0.9) {
510                 errors.push('Interrupting speech seems too short.');
511               } else if (relative > 1.1) {
512                 errors.push('Interrupting speech seems too long.');
513               }
514               callback(errors.length == 0, delta, errors);
515             }
516           }
517         });
518       }, 4000);
519     }
520   }
521 ];
522
523 function updateDependencies() {
524   for (var i = 0; i < updateDependencyFunctions.length; i++) {
525     updateDependencyFunctions[i]();
526   }
527 }
528
529 function registerTest(test) {
530   var outer = document.createElement('div');
531   outer.className = 'outer';
532   $('container').appendChild(outer);
533
534   var buttonWrap = document.createElement('div');
535   buttonWrap.className = 'buttonWrap';
536   outer.appendChild(buttonWrap);
537
538   var button = document.createElement('button');
539   button.className = 'runTestButton';
540   button.innerText = test.name;
541   buttonWrap.appendChild(button);
542
543   var busy = document.createElement('img');
544   busy.src = 'pacman.gif';
545   busy.alt = 'Busy indicator';
546   buttonWrap.appendChild(busy);
547   busy.style.visibility = 'hidden';
548
549   var description = document.createElement('div');
550   description.className = 'description';
551   description.innerHTML = test.description;
552   outer.appendChild(description);
553
554   var resultsWrap = document.createElement('div');
555   resultsWrap.className = 'results';
556   outer.appendChild(resultsWrap);
557   var results = [];
558   for (var j = 0; j < test.trials; j++) {
559     var result = document.createElement('span');
560     resultsWrap.appendChild(result);
561     results.push(result);
562   }
563   var avg = document.createElement('span');
564   resultsWrap.appendChild(avg);
565
566   var messagesWrap = document.createElement('div');
567   messagesWrap.className = 'messages';
568   outer.appendChild(messagesWrap);
569
570   var totalTime;
571   var successCount;
572
573   function finishTrials() {
574     busy.style.visibility = 'hidden';
575     if (successCount == test.trials) {
576       console.log('Test succeeded.');
577       var success = document.createElement('div');
578       success.className = 'success';
579       success.innerText = 'Test succeeded.';
580       messagesWrap.appendChild(success);
581       if (totalTime > 0.0) {
582         var avgTime = totalTime / test.trials;
583         avg.className = 'result';
584         avg.innerText = 'Avg: ' + avgTime.toFixed(3) + ' s';
585         resultMap[test.name] = avgTime;
586         updateDependencies();
587       }
588     } else {
589       console.log('Test failed.');
590       var failure = document.createElement('div');
591       failure.className = 'failure';
592       failure.innerText = 'Test failed.';
593       messagesWrap.appendChild(failure);
594     }
595   }
596
597   function runTest(index, voiceName) {
598     if (emergencyStop) {
599       busy.style.visibility = 'hidden';
600       emergencyStop = false;
601       return;
602     }
603     var testRunName = 'Test run ' + testRunIndex + ', ' +
604                       test.name + ', trial ' + (index+1) + ' of ' +
605                       test.trials;
606     console.log('*** Beginning ' + testRunName +
607                 ' with voice ' + voiceName);
608     test.run(testRunName, voiceName, function(success, resultTime, errors) {
609       if (success) {
610         successCount++;
611       }
612       for (var i = 0; i < errors.length; i++) {
613         console.log(errors[i]);
614         var error = document.createElement('div');
615         error.className = 'error';
616         error.innerText = errors[i];
617         messagesWrap.appendChild(error);
618       }
619       if (resultTime != null) {
620         results[index].className = 'result';
621         results[index].innerText = resultTime.toFixed(3) + ' s';
622         totalTime += resultTime;
623       }
624       index++;
625       if (index < test.trials) {
626         runTest(index, voiceName);
627       } else {
628         finishTrials();
629       }
630     });
631   }
632
633   button.addEventListener('click', function() {
634     var voiceIndex = $('voices').selectedIndex - 1;
635     if (voiceIndex < 0) {
636       alert('Please select a voice first!');
637       return;
638     }
639     testRunIndex++;
640     busy.style.visibility = 'visible';
641     totalTime = 0.0;
642     successCount = 0;
643     messagesWrap.innerHTML = '';
644     var voiceName = voiceArray[voiceIndex].voiceName;
645     runTest(0, voiceName);
646   }, false);
647
648   updateDependencyFunctions.push(function() {
649     for (var i = 0; i < test.dependencies.length; i++) {
650       if (resultMap[test.dependencies[i]] != undefined) {
651         button.disabled = false;
652         outer.className = 'outer';
653       } else {
654         button.disabled = true;
655         outer.className = 'outer disabled';
656       }
657     }
658   });
659 }
660
661 function load() {
662   var voice = localStorage['voice'];
663   chrome.tts.getVoices(function(va) {
664     voiceArray = va;
665     for (var i = 0; i < voiceArray.length; i++) {
666       var opt = document.createElement('option');
667       var name = voiceArray[i].voiceName;
668       if (name == localStorage['voice']) {
669         opt.setAttribute('selected', '');
670       }
671       opt.setAttribute('value', name);
672       opt.innerText = voiceArray[i].voiceName;
673       $('voices').appendChild(opt);
674     }
675   });
676   $('voices').addEventListener('change', function() {
677     var i = $('voices').selectedIndex;
678     localStorage['voice'] = $('voices').item(i).value;
679   }, false);
680   $('stop').addEventListener('click', stop);
681
682   for (var i = 0; i < tests.length; i++) {
683     registerTest(tests[i]);
684   }
685   updateDependencies();
686 }
687
688 function stop() {
689   console.log('*** Emergency stop!');
690   emergencyStop = true;
691   chrome.tts.stop();
692 }
693
694 document.addEventListener('DOMContentLoaded', load);