import streamlit as st
import soundfile as sf
import numpy as np
import os
import tempfile
import torch
if hasattr(torch, "classes") and hasattr(torch.classes, "__path__"):
torch.classes.__path__ = []
import io
from voxcpm import VoxCPM
import base64
import kagglehub
import glob
from audio_recorder_streamlit import audio_recorder
# Page configuration
st.set_page_config(
page_title="Voice Clone & TTS",
page_icon="đī¸",
layout="centered"
)
# Custom CSS for better styling
st.markdown("""
""", unsafe_allow_html=True)
# Header
st.markdown('
đī¸ Voice Clone & Text-to-Speech
Clone any voice and generate speech using AI
', unsafe_allow_html=True)
# Initialize session state for model and audio
if 'model' not in st.session_state:
st.session_state.model = None
if 'reference_audio' not in st.session_state:
st.session_state.reference_audio = None
if 'reference_text' not in st.session_state:
st.session_state.reference_text = ""
if 'model_loaded' not in st.session_state:
st.session_state.model_loaded = False
@st.cache_data
def download_charlie_kirk_dataset():
"""Download Charlie Kirk dataset from Kaggle"""
try:
with st.spinner("Downloading Charlie Kirk dataset from Kaggle..."):
path = kagglehub.dataset_download("bwandowando/charlie-kirk-twitter-dataset")
return path
except Exception as e:
st.warning(f"Could not download dataset: {str(e)}")
return None
def get_sample_audio_from_dataset(dataset_path):
"""Find a suitable audio file from the dataset"""
if not dataset_path or not os.path.exists(dataset_path):
return None, None
# Look for audio/video files (common formats)
audio_extensions = ['*.wav', '*.mp3', '*.m4a', '*.flac', '*.mp4']
audio_files = []
for ext in audio_extensions:
audio_files.extend(glob.glob(os.path.join(dataset_path, '**', ext), recursive=True))
if audio_files:
# Return the first audio file found
return audio_files[0], "Sample from Charlie Kirk dataset"
return None, None
@st.cache_resource
def load_model():
"""Load the VoxCPM model (cached)"""
try:
# Check for MPS (Apple Silicon) or CUDA
if torch.backends.mps.is_available():
device = "mps"
st.info("đ Apple Silicon MPS detected - using Metal acceleration")
elif torch.cuda.is_available():
device = "cuda"
st.info("đ CUDA detected - using GPU acceleration")
else:
device = "cpu"
st.warning("â ī¸ No GPU detected - using CPU (this will be slow)")
with st.spinner("Loading VoxCPM model... This may take a few minutes on first run."):
model = VoxCPM.from_pretrained("openbmb/VoxCPM1.5")
return model
except Exception as e:
st.error(f"Error loading model: {str(e)}")
st.info("đĄ Tip: Make sure you have sufficient disk space and internet connection")
return None
def generate_speech(text, reference_audio_path, reference_text, cfg_value=2.0, timesteps=10):
"""Generate speech using the model"""
try:
# Clear MPS cache if needed
if torch.backends.mps.is_available():
torch.mps.empty_cache()
# Check if reference audio exists
if not os.path.exists(reference_audio_path):
st.error(f"Reference audio file not found: {reference_audio_path}")
return None
generated_wav = st.session_state.model.generate(
text=text,
prompt_wav_path=reference_audio_path,
prompt_text=reference_text,
cfg_value=cfg_value,
inference_timesteps=timesteps,
denoise=True,
)
return generated_wav
except Exception as e:
st.error(f"Error generating speech: {str(e)}")
return None
def get_audio_player(audio_data, sample_rate):
"""Create an HTML audio player for the generated audio"""
# Save to bytes buffer
buffer = io.BytesIO()
sf.write(buffer, audio_data, sample_rate, format='wav')
buffer.seek(0)
# Convert to base64 for HTML playback
audio_base64 = base64.b64encode(buffer.read()).decode()
audio_html = f"""
', unsafe_allow_html=True)
st.warning(f"â ī¸ Missing required packages: {', '.join(missing_packages)}")
st.code("pip install " + " ".join(missing_packages))
st.markdown('
', unsafe_allow_html=True)
# Sidebar for configuration
with st.sidebar:
st.header("âī¸ Configuration")
# Model loading section
st.subheader("1. Load Model")
# Show device info
if torch.cuda.is_available():
st.success("â
CUDA available")
elif torch.backends.mps.is_available():
st.success("â
MPS available (Apple Silicon)")
else:
st.warning("â ī¸ Using CPU (slow)")
if st.button("đ Load VoxCPM Model", use_container_width=True):
with st.spinner("Loading model... This may take a few minutes..."):
st.session_state.model = load_model()
if st.session_state.model:
st.session_state.model_loaded = True
st.success("â
Model loaded successfully!")
st.rerun()
if not st.session_state.model_loaded:
st.warning("â ī¸ Please load the model first")
st.stop()
# Voice sample configuration
st.subheader("2. Configure Voice Sample")
# Option to upload custom sample or record
upload_option = st.radio(
"Choose voice sample source:",
["Record your voice", "Upload audio file", "Download Charlie Kirk (Kaggle)"]
)
if upload_option == "Record your voice":
st.info("đ¤ Click the microphone button below to record your voice sample")
st.markdown("**Tips for best results:**")
st.markdown("- Record 5-10 seconds of clear speech")
st.markdown("- Speak naturally in a quiet environment")
st.markdown("- Avoid background noise")
# Audio recorder
audio_bytes = audio_recorder()
if audio_bytes:
# Save recorded audio
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp_file:
tmp_file.write(audio_bytes)
st.session_state.reference_audio = tmp_file.name
st.success("â
Voice recorded successfully!")
st.audio(audio_bytes, format='audio/wav')
st.session_state.reference_text = st.text_area(
"Enter what you said in the recording:",
value=st.session_state.reference_text,
help="Transcript helps improve voice cloning accuracy",
height=100,
placeholder="Type the exact words you spoke in the recording..."
)
elif upload_option == "Upload audio file":
uploaded_file = st.file_uploader(
"Upload voice sample (WAV/MP3/MP4 format)",
type=['wav', 'mp3', 'mp4']
)
if uploaded_file is not None:
# Save uploaded file temporarily
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp_file:
tmp_file.write(uploaded_file.getvalue())
st.session_state.reference_audio = tmp_file.name
st.session_state.reference_text = st.text_area(
"Enter the exact transcript of the audio:",
value=st.session_state.reference_text,
help="This helps the model match the voice more accurately",
height=100
)
# Play uploaded audio
audio_bytes = uploaded_file.getvalue()
st.audio(audio_bytes, format='audio/wav')
else: # Download Charlie Kirk from Kaggle
if st.button("đĨ Download Charlie Kirk Dataset", use_container_width=True):
dataset_path = download_charlie_kirk_dataset()
if dataset_path:
st.info(f"đ Dataset downloaded to: {dataset_path}")
audio_file, transcript = get_sample_audio_from_dataset(dataset_path)
if audio_file:
st.session_state.reference_audio = audio_file
st.session_state.reference_text = transcript or "Sample from Charlie Kirk"
st.success(f"â
Found audio: {os.path.basename(audio_file)}")
else:
st.warning("No audio files found in dataset. Please upload a custom sample.")
if st.session_state.reference_audio and os.path.exists(st.session_state.reference_audio):
st.info(f"đ Using: {os.path.basename(st.session_state.reference_audio)}")
# Allow editing transcript
st.session_state.reference_text = st.text_area(
"Transcript (optional - edit if needed):",
value=st.session_state.reference_text,
help="Provide the transcript of what's said in the audio",
height=100
)
# Play the audio
try:
with open(st.session_state.reference_audio, 'rb') as f:
st.audio(f.read(), format='audio/wav')
except:
pass
# Advanced parameters
st.subheader("3. Advanced Parameters")
cfg_value = st.slider(
"CFG Value (style adherence)",
min_value=1.0,
max_value=3.0,
value=2.0,
step=0.1,
help="Higher values follow the reference voice more closely"
)
timesteps = st.slider(
"Inference Timesteps",
min_value=5,
max_value=20,
value=10,
step=1,
help="Higher values = better quality but slower generation"
)
# Main content area
col1, col2 = st.columns([2, 1])
with col1:
st.header("đ Enter Text to Convert")
text_input = st.text_area(
"Type or paste the text you want to convert to speech:",
height=150,
placeholder="Enter any text here... The AI will speak it in the cloned voice.",
key="text_input"
)
# Character count
char_count = len(text_input)
st.caption(f"Characters: {char_count}")
# Generate button
generate_button = st.button(
"đī¸ Generate Speech",
type="primary",
use_container_width=True,
disabled=not (text_input and st.session_state.reference_audio and st.session_state.reference_text)
)
with col2:
st.header("âšī¸ Instructions")
st.info("""
1. Load the VoxCPM model
2. Record/upload voice sample
3. Provide transcript (optional)
4. Enter text to generate
5. Click 'Generate Speech'
**Tip:** Longer, clearer voice samples produce better clones.
""")
# Generation and playback
if generate_button:
if st.session_state.model and st.session_state.reference_audio:
with st.spinner("đ¤ Generating speech... This may take a moment."):
# Show progress info
progress_bar = st.progress(0)
status_text = st.empty()
status_text.text("Processing audio...")
progress_bar.progress(25)
# Generate speech
generated_audio = generate_speech(
text=text_input,
reference_audio_path=st.session_state.reference_audio,
reference_text=st.session_state.reference_text,
cfg_value=cfg_value,
timesteps=timesteps
)
progress_bar.progress(75)
status_text.text("Finalizing...")
if generated_audio is not None:
st.session_state.generated_audio = generated_audio
st.session_state.sample_rate = st.session_state.model.tts_model.sample_rate
progress_bar.progress(100)
status_text.text("Complete!")
st.markdown('