diff --git a/.github/workflows/update-gh-pages.yml b/.github/workflows/update-gh-pages.yml index 5c6be52a50f..f450e00e55d 100644 --- a/.github/workflows/update-gh-pages.yml +++ b/.github/workflows/update-gh-pages.yml @@ -2,7 +2,11 @@ name: Update gh-pages on: push: - branches: [main] + branches: [ main, browser_tts ] + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true jobs: check-gh-pages: diff --git a/package-lock.json b/package-lock.json index 21d76f25e8e..993db8f9d2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,11 @@ "dexie": "^3.0.3", "dexie-export-import": "^1.0.0", "eventemitter3": "^4.0.7", + "google-tts-api": "^2.0.2", "inquirer": "^8.1.1", "inquirer-fuzzy-path": "^2.3.0", - "lodash": "^4.14.170" + "lodash": "^4.14.170", + "url": "^0.11.0" }, "devDependencies": { "@actions/core": "^1.10.1", @@ -3715,6 +3717,14 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/babel-loader": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", @@ -4045,7 +4055,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -7254,7 +7263,6 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==", - "dev": true, "funding": [ { "type": "individual", @@ -7563,8 +7571,7 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -7638,7 +7645,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -7902,6 +7908,14 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true }, + "node_modules/google-tts-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/google-tts-api/-/google-tts-api-2.0.2.tgz", + "integrity": "sha512-MkQYbBJEdom8hJpfEVDfD3tpBtkz0X59C+FNsoRhbnCiFjZRnzyurGQ5OrAr3xkigII56/jmk0JNwZsp450G+Q==", + "dependencies": { + "axios": "^0.21.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -7942,7 +7956,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -7983,7 +7996,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -11859,7 +11871,6 @@ "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14186,7 +14197,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -15794,6 +15804,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.1.tgz", + "integrity": "sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -15805,6 +15824,25 @@ "requires-port": "^1.0.0" } }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, + "node_modules/url/node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -19522,6 +19560,14 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + }, "babel-loader": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", @@ -19767,7 +19813,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -22158,8 +22203,7 @@ "follow-redirects": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", - "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==", - "dev": true + "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==" }, "fork-ts-checker-webpack-plugin": { "version": "7.2.11", @@ -22379,8 +22423,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "function.prototype.name": { "version": "1.1.5", @@ -22433,7 +22476,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -22635,6 +22677,14 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true }, + "google-tts-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/google-tts-api/-/google-tts-api-2.0.2.tgz", + "integrity": "sha512-MkQYbBJEdom8hJpfEVDfD3tpBtkz0X59C+FNsoRhbnCiFjZRnzyurGQ5OrAr3xkigII56/jmk0JNwZsp450G+Q==", + "requires": { + "axios": "^0.21.0" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -22669,7 +22719,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -22697,8 +22746,7 @@ "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "has-tostringtag": { "version": "1.0.0", @@ -25516,8 +25564,7 @@ "object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "dev": true + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" }, "object-keys": { "version": "1.1.1", @@ -27225,7 +27272,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -28447,6 +28493,30 @@ "punycode": "^2.1.0" } }, + "url": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.1.tgz", + "integrity": "sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==", + "requires": { + "punycode": "^1.4.1", + "qs": "^6.11.0" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + } + } + }, "url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", diff --git a/package.json b/package.json index 5f544a42bf7..fa79d87b426 100644 --- a/package.json +++ b/package.json @@ -161,9 +161,11 @@ "dexie": "^3.0.3", "dexie-export-import": "^1.0.0", "eventemitter3": "^4.0.7", + "google-tts-api": "^2.0.2", "inquirer": "^8.1.1", "inquirer-fuzzy-path": "^2.3.0", - "lodash": "^4.14.170" + "lodash": "^4.14.170", + "url": "^0.11.0" }, "publishConfig": { "access": "public" diff --git a/ui/raidboss/browser_tts_engine.ts b/ui/raidboss/browser_tts_engine.ts index d9da664e9d8..3e80ce7d1da 100644 --- a/ui/raidboss/browser_tts_engine.ts +++ b/ui/raidboss/browser_tts_engine.ts @@ -1,6 +1,17 @@ +import * as googleTTS from 'google-tts-api'; + import { Lang } from '../../resources/languages'; -class TTSItem { +const TTSEngineType = { + SpeechSynthesis: 0, + GoogleTTS: 1, +}; + +interface TTSItem { + play: () => void; +} + +class SpeechTTSItem implements TTSItem { readonly text: string; readonly item: SpeechSynthesisUtterance; @@ -18,18 +29,45 @@ class TTSItem { } } +class GoogleTTSItem implements TTSItem { + readonly text: string; + readonly lang: string; + private readonly item: HTMLAudioElement | null = null; + + constructor(text: string, lang: string) { + this.text = text; + this.lang = lang; + const audio = document.createElement('audio'); + const url = googleTTS.getAudioUrl(text, { lang: lang }); + audio.src = url; + document.body.appendChild(audio); + this.item = audio; + } + + play() { + if (this.item) + void this.item.play(); + } +} + type TTSItemDictionary = { [key: string]: TTSItem; }; export default class BrowserTTSEngine { readonly ttsItems: TTSItemDictionary = {}; + private readonly engineType = TTSEngineType.SpeechSynthesis; + private readonly googleTTSLang: string; private speechLang?: string; private speechVoice?: SpeechSynthesisVoice; private initializeAttempts = 0; - constructor(private cactbotLang: Lang) { - if (window.speechSynthesis !== undefined) { + constructor(private cactbotLang: Lang, private isRemote: boolean) { + this.googleTTSLang = cactbotLang === 'cn' ? 'zh' : cactbotLang; + if (!isRemote) { + this.engineType = TTSEngineType.GoogleTTS; + console.info('BrowserTTS info: running locally in Google TTS mode'); + } else if (window.speechSynthesis !== undefined) { // https://bugs.chromium.org/p/chromium/issues/detail?id=334847 window.speechSynthesis.getVoices(); window.speechSynthesis.onvoiceschanged = () => this.initializeVoice(); @@ -38,6 +76,8 @@ export default class BrowserTTSEngine { } initializeVoice(): boolean { + if (!this.isRemote) + return true; if (window.speechSynthesis === undefined) return false; if (this.speechVoice !== undefined) @@ -56,7 +96,7 @@ export default class BrowserTTSEngine { ko: 'ko-KR', }; - // figure out what TTS engine type we need + // figure out what TTS voice type we need const speechLang = cactbotLangToSpeechLang[this.cactbotLang]; const voice = window.speechSynthesis.getVoices().find((voice) => voice.lang.replaceAll('_', '-') === speechLang @@ -79,14 +119,33 @@ export default class BrowserTTSEngine { return; try { - let ttsItem = this.ttsItems[text]; - if (!ttsItem) { - ttsItem = new TTSItem(text, this.speechLang, this.speechVoice); - this.ttsItems[text] = ttsItem; - } - ttsItem.play(); + const ttsItem = this.ttsItems[text]; + ttsItem ? ttsItem.play() : this.playTTS(text); } catch (e) { console.error('Exception performing TTS', e); } } + + playTTS(text: string): void { + switch (this.engineType) { + case TTSEngineType.SpeechSynthesis: + this.playSpeechTTS(text); + break; + case TTSEngineType.GoogleTTS: + this.playGoogleTTS(text); + break; + } + } + + playSpeechTTS(text: string): void { + const ttsItem = new SpeechTTSItem(text, this.speechLang, this.speechVoice); + this.ttsItems[text] = ttsItem; + ttsItem.play(); + } + + playGoogleTTS(text: string): void { + const ttsItem = new GoogleTTSItem(text, this.googleTTSLang); + this.ttsItems[text] = ttsItem; + ttsItem.play(); + } } diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 6a40ffe0497..a963e3a628c 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -601,8 +601,8 @@ export class PopupText { this.displayLang = this.options.AlertsLanguage ?? this.options.DisplayLanguage ?? this.options.ParserLanguage ?? 'en'; - if (this.options.IsRemoteRaidboss) { - this.ttsEngine = new BrowserTTSEngine(this.displayLang); + if (this.options.IsRemoteRaidboss || this.options.BrowserTTS) { + this.ttsEngine = new BrowserTTSEngine(this.displayLang, this.options.IsRemoteRaidboss); this.ttsSay = (text) => { this.ttsEngine?.play(this.options.TransformTts(text)); }; diff --git a/ui/raidboss/raidboss.html b/ui/raidboss/raidboss.html index bdfdb685c7c..36d144c70fb 100644 --- a/ui/raidboss/raidboss.html +++ b/ui/raidboss/raidboss.html @@ -1,6 +1,7 @@
+