Text to speech urdu

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;
Scroll to Top