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.
10 var updateDependencyFunctions = [];
12 var emergencyStop = false;
15 return document.getElementById(id);
18 function isErrorEvent(evt) {
19 return (evt.type == 'error' ||
20 evt.type == 'interrupted' ||
21 evt.type == 'cancelled');
24 function logEvent(callTime, testRunName, evt) {
25 var elapsed = ((new Date() - callTime) / 1000).toFixed(3);
26 while (elapsed.length < 7) {
27 elapsed = ' ' + elapsed;
29 console.log(elapsed + ' ' + testRunName + ': ' + JSON.stringify(evt));
32 function logSpeakCall(utterance, options, callback) {
34 for (var key in options) {
35 if (key != 'onEvent') {
36 optionsCopy[key] = options[key];
39 console.log('Calling chrome.tts.speak(\'' +
41 JSON.stringify(optionsCopy) + ')');
43 chrome.tts.speak(utterance, options, callback);
45 chrome.tts.speak(utterance, options);
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.',
56 run: function(testRunName, voiceName, callback) {
57 var callTime = new Date();
61 logSpeakCall('Alpha Bravo Charlie Delta Echo', {
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.');
72 } else if (evt.type == 'end') {
73 if (startTime == undefined) {
74 errors.push('Error: no "start" event received!');
77 if (evt.charIndex != 30) {
78 errors.push('Error: end event should have a charIndex of 30.');
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.');
87 var delta = (endTime - startTime) / 1000;
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.');
93 callback(errors.length == 0, delta, warnings.concat(errors));
101 description: 'Speaks twice as fast and compares the time to the baseline.',
102 dependencies: ['Baseline'],
104 run: function(testRunName, voiceName, callback) {
105 var callTime = new Date();
108 logSpeakCall('Alpha Bravo Charlie Delta Echo', {
109 voiceName: voiceName,
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.');
128 callback(errors.length == 0, delta, errors);
136 description: 'Speaks twice as slow and compares the time to the baseline.',
137 dependencies: ['Baseline'],
139 run: function(testRunName, voiceName, callback) {
140 var callTime = new Date();
143 logSpeakCall('Alpha Bravo Charlie Delta Echo', {
144 voiceName: voiceName,
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.');
163 callback(errors.length == 0, delta, errors);
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'],
176 run: function(testRunName, voiceName, callback) {
177 var callTime = new Date();
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);
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.');
208 callback(errors.length == 0, delta, errors);
217 description: '<b>Manual</b> test - verify that the volume is lower.',
220 run: function(testRunName, voiceName, callback) {
221 var callTime = new Date();
222 logSpeakCall('Alpha Bravo Charlie Delta Echo', {
223 voiceName: voiceName,
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, []);
238 description: '<b>Manual</b> test - verify that the pitch is ' +
239 'moderately higher, but quite understandable.',
242 run: function(testRunName, voiceName, callback) {
243 var callTime = new Date();
244 logSpeakCall('Alpha Bravo Charlie Delta Echo', {
245 voiceName: voiceName,
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, []);
260 description: '<b>Manual</b> test - verify that the pitch is ' +
261 'moderately lower, but quite understandable.',
264 run: function(testRunName, voiceName, callback) {
265 var callTime = new Date();
266 logSpeakCall('Alpha Bravo Charlie Delta Echo', {
267 voiceName: voiceName,
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, []);
281 name: 'Word and sentence callbacks',
282 description: 'Checks to see if proper word and sentence callbacks ' +
284 dependencies: ['Baseline'],
286 run: function(testRunName, voiceName, callback) {
287 var callTime = new Date();
290 var wordExpected = [{min: 5, max: 6},
299 var sentenceExpected = [{min: 30, max: 32}]
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 + '.');
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.');
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 + '.');
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.');
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.');
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.');
377 if (startTime == undefined) {
378 errors.push('Error: no "start" event received!');
379 startTime = callTime;
381 var endTime = new Date();
382 var delta = (endTime - startTime) / 1000;
384 errors.push('Default speech rate seems too fast.');
385 } else if (delta > 7.0) {
386 errors.push('Default speech rate seems too slow.');
388 callback(errors.length == 0, delta, errors);
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.',
401 run: function(testRunName, voiceName, callback) {
402 var callTime = new Date();
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();
416 logSpeakCall('Bravo bravo.', {
417 voiceName: voiceName,
419 onEvent: function(evt) {
420 logEvent(callTime, testRunName, evt);
421 if (isErrorEvent(evt)) {
422 callback(false, null, []);
426 logSpeakCall('Charlie charlie', {
427 voiceName: voiceName,
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;
438 var endTime = new Date();
439 var delta = (endTime - startTime) / 1000;
440 callback(errors.length == 0, delta, errors);
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'],
455 run: function(testRunName, voiceName, callback) {
456 var callTime = new Date();
460 logSpeakCall('Just when I\'m about to say something interesting,', {
463 logSpeakCall('it seems that I always get interrupted.', {
464 voiceName: voiceName,
467 logSpeakCall('How rude! Will you ever let me finish?', {
468 voiceName: voiceName,
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();
484 logSpeakCall('Bravo bravo.', {
485 voiceName: voiceName,
487 onEvent: function(evt) {
488 logEvent(callTime, testRunName, evt);
489 if (isErrorEvent(evt)) {
490 callback(false, null, []);
494 logSpeakCall('Charlie charlie', {
495 voiceName: voiceName,
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;
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.');
514 callback(errors.length == 0, delta, errors);
523 function updateDependencies() {
524 for (var i = 0; i < updateDependencyFunctions.length; i++) {
525 updateDependencyFunctions[i]();
529 function registerTest(test) {
530 var outer = document.createElement('div');
531 outer.className = 'outer';
532 $('container').appendChild(outer);
534 var buttonWrap = document.createElement('div');
535 buttonWrap.className = 'buttonWrap';
536 outer.appendChild(buttonWrap);
538 var button = document.createElement('button');
539 button.className = 'runTestButton';
540 button.innerText = test.name;
541 buttonWrap.appendChild(button);
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';
549 var description = document.createElement('div');
550 description.className = 'description';
551 description.innerHTML = test.description;
552 outer.appendChild(description);
554 var resultsWrap = document.createElement('div');
555 resultsWrap.className = 'results';
556 outer.appendChild(resultsWrap);
558 for (var j = 0; j < test.trials; j++) {
559 var result = document.createElement('span');
560 resultsWrap.appendChild(result);
561 results.push(result);
563 var avg = document.createElement('span');
564 resultsWrap.appendChild(avg);
566 var messagesWrap = document.createElement('div');
567 messagesWrap.className = 'messages';
568 outer.appendChild(messagesWrap);
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();
589 console.log('Test failed.');
590 var failure = document.createElement('div');
591 failure.className = 'failure';
592 failure.innerText = 'Test failed.';
593 messagesWrap.appendChild(failure);
597 function runTest(index, voiceName) {
599 busy.style.visibility = 'hidden';
600 emergencyStop = false;
603 var testRunName = 'Test run ' + testRunIndex + ', ' +
604 test.name + ', trial ' + (index+1) + ' of ' +
606 console.log('*** Beginning ' + testRunName +
607 ' with voice ' + voiceName);
608 test.run(testRunName, voiceName, function(success, resultTime, errors) {
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);
619 if (resultTime != null) {
620 results[index].className = 'result';
621 results[index].innerText = resultTime.toFixed(3) + ' s';
622 totalTime += resultTime;
625 if (index < test.trials) {
626 runTest(index, voiceName);
633 button.addEventListener('click', function() {
634 var voiceIndex = $('voices').selectedIndex - 1;
635 if (voiceIndex < 0) {
636 alert('Please select a voice first!');
640 busy.style.visibility = 'visible';
643 messagesWrap.innerHTML = '';
644 var voiceName = voiceArray[voiceIndex].voiceName;
645 runTest(0, voiceName);
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';
654 button.disabled = true;
655 outer.className = 'outer disabled';
662 var voice = localStorage['voice'];
663 chrome.tts.getVoices(function(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', '');
671 opt.setAttribute('value', name);
672 opt.innerText = voiceArray[i].voiceName;
673 $('voices').appendChild(opt);
676 $('voices').addEventListener('change', function() {
677 var i = $('voices').selectedIndex;
678 localStorage['voice'] = $('voices').item(i).value;
680 $('stop').addEventListener('click', stop);
682 for (var i = 0; i < tests.length; i++) {
683 registerTest(tests[i]);
685 updateDependencies();
689 console.log('*** Emergency stop!');
690 emergencyStop = true;
694 document.addEventListener('DOMContentLoaded', load);