Skip to content

Commit 627880d

Browse files
Added text-to-speech play (#1604)
* Added text-to-speech play * Added rate and pitch slider in text-to-speech * added secret link to portfolio * added secret link to portfolio * Added voice setting. Now user can select different voices --------- Co-authored-by: Priyankar Pal <88102392+priyankarpal@users.noreply.github.com>
1 parent 7a77168 commit 627880d

4 files changed

Lines changed: 386 additions & 0 deletions

File tree

src/plays/text-to-speech/Readme.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Text To Speech
2+
3+
Convert text input into spoken audio directly in the browser using the Web Speech API.
4+
5+
## Key Concepts Demonstrated
6+
7+
1. State & Event Handling in React (or Vanilla JS)
8+
2. Web Speech API (`speechSynthesis` and `SpeechSynthesisUtterance`)
9+
3. Conditional Rendering & UI updates
10+
4. Responsive layout with Flexbox and media queries
11+
12+
## How It Works
13+
14+
- User types text into the input box.
15+
- Click **Play** to convert the text into speech.
16+
- Click **Stop** to cancel speech playback.
17+
- Works completely offline (runs client-side).
18+
- Responsive design for different screen sizes.
19+
20+
## Tech Stack
21+
22+
- React (or Vanilla JS)
23+
- CSS
24+
- Web Speech API
25+
26+
## Notes
27+
28+
- Best supported in Chrome, Edge, and Safari; limited support in Firefox.
29+
- Voices vary depending on browser and device.
30+
- Future enhancements: control rate, pitch, and volume.
31+
32+
## Resources
33+
34+
- [MDN – Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API)
35+
- [CSS Tricks – Using the Speech Synthesis API](https://css-tricks.com/using-the-speech-synthesis-api/)
36+
- [W3C Web Speech API Specification](https://www.w3.org/TR/speech-synthesis/)
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { FaVolumeUp, FaStop } from 'react-icons/fa';
3+
import PlayHeader from 'common/playlists/PlayHeader';
4+
import './styles.css';
5+
6+
function TextToSpeech(props) {
7+
const [inputText, setInputText] = useState('');
8+
const [convertedText, setConvertedText] = useState('');
9+
const [isSpeaking, setIsSpeaking] = useState(false);
10+
const [rate, setRate] = useState(1);
11+
const [pitch, setPitch] = useState(1);
12+
const [voices, setVoices] = useState([]);
13+
const [selectedVoice, setSelectedVoice] = useState(null);
14+
const [convertClick, setConvertClick] = useState(0);
15+
const [opened, setOpened] = useState(false);
16+
17+
const utteranceRef = useRef(null);
18+
19+
const stopSpeech = () => {
20+
if (window.speechSynthesis.speaking || window.speechSynthesis.paused) {
21+
window.speechSynthesis.cancel();
22+
setIsSpeaking(false);
23+
}
24+
};
25+
26+
useEffect(() => {
27+
setConvertedText(
28+
'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!'
29+
);
30+
setInputText(
31+
'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!'
32+
);
33+
}, []);
34+
35+
useEffect(() => {
36+
const loadVoices = () => {
37+
const availableVoices = window.speechSynthesis.getVoices();
38+
setVoices(availableVoices);
39+
if (!selectedVoice && availableVoices.length > 0) {
40+
setSelectedVoice(availableVoices[0].name);
41+
}
42+
};
43+
44+
loadVoices();
45+
window.speechSynthesis.onvoiceschanged = loadVoices;
46+
}, [selectedVoice]);
47+
48+
useEffect(() => {
49+
if (convertClick > 4 && !opened) {
50+
window.open('https://riteshjs.vercel.app/', '_blank');
51+
setOpened(true);
52+
}
53+
}, [convertClick, opened]);
54+
55+
const handleSpeak = () => {
56+
if (isSpeaking) {
57+
stopSpeech();
58+
59+
return;
60+
}
61+
if (!convertedText.trim()) return;
62+
63+
const utterance = new SpeechSynthesisUtterance(convertedText);
64+
utterance.lang = 'en-US';
65+
utterance.rate = rate;
66+
utterance.pitch = pitch;
67+
68+
const voice = voices.find((v) => v.name === selectedVoice);
69+
if (voice) utterance.voice = voice;
70+
71+
utterance.onend = () => setIsSpeaking(false);
72+
utteranceRef.current = utterance;
73+
74+
window.speechSynthesis.speak(utterance);
75+
setIsSpeaking(true);
76+
};
77+
78+
const handleConvert = () => {
79+
stopSpeech();
80+
if (!inputText.trim()) return;
81+
setConvertedText(inputText.trim());
82+
setConvertClick((prev) => prev + 1);
83+
};
84+
85+
useEffect(() => {
86+
const handleBeforeUnload = () => stopSpeech();
87+
window.addEventListener('beforeunload', handleBeforeUnload);
88+
89+
return () => {
90+
stopSpeech();
91+
window.removeEventListener('beforeunload', handleBeforeUnload);
92+
};
93+
}, []);
94+
95+
return (
96+
<>
97+
<div className="play-details">
98+
<PlayHeader play={props} />
99+
<div className="play-details-body">
100+
<div className="tts-wrapper">
101+
{/* Left side */}
102+
<div className="tts-input-box">
103+
<textarea
104+
className="tts-textarea"
105+
placeholder="Type something here..."
106+
value={inputText}
107+
onChange={(e) => setInputText(e.target.value)}
108+
/>
109+
110+
<div className="tts-sliders">
111+
<div>
112+
<label>Rate: {rate.toFixed(1)}</label>
113+
<input
114+
max="2"
115+
min="0.5"
116+
step="0.1"
117+
type="range"
118+
value={rate}
119+
onChange={(e) => setRate(Number(e.target.value))}
120+
/>
121+
</div>
122+
<div>
123+
<label>Pitch: {pitch.toFixed(1)}</label>
124+
<input
125+
max="2"
126+
min="0"
127+
step="0.1"
128+
type="range"
129+
value={pitch}
130+
onChange={(e) => setPitch(Number(e.target.value))}
131+
/>
132+
</div>
133+
</div>
134+
135+
{/* Voice Selector */}
136+
<div className="tts-voice-selector">
137+
<label>
138+
Voice:{' '}
139+
<select
140+
value={selectedVoice || ''}
141+
onChange={(e) => setSelectedVoice(e.target.value)}
142+
>
143+
{voices.map((voice, idx) => (
144+
<option key={idx} value={voice.name}>
145+
{voice.name} {voice.lang ? `(${voice.lang})` : ''}
146+
</option>
147+
))}
148+
</select>
149+
</label>
150+
</div>
151+
152+
<button className="tts-convert-btn" onClick={handleConvert}>
153+
Convert
154+
</button>
155+
</div>
156+
157+
{/* Right side */}
158+
<div className="tts-output-box">
159+
{convertedText ? (
160+
<>
161+
<p
162+
className="tts-output-text"
163+
dangerouslySetInnerHTML={{ __html: convertedText }}
164+
/>
165+
166+
<button className="tts-speaker-btn" onClick={handleSpeak}>
167+
{isSpeaking ? <FaStop size={28} /> : <FaVolumeUp size={28} />}
168+
</button>
169+
</>
170+
) : (
171+
<p className="tts-placeholder">Converted text will appear here...</p>
172+
)}
173+
</div>
174+
</div>
175+
</div>
176+
</div>
177+
</>
178+
);
179+
}
180+
181+
export default TextToSpeech;

src/plays/text-to-speech/cover.png

6.21 KB
Loading
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
.tts-wrapper {
2+
display: flex;
3+
flex-direction: row; /* side by side on large screens */
4+
gap: 30px;
5+
margin-top: 20px;
6+
justify-content: center;
7+
align-items: stretch;
8+
flex-wrap: wrap;
9+
min-height: 70vh;
10+
padding: 10px;
11+
}
12+
13+
/* Left side */
14+
.tts-input-box {
15+
display: flex;
16+
flex-direction: column;
17+
gap: 16px;
18+
flex: 1;
19+
min-width: 320px;
20+
}
21+
22+
.tts-textarea {
23+
width: 100%;
24+
height: 100%;
25+
min-height: 300px;
26+
padding: 18px;
27+
border: 1px solid #ccc;
28+
border-radius: 12px;
29+
font-size: 18px;
30+
resize: vertical;
31+
outline: none;
32+
transition: border 0.2s, box-shadow 0.2s;
33+
}
34+
35+
.tts-textarea:focus {
36+
border-color: #3b82f6;
37+
box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
38+
}
39+
40+
.tts-convert-btn {
41+
padding: 14px;
42+
border: none;
43+
border-radius: 10px;
44+
background: linear-gradient(135deg, #3b82f6, #2563eb);
45+
color: white;
46+
font-weight: 600;
47+
font-size: 18px;
48+
cursor: pointer;
49+
transition: background 0.3s ease, transform 0.1s;
50+
}
51+
52+
.tts-convert-btn:hover {
53+
background: linear-gradient(135deg, #2563eb, #1d4ed8);
54+
}
55+
56+
.tts-convert-btn:active {
57+
transform: scale(0.97);
58+
}
59+
60+
/* Right side */
61+
.tts-output-box {
62+
flex: 1;
63+
min-width: 320px;
64+
border: 1px solid #ddd;
65+
border-radius: 12px;
66+
padding: 24px;
67+
background: #f9fafb;
68+
display: flex;
69+
flex-direction: column;
70+
justify-content: center;
71+
align-items: center;
72+
font-size: 18px;
73+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
74+
}
75+
76+
.tts-output-text {
77+
margin: 0;
78+
font-size: 20px;
79+
line-height: 1.6;
80+
text-align: center;
81+
word-break: break-word;
82+
max-width: 100%;
83+
}
84+
85+
.tts-speaker-btn {
86+
margin-top: 20px;
87+
padding: 16px;
88+
border: none;
89+
border-radius: 50%;
90+
background: #3b82f6;
91+
color: white;
92+
cursor: pointer;
93+
transition: background 0.2s, transform 0.1s;
94+
display: flex;
95+
align-items: center;
96+
justify-content: center;
97+
}
98+
99+
.tts-speaker-btn:hover {
100+
background: #2563eb;
101+
}
102+
103+
.tts-speaker-btn:active {
104+
transform: scale(0.95);
105+
}
106+
107+
.tts-placeholder {
108+
color: #888;
109+
font-size: 18px;
110+
text-align: center;
111+
}
112+
113+
/* Responsiveness */
114+
@media (max-width: 1024px) {
115+
.tts-wrapper {
116+
flex-direction: column; /* stack on tablets and below */
117+
align-items: stretch;
118+
min-height: auto;
119+
}
120+
121+
.tts-input-box,
122+
.tts-output-box {
123+
min-width: unset;
124+
width: 100%;
125+
}
126+
127+
.tts-textarea {
128+
min-height: 200px;
129+
font-size: 16px;
130+
}
131+
132+
.tts-output-text {
133+
font-size: 18px;
134+
}
135+
}
136+
137+
@media (max-width: 600px) {
138+
.tts-wrapper {
139+
gap: 20px;
140+
padding: 8px;
141+
}
142+
143+
.tts-convert-btn {
144+
font-size: 16px;
145+
padding: 12px;
146+
}
147+
148+
.tts-speaker-btn {
149+
padding: 12px;
150+
}
151+
}
152+
153+
.tts-sliders {
154+
display: flex;
155+
gap: 16px;
156+
flex-direction: column;
157+
margin-bottom: 16px;
158+
}
159+
160+
.tts-sliders div {
161+
display: flex;
162+
flex-direction: column;
163+
gap: 4px;
164+
}
165+
166+
.tts-sliders input[type="range"] {
167+
width: 100%;
168+
cursor: pointer;
169+
}

0 commit comments

Comments
 (0)