diff --git a/README.md b/README.md index b78ec25..2dffb6d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Speech synthesis (tts) for the browser. Wrapping the browser Speech Synthesis AP - splitting sentences into several speeches to make it sound more natural, especially for older versions of Chrome (can be disabled) - throwing specific exceptions: explicit exceptions will be thrown if you pass parameters with incompatible values to any of the methods -Work in Chrome, opera and Safari (including ios8 and ios9 devices). Tested successfully on Ipad and Android. +Work in Chrome, opera and Safari. Tested successfully on Ipad and Android. See browser support here : http://caniuse.com/#feat=speech-synthesis ## Demo @@ -63,7 +63,7 @@ You can pass the following properties to the init function: - voice : the voice to use // default is chosen by your browser if not provided - rate // default 1 - pitch // default 1 -- splitSentences // default true +- splitSentences // default true (or a string with custom sentence-deliminating characters: '.?!' is default) - listeners // object of listeners to attach to the SpeechSynthesis object ```javascript diff --git a/demo/build/demo.bundle.js b/demo/build/demo.bundle.js index 545354a..ab8fe4f 100644 --- a/demo/build/demo.bundle.js +++ b/demo/build/demo.bundle.js @@ -1 +1 @@ -!function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=33)}([function(t,e,n){var r=n(19),o="object"==typeof self&&self&&self.Object===Object&&self,u=r||o||Function("return this")();t.exports=u},function(t,e,n){var r=n(62);t.exports=function(t,e,n){var o=null==t?void 0:r(t,e);return void 0===o?n:o}},function(t,e){t.exports=function(t){return null==t}},function(t,e,n){var r=n(13),o=n(35),u=n(36),i="[object Null]",c="[object Undefined]",a=r?r.toStringTag:void 0;t.exports=function(t){return null==t?void 0===t?c:i:a&&a in Object(t)?o(t):u(t)}},function(t,e,n){var r=n(51),o=n(54);t.exports=function(t,e){var n=o(t,e);return r(n)?n:void 0}},function(t,e){var n=Array.isArray;t.exports=n},function(t,e){t.exports=function(t){return null!=t&&"object"==typeof t}},function(t,e,n){var r=n(4)(Object,"create");t.exports=r},function(t,e,n){var r=n(79);t.exports=function(t,e){for(var n=t.length;n--;)if(r(t[n][0],e))return n;return-1}},function(t,e,n){var r=n(84);t.exports=function(t,e){var n=t.__data__;return r(e)?n["string"==typeof e?"string":"hash"]:n.map}},function(t,e){t.exports=function(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}},function(t,e,n){var r=n(18),o=n(37),u=n(39),i=n(43),c=n(44),a=n(23),s=/^\s+|\s+$/g;t.exports=function(t,e,n){if((t=a(t))&&(n||void 0===e))return t.replace(s,"");if(!t||!(e=r(e)))return t;var f=c(t),p=c(e),l=i(f,p),v=u(f,p)+1;return o(f,l,v).join("")}},function(t,e,n){var r=n(0).isFinite;t.exports=function(t){return"number"==typeof t&&r(t)}},function(t,e,n){var r=n(0).Symbol;t.exports=r},function(t,e,n){var r=n(3),o=n(6),u="[object Symbol]";t.exports=function(t){return"symbol"==typeof t||o(t)&&r(t)==u}},function(t,e,n){var r=n(3),o=n(5),u=n(6),i="[object String]";t.exports=function(t){return"string"==typeof t||!o(t)&&u(t)&&r(t)==i}},function(t,e,n){var r=n(24),o=n(25),u=n(29),i=n(15),c=n(58),a="[object Map]",s="[object Set]";t.exports=function(t){if(null==t)return 0;if(u(t))return i(t)?c(t):t.length;var e=o(t);return e==a||e==s?t.size:r(t).length}},function(t,e,n){var r=n(89)(n(93));t.exports=r},function(t,e,n){var r=n(13),o=n(20),u=n(5),i=n(14),c=1/0,a=r?r.prototype:void 0,s=a?a.toString:void 0;t.exports=function t(e){if("string"==typeof e)return e;if(u(e))return o(e,t)+"";if(i(e))return s?s.call(e):"";var n=e+"";return"0"==n&&1/e==-c?"-0":n}},function(t,e,n){(function(e){var n="object"==typeof e&&e&&e.Object===Object&&e;t.exports=n}).call(this,n(34))},function(t,e){t.exports=function(t,e){for(var n=-1,r=null==t?0:t.length,o=Array(r);++n-1&&t%1==0&&t<=n}},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),t.webpackPolyfill=1),t}},function(t,e){t.exports=function(t){for(var e=-1,n=null==t?0:t.length,r=0,o=[];++e=o?t:r(t,e,n)}},function(t,e){t.exports=function(t,e,n){var r=-1,o=t.length;e<0&&(e=-e>o?0:o+e),(n=n>o?o:n)<0&&(n+=o),o=e>n?0:n-e>>>0,e>>>=0;for(var u=Array(o);++r-1;);return n}},function(t,e){t.exports=function(t,e,n,r){for(var o=t.length,u=n+(r?1:-1);r?u--:++u-1;);return n}},function(t,e,n){var r=n(45),o=n(22),u=n(46);t.exports=function(t){return o(t)?u(t):r(t)}},function(t,e){t.exports=function(t){return t.split("")}},function(t,e){var n="[\\ud800-\\udfff]",r="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",o="\\ud83c[\\udffb-\\udfff]",u="[^\\ud800-\\udfff]",i="(?:\\ud83c[\\udde6-\\uddff]){2}",c="[\\ud800-\\udbff][\\udc00-\\udfff]",a="(?:"+r+"|"+o+")"+"?",s="[\\ufe0e\\ufe0f]?"+a+("(?:\\u200d(?:"+[u,i,c].join("|")+")[\\ufe0e\\ufe0f]?"+a+")*"),f="(?:"+[u+r+"?",r,i,c,n].join("|")+")",p=RegExp(o+"(?="+o+")|"+f+s,"g");t.exports=function(t){return t.match(p)||[]}},function(t,e){var n=Object.prototype;t.exports=function(t){var e=t&&t.constructor;return t===("function"==typeof e&&e.prototype||n)}},function(t,e,n){var r=n(49)(Object.keys,Object);t.exports=r},function(t,e){t.exports=function(t,e){return function(n){return t(e(n))}}},function(t,e,n){var r=n(4)(n(0),"DataView");t.exports=r},function(t,e,n){var r=n(26),o=n(52),u=n(10),i=n(27),c=/^\[object .+?Constructor\]$/,a=Function.prototype,s=Object.prototype,f=a.toString,p=s.hasOwnProperty,l=RegExp("^"+f.call(p).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");t.exports=function(t){return!(!u(t)||o(t))&&(r(t)?l:c).test(i(t))}},function(t,e,n){var r=n(53),o=function(){var t=/[^.]+$/.exec(r&&r.keys&&r.keys.IE_PROTO||"");return t?"Symbol(src)_1."+t:""}();t.exports=function(t){return!!o&&o in t}},function(t,e,n){var r=n(0)["__core-js_shared__"];t.exports=r},function(t,e){t.exports=function(t,e){return null==t?void 0:t[e]}},function(t,e,n){var r=n(4)(n(0),"Promise");t.exports=r},function(t,e,n){var r=n(4)(n(0),"Set");t.exports=r},function(t,e,n){var r=n(4)(n(0),"WeakMap");t.exports=r},function(t,e,n){var r=n(59),o=n(22),u=n(61);t.exports=function(t){return o(t)?u(t):r(t)}},function(t,e,n){var r=n(60)("length");t.exports=r},function(t,e){t.exports=function(t){return function(e){return null==e?void 0:e[t]}}},function(t,e){var n="[\\ud800-\\udfff]",r="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",o="\\ud83c[\\udffb-\\udfff]",u="[^\\ud800-\\udfff]",i="(?:\\ud83c[\\udde6-\\uddff]){2}",c="[\\ud800-\\udbff][\\udc00-\\udfff]",a="(?:"+r+"|"+o+")"+"?",s="[\\ufe0e\\ufe0f]?"+a+("(?:\\u200d(?:"+[u,i,c].join("|")+")[\\ufe0e\\ufe0f]?"+a+")*"),f="(?:"+[u+r+"?",r,i,c,n].join("|")+")",p=RegExp(o+"(?="+o+")|"+f+s,"g");t.exports=function(t){for(var e=p.lastIndex=0;p.test(t);)++e;return e}},function(t,e,n){var r=n(63),o=n(88);t.exports=function(t,e){for(var n=0,u=(e=r(e,t)).length;null!=t&&n-1}},function(t,e,n){var r=n(8);t.exports=function(t,e){var n=this.__data__,o=r(n,t);return o<0?(++this.size,n.push([t,e])):n[o][1]=e,this}},function(t,e,n){var r=n(9);t.exports=function(t){var e=r(this,t).delete(t);return this.size-=e?1:0,e}},function(t,e){t.exports=function(t){var e=typeof t;return"string"==e||"number"==e||"symbol"==e||"boolean"==e?"__proto__"!==t:null===t}},function(t,e,n){var r=n(9);t.exports=function(t){return r(this,t).get(t)}},function(t,e,n){var r=n(9);t.exports=function(t){return r(this,t).has(t)}},function(t,e,n){var r=n(9);t.exports=function(t,e){var n=r(this,t),o=n.size;return n.set(t,e),this.size+=n.size==o?0:1,this}},function(t,e,n){var r=n(14),o=1/0;t.exports=function(t){if("string"==typeof t||r(t))return t;var e=t+"";return"0"==e&&1/t==-o?"-0":e}},function(t,e,n){var r=n(90),o=n(25),u=n(91),i=n(92),c="[object Map]",a="[object Set]";t.exports=function(t){return function(e){var n=o(e);return n==c?u(e):n==a?i(e):r(e,t(e))}}},function(t,e,n){var r=n(20);t.exports=function(t,e){return r(e,function(e){return[e,t[e]]})}},function(t,e){t.exports=function(t){var e=-1,n=Array(t.size);return t.forEach(function(t,r){n[++e]=[r,t]}),n}},function(t,e){t.exports=function(t){var e=-1,n=Array(t.size);return t.forEach(function(t){n[++e]=[t,t]}),n}},function(t,e,n){var r=n(94),o=n(24),u=n(29);t.exports=function(t){return u(t)?r(t):o(t)}},function(t,e,n){var r=n(95),o=n(96),u=n(5),i=n(98),c=n(100),a=n(101),s=Object.prototype.hasOwnProperty;t.exports=function(t,e){var n=u(t),f=!n&&o(t),p=!n&&!f&&i(t),l=!n&&!f&&!p&&a(t),v=n||f||p||l,h=v?r(t.length,String):[],d=h.length;for(var y in t)!e&&!s.call(t,y)||v&&("length"==y||p&&("offset"==y||"parent"==y)||l&&("buffer"==y||"byteLength"==y||"byteOffset"==y)||c(y,d))||h.push(y);return h}},function(t,e){t.exports=function(t,e){for(var n=-1,r=Array(t);++n-1&&t%1==0&&t0?t(n):e()},100)})}},{key:"_loadVoices",value:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10;return this._fetchVoices().catch(function(n){if(0===e)throw n;return t._loadVoices(e-1)})}},{key:"hasBrowserSupport",value:function(){return this.browserSupport}},{key:"setVoice",value:function(t){var e,n=speechSynthesis.getVoices();if(y()(t)&&(e=n.find(function(e){return e.name===t})),h()(t)&&(e=t),!e)throw"Error setting voice. The voice you passed is not valid or the voices have not been loaded yet.";this.synthesisVoice=e}},{key:"setLanguage",value:function(t){if(!function(t){return"string"==typeof t&&m.test(t)}(t=t.replace("_","-")))throw"Error setting language. Please verify your locale is BCP47 format (http://schneegans.de/lv/?tags=es-FR&format=text)";this.lang=t}},{key:"setVolume",value:function(t){if(t=parseFloat(t),!(g()(t)&&t>=0&&t<=1))throw"Error setting volume. Please verify your volume value is a number between 0 and 1.";this.volume=t}},{key:"setRate",value:function(t){if(t=parseFloat(t),!(g()(t)&&t>=0&&t<=10))throw"Error setting rate. Please verify your volume value is a number between 0 and 10.";this.rate=t}},{key:"setPitch",value:function(t){if(t=parseFloat(t),!(g()(t)&&t>=0&&t<=2))throw"Error setting pitch. Please verify your pitch value is a number between 0 and 2.";this.pitch=t}},{key:"setSplitSentences",value:function(t){this.splitSentences=t}},{key:"speak",value:function(t){var e=this;return new Promise(function(n,r){var u=t.text,c=t.listeners,a=void 0===c?{}:c,s=t.queue,p=void 0===s||s,v=o()(u);l()(v)&&n(),!p&&e.cancel();var h=[],d=e.splitSentences?function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return j()(t.replace(/\.+/g,".|").replace(/\?/g,"?|").replace(/\!/g,"!|").split("|").map(function(t){return o()(t)}))}(v):[v];d.forEach(function(t,o){var u=o===i()(d)-1,c=new SpeechSynthesisUtterance;e.synthesisVoice&&(c.voice=e.synthesisVoice),e.lang&&(c.lang=e.lang),e.volume&&(c.volume=e.volume),e.rate&&(c.rate=e.rate),e.pitch&&(c.pitch=e.pitch),c.text=t,f()(a).forEach(function(t){var e=_(t,2),o=e[0],i=e[1];c[o]=function(t){i&&i(t),"onerror"===o&&r({utterances:h,lastUtterance:c,error:t}),"onend"===o&&u&&n({utterances:h,lastUtterance:c})}}),h.push(c),speechSynthesis.speak(c)})})}},{key:"pending",value:function(){return speechSynthesis.pending}},{key:"paused",value:function(){return speechSynthesis.paused}},{key:"speaking",value:function(){return speechSynthesis.speaking}},{key:"pause",value:function(){speechSynthesis.pause()}},{key:"resume",value:function(){speechSynthesis.resume()}},{key:"cancel",value:function(){speechSynthesis.cancel()}}]),t}(),O=function(t){var e=window.document.createElement("div"),n='

Available Voices

',t.forEach((function(e){i+='")})),o.innerHTML=i,window.document.body.appendChild(o),function(e){var n=document.getElementById("play"),t=document.getElementById("pause"),o=document.getElementById("resume"),i=document.getElementById("text"),a=document.getElementById("languages");n.addEventListener("click",(function(){var n=a.value,t=a.options[a.selectedIndex].dataset.name;n&&e.setLanguage(a.value),t&&e.setVoice(t),e.speak({text:i.value,queue:!1,listeners:{onstart:function(){console.log("Start utterance")},onend:function(){console.log("End utterance")},onresume:function(){console.log("Resume utterance")},onboundary:function(e){console.log(e.name+" boundary reached after "+e.elapsedTime+" milliseconds.")}}}).then((function(e){console.log("Success !",e)})).catch((function(e){console.error("An error occurred :",e)}))})),t.addEventListener("click",(function(){e.pause()})),o.addEventListener("click",(function(){e.resume()}))}(e)})).catch((function(e){console.error("An error occured while initializing : ",e)}));var n=e.hasBrowserSupport()?"Hurray, your browser supports speech synthesis":"Your browser does NOT support speech synthesis. Try using Chrome of Safari instead !";document.getElementById("support").innerHTML=n}()}]); \ No newline at end of file diff --git a/lib/ios.js b/lib/ios.js index 4244c5b..676a1d5 100644 --- a/lib/ios.js +++ b/lib/ios.js @@ -3,472 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.iOS9voices = exports.iOS8voices = exports.iOSversion = void 0; +exports.filterIOSVoices = exports.isIos = void 0; -//Fallback cache voices for ios -// iOS 8 -var iOSversion = function iOSversion() { - if (/(iPhone|iPad|iPod)/.test(navigator.platform)) { - var v = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); - return parseInt(v[1], 10); - } - - return false; +var isIos = function isIos() { + return /(iPhone|iPad|iPod)/.test(navigator.platform); }; -exports.iOSversion = iOSversion; -var iOS8voices = [{ - name: "pt-BR", - voiceURI: "pt-BR", - lang: "pt-BR", - localService: true, - default: true -}, { - name: "fr-CA", - voiceURI: "fr-CA", - lang: "fr-CA", - localService: true, - default: true -}, { - name: "sk-SK", - voiceURI: "sk-SK", - lang: "sk-SK", - localService: true, - default: true -}, { - name: "th-TH", - voiceURI: "th-TH", - lang: "th-TH", - localService: true, - default: true -}, { - name: "ro-RO", - voiceURI: "ro-RO", - lang: "ro-RO", - localService: true, - default: true -}, { - name: "no-NO", - voiceURI: "no-NO", - lang: "no-NO", - localService: true, - default: true -}, { - name: "fi-FI", - voiceURI: "fi-FI", - lang: "fi-FI", - localService: true, - default: true -}, { - name: "pl-PL", - voiceURI: "pl-PL", - lang: "pl-PL", - localService: true, - default: true -}, { - name: "de-DE", - voiceURI: "de-DE", - lang: "de-DE", - localService: true, - default: true -}, { - name: "nl-NL", - voiceURI: "nl-NL", - lang: "nl-NL", - localService: true, - default: true -}, { - name: "id-ID", - voiceURI: "id-ID", - lang: "id-ID", - localService: true, - default: true -}, { - name: "tr-TR", - voiceURI: "tr-TR", - lang: "tr-TR", - localService: true, - default: true -}, { - name: "it-IT", - voiceURI: "it-IT", - lang: "it-IT", - localService: true, - default: true -}, { - name: "pt-PT", - voiceURI: "pt-PT", - lang: "pt-PT", - localService: true, - default: true -}, { - name: "fr-FR", - voiceURI: "fr-FR", - lang: "fr-FR", - localService: true, - default: true -}, { - name: "ru-RU", - voiceURI: "ru-RU", - lang: "ru-RU", - localService: true, - default: true -}, { - name: "es-MX", - voiceURI: "es-MX", - lang: "es-MX", - localService: true, - default: true -}, { - name: "zh-HK", - voiceURI: "zh-HK", - lang: "zh-HK", - localService: true, - default: true -}, { - name: "sv-SE", - voiceURI: "sv-SE", - lang: "sv-SE", - localService: true, - default: true -}, { - name: "hu-HU", - voiceURI: "hu-HU", - lang: "hu-HU", - localService: true, - default: true -}, { - name: "zh-TW", - voiceURI: "zh-TW", - lang: "zh-TW", - localService: true, - default: true -}, { - name: "es-ES", - voiceURI: "es-ES", - lang: "es-ES", - localService: true, - default: true -}, { - name: "zh-CN", - voiceURI: "zh-CN", - lang: "zh-CN", - localService: true, - default: true -}, { - name: "nl-BE", - voiceURI: "nl-BE", - lang: "nl-BE", - localService: true, - default: true -}, { - name: "en-GB", - voiceURI: "en-GB", - lang: "en-GB", - localService: true, - default: true -}, { - name: "ar-SA", - voiceURI: "ar-SA", - lang: "ar-SA", - localService: true, - default: true -}, { - name: "ko-KR", - voiceURI: "ko-KR", - lang: "ko-KR", - localService: true, - default: true -}, { - name: "cs-CZ", - voiceURI: "cs-CZ", - lang: "cs-CZ", - localService: true, - default: true -}, { - name: "en-ZA", - voiceURI: "en-ZA", - lang: "en-ZA", - localService: true, - default: true -}, { - name: "en-AU", - voiceURI: "en-AU", - lang: "en-AU", - localService: true, - default: true -}, { - name: "da-DK", - voiceURI: "da-DK", - lang: "da-DK", - localService: true, - default: true -}, { - name: "en-US", - voiceURI: "en-US", - lang: "en-US", - localService: true, - default: true -}, { - name: "en-IE", - voiceURI: "en-IE", - lang: "en-IE", - localService: true, - default: true -}, { - name: "he-IL", - voiceURI: "he-IL", - lang: "he-IL", - localService: true, - default: true -}, { - name: "hi-IN", - voiceURI: "hi-IN", - lang: "hi-IN", - localService: true, - default: true -}, { - name: "el-GR", - voiceURI: "el-GR", - lang: "el-GR", - localService: true, - default: true -}, { - name: "ja-JP", - voiceURI: "ja-JP", - lang: "ja-JP", - localService: true, - default: true -}]; // IOS9 +exports.isIos = isIos; + +var filterIOSVoices = function filterIOSVoices(voices) { + var selectableVoices = ['Maged', 'Zuzana', 'Sara', 'Anna', 'Melina', 'Karen', 'Samantha', 'Daniel', 'Rishi', 'Moira', 'Tessa', 'Mónica', 'Paulina', 'Satu', 'Amélie', 'Thomas', 'Carmit', 'Lekha', 'Mariska', 'Damayanti', 'Alice', 'Kyoko', 'Yuna', 'Ellen', 'Xander', 'Nora', 'Zosia', 'Luciana', 'Joana', 'Ioana', 'Milena', 'Laura', 'Alva', 'Kanya', 'Yelda', 'Tian-Tian', 'Sin-Ji', 'Mei-Jia']; + return voices.filter(function (v) { + return selectableVoices.includes(v.name); + }); +}; -exports.iOS8voices = iOS8voices; -var iOS9voices = [{ - name: "Maged", - voiceURI: "com.apple.ttsbundle.Maged-compact", - lang: "ar-SA", - localService: true, - "default": true -}, { - name: "Zuzana", - voiceURI: "com.apple.ttsbundle.Zuzana-compact", - lang: "cs-CZ", - localService: true, - "default": true -}, { - name: "Sara", - voiceURI: "com.apple.ttsbundle.Sara-compact", - lang: "da-DK", - localService: true, - "default": true -}, { - name: "Anna", - voiceURI: "com.apple.ttsbundle.Anna-compact", - lang: "de-DE", - localService: true, - "default": true -}, { - name: "Melina", - voiceURI: "com.apple.ttsbundle.Melina-compact", - lang: "el-GR", - localService: true, - "default": true -}, { - name: "Karen", - voiceURI: "com.apple.ttsbundle.Karen-compact", - lang: "en-AU", - localService: true, - "default": true -}, { - name: "Daniel", - voiceURI: "com.apple.ttsbundle.Daniel-compact", - lang: "en-GB", - localService: true, - "default": true -}, { - name: "Moira", - voiceURI: "com.apple.ttsbundle.Moira-compact", - lang: "en-IE", - localService: true, - "default": true -}, { - name: "Samantha (Enhanced)", - voiceURI: "com.apple.ttsbundle.Samantha-premium", - lang: "en-US", - localService: true, - "default": true -}, { - name: "Samantha", - voiceURI: "com.apple.ttsbundle.Samantha-compact", - lang: "en-US", - localService: true, - "default": true -}, { - name: "Tessa", - voiceURI: "com.apple.ttsbundle.Tessa-compact", - lang: "en-ZA", - localService: true, - "default": true -}, { - name: "Monica", - voiceURI: "com.apple.ttsbundle.Monica-compact", - lang: "es-ES", - localService: true, - "default": true -}, { - name: "Paulina", - voiceURI: "com.apple.ttsbundle.Paulina-compact", - lang: "es-MX", - localService: true, - "default": true -}, { - name: "Satu", - voiceURI: "com.apple.ttsbundle.Satu-compact", - lang: "fi-FI", - localService: true, - "default": true -}, { - name: "Amelie", - voiceURI: "com.apple.ttsbundle.Amelie-compact", - lang: "fr-CA", - localService: true, - "default": true -}, { - name: "Thomas", - voiceURI: "com.apple.ttsbundle.Thomas-compact", - lang: "fr-FR", - localService: true, - "default": true -}, { - name: "Carmit", - voiceURI: "com.apple.ttsbundle.Carmit-compact", - lang: "he-IL", - localService: true, - "default": true -}, { - name: "Lekha", - voiceURI: "com.apple.ttsbundle.Lekha-compact", - lang: "hi-IN", - localService: true, - "default": true -}, { - name: "Mariska", - voiceURI: "com.apple.ttsbundle.Mariska-compact", - lang: "hu-HU", - localService: true, - "default": true -}, { - name: "Damayanti", - voiceURI: "com.apple.ttsbundle.Damayanti-compact", - lang: "id-ID", - localService: true, - "default": true -}, { - name: "Alice", - voiceURI: "com.apple.ttsbundle.Alice-compact", - lang: "it-IT", - localService: true, - "default": true -}, { - name: "Kyoko", - voiceURI: "com.apple.ttsbundle.Kyoko-compact", - lang: "ja-JP", - localService: true, - "default": true -}, { - name: "Yuna", - voiceURI: "com.apple.ttsbundle.Yuna-compact", - lang: "ko-KR", - localService: true, - "default": true -}, { - name: "Ellen", - voiceURI: "com.apple.ttsbundle.Ellen-compact", - lang: "nl-BE", - localService: true, - "default": true -}, { - name: "Xander", - voiceURI: "com.apple.ttsbundle.Xander-compact", - lang: "nl-NL", - localService: true, - "default": true -}, { - name: "Nora", - voiceURI: "com.apple.ttsbundle.Nora-compact", - lang: "no-NO", - localService: true, - "default": true -}, { - name: "Zosia", - voiceURI: "com.apple.ttsbundle.Zosia-compact", - lang: "pl-PL", - localService: true, - "default": true -}, { - name: "Luciana", - voiceURI: "com.apple.ttsbundle.Luciana-compact", - lang: "pt-BR", - localService: true, - "default": true -}, { - name: "Joana", - voiceURI: "com.apple.ttsbundle.Joana-compact", - lang: "pt-PT", - localService: true, - "default": true -}, { - name: "Ioana", - voiceURI: "com.apple.ttsbundle.Ioana-compact", - lang: "ro-RO", - localService: true, - "default": true -}, { - name: "Milena", - voiceURI: "com.apple.ttsbundle.Milena-compact", - lang: "ru-RU", - localService: true, - "default": true -}, { - name: "Laura", - voiceURI: "com.apple.ttsbundle.Laura-compact", - lang: "sk-SK", - localService: true, - "default": true -}, { - name: "Alva", - voiceURI: "com.apple.ttsbundle.Alva-compact", - lang: "sv-SE", - localService: true, - "default": true -}, { - name: "Kanya", - voiceURI: "com.apple.ttsbundle.Kanya-compact", - lang: "th-TH", - localService: true, - "default": true -}, { - name: "Yelda", - voiceURI: "com.apple.ttsbundle.Yelda-compact", - lang: "tr-TR", - localService: true, - "default": true -}, { - name: "Ting-Ting", - voiceURI: "com.apple.ttsbundle.Ting-Ting-compact", - lang: "zh-CN", - localService: true, - "default": true -}, { - name: "Sin-Ji", - voiceURI: "com.apple.ttsbundle.Sin-Ji-compact", - lang: "zh-HK", - localService: true, - "default": true -}, { - name: "Mei-Jia", - voiceURI: "com.apple.ttsbundle.Mei-Jia-compact", - lang: "zh-TW", - localService: true, - "default": true -}]; -exports.iOS9voices = iOS9voices; \ No newline at end of file +exports.filterIOSVoices = filterIOSVoices; \ No newline at end of file diff --git a/lib/speak-tts.js b/lib/speak-tts.js index 800679f..a8bc799 100644 --- a/lib/speak-tts.js +++ b/lib/speak-tts.js @@ -3,19 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.default = void 0; +exports["default"] = void 0; var _utils = require("./utils"); +var _ios = require("./ios"); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } -var SpeakTTS = -/*#__PURE__*/ -function () { +var SpeakTTS = /*#__PURE__*/function () { function SpeakTTS() { _classCallCheck(this, SpeakTTS); @@ -55,7 +55,13 @@ function () { }); _this._loadVoices().then(function (voices) { - // Handle callback onvoiceschanged by hand + if ((0, _ios.isIos)()) { + // iOS does not allow you to select all of the voices it claims + // to have. You only get one per locale. + voices = (0, _ios.filterIOSVoices)(voices); + } // Handle callback onvoiceschanged by hand + + listeners['onvoiceschanged'] && listeners['onvoiceschanged'](voices); // Initialize values if necessary !(0, _utils.isNil)(lang) && _this.setLanguage(lang); @@ -74,7 +80,7 @@ function () { splitSentences: _this.splitSentences, browserSupport: _this.browserSupport }); - }).catch(function (e) { + })["catch"](function (e) { reject(e); }); }); @@ -100,7 +106,7 @@ function () { var _this2 = this; var remainingAttempts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 10; - return this._fetchVoices().catch(function (error) { + return this._fetchVoices()["catch"](function (error) { if (remainingAttempts === 0) throw error; return _this2._loadVoices(remainingAttempts - 1); }); @@ -180,6 +186,14 @@ function () { }, { key: "setSplitSentences", value: function setSplitSentences(splitSentences) { + if (typeof splitSentences !== 'string' && Boolean(splitSentences)) { + splitSentences = '.?!'; + } + + if (splitSentences.match(/[a-zA-Z]/g)) { + throw 'splitSentences contains invalid characters [a-zA-Z]. Set to true or false, or specify custom punctuation characters to split sentences.'; + } + this.splitSentences = splitSentences; } }, { @@ -199,7 +213,7 @@ function () { !queue && _this3.cancel(); // Split into sentences (for better result and bug with some versions of chrome) var utterances = []; - var sentences = _this3.splitSentences ? (0, _utils.splitSentences)(msg) : [msg]; + var sentences = _this3.splitSentences ? (0, _utils.splitSentences)(msg, _this3.splitSentences) : [msg]; sentences.forEach(function (sentence, index) { var isLast = index === (0, _utils.size)(sentences) - 1; var utterance = new SpeechSynthesisUtterance(); @@ -211,9 +225,9 @@ function () { if (_this3.pitch) utterance.pitch = _this3.pitch; //0 to 2 - utterance.text = sentence; // Attach event listeners + utterance.text = sentence; // Attach specified event listeners, always including onerror and onend to resolve/reject the speak promise - Object.keys(listeners).forEach(function (listener) { + Array.from(new Set(Object.keys(listeners).concat(['onerror', 'onend']))).forEach(function (listener) { var fn = listeners[listener]; var newListener = function newListener(data) { @@ -278,4 +292,4 @@ function () { }(); var _default = SpeakTTS; -exports.default = _default; \ No newline at end of file +exports["default"] = _default; \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js index 65897d4..6c7d8d3 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -7,7 +7,13 @@ exports.trim = exports.isObject = exports.isNil = exports.isNan = exports.size = var splitSentences = function splitSentences() { var text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - return text.replace(/\.+/g, '.|').replace(/\?/g, '?|').replace(/\!/g, '!|').split("|").map(function (sentence) { + var splitChars = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '.?!'; + splitChars.split('').forEach(function (c) { + if ('[\\^$.|?*+()'.includes(c)) { + splitChars.replace(c, '\\' + c); + } + }); + return text.replace(new RegExp('([' + splitChars + ']+)', 'g'), '$1|').split('|').map(function (sentence) { return trim(sentence); }).filter(Boolean); }; diff --git a/src/ios.js b/src/ios.js new file mode 100644 index 0000000..23cab5a --- /dev/null +++ b/src/ios.js @@ -0,0 +1,45 @@ +export const isIos = () => /(iPhone|iPad|iPod)/.test(navigator.platform) + +export const filterIOSVoices = (voices) => { + let selectableVoices = [ + 'Maged', + 'Zuzana', + 'Sara', + 'Anna', + 'Melina', + 'Karen', + 'Samantha', + 'Daniel', + 'Rishi', + 'Moira', + 'Tessa', + 'Mónica', + 'Paulina', + 'Satu', + 'Amélie', + 'Thomas', + 'Carmit', + 'Lekha', + 'Mariska', + 'Damayanti', + 'Alice', + 'Kyoko', + 'Yuna', + 'Ellen', + 'Xander', + 'Nora', + 'Zosia', + 'Luciana', + 'Joana', + 'Ioana', + 'Milena', + 'Laura', + 'Alva', + 'Kanya', + 'Yelda', + 'Tian-Tian', + 'Sin-Ji', + 'Mei-Jia' + ] + return voices.filter(v => selectableVoices.includes(v.name)) +} diff --git a/src/speak-tts.js b/src/speak-tts.js index 3b01901..c7dc63d 100644 --- a/src/speak-tts.js +++ b/src/speak-tts.js @@ -1,5 +1,7 @@ import { splitSentences, validateLocale, isString, size, isNan, isNil, isObject, trim } from './utils' +import { isIos, filterIOSVoices } from './ios' + class SpeakTTS { constructor() { this.browserSupport = ('speechSynthesis' in window && 'SpeechSynthesisUtterance' in window) @@ -32,6 +34,12 @@ class SpeakTTS { this._loadVoices() .then(voices => { + if (isIos()) { + // iOS does not allow you to select all of the voices it claims + // to have. You only get one per locale. + voices = filterIOSVoices(voices) + } + // Handle callback onvoiceschanged by hand listeners['onvoiceschanged'] && listeners['onvoiceschanged'](voices) @@ -138,6 +146,12 @@ class SpeakTTS { } setSplitSentences(splitSentences) { + if (typeof splitSentences !== 'string' && Boolean(splitSentences)) { + splitSentences = '.?!' + } + if (splitSentences.match(/[a-zA-Z]/g)) { + throw 'splitSentences contains invalid characters [a-zA-Z]. Set to true or false, or specify custom punctuation characters to split sentences.' + } this.splitSentences = splitSentences } @@ -154,7 +168,7 @@ class SpeakTTS { // Split into sentences (for better result and bug with some versions of chrome) const utterances = [] const sentences = this.splitSentences - ? splitSentences(msg) + ? splitSentences(msg, this.splitSentences) : [msg] sentences.forEach((sentence, index) => { const isLast = index === size(sentences) - 1 @@ -166,8 +180,8 @@ class SpeakTTS { if(this.pitch) utterance.pitch = this.pitch //0 to 2 utterance.text = sentence - // Attach event listeners - Object.keys(listeners).forEach(listener => { + // Attach specified event listeners, always including onerror and onend to resolve/reject the speak promise + Array.from(new Set(Object.keys(listeners).concat(['onerror', 'onend']))).forEach(listener => { const fn = listeners[listener] const newListener = (data) => { fn && fn(data) diff --git a/src/utils.js b/src/utils.js index 279bc9b..7f52e77 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,9 +1,15 @@ -export const splitSentences = (text = '') => text.replace(/\.+/g,'.|') - .replace(/\?/g,'?|') - .replace(/\!/g,'!|') - .split("|") + +export const splitSentences = (text='', splitChars='.?!') => { + splitChars.split('').forEach( c => { + if ('[\\^$.|?*+()'.includes(c)) { + splitChars.replace(c, '\\' + c) + } + }) + return text.replace(new RegExp('([' + splitChars + ']+)', 'g'), '$1|') + .split('|') .map(sentence => trim(sentence)) .filter(Boolean) +} const bcp47LocalePattern = /^(?:(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))$|^((?:[a-z]{2,3}(?:(?:-[a-z]{3}){1,3})?)|[a-z]{4}|[a-z]{5,8})(?:-([a-z]{4}))?(?:-([a-z]{2}|\d{3}))?((?:-(?:[\da-z]{5,8}|\d[\da-z]{3}))*)?((?:-[\da-wy-z](?:-[\da-z]{2,8})+)*)?(-x(?:-[\da-z]{1,8})+)?$|^(x(?:-[\da-z]{1,8})+)$/i; // eslint-disable-line max-len