Marek4321 commited on
Commit
7889259
·
verified ·
1 Parent(s): 6fec816

Upload 5 files

Browse files
Files changed (5) hide show
  1. app.py +455 -0
  2. config.py +296 -0
  3. file_handler.py +277 -0
  4. report_generator.py +399 -0
  5. transcription.py +287 -0
app.py ADDED
@@ -0,0 +1,455 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ from datetime import datetime
4
+ import time
5
+ import traceback
6
+
7
+ # Import modułów
8
+ from transcription import AudioTranscriber
9
+ from report_generator import ReportGenerator
10
+ from file_handler import FileHandler
11
+ from config import NVIDIA_THEME, DEFAULT_SETTINGS
12
+
13
+ # Konfiguracja strony
14
+ st.set_page_config(
15
+ page_title="FGI/IDI Research Analyzer",
16
+ page_icon="🎙️",
17
+ layout="wide",
18
+ initial_sidebar_state="expanded"
19
+ )
20
+
21
+ # Custom CSS - kolorystyka NVIDIA
22
+ st.markdown(f"""
23
+ <style>
24
+ .main {{
25
+ background-color: {NVIDIA_THEME['background']};
26
+ color: {NVIDIA_THEME['text']};
27
+ }}
28
+ .stButton > button {{
29
+ background-color: {NVIDIA_THEME['primary']};
30
+ color: {NVIDIA_THEME['background']};
31
+ border: none;
32
+ border-radius: 5px;
33
+ font-weight: bold;
34
+ }}
35
+ .stButton > button:hover {{
36
+ background-color: {NVIDIA_THEME['accent']};
37
+ color: {NVIDIA_THEME['background']};
38
+ }}
39
+ .sidebar .sidebar-content {{
40
+ background-color: {NVIDIA_THEME['secondary']};
41
+ }}
42
+ .stProgress > div > div {{
43
+ background-color: {NVIDIA_THEME['primary']};
44
+ }}
45
+ .success-box {{
46
+ background-color: rgba(118, 185, 0, 0.1);
47
+ border: 1px solid {NVIDIA_THEME['primary']};
48
+ border-radius: 5px;
49
+ padding: 10px;
50
+ margin: 10px 0;
51
+ }}
52
+ .error-box {{
53
+ background-color: rgba(255, 0, 0, 0.1);
54
+ border: 1px solid #ff0000;
55
+ border-radius: 5px;
56
+ padding: 10px;
57
+ margin: 10px 0;
58
+ }}
59
+ </style>
60
+ """, unsafe_allow_html=True)
61
+
62
+ class FGIIDIAnalyzer:
63
+ def __init__(self):
64
+ self.transcriber = None
65
+ self.report_generator = None
66
+ self.file_handler = FileHandler()
67
+ self.initialize_session_state()
68
+
69
+ def initialize_session_state(self):
70
+ """Inicjalizacja session state"""
71
+ if 'transcriptions' not in st.session_state:
72
+ st.session_state.transcriptions = {}
73
+ if 'uploaded_files' not in st.session_state:
74
+ st.session_state.uploaded_files = []
75
+ if 'processing_status' not in st.session_state:
76
+ st.session_state.processing_status = 'ready'
77
+ if 'final_report' not in st.session_state:
78
+ st.session_state.final_report = None
79
+ if 'research_brief' not in st.session_state:
80
+ st.session_state.research_brief = ""
81
+ if 'logs' not in st.session_state:
82
+ st.session_state.logs = []
83
+
84
+ def log_message(self, message, level="INFO"):
85
+ """Dodaj wiadomość do logów"""
86
+ timestamp = datetime.now().strftime("%H:%M:%S")
87
+ log_entry = f"[{timestamp}] {level}: {message}"
88
+ st.session_state.logs.append(log_entry)
89
+
90
+ # Ograniczenie liczby logów do 100
91
+ if len(st.session_state.logs) > 100:
92
+ st.session_state.logs = st.session_state.logs[-100:]
93
+
94
+ def render_sidebar(self):
95
+ """Renderuj sidebar z konfiguracją"""
96
+ st.sidebar.title("🎙️ FGI/IDI Analyzer")
97
+ st.sidebar.markdown("---")
98
+
99
+ # API Keys
100
+ st.sidebar.subheader("🔑 Konfiguracja API")
101
+
102
+ openai_key = st.sidebar.text_input(
103
+ "OpenAI API Key:",
104
+ type="password",
105
+ help="Klucz do Whisper (transkrypcja) i GPT-4o-mini (raporty)"
106
+ )
107
+
108
+ if openai_key:
109
+ self.transcriber = AudioTranscriber(openai_key)
110
+ self.report_generator = ReportGenerator(openai_key)
111
+ st.sidebar.success("✅ API połączone")
112
+ else:
113
+ st.sidebar.warning("⚠️ Wprowadź klucz API")
114
+
115
+ st.sidebar.markdown("---")
116
+
117
+ # Ustawienia transkrypcji
118
+ st.sidebar.subheader("⚙️ Ustawienia")
119
+
120
+ max_file_size = st.sidebar.selectbox(
121
+ "Maksymalny rozmiar części:",
122
+ [15, 20, 25, 30],
123
+ index=1,
124
+ help="MB - większe pliki będą dzielone na części"
125
+ )
126
+
127
+ auto_compress = st.sidebar.checkbox(
128
+ "Auto-kompresja dużych plików",
129
+ value=True,
130
+ help="Automatyczna kompresja plików >50MB"
131
+ )
132
+
133
+ language = st.sidebar.selectbox(
134
+ "Język transkrypcji:",
135
+ ["pl", "en", "auto"],
136
+ index=0,
137
+ help="Język audio dla Whisper"
138
+ )
139
+
140
+ st.sidebar.markdown("---")
141
+
142
+ # Status systemu
143
+ st.sidebar.subheader("📊 Status")
144
+
145
+ if st.session_state.uploaded_files:
146
+ st.sidebar.info(f"📁 Plików: {len(st.session_state.uploaded_files)}")
147
+
148
+ if st.session_state.transcriptions:
149
+ st.sidebar.info(f"✅ Transkrypcji: {len(st.session_state.transcriptions)}")
150
+
151
+ if st.session_state.final_report:
152
+ st.sidebar.success("📄 Raport gotowy")
153
+
154
+ # Reset session
155
+ if st.sidebar.button("🔄 Reset sesji", type="secondary"):
156
+ for key in list(st.session_state.keys()):
157
+ del st.session_state[key]
158
+ st.rerun()
159
+
160
+ return {
161
+ 'openai_key': openai_key,
162
+ 'max_file_size': max_file_size,
163
+ 'auto_compress': auto_compress,
164
+ 'language': language
165
+ }
166
+
167
+ def render_file_upload(self, settings):
168
+ """Renderuj sekcję upload plików"""
169
+ st.header("📁 Upload plików audio/video")
170
+
171
+ # Research brief
172
+ st.subheader("📋 Brief badawczy (opcjonalny)")
173
+ research_brief = st.text_area(
174
+ "Opisz cele badania, grupę docelową, kluczowe pytania badawcze:",
175
+ value=st.session_state.research_brief,
176
+ height=100,
177
+ help="Ten opis pomoże AI wygenerować lepszy raport"
178
+ )
179
+ st.session_state.research_brief = research_brief
180
+
181
+ # File uploader
182
+ st.subheader("🎙️ Pliki do transkrypcji")
183
+ uploaded_files = st.file_uploader(
184
+ "Wybierz pliki audio/video:",
185
+ type=['mp3', 'wav', 'mp4', 'm4a', 'aac'],
186
+ accept_multiple_files=True,
187
+ help="Obsługiwane formaty: MP3, WAV, MP4, M4A, AAC"
188
+ )
189
+
190
+ if uploaded_files:
191
+ # Walidacja plików
192
+ valid_files = []
193
+ total_size = 0
194
+
195
+ for file in uploaded_files:
196
+ file_size_mb = file.size / (1024 * 1024)
197
+ total_size += file_size_mb
198
+
199
+ # Sprawdź rozmiar pojedynczego pliku
200
+ if file_size_mb > 200: # 200MB limit dla pojedynczego pliku
201
+ st.error(f"❌ {file.name}: Plik za duży ({file_size_mb:.1f}MB). Maksymalnie 200MB.")
202
+ continue
203
+
204
+ valid_files.append({
205
+ 'file': file,
206
+ 'name': file.name,
207
+ 'size_mb': file_size_mb,
208
+ 'needs_splitting': file_size_mb > settings['max_file_size']
209
+ })
210
+
211
+ # Wyświetl informacje o plikach
212
+ if valid_files:
213
+ st.success(f"✅ Załadowano {len(valid_files)} plików ({total_size:.1f}MB)")
214
+
215
+ # Tabela z informacjami o plikach
216
+ for i, file_info in enumerate(valid_files):
217
+ col1, col2, col3 = st.columns([3, 1, 1])
218
+
219
+ with col1:
220
+ st.write(f"📄 {file_info['name']}")
221
+
222
+ with col2:
223
+ st.write(f"{file_info['size_mb']:.1f}MB")
224
+
225
+ with col3:
226
+ if file_info['needs_splitting']:
227
+ st.warning("Będzie podzielony")
228
+ else:
229
+ st.success("OK")
230
+
231
+ st.session_state.uploaded_files = valid_files
232
+ return True
233
+
234
+ return False
235
+
236
+ def render_processing_section(self, settings):
237
+ """Renderuj sekcję przetwarzania"""
238
+ if not st.session_state.uploaded_files:
239
+ st.info("👆 Najpierw załaduj pliki audio/video")
240
+ return
241
+
242
+ if not settings['openai_key']:
243
+ st.warning("⚠️ Wprowadź klucz OpenAI API w sidebarze")
244
+ return
245
+
246
+ st.header("🚀 Przetwarzanie")
247
+
248
+ # Przycisk start
249
+ if st.session_state.processing_status == 'ready':
250
+ if st.button("🎯 Rozpocznij transkrypcję i analizę", type="primary"):
251
+ st.session_state.processing_status = 'running'
252
+ self.process_files(settings)
253
+
254
+ elif st.session_state.processing_status == 'running':
255
+ st.info("⏳ Przetwarzanie w toku...")
256
+
257
+ if st.button("⏹️ Zatrzymaj", type="secondary"):
258
+ st.session_state.processing_status = 'stopped'
259
+ st.warning("Przetwarzanie zatrzymane")
260
+
261
+ # Progress display
262
+ if st.session_state.processing_status == 'running':
263
+ self.render_progress()
264
+
265
+ def render_progress(self):
266
+ """Renderuj postęp przetwarzania"""
267
+ progress_container = st.container()
268
+
269
+ with progress_container:
270
+ # Overall progress
271
+ total_files = len(st.session_state.uploaded_files)
272
+ completed_files = len(st.session_state.transcriptions)
273
+
274
+ progress = completed_files / total_files if total_files > 0 else 0
275
+ st.progress(progress)
276
+ st.write(f"📊 Postęp ogólny: {completed_files}/{total_files} plików")
277
+
278
+ # Current file info
279
+ if completed_files < total_files:
280
+ current_file = st.session_state.uploaded_files[completed_files]['name']
281
+ st.write(f"🔄 Aktualnie: {current_file}")
282
+
283
+ def process_files(self, settings):
284
+ """Główna logika przetwarzania plików"""
285
+ try:
286
+ self.log_message("Rozpoczynam przetwarzanie plików")
287
+
288
+ # Container dla live updates
289
+ status_container = st.empty()
290
+ progress_container = st.empty()
291
+
292
+ # 1. Transkrypcja wszystkich plików
293
+ for i, file_info in enumerate(st.session_state.uploaded_files):
294
+ if st.session_state.processing_status != 'running':
295
+ break
296
+
297
+ status_container.info(f"🎙️ Transkrybuję: {file_info['name']}")
298
+ self.log_message(f"Rozpoczynam transkrypcję: {file_info['name']}")
299
+
300
+ try:
301
+ # Przetwórz plik (podział jeśli potrzeba)
302
+ processed_files = self.file_handler.process_file(
303
+ file_info['file'],
304
+ settings['max_file_size'],
305
+ settings['auto_compress']
306
+ )
307
+
308
+ # Transkrypcja
309
+ transcription = self.transcriber.transcribe_files(
310
+ processed_files,
311
+ language=settings['language']
312
+ )
313
+
314
+ st.session_state.transcriptions[file_info['name']] = transcription
315
+ self.log_message(f"✅ Zakończono transkrypcję: {file_info['name']}")
316
+
317
+ # Update progress
318
+ progress = (i + 1) / len(st.session_state.uploaded_files)
319
+ progress_container.progress(progress)
320
+
321
+ except Exception as e:
322
+ self.log_message(f"❌ Błąd transkrypcji {file_info['name']}: {str(e)}", "ERROR")
323
+ st.error(f"Błąd przy {file_info['name']}: {str(e)}")
324
+
325
+ # 2. Generowanie raportu
326
+ if st.session_state.transcriptions and st.session_state.processing_status == 'running':
327
+ status_container.info("📄 Generuję raport badawczy...")
328
+ self.log_message("Rozpoczynam generowanie raportu")
329
+
330
+ try:
331
+ final_report = self.report_generator.generate_comprehensive_report(
332
+ st.session_state.transcriptions,
333
+ st.session_state.research_brief
334
+ )
335
+
336
+ st.session_state.final_report = final_report
337
+ st.session_state.processing_status = 'completed'
338
+
339
+ status_container.success("✅ Przetwarzanie zakończone!")
340
+ self.log_message("✅ Raport wygenerowany pomyślnie")
341
+
342
+ except Exception as e:
343
+ self.log_message(f"❌ Błąd generowania raportu: {str(e)}", "ERROR")
344
+ st.error(f"Błąd generowania raportu: {str(e)}")
345
+ st.session_state.processing_status = 'error'
346
+
347
+ except Exception as e:
348
+ self.log_message(f"💥 Błąd krytyczny: {str(e)}", "ERROR")
349
+ st.error(f"Błąd krytyczny: {str(e)}")
350
+ st.session_state.processing_status = 'error'
351
+
352
+ def render_results(self):
353
+ """Renderuj wyniki"""
354
+ if not st.session_state.transcriptions and not st.session_state.final_report:
355
+ return
356
+
357
+ st.header("📊 Wyniki")
358
+
359
+ # Tabs dla różnych widoków
360
+ tab1, tab2, tab3 = st.tabs(["📄 Raport", "🎙️ Transkrypcje", "📋 Logi"])
361
+
362
+ with tab1:
363
+ if st.session_state.final_report:
364
+ st.subheader("📄 Raport z badania")
365
+
366
+ # Download button
367
+ if st.download_button(
368
+ label="📥 Pobierz raport (TXT)",
369
+ data=st.session_state.final_report,
370
+ file_name=f"raport_badawczy_{datetime.now().strftime('%Y%m%d_%H%M')}.txt",
371
+ mime="text/plain"
372
+ ):
373
+ st.success("✅ Raport pobierany!")
374
+
375
+ # Display report
376
+ st.markdown("---")
377
+ st.markdown(st.session_state.final_report)
378
+ else:
379
+ st.info("Raport będzie dostępny po zakończeniu przetwarzania")
380
+
381
+ with tab2:
382
+ if st.session_state.transcriptions:
383
+ st.subheader("🎙️ Transkrypcje")
384
+
385
+ for filename, transcription in st.session_state.transcriptions.items():
386
+ with st.expander(f"📄 {filename}"):
387
+ st.write(transcription)
388
+
389
+ # Download individual transcription
390
+ st.download_button(
391
+ label=f"📥 Pobierz {filename}",
392
+ data=transcription,
393
+ file_name=f"transkrypcja_{filename}_{datetime.now().strftime('%Y%m%d_%H%M')}.txt",
394
+ mime="text/plain",
395
+ key=f"download_{filename}"
396
+ )
397
+ else:
398
+ st.info("Transkrypcje będą dostępne po przetworzeniu plików")
399
+
400
+ with tab3:
401
+ st.subheader("📋 Logi procesu")
402
+
403
+ if st.session_state.logs:
404
+ # Scroll to bottom option
405
+ if st.button("⬇️ Przewiń na dół"):
406
+ pass # Auto-scroll jest w CSS
407
+
408
+ # Display logs
409
+ logs_text = "\n".join(st.session_state.logs)
410
+ st.text_area(
411
+ "Logi:",
412
+ value=logs_text,
413
+ height=400,
414
+ disabled=True
415
+ )
416
+ else:
417
+ st.info("Logi będą wyświetlane podczas przetwarzania")
418
+
419
+ def run(self):
420
+ """Główna funkcja aplikacji"""
421
+ # Sidebar
422
+ settings = self.render_sidebar()
423
+
424
+ # Main content
425
+ st.title("🎙️ FGI/IDI Research Analyzer")
426
+ st.markdown("*Automatyczna transkrypcja i analiza wywiadów fokusowych oraz indywidualnych*")
427
+ st.markdown("---")
428
+
429
+ # File upload section
430
+ files_uploaded = self.render_file_upload(settings)
431
+
432
+ st.markdown("---")
433
+
434
+ # Processing section
435
+ self.render_processing_section(settings)
436
+
437
+ st.markdown("---")
438
+
439
+ # Results section
440
+ self.render_results()
441
+
442
+ # Główna aplikacja
443
+ if __name__ == "__main__":
444
+ try:
445
+ app = FGIIDIAnalyzer()
446
+ app.run()
447
+ except Exception as e:
448
+ st.error(f"💥 Błąd aplikacji: {str(e)}")
449
+ st.code(traceback.format_exc())
450
+
451
+ # Log error for debugging
452
+ with open('error_log.txt', 'w', encoding='utf-8') as f:
453
+ f.write(f"Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
454
+
455
+ st.info("Szczegóły błędu zapisane w error_log.txt")
config.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py - Konfiguracja aplikacji FGI/IDI Analyzer
2
+
3
+ # Kolorystyka NVIDIA - Gaming/Tech Style
4
+ NVIDIA_THEME = {
5
+ 'primary': '#00FF88', # Bright neon green (akcenty)
6
+ 'secondary': '#1B1B1B', # Very dark gray (tło sekcji)
7
+ 'background': '#0A0A0A', # Near black (główne tło)
8
+ 'text': '#E0E0E0', # Light gray text
9
+ 'text_secondary': '#A0A0A0', # Darker gray for secondary text
10
+ 'accent': '#00CC66', # Darker green for hover states
11
+ 'error': '#FF4444', # Red
12
+ 'warning': '#FFAA00', # Orange
13
+ 'success': '#00FF88', # Same as primary
14
+ 'border': '#333333', # Dark border
15
+ 'card_bg': '#151515', # Card backgrounds
16
+ }
17
+
18
+ # Ustawienia domyślne
19
+ DEFAULT_SETTINGS = {
20
+ 'max_file_size_mb': 20,
21
+ 'max_total_size_mb': 500,
22
+ 'supported_formats': ['mp3', 'wav', 'mp4', 'm4a', 'aac'],
23
+ 'whisper_model': 'whisper-1',
24
+ 'gpt_model': 'gpt-4o-mini',
25
+ 'default_language': 'pl',
26
+ 'chunk_overlap_seconds': 30,
27
+ 'max_retries': 3,
28
+ 'timeout_seconds': 300,
29
+ }
30
+
31
+ # Prompty dla różnych etapów raportowania
32
+ REPORT_PROMPTS = {
33
+ 'outline_generator': """
34
+ Jesteś ekspertem analizy badań jakościowych. Na podstawie dostarczonych transkrypcji z wywiadów {interview_type} oraz briefu badawczego, stwórz szczegółowy plan raportu badawczego.
35
+
36
+ TRANSKRYPCJE:
37
+ {transcriptions}
38
+
39
+ BRIEF BADAWCZY:
40
+ {brief}
41
+
42
+ ZADANIE:
43
+ Przeanalizuj materiał i stwórz outline raportu, który:
44
+ 1. Odpowie na cele badawcze z briefu
45
+ 2. Uwzględni specyfikę {interview_type}
46
+ 3. Będzie miał logiczną strukturę od ogółu do szczegółu
47
+ 4. Pozwoli na głęboką analizę insights
48
+
49
+ WYMAGANIA:
50
+ - Outline powinien mieć 5-8 głównych sekcji
51
+ - Każda sekcja z 3-5 podpunktami
52
+ - Uwzględnij cytaty/przykłady tam gdzie to sensowne
53
+ - Zaplanuj miejsca na insights, wnioski, rekomendacje
54
+
55
+ FORMAT ODPOWIEDZI:
56
+ ```
57
+ # OUTLINE RAPORTU
58
+
59
+ ## 1. [Nazwa sekcji]
60
+ - [Podpunkt 1]
61
+ - [Podpunkt 2]
62
+ - [Podpunkt 3]
63
+
64
+ ## 2. [Nazwa sekcji]
65
+ ...
66
+ ```
67
+ """,
68
+
69
+ 'section_generator': """
70
+ Jesteś ekspertem analizy badań jakościowych. Napisz szczegółową sekcję raportu zgodnie z planem.
71
+
72
+ CONTEXT:
73
+ - Typ wywiadu: {interview_type}
74
+ - Brief badawczy: {brief}
75
+ - Plan całego raportu: {outline}
76
+
77
+ TRANSKRYPCJE:
78
+ {transcriptions}
79
+
80
+ ZADANIE:
81
+ Napisz sekcję: "{section_title}"
82
+ Podpunkty do uwzględnienia: {section_points}
83
+
84
+ WYMAGANIA:
85
+ - Sekcja powinna mieć 800-1500 słów
86
+ - Użyj konkretnych cytatów z transkrypcji
87
+ - Analizuj głęboko, nie tylko opisuj
88
+ - Połącz insights z celami biznesowymi
89
+ - Używaj podtytułów dla czytelności
90
+ - Zachowaj obiektywność ale wyciągnij wnioski
91
+
92
+ STYLE:
93
+ - Profesjonalny ale przystępny język
94
+ - Strukturyzowany, z jasnymi insights
95
+ - Cytaty w cudzysłowach z oznaczeniem respondenta
96
+ - Wnioski poparte danymi z wywiadów
97
+ """,
98
+
99
+ 'section_expander': """
100
+ Otrzymałeś sekcję raportu, która jest zbyt krótka i powierzchowna. Twoim zadaniem jest ją znacznie rozszerzyć i pogłębić.
101
+
102
+ OBECNA SEKCJA:
103
+ {current_section}
104
+
105
+ DOSTĘPNE TRANSKRYPCJE:
106
+ {transcriptions}
107
+
108
+ CONTEXT:
109
+ {brief}
110
+
111
+ ZADANIE:
112
+ Rozszerz tę sekcję do 1000-1500 słów poprzez:
113
+
114
+ 1. **Pogłębienie analizy** - zadaj sobie pytania:
115
+ - Jakie są głębsze przyczyny tych zachowań/opinii?
116
+ - Jakie wzorce widać w różnych grupach respondentów?
117
+ - Jak to łączy się z celami biznesowymi?
118
+
119
+ 2. **Dodanie cytatów** - znajdź w transkrypcjach:
120
+ - Konkretne przykłady wspierające tezy
121
+ - Różnorodne perspektywy respondentów
122
+ - Emocjonalne reakcje i spontaniczne komentarze
123
+
124
+ 3. **Strukturyzacja** - podziel na podsekcje:
125
+ - Główne tematy/wątki
126
+ - Segmenty respondentów
127
+ - Konkretne insights
128
+
129
+ 4. **Praktyczne wnioski** - dodaj:
130
+ - Implikacje dla biznesu
131
+ - Możliwe działania
132
+ - Ryzyka i szanse
133
+
134
+ WYMAGANIA:
135
+ - Zachowaj oryginalną strukturę ale ją rozbuduj
136
+ - Dodaj minimum 5 cytatów z transkrypcji
137
+ - Każdy wniosek uzasadnij danymi
138
+ - Użyj podtytułów dla czytelności
139
+ """,
140
+
141
+ 'final_assembly': """
142
+ Jesteś ekspertem analizy badań jakościowych. Twoim zadaniem jest sfinalizowanie raportu - dodanie wprowadzenia, executive summary i spójne połączenie wszystkich sekcji.
143
+
144
+ SEKCJE RAPORTU:
145
+ {sections}
146
+
147
+ BRIEF BADAWCZY:
148
+ {brief}
149
+
150
+ METADANE:
151
+ - Typ badania: {interview_type}
152
+ - Liczba wywiadów: {interviews_count}
153
+ - Data analizy: {date}
154
+
155
+ ZADANIE:
156
+ Stwórz kompletny raport dodając:
157
+
158
+ 1. **EXECUTIVE SUMMARY** (300-500 słów):
159
+ - Główne insights z każdej sekcji
160
+ - Key takeaways dla biznesu
161
+ - Top 3 rekomendacje
162
+
163
+ 2. **WPROWADZENIE** (200-300 słów):
164
+ - Cele badania
165
+ - Metodologia
166
+ - Struktura raportu
167
+
168
+ 3. **ZAKOŃCZENIE** (300-400 słów):
169
+ - Podsumowanie głównych wniosków
170
+ - Rekomendacje działań
171
+ - Sugerowane dalsze kroki
172
+
173
+ 4. **SPÓJNOŚĆ**:
174
+ - Dodaj przejścia między sekcjami
175
+ - Ujednolic style i terminologię
176
+ - Sprawdź logiczny przepływ
177
+
178
+ FORMAT:
179
+ ```
180
+ # RAPORT Z BADANIA [TYP]
181
+
182
+ ## EXECUTIVE SUMMARY
183
+ [treść]
184
+
185
+ ## 1. WPROWADZENIE
186
+ [treść]
187
+
188
+ ## 2. METODOLOGIA
189
+ [treść]
190
+
191
+ [SEKCJE GŁÓWNE]
192
+
193
+ ## ZAKOŃCZENIE I REKOMENDACJE
194
+ [treść]
195
+
196
+ ## APPENDIX
197
+ - Informacje o respondentach
198
+ - Dodatkowe cytaty
199
+ ```
200
+ """,
201
+
202
+ 'quality_checker': """
203
+ Otrzymałeś sekcję raportu do oceny jakości. Sprawdź czy spełnia standardy profesjonalnego raportu z badań jakościowych.
204
+
205
+ SEKCJA DO OCENY:
206
+ {section}
207
+
208
+ KRYTERIA OCENY:
209
+ 1. **Długość**: Czy ma 800+ słów?
210
+ 2. **Głębokość**: Czy analizuje przyczyny, nie tylko opisuje?
211
+ 3. **Cytaty**: Czy ma konkretne przykłady z wywiadów?
212
+ 4. **Struktura**: Czy ma logiczny podział i podtytuły?
213
+ 5. **Insights**: Czy wyciąga praktyczne wnioski?
214
+ 6. **Biznesowość**: Czy łączy z celami biznesowymi?
215
+
216
+ ZADANIE:
217
+ Oceń sekcję w skali 1-10 za każde kryterium i podaj:
218
+ - Ogólną ocenę
219
+ - Konkretne problemy do poprawy
220
+ - Sugestie rozszerzeń
221
+
222
+ FORMAT:
223
+ ```
224
+ OCENA JAKOŚCI:
225
+ - Długość: X/10
226
+ - Głębokość: X/10
227
+ - Cytaty: X/10
228
+ - Struktura: X/10
229
+ - Insights: X/10
230
+ - Biznesowość: X/10
231
+
232
+ ŚREDNIA: X/10
233
+
234
+ PROBLEMY:
235
+ - [konkretny problem 1]
236
+ - [konkretny problem 2]
237
+
238
+ SUGESTIE:
239
+ - [sugestia poprawy 1]
240
+ - [sugestia poprawy 2]
241
+
242
+ CZY WYMAGA POPRAWY: TAK/NIE
243
+ ```
244
+ """
245
+ }
246
+
247
+ # Ustawienia modeli
248
+ MODEL_SETTINGS = {
249
+ 'whisper': {
250
+ 'model': 'whisper-1',
251
+ 'language': 'pl',
252
+ 'temperature': 0,
253
+ 'max_retries': 3,
254
+ },
255
+
256
+ 'gpt': {
257
+ 'model': 'gpt-4o-mini',
258
+ 'temperature': 0.3,
259
+ 'max_tokens': 4000,
260
+ 'max_retries': 3,
261
+ 'timeout': 300,
262
+ }
263
+ }
264
+
265
+ # Mapowanie typów wywiadów
266
+ INTERVIEW_TYPES = {
267
+ 'fgi': 'Focus Group Interview (wywiad fokusowy)',
268
+ 'idi': 'In-Depth Interview (wywiad indywidualny)',
269
+ 'auto': 'automatyczne rozpoznanie na podstawie treści'
270
+ }
271
+
272
+ # Ustawienia przetwarzania plików
273
+ FILE_PROCESSING = {
274
+ 'max_single_file_mb': 200,
275
+ 'chunk_size_mb': 20,
276
+ 'supported_audio_formats': ['mp3', 'wav', 'm4a', 'aac'],
277
+ 'supported_video_formats': ['mp4', 'mov', 'avi'],
278
+ 'compression_quality': 64, # kbps dla audio
279
+ 'sample_rate': 16000, # Hz
280
+ }
281
+
282
+ # Komunikaty dla użytkownika
283
+ USER_MESSAGES = {
284
+ 'file_too_large': "Plik {filename} jest za duży ({size}MB). Maksymalnie {max_size}MB. Czy chcesz go skompresować automatycznie?",
285
+ 'compression_success': "✅ Plik {filename} skompresowany z {old_size}MB do {new_size}MB",
286
+ 'transcription_start': "🎙️ Rozpoczynam transkrypcję: {filename}",
287
+ 'transcription_success': "✅ Transkrypcja zakończona: {filename}",
288
+ 'transcription_error': "❌ Błąd transkrypcji {filename}: {error}",
289
+ 'report_generation_start': "📄 Generuję raport badawczy...",
290
+ 'report_section_done': "✅ Sekcja '{section}' wygenerowana",
291
+ 'report_complete': "🎉 Raport badawczy gotowy!",
292
+ 'api_key_missing': "⚠️ Wprowadź klucz OpenAI API",
293
+ 'processing_stopped': "⏹️ Przetwarzanie zatrzymane przez użytkownika",
294
+ 'no_files_uploaded': "📁 Nie załadowano żadnych plików",
295
+ 'session_reset': "🔄 Sesja została zresetowana"
296
+ }
file_handler.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # file_handler.py - Obsługa plików audio/video dla HuggingFace
2
+
3
+ import os
4
+ import tempfile
5
+ import math
6
+ from io import BytesIO
7
+ from typing import List, Dict, Tuple, Union
8
+ import streamlit as st
9
+
10
+ try:
11
+ from pydub import AudioSegment
12
+ PYDUB_AVAILABLE = True
13
+ except ImportError:
14
+ PYDUB_AVAILABLE = False
15
+ st.warning("⚠️ Pydub nie jest dostępny. Zainstaluj: pip install pydub")
16
+
17
+ try:
18
+ import librosa
19
+ import soundfile as sf
20
+ LIBROSA_AVAILABLE = True
21
+ except ImportError:
22
+ LIBROSA_AVAILABLE = False
23
+
24
+ from config import FILE_PROCESSING, USER_MESSAGES
25
+
26
+ class FileHandler:
27
+ """Klasa do obsługi plików audio/video - optymalizowana dla HuggingFace"""
28
+
29
+ def __init__(self):
30
+ self.temp_files = [] # Lista plików tymczasowych do wyczyszczenia
31
+ self.processing_stats = {}
32
+
33
+ def process_file(self, uploaded_file, max_chunk_size_mb: int = 20, auto_compress: bool = True) -> List[str]:
34
+ """
35
+ Główna funkcja przetwarzania pliku
36
+ Returns: Lista ścieżek do plików gotowych do transkrypcji
37
+ """
38
+ try:
39
+ file_size_mb = uploaded_file.size / (1024 * 1024)
40
+
41
+ # Loguj rozpoczęcie przetwarzania
42
+ st.info(f"🔄 Przetwarzam {uploaded_file.name} ({file_size_mb:.1f}MB)")
43
+
44
+ # Sprawdź czy plik wymaga kompresji
45
+ if file_size_mb > 50 and auto_compress:
46
+ compressed_file = self._compress_audio(uploaded_file)
47
+ if compressed_file:
48
+ uploaded_file = compressed_file
49
+ file_size_mb = compressed_file.size / (1024 * 1024)
50
+ st.success(f"✅ Skompresowano do {file_size_mb:.1f}MB")
51
+
52
+ # Sprawdź czy plik wymaga dzielenia
53
+ if file_size_mb > max_chunk_size_mb:
54
+ return self._split_audio_file(uploaded_file, max_chunk_size_mb)
55
+ else:
56
+ # Plik nie wymaga dzielenia - zapisz bezpośrednio
57
+ temp_path = self._save_temp_file(uploaded_file)
58
+ return [temp_path]
59
+
60
+ except Exception as e:
61
+ st.error(f"❌ Błąd przetwarzania {uploaded_file.name}: {str(e)}")
62
+ return []
63
+
64
+ def _compress_audio(self, uploaded_file) -> Union[BytesIO, None]:
65
+ """Kompresja pliku audio używając pydub"""
66
+ if not PYDUB_AVAILABLE:
67
+ st.warning("Pydub niedostępny - pomijam kompresję")
68
+ return None
69
+
70
+ try:
71
+ # Załaduj audio
72
+ audio_data = uploaded_file.read()
73
+ audio = AudioSegment.from_file(BytesIO(audio_data))
74
+
75
+ # Kompresja: mono, lower bitrate, lower sample rate
76
+ compressed = audio.set_channels(1) # Mono
77
+ compressed = compressed.set_frame_rate(16000) # 16kHz (wystarczy dla mowy)
78
+
79
+ # Export do BytesIO
80
+ output = BytesIO()
81
+ compressed.export(
82
+ output,
83
+ format="mp3",
84
+ bitrate="64k", # Niska jakość dla kompresji
85
+ parameters=["-ac", "1"] # Force mono
86
+ )
87
+ output.seek(0)
88
+
89
+ # Stwórz nowy "uploaded file" object
90
+ output.name = uploaded_file.name.replace('.', '_compressed.')
91
+ output.size = len(output.getvalue())
92
+
93
+ return output
94
+
95
+ except Exception as e:
96
+ st.warning(f"Kompresja nieudana: {str(e)}")
97
+ return None
98
+
99
+ def _split_audio_file(self, uploaded_file, max_chunk_size_mb: int) -> List[str]:
100
+ """Dzieli plik audio na mniejsze części"""
101
+ try:
102
+ if not PYDUB_AVAILABLE:
103
+ st.error("❌ Pydub wymagany do dzielenia plików. Zainstaluj: pip install pydub")
104
+ return []
105
+
106
+ # Załaduj cały plik audio
107
+ audio_data = uploaded_file.read()
108
+ audio = AudioSegment.from_file(BytesIO(audio_data))
109
+
110
+ # Oblicz parametry dzielenia
111
+ total_duration_ms = len(audio)
112
+ file_size_mb = uploaded_file.size / (1024 * 1024)
113
+
114
+ # Estymacja liczby części na podstawie rozmiaru
115
+ estimated_parts = math.ceil(file_size_mb / max_chunk_size_mb)
116
+ chunk_duration_ms = total_duration_ms // estimated_parts
117
+
118
+ # Dodaj overlap między częściami (30 sekund)
119
+ overlap_ms = 30 * 1000
120
+
121
+ st.info(f"📂 Dzielę na {estimated_parts} części (~{chunk_duration_ms//60000:.1f} min każda)")
122
+
123
+ parts = []
124
+ base_name = os.path.splitext(uploaded_file.name)[0]
125
+
126
+ for i in range(estimated_parts):
127
+ start_ms = max(0, i * chunk_duration_ms - overlap_ms if i > 0 else 0)
128
+ end_ms = min(total_duration_ms, (i + 1) * chunk_duration_ms + overlap_ms)
129
+
130
+ # Wytnij część
131
+ chunk = audio[start_ms:end_ms]
132
+
133
+ # Zapisz do pliku tymczasowego
134
+ temp_fd, temp_path = tempfile.mkstemp(suffix=f"_part{i+1:02d}.mp3", prefix=f"{base_name}_")
135
+ os.close(temp_fd)
136
+
137
+ chunk.export(temp_path, format="mp3", bitrate="128k")
138
+ parts.append(temp_path)
139
+ self.temp_files.append(temp_path)
140
+
141
+ st.success(f"✅ Część {i+1}/{estimated_parts}: {(end_ms-start_ms)//60000:.1f} min")
142
+
143
+ return parts
144
+
145
+ except Exception as e:
146
+ st.error(f"❌ Błąd dzielenia pliku: {str(e)}")
147
+ return []
148
+
149
+ def _save_temp_file(self, uploaded_file) -> str:
150
+ """Zapisuje uploaded file do pliku tymczasowego"""
151
+ try:
152
+ # Stwórz plik tymczasowy
153
+ suffix = f".{uploaded_file.name.split('.')[-1]}"
154
+ temp_fd, temp_path = tempfile.mkstemp(suffix=suffix)
155
+
156
+ # Zapisz dane
157
+ with os.fdopen(temp_fd, 'wb') as tmp_file:
158
+ tmp_file.write(uploaded_file.read())
159
+
160
+ self.temp_files.append(temp_path)
161
+ return temp_path
162
+
163
+ except Exception as e:
164
+ st.error(f"❌ Błąd zapisu tymczasowego: {str(e)}")
165
+ return ""
166
+
167
+ def get_audio_duration(self, file_path: str) -> float:
168
+ """Pobierz długość pliku audio w sekundach"""
169
+ try:
170
+ if LIBROSA_AVAILABLE:
171
+ duration = librosa.get_duration(filename=file_path)
172
+ return duration
173
+ elif PYDUB_AVAILABLE:
174
+ audio = AudioSegment.from_file(file_path)
175
+ return len(audio) / 1000.0 # Convert ms to seconds
176
+ else:
177
+ # Fallback - estymacja na podstawie rozmiaru
178
+ file_size = os.path.getsize(file_path)
179
+ # Przybliżenie: 1MB ≈ 60 sekund dla typowego audio MP3
180
+ return file_size / (1024 * 1024) * 60
181
+ except:
182
+ # Ostateczny fallback
183
+ file_size = os.path.getsize(file_path)
184
+ return file_size / (1024 * 1024) * 60
185
+
186
+ def validate_file(self, uploaded_file) -> Tuple[bool, str]:
187
+ """Walidacja pliku audio/video"""
188
+ try:
189
+ # Sprawdź rozmiar
190
+ file_size_mb = uploaded_file.size / (1024 * 1024)
191
+ if file_size_mb > FILE_PROCESSING['max_single_file_mb']:
192
+ return False, f"Plik za duży: {file_size_mb:.1f}MB > {FILE_PROCESSING['max_single_file_mb']}MB"
193
+
194
+ # Sprawdź rozszerzenie
195
+ file_ext = uploaded_file.name.split('.')[-1].lower()
196
+ supported_formats = (
197
+ FILE_PROCESSING['supported_audio_formats'] +
198
+ FILE_PROCESSING['supported_video_formats']
199
+ )
200
+
201
+ if file_ext not in supported_formats:
202
+ return False, f"Nieobsługiwany format: .{file_ext}"
203
+
204
+ # Sprawdź czy plik nie jest pusty
205
+ if uploaded_file.size == 0:
206
+ return False, "Plik jest pusty"
207
+
208
+ return True, "OK"
209
+
210
+ except Exception as e:
211
+ return False, f"Błąd walidacji: {str(e)}"
212
+
213
+ def estimate_processing_time(self, uploaded_files: List) -> Dict:
214
+ """Estymuj czas przetwarzania"""
215
+ total_size_mb = sum(f.size for f in uploaded_files) / (1024 * 1024)
216
+ total_duration_est = total_size_mb * 60 # 1MB ≈ 60s audio
217
+
218
+ # Estymacja czasu transkrypcji (Whisper ~1:10 ratio)
219
+ transcription_time = total_duration_est * 1.1
220
+
221
+ # Estymacja czasu generowania raportu (zależnie od liczby wywiadów)
222
+ report_time = len(uploaded_files) * 30 # ~30s per interview dla raportu
223
+
224
+ return {
225
+ 'total_size_mb': total_size_mb,
226
+ 'estimated_audio_duration': total_duration_est,
227
+ 'estimated_transcription_time': transcription_time,
228
+ 'estimated_report_time': report_time,
229
+ 'total_estimated_time': transcription_time + report_time
230
+ }
231
+
232
+ def get_file_info(self, uploaded_file) -> Dict:
233
+ """Pobierz informacje o pliku"""
234
+ file_size_mb = uploaded_file.size / (1024 * 1024)
235
+ file_ext = uploaded_file.name.split('.')[-1].lower()
236
+
237
+ return {
238
+ 'name': uploaded_file.name,
239
+ 'size_mb': file_size_mb,
240
+ 'format': file_ext,
241
+ 'needs_compression': file_size_mb > 50,
242
+ 'needs_splitting': file_size_mb > 20,
243
+ 'estimated_duration': file_size_mb * 60 # Rough estimate
244
+ }
245
+
246
+ def cleanup_temp_files(self):
247
+ """Wyczyść pliki tymczasowe"""
248
+ cleaned = 0
249
+ for temp_file in self.temp_files:
250
+ try:
251
+ if os.path.exists(temp_file):
252
+ os.remove(temp_file)
253
+ cleaned += 1
254
+ except Exception as e:
255
+ st.warning(f"Nie można usunąć {temp_file}: {e}")
256
+
257
+ self.temp_files = []
258
+ if cleaned > 0:
259
+ st.success(f"🧹 Wyczyszczono {cleaned} plików tymczasowych")
260
+
261
+ def get_processing_stats(self) -> Dict:
262
+ """Zwróć statystyki przetwarzania"""
263
+ return {
264
+ 'temp_files_count': len(self.temp_files),
265
+ 'processing_stats': self.processing_stats,
266
+ 'libraries_available': {
267
+ 'pydub': PYDUB_AVAILABLE,
268
+ 'librosa': LIBROSA_AVAILABLE
269
+ }
270
+ }
271
+
272
+ # Test funkcji
273
+ if __name__ == "__main__":
274
+ print("🧪 Test FileHandler")
275
+ handler = FileHandler()
276
+ print(f"📊 Dostępne biblioteki: {handler.get_processing_stats()['libraries_available']}")
277
+ print("✅ FileHandler gotowy do użycia")
report_generator.py ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # report_generator.py - Inteligentny generator raportów z self-prompting
2
+
3
+ import time
4
+ import streamlit as st
5
+ from typing import Dict, List, Optional, Tuple
6
+ from datetime import datetime
7
+
8
+ try:
9
+ from openai import OpenAI
10
+ OPENAI_AVAILABLE = True
11
+ except ImportError:
12
+ OPENAI_AVAILABLE = False
13
+ st.error("❌ OpenAI library nie jest dostępna")
14
+
15
+ from config import REPORT_PROMPTS, MODEL_SETTINGS, INTERVIEW_TYPES
16
+
17
+ class ReportGenerator:
18
+ """Inteligentny generator długich raportów badawczych z self-prompting"""
19
+
20
+ def __init__(self, api_key: str):
21
+ if not OPENAI_AVAILABLE:
22
+ raise Exception("OpenAI library nie jest dostępna")
23
+
24
+ self.client = OpenAI(api_key=api_key)
25
+ self.api_key = api_key
26
+ self.generation_stats = {
27
+ 'sections_generated': 0,
28
+ 'sections_expanded': 0,
29
+ 'total_tokens_used': 0,
30
+ 'total_cost_estimate': 0,
31
+ 'generation_time': 0
32
+ }
33
+
34
+ def generate_comprehensive_report(self, transcriptions: Dict[str, str], brief: str = "") -> str:
35
+ """
36
+ Główna funkcja generowania kompletnego raportu
37
+ Używa strategii wieloetapowej z self-prompting
38
+ """
39
+ start_time = time.time()
40
+
41
+ try:
42
+ st.info("📋 Rozpoczynam generowanie raportu...")
43
+
44
+ # Przygotuj dane
45
+ combined_transcriptions = self._combine_transcriptions(transcriptions)
46
+ interview_type = self._detect_interview_type(combined_transcriptions)
47
+
48
+ st.info(f"🔍 Wykryto typ: {INTERVIEW_TYPES.get(interview_type, 'nieznany')}")
49
+
50
+ # ETAP 1: Generowanie outline'u
51
+ st.info("📝 Etap 1/4: Tworzenie struktury raportu...")
52
+ outline = self._generate_outline(combined_transcriptions, brief, interview_type)
53
+
54
+ if not outline:
55
+ raise Exception("Nie udało się wygenerować struktury raportu")
56
+
57
+ # ETAP 2: Generowanie sekcji po sekcji
58
+ st.info("✍️ Etap 2/4: Generowanie treści sekcji...")
59
+ sections = self._generate_sections_iteratively(
60
+ outline, combined_transcriptions, brief, interview_type
61
+ )
62
+
63
+ # ETAP 3: Rozszerzanie zbyt krótkich sekcji (self-prompting)
64
+ st.info("🔍 Etap 3/4: Pogłębianie analizy...")
65
+ expanded_sections = self._expand_short_sections(
66
+ sections, combined_transcriptions, brief
67
+ )
68
+
69
+ # ETAP 4: Finalne scalenie z wprowadzeniem i podsumowaniem
70
+ st.info("📄 Etap 4/4: Finalne scalenie...")
71
+ final_report = self._assemble_final_report(
72
+ expanded_sections, brief, interview_type, len(transcriptions)
73
+ )
74
+
75
+ # Statystyki
76
+ self.generation_stats['generation_time'] = time.time() - start_time
77
+
78
+ st.success(f"🎉 Raport wygenerowany! ({self.generation_stats['generation_time']:.1f}s)")
79
+ self._log_generation_stats()
80
+
81
+ return final_report
82
+
83
+ except Exception as e:
84
+ st.error(f"❌ Błąd generowania raportu: {str(e)}")
85
+ raise e
86
+
87
+ def _combine_transcriptions(self, transcriptions: Dict[str, str]) -> str:
88
+ """Połącz wszystkie transkrypcje w jeden tekst z oznaczeniami"""
89
+ combined = []
90
+
91
+ for i, (filename, transcription) in enumerate(transcriptions.items(), 1):
92
+ header = f"\n\n=== WYWIAD {i}: {filename} ===\n\n"
93
+ combined.append(header + transcription)
94
+
95
+ return "\n".join(combined)
96
+
97
+ def _detect_interview_type(self, transcriptions: str) -> str:
98
+ """Automatyczne rozpoznanie typu wywiadu"""
99
+ text_lower = transcriptions.lower()
100
+
101
+ # Wskaźniki FGI
102
+ fgi_indicators = [
103
+ 'moderator', 'grupa', 'wszyscy', 'uczestnicy', 'dyskusja',
104
+ 'czy zgadzacie się', 'co myślicie', 'focus group'
105
+ ]
106
+
107
+ # Wskaźniki IDI
108
+ idi_indicators = [
109
+ 'wywiad indywidualny', 'jeden na jeden', 'interviewer',
110
+ 'opowiedz mi', 'jak się czujesz', 'twoje doświadczenie'
111
+ ]
112
+
113
+ fgi_score = sum(1 for indicator in fgi_indicators if indicator in text_lower)
114
+ idi_score = sum(1 for indicator in idi_indicators if indicator in text_lower)
115
+
116
+ if fgi_score > idi_score:
117
+ return 'fgi'
118
+ elif idi_score > fgi_score:
119
+ return 'idi'
120
+ else:
121
+ return 'auto'
122
+
123
+ def _generate_outline(self, transcriptions: str, brief: str, interview_type: str) -> Dict:
124
+ """Generuj strukturę raportu"""
125
+ try:
126
+ prompt = REPORT_PROMPTS['outline_generator'].format(
127
+ transcriptions=transcriptions[:8000], # Limit dla API
128
+ brief=brief or "Brak szczegółowego briefu",
129
+ interview_type=INTERVIEW_TYPES.get(interview_type, 'wywiad')
130
+ )
131
+
132
+ response = self._call_gpt(prompt)
133
+ outline = self._parse_outline(response)
134
+
135
+ st.success(f"✅ Outline: {len(outline)} sekcji zaplanowanych")
136
+ return outline
137
+
138
+ except Exception as e:
139
+ st.error(f"❌ Błąd generowania outline: {e}")
140
+ return {}
141
+
142
+ def _generate_sections_iteratively(self, outline: Dict, transcriptions: str, brief: str, interview_type: str) -> Dict:
143
+ """Generuj sekcje raportu jedna po drugiej"""
144
+ sections = {}
145
+
146
+ for section_title, section_points in outline.items():
147
+ if not section_title or section_title.startswith('#'):
148
+ continue
149
+
150
+ st.info(f"📝 Generuję: {section_title}")
151
+
152
+ try:
153
+ prompt = REPORT_PROMPTS['section_generator'].format(
154
+ transcriptions=transcriptions,
155
+ brief=brief or "Brak szczegółowego briefu",
156
+ interview_type=INTERVIEW_TYPES.get(interview_type, 'wywiad'),
157
+ outline=str(outline),
158
+ section_title=section_title,
159
+ section_points=section_points
160
+ )
161
+
162
+ section_content = self._call_gpt(prompt)
163
+ sections[section_title] = section_content
164
+
165
+ self.generation_stats['sections_generated'] += 1
166
+ st.success(f"✅ {section_title} ({len(section_content.split())} słów)")
167
+
168
+ # Krótka przerwa żeby nie przekroczyć rate limits
169
+ time.sleep(2)
170
+
171
+ except Exception as e:
172
+ st.warning(f"⚠️ Błąd sekcji '{section_title}': {e}")
173
+ sections[section_title] = f"[BŁĄD GENEROWANIA SEKCJI: {e}]"
174
+
175
+ return sections
176
+
177
+ def _expand_short_sections(self, sections: Dict, transcriptions: str, brief: str) -> Dict:
178
+ """Self-prompting: rozszerz zbyt krótkie sekcje"""
179
+ expanded_sections = {}
180
+
181
+ for section_title, section_content in sections.items():
182
+ word_count = len(section_content.split())
183
+
184
+ # Sprawdź czy sekcja wymaga rozszerzenia
185
+ if word_count < 500: # Za krótka sekcja
186
+ st.info(f"🔍 Rozszerzam: {section_title} ({word_count} słów)")
187
+
188
+ try:
189
+ prompt = REPORT_PROMPTS['section_expander'].format(
190
+ current_section=section_content,
191
+ transcriptions=transcriptions,
192
+ brief=brief or "Brak szczegółowego briefu"
193
+ )
194
+
195
+ expanded_content = self._call_gpt(prompt)
196
+ expanded_sections[section_title] = expanded_content
197
+
198
+ new_word_count = len(expanded_content.split())
199
+ self.generation_stats['sections_expanded'] += 1
200
+
201
+ st.success(f"✅ Rozszerzone: {section_title} ({word_count} → {new_word_count} słów)")
202
+
203
+ time.sleep(2) # Rate limit protection
204
+
205
+ except Exception as e:
206
+ st.warning(f"⚠️ Nie udało się rozszerzyć '{section_title}': {e}")
207
+ expanded_sections[section_title] = section_content
208
+ else:
209
+ # Sekcja już wystarczająco długa
210
+ expanded_sections[section_title] = section_content
211
+ st.success(f"✅ {section_title} OK ({word_count} słów)")
212
+
213
+ return expanded_sections
214
+
215
+ def _assemble_final_report(self, sections: Dict, brief: str, interview_type: str, interviews_count: int) -> str:
216
+ """Scal wszystko w finalny raport"""
217
+ try:
218
+ sections_text = "\n\n".join([
219
+ f"## {title}\n\n{content}"
220
+ for title, content in sections.items()
221
+ ])
222
+
223
+ prompt = REPORT_PROMPTS['final_assembly'].format(
224
+ sections=sections_text,
225
+ brief=brief or "Brak szczegółowego briefu",
226
+ interview_type=INTERVIEW_TYPES.get(interview_type, 'wywiad'),
227
+ interviews_count=interviews_count,
228
+ date=datetime.now().strftime("%Y-%m-%d")
229
+ )
230
+
231
+ final_report = self._call_gpt(prompt, max_tokens=4000)
232
+
233
+ # Dodaj metadane na koniec
234
+ metadata = f"""
235
+
236
+ ---
237
+
238
+ ## METADATA RAPORTU
239
+ - **Wygenerowano**: {datetime.now().strftime("%Y-%m-%d %H:%M")}
240
+ - **Typ badania**: {INTERVIEW_TYPES.get(interview_type, 'nieznany')}
241
+ - **Liczba wywiadów**: {interviews_count}
242
+ - **Sekcji wygenerowanych**: {self.generation_stats['sections_generated']}
243
+ - **Sekcji rozszerzonych**: {self.generation_stats['sections_expanded']}
244
+ - **Czas generowania**: {self.generation_stats['generation_time']:.1f}s
245
+ - **Generator**: FGI/IDI Research Analyzer v1.0
246
+ """
247
+
248
+ return final_report + metadata
249
+
250
+ except Exception as e:
251
+ st.error(f"❌ Błąd finalnego scalenia: {e}")
252
+ # Fallback - zwróć przynajmniej sekcje
253
+ return self._create_fallback_report(sections, brief, interview_type)
254
+
255
+ def _call_gpt(self, prompt: str, max_tokens: int = 3000) -> str:
256
+ """Wywołanie GPT API z error handling"""
257
+ try:
258
+ response = self.client.chat.completions.create(
259
+ model=MODEL_SETTINGS['gpt']['model'],
260
+ messages=[
261
+ {"role": "system", "content": "Jesteś ekspertem analizy badań jakościowych. Tworzysz profesjonalne, szczegółowe raporty badawcze."},
262
+ {"role": "user", "content": prompt}
263
+ ],
264
+ temperature=MODEL_SETTINGS['gpt']['temperature'],
265
+ max_tokens=max_tokens
266
+ )
267
+
268
+ # Statystyki
269
+ if hasattr(response, 'usage'):
270
+ self.generation_stats['total_tokens_used'] += response.usage.total_tokens
271
+ # Estymacja kosztu GPT-4o-mini: ~$0.00015 per 1K tokens
272
+ self.generation_stats['total_cost_estimate'] += (response.usage.total_tokens / 1000) * 0.00015
273
+
274
+ return response.choices[0].message.content
275
+
276
+ except Exception as e:
277
+ if "rate limit" in str(e).lower():
278
+ st.warning("⏳ Rate limit - czekam 60s...")
279
+ time.sleep(60)
280
+ return self._call_gpt(prompt, max_tokens)
281
+ else:
282
+ raise e
283
+
284
+ def _parse_outline(self, outline_text: str) -> Dict:
285
+ """Parsuj outline z odpowiedzi GPT"""
286
+ outline = {}
287
+ current_section = None
288
+
289
+ for line in outline_text.split('\n'):
290
+ line = line.strip()
291
+
292
+ if line.startswith('## '):
293
+ # Nowa sekcja
294
+ current_section = line[3:].strip()
295
+ outline[current_section] = []
296
+ elif line.startswith('- ') and current_section:
297
+ # Podpunkt sekcji
298
+ outline[current_section].append(line[2:].strip())
299
+
300
+ return outline
301
+
302
+ def _create_fallback_report(self, sections: Dict, brief: str, interview_type: str) -> str:
303
+ """Fallback raport jeśli final assembly nie zadziała"""
304
+ report_parts = [
305
+ f"# RAPORT Z BADANIA {INTERVIEW_TYPES.get(interview_type, 'INTERVIEW').upper()}",
306
+ f"\n**Data**: {datetime.now().strftime('%Y-%m-%d')}",
307
+ f"**Brief**: {brief or 'Brak szczegółowego briefu'}",
308
+ "\n---\n"
309
+ ]
310
+
311
+ for title, content in sections.items():
312
+ report_parts.append(f"## {title}\n\n{content}\n\n")
313
+
314
+ return "\n".join(report_parts)
315
+
316
+ def _log_generation_stats(self):
317
+ """Wyświetl statystyki generowania"""
318
+ stats = self.generation_stats
319
+
320
+ st.info(f"""
321
+ 📊 **Statystyki generowania:**
322
+ - Sekcji: {stats['sections_generated']} wygenerowanych, {stats['sections_expanded']} rozszerzonych
323
+ - Tokeny: ~{stats['total_tokens_used']:,}
324
+ - Koszt: ~${stats['total_cost_estimate']:.4f}
325
+ - Czas: {stats['generation_time']:.1f}s
326
+ """)
327
+
328
+ def evaluate_section_quality(self, section_content: str) -> Dict:
329
+ """Oceń jakość sekcji (dla debugowania)"""
330
+ try:
331
+ prompt = REPORT_PROMPTS['quality_checker'].format(section=section_content)
332
+ evaluation = self._call_gpt(prompt, max_tokens=500)
333
+
334
+ # Parsuj ocenę (uproszczone)
335
+ lines = evaluation.split('\n')
336
+ scores = {}
337
+
338
+ for line in lines:
339
+ if ':' in line and '/10' in line:
340
+ criterion = line.split(':')[0].strip()
341
+ score = line.split(':')[1].strip().split('/')[0]
342
+ try:
343
+ scores[criterion] = int(score)
344
+ except:
345
+ pass
346
+
347
+ needs_improvement = 'TAK' in evaluation.upper()
348
+
349
+ return {
350
+ 'scores': scores,
351
+ 'needs_improvement': needs_improvement,
352
+ 'evaluation_text': evaluation
353
+ }
354
+
355
+ except Exception as e:
356
+ return {'error': str(e)}
357
+
358
+ def get_generation_stats(self) -> Dict:
359
+ """Zwróć statystyki generowania"""
360
+ return self.generation_stats.copy()
361
+
362
+ # Funkcje pomocnicze
363
+ def estimate_report_length(transcriptions: Dict[str, str]) -> Dict:
364
+ """Estymuj długość finalnego raportu"""
365
+ total_words = sum(len(text.split()) for text in transcriptions.values())
366
+
367
+ # Raporty są zwykle 15-25% długości transkrypcji
368
+ estimated_report_words = int(total_words * 0.2)
369
+ estimated_pages = estimated_report_words / 250 # ~250 słów na stronę
370
+
371
+ return {
372
+ 'transcription_words': total_words,
373
+ 'estimated_report_words': estimated_report_words,
374
+ 'estimated_pages': estimated_pages,
375
+ 'estimated_generation_time': len(transcriptions) * 120 # ~2 min per interview
376
+ }
377
+
378
+ # Test modułu
379
+ if __name__ == "__main__":
380
+ print("🧪 Test ReportGenerator")
381
+
382
+ # Test bez prawdziwego API
383
+ try:
384
+ generator = ReportGenerator("test-key")
385
+ print("✅ ReportGenerator zainicjalizowany")
386
+
387
+ # Test estymacji
388
+ test_transcriptions = {
389
+ "test1.mp3": "To jest przykładowa transkrypcja wywiadu. " * 100,
390
+ "test2.mp3": "To jest druga transkrypcja z badania. " * 150
391
+ }
392
+
393
+ estimates = estimate_report_length(test_transcriptions)
394
+ print(f"📊 Estymacja: {estimates['estimated_report_words']} słów, {estimates['estimated_pages']:.1f} stron")
395
+
396
+ except Exception as e:
397
+ print(f"❌ Błąd testu: {e}")
398
+
399
+ print("✅ Test zakończony")
transcription.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # transcription.py - Moduł transkrypcji audio używając OpenAI Whisper
2
+
3
+ import os
4
+ import time
5
+ import streamlit as st
6
+ from typing import List, Dict, Optional
7
+ from pathlib import Path
8
+
9
+ try:
10
+ from openai import OpenAI
11
+ OPENAI_AVAILABLE = True
12
+ except ImportError:
13
+ OPENAI_AVAILABLE = False
14
+ st.error("❌ OpenAI library nie jest dostępna. Zainstaluj: pip install openai")
15
+
16
+ from config import MODEL_SETTINGS, USER_MESSAGES
17
+
18
+ class AudioTranscriber:
19
+ """Klasa do transkrypcji audio używając OpenAI Whisper API"""
20
+
21
+ def __init__(self, api_key: str):
22
+ if not OPENAI_AVAILABLE:
23
+ raise Exception("OpenAI library nie jest dostępna")
24
+
25
+ self.client = OpenAI(api_key=api_key)
26
+ self.api_key = api_key
27
+ self.transcription_stats = {
28
+ 'total_files': 0,
29
+ 'successful': 0,
30
+ 'failed': 0,
31
+ 'total_duration': 0,
32
+ 'total_cost_estimate': 0
33
+ }
34
+
35
+ def transcribe_files(self, file_paths: List[str], language: str = "pl") -> str:
36
+ """
37
+ Transkrypcja listy plików audio
38
+ Returns: Połączona transkrypcja wszystkich plików
39
+ """
40
+ transcriptions = []
41
+
42
+ for i, file_path in enumerate(file_paths):
43
+ if not os.path.exists(file_path):
44
+ st.error(f"❌ Plik nie istnieje: {file_path}")
45
+ continue
46
+
47
+ try:
48
+ # Pokaż postęp
49
+ if len(file_paths) > 1:
50
+ st.info(f"🎙️ Transkrybuję część {i+1}/{len(file_paths)}")
51
+
52
+ # Transkrypcja pojedynczego pliku
53
+ transcription = self._transcribe_single_file(file_path, language)
54
+
55
+ if transcription:
56
+ transcriptions.append(transcription)
57
+ self.transcription_stats['successful'] += 1
58
+ st.success(f"✅ Część {i+1} zakończona")
59
+ else:
60
+ self.transcription_stats['failed'] += 1
61
+ st.error(f"❌ Błąd części {i+1}")
62
+
63
+ except Exception as e:
64
+ st.error(f"❌ Błąd transkrypcji części {i+1}: {str(e)}")
65
+ self.transcription_stats['failed'] += 1
66
+
67
+ # Połącz wszystkie transkrypcje
68
+ if transcriptions:
69
+ # Jeśli było więcej niż jeden plik, dodaj separatory
70
+ if len(transcriptions) > 1:
71
+ final_transcription = "\n\n=== CZĘŚĆ 1 ===\n\n".join([
72
+ transcriptions[0]
73
+ ] + [
74
+ f"=== CZĘŚĆ {i+1} ===\n\n{text}"
75
+ for i, text in enumerate(transcriptions[1:], 1)
76
+ ])
77
+ else:
78
+ final_transcription = transcriptions[0]
79
+
80
+ return final_transcription
81
+ else:
82
+ raise Exception("Wszystkie transkrypcje zakończone błędem")
83
+
84
+ def _transcribe_single_file(self, file_path: str, language: str = "pl") -> Optional[str]:
85
+ """Transkrypcja pojedynczego pliku"""
86
+ try:
87
+ self.transcription_stats['total_files'] += 1
88
+
89
+ # Sprawdź rozmiar pliku
90
+ file_size = os.path.getsize(file_path)
91
+ file_size_mb = file_size / (1024 * 1024)
92
+
93
+ # OpenAI Whisper ma limit 25MB
94
+ if file_size_mb > 25:
95
+ raise Exception(f"Plik za duży dla Whisper API: {file_size_mb:.1f}MB > 25MB")
96
+
97
+ st.info(f"📤 Wysyłam do Whisper ({file_size_mb:.1f}MB)...")
98
+
99
+ # Otwórz plik i wyślij do API
100
+ with open(file_path, 'rb') as audio_file:
101
+ transcript = self.client.audio.transcriptions.create(
102
+ model=MODEL_SETTINGS['whisper']['model'],
103
+ file=audio_file,
104
+ language=language if language != 'auto' else None,
105
+ temperature=MODEL_SETTINGS['whisper']['temperature']
106
+ )
107
+
108
+ # Estymacja kosztu (Whisper API: $0.006 per minute)
109
+ estimated_duration = file_size_mb * 60 # Rough estimate: 1MB ≈ 1 minute
110
+ estimated_cost = (estimated_duration / 60) * 0.006
111
+ self.transcription_stats['total_duration'] += estimated_duration
112
+ self.transcription_stats['total_cost_estimate'] += estimated_cost
113
+
114
+ st.success(f"✅ Transkrypcja otrzymana (~{estimated_duration:.1f}s audio)")
115
+
116
+ return transcript.text
117
+
118
+ except Exception as e:
119
+ st.error(f"❌ Błąd Whisper API: {str(e)}")
120
+
121
+ # Jeśli błąd rate limit, poczekaj i spróbuj ponownie
122
+ if "rate limit" in str(e).lower():
123
+ st.warning("⏳ Rate limit - czekam 60s i próbuję ponownie...")
124
+ time.sleep(60)
125
+ return self._transcribe_single_file(file_path, language)
126
+
127
+ return None
128
+
129
+ def transcribe_with_retries(self, file_path: str, language: str = "pl", max_retries: int = 3) -> Optional[str]:
130
+ """Transkrypcja z ponawianiem przy błędach"""
131
+ for attempt in range(max_retries):
132
+ try:
133
+ result = self._transcribe_single_file(file_path, language)
134
+ if result:
135
+ return result
136
+
137
+ except Exception as e:
138
+ st.warning(f"⚠️ Próba {attempt + 1}/{max_retries} nieudana: {str(e)}")
139
+
140
+ if attempt < max_retries - 1:
141
+ wait_time = (attempt + 1) * 30 # Exponential backoff
142
+ st.info(f"⏳ Czekam {wait_time}s przed następną próbą...")
143
+ time.sleep(wait_time)
144
+ else:
145
+ st.error(f"❌ Wszystkie {max_retries} prób nieudane")
146
+
147
+ return None
148
+
149
+ def estimate_transcription_time(self, file_paths: List[str]) -> Dict:
150
+ """Estymuj czas i koszt transkrypcji"""
151
+ total_size = sum(os.path.getsize(path) for path in file_paths if os.path.exists(path))
152
+ total_size_mb = total_size / (1024 * 1024)
153
+
154
+ # Estymacje
155
+ estimated_duration_minutes = total_size_mb # 1MB ≈ 1 minute
156
+ estimated_api_time = estimated_duration_minutes * 0.1 # Whisper jest ~10x szybszy niż realtime
157
+ estimated_cost = estimated_duration_minutes * 0.006 # $0.006 per minute
158
+
159
+ return {
160
+ 'total_size_mb': total_size_mb,
161
+ 'estimated_audio_duration': estimated_duration_minutes,
162
+ 'estimated_processing_time': estimated_api_time,
163
+ 'estimated_cost_usd': estimated_cost,
164
+ 'files_count': len(file_paths)
165
+ }
166
+
167
+ def validate_api_key(self) -> bool:
168
+ """Sprawdź czy klucz API działa"""
169
+ try:
170
+ # Spróbuj pobrać listę modeli
171
+ models = self.client.models.list()
172
+ return True
173
+ except Exception as e:
174
+ st.error(f"❌ Nieprawidłowy klucz API: {str(e)}")
175
+ return False
176
+
177
+ def get_transcription_stats(self) -> Dict:
178
+ """Zwróć statystyki transkrypcji"""
179
+ return self.transcription_stats.copy()
180
+
181
+ def detect_interview_type(self, transcription: str) -> str:
182
+ """
183
+ Automatyczne rozpoznanie typu wywiadu na podstawie treści
184
+ Returns: 'fgi', 'idi', lub 'unknown'
185
+ """
186
+ text_lower = transcription.lower()
187
+
188
+ # Wskaźniki FGI (Focus Group)
189
+ fgi_indicators = [
190
+ 'moderator', 'grupa', 'wszyscy', 'kto jeszcze', 'a państwo',
191
+ 'czy zgadzacie się', 'co myślicie', 'focus group',
192
+ 'uczestnicy', 'grupa fokusowa', 'dyskusja grupowa'
193
+ ]
194
+
195
+ # Wskaźniki IDI (Individual)
196
+ idi_indicators = [
197
+ 'wywiad indywidualny', 'jeden na jeden', 'prywatnie',
198
+ 'osobiście', 'indywidualne', 'w cztery oczy'
199
+ ]
200
+
201
+ fgi_score = sum(1 for indicator in fgi_indicators if indicator in text_lower)
202
+ idi_score = sum(1 for indicator in idi_indicators if indicator in text_lower)
203
+
204
+ # Sprawdź także liczbę różnych głosów/osób
205
+ # (FGI zwykle ma więcej przerywników, overlapping speech)
206
+ interruption_patterns = ['...', '[', ']', '(', ')', '--']
207
+ interruption_count = sum(text_lower.count(pattern) for pattern in interruption_patterns)
208
+
209
+ if fgi_score > idi_score and interruption_count > 10:
210
+ return 'fgi'
211
+ elif idi_score > fgi_score:
212
+ return 'idi'
213
+ elif interruption_count > 20: # Dużo przerywników = prawdopodobnie grupa
214
+ return 'fgi'
215
+ else:
216
+ return 'unknown'
217
+
218
+ def clean_transcription(self, transcription: str) -> str:
219
+ """Oczyszczenie i formatowanie transkrypcji"""
220
+ try:
221
+ # Usuń nadmiarowe spacje
222
+ lines = transcription.split('\n')
223
+ cleaned_lines = []
224
+
225
+ for line in lines:
226
+ line = line.strip()
227
+ if line: # Pomijaj puste linie
228
+ # Usuń nadmiarowe spacje
229
+ line = ' '.join(line.split())
230
+ cleaned_lines.append(line)
231
+
232
+ # Połącz z pojedynczymi przerwami linii
233
+ cleaned = '\n\n'.join(cleaned_lines)
234
+
235
+ # Dodaj informacje metadata na początek
236
+ metadata = f"""TRANSKRYPCJA AUDIO
237
+ Data: {time.strftime('%Y-%m-%d %H:%M')}
238
+ Typ: {self.detect_interview_type(cleaned).upper()}
239
+ Długość: ~{len(cleaned.split())} słów
240
+
241
+ ---
242
+
243
+ """
244
+
245
+ return metadata + cleaned
246
+
247
+ except Exception as e:
248
+ st.warning(f"⚠️ Błąd czyszczenia transkrypcji: {e}")
249
+ return transcription
250
+
251
+ # Funkcje pomocnicze dla kompatybilności
252
+ def validate_audio_file(file_path: str) -> bool:
253
+ """Sprawdź czy plik audio jest prawidłowy"""
254
+ if not os.path.exists(file_path):
255
+ return False
256
+
257
+ # Sprawdź rozmiar
258
+ file_size = os.path.getsize(file_path)
259
+ if file_size == 0:
260
+ return False
261
+
262
+ # Sprawdź rozszerzenie
263
+ valid_extensions = ['.mp3', '.wav', '.mp4', '.m4a', '.aac']
264
+ file_ext = Path(file_path).suffix.lower()
265
+
266
+ return file_ext in valid_extensions
267
+
268
+ # Test modułu
269
+ if __name__ == "__main__":
270
+ print("🧪 Test AudioTranscriber")
271
+
272
+ # Test bez prawdziwego API key
273
+ try:
274
+ transcriber = AudioTranscriber("test-key")
275
+ print("✅ AudioTranscriber zainicjalizowany")
276
+
277
+ # Test rozpoznania typu wywiadu
278
+ test_fgi = "Moderator: Co wszyscy myślicie o produkcie? Czy zgadzacie się z tym?"
279
+ test_idi = "Interviewer: A teraz opowiedz mi o swoich doświadczeniach..."
280
+
281
+ print(f"Test FGI: {transcriber.detect_interview_type(test_fgi)}")
282
+ print(f"Test IDI: {transcriber.detect_interview_type(test_idi)}")
283
+
284
+ except Exception as e:
285
+ print(f"❌ Błąd testu: {e}")
286
+
287
+ print("✅ Test zakończony")