File size: 4,281 Bytes
9b73f19
57db340
4677012
57db340
c13b073
 
fc3f34d
d1693f1
fc3f34d
 
 
 
 
9b73f19
4677012
fc3f34d
 
 
 
 
57db340
 
 
 
 
 
 
 
 
 
 
 
 
 
fc3f34d
 
9b73f19
4677012
fc3f34d
9b73f19
fc3f34d
 
 
4677012
fc3f34d
 
9b73f19
fc3f34d
9b73f19
fc3f34d
 
 
9b73f19
fc3f34d
57db340
 
 
 
 
 
 
fc3f34d
 
 
 
 
 
 
9b73f19
fc3f34d
 
c13b073
4677012
fc3f34d
c13b073
 
 
 
 
 
4677012
9b73f19
c13b073
fc3f34d
4677012
 
 
fc3f34d
 
 
4677012
9b73f19
fc3f34d
9b73f19
c13b073
57db340
 
 
 
 
 
 
c13b073
57db340
c13b073
 
 
 
 
57db340
 
 
c13b073
 
57db340
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import os
import time
import tempfile
import warnings
import asyncio
import edge_tts
from transformers import pipeline

# -------------------------------------------------------------------------
# Speech-to-Text (STT): Whisper-small running locally on CPU
# - ~242MB model, good Hindi accuracy, no API dependency
# -------------------------------------------------------------------------
_stt_pipeline = None


def _get_stt_pipeline():
    """Lazily loads the Whisper-small pipeline (only once on first call)."""
    global _stt_pipeline
    if _stt_pipeline is None:
        print("Loading Whisper-small model for STT (this takes ~30s on first run)...")
        # Suppress the duplicate logits-processor warning that appears
        # when language/task are passed alongside the model's own processor setup.
        with warnings.catch_warnings():
            warnings.filterwarnings(
                "ignore",
                message="A custom logits processor of type",
                category=UserWarning,
            )
            _stt_pipeline = pipeline(
                task="automatic-speech-recognition",
                model="openai/whisper-small",
                device="cpu",
                generate_kwargs={"language": "hindi", "task": "transcribe"},
            )
        print("Whisper-small ready.")
    return _stt_pipeline


def speech_to_text(audio_filepath: str) -> str:
    """
    Transcribes a Hindi audio recording into Hindi text (Devanagari).

    Args:
        audio_filepath: Path to the audio file (wav/mp3/webm/ogg).
    Returns:
        Transcribed Hindi string, or an error message beginning with 'Error:'.
    """
    if not audio_filepath:
        return ""
    if not os.path.exists(audio_filepath):
        return f"Error: audio file not found at {audio_filepath}"

    try:
        pipe = _get_stt_pipeline()
        with warnings.catch_warnings():
            warnings.filterwarnings(
                "ignore",
                message="A custom logits processor of type",
                category=UserWarning,
            )
            result = pipe(audio_filepath)
        transcript = result.get("text", "").strip()
        if not transcript:
            return "Error: Could not transcribe audio - please speak clearly and try again."
        return transcript
    except Exception as exc:
        print(f"[STT Error] {exc}")
        return f"Error: Could not transcribe audio ({exc})"


# -------------------------------------------------------------------------
# Text-to-Speech (TTS): Edge-TTS – high-quality neural Hindi voice
# - Saves to /tmp/ so it works on read-only deployments like HF Spaces
# -------------------------------------------------------------------------
async def _async_tts(text: str, output_path: str):
    """Async helper to run the Edge TTS communication."""
    communicate = edge_tts.Communicate(text, "hi-IN-SwaraNeural")
    await communicate.save(output_path)


def text_to_speech(text: str) -> str | None:
    """
    Converts Hindi text to a high-quality spoken MP3 file using edge-tts.

    Saves into /tmp/ to ensure write access regardless of the deployment
    environment (HF Spaces /app directory is read-only).

    Args:
        text: Hindi text in Devanagari.
    Returns:
        Absolute path to the saved MP3 file, or None on failure.
    """
    if not text or text.startswith("Error:"):
        return None
    
    max_retries = 3
    for attempt in range(max_retries):
        try:
            # Use a named temp file so Gradio can serve it correctly
            tmp = tempfile.NamedTemporaryFile(
                suffix=".mp3", delete=False, dir="/tmp"
            )
            output_path = tmp.name
            tmp.close()
            
            # Run the async edge-tts code synchronously
            asyncio.run(_async_tts(text, output_path))
            return output_path
            
        except Exception as exc:
            err = str(exc)
            if "429" in err and attempt < max_retries - 1:
                wait = 2 ** attempt
                print(f"[TTS] Rate-limited, retrying in {wait}s... (attempt {attempt + 1}/{max_retries})")
                time.sleep(wait)
            else:
                print(f"[TTS Error] {exc}")
                return None