| | import streamlit as st |
| | import os |
| | from datetime import datetime |
| | import time |
| | import traceback |
| |
|
| | st.set_page_config( |
| | page_title="QualiInsightLab", |
| | page_icon="🎙️", |
| | layout="wide", |
| | initial_sidebar_state="expanded" |
| | ) |
| |
|
| | |
| | from transcription import AudioTranscriber |
| | from report_generator import ReportGenerator |
| | from file_handler import FileHandler |
| | from config import NVIDIA_THEME, DEFAULT_SETTINGS |
| |
|
| | |
| | st.markdown(f""" |
| | <style> |
| | .main {{ |
| | background-color: {NVIDIA_THEME['background']}; |
| | color: {NVIDIA_THEME['text']}; |
| | }} |
| | .stButton > button {{ |
| | background-color: {NVIDIA_THEME['primary']}; |
| | color: {NVIDIA_THEME['background']}; |
| | border: none; |
| | border-radius: 5px; |
| | font-weight: bold; |
| | }} |
| | .stButton > button:hover {{ |
| | background-color: {NVIDIA_THEME['accent']}; |
| | color: {NVIDIA_THEME['background']}; |
| | }} |
| | .sidebar .sidebar-content {{ |
| | background-color: {NVIDIA_THEME['secondary']}; |
| | }} |
| | .stProgress > div > div {{ |
| | background-color: {NVIDIA_THEME['primary']}; |
| | }} |
| | .success-box {{ |
| | background-color: rgba(118, 185, 0, 0.1); |
| | border: 1px solid {NVIDIA_THEME['primary']}; |
| | border-radius: 5px; |
| | padding: 10px; |
| | margin: 10px 0; |
| | }} |
| | .error-box {{ |
| | background-color: rgba(255, 0, 0, 0.1); |
| | border: 1px solid #ff0000; |
| | border-radius: 5px; |
| | padding: 10px; |
| | margin: 10px 0; |
| | }} |
| | </style> |
| | """, unsafe_allow_html=True) |
| |
|
| | class FGIIDIAnalyzer: |
| | def __init__(self): |
| | self.transcriber = None |
| | self.report_generator = None |
| | self.file_handler = FileHandler() |
| | self.initialize_session_state() |
| | |
| | def initialize_session_state(self): |
| | """Inicjalizacja session state""" |
| | if 'transcriptions' not in st.session_state: |
| | st.session_state.transcriptions = {} |
| | if 'uploaded_files' not in st.session_state: |
| | st.session_state.uploaded_files = [] |
| | if 'processing_status' not in st.session_state: |
| | st.session_state.processing_status = 'ready' |
| | if 'final_report' not in st.session_state: |
| | st.session_state.final_report = None |
| | if 'research_brief' not in st.session_state: |
| | st.session_state.research_brief = "" |
| | if 'logs' not in st.session_state: |
| | st.session_state.logs = [] |
| | |
| | def log_message(self, message, level="INFO"): |
| | """Dodaj wiadomość do logów""" |
| | timestamp = datetime.now().strftime("%H:%M:%S") |
| | log_entry = f"[{timestamp}] {level}: {message}" |
| | st.session_state.logs.append(log_entry) |
| | |
| | |
| | if len(st.session_state.logs) > 100: |
| | st.session_state.logs = st.session_state.logs[-100:] |
| | |
| | |
| | print(log_entry) |
| | |
| | def render_sidebar(self): |
| | """Renderuj sidebar z konfiguracją""" |
| | st.sidebar.title("🎙️ QualiInsightLab") |
| | st.sidebar.markdown("---") |
| | |
| | |
| | st.sidebar.subheader("🔑 Konfiguracja API") |
| | |
| | openai_key = st.sidebar.text_input( |
| | "OpenAI API Key:", |
| | type="password", |
| | help="Klucz do Whisper (transkrypcja) i GPT-4o-mini (raporty)" |
| | ) |
| | |
| | if openai_key: |
| | self.transcriber = AudioTranscriber(openai_key) |
| | self.report_generator = ReportGenerator(openai_key) |
| | st.sidebar.success("✅ API połączone") |
| | else: |
| | st.sidebar.warning("⚠️ Wprowadź klucz API") |
| | |
| | st.sidebar.markdown("---") |
| | |
| | |
| | st.sidebar.subheader("⚙️ Ustawienia") |
| | |
| | max_file_size = st.sidebar.selectbox( |
| | "Maksymalny rozmiar części:", |
| | [10, 15, 20, 25], |
| | index=1, |
| | help="MB - większe pliki będą dzielone na części (max 25MB dla Whisper)" |
| | ) |
| | |
| | auto_compress = st.sidebar.checkbox( |
| | "Auto-kompresja dużych plików", |
| | value=True, |
| | help="Automatyczna kompresja plików >25MB" |
| | ) |
| | |
| | language = st.sidebar.selectbox( |
| | "Język transkrypcji:", |
| | ["pl", "en", "auto"], |
| | index=0, |
| | help="Język audio dla Whisper" |
| | ) |
| | |
| | st.sidebar.markdown("---") |
| | |
| | |
| | st.sidebar.subheader("📊 Status") |
| | |
| | if st.session_state.uploaded_files: |
| | st.sidebar.info(f"📁 Plików: {len(st.session_state.uploaded_files)}") |
| | |
| | if st.session_state.transcriptions: |
| | st.sidebar.info(f"✅ Transkrypcji: {len(st.session_state.transcriptions)}") |
| | |
| | if st.session_state.final_report: |
| | st.sidebar.success("📄 Raport gotowy") |
| | |
| | |
| | if st.sidebar.button("🔄 Reset sesji", type="secondary"): |
| | for key in list(st.session_state.keys()): |
| | del st.session_state[key] |
| | st.rerun() |
| | |
| | return { |
| | 'openai_key': openai_key, |
| | 'max_file_size': max_file_size, |
| | 'auto_compress': auto_compress, |
| | 'language': language |
| | } |
| | |
| | def render_file_upload(self, settings): |
| | """Renderuj sekcję upload plików""" |
| | st.header("📁 Upload plików audio/video") |
| | |
| | |
| | st.subheader("📋 Brief badawczy (opcjonalny)") |
| | research_brief = st.text_area( |
| | "Opisz cele badania, grupę docelową, kluczowe pytania badawcze:", |
| | value=st.session_state.research_brief, |
| | height=100, |
| | help="Ten opis pomoże AI wygenerować lepszy raport" |
| | ) |
| | st.session_state.research_brief = research_brief |
| | |
| | |
| | st.subheader("🎙️ Pliki do transkrypcji") |
| | uploaded_files = st.file_uploader( |
| | "Wybierz pliki audio/video:", |
| | type=['mp3', 'wav', 'mp4', 'm4a', 'aac'], |
| | accept_multiple_files=True, |
| | help="Obsługiwane formaty: MP3, WAV, MP4, M4A, AAC (max 25MB każdy dla Whisper)" |
| | ) |
| | |
| | if uploaded_files: |
| | |
| | valid_files = [] |
| | total_size = 0 |
| | |
| | for file in uploaded_files: |
| | file_size_mb = file.size / (1024 * 1024) |
| | total_size += file_size_mb |
| | |
| | |
| | if file_size_mb > 100: |
| | st.error(f"❌ {file.name}: Plik za duży ({file_size_mb:.1f}MB). Maksymalnie 100MB.") |
| | continue |
| | |
| | valid_files.append({ |
| | 'file': file, |
| | 'name': file.name, |
| | 'size_mb': file_size_mb, |
| | 'needs_splitting': file_size_mb > settings['max_file_size'], |
| | 'needs_compression': file_size_mb > 25 |
| | }) |
| | |
| | |
| | if valid_files: |
| | st.success(f"✅ Załadowano {len(valid_files)} plików ({total_size:.1f}MB)") |
| | |
| | |
| | for i, file_info in enumerate(valid_files): |
| | col1, col2, col3 = st.columns([3, 1, 1]) |
| | |
| | with col1: |
| | st.write(f"📄 {file_info['name']}") |
| | |
| | with col2: |
| | st.write(f"{file_info['size_mb']:.1f}MB") |
| | |
| | with col3: |
| | if file_info['needs_compression']: |
| | st.warning("Kompresja") |
| | elif file_info['needs_splitting']: |
| | st.warning("Podział") |
| | else: |
| | st.success("OK") |
| | |
| | st.session_state.uploaded_files = valid_files |
| | return True |
| | |
| | return False |
| | |
| | def render_processing_section(self, settings): |
| | """Renderuj sekcję przetwarzania""" |
| | if not st.session_state.uploaded_files: |
| | st.info("👆 Najpierw załaduj pliki audio/video") |
| | return |
| | |
| | if not settings['openai_key']: |
| | st.warning("⚠️ Wprowadź klucz OpenAI API w sidebarze") |
| | return |
| | |
| | st.header("🚀 Przetwarzanie") |
| | |
| | |
| | if st.session_state.processing_status == 'ready': |
| | if st.button("🎯 Rozpocznij transkrypcję i analizę", type="primary"): |
| | st.session_state.processing_status = 'running' |
| | |
| | self.process_files(settings) |
| | |
| | elif st.session_state.processing_status == 'running': |
| | st.info("⏳ Przetwarzanie w toku...") |
| | |
| | |
| | elif st.session_state.processing_status == 'completed': |
| | st.success("🎉 Przetwarzanie zakończone pomyślnie!") |
| | |
| | if st.button("🔄 Nowe przetwarzanie", type="primary"): |
| | st.session_state.processing_status = 'ready' |
| | st.session_state.transcriptions = {} |
| | st.session_state.final_report = None |
| | st.rerun() |
| | |
| | elif st.session_state.processing_status == 'error': |
| | st.error("❌ Przetwarzanie zakończone błędem") |
| | |
| | if st.button("🔄 Spróbuj ponownie", type="primary"): |
| | st.session_state.processing_status = 'ready' |
| | st.rerun() |
| | |
| | def process_files(self, settings): |
| | """Główna logika przetwarzania plików - SYNCHRONICZNA""" |
| | try: |
| | self.log_message("🚀 Rozpoczynam przetwarzanie plików") |
| | |
| | |
| | status_container = st.empty() |
| | progress_container = st.empty() |
| | |
| | |
| | for i, file_info in enumerate(st.session_state.uploaded_files): |
| | |
| | status_container.info(f"🎙️ Transkrybuję: {file_info['name']} ({i+1}/{len(st.session_state.uploaded_files)})") |
| | self.log_message(f"📝 Rozpoczynam transkrypcję: {file_info['name']}") |
| | |
| | try: |
| | |
| | processed_files = self.file_handler.process_file( |
| | file_info['file'], |
| | settings['max_file_size'], |
| | settings['auto_compress'] |
| | ) |
| | |
| | if not processed_files: |
| | raise Exception("Nie udało się przetworzyć pliku") |
| | |
| | |
| | transcription = self.transcriber.transcribe_files( |
| | processed_files, |
| | language=settings['language'] |
| | ) |
| | |
| | st.session_state.transcriptions[file_info['name']] = transcription |
| | self.log_message(f"✅ Zakończono transkrypcję: {file_info['name']}") |
| | |
| | |
| | progress = (i + 1) / len(st.session_state.uploaded_files) |
| | progress_container.progress(progress) |
| | |
| | except Exception as e: |
| | self.log_message(f"❌ Błąd transkrypcji {file_info['name']}: {str(e)}", "ERROR") |
| | st.error(f"Błąd przy {file_info['name']}: {str(e)}") |
| | continue |
| | |
| | |
| | if st.session_state.transcriptions: |
| | status_container.info("📄 Generuję raport badawczy...") |
| | self.log_message("📄 Rozpoczynam generowanie raportu") |
| | |
| | try: |
| | final_report = self.report_generator.generate_comprehensive_report( |
| | st.session_state.transcriptions, |
| | st.session_state.research_brief |
| | ) |
| | |
| | st.session_state.final_report = final_report |
| | st.session_state.processing_status = 'completed' |
| | |
| | status_container.success("✅ Przetwarzanie zakończone!") |
| | self.log_message("🎉 Raport wygenerowany pomyślnie!") |
| | |
| | |
| | st.rerun() |
| | |
| | except Exception as e: |
| | self.log_message(f"❌ Błąd generowania raportu: {str(e)}", "ERROR") |
| | st.error(f"Błąd generowania raportu: {str(e)}") |
| | st.session_state.processing_status = 'error' |
| | else: |
| | st.session_state.processing_status = 'error' |
| | self.log_message("❌ Brak transkrypcji do wygenerowania raportu", "ERROR") |
| | |
| | except Exception as e: |
| | self.log_message(f"💥 Błąd krytyczny: {str(e)}", "ERROR") |
| | st.error(f"Błąd krytyczny: {str(e)}") |
| | st.session_state.processing_status = 'error' |
| | |
| | def render_results(self): |
| | """Renderuj wyniki""" |
| | if not st.session_state.transcriptions and not st.session_state.final_report: |
| | return |
| | |
| | st.header("📊 Wyniki") |
| | |
| | |
| | tab1, tab2, tab3 = st.tabs(["📄 Raport", "🎙️ Transkrypcje", "📋 Logi"]) |
| | |
| | with tab1: |
| | if st.session_state.final_report: |
| | st.subheader("📄 Raport z badania") |
| | |
| | |
| | if st.download_button( |
| | label="📥 Pobierz raport (TXT)", |
| | data=st.session_state.final_report, |
| | file_name=f"raport_badawczy_{datetime.now().strftime('%Y%m%d_%H%M')}.txt", |
| | mime="text/plain" |
| | ): |
| | st.success("✅ Raport pobierany!") |
| | |
| | |
| | st.markdown("---") |
| | st.markdown(st.session_state.final_report) |
| | else: |
| | st.info("Raport będzie dostępny po zakończeniu przetwarzania") |
| | |
| | with tab2: |
| | if st.session_state.transcriptions: |
| | st.subheader("🎙️ Transkrypcje") |
| | |
| | for filename, transcription in st.session_state.transcriptions.items(): |
| | with st.expander(f"📄 {filename}"): |
| | st.write(transcription) |
| | |
| | |
| | st.download_button( |
| | label=f"📥 Pobierz {filename}", |
| | data=transcription, |
| | file_name=f"transkrypcja_{filename}_{datetime.now().strftime('%Y%m%d_%H%M')}.txt", |
| | mime="text/plain", |
| | key=f"download_{filename}" |
| | ) |
| | else: |
| | st.info("Transkrypcje będą dostępne po przetworzeniu plików") |
| | |
| | with tab3: |
| | st.subheader("📋 Logi procesu") |
| | |
| | if st.session_state.logs: |
| | |
| | if st.button("🧹 Wyczyść logi"): |
| | st.session_state.logs = [] |
| | st.rerun() |
| | |
| | |
| | logs_text = "\n".join(reversed(st.session_state.logs)) |
| | st.text_area( |
| | "Logi systemu (najnowsze na górze):", |
| | value=logs_text, |
| | height=400, |
| | disabled=True |
| | ) |
| | else: |
| | st.info("Logi będą wyświetlane podczas przetwarzania") |
| | |
| | def run(self): |
| | """Główna funkcja aplikacji""" |
| | |
| | settings = self.render_sidebar() |
| | |
| | |
| | st.title("🎙️ QualiInsightLab") |
| | st.markdown("*Automatyczna transkrypcja i analiza wywiadów fokusowych oraz indywidualnych*") |
| | st.markdown("---") |
| | |
| | |
| | files_uploaded = self.render_file_upload(settings) |
| | |
| | st.markdown("---") |
| | |
| | |
| | self.render_processing_section(settings) |
| | |
| | st.markdown("---") |
| | |
| | |
| | self.render_results() |
| |
|
| | |
| | if __name__ == "__main__": |
| | try: |
| | app = FGIIDIAnalyzer() |
| | app.run() |
| | except Exception as e: |
| | st.error(f"💥 Błąd aplikacji: {str(e)}") |
| | st.code(traceback.format_exc()) |
| | |
| | |
| | with open('error_log.txt', 'w', encoding='utf-8') as f: |
| | f.write(f"Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") |
| | |
| | st.info("Szczegóły błędu zapisane w error_log.txt") |