File size: 6,308 Bytes
e0d2309
 
 
782f6e2
e0d2309
782f6e2
 
44cee7a
56412a3
e0d2309
56412a3
 
 
 
 
 
e0d2309
 
44cee7a
 
 
 
 
 
 
 
 
 
 
 
 
e0d2309
782f6e2
44cee7a
 
782f6e2
44cee7a
e0d2309
44cee7a
 
 
 
 
e0d2309
44cee7a
782f6e2
 
e0d2309
56412a3
 
 
44cee7a
56412a3
44cee7a
e0d2309
 
 
 
 
 
44cee7a
56412a3
 
782f6e2
56412a3
 
 
 
 
 
44cee7a
56412a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782f6e2
e0d2309
44cee7a
 
56412a3
44cee7a
 
 
56412a3
 
 
 
 
 
e0d2309
44cee7a
 
 
e0d2309
44cee7a
 
 
e0d2309
 
782f6e2
44cee7a
782f6e2
44cee7a
 
 
 
 
 
 
 
e0d2309
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44cee7a
 
e0d2309
44cee7a
 
e0d2309
44cee7a
 
e0d2309
 
44cee7a
e0d2309
44cee7a
 
 
e0d2309
 
 
 
 
 
 
44cee7a
e0d2309
 
 
 
 
 
 
 
 
 
 
56412a3
44cee7a
 
56412a3
44cee7a
 
 
56412a3
 
44cee7a
56412a3
44cee7a
 
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""Kannada TTS β€” tier 1: sush0401/IndicF5-Kannada-Bedtime-v2 (fine-tuned).
Tier 2: facebook/mms-tts-kan (VITS, 16kHz, no voice cloning).
Tier 3: gTTS (Google, always works, generic Kannada voice).

ZeroGPU pattern: GPU models loaded to CPU at module scope so ZeroGPU packs their
tensors. Inside inference functions (called from @spaces.GPU), .to("cuda") is
called and ZeroGPU transfers packed tensors to GPU β€” no re-download needed.
"""
from __future__ import annotations
import logging
import os
import re
import tempfile
import numpy as np
import torch

logger = logging.getLogger(__name__)

_FINETUNE_HUB  = "sush0401/IndicF5-Kannada-Bedtime-v2"
_FALLBACK_HUB  = "facebook/mms-tts-kan"

_indic_model   = None   # IndicF5 fine-tuned (with voice cloning)
_mms_model     = None   # MMS-TTS fallback (no voice cloning)
_mms_tok       = None
_use_indic     = False  # set True after successful IndicF5 load
MMS_SR = 16_000


def _load_indic():
    global _indic_model, _use_indic
    from transformers import AutoModel
    logger.info(f"Loading IndicF5 from {_FINETUNE_HUB}")
    # Load to CPU β€” ZeroGPU packs these tensors.
    _indic_model = AutoModel.from_pretrained(
        _FINETUNE_HUB, trust_remote_code=True,
    )
    _use_indic = True
    logger.info("IndicF5 loaded OK")


def _load_mms():
    global _mms_model, _mms_tok
    from transformers import VitsModel, AutoTokenizer
    logger.info(f"Loading MMS-TTS from {_FALLBACK_HUB}")
    _mms_tok   = AutoTokenizer.from_pretrained(_FALLBACK_HUB)
    # Load to CPU β€” ZeroGPU packs these tensors.
    _mms_model = VitsModel.from_pretrained(_FALLBACK_HUB)
    logger.info("MMS-TTS loaded OK")


def _get_model():
    if not _use_indic and _mms_model is None:
        try:
            _load_indic()
        except Exception as e:
            logger.warning(f"IndicF5 load failed ({e}); trying MMS-TTS")
            try:
                _load_mms()
            except Exception as e2:
                logger.warning(f"MMS-TTS load failed too ({e2}); will use gTTS fallback")
    return _use_indic


# Pre-load to CPU at module scope so ZeroGPU packs the tensors.
try:
    _get_model()
except Exception:
    pass


def _split(text: str, max_chars: int = 200):
    parts = re.split(r"(?<=[.!?ΰ₯€])\s+|\n+", text.strip())
    out = []
    for p in parts:
        p = p.strip()
        if not p:
            continue
        while len(p) > max_chars:
            cut = p.rfind(" ", 0, max_chars)
            cut = cut if cut > 0 else max_chars
            out.append(p[:cut].strip())
            p = p[cut:].strip()
        out.append(p)
    return out or [text.strip()]


def _narrate_indic(ref_wav: str, kannada_text: str) -> tuple[np.ndarray, int]:
    model = _indic_model.to("cuda")  # ZeroGPU intercepts inside @spaces.GPU
    silence_sr = 24_000
    silence = np.zeros(int(0.55 * silence_sr), dtype=np.float32)
    chunks = []
    for sent in _split(kannada_text):
        kw = dict(ref_audio_path=ref_wav) if ref_wav and os.path.exists(ref_wav) else {}
        audio = model(sent, **kw)
        audio = np.asarray(audio, dtype=np.float32)
        if audio.size and float(np.max(np.abs(audio))) > 1.0:
            audio = audio / 32768.0
        if audio.size:
            chunks.append(audio)
            chunks.append(silence)
    return (np.concatenate(chunks) if chunks else np.array([], dtype=np.float32)), silence_sr


def _narrate_mms(kannada_text: str) -> tuple[np.ndarray, int]:
    model = _mms_model.to("cuda")  # ZeroGPU intercepts inside @spaces.GPU
    silence = np.zeros(int(0.55 * MMS_SR), dtype=np.float32)
    chunks = []
    for sent in _split(kannada_text):
        if not sent.strip():
            continue
        inputs = _mms_tok(sent, return_tensors="pt").to(model.device)
        with torch.no_grad():
            wav = model(**inputs).waveform
        audio = wav.squeeze().cpu().float().numpy()
        if audio.size:
            chunks.append(audio)
            chunks.append(silence)
    full = np.concatenate(chunks) if chunks else np.array([], dtype=np.float32)
    return full, MMS_SR


def _narrate_gtts(kannada_text: str) -> tuple[np.ndarray, int]:
    """Last-resort: gTTS Kannada (generic Google voice, no GPU needed)."""
    import io
    import librosa
    from gtts import gTTS
    logger.info("Using gTTS Kannada fallback")
    tts = gTTS(text=kannada_text, lang="kn", slow=True)
    fd, mp3_path = tempfile.mkstemp(suffix=".mp3")
    os.close(fd)
    try:
        tts.save(mp3_path)
        data, sr = librosa.load(mp3_path, sr=22050, mono=True)
    finally:
        try:
            os.unlink(mp3_path)
        except OSError:
            pass
    return data.astype(np.float32), sr


def narrate_kannada(ref_wav: str, ref_text: str, kannada_text: str,
                    mood: str = "", energy: float = 0.45) -> str:
    """Narrate Kannada text. Tries: IndicF5 β†’ MMS-TTS β†’ gTTS. Returns WAV path."""
    text = (kannada_text or "").strip()
    if not text:
        raise ValueError("No Kannada text to narrate.")

    use_indic = _get_model()
    full = np.array([], dtype=np.float32)
    sr   = MMS_SR

    # Tier 1: user's fine-tuned IndicF5
    if use_indic:
        try:
            full, sr = _narrate_indic(ref_wav or "", text)
            logger.info("IndicF5 narration OK")
        except Exception as e:
            logger.warning(f"IndicF5 narration failed ({e}); trying MMS-TTS")

    # Tier 2: MMS-TTS-Kan
    if not full.size and _mms_model is not None:
        try:
            full, sr = _narrate_mms(text)
            logger.info("MMS-TTS narration OK")
        except Exception as e:
            logger.warning(f"MMS-TTS narration failed ({e}); trying gTTS")

    # Tier 3: gTTS (always works, no GPU needed)
    if not full.size:
        try:
            full, sr = _narrate_gtts(text)
            logger.info("gTTS narration OK")
        except Exception as e:
            raise RuntimeError(f"All Kannada TTS tiers failed. Last error: {e}") from e

    if not full.size:
        raise RuntimeError("TTS produced no audio.")

    peak = float(np.max(np.abs(full)))
    if peak > 0:
        full = full / peak * 0.92

    import soundfile as sf
    fd, path = tempfile.mkstemp(prefix="bedtime_kn_", suffix=".wav")
    os.close(fd)
    sf.write(path, full, sr)
    return path