Создать APK бесплатно

JavaScript bridge API

Синтез речи

Голоса устройства, звучащие со страницы.

SpeechRecognition веб-стандарт#

webkitSpeechRecognition / SpeechRecognition — speech to text

Пример

Стандартный Web Speech API для диктовки: пользователь говорит, страница получает текст. В Chrome на Android он есть, а в Android WebView нет, поэтому приложение предоставляет его поверх собственного распознавателя речи Android. Код, написанный для Chrome, работает без изменений.

Возвращает: start() начинает слушать; события result содержат event.results[i][0].transcript (а также .confidence, .isFinal); события error содержат event.error (not-allowed, no-speech, audio-capture, network, aborted, language-not-supported, service-not-allowed); end всегда приходит последним. Требуется: микрофон. Сборка включает его сама, когда в вашем коде упоминается SpeechRecognition, а Android спрашивает пользователя при первом запуске start().

<button id="mic">Dictate</button>
<textarea id="note"></textarea>
<script>
const Rec = window.SpeechRecognition || window.webkitSpeechRecognition;
const mic = document.getElementById('mic');
if (!Rec) {
  mic.hidden = true;                       // no speech recogniser on this phone
} else {
  mic.addEventListener('click', () => {
    const rec = new Rec();
    rec.lang = 'en-IN';                    // leave empty for the page / phone language
    rec.interimResults = true;             // live text while the user speaks
    rec.onresult = (e) => {
      let text = '';
      for (const r of e.results) text += r[0].transcript;
      document.getElementById('note').value = text;
    };
    rec.onerror = (e) => {
      if (e.error === 'not-allowed') alert('Allow the microphone to dictate.');
    };
    rec.onend = () => { mic.disabled = false; };
    mic.disabled = true;
    rec.start();
  });
}
</script>

Непрерывная диктовка, которая останавливается по запросу и сообщает о каждой ошибке:

const Rec = window.SpeechRecognition || window.webkitSpeechRecognition;
let rec = null;
function toggleDictation() {
  if (!Rec) return showMessage('Voice typing is not available on this phone.');
  if (rec) { rec.stop(); return; }                   // stop() delivers what was heard
  rec = new Rec();
  rec.continuous = true;
  rec.onresult = (e) => {
    for (let i = e.resultIndex; i < e.results.length; i++) {
      if (e.results[i].isFinal) appendToNote(e.results[i][0].transcript);
    }
  };
  rec.onerror = (e) => {
    const msg = { 'not-allowed': 'Allow the microphone first.', 'no-speech': 'Nothing was heard.',
                  'audio-capture': 'No microphone found.', 'network': 'Speech service unreachable.' }[e.error];
    if (msg) showMessage(msg);
  };
  rec.onend = () => { rec = null; };
  rec.start();
}

Произнесите команду и выполните её на языке телефона:

const Rec = window.SpeechRecognition || window.webkitSpeechRecognition;
function listenForCommand() {
  if (!Rec) return;
  const rec = new Rec();
  rec.lang = navigator.language;
  rec.onresult = (e) => {
    const said = e.results[0][0].transcript.toLowerCase();
    if (said.includes('next')) goNext();
    else if (said.includes('back')) goBack();
    else showMessage('Heard: ' + said);
  };
  rec.onerror = (e) => {
    if (e.error === 'language-not-supported') showMessage('This language is not supported here.');
  };
  rec.start();
}

Примечания: continuous = true продолжает слушать несколько предложений подряд, пока вы не вызовете stop(); после трёх тихих раундов он завершается с no-speech, чтобы забытый распознаватель не слушал вечно. stop() выдаёт услышанное; abort() отбрасывает его. Распознавание останавливается, когда приложение уходит в фон, как и при скрытии вкладки Chrome. Использовать микрофон таким образом могут только страницы, принадлежащие приложению (его собственные файлы или сайт создателя); любой другой сайт получает not-allowed. API отсутствует, если на телефоне нет службы распознавания речи, поэтому проверяйте его, как показано выше. Внутренние транспорты: __srStart, __srStop, __srAbort, __srAvailable; результаты приходят как appmint:sr. Приложения, созданные ИИ, используют speech.start() из @/lib/appmintNative.

speechSynthesis веб-стандарт#

window.speechSynthesis + SpeechSynthesisUtterance

Пример

Читает текст вслух собственным движком синтеза речи телефона, используя обычный Web Speech API.

Возвращает: speechSynthesis.speak(u) ничего не возвращает; о том, что произошло, сообщают события высказывания start / boundary / end / error / pause / resume (как свойства onstart … или через addEventListener). getVoices() синхронно возвращает массив { voiceURI, name, lang, localService, default }. Требуется: ничего. В Android WebView нет собственного работающего речевого API; приложение устанавливает этот при запуске документа в каждом фрейме.

Произнесите фразу и узнайте, когда она закончена:

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
};

Голоса появляются через мгновение после запуска. Прочитайте их снова, когда сработает voiceschanged:

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);
}

Чтение статьи вслух с одной кнопкой «Пауза / Продолжить», которая подсвечивает читаемый абзац:

var toggle = document.getElementById('pause-toggle');

toggle.onclick = function () {
  if (!('speechSynthesis' in window) || !speechSynthesis.speaking) return;
  if (speechSynthesis.paused) speechSynthesis.resume();   // continues from the last word
  else speechSynthesis.pause();
};

function readArticle() {
  if (!('speechSynthesis' in window)) return;
  speechSynthesis.cancel();                               // start over
  document.querySelectorAll('article p').forEach(function (p) {
    var u = new SpeechSynthesisUtterance(p.textContent);
    u.onstart = function () { p.classList.add('reading'); };
    u.onend = u.onerror = function () { p.classList.remove('reading'); };
    u.onpause = function () { toggle.textContent = 'Resume'; };
    u.onresume = function () { toggle.textContent = 'Pause'; };
    speechSynthesis.speak(u);                             // paragraphs queue in order
  });
  toggle.textContent = 'Pause';
}

Примечания:

  • volume (0-1), voice (точный голос из getVoices()), rate, pitch и lang — всё это доходит до движка телефона. При наличии voice и lang побеждает голос; если нет ни того, ни другого, говорит голос движка по умолчанию.
  • boundary срабатывает перед каждым словом с charIndex / charLength (Android 8+; движки, которые не сообщают позиции слов, их не отправляют).
  • pause() / resume(): Движок Android не умеет приостанавливаться посреди слова, поэтому pause() останавливает его, а resume() произносит остаток фразы начиная с последнего слова, о котором сообщил движок (срабатывают события pause / resume). Если движок не сообщает позиции слов, resume() начинает это высказывание заново с начала. speaking остаётся true во время паузы; pending равно true, пока ждут высказывания в очереди.
  • cancel() срабатывает error с error: 'interrupted' для произносимого высказывания и 'canceled' для стоящих в очереди, как велит стандарт, — не показывайте их как сбои.
  • Если на телефоне нет голосовых данных для языка, движок всё равно может говорить на языке по умолчанию. Сначала проверьте getVoices() для нужного языка.
  • Сырые события, на которых это построено, — appmint:tts (см. ttsSpeak). Они не нужны, если вы используете этот API.

__srAbort мост#

window.WebToApk.__srAbort(id: String)

SpeechRecognition.abort(): немедленно остановить и отбросить.

Пример

Описано вместе с WebToApk.__srStart — пример там показывает и этот.

Внутренний транспорт для webkitSpeechRecognition / SpeechRecognition (запуск, остановка, прерывание и наличие распознавателя на телефоне). Страницы используют стандартный API; никогда не вызывайте это напрямую, потому что методы __ могут измениться без предупреждения.

Возвращает: __srStart(id, options) возвращает "", когда начинается прослушивание, или имя ошибки Web Speech (not-allowed, service-not-allowed). Всё дальнейшее приходит как события appmint:sr: {id, type} для start/audiostart/speechstart/speechend/audioend/end, {id, type:'result', final, alternatives:[{transcript, confidence}]} и {id, type:'error', error}. Требуется: микрофон (включается по форме вызова).

Используйте публичный API:

const rec = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
rec.onresult = (e) => console.log(e.results[0][0].transcript);
rec.start();

Непрерывная диктовка с текстом в реальном времени и выбором языка:

const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SR) {
  const rec = new SR();
  rec.lang = 'en-US';
  rec.continuous = true;
  rec.interimResults = true;
  rec.onresult = (e) => {
    const r = e.results[e.results.length - 1];
    note.value = r[0].transcript + (r.isFinal ? '\n' : '');
  };
  micBtn.onclick = () => rec.start();
  stopBtn.onclick = () => rec.stop();
}

Сообщайте об ошибках, например об отказе в доступе к микрофону:

rec.onerror = (e) => {
  if (e.error === 'not-allowed' || e.error === 'service-not-allowed') showMessage('Allow the microphone to dictate.');
  else showMessage('Speech error: ' + e.error);
};
rec.onend = () => micBtn.classList.remove('live');

Примечания: Реализовано в оболочке приложения (SpeechRecognitionBridge.kt) поверх SpeechRecognizer Android. __srStart отклоняется для страниц, не принадлежащих приложению.

__srAvailable мост#

window.WebToApk.__srAvailable(): Boolean

Установлена ли служба распознавания речи (webkitSpeechRecognition существует только тогда).

Пример

Описано вместе с WebToApk.__srStart — пример там показывает и этот.

Внутренний транспорт для webkitSpeechRecognition / SpeechRecognition (запуск, остановка, прерывание и наличие распознавателя на телефоне). Страницы используют стандартный API; никогда не вызывайте это напрямую, потому что методы __ могут измениться без предупреждения.

Возвращает: __srStart(id, options) возвращает "", когда начинается прослушивание, или имя ошибки Web Speech (not-allowed, service-not-allowed). Всё дальнейшее приходит как события appmint:sr: {id, type} для start/audiostart/speechstart/speechend/audioend/end, {id, type:'result', final, alternatives:[{transcript, confidence}]} и {id, type:'error', error}. Требуется: микрофон (включается по форме вызова).

Используйте публичный API:

const rec = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
rec.onresult = (e) => console.log(e.results[0][0].transcript);
rec.start();

Непрерывная диктовка с текстом в реальном времени и выбором языка:

const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SR) {
  const rec = new SR();
  rec.lang = 'en-US';
  rec.continuous = true;
  rec.interimResults = true;
  rec.onresult = (e) => {
    const r = e.results[e.results.length - 1];
    note.value = r[0].transcript + (r.isFinal ? '\n' : '');
  };
  micBtn.onclick = () => rec.start();
  stopBtn.onclick = () => rec.stop();
}

Сообщайте об ошибках, например об отказе в доступе к микрофону:

rec.onerror = (e) => {
  if (e.error === 'not-allowed' || e.error === 'service-not-allowed') showMessage('Allow the microphone to dictate.');
  else showMessage('Speech error: ' + e.error);
};
rec.onend = () => micBtn.classList.remove('live');

Примечания: Реализовано в оболочке приложения (SpeechRecognitionBridge.kt) поверх SpeechRecognizer Android. __srStart отклоняется для страниц, не принадлежащих приложению.

__srStart мост#

window.WebToApk.__srStart(id: String, optionsJson: String): String

SpeechRecognition.start(): "" при запуске (события приходят как `appmint:sr`), иначе имя ошибки Web Speech. С проверкой источника: сказанное пользователем — это его данные.

Пример

Внутренний транспорт для webkitSpeechRecognition / SpeechRecognition (запуск, остановка, прерывание и наличие распознавателя на телефоне). Страницы используют стандартный API; никогда не вызывайте это напрямую, потому что методы __ могут измениться без предупреждения.

Возвращает: __srStart(id, options) возвращает "", когда начинается прослушивание, или имя ошибки Web Speech (not-allowed, service-not-allowed). Всё дальнейшее приходит как события appmint:sr: {id, type} для start/audiostart/speechstart/speechend/audioend/end, {id, type:'result', final, alternatives:[{transcript, confidence}]} и {id, type:'error', error}. Требуется: микрофон (включается по форме вызова).

Используйте публичный API:

const rec = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
rec.onresult = (e) => console.log(e.results[0][0].transcript);
rec.start();

Непрерывная диктовка с текстом в реальном времени и выбором языка:

const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SR) {
  const rec = new SR();
  rec.lang = 'en-US';
  rec.continuous = true;
  rec.interimResults = true;
  rec.onresult = (e) => {
    const r = e.results[e.results.length - 1];
    note.value = r[0].transcript + (r.isFinal ? '\n' : '');
  };
  micBtn.onclick = () => rec.start();
  stopBtn.onclick = () => rec.stop();
}

Сообщайте об ошибках, например об отказе в доступе к микрофону:

rec.onerror = (e) => {
  if (e.error === 'not-allowed' || e.error === 'service-not-allowed') showMessage('Allow the microphone to dictate.');
  else showMessage('Speech error: ' + e.error);
};
rec.onend = () => micBtn.classList.remove('live');

Примечания: Реализовано в оболочке приложения (SpeechRecognitionBridge.kt) поверх SpeechRecognizer Android. __srStart отклоняется для страниц, не принадлежащих приложению.

__srStop мост#

window.WebToApk.__srStop(id: String)

SpeechRecognition.stop(): прекратить слушать и выдать услышанное.

Пример

Описано вместе с WebToApk.__srStart — пример там показывает и этот.

Внутренний транспорт для webkitSpeechRecognition / SpeechRecognition (запуск, остановка, прерывание и наличие распознавателя на телефоне). Страницы используют стандартный API; никогда не вызывайте это напрямую, потому что методы __ могут измениться без предупреждения.

Возвращает: __srStart(id, options) возвращает "", когда начинается прослушивание, или имя ошибки Web Speech (not-allowed, service-not-allowed). Всё дальнейшее приходит как события appmint:sr: {id, type} для start/audiostart/speechstart/speechend/audioend/end, {id, type:'result', final, alternatives:[{transcript, confidence}]} и {id, type:'error', error}. Требуется: микрофон (включается по форме вызова).

Используйте публичный API:

const rec = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
rec.onresult = (e) => console.log(e.results[0][0].transcript);
rec.start();

Непрерывная диктовка с текстом в реальном времени и выбором языка:

const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SR) {
  const rec = new SR();
  rec.lang = 'en-US';
  rec.continuous = true;
  rec.interimResults = true;
  rec.onresult = (e) => {
    const r = e.results[e.results.length - 1];
    note.value = r[0].transcript + (r.isFinal ? '\n' : '');
  };
  micBtn.onclick = () => rec.start();
  stopBtn.onclick = () => rec.stop();
}

Сообщайте об ошибках, например об отказе в доступе к микрофону:

rec.onerror = (e) => {
  if (e.error === 'not-allowed' || e.error === 'service-not-allowed') showMessage('Allow the microphone to dictate.');
  else showMessage('Speech error: ' + e.error);
};
rec.onend = () => micBtn.classList.remove('live');

Примечания: Реализовано в оболочке приложения (SpeechRecognitionBridge.kt) поверх SpeechRecognizer Android. __srStart отклоняется для страниц, не принадлежащих приложению.

__ttsSpeak мост#

window.WebToApk.__ttsSpeak(json: String)

Полный вызов speak из полифила speechSynthesis: `{text, lang, rate, pitch, volume (0-1), voice (имя из getVoices() или ''), id}`. [ttsSpeak] остаётся для страниц, которые вызывают мост напрямую; в нём нет громкости и голоса.

Пример

Внутренний транспорт для speechSynthesis.speak(utterance): полное высказывание — текст, язык, скорость, высота тона, громкость и выбранный голос — одной JSON-строкой. Страницы используют стандартный API speechSynthesis; никогда не вызывайте это напрямую.

Возвращает: ничего. Ход выполнения приходит как события appmint:tts, которые слой speechSynthesis превращает в события высказывания start / boundary / end / error. Требуется: ничего.

Стандартный API, который это обеспечивает:

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);
}

Выбор голоса, который пользователь делает один раз; каждое следующее высказывание использует именно этот голос:

var picker = document.getElementById('voice-picker');

function listVoices() {
  var saved = localStorage.getItem('voiceName') || '';
  picker.innerHTML = '';
  speechSynthesis.getVoices().forEach(function (v) {
    picker.add(new Option(v.name + ' (' + v.lang + ')', v.name, false, v.name === saved));
  });
}
if ('speechSynthesis' in window) {
  listVoices();                                   // [] until the engine is ready
  speechSynthesis.onvoiceschanged = listVoices;
}
picker.onchange = function () { localStorage.setItem('voiceName', picker.value); };

function speakWithSavedVoice(text) {
  var name = localStorage.getItem('voiceName');
  var voice = speechSynthesis.getVoices().filter(function (v) { return v.name === name; })[0];
  var u = new SpeechSynthesisUtterance(text);
  if (voice) u.voice = voice;                     // its language is used too when u.lang is empty
  speechSynthesis.speak(u);
}

Подсказки для тренировки: тихо и быстро для обратного отсчёта, на полной громкости и выше — для смены:

function cue(text, loud) {
  if (!('speechSynthesis' in window)) return;
  var u = new SpeechSynthesisUtterance(text);
  u.lang = 'en-US';
  u.rate = 1.4;                  // 0.1 .. 4
  u.pitch = loud ? 1.3 : 1;      // 0.1 .. 4
  u.volume = loud ? 1 : 0.4;     // 0 .. 1
  u.onerror = function (e) {
    if (e.error === 'synthesis-failed') console.log('Cue not spoken:', text);
  };
  speechSynthesis.speak(u);      // queues after the previous cue
}

cue('Ten seconds left', false);
cue('Switch sides!', true);

Примечания: JSON — это {text, lang, rate, pitch, volume, voice, id} (voice — имя из getVoices() или ''). Прежний ttsSpeak(text, lang, rate, pitch, id) остаётся для страниц, которые вызывают мост напрямую; в нём нет громкости и голоса.

ttsCancel мост#

window.WebToApk.ttsCancel()

Обеспечивает полифил window.speechSynthesis, который устанавливается при запуске документа в каждом фрейме, — страницам следует использовать СТАНДАРТНЫЙ API speechSynthesis, а эти вызовы будут сделаны за них. В Android WebView нет собственного работающего speechSynthesis, поэтому и существует такая подмена.

Описано в своей группе, Синтез речи, а не отдельно.

Пример

Немедленно останавливает речь и отбрасывает всё, что ещё ждало произнесения. Обычно страницы вызывают стандартный speechSynthesis.cancel(), который вызывает это за вас.

Возвращает: ничего. Требуется: ничего.

Публичный способ (используйте его):

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

Прямой вызов:

if (window.WebToApk && typeof window.WebToApk.ttsCancel === 'function') {
  window.WebToApk.ttsCancel();
}

Произносите только самый новый статус и отбрасывайте всё более старое, что ещё стоит в очереди (отслеживание доставки, счёт матча):

function announceLatest(text) {
  if (!('speechSynthesis' in window)) return;
  speechSynthesis.cancel();   // the old line gets error 'interrupted', queued ones 'canceled'
  var u = new SpeechSynthesisUtterance(text);
  u.onerror = function (e) {
    if (e.error === 'interrupted' || e.error === 'canceled') return;   // replaced, not a failure
    showToast('Could not read that aloud');
  };
  speechSynthesis.speak(u);
}

announceLatest('Your driver is 5 minutes away');
announceLatest('Your driver has arrived');   // cuts the first one off

Примечания: Речь, которая прервана или отброшена, не отправляет события appmint:tts end или error, поэтому onend высказывания не срабатывает после отмены. Если вы сами отслеживаете «сырые» идентификаторы ttsSpeak, забудьте их при вызове ttsCancel().

ttsGetVoices мост#

window.WebToApk.ttsGetVoices(): String

JSON-массив установленных голосов: [{name, lang, default}]. Пуст, пока движок не завершит инициализацию, — тогда страница получает 'voiceschanged' и запрашивает список снова, точно как в асинхронном контракте getVoices() в Chrome.

Пример

Перечисляет голоса синтеза речи, установленные на телефоне. Обычно страницы используют стандартный speechSynthesis.getVoices(), который вызывает это за вас.

Возвращает: JSON-строку синхронно: [{ "name": "en-us-x-sfg-local", "lang": "en-US", "default": false }, ...]. Пока движок не запущен, это "[]". Требуется: ничего.

Публичный способ (используйте его):

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;
}

Прямой вызов (помните 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;
  }
});

Говорите на языке телефона, если для него установлен голос, иначе пусть движок использует голос по умолчанию:

function bestSpeechLang() {
  if (!(window.WebToApk && typeof window.WebToApk.ttsGetVoices === 'function')) return '';
  var voices;
  try { voices = JSON.parse(window.WebToApk.ttsGetVoices()); } catch (e) { return ''; }
  var want = navigator.language || 'en-US';            // e.g. 'ta-IN'
  var exact = voices.filter(function (v) { return v.lang === want; })[0];
  if (exact) return exact.lang;
  var base = want.split('-')[0];
  var near = voices.filter(function (v) { return v.lang.split('-')[0] === base; })[0];
  return near ? near.lang : '';                        // '' = the engine's default voice
}

if (window.WebToApk && typeof window.WebToApk.ttsSpeak === 'function') {
  window.WebToApk.ttsSpeak(greetingText(), bestSpeechLang(), 1.0, 1.0, 'greeting');
}

Примечания: lang — тег BCP-47, например en-US или ta-IN. name голоса — собственный идентификатор движка, и при произнесении выбрать голос по имени нельзя; используется только его язык.

ttsIsSpeaking мост#

window.WebToApk.ttsIsSpeaking(): Boolean

Обеспечивает полифил window.speechSynthesis, который устанавливается при запуске документа в каждом фрейме, — страницам следует использовать СТАНДАРТНЫЙ API speechSynthesis, а эти вызовы будут сделаны за них. В Android WebView нет собственного работающего speechSynthesis, поэтому и существует такая подмена.

Описано в своей группе, Синтез речи, а не отдельно.

Пример

Сообщает, говорит ли движок синтеза речи телефона в данный момент. Обычно вместо этого страницы используют speechSynthesis.speaking или onend высказывания.

Возвращает: true / false синхронно. false до запуска движка. Требуется: ничего.

Публичный способ (используйте его):

if ('speechSynthesis' in window && speechSynthesis.speaking) {
  speechSynthesis.cancel();
}

Прямой вызов обращается к самому движку, поэтому он точен, даже если речь была запущена через «сырой» 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);

Подождите, пока приложение закончит говорить, прежде чем начинать запись, чтобы микрофон не ловил собственный голос приложения:

function whenQuiet(fn) {
  if (!(window.WebToApk && typeof window.WebToApk.ttsIsSpeaking === 'function')) { fn(); return; }
  (function check() {
    if (window.WebToApk.ttsIsSpeaking()) setTimeout(check, 300);
    else fn();
  })();
}

document.getElementById('answer-btn').addEventListener('click', function () {
  whenQuiet(startRecordingAnswer);   // e.g. after the question was read aloud
});

Примечания: Не опрашивайте слишком часто; событие (onend или appmint:tts с event: 'end') лучше, если вы сами запустили речь.

ttsSpeak мост#

window.WebToApk.ttsSpeak(text: String, lang: String, rate: Double, pitch: Double, utteranceId: String)

Обеспечивает полифил window.speechSynthesis, который устанавливается при запуске документа в каждом фрейме, — страницам следует использовать СТАНДАРТНЫЙ API speechSynthesis, а эти вызовы будут сделаны за них. В Android WebView нет собственного работающего speechSynthesis, поэтому и существует такая подмена.

Описано в своей группе, Синтез речи, а не отдельно.

Пример

Начинает произносить текст движком синтеза речи телефона. Обычно страницы используют стандартный API speechSynthesis, который вызывает это за вас.

Возвращает: ничего. Ход выполнения приходит позже как событие appmint:tts (и хук window.onAppMintTts(detail)) с detail = { id, event }, где event — это 'start', 'end' или 'error', а id — переданный вами utteranceId. Требуется: ничего.

Публичный способ (используйте его):

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);
}

Прямой вызов, если нужны собственные идентификаторы. Аргументы: 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'); });

Прочитайте список шагов по порядку со своими идентификаторами и подсвечивайте произносимый шаг:

var steps = Array.prototype.slice.call(document.querySelectorAll('.recipe-step'));

window.addEventListener('appmint:tts', function (e) {
  var d = e.detail || {};
  var m = /^recipe_(\d+)$/.exec(d.id || '');
  if (!m) return;                                  // not one of ours
  var el = steps[Number(m[1])];
  if (d.event === 'start') el.classList.add('reading');
  if (d.event === 'end' || d.event === 'error') el.classList.remove('reading');
});

document.getElementById('read-recipe').addEventListener('click', function () {
  if (!(window.WebToApk && typeof window.WebToApk.ttsSpeak === 'function')) return;
  steps.forEach(function (el, i) {
    // '' = the engine's default voice; 0.9 = a little slower for cooking
    window.WebToApk.ttsSpeak(el.textContent, '', 0.9, 1.0, 'recipe_' + i);
  });
});

Примечания: Высказывания встают в очередь одно за другим. lang может быть '' (голос движка по умолчанию); rate и pitch ограничиваются диапазоном 0,1–4. Если движок ещё запускается, текст ждёт и произносится, когда он готов; если движок запустить не удаётся, каждое ожидающее высказывание получает 'error'. Используйте собственный префикс идентификатора; идентификаторы, начинающиеся с wta_u, принадлежат слою speechSynthesis. На Android 8+ appmint:tts также передаёт {id, event: 'boundary', charIndex, charLength} перед каждым словом. В этом прямом вызове нет громкости и голоса — они есть в speechSynthesis.

ttsWarmUp мост#

window.WebToApk.ttsWarmUp()

Обеспечивает полифил window.speechSynthesis, который устанавливается при запуске документа в каждом фрейме, — страницам следует использовать СТАНДАРТНЫЙ API speechSynthesis, а эти вызовы будут сделаны за них. В Android WebView нет собственного работающего speechSynthesis, поэтому и существует такая подмена.

Описано в своей группе, Синтез речи, а не отдельно.

Пример

Заранее запускает движок синтеза речи телефона, чтобы первый speak() не запаздывал, а getVoices() заполнился. Слой speechSynthesis приложения уже вызывает это при запуске страницы, поэтому страницам это почти никогда не нужно.

Возвращает: ничего. Когда движок готов, срабатывает событие appmint:tts с detail = { id: '', event: 'voiceschanged' } (и выполняется speechSynthesis.onvoiceschanged). Требуется: ничего.

Публичный способ (используйте его): просто читайте голоса, когда они готовы.

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

Прямой вызов, затем дождитесь события готовности:

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
}

Запустите движок, когда открывается экран «Прочитать вслух», и включите его кнопку, когда движок готов:

var readBtn = document.getElementById('read-aloud');

function engineReady() {
  try { return JSON.parse(window.WebToApk.ttsGetVoices()).length > 0; } catch (e) { return false; }
}

function openReaderScreen() {
  showReaderScreen();
  if (!(window.WebToApk && typeof window.WebToApk.ttsWarmUp === 'function')) return;
  readBtn.disabled = !engineReady();       // already started earlier? enable right away
  window.WebToApk.ttsWarmUp();             // no-op when the engine is already running
}

window.addEventListener('appmint:tts', function (e) {
  if (e.detail && e.detail.event === 'voiceschanged') readBtn.disabled = false;
});

Примечания: voiceschanged срабатывает один раз, когда движок завершает запуск. Если он уже запущен (например, потому что его «разогрел» слой speechSynthesis), вы не получите его снова; вызовите ttsGetVoices() напрямую.