Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d05ab3c
#617 moved speechServices to separate folder "speech"
klues Sep 25, 2025
768dd47
#617 removed responsive voice, added MS Azure
klues Sep 25, 2025
5160512
#617 cache speech to own cache, max. 1000 entries, cache MS Azure voi…
klues Sep 25, 2025
d0fe9ff
#617 expose localStorageService for testing purposes
klues Sep 25, 2025
a873d1f
#617 added possibility to set azure params by url param for testing p…
klues Oct 1, 2025
dc83841
#617 added method to analyze azure speech voice objects
klues Nov 6, 2025
f993efc
Merge branch 'master' into 617/more-tts-voices
klues Dec 10, 2025
4b68f2e
#617 no converting to lower case because it's possible that different…
klues Dec 12, 2025
66cbd7d
#617 only send "stop" requests to external service if the external se…
klues Dec 12, 2025
9d98973
#617 added JWT token authentication
klues Dec 12, 2025
d010593
#617 added postJson() util method
klues Dec 12, 2025
a644595
#617 adapted paths to match API of Asterics-AAC-TTs-Proxy
klues Dec 12, 2025
3ed707d
#617 re-enabled azure TTS voices for testing
klues Dec 15, 2025
4d2fdc8
#617 first use current url param azure keys
klues Dec 15, 2025
817e503
#617 sort back "standard device voice" on non-iOS devices in order to…
klues Dec 15, 2025
308650d
#617 test azure: import credentials from url before using them
klues Dec 15, 2025
b78341d
#617 determine which voice to use by defined order of voices (don't u…
klues Dec 15, 2025
77f9a6d
Merge branch 'master' into 617/more-tts-voices
klues Mar 19, 2026
6270622
fixed path, see https://github.com/asterics/Asterics-AAC/issues/617
klues Mar 19, 2026
a1395e3
Merge branch 'master' into 617/more-tts-voices
klues Mar 25, 2026
56d3910
Merge branch 'master' into 617/more-tts-voices
klues Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 0 additions & 179 deletions app/lib/responsive-voice.js

This file was deleted.

57 changes: 18 additions & 39 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,48 +154,27 @@ <h2 class="hide-mobile">{{ $t('users') }}</h2>
<script type="text/javascript" src='app/lib/sjcl.min.js'></script>

<script type="text/javascript">
if (Function.bind) {
//remove RV log messages without altering RV source (forbidden by license)
window.originalLog = console.log.bind(console);
console.log = function () {
if (['ResponsiveVoice r1.6.5', 'isHidden: false', 'Prerender: false', 'Configuring'].indexOf(arguments[0]) !== -1) {
return;
} else if (arguments[0] === 'RV: Voice support ready') {
console.log = window.originalLog;
} else {
window.originalLog.apply(null, arguments);
}
}

//determine time needed for JSON.parse/stringify
/**window.originalJSONParse = JSON.parse;
window.originalJSONStringify = JSON.stringify;
window.parseTime = 0;

JSON.parse = function (arguments) {
let start = new Date().getTime();
let result = window.originalJSONParse(arguments);
window.parseTime += (new Date().getTime() - start);
console.warn(window.parseTime / 1000);
return result;
}

JSON.stringify = function (arguments) {
let start = new Date().getTime();
let result = window.originalJSONStringify(arguments);
window.parseTime += (new Date().getTime() - start);
console.warn(window.parseTime / 1000);
return result;
}*/
//determine time needed for JSON.parse/stringify
/**window.originalJSONParse = JSON.parse;
window.originalJSONStringify = JSON.stringify;
window.parseTime = 0;

JSON.parse = function (arguments) {
let start = new Date().getTime();
let result = window.originalJSONParse(arguments);
window.parseTime += (new Date().getTime() - start);
console.warn(window.parseTime / 1000);
return result;
}
</script>

<script>
var rvApiKey="zGuJFLIV";
var rvApiEndpoint = "https://texttospeech.responsivevoice.org/v1/text:synthesize";
JSON.stringify = function (arguments) {
let start = new Date().getTime();
let result = window.originalJSONStringify(arguments);
window.parseTime += (new Date().getTime() - start);
console.warn(window.parseTime / 1000);
return result;
}*/
</script>
<!--script src="https://code.responsivevoice.org/responsivevoice.js"></script-->
<script type="text/javascript" src='app/lib/responsive-voice.js'></script>

<script type="module" src="app/build/asterics-aac.bundle.js"></script>
</body>
Expand Down
18 changes: 17 additions & 1 deletion serviceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ workbox.routing.registerRoute(({ url, request, event }) => {
return shouldCacheImage(url, request);
}, dynamicImageHandler);

workbox.routing.registerRoute(({url, request, event}) => {
return shouldCacheSpeech(url, request);
}, new workbox.strategies.CacheFirst({
cacheName: 'speech-cache',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 1000,
purgeOnQuotaError: true
})
]
}));

self.addEventListener('install', (event) => {
console.log('installing service worker ...');

Expand Down Expand Up @@ -211,14 +223,18 @@ function shouldCacheImage(url, request) {
}

function shouldCacheStaleWhileRevalidate(url, request) {
return url.href.startsWith('https://asterics.github.io/Asterics-AAC-Data');
return url.href.startsWith('https://asterics.github.io/Asterics-AAC-Data') || url.href.endsWith('tts.speech.microsoft.com/cognitiveservices/voices/list');
}

function shouldCacheNormal(url, request) {
let isOwnHost = url.hostname === self.location.hostname;
return isOwnHost && !shouldCacheImage(url, request) && !shouldCacheStaleWhileRevalidate(url);
}

function shouldCacheSpeech(url, request) {
return url.href.endsWith('tts.speech.microsoft.com/cognitiveservices/v1');
}

function sendToClients(msg) {
self.clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage(msg));
Expand Down
2 changes: 1 addition & 1 deletion src/js/input/clicking.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { L } from '../util/lquery.js';
import {speechService} from "../service/speechService.js";
import {speechService} from "../service/speech/speechService.js";

let Clicker = {};

Expand Down
2 changes: 1 addition & 1 deletion src/js/input/hovering.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { L } from '../util/lquery.js';
import $ from '../externals/jquery.js';
import { inputEventHandler } from './inputEventHandler';
import { util } from '../util/util';
import { speechService } from '../service/speechService';
import { speechService } from '../service/speech/speechService';
import { i18nService } from '../service/i18nService';
import { MainVue } from '../vue/mainVue';
import { stateService } from '../service/stateService';
Expand Down
2 changes: 1 addition & 1 deletion src/js/service/actionService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { areService } from './areService';
import { openHABService } from './openHABService';
import { httpService } from './httpService.js';
import { dataService } from './data/dataService';
import { speechService } from './speechService';
import { speechService } from './speech/speechService';
import { collectElementService } from './collectElementService';
import { predictionService } from './predictionService';
import { Router } from './../router';
Expand Down
2 changes: 1 addition & 1 deletion src/js/service/collectElementService.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import $ from '../externals/jquery.js';
import { GridElement } from '../model/GridElement';
import { speechService } from './speechService';
import { speechService } from './speech/speechService';
import { constants } from './../util/constants';
import { util } from './../util/util';
import { predictionService } from './predictionService';
Expand Down
2 changes: 1 addition & 1 deletion src/js/service/matrixMessenger/matrixAppService.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { matrixService } from './matrixService';
import $ from '../../externals/jquery';
import { constants } from '../../util/constants';
import { imageUtil } from '../../util/imageUtil';
import { speechService } from '../speechService';
import { speechService } from '../speech/speechService';

let matrixAppService = {};
let currentRoom = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { stateService } from './stateService';
import { constants } from '../util/constants';
import { util } from '../util/util.js';
import $ from '../externals/jquery.js';
import { audioUtil } from '../util/audioUtil.js';
import { stateService } from '../stateService';
import { constants } from '../../util/constants';
import { util } from '../../util/util.js';
import $ from '../../externals/jquery.js';
import { audioUtil } from '../../util/audioUtil.js';
import { speechServiceExternal } from './speechServiceExternal.js';
import { localStorageService } from './data/localStorageService.js';
import { i18nService } from './i18nService';
import voiceUtil from '../util/voiceUtil';
import { localStorageService } from '../data/localStorageService.js';
import { i18nService } from '../i18nService.js';
import voiceUtil from '../../util/voiceUtil.js';
import { speechServiceAzure } from './speechServiceAzure.js';

let speechService = {};

Expand All @@ -16,15 +17,13 @@ let _voicePitch = 1;
let _voiceRate = 1;
let _voiceLangIsTextLang = false;
let allVoices = [];
let responsiveVoiceVoices = JSON.parse(
'[{"name":"UK English Female","lang":"en-GB"},{"name":"UK English Male","lang":"en-GB"},{"name":"US English Female","lang":"en-US"},{"name":"US English Male","lang":"en-US"},{"name":"Arabic Male","lang":"ar-SA"},{"name":"Arabic Female","lang":"ar-SA"},{"name":"Armenian Male","lang":"hy-AM"},{"name":"Australian Female","lang":"en-AU"},{"name":"Australian Male","lang":"en-AU"},{"name":"Bangla Bangladesh Female","lang":"bn-BD"},{"name":"Bangla Bangladesh Male","lang":"bn-BD"},{"name":"Bangla India Female","lang":"bn-IN"},{"name":"Bangla India Male","lang":"bn-IN"},{"name":"Brazilian Portuguese Female","lang":"pt-BR"},{"name":"Chinese Female","lang":"zh-CN"},{"name":"Chinese Male","lang":"zh-CN"},{"name":"Chinese (Hong Kong) Female","lang":"zh-HK"},{"name":"Chinese (Hong Kong) Male","lang":"zh-HK"},{"name":"Chinese Taiwan Female","lang":"zh-TW"},{"name":"Chinese Taiwan Male","lang":"zh-TW"},{"name":"Czech Female","lang":"cs-CZ"},{"name":"Danish Female","lang":"da-DK"},{"name":"Deutsch Female","lang":"de-DE"},{"name":"Deutsch Male","lang":"de-DE"},{"name":"Dutch Female","lang":"nl-NL"},{"name":"Dutch Male","lang":"nl-NL"},{"name":"Estonian Male","lang":"et-EE"},{"name":"Filipino Female","lang":"fil-PH"},{"name":"Finnish Female","lang":"fi-FI"},{"name":"French Female","lang":"fr-FR"},{"name":"French Male","lang":"fr-FR"},{"name":"French Canadian Female","lang":"fr-CA"},{"name":"French Canadian Male","lang":"fr-CA"},{"name":"Greek Female","lang":"el-GR"},{"name":"Hindi Female","lang":"hi-IN"},{"name":"Hindi Male","lang":"hi-IN"},{"name":"Hungarian Female","lang":"hu-HU"},{"name":"Indonesian Female","lang":"id-ID"},{"name":"Indonesian Male","lang":"id-ID"},{"name":"Italian Female","lang":"it-IT"},{"name":"Italian Male","lang":"it-IT"},{"name":"Japanese Female","lang":"ja-JP"},{"name":"Japanese Male","lang":"ja-JP"},{"name":"Korean Female","lang":"ko-KR"},{"name":"Korean Male","lang":"ko-KR"},{"name":"Latin Male","lang":"la"},{"name":"Nepali","lang":"ne-NP"},{"name":"Norwegian Female","lang":"nb-NO"},{"name":"Norwegian Male","lang":"nb-NO"},{"name":"Polish Female","lang":"pl-PL"},{"name":"Polish Male","lang":"pl-PL"},{"name":"Portuguese Female","lang":"pt-BR"},{"name":"Portuguese Male","lang":"pt-BR"},{"name":"Romanian Female","lang":"ro-RO"},{"name":"Russian Female","lang":"ru-RU"},{"name":"Sinhala","lang":"si-LK"},{"name":"Slovak Female","lang":"sk-SK"},{"name":"Spanish Female","lang":"es-ES"},{"name":"Spanish Latin American Female","lang":"es-MX"},{"name":"Spanish Latin American Male","lang":"es-MX"},{"name":"Swedish Female","lang":"sv-SE"},{"name":"Swedish Male","lang":"sv-SE"},{"name":"Tamil Female","lang":"hi-IN"},{"name":"Tamil Male","lang":"hi-IN"},{"name":"Thai Female","lang":"th-TH"},{"name":"Thai Male","lang":"th-TH"},{"name":"Turkish Female","lang":"tr-TR"},{"name":"Turkish Male","lang":"tr-TR"},{"name":"Ukrainian Female","lang":"uk-UA"},{"name":"Vietnamese Female","lang":"vi-VN"},{"name":"Vietnamese Male","lang":"vi-VN"},{"name":"Afrikaans Male","lang":"af-ZA"},{"name":"Albanian Male","lang":"sq-AL"},{"name":"Bosnian Male","lang":"bs"},{"name":"Catalan Male","lang":"ca-ES"},{"name":"Croatian Male","lang":"hr-HR"},{"name":"Esperanto Male","lang":"eo"},{"name":"Icelandic Male","lang":"is-IS"},{"name":"Latvian Male","lang":"lv-LV"},{"name":"Macedonian Male","lang":"mk-MK"},{"name":"Moldavian Female","lang":"md"},{"name":"Montenegrin Male","lang":"me"},{"name":"Serbian Male","lang":"sr-RS"},{"name":"Serbo-Croatian Male","lang":"hr-HR"},{"name":"Swahili Male","lang":"sw-KE"},{"name":"Welsh Male","lang":"cy"},{"name":"Fallback UK Female","lang":"en-GB"}]'
);

let currentSpeakArray = [];
let voiceIgnoreList = ['com.apple.speech.synthesis.voice']; //joke voices by Apple
let voiceSortBackList = ['com.apple.eloquence'];
let hasSpoken = false;
let isSpeakingNative = false;
let startedSpeakingRV = false;

let _initPromiseResolveFn;
let initPromise = new Promise(resolve => {
_initPromiseResolveFn = resolve;
Expand Down Expand Up @@ -82,7 +81,6 @@ speechService.speak = function (textOrOject, options = {}) {
if (!text) {
return;
}
text = text.toLowerCase();
if (options.voiceLangIsTextLang &&
preferredVoiceId &&
i18nService.getBaseLang(prefVoiceLang) !== i18nService.getBaseLang(langToUse) &&
Expand All @@ -95,13 +93,11 @@ speechService.speak = function (textOrOject, options = {}) {
speechService.stopSpeaking();
}
let voices = getVoicesById(preferredVoiceId) || getVoicesByLang(langToUse);
let nativeVoices = voices.filter((voice) => voice.type === constants.VOICE_TYPE_NATIVE);
let responsiveVoices = voices.filter((voice) => voice.type === constants.VOICE_TYPE_RESPONSIVEVOICE);
let externalVoices = voices.filter((voice) => voice.type === constants.VOICE_TYPE_EXTERNAL_PLAYING || voice.type === constants.VOICE_TYPE_EXTERNAL_DATA);
if (speechService.nativeSpeechSupported() && nativeVoices.length > 0) {
let voiceToUse = voices[0] || {};
if (speechService.nativeSpeechSupported() && voiceToUse.type === constants.VOICE_TYPE_NATIVE) {
var msg = new SpeechSynthesisUtterance(text);
msg.voice = nativeVoices[0].ref;
let isSelectedVoice = nativeVoices[0].id === preferredVoiceId;
msg.voice = voiceToUse.ref;
let isSelectedVoice = voiceToUse.id === preferredVoiceId;
msg.pitch = isSelectedVoice && !options.useStandardRatePitch ? _voicePitch : 1;
msg.rate = options.rate || (isSelectedVoice && !options.useStandardRatePitch ? _voiceRate : 1);
msg.volume = userSettings.systemVolume / 100.0;
Expand All @@ -118,16 +114,12 @@ speechService.speak = function (textOrOject, options = {}) {
msg.addEventListener('end', () => {
isSpeakingNative = false;
})
} else if (responsiveVoices.length > 0) {
let isSelectedVoice = responsiveVoices[0].id === preferredVoiceId;
responsiveVoice.speak(text, responsiveVoices[0].name, {
rate: options.rate || (isSelectedVoice && !options.useStandardRatePitch ? _voiceRate : 1),
pitch: isSelectedVoice && !options.useStandardRatePitch ? _voicePitch : 1
});
startedSpeakingRV = true;
} else if (voiceToUse.type === constants.VOICE_TYPE_MS_AZURE) {
speechServiceAzure.speak(text, voiceToUse.id);
hasSpoken = true;
} else if (externalVoices.length > 0) {
speechServiceExternal.speak(text, externalVoices[0].ref.providerId, externalVoices[0]);
} else if (voiceToUse.type === constants.VOICE_TYPE_EXTERNAL_DATA ||
voiceToUse.type === constants.VOICE_TYPE_EXTERNAL_PLAYING) {
speechServiceExternal.speak(text, voiceToUse.ref.providerId, voiceToUse);
}
testIsSpeaking();
setTimeout(() => {
Expand Down Expand Up @@ -203,20 +195,18 @@ speechService.speakArray = async function (array, progressFn, index) {
speechService.stopSpeaking = function () {
currentSpeakArray = [];
isSpeakingNative = false;
startedSpeakingRV = false;
if (speechService.nativeSpeechSupported()) {
window.speechSynthesis.cancel();
}
responsiveVoice.cancel();
speechServiceExternal.stop();
speechServiceAzure.stop();
};

speechService.isSpeaking = async function () {
let isSpeakingRV = startedSpeakingRV && responsiveVoice.isPlaying();
if (isSpeakingNative || isSpeakingRV) {
if (isSpeakingNative) {
return true;
}
return await speechServiceExternal.isSpeaking();
return (await speechServiceExternal.isSpeaking()) || (await speechServiceAzure.isSpeaking());
};

speechService.doAfterFinishedSpeaking = async function (fn) {
Expand Down Expand Up @@ -288,6 +278,14 @@ speechService.getVoicesInitialized = async function () {
}

speechService.voiceSortFn = function (a, b) {
if (!constants.IS_IOS) {
if (a.id === constants.VOICE_DEVICE_DEFAULT) {
return 1;
}
if (b.id === constants.VOICE_DEVICE_DEFAULT) {
return -1;
}
}
if (a.lang !== b.lang) {
let lang1 = i18nService.te(`lang.${a.lang}`) ? i18nService.t(`lang.${a.lang}`) : a.langFull;
let lang2 = i18nService.te(`lang.${b.lang}`) ? i18nService.t(`lang.${b.lang}`) : b.langFull;
Expand Down Expand Up @@ -430,15 +428,17 @@ async function init() {
};
}
addVoice(constants.VOICE_DEVICE_DEFAULT, await i18nService.tLoad("defaultDeviceVoice"), i18nService.getBrowserLang(), constants.VOICE_TYPE_NATIVE, true, undefined);
responsiveVoiceVoices.forEach((voice) => {
addVoice(voice.name, voice.name, voice.lang, constants.VOICE_TYPE_RESPONSIVEVOICE, false, voice);
});

await speechServiceExternal.init();
let externalVoices = await speechServiceExternal.getVoices();
for (let voice of externalVoices) {
let azureVoices = await speechServiceAzure.getVoices();
for (let voice of externalVoices.concat(azureVoices)) {
addVoice(voice.id, voice.name, voice.lang, voice.type, voice.local || false, voice);
}
allVoices.sort(speechService.voiceSortFn);
_initPromiseResolveFn();
let voices = await speechServiceAzure.getVoices();
log.warn(voices);
}
init();

Expand Down
Loading
Loading