import streamlit as st import os import tempfile import time import logging from typing import List, Dict, Any import uuid from pathlib import Path from dotenv import load_dotenv load_dotenv() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def create_interactive_citations(response_text: str, sources_used: List[Dict[str, Any]]) -> str: import re logger.info(f"Processing interactive citations for {len(sources_used)} sources") citation_map = {} for source in sources_used: ref = source.get('reference', '') if ref: match = re.search(r'\[(\d+)\]', ref) if match: num = match.group(1) citation_map[num] = source def replace_citation(match): """Replace citation number with interactive element""" full_match = match.group(0) # e.g., '[1]' num = match.group(1) # e.g., '1' if num in citation_map: source = citation_map[num] chunk_content = "Content not available" source_info = f"Source: {source.get('source_file', 'Unknown')}" if source.get('page_number'): source_info += f", Page: {source['page_number']}" try: if st.session_state.pipeline and st.session_state.pipeline['vector_db']: chunk_id = source.get('chunk_id') logger.info(f"Processing citation {num} with chunk_id: {chunk_id}") if chunk_id: chunk_data = st.session_state.pipeline['vector_db'].get_chunk_by_id(chunk_id) logger.info(f"Retrieved chunk data: {chunk_data is not None}") if chunk_data and chunk_data.get('content'): chunk_content = chunk_data['content'] logger.info(f"Got chunk content: {len(chunk_content)} characters") if len(chunk_content) > 300: chunk_content = chunk_content[:300] + "..." else: chunk_content = "Chunk content not available" logger.warning(f"Chunk data missing or no content: {chunk_data}") else: chunk_content = "No chunk ID provided" logger.warning(f"No chunk_id in source: {source}") else: chunk_content = "Vector database not available" logger.warning("Pipeline or vector_db not available") except Exception as e: logger.error(f"Error retrieving chunk content for citation {num}: {e}") chunk_content = f"Error retrieving chunk content: {str(e)}" chunk_content_escaped = (chunk_content .replace('<', '<') .replace('>', '>') .replace('\n', '
') .replace('"', '"')) source_info_escaped = (source_info .replace('<', '<') .replace('>', '>') .replace('"', '"')) return f''' {num}
{source_info_escaped}
{chunk_content_escaped}
''' else: return full_match # Replace all citation patterns [1], [2], etc. interactive_text = re.sub(r'\[(\d+)\]', replace_citation, response_text) return interactive_text from src.document_processing.doc_processor import DocumentProcessor from src.embeddings.embedding_generator import EmbeddingGenerator from src.vector_database.milvus_vector_db import MilvusVectorDB from src.generation.rag import RAGGenerator from src.memory.memory_layer import NotebookMemoryLayer from src.audio_processing.audio_transcriber import AudioTranscriber from src.audio_processing.youtube_transcriber import YouTubeTranscriber from src.web_scraping.web_scraper import WebScraper from src.podcast.script_generator import PodcastScriptGenerator from src.podcast.text_to_speech import PodcastTTSGenerator st.set_page_config( page_title="NotebookLM", page_icon="🧠", layout="wide", initial_sidebar_state="expanded" ) st.markdown(""" """, unsafe_allow_html=True) # Initialize session state def init_session_state(): if 'pipeline' not in st.session_state: st.session_state.pipeline = None if 'sources' not in st.session_state: st.session_state.sources = [] if 'chat_history' not in st.session_state: st.session_state.chat_history = [] if 'session_id' not in st.session_state: st.session_state.session_id = str(uuid.uuid4()) if 'show_source_dialog' not in st.session_state: st.session_state.show_source_dialog = False if 'pipeline_initialized' not in st.session_state: st.session_state.pipeline_initialized = False def reset_chat(): try: # Clear existing session from Zep if memory is available # if st.session_state.pipeline and st.session_state.pipeline['memory']: memory = st.session_state.pipeline['memory'] try: memory.clear_session() st.success("✅ Zep session cleared and recreated") except Exception as e: st.warning(f"Could not clear Zep session: {str(e)}") st.session_state.chat_history = [] st.session_state.session_id = str(uuid.uuid4()) # Reinitialize memory with new session if available if st.session_state.pipeline and st.session_state.pipeline['memory']: new_memory = NotebookMemoryLayer( user_id="streamlit_user", session_id=st.session_state.session_id, create_new_session=True ) st.session_state.pipeline['memory'] = new_memory st.success("✅ New Zep session initialized") st.success("✅ Chat reset successfully!") st.rerun() except Exception as e: st.error(f"❌ Error resetting chat: {str(e)}") def initialize_pipeline(): if st.session_state.pipeline_initialized: return True try: openai_key = os.getenv("OPENAI_API_KEY") assemblyai_key = os.getenv("ASSEMBLYAI_API_KEY") firecrawl_key = os.getenv("FIRECRAWL_API_KEY") zep_key = os.getenv("ZEP_API_KEY") with st.spinner("Initializing NotebookLM pipeline..."): doc_processor = DocumentProcessor() embedding_generator = EmbeddingGenerator() vector_db = MilvusVectorDB( db_path=f"./milvus_lite_{st.session_state.session_id[:8]}.db", collection_name=f"collection_{st.session_state.session_id[:8]}" ) rag_generator = RAGGenerator( embedding_generator=embedding_generator, vector_db=vector_db, openai_api_key=openai_key ) audio_transcriber = AudioTranscriber(assemblyai_key) if assemblyai_key else None youtube_transcriber = YouTubeTranscriber(assemblyai_key) if assemblyai_key else None web_scraper = WebScraper(firecrawl_key) if firecrawl_key else None podcast_script_generator = PodcastScriptGenerator(openai_key) if openai_key else None try: podcast_tts_generator = PodcastTTSGenerator() if openai_key else None if podcast_tts_generator: logger.info("PodcastTTSGenerator initialized successfully") except ImportError: logger.warning("Kokoro TTS not available. Podcast audio generation will be disabled.") podcast_tts_generator = None except Exception as e: logger.error(f"Error initializing TTS: {e}") podcast_tts_generator = None memory = None if zep_key: memory = NotebookMemoryLayer( user_id="streamlit_user", session_id=st.session_state.session_id, create_new_session=True ) st.session_state.pipeline = { 'doc_processor': doc_processor, 'embedding_generator': embedding_generator, 'vector_db': vector_db, 'rag_generator': rag_generator, 'audio_transcriber': audio_transcriber, 'youtube_transcriber': youtube_transcriber, 'web_scraper': web_scraper, 'podcast_script_generator': podcast_script_generator, 'podcast_tts_generator': podcast_tts_generator, 'memory': memory } st.session_state.pipeline_initialized = True st.success("✅ Pipeline initialized successfully!") return True except Exception as e: st.error(f"❌ Failed to initialize pipeline: {str(e)}") return False def process_uploaded_files(uploaded_files): if not st.session_state.pipeline: return pipeline = st.session_state.pipeline with st.spinner(f"Processing {len(uploaded_files)} file(s)..."): for uploaded_file in uploaded_files: try: with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as tmp_file: tmp_file.write(uploaded_file.getbuffer()) temp_path = tmp_file.name if uploaded_file.type.startswith('audio/'): if pipeline['audio_transcriber']: chunks = pipeline['audio_transcriber'].transcribe_audio(temp_path) source_type = "Audio" for chunk in chunks: chunk.source_file = uploaded_file.name else: st.warning(f"Audio processing not available for {uploaded_file.name}") os.unlink(temp_path) continue else: chunks = pipeline['doc_processor'].process_document(temp_path) source_type = "Document" for chunk in chunks: chunk.source_file = uploaded_file.name if chunks: embedded_chunks = pipeline['embedding_generator'].generate_embeddings(chunks) if len(st.session_state.sources) == 0: pipeline['vector_db'].create_index(use_binary_quantization=False) pipeline['vector_db'].insert_embeddings(embedded_chunks) source_info = { 'name': uploaded_file.name, 'type': source_type, 'size': f"{len(uploaded_file.getbuffer()) / 1024:.1f} KB", 'chunks': len(chunks), 'uploaded_at': time.strftime("%Y-%m-%d %H:%M") } st.session_state.sources.append(source_info) st.success(f"✅ Processed {uploaded_file.name}: {len(chunks)} chunks") os.unlink(temp_path) except Exception as e: st.error(f"❌ Failed to process {uploaded_file.name}: {str(e)}") if 'temp_path' in locals(): os.unlink(temp_path) def process_urls(urls_text): if not st.session_state.pipeline or not st.session_state.pipeline['web_scraper']: st.warning("Web scraping not available (missing FIRECRAWL_API_KEY)") return urls = [url.strip() for url in urls_text.split('\n') if url.strip()] if not urls: return pipeline = st.session_state.pipeline with st.spinner(f"Scraping {len(urls)} URL(s)..."): for url in urls: try: chunks = pipeline['web_scraper'].scrape_url(url) if chunks: for chunk in chunks: chunk.source_file = url embedded_chunks = pipeline['embedding_generator'].generate_embeddings(chunks) # Create index if first document if len(st.session_state.sources) == 0: pipeline['vector_db'].create_index(use_binary_quantization=False) pipeline['vector_db'].insert_embeddings(embedded_chunks) source_info = { 'name': url, 'type': "Website", 'size': f"{len(chunks)} chunks", 'chunks': len(chunks), 'uploaded_at': time.strftime("%Y-%m-%d %H:%M") } st.session_state.sources.append(source_info) st.success(f"✅ Scraped {url}: {len(chunks)} chunks") else: st.warning(f"No content extracted from {url}") except Exception as e: st.error(f"❌ Failed to scrape {url}: {str(e)}") def process_youtube_video(youtube_url): if not st.session_state.pipeline or not st.session_state.pipeline['youtube_transcriber']: st.warning("YouTube processing not available (missing ASSEMBLYAI_API_KEY)") return pipeline = st.session_state.pipeline transcriber = pipeline['youtube_transcriber'] with st.spinner("Processing YouTube video..."): try: chunks = transcriber.transcribe_youtube_video(youtube_url, cleanup_audio=True) if chunks: video_id = transcriber.extract_video_id(youtube_url) video_name = f"YouTube Video {video_id}" for chunk in chunks: chunk.source_file = video_name embedded_chunks = pipeline['embedding_generator'].generate_embeddings(chunks) if len(st.session_state.sources) == 0: pipeline['vector_db'].create_index(use_binary_quantization=False) pipeline['vector_db'].insert_embeddings(embedded_chunks) source_info = { 'name': video_name, 'type': "YouTube Video", 'size': f"{len(chunks)} utterances", 'chunks': len(chunks), 'uploaded_at': time.strftime("%Y-%m-%d %H:%M"), 'url': youtube_url, 'video_id': video_id } st.session_state.sources.append(source_info) st.success(f"✅ Processed YouTube video: {len(chunks)} utterances") else: st.warning("No transcript content extracted from the video") except Exception as e: st.error(f"❌ Failed to process YouTube video: {str(e)}") logger.error(f"YouTube processing error: {str(e)}") def process_text(text_content): if not st.session_state.pipeline or not text_content.strip(): return pipeline = st.session_state.pipeline with st.spinner("Processing text..."): try: with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as tmp_file: tmp_file.write(text_content) temp_path = tmp_file.name chunks = pipeline['doc_processor'].process_document(temp_path) original_name = f"Pasted Text ({time.strftime('%H:%M')})" for chunk in chunks: chunk.source_file = original_name if chunks: embedded_chunks = pipeline['embedding_generator'].generate_embeddings(chunks) if len(st.session_state.sources) == 0: pipeline['vector_db'].create_index(use_binary_quantization=False) pipeline['vector_db'].insert_embeddings(embedded_chunks) source_info = { 'name': original_name, 'type': "Text", 'size': f"{len(text_content)} chars", 'chunks': len(chunks), 'uploaded_at': time.strftime("%Y-%m-%d %H:%M") } st.session_state.sources.append(source_info) st.success(f"✅ Processed text: {len(chunks)} chunks") os.unlink(temp_path) except Exception as e: st.error(f"❌ Failed to process text: {str(e)}") def render_sources_sidebar(): with st.sidebar: st.markdown('
📚 Sources
', unsafe_allow_html=True) # if st.button("➕ Add", use_container_width=True): # st.session_state.show_source_dialog = True # Display sources if st.session_state.sources: st.markdown(f'
{len(st.session_state.sources)} sources
', unsafe_allow_html=True) for i, source in enumerate(st.session_state.sources): with st.container(): st.markdown(f'''
{source['name']}
{source['type']} • {source['size']} • {source['chunks']} chunks
{source['uploaded_at']}
''', unsafe_allow_html=True) else: st.markdown("""

Saved sources will appear here

Click Add source above to add PDFs, websites, text, videos, or audio files.

""", unsafe_allow_html=True) def render_source_upload_dialog(): st.markdown("### 📁 Add sources") st.markdown(""" Sources let NotebookLM base its responses on the information that matters most to you. (Examples: marketing plans, course reading, research notes, meeting transcripts, sales documents, etc.) """) # File upload section st.markdown("#### Upload sources") uploaded_files = st.file_uploader( "Drag & drop or choose file to upload", accept_multiple_files=True, type=['pdf', 'txt', 'md', 'mp3', 'wav', 'm4a', 'ogg'], help="Supported file types: PDF, .txt, Markdown, Audio (e.g. mp3)" ) if uploaded_files: if st.button("Process Files"): process_uploaded_files(uploaded_files) st.rerun() # Tabs for different input methods tab1, tab2, tab3 = st.tabs(["🌐 Website", "🎥 YouTube", "📋 Paste text"]) with tab1: st.markdown("#### Website URLs") urls_text = st.text_area( "Paste in Web URLs below to upload as sources", placeholder="https://example.com\nhttps://another-site.com", help="To add multiple URLs, separate with a space or new line.\nOnly the visible text on the website will be imported.\nPaid articles are not supported." ) if st.button("Process URLs", key="url_btn") and urls_text.strip(): process_urls(urls_text) st.rerun() with tab2: st.markdown("#### YouTube Videos") youtube_url = st.text_input( "Paste YouTube URL", placeholder="https://www.youtube.com/watch?v=...", help="Paste a YouTube video URL to extract and transcribe its audio content" ) if st.button("Process YouTube Video", key="youtube_btn") and youtube_url.strip(): process_youtube_video(youtube_url.strip()) st.rerun() with tab3: st.markdown("#### Paste copied text") text_content = st.text_area( "Paste your copied text below to upload as a source", placeholder="Paste text here...", height=200 ) if st.button("Process Text", key="text_btn") and text_content.strip(): process_text(text_content) st.rerun() def render_chat_interface(): col1, col2 = st.columns([3, 1]) with col1: st.markdown('
💬 Chat
', unsafe_allow_html=True) with col2: if st.session_state.chat_history: if st.button("🗑️ Reset", help="Clear chat history and start new session"): reset_chat() if not st.session_state.sources: st.markdown("""
Add a source in the "Add Sources" tab to get started
""", unsafe_allow_html=True) else: # Display chat history for message in st.session_state.chat_history: if message['role'] == 'user': st.markdown(f'''
You: {message['content']}
''', unsafe_allow_html=True) else: content_to_display = message.get('interactive_content', message['content']) st.markdown(f'''
Assistant: {content_to_display}
''', unsafe_allow_html=True) if 'citations' in message and not message.get('interactive_content'): citation_html = "".join([f'{cite}' for cite in message['citations']]) st.markdown(f'
{citation_html}
', unsafe_allow_html=True) col1, col2 = st.columns([10, 1]) with col1: query = st.text_input( "Upload a source to get started", placeholder="Ask me anything about your sources...", key="chat_input" ) with col2: send_button = st.button("➤", key="send_btn") if send_button and query.strip() and st.session_state.pipeline: with st.spinner("Thinking..."): try: result = st.session_state.pipeline['rag_generator'].generate_response(query) # Add to chat history st.session_state.chat_history.append({ 'role': 'user', 'content': query }) interactive_response = None if result.sources_used: try: interactive_response = create_interactive_citations(result.response, result.sources_used) logger.info(f"Created interactive citations for {len(result.sources_used)} sources") except Exception as e: logger.error(f"Failed to create interactive citations: {e}") else: logger.info("No sources available for interactive citations") citations = [] for source in result.sources_used: cite_text = f"Source: {source.get('source_file', 'Unknown')}" if source.get('page_number'): cite_text += f", Page: {source['page_number']}" citations.append(cite_text) st.session_state.chat_history.append({ 'role': 'assistant', 'content': result.response, 'interactive_content': interactive_response, 'citations': citations, 'sources_used': result.sources_used }) if st.session_state.pipeline['memory']: st.session_state.pipeline['memory'].save_conversation_turn(result) st.rerun() except Exception as e: st.error(f"Error generating response: {str(e)}") def generate_podcast(selected_source: str, podcast_style: str, podcast_length: str): if not st.session_state.pipeline or not st.session_state.pipeline['podcast_script_generator']: st.error("Podcast generation not available. Please check your OpenAI API key.") return pipeline = st.session_state.pipeline try: source_info = None for source in st.session_state.sources: if source['name'] == selected_source: source_info = source break if not source_info: st.error(f"Could not find source: {selected_source}") return # Gather content from the selected source with st.spinner(f"📚 Gathering content from {selected_source}..."): try: query_embedding = pipeline['embedding_generator'].generate_query_embedding(f"content from {selected_source}") search_results = pipeline['vector_db'].search( query_embedding, limit=50, filter_expr=f'source_file == "{selected_source}"' ) if not search_results: st.error(f"Could not find content for {selected_source}. Please try again.") return search_results.sort(key=lambda x: x.get('chunk_index', 0)) except Exception as e: st.error(f"Error retrieving content from {selected_source}: {e}") return with st.spinner("✍️ Generating podcast script..."): script_generator = pipeline['podcast_script_generator'] if source_info['type'] == 'Website': # For websites, use the specialized website method from dataclasses import dataclass @dataclass class ChunkLike: content: str chunks = [ChunkLike(content=result['content']) for result in search_results] podcast_script = script_generator.generate_script_from_website( website_chunks=chunks, source_url=selected_source, podcast_style=podcast_style.lower(), target_duration=podcast_length ) else: # For documents, audio, text, etc., use the text method combined_content = "\n\n".join([result['content'] for result in search_results]) podcast_script = script_generator.generate_script_from_text( text_content=combined_content, source_name=selected_source, podcast_style=podcast_style.lower(), target_duration=podcast_length ) st.success(f"✅ Generated podcast script with {podcast_script.total_lines} dialogue segments!") # Store script in session state for audio generation st.session_state.current_podcast_script = podcast_script # Automatically generate audio if TTS is available tts_generator = pipeline.get('podcast_tts_generator') if tts_generator: with st.spinner("🎵 Generating podcast... This may take several minutes..."): try: import tempfile temp_dir = tempfile.mkdtemp(prefix="podcast_") # Generate audio audio_files = tts_generator.generate_podcast_audio( podcast_script=podcast_script, output_dir=temp_dir, combine_audio=True ) st.success(f"✅ Generated {len(audio_files)} audio files!") st.markdown("### 🎙️ Generated Podcast") for audio_file in audio_files: file_name = Path(audio_file).name if "complete_podcast" in file_name: st.audio(audio_file, format="audio/wav") with open(audio_file, "rb") as f: st.download_button( label="📥 Download Complete Podcast", data=f.read(), file_name=f"complete_podcast_{int(time.time())}.wav", mime="audio/wav" ) except Exception as e: st.error(f"❌ Audio generation failed: {str(e)}") logger.error(f"Audio generation error: {e}") if "No module named" in str(e): st.error("🔧 Missing dependency. Please check the installation.") elif "File" in str(e) and "not found" in str(e): st.error("📁 File system error. Check permissions and disk space.") else: st.warning("⚠️ Audio generation not available - TTS not initialized.") # Display the generated script st.markdown("### 📝 Generated Podcast Script") col1, col2, col3 = st.columns(3) with col1: st.metric("📊 Total Lines", podcast_script.total_lines) with col2: st.metric("⏱️ Est. Duration", podcast_script.estimated_duration) with col3: st.metric("📚 Source Type", source_info['type']) # Display script content with st.expander("👀 View Complete Script", expanded=True): for i, line_dict in enumerate(podcast_script.script, 1): speaker, dialogue = next(iter(line_dict.items())) # Color code speakers if speaker == "Speaker 1": st.markdown(f'
👩 {speaker}: {dialogue}
', unsafe_allow_html=True) else: st.markdown(f'
👨 {speaker}: {dialogue}
', unsafe_allow_html=True) script_json = podcast_script.to_json() st.download_button( label="📥 Download Script (JSON)", data=script_json, file_name=f"podcast_script_{int(time.time())}.json", mime="application/json" ) except Exception as e: st.error(f"❌ Podcast generation failed: {str(e)}") logger.error(f"Podcast generation error: {e}") def render_studio_tab(): st.markdown('
🎙️ Studio
', unsafe_allow_html=True) if not st.session_state.sources: st.markdown("""

Studio output will be saved here.

After adding sources, click to add Podcast Generation and more!

""", unsafe_allow_html=True) else: st.markdown("#### 🎙️ Generate Podcast") st.markdown("Create an AI-generated podcast discussion from your documents") source_names = [source['name'] for source in st.session_state.sources] selected_source = st.selectbox( "Select source for podcast", source_names, index=0 if source_names else None, help="Choose a document to create a podcast discussion about" ) col1, col2 = st.columns(2) with col1: podcast_style = st.selectbox( "Podcast Style", ["Conversational", "Interview", "Debate", "Educational"] ) with col2: podcast_length = st.selectbox( "Duration", ["5 minutes", "10 minutes", "15 minutes", "20 minutes"] ) if st.button("🎙️ Generate Podcast", use_container_width=True): if selected_source: generate_podcast(selected_source, podcast_style, podcast_length) else: st.warning("Please select a source for the podcast") def main(): init_session_state() st.markdown("""

🧠 NotebookLM: Understand Anything

""", unsafe_allow_html=True) if not initialize_pipeline(): st.stop() render_sources_sidebar() tab1, tab2, tab3 = st.tabs(["📁 Add Sources", "💬 Chat", "🎙️ Studio"]) with tab1: render_source_upload_dialog() with tab2: render_chat_interface() with tab3: render_studio_tab() st.markdown("---") st.markdown("""
NotebookLM can be inaccurate; please double check its responses.
""", unsafe_allow_html=True) if __name__ == "__main__": main()