sauti_tts_api / app.py
Pecson's picture
create_app.py
983b99a verified
Raw History Blame Contribute Delete
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()