# Text to speech - JavaScript bridge API

> The device voices, spoken from the page.

- **Applies to:** AppMint
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebtoapk.com/docs/api/speech

### `speechSynthesis`

```js
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:

```js
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:

```js
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 from `getVoices()`), `rate`, `pitch` and `lang` all reach the phone's engine. With a `voice` and a `lang`, the voice wins; with neither, the engine's default voice speaks.
- `boundary` fires before each word with `charIndex` / `charLength` (Android 8+; engines that report no word positions send none).
- `pause()` / `resume()`: Android's engine cannot pause mid-word, so `pause()` stops it and `resume()` speaks the rest of the sentence from the last word the engine reported (`pause` / `resume` events fire). On an engine that reports no word positions, `resume()` starts that utterance again from its beginning. `speaking` stays `true` while paused; `pending` is `true` while queued utterances wait.
- `cancel()` fires `error` with `error: '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` (see `ttsSpeak`). You do not need them when you use this API.

### `__ttsSpeak`

```js
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:

```js
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`

```js
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):

```js
document.getElementById('stop-reading').addEventListener('click', function () {
  if ('speechSynthesis' in window) speechSynthesis.cancel();
});
```

The raw call:

```js
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`

```js
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):

```js
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`):

```js
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`

```js
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):

```js
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`:

```js
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`

```js
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):

```js
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`:

```js
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`

```js
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.

```js
if ('speechSynthesis' in window) {
  speechSynthesis.onvoiceschanged = function () {
    console.log('voices:', speechSynthesis.getVoices().length);
  };
}
```

The raw call, then wait for the ready event:

```js
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.

