import React, { useState, useEffect, useRef } from 'react';
import { Play, Pause, Square, RefreshCw, Volume2, Settings, Type } from 'lucide-react';
const UrduTTS = () => {
// State
const [text, setText] = useState('');
const [voices, setVoices] = useState([]);
const [selectedVoice, setSelectedVoice] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [currentSentenceIndex, setCurrentSentenceIndex] = useState(0);
const [sentences, setSentences] = useState([]);
const [rate, setRate] = useState(1.0);
const [pitch, setPitch] = useState(1.0);
const [showSettings, setShowSettings] = useState(false);
// Refs for managing playback loop
const synth = useRef(window.speechSynthesis);
const utteranceRef = useRef(null);
// Load Voices
useEffect(() => {
const loadVoices = () => {
let availableVoices = synth.current.getVoices();
setVoices(availableVoices);
// Try to auto-select an Urdu voice
const urduVoice = availableVoices.find(
(v) => v.lang.includes('ur') || v.name.toLowerCase().includes('urdu')
);
if (urduVoice) {
setSelectedVoice(urduVoice);
} else {
// Fallback to first available if no Urdu found (user might need to install pack)
if (availableVoices.length > 0) setSelectedVoice(availableVoices[0]);
}
};
loadVoices();
// Chrome requires this event listener
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = loadVoices;
}
return () => {
synth.current.cancel();
};
}, []);
// Split text into chunks (sentences) using Urdu and Latin punctuation
const chunkText = (inputText) => {
if (!inputText) return [];
// Split by Urdu full stop (۔), Question mark (؟), Latin ., ?, !, and newlines
// We keep the delimiters to maintain sentence structure
const regex = /([۔؟!\.\?\n]+)/;
const parts = inputText.split(regex);
const cleanSentences = [];
for (let i = 0; i < parts.length; i += 2) {
const sentence = parts[i];
const delimiter = parts[i + 1] || '';
if (sentence.trim().length > 0) {
cleanSentences.push(sentence + delimiter);
}
}
return cleanSentences;
};
const handlePlay = () => {
if (isPaused) {
resume();
return;
}
if (!text.trim()) return;
// Reset and start new
synth.current.cancel();
const chunks = chunkText(text);
setSentences(chunks);
setCurrentSentenceIndex(0);
setIsPlaying(true);
setIsPaused(false);
speakChunk(0, chunks);
};
const speakChunk = (index, currentChunks) => {
if (index >= currentChunks.length) {
setIsPlaying(false);
setCurrentSentenceIndex(0);
return;
}
const chunkText = currentChunks[index];
const utterance = new SpeechSynthesisUtterance(chunkText);
if (selectedVoice) {
utterance.voice = selectedVoice;
}
utterance.rate = rate;
utterance.pitch = pitch;
// utterance.lang = 'ur-PK'; // Force Urdu lang hint
utterance.onend = () => {
setCurrentSentenceIndex(prev => {
const next = prev + 1;
if (isPlaying) { // Check if we should continue
speakChunk(next, currentChunks);
}
return next;
});
};
utterance.onerror = (e) => {
console.error("Speech error", e);
setIsPlaying(false);
};
utteranceRef.current = utterance;
synth.current.speak(utterance);
};
const handlePause = () => {
synth.current.pause();
setIsPaused(true);
setIsPlaying(false);
};
const resume = () => {
synth.current.resume();
setIsPaused(false);
setIsPlaying(true);
};
const handleStop = () => {
synth.current.cancel();
setIsPlaying(false);
setIsPaused(false);
setCurrentSentenceIndex(0);
};
return (
<div className="min-h-screen bg-gray-50 text-gray-800 font-sans p-4 md:p-8">
{/* Google Fonts Import for Noto Nastaliq Urdu */}
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu:wght@400;700&family=Noto+Sans:wght@400;600&display=swap');
.font-urdu { font-family: 'Noto Nastaliq Urdu', serif; }
`}</style>
<div className="max-w-4xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden">
{/* Header */}
<div className="bg-emerald-600 p-6 text-white flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Volume2 className="w-6 h-6" />
Urdu Text to Speech
</h1>
<p className="text-emerald-100 text-sm mt-1">Unlimited words playback engine</p>
</div>
<button
onClick={() => setShowSettings(!showSettings)}
className="p-2 hover:bg-emerald-700 rounded-full transition-colors"
>
<Settings className="w-6 h-6" />
</button>
</div>
{/* Settings Panel */}
{showSettings && (
<div className="bg-gray-100 p-6 border-b border-gray-200 grid grid-cols-1 md:grid-cols-2 gap-6 animate-in slide-in-from-top-4 duration-300">
<div>
<label className="block text-sm font-semibold text-gray-600 mb-2">Select Voice</label>
<select
value={selectedVoice?.name || ''}
onChange={(e) => {
const voice = voices.find(v => v.name === e.target.value);
setSelectedVoice(voice);
}}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 outline-none bg-white"
>
{voices.map((v, i) => (
<option key={i} value={v.name}>
{v.name} ({v.lang})
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-2">
Tip: Look for "Urdu", "Pakistan", or "Google" voices for best results.
</p>
</div>
<div className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-1">
<span className="font-semibold text-gray-600">Speed</span>
<span>{rate}x</span>
</div>
<input
type="range" min="0.5" max="2" step="0.1"
value={rate}
onChange={(e) => setRate(parseFloat(e.target.value))}
className="w-full accent-emerald-600 h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer"
/>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="font-semibold text-gray-600">Pitch</span>
<span>{pitch}</span>
</div>
<input
type="range" min="0.5" max="2" step="0.1"
value={pitch}
onChange={(e) => setPitch(parseFloat(e.target.value))}
className="w-full accent-emerald-600 h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer"
/>
</div>
</div>
</div>
)}
{/* Main Content Area */}
<div className="p-6 md:p-8 space-y-6">
{/* Input Area */}
<div className="relative">
<div className="absolute top-3 left-3 flex gap-2">
<span className="bg-gray-100 text-xs px-2 py-1 rounded text-gray-500">
{text.length} chars
</span>
</div>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="یہاں اپنا اردو متن لکھیں..."
className="w-full h-64 p-6 text-xl md:text-2xl leading-loose border-2 border-gray-200 rounded-xl focus:border-emerald-500 focus:ring-4 focus:ring-emerald-50 outline-none resize-none font-urdu transition-all shadow-inner"
dir="rtl"
/>
{/* Overlay for highlighting (simple implementation) */}
{isPlaying && sentences.length > 0 && (
<div className="mt-4 p-4 bg-emerald-50 rounded-lg border border-emerald-100 text-center">
<span className="text-xs font-bold text-emerald-600 uppercase tracking-wider mb-2 block">Currently Reading</span>
<p className="text-xl font-urdu text-emerald-800 dir-rtl">
{sentences[currentSentenceIndex]}
</p>
</div>
)}
</div>
{/* Controls */}
<div className="flex flex-wrap items-center justify-center gap-4 py-4">
{!isPlaying && !isPaused ? (
<button
onClick={handlePlay}
disabled={!text}
className={`flex items-center gap-2 px-8 py-4 rounded-full text-white font-bold text-lg shadow-lg transform transition-all hover:scale-105 active:scale-95 ${!text ? 'bg-gray-400 cursor-not-allowed' : 'bg-emerald-600 hover:bg-emerald-700'}`}
>
<Play className="fill-current w-5 h-5" />
Play Text
</button>
) : (
<>
{isPaused ? (
<button
onClick={handlePlay}
className="flex items-center gap-2 px-8 py-4 rounded-full bg-emerald-600 text-white font-bold text-lg shadow-lg hover:bg-emerald-700 transform transition-all hover:scale-105 active:scale-95"
>
<Play className="fill-current w-5 h-5" />
Resume
</button>
) : (
<button
onClick={handlePause}
className="flex items-center gap-2 px-8 py-4 rounded-full bg-amber-500 text-white font-bold text-lg shadow-lg hover:bg-amber-600 transform transition-all hover:scale-105 active:scale-95"
>
<Pause className="fill-current w-5 h-5" />
Pause
</button>
)}
<button
onClick={handleStop}
className="flex items-center gap-2 px-6 py-4 rounded-full bg-red-100 text-red-600 font-bold text-lg hover:bg-red-200 transition-colors"
>
<Square className="fill-current w-5 h-5" />
Stop
</button>
</>
)}
<button
onClick={() => setText('')}
className="flex items-center gap-2 px-4 py-4 rounded-full text-gray-500 hover:bg-gray-100 transition-colors"
title="Clear Text"
>
<RefreshCw className="w-5 h-5" />
</button>
</div>
{/* Instructions / Footer */}
<div className="border-t border-gray-100 pt-6 text-center text-gray-400 text-sm">
<p>Paste unlimited text. The tool automatically splits it into chunks to prevent browser timeouts.</p>
{!voices.some(v => v.lang.includes('ur')) && (
<p className="mt-2 text-amber-600 bg-amber-50 inline-block px-3 py-1 rounded-full text-xs">
⚠️ No specific Urdu voice detected on this device. The accent may sound English.
</p>
)}
</div>
</div>
</div>
</div>
);
};
export default UrduTTS;