JavaScript bridge API
Text to speech
The device voices, spoken from the page.
speechSynthesis web standard#
window.speechSynthesis + SpeechSynthesisUtterance
Example
Reads text aloud with the phone's own text-to-speech engine, using the normal Web Speech API.
Returns: speechSynthesis.speak(u) returns nothing; the utterance's start / boundary / end / error / pause / resume events (as onstart … properties or with addEventListener) tell you what happened. getVoices() returns an array of { voiceURI, name, lang, localService, default } synchronously. Needs: nothing. Android WebView has no working speech API of its own; the app installs this one at document start in every frame.
Speak a sentence and know when it is done:
function say(text) {
if (!('speechSynthesis' in window)) { alert('Speech is not available here'); return; }
var u = new SpeechSynthesisUtterance(text);
u.lang = 'en-US'; // BCP-47 tag: 'ta-IN', 'hi-IN', 'fr-FR' ...
u.rate = 1.2; // 0.1 .. 4 (1 = normal)
u.pitch = 1; // 0.1 .. 4 (1 = normal)
u.onstart = function () { document.body.classList.add('speaking'); };
u.onend = function () { document.body.classList.remove('speaking'); };
u.onerror = function (e) {
document.body.classList.remove('speaking');
console.log('Speech failed:', e.error); // 'synthesis-failed'
};
speechSynthesis.speak(u); // queues after anything already speaking
}
document.getElementById('stop').onclick = function () {
if ('speechSynthesis' in window) speechSynthesis.cancel(); // stops now, clears the queue
};Voices arrive a moment after start-up. Read them again when voiceschanged fires:
var voices = [];
function loadVoices() {
voices = speechSynthesis.getVoices(); // [] until the engine is ready
var tamil = voices.filter(function (v) { return v.lang.indexOf('ta') === 0; });
document.getElementById('tamil-btn').hidden = tamil.length === 0;
}
if ('speechSynthesis' in window) {
loadVoices();
speechSynthesis.addEventListener('voiceschanged', loadVoices);
}
function sayWithVoice(text, voice) {
var u = new SpeechSynthesisUtterance(text);
u.voice = voice; // a voice from getVoices(): that exact voice is used
u.volume = 0.8; // 0 .. 1
speechSynthesis.speak(u);
}Notes:
volume(0-1),voice(the exact voice fromgetVoices()),rate,pitchandlangall reach the phone's engine. With avoiceand alang, the voice wins; with neither, the engine's default voice speaks.boundaryfires before each word withcharIndex/charLength(Android 8+; engines that report no word positions send none).pause()/resume(): Android's engine cannot pause mid-word, sopause()stops it andresume()speaks the rest of the sentence from the last word the engine reported (pause/resumeevents fire). On an engine that reports no word positions,resume()starts that utterance again from its beginning.speakingstaystruewhile paused;pendingistruewhile queued utterances wait.cancel()fireserrorwitherror: 'interrupted'on the utterance being spoken and'canceled'on queued ones, as the standard says - do not show those as failures.- If the phone has no voice data for the language, the engine may still speak in its default language. Check
getVoices()for the language first. - The raw events behind this are
appmint:tts(seettsSpeak). You do not need them when you use this API.
__ttsSpeak bridge#
window.WebToApk.__ttsSpeak(json: String)
The speechSynthesis polyfill's full speak: `{text, lang, rate, pitch, volume (0-1), voice (a getVoices() name, or ''), id}`. [ttsSpeak] stays for pages that call the bridge directly; it has no volume or voice.
Example
Internal transport behind speechSynthesis.speak(utterance): the full utterance - text, language, rate, pitch, volume and the chosen voice - as one JSON string. Pages use the standard speechSynthesis API; never call this directly.
Returns: nothing. Progress arrives as appmint:tts events, which the speechSynthesis layer turns into the utterance's start / boundary / end / error events. Needs: nothing.
The standard API this carries:
function readAloud(text) {
if (!('speechSynthesis' in window)) return;
var voice = speechSynthesis.getVoices().filter(function (v) { return v.lang === 'en-GB'; })[0];
var u = new SpeechSynthesisUtterance(text);
if (voice) u.voice = voice; // that exact voice
u.volume = 0.6; // 0 .. 1
u.addEventListener('boundary', function (e) { highlightWordAt(e.charIndex, e.charLength); });
u.addEventListener('end', function () { clearHighlight(); });
speechSynthesis.speak(u);
}Notes: The JSON is {text, lang, rate, pitch, volume, voice, id} (voice is a getVoices() name or ''). The older ttsSpeak(text, lang, rate, pitch, id) stays for pages that call the bridge directly; it has no volume or voice.
ttsCancel bridge#
window.WebToApk.ttsCancel()
Backs the window.speechSynthesis polyfill, which is installed at document start in every frame - pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.
Described by its group, Text-to-speech, rather than on its own.
Example
Stops speech right now and throws away everything still waiting to be spoken. Pages normally call the standard speechSynthesis.cancel(), which calls this for you.
Returns: nothing. Needs: nothing.
The public way (use this):
document.getElementById('stop-reading').addEventListener('click', function () {
if ('speechSynthesis' in window) speechSynthesis.cancel();
});The raw call:
if (window.WebToApk && typeof window.WebToApk.ttsCancel === 'function') {
window.WebToApk.ttsCancel();
}Notes: Speech that is cut off or dropped sends no appmint:tts end or error event, so an utterance's onend does not run after a cancel. If you track raw ttsSpeak ids yourself, forget them when you call ttsCancel().
ttsGetVoices bridge#
window.WebToApk.ttsGetVoices(): String
JSON array of installed voices: [{name, lang, default}]. Empty until the engine finishes init - the page gets 'voiceschanged' then and re-queries, exactly like Chrome's async getVoices() contract.
Example
Lists the text-to-speech voices installed on the phone. Pages normally use the standard speechSynthesis.getVoices(), which calls this for you.
Returns: a JSON string, synchronously: [{ "name": "en-us-x-sfg-local", "lang": "en-US", "default": false }, ...]. It is "[]" until the engine has started. Needs: nothing.
The public way (use this):
function fillLanguages() {
var langs = {};
speechSynthesis.getVoices().forEach(function (v) { langs[v.lang] = true; });
document.getElementById('langs').textContent = Object.keys(langs).join(', ');
}
if ('speechSynthesis' in window) {
fillLanguages();
speechSynthesis.onvoiceschanged = fillLanguages;
}The raw call (remember JSON.parse):
function nativeVoices() {
if (!(window.WebToApk && typeof window.WebToApk.ttsGetVoices === 'function')) return [];
try { return JSON.parse(window.WebToApk.ttsGetVoices()); } catch (e) { return []; }
}
window.addEventListener('appmint:tts', function (e) {
if (e.detail && e.detail.event === 'voiceschanged') {
var hindi = nativeVoices().filter(function (v) { return v.lang.indexOf('hi') === 0; });
document.getElementById('hindi-btn').hidden = hindi.length === 0;
}
});Notes: lang is a BCP-47 tag such as en-US or ta-IN. A voice's name is the engine's own id and cannot be chosen by name when speaking; only its language is used.
ttsIsSpeaking bridge#
window.WebToApk.ttsIsSpeaking(): Boolean
Backs the window.speechSynthesis polyfill, which is installed at document start in every frame - pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.
Described by its group, Text-to-speech, rather than on its own.
Example
Tells you if the phone's text-to-speech engine is speaking at this moment. Pages normally use speechSynthesis.speaking or the utterance's onend instead.
Returns: true / false, synchronously. false before the engine has started. Needs: nothing.
The public way (use this):
if ('speechSynthesis' in window && speechSynthesis.speaking) {
speechSynthesis.cancel();
}The raw call asks the engine itself, so it is exact even if speech was started with the raw ttsSpeak:
function isSpeaking() {
if (window.WebToApk && typeof window.WebToApk.ttsIsSpeaking === 'function') {
return window.WebToApk.ttsIsSpeaking() === true;
}
return false;
}
var btn = document.getElementById('read-btn');
setInterval(function () {
btn.textContent = isSpeaking() ? 'Stop' : 'Read aloud';
}, 500);Notes: Do not poll fast; an event (onend, or appmint:tts with event: 'end') is better when you started the speech yourself.
ttsSpeak bridge#
window.WebToApk.ttsSpeak(text: String, lang: String, rate: Double, pitch: Double, utteranceId: String)
Backs the window.speechSynthesis polyfill, which is installed at document start in every frame - pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.
Described by its group, Text-to-speech, rather than on its own.
Example
Starts speaking text with the phone's text-to-speech engine. Pages normally use the standard speechSynthesis API, which calls this for you.
Returns: nothing. Progress arrives later as the appmint:tts event (and the window.onAppMintTts(detail) hook) with detail = { id, event }, where event is 'start', 'end' or 'error', and id is the utteranceId you passed. Needs: nothing.
The public way (use this):
if ('speechSynthesis' in window) {
var u = new SpeechSynthesisUtterance('Your order is ready');
u.lang = 'en-GB';
u.onend = function () { console.log('done'); };
speechSynthesis.speak(u);
}The raw call, if you want your own ids. Arguments: text, lang, rate, pitch, utteranceId:
var ttsWaiting = {};
window.addEventListener('appmint:tts', function (e) {
var d = e.detail || {};
if (d.event === 'voiceschanged') return; // engine ready, id is ''
var done = ttsWaiting[d.id];
if (!done || d.event === 'start') return;
delete ttsWaiting[d.id];
done(d.event === 'end'); // 'end' = spoke it, 'error' = failed
});
function speakRaw(text) {
return new Promise(function (resolve) {
if (!(window.WebToApk && typeof window.WebToApk.ttsSpeak === 'function')) { resolve(false); return; }
var id = 'mine_' + Date.now();
ttsWaiting[id] = resolve;
window.WebToApk.ttsSpeak(String(text), 'en-US', 1.0, 1.0, id);
});
}
speakRaw('Hello').then(function (ok) { console.log(ok ? 'spoken' : 'could not speak'); });Notes: Utterances queue one after another. lang may be '' (the engine's default voice); rate and pitch are clamped to 0.1-4. If the engine is still starting, the text waits and is spoken when it is ready; if the engine cannot start, each waiting utterance gets 'error'. Use your own id prefix; ids starting wta_u belong to the speechSynthesis layer. On Android 8+ appmint:tts also carries {id, event: 'boundary', charIndex, charLength} before each word. This raw call has no volume or voice - speechSynthesis has both.
ttsWarmUp bridge#
window.WebToApk.ttsWarmUp()
Backs the window.speechSynthesis polyfill, which is installed at document start in every frame - pages should use the STANDARD speechSynthesis API and these will be called for them. Android WebView ships no working speechSynthesis of its own, which is why the reroute exists.
Described by its group, Text-to-speech, rather than on its own.
Example
Starts the phone's text-to-speech engine early, so the first speak() has no delay and getVoices() fills in. The app's speechSynthesis layer already calls this at page start, so pages almost never need it.
Returns: nothing. When the engine is ready, the appmint:tts event fires with detail = { id: '', event: 'voiceschanged' } (and speechSynthesis.onvoiceschanged runs). Needs: nothing.
The public way (use this): just read voices when they are ready.
if ('speechSynthesis' in window) {
speechSynthesis.onvoiceschanged = function () {
console.log('voices:', speechSynthesis.getVoices().length);
};
}The raw call, then wait for the ready event:
window.addEventListener('appmint:tts', function (e) {
if (e.detail && e.detail.event === 'voiceschanged') {
var list = JSON.parse(window.WebToApk.ttsGetVoices());
console.log('engine ready,', list.length, 'voices');
}
});
if (window.WebToApk && typeof window.WebToApk.ttsWarmUp === 'function') {
window.WebToApk.ttsWarmUp(); // safe to call many times
}Notes: voiceschanged fires once, when the engine finishes starting. If it already started (for example because the speechSynthesis layer warmed it), you will not get it again; call ttsGetVoices() directly.