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
JavaScript
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();