Spaces:
Running
Running
Download app.py from Pecson/sauti_tts_api: direct link, hf CLI and curl.
- Browser
- Download file 17.8 kB
-
https://huggingface.co/spaces/Pecson/sauti_tts_api/resolve/main/app.py
- Command line
-
hf download hf://spaces/Pecson/sauti_tts_api/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Pecson/sauti_tts_api/resolve/main/app.py
17.8 kB
| 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( | |
| """ | |
| <div class="sautiai-header"> | |
| <div class="sautiai-logo"> | |
| <span>AI</span> | |
| </div> | |
| <div class="sautiai-title"> | |
| SAUTIAI | |
| </div> | |
| <div class="sautiai-subtitle"> | |
| Kiswahili AI Voice Generator | |
| </div> | |
| <div class="badges"> | |
| <div class="badge"> | |
| AI VOICE | |
| </div> | |
| <div class="badge"> | |
| KISWAHILI | |
| </div> | |
| <div class="badge"> | |
| F5-TTS | |
| </div> | |
| <div class="badge"> | |
| VOICE AI | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| # -------------------------------------------------------- | |
| # MAIN CARD | |
| # -------------------------------------------------------- | |
| with gr.Column( | |
| elem_classes="main-card" | |
| ): | |
| gr.HTML( | |
| """ | |
| <div class="section-title"> | |
| Maandishi ya Sauti | |
| </div> | |
| <div class="section-description"> | |
| Andika maandishi ya Kiswahili unayotaka | |
| SautiAI iyasome kwa kutumia sauti yako. | |
| </div> | |
| """ | |
| ) | |
| # ---------------------------------------------------- | |
| # 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( | |
| """ | |
| <div class="section-title" | |
| style="margin-top:20px;"> | |
| Sauti Iliyotengenezwa | |
| </div> | |
| <div class="section-description"> | |
| Sauti yako itaonekana hapa baada ya | |
| mchakato kukamilika. | |
| </div> | |
| """ | |
| ) | |
| output_audio = gr.Audio( | |
| label="SautiAI Audio", | |
| type="filepath", | |
| interactive=False, | |
| elem_classes="audio-box" | |
| ) | |
| # -------------------------------------------------------- | |
| # FOOTER | |
| # -------------------------------------------------------- | |
| gr.HTML( | |
| """ | |
| <div class="sautiai-footer"> | |
| <strong>SautiAI</strong> | |
| · Kiswahili AI Voice Technology | |
| <br> | |
| Powered by F5-TTS | |
| </div> | |
| """ | |
| ) | |
| # ======================================================== | |
| # 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() |