UNPKG

apphouse

Version:

Component library for React that uses observable state management and theme-able components.

309 lines (280 loc) 8.73 kB
import { makeAutoObservable } from 'mobx'; import { List } from '../models/List'; import { IUtteranceOptions, Utterance } from './textToSpeech/Utterance'; import { getUniqueId } from '../utils/string/getUniqueId'; type TextToSpeechStatusType = 'speaking' | 'paused' | 'cancelled' | 'idle'; /** * A class that represents a text to speech synthesizer * It is a wrapper around the SpeechSynthesis class, that handles multiple utterances */ export class TextToSpeech { synthesizer: SpeechSynthesis | null = null; selectedVoiceIndex: number; utterances: List<Utterance>; currentUtterance: Utterance | null; textToSpeak: string | null = null; status: TextToSpeechStatusType; constructor() { this.selectedVoiceIndex = 0; this.synthesizer = window.speechSynthesis; this.utterances = new List<Utterance>(); this.currentUtterance = null; this.textToSpeak = null; this.status = 'idle'; const _cancel = this.cancel; window.addEventListener('beforeunload', function () { console.log('beforeunload'); // Stop speech synthesis when refreshing the window _cancel(); }); makeAutoObservable(this); } get voiceList() { return this.synthesizer ?.getVoices() .filter( (voice) => voice.lang.includes('pt') || voice.lang.includes('en') || voice.lang.includes('es') ); } get voices() { const voices = this.voiceList; return ( voices?.map((voice, index) => ({ label: `${voice.name} (${voice.lang}) `, value: index, details: voice })) || [] ); } setSelectedVoiceIndex = (voiceIndex: number) => { this.selectedVoiceIndex = voiceIndex; }; private init = () => { if ('speechSynthesis' in window) { this.synthesizer = window.speechSynthesis; } }; /** * This method will immediately pause any utterances that are being spoken. */ pause = () => { try { if (this.synthesizer?.speaking) { this.synthesizer?.pause(); this.setStatus('paused'); } } catch (error) { console.error(error); } }; /** * This method will cause the browser to resume speaking an * utterance that was previously paused. */ resume = () => { if (this.synthesizer?.paused) { try { this.synthesizer.resume(); this.setStatus('speaking'); } catch (error) { console.error(error); } } }; /** * This method will immediately stop any utterances * that are being spoken */ cancel = () => { try { if (this.synthesizer) { this.synthesizer?.cancel(); this.setStatus('cancelled'); } } catch (error) { console.error(error); } }; /** * Helper method to set a status so the status is observable * @param status the status to set */ private setStatus = (status: TextToSpeechStatusType) => { console.log('setting speech synthesis status', status); this.status = status; }; /** * Speaks the text passed in as an argument or the textToSpeak property. * If there is a current utterance, it will be cancelled and a new one * will be created. If it speaking, it will be ignored. * If an id is passed, it will be used to identify the utterance and speak that utterance. * @param text the text to speak */ speak = (text?: string, id?: string) => { const textToSpeak = text || this.textToSpeak || ''; // first we check if the synthesizer is available if (this.synthesizer === null) { // if not, we try to initialize it this.init(); } // get the current utterance const currentUtterance = this.currentUtterance; // there is a current utterance if (currentUtterance) { if (currentUtterance.id === id) { this.speakUtterance(currentUtterance, textToSpeak); } else { // if it is not the same, we cancel the current utterance // and switch to the utterance with the utterance id this.cancel(); if (id) { this.speakUtteranceId(id, textToSpeak); } else { this.speakUtterance(currentUtterance, textToSpeak); } } } else { // there's no current utterance, let's create a new utterance // and try speaking again this.createUtterance(id, text); this.speak(text, id); } }; private speakUtteranceId = (id: string, text: string) => { // check if utterance exists let utterance = this.utterances.get(id); if (utterance) { utterance.setText(text); this.setCurrentUtterance(utterance); } else { utterance = this.createUtterance(id, text); } this.speakUtterance(utterance, text); }; private speakUtterance = (utterance: Utterance, text: string) => { utterance.setText(text); const synth = this.synthesizer; if (!synth) { // if the synthesizer is not available, we try to initialize it this.init(); // and try speaking again this.speakUtterance(utterance, text); } else { if (synth.speaking) { if (synth.paused) { this.resume(); return; } else { // ignore as it is already speaking return; } } // let's cancel current speech synthesis this.cancel(); // set the current utterance const utteranceInstance = utterance.utterance; utteranceInstance && synth.speak(utteranceInstance); this.setStatus('speaking'); } }; restart = (utteranceId: string) => { this.synthesizer?.cancel(); const utterance = this.utterances.get(utteranceId); if (utterance) { utterance.restart(); this.speak(utterance.text, utteranceId); } }; setCurrentUtterance = (utterance: Utterance) => { this.currentUtterance = utterance; }; /** * Method to initialize an utterance. * If the utterance already exists it sets it as the current utterance. * If it doesn't exist, it creates a new utterance and sets it as the current utterance. * @param {string} id the id of the utterance, must be unique */ initUtterance = (id: string) => { const utterance = this.utterances.get(id); if (utterance) { this.setCurrentUtterance(utterance); } else { this.createUtterance(id); } }; /** * Method to set the rate of the utterance. * When setting the rate, unfortunately, the speech synthesis API * does not allow us to set the rate of an utterance on the fly. So we * have to cancel the current utterance and create a new one with the new rate. */ setRate = (id: string, rate: number) => { const wasPreviouslySpeaking = this.status === 'speaking'; // get the utterance const utterance = this.utterances.get(id); if (wasPreviouslySpeaking && utterance?.status === 'speaking') { // cancel current utterance this.cancel(); } if (utterance) { utterance.setRate(rate); // if it was previously speaking, we speak again if (wasPreviouslySpeaking) { this.speakUtteranceId(id, utterance.unread); } } }; /** * A method that sets the voice of the speech synthesis. * When setting a different voice, unfortunately, the speech synthesis API * does not allow us to set the voice of an utterance on the fly. So we * have to cancel the current utterance and create a new one with the new * voice. */ setVoice = (id: string, voice: SpeechSynthesisVoice) => { const wasPreviouslySpeaking = this.status === 'speaking'; // get the utterance const utterance = this.utterances.get(id); if (wasPreviouslySpeaking && utterance?.status === 'speaking') { // cancel current utterance this.cancel(); } if (utterance) { utterance.setVoice(voice); // if it was previously speaking, we speak again if (wasPreviouslySpeaking) { this.speakUtteranceId(id, utterance.unread); } } }; /** * Method to create a new utterance and add it to the list of utterances. * In addition, it also sets the current utterance to the newly created one. * It is used privately * @param id * @param text * @param options * @returns */ private createUtterance = ( id?: string, text?: string, options?: IUtteranceOptions ): Utterance => { const utterance = new Utterance( id || getUniqueId(), text || '', options || {} ); this.utterances.set(utterance); this.setCurrentUtterance(utterance); return utterance; }; setTextToSpeak = (text: string) => { this.textToSpeak = text; }; } export const textToSpeech = new TextToSpeech();