| |
| import os |
| import sys |
| import torch |
| import soundfile as sf |
| from huggingface_hub import hf_hub_download |
| from loguru import logger |
|
|
| |
| from sanskrit_phonemizer import devanagari_to_ipa |
|
|
| |
| try: |
| from kokoro.model import KModel |
| except ImportError: |
| |
| sys.path.append(os.path.join(os.path.dirname(__file__), 'kokoro')) |
| from kokoro.model import KModel |
|
|
| class SanskritKokoroTTS: |
| def __init__(self, repo_id='hexgrad/Kokoro-82M', device='cpu'): |
| logger.info(f"Initializing Kokoro KModel from repo '{repo_id}' on device '{device}'...") |
| self.repo_id = repo_id |
| self.device = device |
| |
| |
| self.model = KModel(repo_id=repo_id).to(device).eval() |
| self.voices = {} |
| |
| def get_voice_pack(self, voice_name): |
| """ |
| Loads the voice style pack (e.g. 'hm_omega', 'hf_alpha'). |
| Downloads from Hugging Face if not cached locally. |
| """ |
| if voice_name in self.voices: |
| return self.voices[voice_name] |
| |
| logger.info(f"Loading voice pack '{voice_name}'...") |
| if voice_name.endswith('.pt'): |
| f_path = voice_name |
| else: |
| f_path = hf_hub_download(repo_id=self.repo_id, filename=f'voices/{voice_name}.pt') |
| |
| pack = torch.load(f_path, map_location=self.device, weights_only=True) |
| self.voices[voice_name] = pack |
| return pack |
|
|
| def synthesize_chunk(self, ipa_text, voice_pack, speed=1.0): |
| """ |
| Synthesizes a single chunk of IPA phonemes (max 510 characters). |
| """ |
| if not ipa_text.strip(): |
| return None |
| |
| |
| if len(ipa_text) > 510: |
| logger.warning(f"Chunk too long ({len(ipa_text)} > 510). Truncating to 510 characters.") |
| ipa_text = ipa_text[:510] |
| |
| |
| |
| idx = max(0, min(len(ipa_text) - 1, len(voice_pack) - 1)) |
| style_vector = voice_pack[idx].unsqueeze(0).to(self.device) |
| |
| |
| with torch.no_grad(): |
| audio = self.model(ipa_text, style_vector[0], speed) |
| return audio |
|
|
| def synthesize(self, text, voice_name='hm_omega', speed=1.0, split_pattern=r'[।॥\n\.\?!]+'): |
| """ |
| Synthesizes Sanskrit Devanagari text. |
| Automatically splits long texts into clauses or sentences to fit Kokoro's context window, |
| synthesizes them on CPU, and concatenates the resulting audio. |
| """ |
| voice_pack = self.get_voice_pack(voice_name) |
| |
| |
| import re |
| segments = re.split(split_pattern, text) |
| segments = [s.strip() for s in segments if s.strip()] |
| |
| if not segments: |
| logger.warning("No text to synthesize.") |
| return None |
| |
| audio_chunks = [] |
| |
| for idx, segment in enumerate(segments): |
| |
| ipa_str = devanagari_to_ipa(segment) |
| if not ipa_str.strip(): |
| continue |
| |
| logger.info(f"Synthesizing segment {idx+1}/{len(segments)}:") |
| logger.info(f" Deva: {segment}") |
| logger.info(f" IPA: {ipa_str}") |
| |
| |
| audio = self.synthesize_chunk(ipa_str, voice_pack, speed) |
| if audio is not None: |
| audio_chunks.append(audio) |
| |
| |
| |
| silence = torch.zeros(int(24000 * 0.4)) |
| audio_chunks.append(silence) |
| |
| if not audio_chunks: |
| return None |
| |
| |
| final_audio = torch.cat(audio_chunks[:-1]) |
| return final_audio |
|
|
| def main(): |
| import argparse |
| parser = argparse.ArgumentParser(description="Sanskrit Text-to-Speech using Kokoro on CPU") |
| parser.add_argument("text", type=str, help="Sanskrit Devanagari text to synthesize") |
| parser.add_argument("-o", "--output", type=str, default="sanskrit_output.wav", help="Output WAV file path") |
| parser.add_argument("-v", "--voice", type=str, default="hm_omega", help="Voice name (e.g. hm_omega, hf_alpha)") |
| parser.add_argument("-s", "--speed", type=float, default=1.0, help="Speech speed multiplier") |
| |
| args = parser.parse_args() |
| |
| tts = SanskritKokoroTTS() |
| audio = tts.synthesize(args.text, voice_name=args.voice, speed=args.speed) |
| |
| if audio is not None: |
| sf.write(args.output, audio.numpy(), 24000) |
| logger.info(f"Saved synthesized Sanskrit audio to {args.output}") |
| else: |
| logger.error("Synthesis failed.") |
|
|
| if __name__ == '__main__': |
| main() |
|
|