import gradio as gr import torch import torchaudio import soundfile as sf import os import tempfile import spaces from datetime import datetime from omnivoice import OmniVoice # ─── Language selection ─── LANGUAGE_CHOICES = [ "Kabyle (default)", "Standard Moroccan Tamazight", "Tahaggart Tamahaq", "Algerian Arabic" ] LANG_CODE_MAP = { "Kabyle (default)": "kab", "Standard Moroccan Tamazight": "zgh", "Tahaggart Tamahaq": "thv", "Algerian Arabic": "arq", } # Default Kabyle text (kept as original) DEFAULT_TEXT = """Awal n "Uṛdinatur" neqqar-as "Aselkim" s teqbaylit. Ma yella d "Linux" d Anagraw n Wammud.""" # Example sentences for each language (displayed when selected) EXAMPLE_SENTENCES = { "Kabyle (default)": DEFAULT_TEXT, "Standard Moroccan Tamazight": "ⴰⵣⵓⵍ ⵎⴰⵙⵙⴰ ⵎⵎⵉ ⵏⵏⵓⵏ. ⵎⴰⵏⵉⴽ ⵜⵍⵍⵉⴷ? ⴰⴷ ⵏⵏⵓⵖ ⵏⵏⴰⵖ ⴰⵙⵙⴰ.", "Tahaggart Tamahaq": "ⵎⴰⵙⵙⴰ ⵏⵏⵓⵏ, ⵎⴰⵏⵉⴽ ⵜⵏⵏⴰⵍⴰⵎ? ⴰⴷⴰⵖ ⵏⴰⵔⴰ ⵙ ⵓⵖⵔⵎ ⵏⵏⵖ.", "Algerian Arabic": "شحال شْبَابْ ليوم. ليوما رانا حابين نروحو للبحر. تحب تجي معانا ولٌا لا؟" } # ─── Pre‑loaded cloned voices ─── PRELOADED_VOICES = { "Upload my own": None, "Muhya (pre‑loaded)": "assets/muhya.mp3", } # ─── Model ─── print("Loading model...") device = "cuda" if torch.cuda.is_available() else "cpu" dtype = torch.float16 if device == "cuda" else torch.float32 model = OmniVoice.from_pretrained("k2-fsa/OmniVoice", device_map=device, dtype=dtype) print(f"Model loaded ({device})") MAX_WORDS = 50 def _count_words(text): """Count words in a string (splits on whitespace).""" if not text: return 0 return len(text.strip().split()) def _build_instruct(gender, age, pitch, style): parts = [] if gender and gender != "Auto": parts.append(gender.lower()) if age and age != "Auto": parts.append(age.lower()) if pitch and pitch != "Auto": parts.append(f"{pitch.lower()} pitch") if style and style != "Auto": parts.append(style.lower()) return ", ".join(parts) if parts else None def _save_audio(audio_tensor, sample_rate=24000): """Save audio tensor to a temporary WAV file with robust shape handling.""" try: if not isinstance(audio_tensor, torch.Tensor): audio_tensor = torch.tensor(audio_tensor) audio_tensor = audio_tensor.cpu() # Normalize shape: ensure [channels, samples] or [samples] while audio_tensor.dim() > 2: audio_tensor = audio_tensor.squeeze(0) if audio_tensor.dim() == 1: # Mono: [samples] -> [samples, 1] for soundfile audio_np = audio_tensor.unsqueeze(-1).numpy() elif audio_tensor.dim() == 2: # Could be [channels, samples] or [samples, channels] # OmniVoice typically outputs [1, samples] or [channels, samples] if audio_tensor.shape[0] <= 4 and audio_tensor.shape[1] > audio_tensor.shape[0]: # Likely [channels, samples] -> transpose to [samples, channels] audio_np = audio_tensor.T.numpy() else: # Likely [samples, channels] already audio_np = audio_tensor.numpy() else: audio_np = audio_tensor.numpy() # Ensure 2D for soundfile: [samples, channels] if audio_np.ndim == 1: audio_np = audio_np.reshape(-1, 1) with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: sf.write(f.name, audio_np, sample_rate) return f.name except Exception as e: raise RuntimeError(f"Failed to save audio: {e}") def update_example_text(lang_choice): return EXAMPLE_SENTENCES.get(lang_choice, DEFAULT_TEXT) # ─── Helper to force gender and switch to Voice Design mode ─── def set_male(): return [gr.update(value="Male"), gr.update(value="Voice Design")] def set_female(): return [gr.update(value="Female"), gr.update(value="Voice Design")] # ─── Voice Design / Auto ─── @spaces.GPU def generate_design(text, mode, lang_choice, gender, age, pitch, style, speed, duration, num_step, guidance_scale, denoise, postprocess): if not text or not text.strip(): return None, "Please enter text." word_count = _count_words(text) if word_count > MAX_WORDS: return None, f"Text too long: {word_count} words (max {MAX_WORDS}). Please shorten your input." lang_code = LANG_CODE_MAP.get(lang_choice, "kab") kwargs = dict(num_step=int(num_step), guidance_scale=guidance_scale, denoise=denoise) kwargs["language"] = lang_code if mode == "Voice Design": instruct = _build_instruct(gender, age, pitch, style) if instruct: kwargs["instruct"] = instruct if duration and duration > 0: kwargs["duration"] = duration else: kwargs["speed"] = speed if postprocess: kwargs["postprocess_output"] = True try: audio = model.generate(text=text, **kwargs) path = _save_audio(audio[0], 24000) duration_sec = audio[0].shape[-1] / 24000 if hasattr(audio[0], 'shape') else 0 return path, f"Generation complete ({duration_sec:.1f}s)" except Exception as e: return None, f"Error: {e}" # ─── Voice Clone ─── @spaces.GPU def generate_clone(text, voice_choice, ref_audio, ref_text, lang_choice, speed, duration, num_step, guidance_scale, denoise, postprocess): if not text or not text.strip(): return None, "Please enter text." word_count = _count_words(text) if word_count > MAX_WORDS: return None, f"Text too long: {word_count} words (max {MAX_WORDS}). Please shorten your input." # Determine the actual reference audio path preloaded_path = PRELOADED_VOICES.get(voice_choice) if preloaded_path: ref_audio = preloaded_path elif ref_audio is None: return None, "Please upload reference audio or select a pre‑loaded voice." # Ensure ref_audio is a valid file path if isinstance(ref_audio, tuple): ref_audio = ref_audio[0] # Gradio sometimes returns (sample_rate, data) tuples lang_code = LANG_CODE_MAP.get(lang_choice, "kab") kwargs = dict(num_step=int(num_step), guidance_scale=guidance_scale, denoise=denoise) kwargs["language"] = lang_code if duration and duration > 0: kwargs["duration"] = duration else: kwargs["speed"] = speed if postprocess: kwargs["postprocess_output"] = True try: audio = model.generate( text=text, ref_audio=ref_audio, ref_text=ref_text if ref_text and ref_text.strip() else None, **kwargs, ) path = _save_audio(audio[0], 24000) duration_sec = audio[0].shape[-1] / 24000 if hasattr(audio[0], 'shape') else 0 return path, f"Generation complete ({duration_sec:.1f}s)" except Exception as e: return None, f"Error: {e}" def toggle_ref_audio(voice_choice): """Show/hide the manual upload field based on voice selection.""" return gr.update(visible=(voice_choice == "Upload my own")) # ─── UI ─── CSS = """ .main-title { text-align: center; font-size: 1.8em; font-weight: 800; margin-bottom: 0; } .subtitle { text-align: center; color: #888; font-size: 0.9em; margin-bottom: 1em; } footer { display: none !important; } .word-counter { text-align: right; font-size: 0.85em; color: #666; margin-top: -0.5em; } .word-counter.over-limit { color: #d32f2f; font-weight: bold; } """ with gr.Blocks(title="OmniVoice") as app: gr.HTML("
AI Voice Generator — Kabyle + Regional Languages
") with gr.Tabs(): # ── Voice Design / Auto ── with gr.Tab("Voice Design"): with gr.Row(): with gr.Column(scale=1): d_text = gr.Textbox( label="Text to speak", lines=6, placeholder=f"Enter text in the selected language... (max {MAX_WORDS} words)", value=DEFAULT_TEXT ) d_word_counter = gr.HTML( value=f'