Marek4321 commited on
Commit
038b4c5
·
verified ·
1 Parent(s): 97cca2b

Update file_handler.py

Browse files
Files changed (1) hide show
  1. file_handler.py +296 -82
file_handler.py CHANGED
@@ -1,4 +1,4 @@
1
- # file_handler.py - Obsługa plików audio/video dla HuggingFace
2
 
3
  import os
4
  import tempfile
@@ -12,7 +12,7 @@ try:
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
@@ -24,86 +24,124 @@ except ImportError:
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 = 15, 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
 
@@ -111,12 +149,15 @@ class FileHandler:
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 = 5 * 1000
120
 
121
  st.info(f"📂 Dzielę na {estimated_parts} części (~{chunk_duration_ms//60000:.1f} min każda)")
122
 
@@ -124,22 +165,41 @@ class FileHandler:
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:
@@ -149,13 +209,16 @@ class FileHandler:
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
@@ -164,73 +227,103 @@ class FileHandler:
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
 
@@ -238,40 +331,161 @@ class FileHandler:
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")
 
1
+ # file_handler.py - Poprawiony handler plików dla Whisper API
2
 
3
  import os
4
  import tempfile
 
12
  PYDUB_AVAILABLE = True
13
  except ImportError:
14
  PYDUB_AVAILABLE = False
15
+ st.warning("⚠️ Pydub nie jest dostępny. Funkcje kompresji ograniczone.")
16
 
17
  try:
18
  import librosa
 
24
  from config import FILE_PROCESSING, USER_MESSAGES
25
 
26
  class FileHandler:
27
+ """Klasa do obsługi plików audio/video - zoptymalizowana dla Whisper API (max 25MB)"""
28
 
29
  def __init__(self):
30
+ self.temp_files = []
31
  self.processing_stats = {}
32
 
33
+ # Whisper API limits
34
+ self.WHISPER_MAX_SIZE_MB = 25
35
+ self.SAFE_CHUNK_SIZE_MB = 20 # Bezpieczny rozmiar chunka
36
+
37
+ def process_file(self, uploaded_file, max_chunk_size_mb: int = 20, auto_compress: bool = True) -> List[str]:
38
  """
39
+ Główna funkcja przetwarzania pliku dla Whisper API
40
+ Returns: Lista ścieżek do plików gotowych do transkrypcji (każdy <25MB)
41
  """
42
  try:
43
  file_size_mb = uploaded_file.size / (1024 * 1024)
44
 
 
45
  st.info(f"🔄 Przetwarzam {uploaded_file.name} ({file_size_mb:.1f}MB)")
46
 
47
+ # Sprawdź czy plik mieści się w limicie Whisper
48
+ if file_size_mb <= self.WHISPER_MAX_SIZE_MB:
49
+ # Plik OK - zapisz bezpośrednio
50
+ temp_path = self._save_temp_file(uploaded_file)
51
+ if temp_path:
52
+ st.success(f"✅ Plik gotowy do transkrypcji ({file_size_mb:.1f}MB)")
53
+ return [temp_path]
54
+ else:
55
+ return []
56
+
57
+ # Plik za duży - wymaga przetwarzania
58
+ if file_size_mb > 100:
59
+ st.error(f"❌ Plik zbyt duży ({file_size_mb:.1f}MB). Maksymalnie 100MB.")
60
+ return []
61
+
62
+ # Strategia 1: Kompresja
63
+ if auto_compress and file_size_mb > self.WHISPER_MAX_SIZE_MB:
64
+ compressed_file = self._compress_audio_for_whisper(uploaded_file)
65
  if compressed_file:
66
+ compressed_size_mb = len(compressed_file.getvalue()) / (1024 * 1024)
67
+ if compressed_size_mb <= self.WHISPER_MAX_SIZE_MB:
68
+ temp_path = self._save_bytesio_to_temp(compressed_file, uploaded_file.name)
69
+ if temp_path:
70
+ st.success(f"✅ Skompresowano: {file_size_mb:.1f}MB → {compressed_size_mb:.1f}MB")
71
+ return [temp_path]
72
 
73
+ # Strategia 2: Podział na części
74
+ return self._split_audio_for_whisper(uploaded_file, max_chunk_size_mb)
 
 
 
 
 
75
 
76
  except Exception as e:
77
  st.error(f"❌ Błąd przetwarzania {uploaded_file.name}: {str(e)}")
78
  return []
79
 
80
+ def _compress_audio_for_whisper(self, uploaded_file) -> Union[BytesIO, None]:
81
+ """Agresywna kompresja audio dla Whisper API"""
82
  if not PYDUB_AVAILABLE:
83
  st.warning("Pydub niedostępny - pomijam kompresję")
84
  return None
85
 
86
  try:
87
+ st.info("🗜️ Kompresuję audio...")
88
+
89
  # Załaduj audio
90
  audio_data = uploaded_file.read()
91
+ uploaded_file.seek(0) # Reset dla dalszego użycia
92
+
93
  audio = AudioSegment.from_file(BytesIO(audio_data))
94
 
95
+ # Agresywna kompresja dla Whisper (jakość mowy)
96
  compressed = audio.set_channels(1) # Mono
97
  compressed = compressed.set_frame_rate(16000) # 16kHz (wystarczy dla mowy)
98
 
99
+ # Jeszcze więcej kompresji jeśli potrzeba
100
+ original_size_mb = uploaded_file.size / (1024 * 1024)
101
+
102
+ if original_size_mb > 50:
103
+ # Bardzo duży plik - maksymalna kompresja
104
+ bitrate = "32k"
105
+ elif original_size_mb > 35:
106
+ # Duży plik - silna kompresja
107
+ bitrate = "48k"
108
+ else:
109
+ # Średni plik - umiarkowana kompresja
110
+ bitrate = "64k"
111
+
112
  # Export do BytesIO
113
  output = BytesIO()
114
  compressed.export(
115
  output,
116
  format="mp3",
117
+ bitrate=bitrate,
118
+ parameters=["-ac", "1", "-ar", "16000"]
119
  )
120
  output.seek(0)
121
 
122
+ # Sprawdź rozmiar wyniku
123
+ compressed_size_mb = len(output.getvalue()) / (1024 * 1024)
 
124
 
125
+ if compressed_size_mb <= self.WHISPER_MAX_SIZE_MB:
126
+ return output
127
+ else:
128
+ st.warning(f"⚠️ Kompresja niewystarczająca ({compressed_size_mb:.1f}MB). Przechodzę do dzielenia.")
129
+ return None
130
 
131
  except Exception as e:
132
  st.warning(f"Kompresja nieudana: {str(e)}")
133
  return None
134
 
135
+ def _split_audio_for_whisper(self, uploaded_file, max_chunk_size_mb: int) -> List[str]:
136
+ """Dzieli plik audio na części <25MB dla Whisper"""
137
  try:
138
  if not PYDUB_AVAILABLE:
139
  st.error("❌ Pydub wymagany do dzielenia plików. Zainstaluj: pip install pydub")
140
  return []
141
 
142
+ st.info("✂️ Dzielę plik na części...")
143
+
144
+ # Załaduj audio
145
  audio_data = uploaded_file.read()
146
  audio = AudioSegment.from_file(BytesIO(audio_data))
147
 
 
149
  total_duration_ms = len(audio)
150
  file_size_mb = uploaded_file.size / (1024 * 1024)
151
 
152
+ # Bezpieczny rozmiar chunka (mniejszy niż limit Whisper)
153
+ safe_chunk_size_mb = min(max_chunk_size_mb, self.SAFE_CHUNK_SIZE_MB)
154
+
155
+ # Estymacja liczby części
156
+ estimated_parts = math.ceil(file_size_mb / safe_chunk_size_mb)
157
  chunk_duration_ms = total_duration_ms // estimated_parts
158
 
159
+ # Overlap między częściami (10 sekund)
160
+ overlap_ms = 10 * 1000
161
 
162
  st.info(f"📂 Dzielę na {estimated_parts} części (~{chunk_duration_ms//60000:.1f} min każda)")
163
 
 
165
  base_name = os.path.splitext(uploaded_file.name)[0]
166
 
167
  for i in range(estimated_parts):
168
+ start_ms = max(0, i * chunk_duration_ms - (overlap_ms if i > 0 else 0))
169
  end_ms = min(total_duration_ms, (i + 1) * chunk_duration_ms + overlap_ms)
170
 
171
  # Wytnij część
172
  chunk = audio[start_ms:end_ms]
173
 
174
+ # Lekka kompresja części
175
+ chunk = chunk.set_channels(1) # Mono
176
+ chunk = chunk.set_frame_rate(22050) # Dobra jakość ale kompaktowa
177
+
178
  # Zapisz do pliku tymczasowego
179
+ temp_fd, temp_path = tempfile.mkstemp(
180
+ suffix=f"_part{i+1:02d}.mp3",
181
+ prefix=f"{base_name}_"
182
+ )
183
  os.close(temp_fd)
184
 
185
+ chunk.export(temp_path, format="mp3", bitrate="96k")
186
+
187
+ # Sprawdź rozmiar części
188
+ part_size_mb = os.path.getsize(temp_path) / (1024 * 1024)
189
+
190
+ if part_size_mb > self.WHISPER_MAX_SIZE_MB:
191
+ st.error(f"❌ Część {i+1} nadal za duża ({part_size_mb:.1f}MB)")
192
+ os.remove(temp_path)
193
+ continue
194
+
195
  parts.append(temp_path)
196
  self.temp_files.append(temp_path)
197
 
198
+ st.success(f"✅ Część {i+1}/{estimated_parts}: {part_size_mb:.1f}MB, {(end_ms-start_ms)//60000:.1f} min")
199
 
200
+ if not parts:
201
+ st.error("❌ Nie udało się utworzyć żadnej prawidłowej części")
202
+
203
  return parts
204
 
205
  except Exception as e:
 
209
  def _save_temp_file(self, uploaded_file) -> str:
210
  """Zapisuje uploaded file do pliku tymczasowego"""
211
  try:
 
212
  suffix = f".{uploaded_file.name.split('.')[-1]}"
213
  temp_fd, temp_path = tempfile.mkstemp(suffix=suffix)
214
 
215
  # Zapisz dane
216
  with os.fdopen(temp_fd, 'wb') as tmp_file:
217
+ content = uploaded_file.read()
218
+ tmp_file.write(content)
219
+
220
+ # Reset pozycji dla dalszego użycia
221
+ uploaded_file.seek(0)
222
 
223
  self.temp_files.append(temp_path)
224
  return temp_path
 
227
  st.error(f"❌ Błąd zapisu tymczasowego: {str(e)}")
228
  return ""
229
 
230
+ def _save_bytesio_to_temp(self, bytes_io: BytesIO, original_name: str) -> str:
231
+ """Zapisz BytesIO do pliku tymczasowego"""
232
  try:
233
+ suffix = f"_compressed.mp3"
234
+ base_name = os.path.splitext(original_name)[0]
235
+
236
+ temp_fd, temp_path = tempfile.mkstemp(
237
+ suffix=suffix,
238
+ prefix=f"{base_name}_"
239
+ )
240
+
241
+ with os.fdopen(temp_fd, 'wb') as tmp_file:
242
+ tmp_file.write(bytes_io.getvalue())
243
+
244
+ self.temp_files.append(temp_path)
245
+ return temp_path
246
+
247
+ except Exception as e:
248
+ st.error(f"❌ Błąd zapisu skompresowanego: {str(e)}")
249
+ return ""
250
 
251
+ def validate_file_for_whisper(self, uploaded_file) -> Tuple[bool, str]:
252
+ """Walidacja pliku dla Whisper API"""
253
  try:
254
  # Sprawdź rozmiar
255
  file_size_mb = uploaded_file.size / (1024 * 1024)
256
+
257
+ if file_size_mb == 0:
258
+ return False, "Plik jest pusty"
259
+
260
+ if file_size_mb > 100: # Rozumny limit dla przetwarzania
261
+ return False, f"Plik za duży: {file_size_mb:.1f}MB > 100MB"
262
 
263
  # Sprawdź rozszerzenie
264
  file_ext = uploaded_file.name.split('.')[-1].lower()
265
+ supported_formats = ['mp3', 'wav', 'mp4', 'm4a', 'aac', 'mov', 'avi']
 
 
 
266
 
267
  if file_ext not in supported_formats:
268
  return False, f"Nieobsługiwany format: .{file_ext}"
269
 
270
+ # Ostrzeżenie dla dużych plików
271
+ if file_size_mb > self.WHISPER_MAX_SIZE_MB:
272
+ return True, f"Plik wymaga przetwarzania ({file_size_mb:.1f}MB > {self.WHISPER_MAX_SIZE_MB}MB)"
273
 
274
  return True, "OK"
275
 
276
  except Exception as e:
277
  return False, f"Błąd walidacji: {str(e)}"
278
 
279
+ def get_audio_duration(self, file_path: str) -> float:
280
+ """Pobierz długość pliku audio w sekundach"""
281
+ try:
282
+ if LIBROSA_AVAILABLE:
283
+ duration = librosa.get_duration(filename=file_path)
284
+ return duration
285
+ elif PYDUB_AVAILABLE:
286
+ audio = AudioSegment.from_file(file_path)
287
+ return len(audio) / 1000.0
288
+ else:
289
+ # Fallback - estymacja na podstawie rozmiaru
290
+ file_size = os.path.getsize(file_path)
291
+ return file_size / (1024 * 1024) * 60
292
+ except:
293
+ file_size = os.path.getsize(file_path)
294
+ return file_size / (1024 * 1024) * 60
295
+
296
  def estimate_processing_time(self, uploaded_files: List) -> Dict:
297
  """Estymuj czas przetwarzania"""
298
  total_size_mb = sum(f.size for f in uploaded_files) / (1024 * 1024)
299
+ total_duration_est = total_size_mb * 60
300
+
301
+ # Czas przetwarzania (kompresja/dzielenie)
302
+ processing_time = 0
303
+ for f in uploaded_files:
304
+ file_size_mb = f.size / (1024 * 1024)
305
+ if file_size_mb > self.WHISPER_MAX_SIZE_MB:
306
+ processing_time += file_size_mb * 2 # ~2s per MB for processing
307
 
308
  # Estymacja czasu transkrypcji (Whisper ~1:10 ratio)
309
+ transcription_time = total_duration_est * 0.1
310
 
311
+ # Estymacja czasu generowania raportu
312
+ report_time = len(uploaded_files) * 30
313
 
314
  return {
315
  'total_size_mb': total_size_mb,
316
  'estimated_audio_duration': total_duration_est,
317
+ 'estimated_processing_time': processing_time,
318
  'estimated_transcription_time': transcription_time,
319
  'estimated_report_time': report_time,
320
+ 'total_estimated_time': processing_time + transcription_time + report_time,
321
+ 'files_needing_processing': sum(1 for f in uploaded_files
322
+ if f.size / (1024 * 1024) > self.WHISPER_MAX_SIZE_MB)
323
  }
324
 
325
  def get_file_info(self, uploaded_file) -> Dict:
326
+ """Pobierz szczegółowe informacje o pliku"""
327
  file_size_mb = uploaded_file.size / (1024 * 1024)
328
  file_ext = uploaded_file.name.split('.')[-1].lower()
329
 
 
331
  'name': uploaded_file.name,
332
  'size_mb': file_size_mb,
333
  'format': file_ext,
334
+ 'whisper_ready': file_size_mb <= self.WHISPER_MAX_SIZE_MB,
335
+ 'needs_compression': file_size_mb > self.WHISPER_MAX_SIZE_MB and file_size_mb <= 50,
336
+ 'needs_splitting': file_size_mb > 50,
337
+ 'too_large': file_size_mb > 100,
338
+ 'estimated_duration': file_size_mb * 60,
339
+ 'estimated_processing_time': max(0, file_size_mb - self.WHISPER_MAX_SIZE_MB) * 2
340
  }
341
 
342
  def cleanup_temp_files(self):
343
  """Wyczyść pliki tymczasowe"""
344
  cleaned = 0
345
+ errors = 0
346
+
347
  for temp_file in self.temp_files:
348
  try:
349
  if os.path.exists(temp_file):
350
  os.remove(temp_file)
351
  cleaned += 1
352
  except Exception as e:
353
+ errors += 1
354
  st.warning(f"Nie można usunąć {temp_file}: {e}")
355
 
356
  self.temp_files = []
357
+
358
  if cleaned > 0:
359
  st.success(f"🧹 Wyczyszczono {cleaned} plików tymczasowych")
360
+
361
+ if errors > 0:
362
+ st.warning(f"⚠️ {errors} plików nie udało się usunąć")
363
 
364
  def get_processing_stats(self) -> Dict:
365
  """Zwróć statystyki przetwarzania"""
366
  return {
367
  'temp_files_count': len(self.temp_files),
368
+ 'whisper_max_size_mb': self.WHISPER_MAX_SIZE_MB,
369
+ 'safe_chunk_size_mb': self.SAFE_CHUNK_SIZE_MB,
370
  'processing_stats': self.processing_stats,
371
  'libraries_available': {
372
  'pydub': PYDUB_AVAILABLE,
373
  'librosa': LIBROSA_AVAILABLE
374
  }
375
  }
376
+
377
+ def analyze_upload_batch(self, uploaded_files: List) -> Dict:
378
+ """Analizuj całą paczkę plików"""
379
+ analysis = {
380
+ 'total_files': len(uploaded_files),
381
+ 'total_size_mb': 0,
382
+ 'whisper_ready': 0,
383
+ 'need_compression': 0,
384
+ 'need_splitting': 0,
385
+ 'too_large': 0,
386
+ 'estimated_parts': 0,
387
+ 'file_details': []
388
+ }
389
+
390
+ for file in uploaded_files:
391
+ info = self.get_file_info(file)
392
+ analysis['file_details'].append(info)
393
+ analysis['total_size_mb'] += info['size_mb']
394
+
395
+ if info['whisper_ready']:
396
+ analysis['whisper_ready'] += 1
397
+ elif info['needs_compression']:
398
+ analysis['need_compression'] += 1
399
+ elif info['needs_splitting']:
400
+ analysis['need_splitting'] += 1
401
+ # Estymacja liczby części
402
+ parts = math.ceil(info['size_mb'] / self.SAFE_CHUNK_SIZE_MB)
403
+ analysis['estimated_parts'] += parts
404
+ elif info['too_large']:
405
+ analysis['too_large'] += 1
406
+
407
+ return analysis
408
+
409
+ def create_processing_plan(self, uploaded_files: List) -> str:
410
+ """Stwórz plan przetwarzania dla użytkownika"""
411
+ analysis = self.analyze_upload_batch(uploaded_files)
412
+
413
+ plan = f"""
414
+ 📋 **PLAN PRZETWARZANIA**
415
+
416
+ 📊 **Podsumowanie:**
417
+ - Plików: {analysis['total_files']} ({analysis['total_size_mb']:.1f}MB)
418
+ - Gotowych do transkrypcji: {analysis['whisper_ready']}
419
+ - Wymagających kompresji: {analysis['need_compression']}
420
+ - Wymagających dzielenia: {analysis['need_splitting']}
421
+ - Za dużych: {analysis['too_large']}
422
 
423
+ """
424
+
425
+ if analysis['estimated_parts'] > 0:
426
+ plan += f"- Szacowana liczba części: {analysis['estimated_parts']}\n"
427
+
428
+ if analysis['too_large'] > 0:
429
+ plan += f"\n❌ **PLIKI ZA DUŻE (>100MB):**\n"
430
+ for info in analysis['file_details']:
431
+ if info['too_large']:
432
+ plan += f"- {info['name']}: {info['size_mb']:.1f}MB\n"
433
+
434
+ if analysis['need_splitting'] > 0:
435
+ plan += f"\n✂️ **PLIKI DO PODZIELENIA:**\n"
436
+ for info in analysis['file_details']:
437
+ if info['needs_splitting']:
438
+ parts = math.ceil(info['size_mb'] / self.SAFE_CHUNK_SIZE_MB)
439
+ plan += f"- {info['name']}: {info['size_mb']:.1f}MB → ~{parts} części\n"
440
+
441
+ if analysis['need_compression'] > 0:
442
+ plan += f"\n🗜️ **PLIKI DO KOMPRESJI:**\n"
443
+ for info in analysis['file_details']:
444
+ if info['needs_compression']:
445
+ plan += f"- {info['name']}: {info['size_mb']:.1f}MB\n"
446
+
447
+ # Estymacja czasów
448
+ times = self.estimate_processing_time(uploaded_files)
449
+ plan += f"""
450
+ ⏱️ **ESTYMACJA CZASÓW:**
451
+ - Przetwarzanie plików: ~{times['estimated_processing_time']:.1f}s
452
+ - Transkrypcja: ~{times['estimated_transcription_time']:.1f}s
453
+ - Generowanie raportu: ~{times['estimated_report_time']:.1f}s
454
+ - **ŁĄCZNIE: ~{times['total_estimated_time']:.1f}s ({times['total_estimated_time']/60:.1f} min)**
455
+ """
456
+
457
+ return plan
458
+
459
+ # Funkcje pomocnicze
460
+ def check_file_size_for_whisper(file_path: str) -> Tuple[bool, float]:
461
+ """Sprawdź czy plik mieści się w limicie Whisper"""
462
+ try:
463
+ size_mb = os.path.getsize(file_path) / (1024 * 1024)
464
+ return size_mb <= 25, size_mb
465
+ except:
466
+ return False, 0
467
+
468
+ def estimate_compression_ratio(file_ext: str) -> float:
469
+ """Estymuj współczynnik kompresji dla różnych formatów"""
470
+ ratios = {
471
+ 'wav': 0.1, # WAV kompresuje się bardzo dobrze
472
+ 'aac': 0.7, # AAC już skompresowany
473
+ 'mp3': 0.8, # MP3 już skompresowany
474
+ 'm4a': 0.7, # M4A już skompresowany
475
+ 'mp4': 0.5, # Video można mocno skompresować audio
476
+ 'mov': 0.5,
477
+ 'avi': 0.4
478
+ }
479
+ return ratios.get(file_ext.lower(), 0.6)
480
+
481
+ # Test modułu
482
  if __name__ == "__main__":
483
  print("🧪 Test FileHandler")
484
  handler = FileHandler()
485
+
486
+ stats = handler.get_processing_stats()
487
+ print(f"📊 Biblioteki: {stats['libraries_available']}")
488
+ print(f"🎯 Limit Whisper: {stats['whisper_max_size_mb']}MB")
489
+ print(f"🔒 Bezpieczny chunk: {stats['safe_chunk_size_mb']}MB")
490
+
491
  print("✅ FileHandler gotowy do użycia")