import os import time import subprocess import gradio as gr # ============================================================ # SAUTIAI # Kiswahili AI Voice Generator - F5-TTS # ============================================================ APP_NAME = "SautiAI" BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MODEL_DIR = os.path.join( BASE_DIR, "ckpts", "sauti_tts" ) VOCAB_DIR = os.path.join( BASE_DIR, "data" ) OUTPUT_DIR = os.path.join( BASE_DIR, "outputs" ) MODEL_FILE = os.path.join( MODEL_DIR, "model_last.pt" ) VOCAB_FILE = os.path.join( VOCAB_DIR, "vocab.txt" ) # ============================================================ # FIXED REFERENCE VOICE # Usiweke upload/record kwenye UI. # Weka ref_clean.wav kwenye root ya Hugging Face Space. # ============================================================ REF_AUDIO = os.path.join( BASE_DIR, "ref_clean.wav" ) REF_TEXT = "Karibu kwenye app yangu ya Kiswahili." MAX_TEXT_LENGTH = 1000 # ============================================================ # CREATE DIRECTORIES # ============================================================ os.makedirs(MODEL_DIR, exist_ok=True) os.makedirs(VOCAB_DIR, exist_ok=True) os.makedirs(OUTPUT_DIR, exist_ok=True) # ============================================================ # DOWNLOAD MODEL # ============================================================ def download_file(repo_file, destination): result = subprocess.run( [ "huggingface-cli", "download", "msingiai/sauti-tts", repo_file, "--local-dir", destination ], capture_output=True, text=True ) if result.returncode != 0: raise RuntimeError( "Hugging Face download failed:\n" + result.stderr[-3000:] ) def prepare_model(): if not os.path.exists(MODEL_FILE): print("Downloading SautiAI model...") download_file( "model_last.pt", MODEL_DIR ) if not os.path.exists(VOCAB_FILE): print("Downloading vocabulary...") download_file( "vocab.txt", VOCAB_DIR ) # ============================================================ # PREPARE MODEL # ============================================================ prepare_model() # ============================================================ # F5-TTS # ============================================================ from f5_tts.api import F5TTS print("Loading F5-TTS model...") f5tts = F5TTS( model="F5TTS_v1_Base", ckpt_file=MODEL_FILE, vocab_file=VOCAB_FILE, device="cpu", use_ema=False ) print("F5-TTS model loaded successfully.") # ============================================================ # GENERATE AUDIO # ============================================================ def generate_audio( text, speed, cfg_strength, nfe_step, target_rms, remove_silence ): # -------------------------------------------------------- # CHECK TEXT # -------------------------------------------------------- if not text or not text.strip(): return ( None, "⚠️ Tafadhali andika maandishi kwanza." ) text = text.strip() # -------------------------------------------------------- # CHECK TEXT LENGTH # -------------------------------------------------------- if len(text) > MAX_TEXT_LENGTH: return ( None, ( "⚠️ Maandishi ni marefu sana.\n" f"Upeo ni herufi {MAX_TEXT_LENGTH}." ) ) # -------------------------------------------------------- # CHECK REFERENCE VOICE # -------------------------------------------------------- if not os.path.exists(REF_AUDIO): return ( None, ( "❌ Sauti rejeleo haipatikani.\n\n" "Hakikisha faili hili lipo kwenye root ya Space:\n" "ref_clean.wav" ) ) # -------------------------------------------------------- # CREATE UNIQUE OUTPUT FILE # -------------------------------------------------------- timestamp = int( time.time() * 1000 ) output_file = os.path.join( OUTPUT_DIR, f"sautiai_{timestamp}.wav" ) # -------------------------------------------------------- # GENERATION # -------------------------------------------------------- try: print( "Generating audio for:", text ) f5tts.infer( ref_file=REF_AUDIO, ref_text=REF_TEXT, gen_text=text, file_wave=output_file, target_rms=float( target_rms ), cross_fade_duration=0.15, cfg_strength=float( cfg_strength ), nfe_step=int( nfe_step ), speed=float( speed ), remove_silence=bool( remove_silence ), seed=42 ) # ---------------------------------------------------- # CHECK OUTPUT # ---------------------------------------------------- if not os.path.exists(output_file): return ( None, "❌ Sauti haikutengenezwa." ) print( "Audio generated:", output_file ) return ( output_file, "✓ SAUTI IMEKAMILIKA" ) except Exception as e: print( "Generation error:", str(e) ) return ( None, "❌ Kosa wakati wa kutengeneza sauti:\n\n" + str(e) ) # ============================================================ # RESET # ============================================================ def reset_app(): return ( "", 0.95, 1.5, 16, 0.1, True, None, "Tayari. Andika maandishi yako." ) # ============================================================ # CSS # ============================================================ CSS = """ * { box-sizing: border-box; } body { margin: 0; background: radial-gradient( circle at 20% 10%, rgba(0, 229, 255, 0.12), transparent 30% ), radial-gradient( circle at 80% 20%, rgba(0, 110, 255, 0.10), transparent 30% ), #05070a; color: #ffffff; font-family: Arial, Helvetica, sans-serif; } /* Main container */ .gradio-container { max-width: 1050px !important; margin: auto !important; padding: 20px !important; } /* Header */ .sautiai-header { text-align: center; padding: 25px 15px 20px 15px; } .sautiai-logo { width: 82px; height: 82px; margin: auto; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: radial-gradient( circle, #07151c 30%, #061016 70% ); border: 2px solid #00e5ff; box-shadow: 0 0 15px rgba(0, 229, 255, 0.6), 0 0 45px rgba(0, 150, 255, 0.25); } .sautiai-logo span { font-size: 35px; font-weight: 900; color: #00e5ff; } .sautiai-title { margin-top: 14px; font-size: 38px; font-weight: 900; letter-spacing: 2px; color: #ffffff; text-shadow: 0 0 12px rgba(0, 229, 255, 0.55); } .sautiai-subtitle { margin-top: 6px; color: #8fa7b5; font-size: 15px; } /* Badges */ .badges { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 18px; } .badge { padding: 7px 13px; border-radius: 30px; border: 1px solid rgba(0, 229, 255, 0.35); background: rgba(0, 229, 255, 0.06); color: #79edff; font-size: 11px; font-weight: 700; letter-spacing: 1px; } /* Main card */ .main-card { border-radius: 24px !important; border: 1px solid rgba(0, 229, 255, 0.20) !important; background: linear-gradient( 145deg, rgba(10, 18, 24, 0.96), rgba(5, 9, 13, 0.96) ) !important; box-shadow: 0 15px 50px rgba(0, 0, 0, 0.45), inset 0 0 30px rgba(0, 229, 255, 0.025); padding: 25px !important; } /* Section titles */ .section-title { font-size: 20px; font-weight: 900; color: #ffffff; margin-bottom: 5px; } .section-description { color: #8195a1; font-size: 13px; margin-bottom: 15px; } /* Textbox */ textarea { background: #080d11 !important; border: 1px solid rgba(0, 229, 255, 0.22) !important; border-radius: 16px !important; color: #ffffff !important; font-size: 16px !important; line-height: 1.65 !important; } textarea:focus { border-color: #00e5ff !important; box-shadow: 0 0 0 2px rgba(0, 229, 255, 0.10) !important; } /* Labels */ label { color: #b7cad4 !important; font-weight: 700 !important; } /* Generate button */ .generate-btn { border-radius: 16px !important; min-height: 56px !important; border: 1px solid #00e5ff !important; background: linear-gradient( 90deg, #007c99, #005ee8 ) !important; color: #ffffff !important; font-size: 17px !important; font-weight: 900 !important; letter-spacing: 0.5px !important; box-shadow: 0 0 18px rgba(0, 229, 255, 0.20) !important; } .generate-btn:hover { filter: brightness(1.15); box-shadow: 0 0 28px rgba(0, 229, 255, 0.35) !important; } /* Reset */ .reset-btn { border-radius: 14px !important; min-height: 45px !important; background: #0b1116 !important; border: 1px solid rgba(255,255,255,0.12) !important; color: #a9bac3 !important; } /* Settings */ .settings-box { border-radius: 16px !important; border: 1px solid rgba(0, 229, 255, 0.12) !important; background: rgba(0, 229, 255, 0.025) !important; margin-top: 18px !important; } /* Status */ .status-box textarea { min-height: 75px !important; } /* Audio */ .audio-box { border-radius: 18px !important; border: 1px solid rgba(0, 229, 255, 0.18) !important; background: #080d11 !important; padding: 8px !important; } /* Footer */ .sautiai-footer { text-align: center; color: #53656f; font-size: 12px; padding: 25px 10px 10px 10px; } .sautiai-footer strong { color: #00dfff; } /* Mobile */ @media (max-width: 700px) { .gradio-container { padding: 10px !important; } .sautiai-title { font-size: 30px; } .sautiai-logo { width: 70px; height: 70px; } .main-card { padding: 16px !important; border-radius: 19px !important; } .generate-btn { min-height: 52px !important; } } """ # ============================================================ # GRADIO APP # ============================================================ with gr.Blocks( title="SautiAI - Kiswahili AI Voice", css=CSS ) as demo: # -------------------------------------------------------- # HEADER # -------------------------------------------------------- gr.HTML( """