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( """
SAUTIAI
Kiswahili AI Voice Generator
AI VOICE
KISWAHILI
F5-TTS
VOICE AI
""" ) # -------------------------------------------------------- # MAIN CARD # -------------------------------------------------------- with gr.Column( elem_classes="main-card" ): gr.HTML( """
Maandishi ya Sauti
Andika maandishi ya Kiswahili unayotaka SautiAI iyasome kwa kutumia sauti yako.
""" ) # ---------------------------------------------------- # TEXT INPUT # ---------------------------------------------------- text_input = gr.Textbox( label="Maandishi", placeholder=( "Mfano: Karibu kwenye teknolojia " "ya kisasa ya akili bandia..." ), lines=7, max_lines=15, show_label=True ) # ---------------------------------------------------- # SETTINGS # ---------------------------------------------------- with gr.Accordion( "⚙ Mipangilio ya Sauti", open=False, elem_classes="settings-box" ): with gr.Row(): speed = gr.Slider( minimum=0.5, maximum=1.5, value=0.95, step=0.05, label="Speed" ) cfg_strength = gr.Slider( minimum=0.5, maximum=3.0, value=1.5, step=0.1, label="CFG Strength" ) with gr.Row(): nfe_step = gr.Slider( minimum=2, maximum=32, value=16, step=1, label="NFE Steps" ) target_rms = gr.Slider( minimum=0.05, maximum=0.5, value=0.1, step=0.01, label="Target RMS" ) remove_silence = gr.Checkbox( value=True, label="Ondoa ukimya wa ziada" ) # ---------------------------------------------------- # BUTTONS # ---------------------------------------------------- with gr.Row(): generate_button = gr.Button( "🎙 ZALISHA SAUTI", variant="primary", elem_classes="generate-btn" ) reset_button = gr.Button( "↻ SAFISHA", elem_classes="reset-btn" ) # ---------------------------------------------------- # STATUS # ---------------------------------------------------- status = gr.Textbox( label="Hali", value="Tayari. Andika maandishi yako.", interactive=False, elem_classes="status-box" ) # ---------------------------------------------------- # OUTPUT # ---------------------------------------------------- gr.HTML( """
Sauti Iliyotengenezwa
Sauti yako itaonekana hapa baada ya mchakato kukamilika.
""" ) output_audio = gr.Audio( label="SautiAI Audio", type="filepath", interactive=False, elem_classes="audio-box" ) # -------------------------------------------------------- # FOOTER # -------------------------------------------------------- gr.HTML( """ """ ) # ======================================================== # GENERATE EVENT # ======================================================== generate_button.click( fn=generate_audio, inputs=[ text_input, speed, cfg_strength, nfe_step, target_rms, remove_silence ], outputs=[ output_audio, status ] ) # ======================================================== # RESET EVENT # ======================================================== reset_button.click( fn=reset_app, inputs=[], outputs=[ text_input, speed, cfg_strength, nfe_step, target_rms, remove_silence, output_audio, status ] ) # ============================================================ # LAUNCH # ============================================================ if __name__ == "__main__": demo.launch()