import os
import tempfile
import streamlit as st
from pathlib import Path
from pydub import AudioSegment
from scraper import WebScraper
from script_generator import ScriptGenerator
from tts_converter import TTSConverter
# Page config
st.set_page_config(
page_title="AI Podcast Generator",
page_icon="đī¸",
layout="wide"
)
# Custom CSS
st.markdown("""
""", unsafe_allow_html=True)
# Header
st.markdown('
đī¸ AI Podcast Generator
', unsafe_allow_html=True)
# Powered by widget
st.markdown('''
''', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
# Sidebar
with st.sidebar:
st.header("âī¸ Configuration")
st.subheader("API Keys")
minimax_key = st.text_input("Minimax API Key", type="password", help="Get your key from platform.minimax.io")
firecrawl_key = st.text_input("Firecrawl API Key", type="password", help="Get your key from firecrawl.dev")
openrouter_key = st.text_input("OpenRouter API Key", type="password", help="Get your key from openrouter.ai")
st.divider()
st.subheader("đ Input")
url = st.text_input("Article URL", placeholder="https://example.com/article")
st.divider()
generate_btn = st.button("đ Generate Podcast", type="primary", use_container_width=True)
# Main area
if generate_btn:
os.environ['FIRECRAWL_API_KEY'] = firecrawl_key
os.environ['OPENROUTER_API_KEY'] = openrouter_key
os.environ['MINIMAX_API_KEY'] = minimax_key
progress_container = st.container()
with progress_container:
# Step 1: Scraping
with st.status("đ Scraping content from URL...", expanded=True) as status:
try:
scraper = WebScraper()
content = scraper.scrape(url)
st.success(f"â
Successfully scraped {len(content)} characters")
status.update(label="â
Content scraped successfully!", state="complete")
except Exception as e:
st.error(f"â Scraping failed: {str(e)}")
st.stop()
# Step 2: Script Generation
with st.status("âī¸ Generating podcast script...", expanded=True) as status:
try:
generator = ScriptGenerator()
script = generator.generate(content)
st.success("â
Podcast script generated successfully")
status.update(label="â
Script generated successfully!", state="complete")
except Exception as e:
st.error(f"â Script generation failed: {str(e)}")
st.stop()
# Step 3: Audio Generation
with st.status("đī¸ Converting script to audio...", expanded=True) as status:
try:
converter = TTSConverter()
# Parse script to get segment count
segments = converter._parse_script(script)
total_segments = len(segments)
st.info(f"Generating {total_segments} audio segments...")
progress_bar = st.progress(0)
progress_text = st.empty()
# Create persistent audio directory
audio_dir = tempfile.mkdtemp()
audio_files = []
for i, (speaker, text) in enumerate(segments, 1):
progress_text.text(f"Processing segment {i}/{total_segments} ...")
voice = converter.voices.get(speaker, converter.voices["male"])
audio_file = os.path.join(audio_dir, f"segment_{i:03d}.mp3")
converter._generate_and_save_speech(text, voice, audio_file)
# Verify file exists before adding to list
if os.path.exists(audio_file) and os.path.getsize(audio_file) > 0:
audio_files.append((speaker, audio_file))
else:
raise Exception(f"Failed to generate audio for segment {i}")
progress_bar.progress(i / total_segments)
progress_text.text("â
All segments generated!")
status.update(label="â
Audio generated successfully!", state="complete")
except Exception as e:
st.error(f"â Audio generation failed: {str(e)}")
st.stop()
# Step 4: Merge Audio
with st.status("đ Merging audio segments...", expanded=True) as status:
try:
# Verify all files exist and filter out corrupted ones
st.info(f"Verifying {len(audio_files)} audio files...")
valid_files = []
corrupted_segments = []
for i, (speaker, filepath) in enumerate(audio_files, 1):
if not os.path.exists(filepath):
corrupted_segments.append(i)
st.warning(f"â ī¸ Segment {i} missing")
continue
file_size = os.path.getsize(filepath)
if file_size < 1024: # Less than 1KB is likely corrupted
corrupted_segments.append(i)
st.warning(f"â ī¸ Segment {i} corrupted ({file_size} bytes)")
continue
# Verify it's a valid MP3
try:
test_audio = AudioSegment.from_mp3(filepath)
if len(test_audio) < 100: # Less than 100ms is suspicious
corrupted_segments.append(i)
st.warning(f"â ī¸ Segment {i} too short")
continue
except Exception as e:
corrupted_segments.append(i)
st.warning(f"â ī¸ Segment {i} invalid MP3: {str(e)}")
continue
valid_files.append((speaker, filepath))
st.text(f"â Segment {i} verified ({file_size} bytes)")
if corrupted_segments:
st.warning(f"â ī¸ Skipping {len(corrupted_segments)} corrupted segments: {corrupted_segments}")
if len(valid_files) == 0:
raise Exception("No valid audio segments to merge")
st.info(f"Merging {len(valid_files)} valid audio segments...")
combined = AudioSegment.empty()
for i, (speaker, filepath) in enumerate(valid_files, 1):
st.text(f"Adding segment {i}/{len(valid_files)}...")
audio = AudioSegment.from_mp3(filepath)
combined += audio
# Export merged audio
output_path = os.path.join(audio_dir, "full_podcast.mp3")
st.info("Exporting final podcast...")
combined.export(output_path, format="mp3")
# Verify output file
if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
raise Exception("Failed to create merged podcast file")
st.success(f"â
Podcast merged successfully ({os.path.getsize(output_path)} bytes)")
if corrupted_segments:
st.info(f"âšī¸ Note: {len(corrupted_segments)} segments were skipped due to corruption")
status.update(label="â
Podcast ready!", state="complete")
# Update audio_files to only include valid ones
audio_files = valid_files
except Exception as e:
st.error(f"â Audio merging failed: {str(e)}")
st.write("Debug info:")
st.write(f"Audio directory: {audio_dir}")
st.write(f"Files in directory: {os.listdir(audio_dir) if os.path.exists(audio_dir) else 'Directory not found'}")
st.stop()
st.divider()
st.header("đ Results")
tab1, tab2 = st.tabs(["đ§ Podcast", "đ Script"])
with tab1:
st.subheader("Full Podcast")
with open(output_path, "rb") as f:
st.audio(f.read(), format="audio/mp3")
with open(output_path, "rb") as f:
st.download_button(
label="đĨ Download Full Podcast",
data=f.read(),
file_name="podcast.mp3",
mime="audio/mp3",
use_container_width=True
)
with tab2:
st.subheader("Podcast Script")
st.text_area("", script, height=400)
st.download_button(
label="đĨ Download Script",
data=script,
file_name="podcast_script.txt",
mime="text/plain",
use_container_width=True
)
st.balloons()
else:
st.info("đ Get started by entering your API keys in the sidebar and providing an article URL")
with st.expander("âšī¸ How it works"):
st.markdown("""
1. **Scraping**: We use Firecrawl to extract clean content from any URL
2. **Script Generation**: Minimax-M2.1 transforms the content into an engaging dialogue
3. **Audio Synthesis**: Minimax Speech 2.6 creates natural-sounding voices for both hosts
4. **Merging**: All segments are combined into one seamless podcast
""")
with st.expander("đ Where to get API keys"):
st.markdown("""
- **Minimax**: [platform.minimax.io](https://platform.minimax.io)
- **Firecrawl**: [firecrawl.dev](https://firecrawl.dev)
- **OpenRouter**: [openrouter.ai](https://openrouter.ai)
""")