Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| import os | |
| from pathlib import Path | |
| import sys | |
| from uuid import uuid4 | |
| import gradio as gr | |
| try: | |
| import spaces | |
| except ModuleNotFoundError: # Local and paid-GPU runtimes do not require the package. | |
| spaces = None | |
| ROOT = Path(__file__).resolve().parent | |
| for relative in ("vendor/chatterbox_tts_0_1_7", "src", "scripts"): | |
| path = ROOT / relative | |
| if str(path) not in sys.path: | |
| sys.path.insert(0, str(path)) | |
| from urdu_s2s.api import DEFAULT_PRAXY_ANCHOR, result_to_api_payload, run_live_tts | |
| from urdu_s2s.live_providers import ( | |
| OpenAIBridgeProvider, | |
| OpenAICompatibleChatClient, | |
| neutralize_devanagari_user_address, | |
| normalize_devanagari_tts_pronunciation, | |
| ) | |
| from urdu_s2s.schemas import BridgeResult, ReplyResult, SpeechToSpeechRequest | |
| from urdu_s2s.tts_providers import ChatterboxPraxyTTSProvider | |
| OUTPUT_DIR = ROOT / "artifacts/gradio_tts_outputs" | |
| OUTPUT_DIR.mkdir(parents=True, exist_ok=True) | |
| def env(name: str, default: str) -> str: | |
| return os.environ.get(name, default) | |
| def gpu_runtime(func): | |
| if spaces is None: | |
| return func | |
| return spaces.GPU(duration=int(env("S2S_ZEROGPU_DURATION", "120")))(func) | |
| def is_devanagari(text: str) -> bool: | |
| return any("\u0900" <= char <= "\u097f" for char in text) | |
| def voice_prompt_audio_path() -> Path: | |
| return Path(env("S2S_VOICE_PROMPT_AUDIO_PATH", str(DEFAULT_PRAXY_ANCHOR))) | |
| def chatterbox_device() -> str: | |
| return env("S2S_CHATTERBOX_DEVICE", "cuda") | |
| def chatterbox_t3_model() -> str: | |
| return env("S2S_CHATTERBOX_T3_MODEL", "v3") | |
| def synthesize_s2s(mic_audio_path: str | None, upload_audio_path: str | None, prompt_roman_urdu: str): | |
| audio_path = mic_audio_path or upload_audio_path | |
| if not audio_path: | |
| raise gr.Error("Please stop the recorder or upload an audio file first.") | |
| request_id = f"hf_{uuid4().hex}" | |
| input_audio = Path(audio_path) | |
| common = { | |
| "audio_path": input_audio, | |
| "request_id": request_id, | |
| "prompt_roman_urdu": prompt_roman_urdu or "", | |
| "whisper_model": env("S2S_WHISPER_MODEL", "small"), | |
| "whisper_language": env("S2S_WHISPER_LANGUAGE", "ur"), | |
| "whisper_device": env("S2S_WHISPER_DEVICE", "cpu"), | |
| "whisper_compute_type": env("S2S_WHISPER_COMPUTE_TYPE", "int8"), | |
| } | |
| result = run_live_tts( | |
| **common, | |
| output_audio_path=OUTPUT_DIR / f"{request_id}.wav", | |
| voice_prompt_audio_path=voice_prompt_audio_path(), | |
| chatterbox_device=chatterbox_device(), | |
| chatterbox_t3_model=chatterbox_t3_model(), | |
| ) | |
| payload = result_to_api_payload(result, repo_root=ROOT) | |
| output_audio = payload["tts_audio_path"] if payload["tts_audio_exists"] else None | |
| return ( | |
| output_audio, | |
| payload["asr_transcript"], | |
| payload["assistant_reply_urdu"], | |
| ) | |
| def synthesize_tts(input_text: str): | |
| text = (input_text or "").strip() | |
| if not text: | |
| raise gr.Error("Please enter the text you want spoken.") | |
| request_id = f"hf_tts_{uuid4().hex}" | |
| request = SpeechToSpeechRequest( | |
| request_id=request_id, | |
| audio_path=Path(""), | |
| metadata={"input_mode": "text"}, | |
| ) | |
| reply = ReplyResult(text_urdu=text, provider="user_text", model="typed_input") | |
| if is_devanagari(text): | |
| speech_text = neutralize_devanagari_user_address( | |
| normalize_devanagari_tts_pronunciation(text) | |
| ) | |
| bridge = BridgeResult( | |
| text_devanagari=speech_text, | |
| provider="direct_devanagari_text", | |
| model="user_input", | |
| ) | |
| else: | |
| bridge = OpenAIBridgeProvider(chat_client=OpenAICompatibleChatClient()).convert( | |
| request, | |
| reply, | |
| ) | |
| tts_result = ChatterboxPraxyTTSProvider( | |
| output_audio_path=OUTPUT_DIR / f"{request_id}.wav", | |
| voice_prompt_audio_path=voice_prompt_audio_path(), | |
| device=chatterbox_device(), | |
| t3_model=chatterbox_t3_model(), | |
| ).synthesize(request, reply, bridge) | |
| output_audio = str(tts_result.audio_path) if tts_result.audio_path.exists() else None | |
| return output_audio | |
| with gr.Blocks(title="Urdu S2S") as demo: | |
| gr.Markdown("# Urdu S2S") | |
| gr.Markdown( | |
| """ | |
| A Pakistani Urdu speech model demo for conversational voice responses. It supports | |
| both speech-to-speech and text-to-speech testing with the same Urdu voice style. | |
| """ | |
| ) | |
| with gr.Accordion("About this model", open=False): | |
| gr.Markdown( | |
| """ | |
| **What it is** | |
| Urdu S2S is an experimental Urdu voice assistant model focused on natural | |
| Pakistani Urdu pronunciation, short spoken replies, and a warm female assistant | |
| voice. It is designed for interactive product testing rather than long-form | |
| narration. | |
| **Architecture** | |
| Urdu S2S uses a modular speech pipeline with dedicated components for speech | |
| recognition, Urdu response generation, pronunciation-aware speech preparation, | |
| and neural speech synthesis. The user-facing experience is a single | |
| speech-to-speech model demo, while the underlying design lets each part improve | |
| independently as stronger Urdu speech models become available. | |
| At a high level, incoming audio is normalized and transcribed, the dialogue | |
| layer produces a concise Urdu assistant response, a pronunciation layer prepares | |
| the response for stable speech generation, and the synthesis layer renders the | |
| final spoken voice. | |
| **What it can do** | |
| - Record or upload Urdu speech and receive a spoken Urdu response. | |
| - Generate speech directly from typed Urdu, Roman Urdu, or Devanagari text. | |
| - Preserve common names, numbers, dates, and English terms where possible. | |
| - Use Urdu-first phrasing such as “baraye meherbani” instead of overly Hindi wording. | |
| - Avoid assuming the user's gender in normal assistant replies. | |
| **Best test inputs** | |
| Use short, natural prompts of 2 to 10 seconds. The current demo works best for | |
| assistant-style requests such as greetings, clarifying questions, simple customer | |
| support, scheduling drafts, travel questions, and account/helpdesk style prompts. | |
| **Current limits** | |
| This is an MVP. Very noisy audio, long recordings, mixed languages, unusual names, | |
| and dense factual questions may still produce transcription or pronunciation errors. | |
| The first generation after the Space wakes up can be slower while the speech model | |
| loads. | |
| """ | |
| ) | |
| with gr.Tabs(): | |
| with gr.Tab("Speech to speech"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| mic_audio = gr.Audio( | |
| label="Record speech", | |
| sources=["microphone"], | |
| type="filepath", | |
| ) | |
| upload_audio = gr.Audio( | |
| label="Upload speech", | |
| sources=["upload"], | |
| type="filepath", | |
| ) | |
| prompt = gr.Textbox( | |
| label="Optional Roman Urdu hint", | |
| placeholder="Example: reply like a polite Pakistani Urdu assistant", | |
| ) | |
| run_s2s = gr.Button("Generate speech response", variant="primary") | |
| with gr.Column(): | |
| output_audio = gr.Audio(label="Response audio", type="filepath") | |
| transcript = gr.Textbox(label="Transcript", rtl=True) | |
| reply = gr.Textbox(label="Reply", rtl=True) | |
| run_s2s.click( | |
| fn=synthesize_s2s, | |
| inputs=[mic_audio, upload_audio, prompt], | |
| outputs=[output_audio, transcript, reply], | |
| api_name="s2s", | |
| concurrency_limit=1, | |
| ) | |
| with gr.Tab("Text to speech"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| text_input = gr.Textbox( | |
| label="Text", | |
| lines=5, | |
| placeholder="Type Urdu, Roman Urdu, or Devanagari text to speak.", | |
| ) | |
| run_tts = gr.Button("Generate speech", variant="primary") | |
| with gr.Column(): | |
| tts_output_audio = gr.Audio(label="Speech audio", type="filepath") | |
| run_tts.click( | |
| fn=synthesize_tts, | |
| inputs=[text_input], | |
| outputs=[tts_output_audio], | |
| api_name="tts", | |
| concurrency_limit=1, | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(max_size=8).launch(server_name="0.0.0.0", server_port=7860) | |