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); 695