UNPKG

commontown-text-to-speech

Version:

Overview -------- This npm package provides a simple and convenient way to perform text-to-speech (TTS) synthesis in your web applications. It utilizes either the Web Speech API or an external API to convert text into spoken language.

299 lines (263 loc) 8.74 kB
import axios from 'axios'; const synth = window.speechSynthesis; let voices = []; let isSpeaking = false; let shouldContinue = true; let currentSpeech = null; let currentSentenceIndex = 0; let sentences = []; let currentAudioElement = null; // Track the current audio element enableAutoTTS(); function enableAutoTTS() { if (typeof window === 'undefined') { return; } const isiOS = navigator.platform && /iPad|iPhone|iPod|MacIntel/.test(navigator.platform); if (!isiOS) { return; } const simulateSpeech = () => { const lecture = new SpeechSynthesisUtterance('hello'); lecture.volume = 0; speechSynthesis.speak(lecture); document.removeEventListener('click', simulateSpeech); }; document.addEventListener('click', simulateSpeech); } window.speak = () => { const domain = window.domainData; let button = document.getElementById('speak-button'); if (button) { button.addEventListener('click', () => { if (!isSpeaking) { const text = document.getElementById('text-to-speak').value; const lang = document.getElementById('language-select').value; const rate = document.getElementById('rate').value; const pitch = document.getElementById('pitch').value; // console.log(lang); getAudio({ domain, text, lang, rate, pitch }); } // If already speaking, do nothing }); } }; function waitForVoices() { return new Promise((resolve, reject) => { if (!synth) { reject(new Error('Speech synthesis is not supported in this browser.')); // return; } let voices = synth.getVoices(); if (voices.length !== 0) { resolve(voices); } else { synth.onvoiceschanged = () => { voices = synth.getVoices(); if (voices.length !== 0) { resolve(voices); } }; } }); } // Populate the voices list when available async function populateVoiceList() { try { voices = await waitForVoices(); // console.log('Voices available:', voices); } catch (error) { console.error(error.message); } } if (typeof window !== 'undefined' && typeof window.speechSynthesis !== 'undefined') { if ('addEventListener' in window.speechSynthesis) { window.speechSynthesis.addEventListener('voiceschanged', function () { // The list of available voices has changed }); } else { //for safari window.speechSynthesis.onvoiceschanged = function () { // The list of available voices has changed }; } } // List of languages that force to call from backend const API_LANGUAGES = [ // 'ta', // Tamil 'ar', // Arabic 'bn', // Bengali 'gu', // Gujarati 'pa', // Punjabi 'hi', // Hindi 'ur', // Urdu 'ur-PK', //Urdu (Pakistan) ]; function isAndroidDevice() { return /Android/i.test(navigator.userAgent); } export const getAudio = async (ttsParameter, onComplete) => { const { domain, text, lang, rate = 1, pitch = 1, voice = '', fetchFromServer, mp3 } = ttsParameter; window.handleOnComplete = function () { if (onComplete && typeof onComplete === 'function') { onComplete(); } }; // If mp3 URL is provided, play it directly if (mp3) { playAudioFromUrl(mp3, onComplete); return; } if (!lang || !text) return; // If force fetch from server, skip local synthesis if (fetchFromServer) { // console.log("Force fetching TTS from server."); getAudioFromApi({ domain, text, lang, rate, voice }, onComplete); return; } if (API_LANGUAGES.includes(lang)) { // console.log("Using API for TTS or no speech synthesis available."); getAudioFromApi({ domain, text, lang, rate, voice }, onComplete); return; } else if (isAndroidDevice() && typeof window.appInterface !== 'undefined') { const postMsg = { text: text, lang: lang, action: 'read', rate: rate }; // console.log("Sending post message to Android app interface:", postMsg); window.appInterface.postMessage(JSON.stringify(postMsg)); return; } if (voices.length === 0) await populateVoiceList(); isSpeaking = true; shouldContinue = true; currentSentenceIndex = 0; sentences = text.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [text]; const speakSentence = () => { if (currentSentenceIndex >= sentences.length || !shouldContinue) { isSpeaking = false; if (onComplete && typeof onComplete === 'function') { onComplete(); } return; } const sentence = sentences[currentSentenceIndex].trim(); const msg = new SpeechSynthesisUtterance(sentence); msg.rate = rate; msg.pitch = pitch; if (lang === 'ms') { msg.voice = voices.find((v) => v.lang === 'ms-MY') || voices.find((v) => v.lang === 'id-ID') || voices.find((v) => v.lang === 'ms_MY') || voices.find((v) => v.lang === 'in_ID'); msg.lang = 'id-ID'; } else if (lang === 'en') { msg.voice = voices.find((v) => v.lang === 'en-US' && v.name === 'Samantha') || voices.find((v) => v.lang === 'en-GB' && v.name === 'Daniel') || voices.find((v) => v.lang === 'en-US' && v.name === 'Junior') || voices.find((v) => v.lang === 'en-GB') || voices.find((v) => v.lang === 'en-US') || voices.find((v) => v.lang === 'en_GB') || voices.find((v) => v.lang === 'en_US'); msg.lang = 'en-GB'; } else if (lang === 'zh') { msg.voice = voices.find((v) => v.lang.includes('zh-CN')) || voices.find((v) => v.lang.includes('zh')); msg.lang = 'zh-CN'; } msg.onend = () => { currentSentenceIndex++; speakSentence(); }; msg.onerror = (event) => { console.error('Speech synthesis error:', event); isSpeaking = false; }; currentSpeech = msg; if (msg.voice) { window.speechSynthesis.speak(msg); } else { getAudioFromApi({ domain, text: msg.text, lang, rate, voice }, onComplete); currentSentenceIndex++; speakSentence(); } }; speakSentence(); }; export const stopSpeech = () => { // Stop speech synthesis if active if (synth && isSpeaking) { synth.cancel(); } // Stop audio playback if active if (currentAudioElement) { currentAudioElement.pause(); currentAudioElement.currentTime = 0; currentAudioElement = null; } shouldContinue = false; isSpeaking = false; currentSpeech = null; currentSentenceIndex = 0; }; export const getAudioFromApi = (ttsParameter, onComplete) => { const { domain, text, lang, rate, voice } = ttsParameter; const params = new URLSearchParams(); let speed_type = 'normal'; if (rate <= 0.5) { speed_type = 'very_slow'; } else if (rate >= 1.5) { speed_type = 'very_fast'; } if (lang) params.set('lang', lang); if (text) params.set('text', text); if (speed_type) params.set('speed_type', speed_type); if (voice) params.set('voice', voice); const url = `${domain}/_ca/speech/getAudio?${params.toString()}`; fetch(url, { method: 'GET', credentials: 'include', }) .then(async (response) => { if (response.ok && response.status === 200) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); playAudioFromUrl(url, onComplete); } }) .catch((error) => { console.error('Error fetching audio from API:', error); if (typeof onComplete === 'function') { onComplete(); } }); }; // Helper function to play audio from URL function playAudioFromUrl(url, onComplete) { // Stop any currently playing audio if (currentAudioElement) { currentAudioElement.pause(); currentAudioElement.currentTime = 0; } isSpeaking = true; currentAudioElement = new Audio(url); // First try to play immediately (might work if user has interacted with page) currentAudioElement .play() .then(() => { // Successfully playing currentAudioElement.onended = () => { isSpeaking = false; currentAudioElement = null; if (typeof onComplete === 'function') { onComplete(); } }; currentAudioElement.onerror = () => { isSpeaking = false; currentAudioElement = null; if (typeof onComplete === 'function') { onComplete(); } }; }) .catch((err) => { console.warn('Initial playback blocked:', err); isSpeaking = false; currentAudioElement = null; }); } populateVoiceList();