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('''
Powered by
Minimax Firecrawl
''', unsafe_allow_html=True) st.markdown('
Transform any web article into an engaging podcast
', 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) """)