|
|
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") |