diff --git a/src/plays/text-to-speech/Readme.md b/src/plays/text-to-speech/Readme.md new file mode 100644 index 000000000..144eed4b8 --- /dev/null +++ b/src/plays/text-to-speech/Readme.md @@ -0,0 +1,36 @@ +# Text To Speech + +Convert text input into spoken audio directly in the browser using the Web Speech API. + +## Key Concepts Demonstrated + +1. State & Event Handling in React (or Vanilla JS) +2. Web Speech API (`speechSynthesis` and `SpeechSynthesisUtterance`) +3. Conditional Rendering & UI updates +4. Responsive layout with Flexbox and media queries + +## How It Works + +- User types text into the input box. +- Click **Play** to convert the text into speech. +- Click **Stop** to cancel speech playback. +- Works completely offline (runs client-side). +- Responsive design for different screen sizes. + +## Tech Stack + +- React (or Vanilla JS) +- CSS +- Web Speech API + +## Notes + +- Best supported in Chrome, Edge, and Safari; limited support in Firefox. +- Voices vary depending on browser and device. +- Future enhancements: control rate, pitch, and volume. + +## Resources + +- [MDN – Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API) +- [CSS Tricks – Using the Speech Synthesis API](https://css-tricks.com/using-the-speech-synthesis-api/) +- [W3C Web Speech API Specification](https://www.w3.org/TR/speech-synthesis/) diff --git a/src/plays/text-to-speech/TextToSpeech.jsx b/src/plays/text-to-speech/TextToSpeech.jsx new file mode 100644 index 000000000..3286a6d0f --- /dev/null +++ b/src/plays/text-to-speech/TextToSpeech.jsx @@ -0,0 +1,181 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { FaVolumeUp, FaStop } from 'react-icons/fa'; +import PlayHeader from 'common/playlists/PlayHeader'; +import './styles.css'; + +function TextToSpeech(props) { + const [inputText, setInputText] = useState(''); + const [convertedText, setConvertedText] = useState(''); + const [isSpeaking, setIsSpeaking] = useState(false); + const [rate, setRate] = useState(1); + const [pitch, setPitch] = useState(1); + const [voices, setVoices] = useState([]); + const [selectedVoice, setSelectedVoice] = useState(null); + const [convertClick, setConvertClick] = useState(0); + const [opened, setOpened] = useState(false); + + const utteranceRef = useRef(null); + + const stopSpeech = () => { + if (window.speechSynthesis.speaking || window.speechSynthesis.paused) { + window.speechSynthesis.cancel(); + setIsSpeaking(false); + } + }; + + useEffect(() => { + setConvertedText( + 'Hello there! This feature is powered by the Web Speech API, built by Ritesh. Try generating a few audios to unlock a secret. You can play with rate, pitch, and voice settings. Enjoy experimenting!' + ); + setInputText( + 'Hello there! This feature is powered by the Web Speech API, built by Ritesh. Try generating a few audios to unlock a secret. You can play with rate, pitch, and voice settings. Enjoy experimenting!' + ); + }, []); + + useEffect(() => { + const loadVoices = () => { + const availableVoices = window.speechSynthesis.getVoices(); + setVoices(availableVoices); + if (!selectedVoice && availableVoices.length > 0) { + setSelectedVoice(availableVoices[0].name); + } + }; + + loadVoices(); + window.speechSynthesis.onvoiceschanged = loadVoices; + }, [selectedVoice]); + + useEffect(() => { + if (convertClick > 4 && !opened) { + window.open('https://riteshjs.vercel.app/', '_blank'); + setOpened(true); + } + }, [convertClick, opened]); + + const handleSpeak = () => { + if (isSpeaking) { + stopSpeech(); + + return; + } + if (!convertedText.trim()) return; + + const utterance = new SpeechSynthesisUtterance(convertedText); + utterance.lang = 'en-US'; + utterance.rate = rate; + utterance.pitch = pitch; + + const voice = voices.find((v) => v.name === selectedVoice); + if (voice) utterance.voice = voice; + + utterance.onend = () => setIsSpeaking(false); + utteranceRef.current = utterance; + + window.speechSynthesis.speak(utterance); + setIsSpeaking(true); + }; + + const handleConvert = () => { + stopSpeech(); + if (!inputText.trim()) return; + setConvertedText(inputText.trim()); + setConvertClick((prev) => prev + 1); + }; + + useEffect(() => { + const handleBeforeUnload = () => stopSpeech(); + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + stopSpeech(); + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, []); + + return ( + <> +
+ +
+
+ {/* Left side */} +
+