staraks commited on
Commit
4d24db2
·
verified ·
1 Parent(s): dfc0e4d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +482 -685
app.py CHANGED
@@ -1,6 +1,6 @@
1
  # app.py
2
- # Whisper transcription app - Redesigned UI with fine-tune-from-old-files + large-v3 model option
3
- # Drop-in replacement. Requires dependencies: gradio, whisper, pydub, pyzipper, python-docx, ffmpeg.
4
 
5
  import os
6
  import sys
@@ -19,20 +19,18 @@ os.environ["PYTHONUNBUFFERED"] = "1"
19
 
20
  print("DEBUG: app.py bootstrap starting", flush=True)
21
 
22
- # Third-party imports (must be installed in the environment)
23
  try:
24
- from docx import Document
25
- import whisper
26
  import gradio as gr
27
- import pyzipper
28
  from pydub import AudioSegment
 
 
29
  except Exception as e:
30
  print("FATAL: import error for third-party libs:", e, flush=True)
31
  traceback.print_exc()
32
  raise
33
 
34
- print("DEBUG: imports OK", flush=True)
35
-
36
  # ---------- Config ----------
37
  MEMORY_FILE = "memory.json"
38
  MEMORY_LOCK = threading.Lock()
@@ -44,15 +42,12 @@ FFMPEG_CANDIDATES = [
44
  ("pcm_s16le", 44100, 2),
45
  ("mulaw", 8000, 1),
46
  ]
47
- # Fine-tune globals
48
- FINETUNE_PROC = None
49
- FINETUNE_LOCK = threading.Lock()
50
- FINETUNE_LOG = os.path.join(tempfile.gettempdir(), "finetune_logs.txt")
51
  FINETUNE_WORKDIR = os.path.join(tempfile.gettempdir(), "finetune_workdir")
52
  os.makedirs(FINETUNE_WORKDIR, exist_ok=True)
53
- # ----------------------------
54
 
55
- # ---------- Utilities / Memory / Postprocessing ----------
56
  def load_memory():
57
  try:
58
  if os.path.exists(MEMORY_FILE):
@@ -73,7 +68,6 @@ def load_memory():
73
  pass
74
  return mem
75
 
76
-
77
  def save_memory(mem):
78
  with MEMORY_LOCK:
79
  try:
@@ -82,53 +76,31 @@ def save_memory(mem):
82
  except Exception:
83
  traceback.print_exc()
84
 
85
-
86
  memory = load_memory()
87
 
88
-
89
- MEDICAL_ABBREVIATIONS = {
90
- "pt": "patient",
91
- "dx": "diagnosis",
92
- "hx": "history",
93
- "sx": "symptoms",
94
- "c/o": "complains of",
95
- "bp": "blood pressure",
96
- "hr": "heart rate",
97
- "o2": "oxygen",
98
- "r/o": "rule out",
99
- "adm": "admit",
100
- "disch": "discharge",
101
- }
102
-
103
- DRUG_NORMALIZATION = {
104
- "metformin": "Metformin",
105
- "aspirin": "Aspirin",
106
- "amoxicillin": "Amoxicillin",
107
- }
108
-
109
 
110
  def expand_abbreviations(text):
111
  tokens = re.split(r"(\s+)", text)
112
- out = []
113
  for t in tokens:
114
  key = t.lower().strip(".,;:")
115
  if key in MEDICAL_ABBREVIATIONS:
116
- trailing = ""
117
- m = re.match(r"([A-Za-z0-9/]+)([.,;:]*)", t)
118
  if m:
119
- trailing = m.group(2) or ""
120
- out.append(MEDICAL_ABBREVIATIONS[key] + trailing)
121
  else:
122
  out.append(t)
123
  return "".join(out)
124
 
125
-
126
  def normalize_drugs(text):
127
- for k, v in DRUG_NORMALIZATION.items():
128
  text = re.sub(rf"\b{k}\b", v, text, flags=re.IGNORECASE)
129
  return text
130
 
131
-
132
  def punctuation_and_capitalization(text):
133
  text = text.strip()
134
  if not text:
@@ -136,7 +108,7 @@ def punctuation_and_capitalization(text):
136
  if not re.search(r"[.?!]\s*$", text):
137
  text = text.rstrip() + "."
138
  parts = re.split(r"([.?!]\s+)", text)
139
- out = []
140
  for p in parts:
141
  if p and not re.match(r"[.?!]\s+", p):
142
  out.append(p.capitalize())
@@ -144,67 +116,40 @@ def punctuation_and_capitalization(text):
144
  out.append(p)
145
  return "".join(out)
146
 
147
-
148
- def postprocess_transcript(text, format_soap=False):
149
  if not text:
150
  return text
151
- t = re.sub(r"\s+", " ", text).strip()
152
  t = expand_abbreviations(t)
153
  t = normalize_drugs(t)
154
  t = punctuation_and_capitalization(t)
155
- if format_soap:
156
- sentences = re.split(r"(?<=[.?!])\s+", t)
157
- subj = sentences[0] if len(sentences) >= 1 else ""
158
- obj = sentences[1] if len(sentences) >= 2 else ""
159
- assessment = ""
160
- for kw in ["diagnosis", "dx", "rule out", "r/o", "probable"]:
161
- if kw in t.lower():
162
- assessment = "Assessment: " + subj
163
- break
164
- soap = f"S: {subj}\nO: {obj}\nA: {assessment}\nP: Plan: follow up as indicated."
165
- return soap
166
  return t
167
 
168
-
169
  def extract_words_and_phrases(text):
170
  words = re.findall(r"[A-Za-z0-9\-']+", text)
171
  sentences = [s.strip() for s in re.split(r"(?<=[.?!])\s+", text) if s.strip()]
172
  return [w for w in words if w.strip()], sentences
173
 
174
-
175
  def update_memory_with_transcript(transcript):
176
  global memory
177
  words, sentences = extract_words_and_phrases(transcript)
178
- changed = False
179
  with MEMORY_LOCK:
180
  for w in words:
181
- lw = w.lower()
182
- if lw in memory["words"]:
183
- memory["words"][lw] += 1
184
- else:
185
- memory["words"][lw] = 1
186
- changed = True
187
  for s in sentences:
188
- key = s.strip()
189
- if key in memory["phrases"]:
190
- memory["phrases"][key] += 1
191
- else:
192
- memory["phrases"][key] = 1
193
- changed = True
194
  if changed:
195
- try:
196
- with open(MEMORY_FILE, "w", encoding="utf-8") as fh:
197
- json.dump(memory, fh, ensure_ascii=False, indent=2)
198
- except Exception:
199
- pass
200
-
201
 
202
- def memory_correct_text(text, min_ratio=0.85):
203
  if not text or (not memory.get("words") and not memory.get("phrases")):
204
  return text
205
-
206
  def fix_word(w):
207
- lw = w.lower()
208
  if lw in memory["words"]:
209
  return w
210
  candidates = get_close_matches(lw, memory["words"].keys(), n=1, cutoff=min_ratio)
@@ -214,285 +159,178 @@ def memory_correct_text(text, min_ratio=0.85):
214
  return cand.capitalize()
215
  return cand
216
  return w
217
-
218
  tokens = re.split(r"(\W+)", text)
219
- corrected_tokens = []
220
  for tok in tokens:
221
  if re.match(r"^[A-Za-z0-9\-']+$", tok):
222
- corrected_tokens.append(fix_word(tok))
223
  else:
224
- corrected_tokens.append(tok)
225
- corrected = "".join(corrected_tokens)
226
-
227
- for phrase in sorted(memory.get("phrases", {}).keys(), key=lambda s: -len(s)):
228
  low_phrase = phrase.lower()
229
  if len(low_phrase) < 8:
230
  continue
231
- if low_phrase in corrected.lower():
232
- corrected = re.sub(re.escape(phrase), phrase, corrected, flags=re.IGNORECASE)
233
- return corrected
234
-
235
-
236
- # ---------- Memory management helpers ----------
237
- def import_memory_file(uploaded):
238
- global memory
239
- if not uploaded:
240
- return "No file provided."
241
- path = None
242
- try:
243
- if isinstance(uploaded, (str, os.PathLike)):
244
- path = str(uploaded)
245
- elif hasattr(uploaded, "name"):
246
- path = uploaded.name
247
- elif isinstance(uploaded, dict) and uploaded.get("name"):
248
- path = uploaded["name"]
249
- else:
250
- return "Unable to determine uploaded file path."
251
- with open(path, "r", encoding="utf-8") as fh:
252
- raw = fh.read()
253
- try:
254
- parsed = json.loads(raw)
255
- if isinstance(parsed, dict):
256
- with MEMORY_LOCK:
257
- parsed_words = parsed.get("words", {})
258
- parsed_phrases = parsed.get("phrases", {})
259
- for k, v in parsed_words.items():
260
- memory["words"][k.lower()] = memory["words"].get(k.lower(), 0) + int(v)
261
- for k, v in parsed_phrases.items():
262
- memory["phrases"][k] = memory["phrases"].get(k, 0) + int(v)
263
- save_memory(memory)
264
- return f"Imported JSON memory (words={len(parsed_words)}, phrases={len(parsed_phrases)})."
265
- except Exception:
266
- pass
267
- lines = [l.strip() for l in raw.splitlines() if l.strip()]
268
- added_words = 0
269
- added_phrases = 0
270
- with MEMORY_LOCK:
271
- for line in lines:
272
- if "," in line:
273
- parts = [p.strip() for p in line.split(",", 1)]
274
- key = parts[0].lower()
275
- try:
276
- cnt = int(parts[1])
277
- except Exception:
278
- cnt = 1
279
- memory["words"][key] = memory["words"].get(key, 0) + cnt
280
- added_words += 1
281
- else:
282
- if len(line.split()) <= 3:
283
- key = line.lower()
284
- memory["words"][key] = memory["words"].get(key, 0) + 1
285
- added_words += 1
286
- else:
287
- memory["phrases"][line] = memory["phrases"].get(line, 0) + 1
288
- added_phrases += 1
289
- save_memory(memory)
290
- return f"Imported {added_words} words and {added_phrases} phrases from file."
291
- except Exception as e:
292
- traceback.print_exc()
293
- return f"Import failed: {e}"
294
-
295
 
296
- def add_memory_entry(entry):
297
- global memory
298
- if not entry or not entry.strip():
299
- return "No entry provided."
300
- e = entry.strip()
301
- with MEMORY_LOCK:
302
- if len(e.split()) <= 3:
303
- key = e.lower()
304
- memory["words"][key] = memory["words"].get(key, 0) + 1
305
- save_memory(memory)
306
- return f"Added/updated word: '{key}'."
307
- else:
308
- memory["phrases"][e] = memory["phrases"].get(e, 0) + 1
309
- save_memory(memory)
310
- return f"Added/updated phrase: '{e}'."
311
-
312
-
313
- def clear_memory():
314
- global memory
315
- with MEMORY_LOCK:
316
- memory = {"words": {}, "phrases": {}}
317
- save_memory(memory)
318
- return "Memory cleared."
319
-
320
-
321
- def view_memory(limit=4000):
322
- w = memory.get("words", {})
323
- p = memory.get("phrases", {})
324
- out_lines = []
325
- out_lines.append("WORDS (top 50):")
326
- for k, v in sorted(w.items(), key=lambda kv: -kv[1])[:50]:
327
- out_lines.append(f"{k}: {v}")
328
- out_lines.append("")
329
- out_lines.append("PHRASES (top 50):")
330
- for k, v in sorted(p.items(), key=lambda kv: -kv[1])[:50]:
331
- out_lines.append(f"{k}: {v}")
332
- out = "\n".join(out_lines)
333
- if len(out) > limit:
334
- out = out[:limit] + "\n...truncated..."
335
- return out
336
-
337
-
338
- # ---------- File utilities ----------
339
- def save_as_word(text, filename=None):
340
- if filename is None:
341
- filename = os.path.join(tempfile.gettempdir(), "merged_transcripts.docx")
342
- doc = Document()
343
- doc.add_paragraph(text)
344
- doc.save(filename)
345
- return filename
346
-
347
-
348
- # ---------- Conversion helpers (pydub + ffmpeg fallback) ----------
349
  def _ffmpeg_convert(input_path, out_path, fmt, sr, ch):
 
 
 
 
 
350
  try:
351
- cmd = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y"]
352
- if fmt in ("s16le", "pcm_s16le", "mulaw"):
353
- cmd += ["-f", fmt, "-ar", str(sr), "-ac", str(ch), "-i", input_path, out_path]
354
- else:
355
- cmd += ["-i", input_path, "-ar", str(sr), "-ac", str(ch), out_path]
356
  proc = subprocess.run(cmd, capture_output=True, timeout=60, text=True)
357
- stdout_stderr = (proc.stdout or "") + (proc.stderr or "")
358
- if proc.returncode == 0 and os.path.exists(out_path) and os.path.getsize(out_path) > MIN_WAV_SIZE:
359
  return True, stdout_stderr
360
  else:
361
  try:
362
- if os.path.exists(out_path):
363
- os.unlink(out_path)
364
- except Exception:
365
- pass
366
  return False, stdout_stderr
367
  except Exception as e:
368
  try:
369
- if os.path.exists(out_path):
370
- os.unlink(out_path)
371
- except Exception:
372
- pass
373
  return False, str(e)
374
 
375
-
376
  def convert_to_wav_if_needed(input_path):
377
- input_path = str(input_path)
378
- lower = input_path.lower()
379
- if lower.endswith(".wav"):
380
  return input_path
381
-
382
- auto_err = ""
383
- tmp = None
384
  try:
385
  tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
386
  tmp.close()
387
  AudioSegment.from_file(input_path).export(tmp.name, format="wav")
388
- if os.path.exists(tmp.name) and os.path.getsize(tmp.name) > MIN_WAV_SIZE:
389
  return tmp.name
390
  else:
391
- try:
392
- os.unlink(tmp.name)
393
- except Exception:
394
- pass
395
  except Exception:
396
- auto_err = traceback.format_exc()
397
  try:
398
- if tmp and os.path.exists(tmp.name):
399
- os.unlink(tmp.name)
400
- except Exception:
401
- pass
402
-
403
  diag_dir = tempfile.mkdtemp(prefix="dct_diag_")
404
  diag_log = os.path.join(diag_dir, "conversion_diagnostics.txt")
405
- diagnostics = []
406
- for fmt, sr, ch in FFMPEG_CANDIDATES:
407
  out_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
408
  out_wav.close()
409
  success, debug = _ffmpeg_convert(input_path, out_wav.name, fmt, sr, ch)
410
  diagnostics.append(f"TRY fmt={fmt} sr={sr} ch={ch} success={success}\n{debug}\n")
411
  if success:
412
  try:
413
- with open(diag_log, "w", encoding="utf-8") as fh:
414
  fh.write("pydub auto error:\n")
415
- fh.write(auto_err + "\n\n")
416
  fh.write("Successful ffmpeg candidate:\n")
417
  fh.write(f"fmt={fmt} sr={sr} ch={ch}\n\n")
418
  fh.write("Diagnostics:\n")
419
  fh.write("\n".join(diagnostics))
420
- except Exception:
421
- pass
422
  return out_wav.name
423
  else:
424
  try:
425
- if os.path.exists(out_wav.name):
426
- os.unlink(out_wav.name)
427
- except Exception:
428
- pass
429
-
430
  try:
431
- fp = subprocess.run(
432
- ["ffprobe", "-v", "error", "-show_format", "-show_streams", input_path],
433
- capture_output=True,
434
- text=True,
435
- timeout=10,
436
- )
437
- diagnostics.append("FFPROBE:\n" + (fp.stdout.strip() or fp.stderr.strip()))
438
  except Exception as e:
439
- diagnostics.append("ffprobe failed: " + str(e))
440
  try:
441
- with open(input_path, "rb") as fh:
442
  head = fh.read(512)
443
- diagnostics.append("HEX PREVIEW:\n" + head.hex())
444
  except Exception as e:
445
- diagnostics.append("could not read head: " + str(e))
446
-
447
  try:
448
- with open(diag_log, "w", encoding="utf-8") as fh:
449
  fh.write("pydub auto error:\n")
450
- fh.write(auto_err + "\n\n")
451
  fh.write("Full diagnostics:\n\n")
452
  fh.write("\n\n".join(diagnostics))
453
  except Exception as e:
454
  raise Exception(f"Conversion failed; diagnostics write error: {e}")
455
-
456
  raise Exception(f"Could not convert file to WAV. Diagnostics saved to: {diag_log}")
457
 
458
-
459
- # ---------- Whisper model cache (supports large-v3 option) ----------
460
- MODEL_CACHE = {}
461
-
462
-
463
  def get_whisper_model(name, device=None):
 
464
  if name not in MODEL_CACHE:
465
  print(f"DEBUG: loading whisper model '{name}'", flush=True)
466
- # Whisper API may accept "large-v3" if installed; otherwise map it or change dropdown.
467
  if device:
468
- MODEL_CACHE[name] = whisper.load_model(name, device=device)
 
 
 
 
469
  else:
470
  MODEL_CACHE[name] = whisper.load_model(name)
471
  return MODEL_CACHE[name]
472
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
- # ---------- ZIP extraction helpers ----------
475
  def extract_zip_list(zip_file, zip_password):
476
  temp_extract_dir = os.path.join(tempfile.gettempdir(), "extracted_audio")
477
  try:
478
  if os.path.exists(temp_extract_dir):
479
- try:
480
- shutil.rmtree(temp_extract_dir)
481
- except Exception:
482
- pass
483
  os.makedirs(temp_extract_dir, exist_ok=True)
484
- extracted = []
485
- logs = []
486
  with pyzipper.ZipFile(zip_file, "r") as zf:
487
  if zip_password:
488
- try:
489
- zf.setpassword(zip_password.encode())
490
- except Exception:
491
- logs.append("Warning: failed to set zip password (unexpected).")
492
- exts = [".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a", ".dat", ".dct"]
493
  for info in zf.infolist():
494
- if info.is_dir():
495
- continue
496
  _, ext = os.path.splitext(info.filename)
497
  if ext.lower() in exts:
498
  try:
@@ -500,9 +338,6 @@ def extract_zip_list(zip_file, zip_password):
500
  except RuntimeError as e:
501
  logs.append(f"Password required/incorrect for {info.filename}: {e}")
502
  continue
503
- except pyzipper.BadZipFile:
504
- logs.append(f"Bad zip entry: {info.filename}")
505
- continue
506
  except Exception as e:
507
  logs.append(f"Error extracting {info.filename}: {e}")
508
  continue
@@ -518,411 +353,373 @@ def extract_zip_list(zip_file, zip_password):
518
  traceback.print_exc()
519
  return [], f"Extraction failed: {e}"
520
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
 
522
- # ---------- Transcription generator ----------
523
- def transcribe_multiple(
524
- selected_paths,
525
- model_name,
526
- advanced_options,
527
- merge_checkbox,
528
- enable_memory=False,
529
- device=None,
530
- ):
531
- log = []
532
- transcripts = []
533
- word_file_path = None
534
-
535
- if not selected_paths:
536
- log.append("No audio files provided.")
537
- yield "\n\n".join(log), "\n\n".join(transcripts), None, 100
538
- return
539
-
540
- yield "", "", None, 0
541
-
542
- yield "\n\n".join(log), "\n\n".join(transcripts), None, 5
543
- try:
544
- model = get_whisper_model(model_name, device=device)
545
- log.append(f"Loaded Whisper model: {model_name}")
546
- except Exception as e:
547
- log.append(f"Failed to load model {model_name}: {e}")
548
- yield "\n\n".join(log), "\n\n".join(transcripts), None, 100
549
- return
550
-
551
- total = len(selected_paths)
552
- for idx, p in enumerate(selected_paths, start=1):
553
- log.append(f"Processing file ({idx}/{total}): {os.path.basename(str(p))}")
554
- yield "\n\n".join(log), "\n\n".join(transcripts), None, int(5 + (idx - 1) * 80 / max(1, total))
555
-
556
- wav = None
557
- try:
558
- wav = convert_to_wav_if_needed(p)
559
- log.append(f"Converted to WAV: {os.path.basename(str(wav))}")
560
- except Exception as e:
561
- log.append(f"Conversion failed for {os.path.basename(str(p))}: {e}")
562
- transcripts.append(f"FILE: {os.path.basename(str(p))}\nERROR: Conversion failed: {e}")
563
- yield "\n\n".join(log), "\n\n".join(transcripts), None, int(5 + idx * 80 / max(1, total))
564
- continue
565
-
566
- try:
567
- whisper_opts = {}
568
- if isinstance(advanced_options, dict):
569
- whisper_opts.update(advanced_options)
570
-
571
- result = model.transcribe(wav, **whisper_opts)
572
- text = result.get("text", "").strip()
573
- log.append(f"Transcribed: {len(text)} chars")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
 
575
- if enable_memory:
576
- text = memory_correct_text(text)
577
- text = postprocess_transcript(text)
578
- transcripts.append(f"FILE: {os.path.basename(str(p))}\n{text}\n")
579
 
580
- if enable_memory:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
  try:
582
- update_memory_with_transcript(text)
583
- log.append("Memory updated.")
584
- except Exception:
585
- pass
586
-
587
- yield "\n\n".join(log), "\n\n".join(transcripts), None, int(10 + idx * 85 / max(1, total))
588
- except Exception as e:
589
- log.append(f"Transcription failed for {os.path.basename(str(p))}: {e}")
590
- transcripts.append(f"FILE: {os.path.basename(str(p))}\nERROR: Transcription failed: {e}")
591
- yield "\n\n".join(log), "\n\n".join(transcripts), None, int(10 + idx * 85 / max(1, total))
592
- continue
593
- finally:
594
- try:
595
- if wav and os.path.exists(wav):
596
- tmpdir = tempfile.gettempdir()
597
  try:
598
- common = os.path.commonpath([os.path.abspath(tmpdir), os.path.abspath(wav)])
599
- if common == os.path.abspath(tmpdir) and not str(p).lower().endswith(".wav"):
600
- os.unlink(wav)
601
  except Exception:
602
- try:
603
- if tmpdir in os.path.abspath(wav) and not str(p).lower().endswith(".wav"):
604
- os.unlink(wav)
605
- except Exception:
606
- pass
607
- except Exception:
608
- pass
609
-
610
- if merge_checkbox:
611
- try:
612
- merged_text = "\n\n".join(transcripts)
613
- word_file_path = save_as_word(merged_text)
614
- log.append(f"Merged transcript saved: {word_file_path}")
615
- except Exception as e:
616
- log.append(f"Failed to save merged file: {e}")
617
- word_file_path = None
618
-
619
- yield "\n\n".join(log), "\n\n".join(transcripts), word_file_path, 100
620
-
621
-
622
- # ---------- Fine-tune helpers (updated to include old-files option) ----------
623
- def _safe_write_log(msg):
624
- try:
625
- with open(FINETUNE_LOG, "a", encoding="utf-8") as fh:
626
- fh.write(msg + "\n")
627
- except Exception:
628
- pass
629
-
630
-
631
- def _collect_old_files_into(dst_dir, old_dir_path):
632
- """
633
- Copy audio + matching .txt transcripts from old_dir_path into dst_dir.
634
- Returns count of copied pairs and messages.
635
- """
636
- msgs = []
637
- copied = 0
638
- try:
639
- if not os.path.isdir(old_dir_path):
640
- return 0, f"Old-files path is not a directory: {old_dir_path}"
641
- for root, _, files in os.walk(old_dir_path):
642
- for f in files:
643
- if f.lower().endswith((".wav", ".mp3", ".flac", ".m4a", ".ogg")):
644
- src_audio = os.path.join(root, f)
645
- base = os.path.splitext(f)[0]
646
- # prefer .txt transcript with same basename
647
- possible_txt = os.path.join(root, base + ".txt")
648
- rel_subdir = os.path.relpath(root, old_dir_path)
649
- target_subdir = os.path.join(dst_dir, rel_subdir)
650
- os.makedirs(target_subdir, exist_ok=True)
651
- target_audio = os.path.join(target_subdir, f)
652
- shutil.copy2(src_audio, target_audio)
653
- if os.path.exists(possible_txt):
654
- shutil.copy2(possible_txt, os.path.join(target_subdir, base + ".txt"))
655
- msgs.append(f"Copied pair: {os.path.join(rel_subdir, f)} + .txt")
656
  else:
657
- msgs.append(f"Copied audio (no transcript found): {os.path.join(rel_subdir, f)}")
658
- copied += 1
659
- return copied, "\n".join(msgs)
660
- except Exception as e:
661
- traceback.print_exc()
662
- return copied, f"Error copying old files: {e}"
663
-
664
-
665
- def prepare_finetune_dataset(uploaded_zip_or_dir, include_old_files=False, old_files_dir=""):
666
- """
667
- Prepares finetune working dir. If include_old_files=True and old_files_dir provided,
668
- it will copy audio+transcripts from old_files_dir into the dataset area.
669
- Returns (status_message, manifest_path_or_empty).
670
- """
671
- dst = os.path.join(FINETUNE_WORKDIR, "data")
672
- try:
673
- if os.path.exists(dst):
674
- shutil.rmtree(dst)
675
- os.makedirs(dst, exist_ok=True)
676
- except Exception as e:
677
- return f"Failed to prepare workdir: {e}", ""
678
-
679
- path = None
680
- try:
681
- if uploaded_zip_or_dir:
682
- if isinstance(uploaded_zip_or_dir, (str, os.PathLike)):
683
- path = str(uploaded_zip_or_dir)
684
- elif hasattr(uploaded_zip_or_dir, "name"):
685
- path = uploaded_zip_or_dir.name
686
- elif isinstance(uploaded_zip_or_dir, dict) and uploaded_zip_or_dir.get("name"):
687
- path = uploaded_zip_or_dir["name"]
688
- except Exception as e:
689
- return f"Unable to determine uploaded path: {e}", ""
690
-
691
- # If zip uploaded, extract into dst
692
- if path and os.path.isfile(path) and path.lower().endswith(".zip"):
693
- try:
694
- with pyzipper.ZipFile(path, "r") as zf:
695
- zf.extractall(dst)
696
- except Exception as e:
697
- return f"Failed to extract ZIP: {e}", ""
698
- elif path and os.path.isdir(path):
699
- try:
700
- for item in os.listdir(path):
701
- s = os.path.join(path, item)
702
- d = os.path.join(dst, item)
703
- if os.path.isdir(s):
704
- shutil.copytree(s, d)
705
- else:
706
- shutil.copy2(s, d)
707
- except Exception as e:
708
- return f"Failed to copy dataset dir: {e}", ""
709
-
710
- # If include_old_files, copy them into dst as well (allow merging)
711
- old_msgs = ""
712
- if include_old_files and old_files_dir:
713
- # normalize path (uploads from Gradio may be filepaths)
714
- old_path = None
715
- if isinstance(old_files_dir, (str, os.PathLike)):
716
- old_path = str(old_files_dir)
717
- elif hasattr(old_files_dir, "name"):
718
- old_path = old_files_dir.name
719
- elif isinstance(old_files_dir, dict) and old_files_dir.get("name"):
720
- old_path = old_files_dir["name"]
721
- if old_path:
722
- copied, msg = _collect_old_files_into(dst, old_path)
723
- old_msgs = f"\nOld-files: copied {copied} audio files.\nDetails:\n{msg}"
724
-
725
- # Now try to find a manifest/transcripts in dst or build one from .wav + .txt pairs
726
- transcripts_candidates = [
727
- os.path.join(dst, "transcripts.tsv"),
728
- os.path.join(dst, "metadata.tsv"),
729
- os.path.join(dst, "manifest.tsv"),
730
- os.path.join(dst, "transcripts.txt"),
731
- os.path.join(dst, "metadata.txt"),
732
- os.path.join(dst, "manifest.jsonl"),
733
- ]
734
- manifest_path = os.path.join(FINETUNE_WORKDIR, "manifest.tsv")
735
- found = False
736
-
737
- for tpath in transcripts_candidates:
738
- if os.path.exists(tpath):
739
- try:
740
- shutil.copy2(tpath, manifest_path)
741
- found = True
742
- break
743
- except Exception:
744
- pass
745
-
746
- if not found:
747
- # discover audio files and matching .txt transcripts (same basename)
748
- audio_files = []
749
- for root, _, files in os.walk(dst):
750
- for f in files:
751
- if f.lower().endswith((".wav", ".mp3", ".flac", ".m4a", ".ogg")):
752
- audio_files.append(os.path.join(root, f))
753
- if not audio_files:
754
- return f"No audio files found in dataset.{old_msgs}", ""
755
- entries = []
756
- missing_transcripts = 0
757
- for a in audio_files:
758
- base = os.path.splitext(a)[0]
759
- t_candidate = base + ".txt"
760
- transcript = ""
761
- if os.path.exists(t_candidate):
762
- try:
763
- with open(t_candidate, "r", encoding="utf-8") as fh:
764
- transcript = fh.read().strip().replace("\n", " ")
765
- except Exception:
766
- transcript = ""
767
- else:
768
- missing_transcripts += 1
769
- entries.append(f"{a}\t{transcript}")
770
- try:
771
- with open(manifest_path, "w", encoding="utf-8") as fh:
772
- fh.write("\n".join(entries))
773
- found = True
774
- except Exception as e:
775
- return f"Failed to write manifest: {e}{old_msgs}", ""
776
-
777
- if not found:
778
- return f"Failed to locate or build manifest.{old_msgs}", ""
779
-
780
- status_msg = f"Dataset prepared. Manifest: {manifest_path}{old_msgs}"
781
- if missing_transcripts > 0:
782
- status_msg += f"\nWarning: {missing_transcripts} audio files have no matching .txt transcript (empty transcripts saved)."
783
-
784
- return status_msg, manifest_path
785
-
786
-
787
- def start_finetune(manifest_path, base_model, epochs, batch_size, lr, output_dir):
788
- global FINETUNE_PROC
789
- with FINETUNE_LOCK:
790
- if FINETUNE_PROC and FINETUNE_PROC.poll() is None:
791
- return "Fine-tune already running."
792
-
793
- outdir = output_dir or os.path.join(FINETUNE_WORKDIR, "output")
794
- os.makedirs(outdir, exist_ok=True)
795
-
796
- try:
797
- if os.path.exists(FINETUNE_LOG):
798
- os.remove(FINETUNE_LOG)
799
- except Exception:
800
- pass
801
-
802
- START_CMD = [
803
- sys.executable,
804
- "fine_tune.py",
805
- "--manifest",
806
- manifest_path,
807
- "--base_model",
808
- base_model,
809
- "--epochs",
810
- str(epochs),
811
- "--batch_size",
812
- str(batch_size),
813
- "--lr",
814
- str(lr),
815
- "--output_dir",
816
- outdir,
817
- ]
818
- try:
819
- logfile = open(FINETUNE_LOG, "a", encoding="utf-8")
820
- proc = subprocess.Popen(START_CMD, stdout=logfile, stderr=logfile, cwd=os.getcwd())
821
- FINETUNE_PROC = proc
822
- _safe_write_log(f"Started fine-tune: PID={proc.pid}, cmd={' '.join(START_CMD)}")
823
- return f"Fine-tune started (PID={proc.pid}). Logs: {FINETUNE_LOG}"
824
- except FileNotFoundError as e:
825
- return f"Training script not found: {e}. Put your training script 'fine_tune.py' in project root or change START_CMD."
826
- except Exception as e:
827
- return f"Failed to start fine-tune: {e}"
828
-
829
-
830
- def stop_finetune():
831
- global FINETUNE_PROC
832
- with FINETUNE_LOCK:
833
- if not FINETUNE_PROC:
834
- return "No running fine-tune process."
835
- try:
836
- FINETUNE_PROC.terminate()
837
- FINETUNE_PROC.wait(timeout=10)
838
- pid = FINETUNE_PROC.pid
839
- FINETUNE_PROC = None
840
- _safe_write_log(f"Terminated fine-tune PID={pid}")
841
- return f"Terminated fine-tune PID={pid}"
842
- except Exception as e:
843
- try:
844
- FINETUNE_PROC.kill()
845
- except Exception:
846
- pass
847
- FINETUNE_PROC = None
848
- return f"Force killed fine-tune process: {e}"
849
-
850
-
851
- def tail_finetune_logs(lines=50):
852
- try:
853
- if not os.path.exists(FINETUNE_LOG):
854
- return "No logs yet."
855
- with open(FINETUNE_LOG, "r", encoding="utf-8", errors="ignore") as fh:
856
- all_lines = fh.read().splitlines()
857
- last = all_lines[-lines:]
858
- return "\n".join(last)
859
- except Exception as e:
860
- return f"Failed to read logs: {e}"
861
-
862
 
863
- # ----------------------- Gradio UI (updated fine-tune controls + large-v3) -----------------------
864
- print("DEBUG: building Gradio Blocks", flush=True)
865
- with gr.Blocks(title="Whisper Transcriber — Fine-tune from old files + large-v3") as demo:
866
- gr.Markdown("<h1 style='margin-bottom:0.25rem;'>Whisper Transcriber — Fine-tune Enhancements</h1>")
867
- gr.Markdown("<p style='margin-top:0.1rem;color:#666;'>Now supports including old audio+transcript folders in fine-tune dataset and a 'large-v3' model option.</p>")
868
-
869
- with gr.Tabs():
870
- # For brevity, include only Fine-tune tab here since transcribe tabs exist in your app.
871
- with gr.TabItem("Fine-tune (with old-files)"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
872
  with gr.Row():
873
  with gr.Column(scale=1):
874
- gr.Markdown("### Dataset")
875
- ft_upload = gr.File(label="Upload training ZIP or folder (zip)", file_count="single", type="filepath")
876
- include_old_chk = gr.Checkbox(label="Include old audio+transcript folder", value=False)
877
- old_files_dir = gr.File(label="Old files folder (select directory path)", file_count="single", type="filepath")
878
- ft_prepare_btn = gr.Button("Prepare dataset (merge uploaded + old files)")
879
- ft_prepare_status = gr.Textbox(label="Prepare status / manifest", interactive=False, lines=6)
880
-
881
- gr.Markdown("### Training parameters")
882
- ft_base_model = gr.Dropdown(choices=["small", "base", "medium", "large", "large-v3"], value="small", label="Base model")
883
- ft_epochs = gr.Slider(minimum=1, maximum=100, value=3, step=1, label="Epochs")
884
- ft_batch = gr.Number(label="Batch size", value=8)
885
- ft_lr = gr.Number(label="Learning rate", value=1e-5, precision=8)
886
- ft_output_dir = gr.Textbox(label="Output dir (optional)", value="", placeholder="Leave blank to use temp output")
887
-
888
- ft_start_btn = gr.Button("Start Fine-tune")
889
- ft_stop_btn = gr.Button("Stop Fine-tune")
890
- ft_start_status = gr.Textbox(label="Start/Stop status", interactive=False, lines=6)
891
-
892
- ft_tail_btn = gr.Button("Tail training logs")
893
- ft_logs = gr.Textbox(label="Training logs (tail)", interactive=False, lines=12)
894
- with gr.Column(scale=1):
895
- gr.Markdown("### Notes & Tips")
896
- gr.Markdown(
897
- "- Old-files folder: include audio files and matching `.txt` transcripts with same basename (e.g. `call1.wav` + `call1.txt`).\n"
898
- "- When preparing dataset with both uploaded ZIP and old-files enabled, contents are merged into the fine-tune workspace.\n"
899
- "- If you pick `large-v3` ensure your installed whisper package supports that model name; otherwise select an available model.\n"
900
- )
901
-
902
- def _prepare_action_with_old(uploaded, include_old, old_dir):
903
- status, manifest = prepare_finetune_dataset(uploaded, include_old_files=include_old, old_files_dir=old_dir)
904
- return status
905
-
906
- ft_prepare_btn.click(fn=_prepare_action_with_old, inputs=[ft_upload, include_old_chk, old_files_dir], outputs=[ft_prepare_status])
907
-
908
- def _start_action(ft_prepare_status_txt, ft_base_model, ft_epochs, ft_batch, ft_lr, ft_output_dir):
909
- manifest_guess = os.path.join(FINETUNE_WORKDIR, "manifest.tsv")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
910
  if not os.path.exists(manifest_guess):
911
- return "Manifest not found. Prepare dataset first or manually provide manifest."
912
- status = start_finetune(manifest_guess, ft_base_model, int(ft_epochs), int(ft_batch), float(ft_lr), ft_output_dir)
913
- return status
914
-
915
- ft_start_btn.click(fn=_start_action, inputs=[ft_prepare_status, ft_base_model, ft_epochs, ft_batch, ft_lr, ft_output_dir], outputs=[ft_start_status])
916
- ft_stop_btn.click(fn=lambda: stop_finetune(), inputs=[], outputs=[ft_start_status])
917
- ft_tail_btn.click(fn=lambda: tail_finetune_logs(), inputs=[], outputs=[ft_logs])
918
-
919
- # ---------- Launch ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
  if __name__ == "__main__":
921
  port = int(os.environ.get("PORT", 7860))
922
  print("DEBUG: launching Gradio on port", port, flush=True)
923
- try:
924
- demo.queue().launch(server_name="0.0.0.0", server_port=port)
925
- except Exception as e:
926
- print("FATAL: demo.launch failed:", e, flush=True)
927
- traceback.print_exc()
928
- raise
 
1
  # app.py
2
+ # Whisper Transcriber Beautiful UI + Tabs (Audio Transcribe focused)
3
+ # Drop-in replacement. Requires: gradio, whisper, pydub, pyzipper, python-docx, ffmpeg installed.
4
 
5
  import os
6
  import sys
 
19
 
20
  print("DEBUG: app.py bootstrap starting", flush=True)
21
 
22
+ # Third-party imports
23
  try:
 
 
24
  import gradio as gr
25
+ import whisper
26
  from pydub import AudioSegment
27
+ import pyzipper
28
+ from docx import Document
29
  except Exception as e:
30
  print("FATAL: import error for third-party libs:", e, flush=True)
31
  traceback.print_exc()
32
  raise
33
 
 
 
34
  # ---------- Config ----------
35
  MEMORY_FILE = "memory.json"
36
  MEMORY_LOCK = threading.Lock()
 
42
  ("pcm_s16le", 44100, 2),
43
  ("mulaw", 8000, 1),
44
  ]
45
+
46
+ MODEL_CACHE = {}
 
 
47
  FINETUNE_WORKDIR = os.path.join(tempfile.gettempdir(), "finetune_workdir")
48
  os.makedirs(FINETUNE_WORKDIR, exist_ok=True)
 
49
 
50
+ # ---------- Helpers (conversion, whisper, memory, small postprocessing) ----------
51
  def load_memory():
52
  try:
53
  if os.path.exists(MEMORY_FILE):
 
68
  pass
69
  return mem
70
 
 
71
  def save_memory(mem):
72
  with MEMORY_LOCK:
73
  try:
 
76
  except Exception:
77
  traceback.print_exc()
78
 
 
79
  memory = load_memory()
80
 
81
+ MEDICAL_ABBREVIATIONS = {"pt":"patient","dx":"diagnosis","hx":"history","sx":"symptoms","c/o":"complains of","bp":"blood pressure","hr":"heart rate","o2":"oxygen","r/o":"rule out","adm":"admit","disch":"discharge"}
82
+ DRUG_NORMALIZATION = {"metformin":"Metformin","aspirin":"Aspirin","amoxicillin":"Amoxicillin"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  def expand_abbreviations(text):
85
  tokens = re.split(r"(\s+)", text)
86
+ out=[]
87
  for t in tokens:
88
  key = t.lower().strip(".,;:")
89
  if key in MEDICAL_ABBREVIATIONS:
90
+ trailing=""
91
+ m=re.match(r"([A-Za-z0-9/]+)([.,;:]*)",t)
92
  if m:
93
+ trailing=m.group(2) or ""
94
+ out.append(MEDICAL_ABBREVIATIONS[key]+trailing)
95
  else:
96
  out.append(t)
97
  return "".join(out)
98
 
 
99
  def normalize_drugs(text):
100
+ for k,v in DRUG_NORMALIZATION.items():
101
  text = re.sub(rf"\b{k}\b", v, text, flags=re.IGNORECASE)
102
  return text
103
 
 
104
  def punctuation_and_capitalization(text):
105
  text = text.strip()
106
  if not text:
 
108
  if not re.search(r"[.?!]\s*$", text):
109
  text = text.rstrip() + "."
110
  parts = re.split(r"([.?!]\s+)", text)
111
+ out=[]
112
  for p in parts:
113
  if p and not re.match(r"[.?!]\s+", p):
114
  out.append(p.capitalize())
 
116
  out.append(p)
117
  return "".join(out)
118
 
119
+ def postprocess_transcript(text):
 
120
  if not text:
121
  return text
122
+ t = re.sub(r"\s+"," ",text).strip()
123
  t = expand_abbreviations(t)
124
  t = normalize_drugs(t)
125
  t = punctuation_and_capitalization(t)
 
 
 
 
 
 
 
 
 
 
 
126
  return t
127
 
 
128
  def extract_words_and_phrases(text):
129
  words = re.findall(r"[A-Za-z0-9\-']+", text)
130
  sentences = [s.strip() for s in re.split(r"(?<=[.?!])\s+", text) if s.strip()]
131
  return [w for w in words if w.strip()], sentences
132
 
 
133
  def update_memory_with_transcript(transcript):
134
  global memory
135
  words, sentences = extract_words_and_phrases(transcript)
136
+ changed=False
137
  with MEMORY_LOCK:
138
  for w in words:
139
+ lw=w.lower()
140
+ memory["words"][lw]=memory["words"].get(lw,0)+1
141
+ changed=True
 
 
 
142
  for s in sentences:
143
+ memory["phrases"][s]=memory["phrases"].get(s,0)+1
144
+ changed=True
 
 
 
 
145
  if changed:
146
+ save_memory(memory)
 
 
 
 
 
147
 
148
+ def memory_correct_text(text,min_ratio=0.85):
149
  if not text or (not memory.get("words") and not memory.get("phrases")):
150
  return text
 
151
  def fix_word(w):
152
+ lw=w.lower()
153
  if lw in memory["words"]:
154
  return w
155
  candidates = get_close_matches(lw, memory["words"].keys(), n=1, cutoff=min_ratio)
 
159
  return cand.capitalize()
160
  return cand
161
  return w
 
162
  tokens = re.split(r"(\W+)", text)
163
+ corrected=[]
164
  for tok in tokens:
165
  if re.match(r"^[A-Za-z0-9\-']+$", tok):
166
+ corrected.append(fix_word(tok))
167
  else:
168
+ corrected.append(tok)
169
+ corrected_text = "".join(corrected)
170
+ for phrase in sorted(memory.get("phrases",{}).keys(), key=lambda s:-len(s)):
 
171
  low_phrase = phrase.lower()
172
  if len(low_phrase) < 8:
173
  continue
174
+ if low_phrase in corrected_text.lower():
175
+ corrected_text = re.sub(re.escape(phrase), phrase, corrected_text, flags=re.IGNORECASE)
176
+ return corrected_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ # ---------- Conversion helpers ----------
179
+ MIN_WAV_SIZE = MIN_WAV_SIZE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  def _ffmpeg_convert(input_path, out_path, fmt, sr, ch):
181
+ cmd = ["ffmpeg","-hide_banner","-loglevel","error","-y"]
182
+ if fmt in ("s16le","pcm_s16le","mulaw"):
183
+ cmd += ["-f",fmt,"-ar",str(sr),"-ac",str(ch),"-i",input_path,out_path]
184
+ else:
185
+ cmd += ["-i",input_path,"-ar",str(sr),"-ac",str(ch),out_path]
186
  try:
 
 
 
 
 
187
  proc = subprocess.run(cmd, capture_output=True, timeout=60, text=True)
188
+ stdout_stderr=(proc.stdout or "")+(proc.stderr or "")
189
+ if proc.returncode==0 and os.path.exists(out_path) and os.path.getsize(out_path)>MIN_WAV_SIZE:
190
  return True, stdout_stderr
191
  else:
192
  try:
193
+ if os.path.exists(out_path): os.unlink(out_path)
194
+ except Exception: pass
 
 
195
  return False, stdout_stderr
196
  except Exception as e:
197
  try:
198
+ if os.path.exists(out_path): os.unlink(out_path)
199
+ except Exception: pass
 
 
200
  return False, str(e)
201
 
 
202
  def convert_to_wav_if_needed(input_path):
203
+ input_path=str(input_path)
204
+ if input_path.lower().endswith(".wav"):
 
205
  return input_path
206
+ tmp=None
207
+ auto_err=""
 
208
  try:
209
  tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
210
  tmp.close()
211
  AudioSegment.from_file(input_path).export(tmp.name, format="wav")
212
+ if os.path.exists(tmp.name) and os.path.getsize(tmp.name)>MIN_WAV_SIZE:
213
  return tmp.name
214
  else:
215
+ try: os.unlink(tmp.name)
216
+ except Exception: pass
 
 
217
  except Exception:
218
+ auto_err=traceback.format_exc()
219
  try:
220
+ if tmp and os.path.exists(tmp.name): os.unlink(tmp.name)
221
+ except Exception: pass
 
 
 
222
  diag_dir = tempfile.mkdtemp(prefix="dct_diag_")
223
  diag_log = os.path.join(diag_dir, "conversion_diagnostics.txt")
224
+ diagnostics=[]
225
+ for fmt,sr,ch in FFMPEG_CANDIDATES:
226
  out_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
227
  out_wav.close()
228
  success, debug = _ffmpeg_convert(input_path, out_wav.name, fmt, sr, ch)
229
  diagnostics.append(f"TRY fmt={fmt} sr={sr} ch={ch} success={success}\n{debug}\n")
230
  if success:
231
  try:
232
+ with open(diag_log,"w",encoding="utf-8") as fh:
233
  fh.write("pydub auto error:\n")
234
+ fh.write(auto_err+"\n\n")
235
  fh.write("Successful ffmpeg candidate:\n")
236
  fh.write(f"fmt={fmt} sr={sr} ch={ch}\n\n")
237
  fh.write("Diagnostics:\n")
238
  fh.write("\n".join(diagnostics))
239
+ except Exception: pass
 
240
  return out_wav.name
241
  else:
242
  try:
243
+ if os.path.exists(out_wav.name): os.unlink(out_wav.name)
244
+ except Exception: pass
 
 
 
245
  try:
246
+ fp = subprocess.run(["ffprobe","-v","error","-show_format","-show_streams",input_path], capture_output=True, text=True, timeout=10)
247
+ diagnostics.append("FFPROBE:\n"+(fp.stdout.strip() or fp.stderr.strip()))
 
 
 
 
 
248
  except Exception as e:
249
+ diagnostics.append("ffprobe failed: "+str(e))
250
  try:
251
+ with open(input_path,"rb") as fh:
252
  head = fh.read(512)
253
+ diagnostics.append("HEX PREVIEW:\n"+head.hex())
254
  except Exception as e:
255
+ diagnostics.append("could not read head: "+str(e))
 
256
  try:
257
+ with open(diag_log,"w",encoding="utf-8") as fh:
258
  fh.write("pydub auto error:\n")
259
+ fh.write(auto_err+"\n\n")
260
  fh.write("Full diagnostics:\n\n")
261
  fh.write("\n\n".join(diagnostics))
262
  except Exception as e:
263
  raise Exception(f"Conversion failed; diagnostics write error: {e}")
 
264
  raise Exception(f"Could not convert file to WAV. Diagnostics saved to: {diag_log}")
265
 
266
+ # ---------- Whisper model loader ----------
 
 
 
 
267
  def get_whisper_model(name, device=None):
268
+ # caching
269
  if name not in MODEL_CACHE:
270
  print(f"DEBUG: loading whisper model '{name}'", flush=True)
271
+ # whisper.load_model accepts model names like "small","medium", "large-v3" depending on installation
272
  if device:
273
+ try:
274
+ MODEL_CACHE[name] = whisper.load_model(name, device=device)
275
+ except TypeError:
276
+ # fallback signature
277
+ MODEL_CACHE[name] = whisper.load_model(name)
278
  else:
279
  MODEL_CACHE[name] = whisper.load_model(name)
280
  return MODEL_CACHE[name]
281
 
282
+ # ---------- Transcription helpers ----------
283
+ def transcribe_single(audio_path, model_name="small", enable_memory=False, device_choice="auto"):
284
+ logs=[]
285
+ transcript_text=""
286
+ download_path=None
287
+ try:
288
+ if not audio_path:
289
+ return None, "No audio provided.", ""
290
+ path = str(audio_path)
291
+ device = None if device_choice=="auto" else device_choice
292
+ model = get_whisper_model(model_name, device=device)
293
+ logs.append(f"Loaded model: {model_name}")
294
+ wav = convert_to_wav_if_needed(path)
295
+ logs.append(f"Converted to WAV: {os.path.basename(wav)}")
296
+ result = model.transcribe(wav)
297
+ text = result.get("text","").strip()
298
+ if enable_memory:
299
+ text = memory_correct_text(text)
300
+ text = postprocess_transcript(text)
301
+ transcript_text = text
302
+ if enable_memory:
303
+ try:
304
+ update_memory_with_transcript(text)
305
+ logs.append("Memory updated.")
306
+ except Exception:
307
+ pass
308
+ # don't delete original user-uploaded wav; delete tmp if created
309
+ if wav and wav != path and os.path.exists(wav):
310
+ try: os.unlink(wav)
311
+ except Exception: pass
312
+ return path, transcript_text, "\n".join(logs)
313
+ except Exception as e:
314
+ tb = traceback.format_exc()
315
+ return None, "", f"Error: {e}\n{tb}"
316
 
317
+ # ---------- ZIP helpers (kept small, re-use earlier pattern) ----------
318
  def extract_zip_list(zip_file, zip_password):
319
  temp_extract_dir = os.path.join(tempfile.gettempdir(), "extracted_audio")
320
  try:
321
  if os.path.exists(temp_extract_dir):
322
+ try: shutil.rmtree(temp_extract_dir)
323
+ except Exception: pass
 
 
324
  os.makedirs(temp_extract_dir, exist_ok=True)
325
+ extracted=[]
326
+ logs=[]
327
  with pyzipper.ZipFile(zip_file, "r") as zf:
328
  if zip_password:
329
+ try: zf.setpassword(zip_password.encode())
330
+ except Exception: logs.append("Warning: failed to set zip password (unexpected).")
331
+ exts = [".mp3",".wav",".aac",".flac",".ogg",".m4a",".dat",".dct"]
 
 
332
  for info in zf.infolist():
333
+ if info.is_dir(): continue
 
334
  _, ext = os.path.splitext(info.filename)
335
  if ext.lower() in exts:
336
  try:
 
338
  except RuntimeError as e:
339
  logs.append(f"Password required/incorrect for {info.filename}: {e}")
340
  continue
 
 
 
341
  except Exception as e:
342
  logs.append(f"Error extracting {info.filename}: {e}")
343
  continue
 
353
  traceback.print_exc()
354
  return [], f"Extraction failed: {e}"
355
 
356
+ # ---------- UI: Beautiful CSS ----------
357
+ CSS = """
358
+ :root{
359
+ --accent:#4f46e5;
360
+ --muted:#6b7280;
361
+ --card:#ffffff;
362
+ --bg:#f7f8fb;
363
+ --glass: rgba(255,255,255,0.65);
364
+ }
365
+ body { background: var(--bg); font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
366
+ .header {
367
+ padding: 18px 24px;
368
+ border-radius: 12px;
369
+ background: linear-gradient(90deg, rgba(79,70,229,0.12), rgba(99,102,241,0.04));
370
+ margin-bottom: 18px;
371
+ display:flex;align-items:center;gap:16px;
372
+ }
373
+ .app-icon {
374
+ width:62px;height:62px;border-radius:12px;background:linear-gradient(135deg,var(--accent),#06b6d4);display:flex;align-items:center;justify-content:center;color:white;font-weight:700;font-size:24px;
375
+ }
376
+ .header-title h1 { margin:0;font-size:20px;}
377
+ .header-sub { color:var(--muted); margin-top:4px;font-size:13px;}
378
+ .card { background:var(--card); border-radius:12px; padding:14px; box-shadow: 0 6px 20px rgba(16,24,40,0.06); }
379
+ .controls { display:flex;flex-direction:column; gap:10px; }
380
+ .btn-primary { background: linear-gradient(90deg,var(--accent),#06b6d4); color:white; border-radius:10px; padding:10px 14px; border:none; cursor:pointer; font-weight:600;}
381
+ .small-muted { color:var(--muted); font-size:13px; }
382
+ .transcript-area { white-space:pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace; background:#0f172a; color:#e6eef8; padding:12px; border-radius:10px; min-height:220px; }
383
+ .audio-player-card { border-radius:10px; padding:8px; background: linear-gradient(180deg, rgba(255,255,255,0.9), rgba(255,255,255,0.7)); box-shadow: 0 6px 18px rgba(15,23,42,0.04); }
384
+ .small-note { color:var(--muted); font-size:12px;}
385
+ .grid { display:grid; grid-template-columns: 1fr 1fr; gap:14px; }
386
+ @media (max-width:900px){ .grid{ grid-template-columns: 1fr; } .header{flex-direction:column;align-items:flex-start;} }
387
+ """
388
+
389
+ # ---------- Build Gradio UI (beautiful + audio transcribe tab) ----------
390
+ with gr.Blocks(title="Whisper Transcriber — Beautiful UI", css=CSS) as demo:
391
+ # Header
392
+ with gr.Row(elem_classes="header"):
393
+ with gr.Column(scale=0):
394
+ gr.HTML("<div class='app-icon'>WT</div>")
395
+ with gr.Column(elem_id="header-title"):
396
+ gr.HTML("<h1 style='margin:0'>Whisper Transcriber</h1>")
397
+ gr.Markdown("<div class='header-sub'>Fast single-file transcribe, batch workflows, memory & fine-tune — now with a beautiful Audio tab ✨</div>")
398
 
399
+ with gr.Tabs():
400
+ # ---- Audio Transcribe Tab ----
401
+ with gr.TabItem("Audio Transcribe 🎙️"):
402
+ with gr.Row():
403
+ # Left: controls
404
+ with gr.Column(scale=1):
405
+ gr.Markdown("### Quick Single Audio Transcribe")
406
+ with gr.Box(elem_classes="card"):
407
+ single_audio = gr.Audio(label="Upload or record audio", type="filepath")
408
+ with gr.Row():
409
+ model_select = gr.Dropdown(choices=["small","medium","large","large-v3","base"], value="large-v3", label="Model")
410
+ device_select = gr.Dropdown(choices=["auto","cpu","cuda"], value="auto", label="Device")
411
+ with gr.Row():
412
+ mem_toggle = gr.Checkbox(label="Enable correction memory", value=False)
413
+ format_button = gr.Dropdown(choices=["Plain","SOAP (medical)"], value="Plain", label="Format")
414
+ transcribe_btn = gr.Button("Transcribe", elem_classes="btn-primary")
415
+ gr.Markdown("<div class='small-note'>Tip: Use <strong>large-v3</strong> for best accuracy if your environment supports it.</div>")
416
+
417
+ # Right: player + transcript
418
+ with gr.Column(scale=1):
419
+ with gr.Box(elem_classes="card audio-player-card"):
420
+ gr.Markdown("### Preview & Player")
421
+ audio_preview = gr.Audio(label="Player", interactive=False)
422
+ gr.HTML("<div style='height:8px'></div>")
423
+ gr.Markdown("<div class='small-muted'>Use the player to preview. Click Transcribe to generate the cleaned transcript on the right.</div>")
424
+
425
+ gr.Markdown("<div style='height:12px'></div>")
426
+ with gr.Box(elem_classes="card"):
427
+ gr.Markdown("### Transcript")
428
+ transcript_out = gr.Textbox(label="", lines=12, interactive=False, elem_classes="transcript-area")
429
+ transcript_logs = gr.Textbox(label="Logs", lines=6, interactive=False)
430
+
431
+ # Transcribe action
432
+ def _do_single_transcribe(audio_file, model_name, device_choice, enable_memory, fmt_choice):
433
+ player_path, transcript, logs = transcribe_single(audio_file, model_name=model_name, enable_memory=enable_memory, device_choice=device_choice)
434
+ if fmt_choice == "SOAP":
435
+ # minimal SOAP formatting if user selected it (basic)
436
+ sentences = re.split(r"(?<=[.?!])\s+", transcript)
437
+ subj = sentences[0] if sentences else ""
438
+ obj = sentences[1] if len(sentences) > 1 else ""
439
+ soap = f"S: {subj}\nO: {obj}\nA: Assessment pending\nP: Plan: follow up"
440
+ transcript = soap
441
+ return player_path, transcript, logs
442
+
443
+ transcribe_btn.click(fn=_do_single_transcribe, inputs=[single_audio, model_select, device_select, mem_toggle, format_button], outputs=[audio_preview, transcript_out, transcript_logs])
444
+
445
+ # ---- Batch Transcribe Tab ----
446
+ with gr.TabItem("Batch Transcribe 📦"):
447
+ with gr.Row():
448
+ with gr.Column(scale=1):
449
+ with gr.Box(elem_classes="card"):
450
+ gr.Markdown("### Batch upload or ZIP")
451
+ batch_files = gr.File(label="Upload audio files (multiple)", file_count="multiple", type="filepath")
452
+ batch_zip = gr.File(label="Or upload ZIP (optional)", file_count="single", type="filepath")
453
+ zip_password = gr.Textbox(label="ZIP password (optional)", placeholder="leave empty if none")
454
+ batch_model = gr.Dropdown(choices=["small","medium","large","large-v3","base"], value="small", label="Model")
455
+ batch_device = gr.Dropdown(choices=["auto","cpu","cuda"], value="auto", label="Device")
456
+ batch_merge = gr.Checkbox(label="Merge all transcripts to .docx", value=True)
457
+ batch_mem = gr.Checkbox(label="Enable memory corrections", value=False)
458
+ batch_extract_btn = gr.Button("Extract ZIP & Show Files")
459
+ batch_extract_logs = gr.Textbox(label="Extraction logs", lines=6, interactive=False)
460
+ batch_select = gr.CheckboxGroup(choices=[], label="Select extracted files (optional)")
461
+ batch_trans_btn = gr.Button("Start Batch Transcription", elem_classes="btn-primary")
462
+ with gr.Column(scale=1):
463
+ with gr.Box(elem_classes="card"):
464
+ gr.Markdown("### Batch Output")
465
+ batch_trans_out = gr.Textbox(label="Transcript (combined)", lines=18, interactive=False)
466
+ batch_logs = gr.Textbox(label="Logs", lines=10, interactive=False)
467
+ batch_download = gr.File(label="Merged .docx (when available)")
468
+
469
+ def _extract_zip_for_ui(zip_file, password):
470
+ if not zip_file:
471
+ return [], "No zip provided."
472
+ zip_path = zip_file.name if hasattr(zip_file, "name") else str(zip_file)
473
+ extracted, logs = extract_zip_list(zip_path, password)
474
+ # return full paths as values, but show basenames in logs
475
+ return extracted, logs + "\n\nFiles:\n" + "\n".join([os.path.basename(p) for p in extracted])
476
+
477
+ batch_extract_btn.click(fn=_extract_zip_for_ui, inputs=[batch_zip, zip_password], outputs=[batch_select, batch_extract_logs])
478
+
479
+ # reuse transcribe_multiple from earlier designs if available; simplified here to call transcribe_single sequentially
480
+ def _batch_transcribe(selected_check, uploaded_files, zip_selected, model_name, device_name, merge_flag, enable_mem):
481
+ # build list
482
+ paths=[]
483
+ if selected_check:
484
+ paths.extend(selected_check)
485
+ if uploaded_files:
486
+ if isinstance(uploaded_files, list):
487
+ paths.extend([str(x) for x in uploaded_files])
488
+ else:
489
+ paths.append(str(uploaded_files))
490
+ if not paths and zip_selected:
491
+ paths.extend(zip_selected if isinstance(zip_selected, list) else [zip_selected])
492
+ logs=[]
493
+ transcripts=[]
494
+ out_doc=None
495
+ for p in paths:
496
+ try:
497
+ _, txt, lg = transcribe_single(p, model_name=model_name, enable_memory=enable_mem, device_choice=device_name)
498
+ logs.append(lg)
499
+ transcripts.append(f"FILE: {os.path.basename(str(p))}\n{txt}\n")
500
+ except Exception as e:
501
+ logs.append(f"Failed {p}: {e}")
502
+ combined = "\n\n".join(transcripts)
503
+ if merge_flag:
504
+ try:
505
+ out_doc = save_as_word(combined)
506
+ logs.append(f"Merged saved: {out_doc}")
507
+ except Exception as e:
508
+ logs.append(f"Merge failed: {e}")
509
+ return combined, "\n".join(logs), out_doc
510
 
511
+ batch_trans_btn.click(fn=_batch_transcribe, inputs=[batch_select, batch_files, batch_select, batch_model, batch_device, batch_merge, batch_mem], outputs=[batch_trans_out, batch_logs, batch_download])
 
 
 
512
 
513
+ # ---- Memory Tab ----
514
+ with gr.TabItem("Memory 🧠"):
515
+ with gr.Row():
516
+ with gr.Column(scale=1):
517
+ with gr.Box(elem_classes="card"):
518
+ gr.Markdown("### Correction Memory")
519
+ mem_upload = gr.File(label="Import memory (JSON or text)", file_count="single", type="filepath")
520
+ mem_import_btn = gr.Button("Import Memory")
521
+ mem_add_text = gr.Textbox(label="Add word / phrase", placeholder="Type and click Add")
522
+ mem_add_btn = gr.Button("Add to Memory")
523
+ mem_clear_btn = gr.Button("Clear Memory")
524
+ mem_view_btn = gr.Button("View Memory")
525
+ mem_status = gr.Textbox(label="Memory status / preview", lines=12, interactive=False)
526
+
527
+ def _import_mem(uploaded):
528
+ if not uploaded:
529
+ return "No file provided."
530
+ path = uploaded.name if hasattr(uploaded,"name") else str(uploaded)
531
  try:
532
+ with open(path,"r",encoding="utf-8") as fh:
533
+ raw = fh.read()
534
+ parsed = None
 
 
 
 
 
 
 
 
 
 
 
 
535
  try:
536
+ parsed = json.loads(raw)
 
 
537
  except Exception:
538
+ parsed=None
539
+ if isinstance(parsed, dict):
540
+ with MEMORY_LOCK:
541
+ for k,v in parsed.get("words",{}).items():
542
+ memory["words"][k.lower()] = memory["words"].get(k.lower(),0)+int(v)
543
+ for k,v in parsed.get("phrases",{}).items():
544
+ memory["phrases"][k] = memory["phrases"].get(k,0)+int(v)
545
+ save_memory(memory)
546
+ return f"Imported JSON memory (words={len(parsed.get('words',{}))}, phrases={len(parsed.get('phrases',{}))})."
547
+ # fallback parse lines
548
+ lines=[l.strip() for l in raw.splitlines() if l.strip()]
549
+ added=0
550
+ with MEMORY_LOCK:
551
+ for line in lines:
552
+ if "," in line:
553
+ k,c = line.split(",",1)
554
+ try: cnt=int(c)
555
+ except: cnt=1
556
+ memory["words"][k.lower()]=memory["words"].get(k.lower(),0)+cnt
557
+ else:
558
+ memory["words"][line.lower()]=memory["words"].get(line.lower(),0)+1
559
+ added+=1
560
+ save_memory(memory)
561
+ return f"Imported {added} entries."
562
+ except Exception as e:
563
+ return f"Import failed: {e}"
564
+
565
+ def _add_mem(entry):
566
+ if not entry or not entry.strip():
567
+ return "No entry provided."
568
+ e=entry.strip()
569
+ with MEMORY_LOCK:
570
+ if len(e.split())<=3:
571
+ memory["words"][e.lower()]=memory["words"].get(e.lower(),0)+1
572
+ save_memory(memory)
573
+ return f"Added word: {e.lower()}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  else:
575
+ memory["phrases"][e]=memory["phrases"].get(e,0)+1
576
+ save_memory(memory)
577
+ return f"Added phrase: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
 
579
+ def _clear_mem():
580
+ global memory
581
+ with MEMORY_LOCK:
582
+ memory={"words":{}, "phrases":{}}
583
+ save_memory(memory)
584
+ return "Memory cleared."
585
+
586
+ def _view_mem():
587
+ w=memory.get("words",{})
588
+ p=memory.get("phrases",{})
589
+ out=[]
590
+ out.append("WORDS (top 30):")
591
+ for k,v in sorted(w.items(), key=lambda kv:-kv[1])[:30]:
592
+ out.append(f"{k}: {v}")
593
+ out.append("")
594
+ out.append("PHRASES (top 20):")
595
+ for k,v in sorted(p.items(), key=lambda kv:-kv[1])[:20]:
596
+ out.append(f"{k}: {v}")
597
+ return "\n".join(out)
598
+
599
+ mem_import_btn.click(fn=_import_mem, inputs=[mem_upload], outputs=[mem_status])
600
+ mem_add_btn.click(fn=_add_mem, inputs=[mem_add_text], outputs=[mem_status])
601
+ mem_clear_btn.click(fn=_clear_mem, inputs=[], outputs=[mem_status])
602
+ mem_view_btn.click(fn=_view_mem, inputs=[], outputs=[mem_status])
603
+
604
+ # ---- Fine-tune Tab (compact) ----
605
+ with gr.TabItem("Fine-tune ⚙️"):
606
  with gr.Row():
607
  with gr.Column(scale=1):
608
+ with gr.Box(elem_classes="card"):
609
+ gr.Markdown("### Prepare & Launch Fine-tune")
610
+ ft_upload = gr.File(label="Upload dataset ZIP (optional)", file_count="single", type="filepath")
611
+ ft_include_old = gr.Checkbox(label="Include old audio+transcript folder", value=False)
612
+ ft_old = gr.File(label="Old files folder (optional)", file_count="single", type="filepath")
613
+ ft_prepare_btn = gr.Button("Prepare dataset")
614
+ ft_manifest_box = gr.Textbox(label="Prepare status / manifest", lines=4, interactive=False)
615
+ ft_model = gr.Dropdown(choices=["small","base","medium","large","large-v3"], value="small", label="Base model")
616
+ ft_epochs = gr.Slider(minimum=1, maximum=100, value=3, step=1, label="Epochs")
617
+ ft_start = gr.Button("Start (uses fine_tune.py placeholder)")
618
+ ft_stop = gr.Button("Stop")
619
+ ft_status = gr.Textbox(label="Start/Stop status", lines=4, interactive=False)
620
+ ft_logs = gr.Textbox(label="Training logs (tail)", lines=8, interactive=False)
621
+
622
+ # Minimal prepare function reusing earlier code (kept compact)
623
+ def _prepare_finetune(uploaded_zip, include_old, old_dir):
624
+ dst=os.path.join(FINETUNE_WORKDIR,"data")
625
+ try:
626
+ if os.path.exists(dst): shutil.rmtree(dst)
627
+ os.makedirs(dst, exist_ok=True)
628
+ except Exception as e:
629
+ return f"Workdir creation failed: {e}"
630
+ # extract uploaded zip if provided
631
+ if uploaded_zip:
632
+ zpath = uploaded_zip.name if hasattr(uploaded_zip,"name") else str(uploaded_zip)
633
+ if os.path.isfile(zpath) and zpath.lower().endswith(".zip"):
634
+ try:
635
+ with pyzipper.ZipFile(zpath,"r") as zf:
636
+ zf.extractall(dst)
637
+ except Exception as e:
638
+ return f"ZIP extract failed: {e}"
639
+ # include old files if requested
640
+ old_msgs=""
641
+ if include_old and old_dir:
642
+ old_path = old_dir.name if hasattr(old_dir,"name") else str(old_dir)
643
+ if os.path.isdir(old_path):
644
+ copied=0; msgs=[]
645
+ for root,_,files in os.walk(old_path):
646
+ for f in files:
647
+ if f.lower().endswith((".wav",".mp3",".flac",".m4a",".ogg")):
648
+ base=os.path.splitext(f)[0]
649
+ src=os.path.join(root,f)
650
+ rel = os.path.relpath(root, old_path)
651
+ tgt_dir = os.path.join(dst, rel)
652
+ os.makedirs(tgt_dir, exist_ok=True)
653
+ shutil.copy2(src, os.path.join(tgt_dir,f))
654
+ ttxt = os.path.join(root, base+".txt")
655
+ if os.path.exists(ttxt):
656
+ shutil.copy2(ttxt, os.path.join(tgt_dir, base+".txt"))
657
+ copied+=1
658
+ old_msgs = f"\nCopied ~{copied} old audio files."
659
+ else:
660
+ old_msgs = "\nOld-files path not a directory."
661
+ # simple manifest builder: audio \t transcript(empty or from .txt)
662
+ manifest = os.path.join(FINETUNE_WORKDIR,"manifest.tsv")
663
+ auds=[]
664
+ for root,_,files in os.walk(dst):
665
+ for f in files:
666
+ if f.lower().endswith((".wav",".mp3",".flac",".m4a",".ogg")):
667
+ auds.append(os.path.join(root,f))
668
+ if not auds:
669
+ return "No audio files found in prepared dataset."+old_msgs
670
+ lines=[]
671
+ missing=0
672
+ for a in auds:
673
+ base=os.path.splitext(a)[0]; tfile=base+".txt"
674
+ txt=""
675
+ if os.path.exists(tfile):
676
+ try:
677
+ with open(tfile,"r",encoding="utf-8") as fh: txt=fh.read().strip().replace("\n"," ")
678
+ except: txt=""
679
+ else:
680
+ missing+=1
681
+ lines.append(f"{a}\t{txt}")
682
+ try:
683
+ with open(manifest,"w",encoding="utf-8") as fh: fh.write("\n".join(lines))
684
+ except Exception as e:
685
+ return f"Manifest write failed: {e}"
686
+ out = f"Prepared manifest: {manifest}{old_msgs}"
687
+ if missing>0: out += f"\nWarning: {missing} audio files missing transcripts."
688
+ return out
689
+
690
+ def _start_ft(dummy, model, epochs):
691
+ # placeholder: you must supply fine_tune.py in root or change this behavior
692
+ manifest_guess = os.path.join(FINETUNE_WORKDIR,"manifest.tsv")
693
  if not os.path.exists(manifest_guess):
694
+ return "Manifest not found. Prepare dataset first."
695
+ try:
696
+ cmd = [sys.executable, "fine_tune.py", "--manifest", manifest_guess, "--base_model", model, "--epochs", str(int(epochs))]
697
+ # run in background (simple)
698
+ p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
699
+ return f"Started fine-tune (PID {p.pid}). Make sure your training script exists."
700
+ except Exception as e:
701
+ return f"Failed to start fine-tune: {e}"
702
+
703
+ ft_prepare.click(fn=_prepare_finetune, inputs=[ft_upload, ft_include_old, ft_old], outputs=[ft_manifest_box])
704
+ ft_start.click(fn=_start_ft, inputs=[ft_manifest_box, ft_model, ft_epochs], outputs=[ft_status])
705
+ ft_stop.click(fn=lambda: "Stop not implemented in placeholder", inputs=[], outputs=[ft_status])
706
+
707
+ # ---- Settings Tab ----
708
+ with gr.TabItem("Settings ⚙️"):
709
+ with gr.Row():
710
+ with gr.Column():
711
+ gr.Markdown("### Runtime & tips")
712
+ gr.Markdown("- Use large-v3 only if your `whisper` package supports it.")
713
+ gr.Markdown("- Extraction writes to system temp `extracted_audio`. Re-extracting overwrites it.")
714
+ gr.Markdown("- Keep default ZIP password empty for safety.")
715
+ with gr.Column():
716
+ gr.Markdown("### Diagnostics")
717
+ diag_btn = gr.Button("Show memory summary")
718
+ diag_out = gr.Textbox(label="Diagnostics", lines=12, interactive=False)
719
+ diag_btn.click(fn=lambda: (lambda: view_memory())(), inputs=[], outputs=[diag_out])
720
+
721
+ # Launch
722
  if __name__ == "__main__":
723
  port = int(os.environ.get("PORT", 7860))
724
  print("DEBUG: launching Gradio on port", port, flush=True)
725
+ demo.queue().launch(server_name="0.0.0.0", server_port=port)