├── app ├── .gitkeep └── services │ ├── speech-reader.js │ └── speech-recorder.js ├── addon ├── .gitkeep ├── defaults │ └── recorder.js ├── services │ ├── speech-reader.js │ └── speech-recorder.js └── -privates │ └── reader.js ├── vendor └── .gitkeep ├── tests ├── unit │ ├── .gitkeep │ ├── services │ │ ├── speech-reader-test.js │ │ └── speech-recorder-test.js │ └── -privates │ │ └── reader-test.js ├── integration │ └── .gitkeep ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── components │ │ │ ├── .gitkeep │ │ │ └── speakable-component.js │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── templates │ │ │ ├── components │ │ │ │ ├── .gitkeep │ │ │ │ └── speakable-component.hbs │ │ │ ├── application.hbs │ │ │ └── index.hbs │ │ ├── resolver.js │ │ ├── styles │ │ │ └── app.css │ │ ├── router.js │ │ ├── app.js │ │ └── index.html │ ├── public │ │ ├── robots.txt │ │ ├── images │ │ │ └── GitHub-Mark-32px.png │ │ └── crossdomain.xml │ └── config │ │ └── environment.js ├── test-helper.js ├── helpers │ ├── destroy-app.js │ ├── resolver.js │ ├── start-app.js │ └── module-for-acceptance.js ├── .jshintrc └── index.html ├── assets ├── vendor-d41d8cd98f00b204e9800998ecf8427e.css ├── dummy-ca616ce657a66d12c492b61650ef94c8.css └── dummy-7ed982097601083b311bfc03c8b52a90.js ├── .watchmanconfig ├── robots.txt ├── bower.json ├── .bowerrc ├── index.js ├── config ├── environment.js └── ember-try.js ├── images └── GitHub-Mark-32px-f87561b8bb354ef83b09a66e54f70e08.png ├── .npmignore ├── testem.js ├── .ember-cli ├── .gitignore ├── .editorconfig ├── crossdomain.xml ├── .jshintrc ├── ember-cli-build.js ├── LICENSE.md ├── .travis.yml ├── LICENSE ├── index.html ├── package.json └── README.md /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/vendor-d41d8cd98f00b204e9800998ecf8427e.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-speak", 3 | "dependencies": {} 4 | } 5 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/services/speech-reader.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-speak/services/speech-reader'; 2 | -------------------------------------------------------------------------------- /app/services/speech-recorder.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-speak/services/speech-recorder'; 2 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: 'ember-speak' 6 | }; 7 | -------------------------------------------------------------------------------- /assets/dummy-ca616ce657a66d12c492b61650ef94c8.css: -------------------------------------------------------------------------------- 1 | pre{tab-size:2}.badges{margin-top:20px}.badge{display:inline-block;margin-top:25px} -------------------------------------------------------------------------------- /tests/dummy/public/images/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsteuwer/ember-speak/HEAD/tests/dummy/public/images/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 'use strict'; 3 | 4 | module.exports = function(/* environment, appConfig */) { 5 | return { }; 6 | }; 7 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /images/GitHub-Mark-32px-f87561b8bb354ef83b09a66e54f70e08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsteuwer/ember-speak/HEAD/images/GitHub-Mark-32px-f87561b8bb354ef83b09a66e54f70e08.png -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | pre { 2 | tab-size: 2; 3 | } 4 | 5 | .badges { 6 | margin-top: 20px; 7 | } 8 | 9 | .badge { 10 | display: inline-block; 11 | margin-top: 25px; 12 | } 13 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{!-- The following component displays Ember's default welcome message. --}} 2 | {{welcome-page}} 3 | {{!-- Feel free to remove this! --}} 4 | 5 | {{outlet}} 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .gitignore 11 | .jshintrc 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | "framework": "qunit", 4 | "test_page": "tests/index.html?hidepassed", 5 | "disable_watching": true, 6 | "launch_in_ci": [ 7 | "PhantomJS" 8 | ], 9 | "launch_in_dev": [ 10 | "PhantomJS", 11 | "Chrome" 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | const Router = Ember.Router.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | }); 11 | 12 | export default Router; 13 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log* 17 | testem.log 18 | *.swp 19 | */*.swp 20 | -------------------------------------------------------------------------------- /addon/defaults/recorder.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | computed, 5 | } = Ember; 6 | 7 | export default Ember.Object.extend(Ember.Evented, { 8 | fullTranscript: '', 9 | isAvailable: computed.bool('_available').readOnly(), 10 | isRecording: computed.bool('_recording').readOnly(), 11 | _available: false, 12 | _recording: false, 13 | }); 14 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/speakable-component.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if readingAvailable}} 3 | 4 | {{else}} 5 |
6 | Your browser doesn't support SpeechSynthesis. 7 |
8 | {{/if}} 9 |
10 | {{yield}} 11 | -------------------------------------------------------------------------------- /tests/unit/services/speech-reader-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:speech-reader', 'Unit | Service | speech reader', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['service:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | let service = this.subject(); 11 | assert.ok(service); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/services/speech-recorder-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:speech-recorder', 'Unit | Service | speech recorder', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['service:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | let service = this.subject(); 11 | assert.ok(service); 12 | }); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | let App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import config from '../../config/environment'; 4 | 5 | export default function startApp(attrs) { 6 | let application; 7 | 8 | let attributes = Ember.merge({}, config.APP); 9 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | Ember.run(() => { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esversion": 6, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | var app = new EmberAddon(defaults, { 7 | prepend: 'https://tsteuwer.github.io/ember-speak/' 8 | // Add options here 9 | }); 10 | 11 | /* 12 | This build file specifies the options for the dummy test app of this 13 | addon, located in `/tests/dummy` 14 | This build file does *not* influence how the addon or the app using it 15 | behave. You most likely want to be modifying `./index.js` or app's build file 16 | */ 17 | 18 | return app.toTree(); 19 | }; 20 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { Promise } } = Ember; 7 | 8 | export default function(name, options = {}) { 9 | module(name, { 10 | beforeEach() { 11 | this.application = startApp(); 12 | 13 | if (options.beforeEach) { 14 | return options.beforeEach.apply(this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 20 | return Promise.resolve(afterEach).then(() => destroyApp(this.application)); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /tests/dummy/app/components/speakable-component.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import layout from '../templates/components/speakable-component'; 3 | 4 | export default Ember.Component.extend({ 5 | reader: null, 6 | classNames: ['well', 'well-sm'], 7 | speechReader: Ember.inject.service(), 8 | readingAvailable: Ember.computed.alias('speechReader.isAvailable'), 9 | layout, 10 | actions: { 11 | togglePlaying, 12 | } 13 | }); 14 | 15 | function togglePlaying() { 16 | const reader = this.get('reader'); 17 | if (reader) { 18 | reader.cancel(); 19 | reader.destroy(); 20 | this.set('reader', null); 21 | } else { 22 | const speechReader = this.get('speechReader'); 23 | const newReader = speechReader.getNewReader(this.$().text().trim()); 24 | this.set('reader', newReader); 25 | newReader.play(); 26 | } 27 | } 28 | // END-SNIPPET 29 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | inject: { 5 | service, 6 | }, 7 | } = Ember; 8 | 9 | export default Ember.Route.extend({ 10 | speechRecorder: service(), 11 | speechReader: service(), 12 | model() { 13 | const speechRecorder = this.get('speechRecorder'); 14 | const speechReader = this.get('speechReader'); 15 | speechRecorder.setLanguage('en-US'); 16 | speechReader.setLanguage('en-US'); 17 | 18 | return Ember.Object.create({ 19 | githubLink: 'https://github.com/tsteuwer/ember-speak', 20 | travisBadge: 'https://travis-ci.org/tsteuwer/ember-speak.svg?branch=master', 21 | travisLink: 'https://travis-ci.org/tsteuwer/ember-speak', 22 | npmBadge: 'https://badge.fury.io/js/ember-speak.svg', 23 | npmLink: 'http://badge.fury.io/js/ember-speak', 24 | isAvailable: speechRecorder.get('isAvailable'), 25 | readerAvailable: speechReader.get('isAvailable'), 26 | }); 27 | }, 28 | setupController(controller, model) { 29 | controller.setProperties({model}); 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "location", 6 | "setTimeout", 7 | "$", 8 | "-Promise", 9 | "define", 10 | "console", 11 | "visit", 12 | "exists", 13 | "fillIn", 14 | "click", 15 | "keyEvent", 16 | "triggerEvent", 17 | "find", 18 | "findWithAssert", 19 | "wait", 20 | "DS", 21 | "andThen", 22 | "currentURL", 23 | "currentPath", 24 | "currentRouteName" 25 | ], 26 | "node": false, 27 | "browser": false, 28 | "boss": true, 29 | "curly": true, 30 | "debug": false, 31 | "devel": false, 32 | "eqeqeq": true, 33 | "evil": true, 34 | "forin": false, 35 | "immed": false, 36 | "laxbreak": false, 37 | "newcap": true, 38 | "noarg": true, 39 | "noempty": false, 40 | "nonew": false, 41 | "nomen": false, 42 | "onevar": false, 43 | "plusplus": false, 44 | "regexp": false, 45 | "undef": true, 46 | "sub": true, 47 | "strict": false, 48 | "white": false, 49 | "eqnull": true, 50 | "esversion": 6, 51 | "unused": true 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "4" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - $HOME/.npm 11 | - $HOME/.cache # includes bowers cache 12 | 13 | env: 14 | # we recommend testing LTS's and latest stable release (bonus points to beta/canary) 15 | - EMBER_TRY_SCENARIO=ember-lts-2.4 16 | - EMBER_TRY_SCENARIO=ember-lts-2.8 17 | - EMBER_TRY_SCENARIO=ember-release 18 | - EMBER_TRY_SCENARIO=ember-beta 19 | - EMBER_TRY_SCENARIO=ember-canary 20 | - EMBER_TRY_SCENARIO=ember-default 21 | 22 | matrix: 23 | fast_finish: true 24 | allow_failures: 25 | - env: EMBER_TRY_SCENARIO=ember-canary 26 | 27 | before_install: 28 | - npm config set spin false 29 | - npm install -g bower phantomjs-prebuilt 30 | - bower --version 31 | - phantomjs --version 32 | 33 | install: 34 | - npm install 35 | - bower install 36 | 37 | script: 38 | # Usually, it's ok to finish the test scenario without reverting 39 | # to the addon's original dependency state, skipping "cleanup". 40 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Troy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | 18 | 19 | {{content-for "body"}} 20 | 21 | 22 | 23 | 24 | {{content-for "body-footer"}} 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'dummy', 6 | environment: environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | } 44 | 45 | if (environment === 'production') { 46 | ENV.locationType = 'hash'; 47 | ENV.rootURL = '/ember-speak/'; 48 | 49 | } 50 | 51 | return ENV; 52 | }; 53 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ember Speak - Text-to-Speech and Speech-to-Text (STT) for Ember 7 | 8 | 9 | 10 | 11 | {{content-for "head"}} 12 | {{content-for "test-head"}} 13 | 14 | 15 | 16 | 17 | 18 | {{content-for "head-footer"}} 19 | {{content-for "test-head-footer"}} 20 | 21 | 22 | {{content-for "body"}} 23 | {{content-for "test-body"}} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{content-for "body-footer"}} 32 | {{content-for "test-body-footer"}} 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | inject: { 5 | service, 6 | }, 7 | } = Ember; 8 | 9 | export default Ember.Controller.extend({ 10 | speechRecorder: service(), 11 | speechReader: service(), 12 | actions: { 13 | record, 14 | reset, 15 | stop, 16 | read, 17 | pause, 18 | resume, 19 | }, 20 | }); 21 | 22 | function record() { 23 | this.send('stop'); 24 | this.send('reset'); 25 | 26 | const model = this.get('model'); 27 | const speechRecorder = this.get('speechRecorder'); 28 | const recorder = speechRecorder.getRecorder(); 29 | 30 | recorder.on('transcribed', (text) => { 31 | model.set('transcript', model.get('transcript') + text); 32 | }); 33 | recorder.on('error', (err) => { 34 | model.set('error', err.msg); 35 | }); 36 | 37 | recorder.start(); 38 | 39 | model.set('recorder', recorder); 40 | } 41 | 42 | function reset() { 43 | this.get('model').setProperties({ 44 | confidence: '', 45 | transcript: '', 46 | error: '', 47 | }); 48 | } 49 | 50 | function stop() { 51 | const recorder = this.get('model.recorder'); 52 | if (recorder) { 53 | recorder.stop(); 54 | } 55 | } 56 | 57 | function read() { 58 | const currentReader = this.get('model.reader'); 59 | if (currentReader) { 60 | console.log('destroying'); 61 | currentReader.destroy(); 62 | } 63 | 64 | const speechReader = this.get('speechReader'); 65 | const reader = speechReader.getNewReader(this.get('model.textToRead')); 66 | 67 | this.set('model.reader', reader); 68 | reader.play(); 69 | } 70 | 71 | function pause() { 72 | this.get('model.reader').pause(); 73 | } 74 | 75 | function resume() { 76 | this.get('model.reader').resume(); 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-speak", 3 | "version": "0.1.1", 4 | "description": "Add text-to-speech (TTS) or speech-to-text (STT) to your Ember app.", 5 | "keywords": [ 6 | "ember-addon", 7 | "text-to-speech", 8 | "speech-to-text", 9 | "TTS", 10 | "STT", 11 | "speech" 12 | ], 13 | "license": "MIT", 14 | "author": "Troy Steuwer", 15 | "demoUURL": "https://tsteuwer.github.io/ember-speak/", 16 | "directories": { 17 | "doc": "doc", 18 | "test": "tests" 19 | }, 20 | "repository": "https://github.com/tsteuwer/ember-speak", 21 | "scripts": { 22 | "build": "ember build", 23 | "start": "ember server", 24 | "test": "ember try:each" 25 | }, 26 | "dependencies": { 27 | "ember-cli-babel": "^5.1.7" 28 | }, 29 | "devDependencies": { 30 | "broccoli-asset-rev": "^2.4.5", 31 | "ember-ajax": "^2.4.1", 32 | "ember-cli": "2.11.0", 33 | "ember-cli-app-version": "^2.0.0", 34 | "ember-cli-dependency-checker": "^1.3.0", 35 | "ember-cli-github-pages": "0.1.2", 36 | "ember-cli-htmlbars": "^1.1.1", 37 | "ember-cli-htmlbars-inline-precompile": "^0.3.3", 38 | "ember-cli-inject-live-reload": "^1.4.1", 39 | "ember-cli-jshint": "^2.0.1", 40 | "ember-cli-qunit": "^3.0.1", 41 | "ember-cli-release": "^0.2.9", 42 | "ember-cli-shims": "^1.0.2", 43 | "ember-cli-sri": "^2.1.0", 44 | "ember-cli-test-loader": "^1.1.0", 45 | "ember-cli-uglify": "^1.2.0", 46 | "ember-data": "^2.11.0", 47 | "ember-disable-prototype-extensions": "^1.1.0", 48 | "ember-export-application-global": "^1.0.5", 49 | "ember-load-initializers": "^0.6.0", 50 | "ember-resolver": "^2.0.3", 51 | "ember-source": "^2.11.0", 52 | "loader.js": "^4.0.10" 53 | }, 54 | "engines": { 55 | "node": ">= 0.12.0" 56 | }, 57 | "ember-addon": { 58 | "configPath": "tests/dummy/config" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | scenarios: [ 4 | { 5 | name: 'ember-lts-2.4', 6 | bower: { 7 | dependencies: { 8 | 'ember': 'components/ember#lts-2-4' 9 | }, 10 | resolutions: { 11 | 'ember': 'lts-2-4' 12 | } 13 | }, 14 | npm: { 15 | devDependencies: { 16 | 'ember-source': null 17 | } 18 | } 19 | }, 20 | { 21 | name: 'ember-lts-2.8', 22 | bower: { 23 | dependencies: { 24 | 'ember': 'components/ember#lts-2-8' 25 | }, 26 | resolutions: { 27 | 'ember': 'lts-2-8' 28 | } 29 | }, 30 | npm: { 31 | devDependencies: { 32 | 'ember-source': null 33 | } 34 | } 35 | }, 36 | { 37 | name: 'ember-release', 38 | bower: { 39 | dependencies: { 40 | 'ember': 'components/ember#release' 41 | }, 42 | resolutions: { 43 | 'ember': 'release' 44 | } 45 | }, 46 | npm: { 47 | devDependencies: { 48 | 'ember-source': null 49 | } 50 | } 51 | }, 52 | { 53 | name: 'ember-beta', 54 | bower: { 55 | dependencies: { 56 | 'ember': 'components/ember#beta' 57 | }, 58 | resolutions: { 59 | 'ember': 'beta' 60 | } 61 | }, 62 | npm: { 63 | devDependencies: { 64 | 'ember-source': null 65 | } 66 | } 67 | }, 68 | { 69 | name: 'ember-canary', 70 | bower: { 71 | dependencies: { 72 | 'ember': 'components/ember#canary' 73 | }, 74 | resolutions: { 75 | 'ember': 'canary' 76 | } 77 | }, 78 | npm: { 79 | devDependencies: { 80 | 'ember-source': null 81 | } 82 | } 83 | }, 84 | { 85 | name: 'ember-default', 86 | npm: { 87 | devDependencies: {} 88 | } 89 | } 90 | ] 91 | }; 92 | -------------------------------------------------------------------------------- /addon/services/speech-reader.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Reader from 'ember-speak/-privates/reader'; 3 | 4 | const { 5 | assert, 6 | computed, 7 | } = Ember; 8 | 9 | export default Ember.Service.extend({ 10 | isAvailable: computed.and('_UtterAPI', '_SynthAPI').readOnly(), 11 | _lang: 'en-US', 12 | init, 13 | getNewReader, 14 | setLanguage, 15 | _getNewUtterance, 16 | }); 17 | 18 | const ERROR_PREFIX = '[SpeechReader] '; 19 | 20 | /** 21 | * Initializer function. Basically just sets the API if available. 22 | * @public 23 | * @overrides 24 | * @memberOf {SpeechReader} 25 | * @return {undefined} 26 | */ 27 | function init() { 28 | this._super(...arguments); 29 | this.setProperties({ 30 | _UtterAPI: window.SpeechSynthesisUtterance, 31 | _SynthAPI: window.speechSynthesis, 32 | }); 33 | } 34 | 35 | /** 36 | * Returns a new reader instance. 37 | * @public 38 | * @memberOf {SpeechReader} 39 | * @return {Reader} 40 | */ 41 | function getNewReader(text = '') { 42 | assert(text, `${ERROR_PREFIX} must be a valid string`); 43 | 44 | if (!this.get('isAvailable')) { 45 | return Ember.Object.create(); 46 | } 47 | 48 | return Reader.create({ 49 | _synth: this.get('_SynthAPI'), 50 | _utterance: this._getNewUtterance(text), 51 | }); 52 | } 53 | 54 | /** 55 | * Returns a new SpeechSynthesisUtterance instance with the desired text 56 | * @public 57 | * @memberOf {SpeechReader} 58 | * @return {SpeechSynthesisUtterance} 59 | */ 60 | function _getNewUtterance(text) { 61 | const Utterance = this.get('_UtterAPI'); 62 | const utter = new Utterance(text); 63 | utter.lang = this.get('_lang'); 64 | 65 | return utter; 66 | } 67 | 68 | /** 69 | * Set the desired language. 70 | * @public 71 | * @memberOf {SpeechReader} 72 | * @param {String} lang A valid BCP 47 language tag 73 | * Note: If a falsy value is provided it will default to en-US 74 | * @return {undefined} 75 | */ 76 | function setLanguage(lang) { 77 | assert(lang, `${ERROR_PREFIX} Language must provide a valid language`); 78 | this.set('_lang', lang || 'en-US'); 79 | } 80 | -------------------------------------------------------------------------------- /addon/services/speech-recorder.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Recorder from 'ember-speak/defaults/recorder'; 3 | 4 | const { 5 | assert, 6 | computed, 7 | } = Ember; 8 | 9 | /** 10 | * Default export. 11 | * @public 12 | * @example 13 | * Inject it into your component, route, controller or whatever. 14 | * 15 | * const speechRecognizer = this.get('speechRecgonizer'); 16 | * speechRecognizer.setLanugage('en-US'); 17 | * speechRecognizer.record().then(result => { 18 | * // success! 19 | * alert(result.transcript); 20 | * }).catch(err => { 21 | * console.log(err.msg); 22 | * }); 23 | */ 24 | export default Ember.Service.extend({ 25 | _API: null, 26 | _lang: 'en-US', 27 | _recording: false, 28 | isAvailable: computed.bool('_API').readOnly(), 29 | isRecording: computed.bool('_recording').readOnly(), 30 | init, 31 | getRecorder, 32 | setLanguage, 33 | _getNewRecorder, 34 | }); 35 | 36 | const ERROR_PREFIX = '[SpeechRecognizer] '; 37 | 38 | /** 39 | * Initializer function. Basically just sets the API if available. 40 | * @public 41 | * @return {undefined} 42 | * @overrides 43 | */ 44 | function init() { 45 | this._super(...arguments); 46 | this.set('_API', window.SpeechRecognition || window.webkitSpeechRecognition); 47 | } 48 | 49 | /** 50 | * Returns a new recorder object. 51 | * @public 52 | * @return {Recorder} 53 | * @example 54 | * 55 | * if (!server.get('isAvailable')) { 56 | * return; 57 | * } 58 | * 59 | * const recorder = service.getRecorder(); 60 | * recorder.on('transcribed', text => { 61 | * console.log(text); 62 | * }).on('error', error => { 63 | * console.debug(error); 64 | * }); 65 | * recorder.start(); 66 | * 67 | * In your templates, there is a computed property you can watch to see if its recording... 68 | * 69 | * {{if model.recorder.isRecording "Recording" "Record"}} 70 | */ 71 | function getRecorder() { 72 | const isAvailable = this.get('isAvailable'); 73 | 74 | if (!isAvailable) { 75 | return Recorder.create(); 76 | } 77 | 78 | const recognizer = this._getNewRecorder(); 79 | const recorder = Recorder.create({ 80 | _available: true, 81 | start() { 82 | recognizer.start(); 83 | }, 84 | stop() { 85 | recognizer.stop(); 86 | }, 87 | restart() { 88 | this.set('fullTranscript', ''); 89 | }, 90 | }); 91 | 92 | recognizer.onresult = (event) => { 93 | let text = ''; 94 | for (let i = event.resultIndex; i < event.results.length; i++) { 95 | if (event.results[i].isFinal) { 96 | text += event.results[i][0].transcript; 97 | } 98 | } 99 | 100 | recorder.trigger('transcribed', text); 101 | recorder.set('fullTranscript', recorder.get('fullTranscript') + text); 102 | }; 103 | 104 | recognizer.onerror = event => { 105 | recognizer.stop(); 106 | recorder.trigger('error', { 107 | msg: `${ERROR_PREFIX} ${event.error}`, 108 | event, 109 | }); 110 | }; 111 | 112 | recognizer.addEventListener('start', () => { 113 | recorder.set('_recording', true); 114 | }); 115 | 116 | recognizer.addEventListener('end', () => { 117 | recorder.set('_recording', false); 118 | }); 119 | 120 | return recorder; 121 | } 122 | 123 | /** 124 | * Set the desired language. 125 | * @public 126 | * @return {undefined} 127 | * @param {String} lang A valid BCP 47 language tag 128 | * Note: If a falsy value is provided it will default to en-US 129 | */ 130 | function setLanguage(lang) { 131 | assert(lang, `${ERROR_PREFIX} Language must provide a valid language`); 132 | this.set('_lang', lang || 'en-US'); 133 | } 134 | 135 | /** 136 | * Returns a new recorder object. This must only be called if its actually available. Otherwise, kabooom! 137 | * @private 138 | * @return {WebkitSpeechRecognition|SpeechRecognition} 139 | */ 140 | function _getNewRecorder() { 141 | const API = this.get('_API'), 142 | recorder = new API(); 143 | 144 | recorder.interimResults = false; 145 | recorder.lang = this.get('_lang'); 146 | recorder.continuous = true; 147 | 148 | return recorder; 149 | } 150 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 |
13 |

Ember Speak

14 |
15 |
16 | Speech-to-Text Example 17 |
18 |
19 | {{#unless model.isAvailable}} 20 |
21 | SpeechRecognition is not available in your browser. 22 |
23 | {{else}} 24 |
25 |
26 |
27 | 30 | 33 |
34 |
35 |
36 |
37 | Transcript 38 |
39 | {{model.transcript}} 40 | {{#if model.error}} 41 |
42 | Error: {{model.error}} 43 |
44 | {{/if}} 45 |
46 |
47 | {{/unless}} 48 |
49 |
50 |
51 |
52 | Text-to-Speech Example 53 |
54 |
55 | {{#unless model.readerAvailable}} 56 |
57 | SpeechUtterance is not available in your browser. 58 |
59 | {{else}} 60 |
61 |
62 | {{textarea class="form-control" value=model.textToRead placeholder="Enter some text here..."}} 63 |
64 |
65 |
66 | 69 | 72 | 75 |
76 |
77 |
78 | {{/unless}} 79 |
80 |
81 |
82 |
83 | Installation 84 |
85 |
86 |
87 |

Install the addon.

88 |
ember install ember-speak
89 |

Inject the service(s) into whichever object you need it in.

90 |
import Ember from 'ember';
 91 | 
 92 | export default Route.Controller.extend({
 93 | 	speechRecorder: Ember.inject.service(),
 94 | 	speechReader: Ember.inject.service(),
 95 | 	model() {
 96 | 		return Ember.Object.create({
 97 | 			recorderAvailable: this.get('speechRecorder.isAvailable'),
 98 | 			readerAvailable: this.get('speechReader.isAvailable'),
 99 | 		});
100 | 	},
101 | });
102 |
103 |
104 |
105 |
106 |
107 | Example: Read Contents Component 108 |
109 |
110 |

One thing I wish sites did were to allow their articles to be read by my phone. This is now as simple as creating a single component. So, while I'm on my mobile device, I can hit play and let it read the entire thing to me while I drive. Here's an example:

111 | speakable-component: JavaScript, Template 112 |
113 | {{#speakable-component}} 114 | Dolphins are a widely distributed and diverse group of aquatic mammals. They are an informal grouping within the order Cetacea, excluding whales and porpoises, so to zoologists the grouping is paraphyletic. The dolphins comprise the extant families Delphinidae (the oceanic dolphins), Platanistidae (the Indian river dolphins), Iniidae (the new world river dolphins), and Pontoporiidae (the brackish dolphins), and the extinct Lipotidae (baiji or Chinese river dolphin). There are 40 extant species of dolphins. Dolphins, alongside other cetaceans, belong to the clade Cetartiodactyla with even-toed ungulates. Their closest living relatives are the hippopotamuses, having diverged about 40 million years ago. 115 |
116 | From Wikipedia 117 | {{/speakable-component}} 118 |
119 |
120 |
121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-speak 2 | 3 | [![Build Status][build-status-img]][build-status-link] 4 | [![NPM][npm-badge-img]][npm-badge-link] 5 | [![Ember Observer Score][ember-observer-badge]][ember-observer-url] 6 | 7 | Add speech-to-text (STT) and text-to-speech (TTS) to your Ember app. 8 | 9 | ## Demo 10 | 11 | A demo is setup over at [https://tsteuwer.github.io/ember-speak/](https://tsteuwer.github.io/ember-speak/). 12 | 13 | ## Browser Support 14 | 15 | [Can I Use?](http://caniuse.com/#feat=speech-synthesis) 16 | 17 | Most browsers have pretty decent support for the [SpeechSynthesis](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis) API. However, there are some things to note in regards to Chrome -- specifically in regards to [this](https://bugs.chromium.org/p/chromium/issues/detail?id=369472) and [this](https://bugs.chromium.org/p/chromium/issues/detail?id=335907). 18 | 19 | **TLDR** 20 | Chrome has a bug where speech would stop reading after about 15 seconds or 200-300 characters. I found a way around this by periodically pausing and resuming the ``speechSynthesis`` instance. This makes it read 100% of the text. 21 | 22 | However, I also found another bug in Chrome which stops all sound from occuring if you pause the utterance for more than 15 seconds. It basically stops reading aloud the text when you resume. This is also filed in those same tickets above. If you do end up using this addon, you may (eek) want to prevent pausing for Chrome users. 23 | 24 | You can also use the ``isAvailable`` computed properties in both ``SpeechRecorder`` and ``SpeechReader`` services. 25 | 26 | ``` 27 | export default Ember.Controller.extend({ 28 | speechReader: Ember.inject.service(), 29 | speechRecorder: Ember.inject.service(), 30 | [...] 31 | init() { 32 | this._super(...arguments); 33 | this.get('model').setProperties({ 34 | readingAvailable: this.get('speechReader.isAvailable'), 35 | recordingAvailable: this.get('speechRecorder.isAvailable'), 36 | }); 37 | }); 38 | ``` 39 | 40 | ## Text-To-Speech (TTS) 41 | 42 | Allow the browser to read text to your users using the [SpeechSynthesis](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis) and [SpeechSynthesisUtterance](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance) APIs. 43 | 44 | 45 | ## Example 46 | 47 | ### Controller 48 | 49 | ``` 50 | export default Ember.Controller.extend({ 51 | speechReader: Ember.inject.service(), 52 | // [...] 53 | actions: { 54 | read(text) { 55 | const currentReader = this.get('model.reader'); 56 | const speechReader = this.get('speechReader'); 57 | 58 | // Always destroy the old reader if used. It will remove events attached to the old utterance since it will be removed from the speechSyntehsis queue 59 | if (currentReader) currentReader.destroy(); 60 | 61 | const reader = speechReader.getNewReader(text); 62 | this.set('model.reader', reader); 63 | 64 | reader.play(); 65 | }, 66 | pause() { 67 | this.get('model.reader').pause(); 68 | }, 69 | resume() { 70 | this.get('model.reader').resume(); 71 | }, 72 | } 73 | }); 74 | ``` 75 | 76 | ### Template 77 | ``` 78 | 81 | {{#if model.reader.isPlaying}} 82 | Playing... 83 | {{else if model.reader.isPaused}} 84 | Paused... 85 | {{/if}} 86 | ``` 87 | 88 | ## Speech-To-Text (STT) 89 | 90 | Allow the browser transcribe what your users are saying via the [SpeechRecognition](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition) API. 91 | 92 | ## Example 93 | 94 | ### Controller 95 | 96 | ``` 97 | export default Ember.Controller.extend({ 98 | speechRecorder: Ember.inject.service(), 99 | // [...] 100 | actions: { 101 | reset() { 102 | this.get('model').setProperties({ 103 | transcript: '', 104 | error: '', 105 | }); 106 | }, 107 | stop() { 108 | const recorder = this.get('model.recorder'); 109 | if (recorder) { 110 | recorder.stop(); 111 | } 112 | }, 113 | record() { 114 | this.send('stop'); 115 | this.send('reset'); 116 | 117 | const model = this.get('model'); 118 | const speechRecorder = this.get('speechRecorder'); 119 | const recorder = speechRecorder.getRecorder(); 120 | 121 | recorder.on('transcribed', (text) => { 122 | model.set('transcript', model.get('transcript') + text); 123 | }); 124 | recorder.on('error', (err) => { 125 | model.set('error', err.msg); 126 | }); 127 | 128 | recorder.start(); 129 | 130 | model.set('recorder', recorder); 131 | }, 132 | } 133 | }); 134 | 135 | ``` 136 | 137 | ### Template 138 | ``` 139 | 142 | {{#if model.recorder.isRecording}} 143 | Recording... 144 | {{/if}} 145 | 146 |
147 | What you've said: {{model.transcript}} 148 | ``` 149 | 150 | ## Addon Maintenance 151 | 152 | ### Installation 153 | 154 | * `git clone ` this repository 155 | * `cd ember-speak` 156 | * `npm install` 157 | * `bower install` 158 | 159 | ### Running 160 | 161 | * `ember serve` 162 | * Visit your app at [http://localhost:4200](http://localhost:4200). 163 | 164 | ### Running Tests 165 | 166 | * `npm test` (Runs `ember try:each` to test your addon against multiple Ember versions) 167 | * `ember test` 168 | * `ember test --server` 169 | 170 | ### Building 171 | 172 | * `ember build` 173 | 174 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 175 | 176 | [build-status-img]: https://travis-ci.org/tsteuwer/ember-speak.svg?branch=master 177 | [build-status-link]: https://travis-ci.org/tsteuwer/ember-speak 178 | [npm-badge-img]: https://badge.fury.io/js/ember-speak.svg 179 | [npm-badge-link]: http://badge.fury.io/js/ember-speak 180 | [ember-observer-badge]: http://emberobserver.com/badges/ember-speak.svg 181 | [ember-observer-url]: http://emberobserver.com/addons/ember-speak 182 | -------------------------------------------------------------------------------- /addon/-privates/reader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PLEASE READ: 3 | * 4 | * There are several issues with Chrome that stop the SpeechSynthesis api from continuing to speak on 5 | * long text. Bascially anything over 14-15 seconds or 200-300 characters of text. For more information, 6 | * please read the issue on Chromes bug list: 7 | * 8 | * https://bugs.chromium.org/p/chromium/issues/detail?id=369472 9 | * 10 | * So, you'll see some console.log's which help this issue. DO NOT REMOVE THEM. I also found that instead 11 | * of using the chunking method described in the gist below, you can set a timer to pause/resume the 12 | * synthesis api to fix the issue. But, it still remains that we need to keep the logging to assure the 13 | * `onend` event fires. 14 | * 15 | * https://gist.github.com/hsed/ef4a2d17f76983588cb6d2a11d4566d6 16 | */ 17 | import Ember from 'ember'; 18 | 19 | const { 20 | computed, 21 | run: { 22 | next, 23 | later, 24 | cancel: cancelTimer, 25 | }, 26 | } = Ember; 27 | 28 | const PAUSE_RESUME_TIMER = 10000; 29 | 30 | export default Ember.Object.extend(Ember.Evented, { 31 | isPaused: computed.bool('_paused').readOnly(), 32 | isPlaying: computed.bool('_playing').readOnly(), 33 | isPlayable: computed.not('_canceled').readOnly(), 34 | _canceled: false, 35 | _paused: false, 36 | _playing: false, 37 | _didPlay: false, 38 | _pauseResumeTimer: null, 39 | _utterance: null, 40 | _synth: null, 41 | init, 42 | play, 43 | pause, 44 | resume, 45 | cancel, 46 | willDestroy, 47 | _clearTimer, 48 | _clearEvents, 49 | _startTimer, 50 | }); 51 | 52 | /** 53 | * Overrides the init method. Here we set the event callbacks with the current context so we can add 54 | * and remove them later during the destroying phase. 55 | * @public 56 | * @overrides 57 | * @memberOf {Reader} 58 | * @return {undefined} 59 | */ 60 | function init() { 61 | this._super(...arguments); 62 | 63 | this.setProperties({ 64 | onEnd: () => { 65 | this._clearTimer(); 66 | this._clearEvents(); 67 | this.setProperties({ 68 | _paused: false, 69 | _playing: false, 70 | }); 71 | }, 72 | onPlay: () => { 73 | this.setProperties({ 74 | _paused: false, 75 | _playing: true, 76 | }); 77 | }, 78 | onError: (event) => { 79 | this.trigger('error', event); 80 | }, 81 | }); 82 | 83 | const utter = this.get('_utterance'); 84 | utter.addEventListener('start', this.get('onPlay')); 85 | utter.addEventListener('resume', this.get('onPlay')); 86 | utter.addEventListener('end', this.get('onEnd')); 87 | utter.addEventListener('error', this.get('onError')); 88 | } 89 | 90 | /** 91 | * Starts playing the desired text. 92 | * @public 93 | * @memberOf {Reader} 94 | * @return {undefined} 95 | */ 96 | function play() { 97 | if (this.get('_didPlay')) { 98 | this.resume(); 99 | return; 100 | } 101 | 102 | this.set('_didPlay', true); 103 | const utter = this.get('_utterance'), 104 | synth = this.get('_synth'); 105 | 106 | synth.cancel(); 107 | 108 | //IMPORTANT!! Do not remove: Logging the object out fixes some onend firing issues. 109 | console.log(utter); 110 | // Placing the speak invocation inside a callback fixes ordering and onend issues 111 | next(() => { 112 | synth.speak(utter); 113 | }); 114 | 115 | this._startTimer(); 116 | } 117 | 118 | /** 119 | * Pauses the reader. 120 | * @public 121 | * @memberOf {Reader} 122 | * @return {undefined} 123 | */ 124 | function pause() { 125 | this._clearTimer(); 126 | this.get('_synth').pause(); 127 | this.setProperties({ 128 | _paused: true, 129 | _playing: false, 130 | }); 131 | } 132 | 133 | /** 134 | * Resumes the reader. 135 | * @public 136 | * @memberOf {Reader} 137 | * @return {undefined} 138 | */ 139 | function resume() { 140 | if (!this.get('_paused')) { 141 | return; 142 | } 143 | 144 | const utter = this.get('_utterance'), 145 | synth = this.get('_synth'); 146 | 147 | this.setProperties({ 148 | _paused: false, 149 | _playing: true, 150 | }); 151 | 152 | //IMPORTANT!! Do not remove: Logging the object out fixes some onend firing issues. 153 | console.log(utter); 154 | // Placing the speak invocation inside a callback fixes ordering and onend issues 155 | next(() => { 156 | synth.resume(); 157 | }); 158 | 159 | this._startTimer(); 160 | } 161 | 162 | /** 163 | * Cancel a reader. 164 | * @public 165 | * @memberOf {Reader} 166 | * @return {undefined} 167 | */ 168 | function cancel() { 169 | this._clearTimer(); 170 | this._clearEvents(); 171 | this.get('_synth').cancel(); 172 | this.setProperties({ 173 | _canceled: true, 174 | _playing: false, 175 | _paused: false, 176 | }); 177 | } 178 | 179 | /** 180 | * Overrides the willDestroy method so we can clear the timer and events so we dont leak them into oblivion. 181 | * @public 182 | * @memberOf {Reader} 183 | * @return {undefined} 184 | */ 185 | function willDestroy() { 186 | this._super(...arguments); 187 | this._clearTimer(); 188 | this._clearEvents(); 189 | } 190 | 191 | /** 192 | * Clear the pause/resume timer. 193 | * @private 194 | * @memberOf {Reader} 195 | * @return {undefined} 196 | */ 197 | function _clearTimer() { 198 | cancelTimer(this.get('_pauseResumeTimer')); 199 | } 200 | 201 | /** 202 | * Remove all events from the SpeechSynthesisUtterance instance. 203 | * @private 204 | * @memberOf {Reader} 205 | * @return {undefined} 206 | */ 207 | function _clearEvents() { 208 | const utter = this.get('_utterance'); 209 | utter.removeEventListener('start', this.get('onPlay')); 210 | utter.removeEventListener('resume', this.get('onPlay')); 211 | utter.removeEventListener('end', this.get('onEnd')); 212 | utter.removeEventListener('error', this.get('onError')); 213 | } 214 | 215 | /** 216 | * Start a timer that will pause/resume the speech synthesis so that it keeps reading long text due 217 | * to the chrome bug (see top of file). 218 | * @private 219 | * @memberOf {Reader} 220 | * @return {undefined} 221 | */ 222 | function _startTimer() { 223 | const synth = this.get('_synth'), 224 | utter = this.get('_utterance'), 225 | that = this; 226 | 227 | this.set('_pauseResumeTimer', later(function pauseResumeTimer() { 228 | that._clearTimer(); 229 | synth.pause(); 230 | 231 | //IMPORTANT!! Do not remove: Logging the object out fixes some onend firing issues. 232 | console.log(utter); 233 | // Placing the speak invocation inside a callback fixes ordering and onend issues 234 | next(() => { 235 | synth.resume(); 236 | }); 237 | 238 | that.set('_pauseResumeTimer', later(pauseResumeTimer, PAUSE_RESUME_TIMER)); 239 | }, PAUSE_RESUME_TIMER)); 240 | } 241 | -------------------------------------------------------------------------------- /tests/unit/-privates/reader-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import Reader from 'ember-speak/-privates/reader'; 3 | import Ember from 'ember'; 4 | 5 | const { 6 | run, 7 | } = Ember; 8 | 9 | let reader; 10 | 11 | module('-privates/reader Unit | Reader', { 12 | // Specify the other units that are required for this test. 13 | beforeEach() { 14 | reader = Reader.create({ 15 | init(){}, 16 | }); 17 | }, 18 | }); 19 | 20 | // Replace this with your real tests. 21 | test('init sets bound functions `onPlay`, `onEnd`, and `onError`', function(assert) { 22 | assert.expect(3); 23 | 24 | reader = Reader.create({ 25 | _utterance: { 26 | addEventListener(){} 27 | }, 28 | }); 29 | 30 | assert.ok(typeof reader.get('onPlay') === 'function', 'it should create the `onPlay` callback'); 31 | assert.ok(typeof reader.get('onEnd') === 'function', 'it should create the `onEnd` callback'); 32 | assert.ok(typeof reader.get('onError') === 'function', 'it should create the `onError` callback'); 33 | }); 34 | 35 | test('init adds event listeners to the SpeechSynthesisUtterance instance', function(assert) { 36 | assert.expect(8); 37 | 38 | const eventsAdded = { 39 | start: false, 40 | resume: false, 41 | end: false, 42 | error: false, 43 | }; 44 | 45 | reader = Reader.create({ 46 | _utterance: { 47 | addEventListener(type, callback) { 48 | eventsAdded[type] = true; 49 | assert.equal(typeof callback, 'function', 'it should have a callback'); 50 | } 51 | }, 52 | }); 53 | 54 | assert.ok(eventsAdded.start, 'the `onstart` event was added'); 55 | assert.ok(eventsAdded.resume, 'the `onresume` event was added'); 56 | assert.ok(eventsAdded.end, 'the `onend` event was added'); 57 | assert.ok(eventsAdded.error, 'the `onerror` event was added'); 58 | }); 59 | 60 | test('play should call resume if already played before', function(assert) { 61 | assert.expect(1); 62 | 63 | reader = Reader.create({ 64 | init(){}, 65 | _utterance: {}, 66 | _synth: { 67 | speak: () => { 68 | assert.ok(false, 'this function should never be called'); 69 | }, 70 | }, 71 | resume() { 72 | assert.ok(true, 'it should call resume instead of the `speechSynthesis`s `speak` method'); 73 | } 74 | }); 75 | 76 | reader.set('_didPlay', true); 77 | reader.play(); 78 | }); 79 | 80 | test('play should cancel any current `speechSynthesis` speeches, speak the new utterance, and start the timer', function(assert) { 81 | const done = assert.async(); 82 | 83 | assert.expect(4); 84 | 85 | reader = Reader.create({ 86 | init(){}, 87 | _utterance: { 88 | text: 'testing', 89 | }, 90 | _synth: { 91 | cancel () { 92 | assert.ok(true, 'cancel should be called'); 93 | }, 94 | speak(utter) { 95 | assert.deepEqual(utter, { text: 'testing' }, 'speak should be called with the same utterance'); 96 | }, 97 | }, 98 | _startTimer() { 99 | assert.ok(true, 'start timer was called'); 100 | } 101 | }); 102 | 103 | reader.play(); 104 | 105 | assert.ok(reader.get('_didPlay'), 'it should set the `didPlay` property to true'); 106 | 107 | run.later(done, 10); 108 | }); 109 | 110 | test('pause should clear the timer, pause the `speechSynthesis` and set the correct status properties', function(assert) { 111 | assert.expect(4); 112 | 113 | reader = Reader.create({ 114 | init(){}, 115 | _synth: { 116 | pause () { 117 | assert.ok(true, 'pause should be called'); 118 | }, 119 | }, 120 | _clearTimer() { 121 | assert.ok(true, 'start timer was called'); 122 | }, 123 | }); 124 | 125 | reader.pause(); 126 | 127 | assert.ok(reader.get('_paused'), '`paused` status should be true'); 128 | assert.notOk(reader.get('_playing'), '`playing` status should be false'); 129 | }); 130 | 131 | test('resume should not continue unless its `paused`', function(assert) { 132 | assert.expect(0); 133 | 134 | reader = Reader.create({ 135 | init(){}, 136 | _synth: { 137 | resume () { 138 | assert.ok(false, 'resume was called'); 139 | }, 140 | }, 141 | _startTimer() { 142 | assert.ok(false, 'start timer was called'); 143 | }, 144 | }); 145 | 146 | reader.set('_paused', false); 147 | reader.resume(); 148 | }); 149 | 150 | test('resume should resume the utterance if paused, and start the timer, and update the status properties', function(assert) { 151 | const done = assert.async(); 152 | 153 | assert.expect(4); 154 | 155 | reader = Reader.create({ 156 | init(){}, 157 | _utterance: { 158 | text: 'testing', 159 | }, 160 | _synth: { 161 | resume() { 162 | assert.ok(true, 'resume called'); 163 | }, 164 | }, 165 | _startTimer() { 166 | assert.ok(true, 'start timer was called'); 167 | } 168 | }); 169 | 170 | reader.set('_paused', true); 171 | reader.resume(); 172 | 173 | assert.notOk(reader.get('_paused'), 'it should set the `_paused` property to false'); 174 | assert.ok(reader.get('_playing'), 'it should set the `_playing` property to true'); 175 | 176 | run.later(done, 10); 177 | }); 178 | 179 | test('cancel should clear the timer, events, cancel the `speechSynthesis` and set the propert status properties', function(assert) { 180 | assert.expect(6); 181 | 182 | reader = Reader.create({ 183 | init(){}, 184 | _synth: { 185 | cancel() { 186 | assert.ok(true, 'cancel called'); 187 | }, 188 | }, 189 | _clearTimer() { 190 | assert.ok(true, 'clear timer was called'); 191 | }, 192 | _clearEvents() { 193 | assert.ok(true, 'clear events was called'); 194 | } 195 | }); 196 | 197 | reader.cancel(); 198 | 199 | assert.notOk(reader.get('_paused'), 'it should set the `_paused` property to false'); 200 | assert.notOk(reader.get('_playing'), 'it should set the `_playing` property to false'); 201 | assert.ok(reader.get('_canceled'), 'it should set the `_canceled` property to true'); 202 | }); 203 | 204 | test('willDestroy should clear the timer and events', function(assert) { 205 | assert.expect(2); 206 | 207 | reader = Reader.create({ 208 | init(){}, 209 | _clearTimer() { 210 | assert.ok(true, 'clear timer called'); 211 | }, 212 | _clearEvents() { 213 | assert.ok(true, 'clear events called'); 214 | }, 215 | }); 216 | 217 | reader.willDestroy(); 218 | }); 219 | 220 | test('_clearEvents removes event listeners on the SpeechSynthesisUtterance instance', function(assert) { 221 | assert.expect(8); 222 | 223 | const eventsRemoved = { 224 | start: false, 225 | resume: false, 226 | end: false, 227 | error: false, 228 | }; 229 | 230 | reader = Reader.create({ 231 | _utterance: { 232 | addEventListener(){}, 233 | removeEventListener(type, callback) { 234 | eventsRemoved[type] = true; 235 | assert.equal(typeof callback, 'function', 'it should have the callback to be removed'); 236 | } 237 | }, 238 | }); 239 | 240 | reader._clearEvents(); 241 | assert.ok(eventsRemoved.start, 'the `onstart` event was removed'); 242 | assert.ok(eventsRemoved.resume, 'the `onresume` event was removed'); 243 | assert.ok(eventsRemoved.end, 'the `onend` event was removed'); 244 | assert.ok(eventsRemoved.error, 'the `onerror` event was removed'); 245 | }); 246 | 247 | test('_startTimer should set a timer that pauses and resumes the `speechSynthesis` api every 10 seconds', function(assert) { 248 | const done = assert.async(); 249 | 250 | assert.expect(3); 251 | 252 | reader = Reader.create({ 253 | init(){}, 254 | _utterance: {}, 255 | _synth: { 256 | pause() { 257 | assert.ok(true, 'paused called'); 258 | }, 259 | resume() { 260 | assert.ok(true, 'resume called'); 261 | } 262 | }, 263 | _clearTimer() { 264 | assert.ok(true, 'clear timer called'); 265 | } 266 | }); 267 | 268 | reader._startTimer(); 269 | 270 | run.later(() => { 271 | run.cancel(reader.get('_pauseResumeTimer')); 272 | done(); 273 | }, 10500); 274 | }); 275 | -------------------------------------------------------------------------------- /assets/dummy-7ed982097601083b311bfc03c8b52a90.js: -------------------------------------------------------------------------------- 1 | "use strict";define("dummy/app",["exports","ember","dummy/resolver","ember-load-initializers","dummy/config/environment"],function(e,t,n,l,a){var r=void 0;t.default.MODEL_FACTORY_INJECTIONS=!0,r=t.default.Application.extend({modulePrefix:a.default.modulePrefix,podModulePrefix:a.default.podModulePrefix,Resolver:n.default}),(0,l.default)(r,a.default.modulePrefix),e.default=r}),define("dummy/controllers/index",["exports","ember"],function(e,t){function n(){this.send("stop"),this.send("reset");var e=this.get("model"),t=this.get("speechRecorder"),n=t.getRecorder();n.on("transcribed",function(t){e.set("transcript",e.get("transcript")+t)}),n.on("error",function(t){e.set("error",t.msg)}),n.start(),e.set("recorder",n)}function l(){this.get("model").setProperties({confidence:"",transcript:"",error:""})}function a(){var e=this.get("model.recorder");e&&e.stop()}function r(){var e=this.get("model.reader");e&&(console.log("destroying"),e.destroy());var t=this.get("speechReader"),n=t.getNewReader(this.get("model.textToRead"));this.set("model.reader",n),n.play()}function s(){this.get("model.reader").pause()}function i(){this.get("model.reader").resume()}var o=t.default.inject.service;e.default=t.default.Controller.extend({speechRecorder:o(),speechReader:o(),actions:{record:n,reset:l,stop:a,read:r,pause:s,resume:i}})}),define("dummy/helpers/app-version",["exports","ember","dummy/config/environment","ember-cli-app-version/utils/regexp"],function(e,t,n,l){function a(e){var t=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return t.hideSha?r.match(l.versionRegExp)[0]:t.hideVersion?r.match(l.shaRegExp)[0]:r}e.appVersion=a;var r=n.default.APP.version;e.default=t.default.Helper.helper(a)}),define("dummy/helpers/pluralize",["exports","ember-inflector/lib/helpers/pluralize"],function(e,t){e.default=t.default}),define("dummy/helpers/singularize",["exports","ember-inflector/lib/helpers/singularize"],function(e,t){e.default=t.default}),define("dummy/initializers/app-version",["exports","ember-cli-app-version/initializer-factory","dummy/config/environment"],function(e,t,n){var l=n.default.APP,a=l.name,r=l.version;e.default={name:"App Version",initialize:(0,t.default)(a,r)}}),define("dummy/initializers/container-debug-adapter",["exports","ember-resolver/container-debug-adapter"],function(e,t){e.default={name:"container-debug-adapter",initialize:function(){var e=arguments[1]||arguments[0];e.register("container-debug-adapter:main",t.default),e.inject("container-debug-adapter:main","namespace","application:main")}}}),define("dummy/initializers/data-adapter",["exports","ember"],function(e,t){e.default={name:"data-adapter",before:"store",initialize:function(){}}}),define("dummy/initializers/ember-data",["exports","ember-data/setup-container","ember-data/-private/core"],function(e,t,n){e.default={name:"ember-data",initialize:t.default}}),define("dummy/initializers/export-application-global",["exports","ember","dummy/config/environment"],function(e,t,n){function l(){var e=arguments[1]||arguments[0];if(n.default.exportApplicationGlobal!==!1){var l;if("undefined"!=typeof window)l=window;else if("undefined"!=typeof global)l=global;else{if("undefined"==typeof self)return;l=self}var a,r=n.default.exportApplicationGlobal;a="string"==typeof r?r:t.default.String.classify(n.default.modulePrefix),l[a]||(l[a]=e,e.reopen({willDestroy:function(){this._super.apply(this,arguments),delete l[a]}}))}}e.initialize=l,e.default={name:"export-application-global",initialize:l}}),define("dummy/initializers/injectStore",["exports","ember"],function(e,t){e.default={name:"injectStore",before:"store",initialize:function(){}}}),define("dummy/initializers/store",["exports","ember"],function(e,t){e.default={name:"store",after:"ember-data",initialize:function(){}}}),define("dummy/initializers/transforms",["exports","ember"],function(e,t){e.default={name:"transforms",before:"store",initialize:function(){}}}),define("dummy/instance-initializers/ember-data",["exports","ember-data/-private/instance-initializers/initialize-store-service"],function(e,t){e.default={name:"ember-data",initialize:t.default}}),define("dummy/resolver",["exports","ember-resolver"],function(e,t){e.default=t.default}),define("dummy/router",["exports","ember","dummy/config/environment"],function(e,t,n){var l=t.default.Router.extend({location:n.default.locationType,rootURL:n.default.rootURL});l.map(function(){}),e.default=l}),define("dummy/routes/index",["exports","ember"],function(e,t){var n=t.default.inject.service;e.default=t.default.Route.extend({speechRecorder:n(),speechReader:n(),model:function(){var e=this.get("speechRecorder"),n=this.get("speechReader");return e.setLanguage("en-US"),n.setLanguage("en-US"),t.default.Object.create({githubLink:"https://github.com/tsteuwer/ember-speak",travisBadge:"https://travis-ci.org/tsteuwer/ember-speak.svg?branch=master",travisLink:"https://travis-ci.org/tsteuwer/ember-speak",npmBadge:"https://badge.fury.io/js/ember-speak.svg",npmLink:"http://badge.fury.io/js/ember-speak",isAvailable:e.get("isAvailable"),readerAvailable:n.get("isAvailable")})},setupController:function(e,t){e.setProperties({model:t})}})}),define("dummy/services/ajax",["exports","ember-ajax/services/ajax"],function(e,t){Object.defineProperty(e,"default",{enumerable:!0,get:function(){return t.default}})}),define("dummy/services/speech-reader",["exports","ember-speak/services/speech-reader"],function(e,t){Object.defineProperty(e,"default",{enumerable:!0,get:function(){return t.default}})}),define("dummy/services/speech-recorder",["exports","ember-speak/services/speech-recorder"],function(e,t){Object.defineProperty(e,"default",{enumerable:!0,get:function(){return t.default}})}),define("dummy/templates/application",["exports"],function(e){e.default=Ember.HTMLBars.template({id:"AKY2Sa5S",block:'{"statements":[["append",["unknown",["welcome-page"]],false],["text","\\n"],["text","\\n"],["append",["unknown",["outlet"]],false],["text","\\n"]],"locals":[],"named":[],"yields":[],"blocks":[],"hasPartials":false}',meta:{moduleName:"dummy/templates/application.hbs"}})}),define("dummy/templates/index",["exports"],function(e){e.default=Ember.HTMLBars.template({id:"43ZwgJBS",block:'{"statements":[["open-element","div",[]],["static-attr","class","container"],["flush-element"],["text","\\n\\t"],["open-element","div",[]],["static-attr","class","pull-right badges"],["flush-element"],["text","\\n\\t\\t"],["open-element","a",[]],["static-attr","class","top-badge"],["dynamic-attr","href",["unknown",["model","npmLink"]],null],["flush-element"],["text","\\n\\t\\t\\t"],["open-element","img",[]],["dynamic-attr","src",["unknown",["model","npmBadge"]],null],["flush-element"],["close-element"],["text","\\n\\t\\t"],["close-element"],["text","\\n\\t\\t"],["open-element","a",[]],["static-attr","class","top-badge"],["dynamic-attr","href",["unknown",["model","travisLink"]],null],["flush-element"],["text","\\n\\t\\t\\t"],["open-element","img",[]],["dynamic-attr","src",["unknown",["model","travisBadge"]],null],["flush-element"],["close-element"],["text","\\n\\t\\t"],["close-element"],["text","\\n\\t\\t"],["open-element","a",[]],["static-attr","class","top-badge github-logo"],["dynamic-attr","href",["unknown",["model","githubLink"]],null],["flush-element"],["text","\\n\\t\\t\\t"],["open-element","img",[]],["static-attr","width","32px"],["static-attr","height","32px"],["static-attr","src","/images/GitHub-Mark-32px-f87561b8bb354ef83b09a66e54f70e08.png"],["flush-element"],["close-element"],["text","\\n\\t\\t"],["close-element"],["text","\\n\\t"],["close-element"],["text","\\n\\t"],["open-element","h1",[]],["flush-element"],["text","Ember Speak"],["close-element"],["text","\\n\\t"],["open-element","div",[]],["static-attr","class","panel panel-default"],["flush-element"],["text","\\n\\t\\t"],["open-element","div",[]],["static-attr","class","panel-heading"],["flush-element"],["text","\\n\\t\\t\\t"],["open-element","strong",[]],["flush-element"],["text","Speech-to-Text Example"],["close-element"],["text","\\n\\t\\t"],["close-element"],["text","\\n\\t\\t"],["open-element","div",[]],["static-attr","class","panel-body"],["flush-element"],["text","\\n"],["block",["unless"],[["get",["model","isAvailable"]]],null,4,3],["text","\\t\\t"],["close-element"],["text","\\n\\t"],["close-element"],["text","\\n\\t"],["open-element","div",[]],["static-attr","class","panel panel-default"],["flush-element"],["text","\\n\\t\\t"],["open-element","div",[]],["static-attr","class","panel-heading"],["flush-element"],["text","\\n\\t\\t\\t"],["open-element","strong",[]],["flush-element"],["text","Text-to-Speech Example"],["close-element"],["text","\\n\\t\\t"],["close-element"],["text","\\n\\t\\t"],["open-element","div",[]],["static-attr","class","panel-body"],["flush-element"],["text","\\n"],["block",["unless"],[["get",["model","readerAvailable"]]],null,1,0],["text","\\t\\t"],["close-element"],["text","\\n\\t"],["close-element"],["text","\\n\\t"],["open-element","div",[]],["static-attr","class","panel panel-default"],["flush-element"],["text","\\n\\t\\t"],["open-element","div",[]],["static-attr","class","panel-heading"],["flush-element"],["text","\\n\\t\\t\\t"],["open-element","strong",[]],["flush-element"],["text","Installation"],["close-element"],["text","\\n\\t\\t"],["close-element"],["text","\\n\\t\\t"],["open-element","div",[]],["static-attr","class","panel-body"],["flush-element"],["text","\\n\\t\\t\\t"],["open-element","div",[]],["flush-element"],["text","\\n\\t\\t\\t\\t"],["open-element","p",[]],["flush-element"],["text","Install the addon."],["close-element"],["text","\\n"],["open-element","pre",[]],["flush-element"],["text","ember install ember-speak"],["close-element"],["text","\\n\\t\\t\\t\\t"],["open-element","p",[]],["flush-element"],["text","Inject the service(s) into whichever object you need it in."],["close-element"],["text","\\n"],["open-element","pre",[]],["flush-element"],["text","import Ember from \'ember\';\\n\\nexport default Route.Controller.extend({\\n\\tspeechRecorder: Ember.inject.service(),\\n\\tspeechReader: Ember.inject.service(),\\n\\tmodel() {\\n\\t\\treturn Ember.Object.create({\\n\\t\\t\\trecorderAvailable: this.get(\'speechRecorder.isAvailable\'),\\n\\t\\t\\treaderAvailable: this.get(\'speechReader.isAvailable\'),\\n\\t\\t});\\n\\t},\\n});"],["close-element"],["text","\\n\\t\\t\\t"],["close-element"],["text","\\n\\t\\t"],["close-element"],["text","\\n\\t"],["close-element"],["text","\\n"],["close-element"],["text","\\n"]],"locals":[],"named":[],"yields":[],"blocks":[{"statements":[["text","\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","row"],["flush-element"],["text","\\n\\t\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","col-sm-8 col-md-9"],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t"],["append",["helper",["textarea"],null,[["class","value","placeholder"],["form-control",["get",["model","textToRead"]],"Enter some text here..."]]],false],["text","\\n\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","col-sm-4 col-md-3"],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","btn-group"],["static-attr","role","group"],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t"],["open-element","button",[]],["static-attr","class","btn btn-success"],["modifier",["action"],[["get",[null]],"read"]],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t\\tRead\\n\\t\\t\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t"],["open-element","button",[]],["static-attr","class","btn btn-danger"],["dynamic-attr","disabled",["helper",["unless"],[["get",["model","reader","isPlaying"]],"disabled"],null],null],["modifier",["action"],[["get",[null]],"pause"]],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t\\tPause\\n\\t\\t\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t"],["open-element","button",[]],["static-attr","class","btn"],["dynamic-attr","disabled",["helper",["unless"],[["get",["model","reader","isPaused"]],"disabled"],null],null],["modifier",["action"],[["get",[null]],"resume"]],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t\\tResume\\n\\t\\t\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t"],["close-element"],["text","\\n"]],"locals":[]},{"statements":[["text","\\t\\t\\t\\t"],["open-element","div",[]],["flush-element"],["text","\\n\\t\\t\\t\\t\\tSpeechUtterance is not available in your browser.\\n\\t\\t\\t\\t"],["close-element"],["text","\\n"]],"locals":[]},{"statements":[["text","\\t\\t\\t\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","alert alert-warning"],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t\\tError: "],["append",["unknown",["model","error"]],false],["text","\\n\\t\\t\\t\\t\\t\\t\\t"],["close-element"],["text","\\n"]],"locals":[]},{"statements":[["text","\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","row"],["flush-element"],["text","\\n\\t\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","col-sm-4 col-md-3 col-lg-2"],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","btn-group"],["static-attr","role","group"],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t"],["open-element","button",[]],["static-attr","class","btn btn-success"],["dynamic-attr","disabled",["helper",["if"],[["get",["model","recorder","isRecording"]],"disabled"],null],null],["modifier",["action"],[["get",[null]],"record"]],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t\\tRecord\\t\\n\\t\\t\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t"],["open-element","button",[]],["static-attr","class","btn btn-danger"],["dynamic-attr","disabled",["helper",["unless"],[["get",["model","recorder","isRecording"]],"disabled"],null],null],["modifier",["action"],[["get",[null]],"stop"]],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\t\\t\\tStop\\n\\t\\t\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t\\t\\t"],["open-element","hr",[]],["static-attr","class","visible-xs"],["flush-element"],["close-element"],["text","\\n\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","col-sm-8 col-md-9 col-lg-10"],["flush-element"],["text","\\n\\t\\t\\t\\t\\t\\tTranscript\\n\\t\\t\\t\\t\\t\\t"],["open-element","hr",[]],["flush-element"],["close-element"],["text","\\n\\t\\t\\t\\t\\t\\t"],["append",["unknown",["model","transcript"]],false],["text","\\n"],["block",["if"],[["get",["model","error"]]],null,2],["text","\\t\\t\\t\\t\\t"],["close-element"],["text","\\n\\t\\t\\t\\t"],["close-element"],["text","\\n"]],"locals":[]},{"statements":[["text","\\t\\t\\t\\t"],["open-element","div",[]],["static-attr","class","alert alert-danger"],["flush-element"],["text","\\n\\t\\t\\t\\t\\tSpeechRecognition is not available in your browser.\\n\\t\\t\\t\\t"],["close-element"],["text","\\n"]],"locals":[]}],"hasPartials":false}',meta:{moduleName:"dummy/templates/index.hbs"}})}),define("dummy/config/environment",["ember"],function(e){try{var t=document.querySelector('meta[name="dummy/config/environment"]').getAttribute("content"),n=JSON.parse(unescape(t)),l={default:n};return Object.defineProperty(l,"__esModule",{value:!0}),l}catch(e){throw new Error('Could not read config from meta tag with name "dummy/config/environment".')}}),runningTests||require("dummy/app").default.create({name:"ember-speak",version:"0.1.1"}); --------------------------------------------------------------------------------