#!/usr/bin/env python3 #!/usr/bin/env python3 import threading # --- HOTFIX for Python 3.13.5 _DeleteDummyThreadOnDel spam --- # The CPython fix in 3.13.4 didn't fully kill it for debugpy/asyncio cases if hasattr(threading, '_DeleteDummyThreadOnDel'): threading._DeleteDummyThreadOnDel.__del__ = lambda self: None import os import shutil # ... rest of your imports import os import shutil import subprocess import json from pathlib import Path import wave import requests from concurrent.futures import ThreadPoolExecutor, as_completed os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" CHUNK_DURATION_SEC = 600 # target chunk size OVERLAP_SEC = 8 # overlap to prevent cut words VAD_SEARCH_WINDOW = 15 # look for silence within 15s of target WORKER_URL = "http://192.168.5.55:8000/transcribe" MAX_WORKERS = 4 # perfect for 4070 Ti Super 16GB def get_duration(video_path: str) -> float: cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "json", video_path] result = subprocess.run(cmd, capture_output=True, text=True) return float(json.loads(result.stdout)["format"]["duration"]) def find_silence_near(video_path: str, target_time: float) -> float: """Find nearest silence within VAD_SEARCH_WINDOW of target_time""" start_search = max(0, target_time - VAD_SEARCH_WINDOW/2) cmd = [ "ffmpeg", "-ss", str(start_search), "-t", str(VAD_SEARCH_WINDOW), "-i", video_path, "-af", "silencedetect=noise=-30dB:d=0.5", "-f", "null", "-" ] result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True) # Parse last silence end before target best = target_time for line in result.stderr.splitlines(): if "silence_end:" in line: try: t = float(line.split("silence_end:")[1].split("|")[0].strip()) abs_t = start_search + t if abs(abs_t - target_time) < abs(best - target_time): best = abs_t except: pass return best def extract_audio_and_chunk_smart(video_path: str, output_dir: Path): print(f"šŸš€ Smart chunking: target {CHUNK_DURATION_SEC}s + {OVERLAP_SEC}s overlap") output_dir.mkdir(parents=True, exist_ok=True) total_dur = get_duration(video_path) print(f"šŸ“ Total duration: {total_dur/60:.1f} min") chunks = [] start = 0.0 idx = 0 while start < total_dur: # Find smart cut point target_end = start + CHUNK_DURATION_SEC if target_end >= total_dur: end = total_dur else: end = find_silence_near(video_path, target_end) # clamp end = max(start + CHUNK_DURATION_SEC * 0.7, min(end, start + CHUNK_DURATION_SEC * 1.3)) chunk_path = output_dir / f"chunk_{idx:03d}.wav" # Extract with overlap for next chunk extract_end = end + (OVERLAP_SEC if end < total_dur else 0) cmd = [ "ffmpeg", "-y", "-ss", str(start), "-to", str(extract_end), "-i", video_path, "-vn", "-ac", "1", "-ar", "16000", "-acodec", "pcm_s16le", str(chunk_path) ] subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) chunks.append((chunk_path, start, end)) # path, true_start, true_end (without overlap) print(f" Chunk {idx}: {start:.1f}s -> {end:.1f}s (file to {extract_end:.1f}s)") if end >= total_dur: break start = end - OVERLAP_SEC/2 # start next chunk with half overlap idx += 1 return chunks def format_srt_time(seconds: float): h = int(seconds // 3600) m = int((seconds % 3600) // 60) s = int(seconds % 60) ms = int((seconds % 1) * 1000) return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}" def send_chunk_for_transcription(args): chunk_path, true_start, true_end, chunk_idx = args try: with open(chunk_path, 'rb') as f: files = {'file': (chunk_path.name, f, 'audio/wav')} resp = requests.post(WORKER_URL, files=files, timeout=1200) resp.raise_for_status() data = resp.json() # Filter to only keep segments within true window (dedupe overlap) filtered = [] for seg in data['segments']: abs_start = true_start + seg['start'] # keep if it starts before the true_end (allow small spill) if abs_start < true_end + OVERLAP_SEC/2: # if it's in overlap zone of NEXT chunk, only keep first half if abs_start < true_end - OVERLAP_SEC/2 or chunk_idx == 0: filtered.append(seg) elif seg['start'] < (true_end - true_start) - OVERLAP_SEC/2 + 1.0: filtered.append(seg) return chunk_idx, true_start, filtered, None except Exception as e: return chunk_idx, true_start, None, str(e) def transcribe_to_srt_remote(video_path: str, force: bool = False): video_path_obj = Path(video_path) srt_path = video_path_obj.with_suffix(".srt") temp_dir = video_path_obj.parent / f"temp_chunks_{video_path_obj.stem}" if srt_path.exists() and not force: print(f"āŒ SRT already exists: {srt_path}") return if srt_path.exists(): srt_path.unlink() audio_chunks = extract_audio_and_chunk_smart(str(video_path_obj), temp_dir) jobs = [(path, s, e, idx) for idx, (path, s, e) in enumerate(audio_chunks)] results = [] executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) try: futures = {executor.submit(send_chunk_for_transcription, job): job for job in jobs} for future in as_completed(futures): idx, offset, segments, error = future.result() if error: print(f"āŒ Chunk {idx} failed: {error}") else: print(f"āœ… Chunk {idx} done: {len(segments)} segs (offset {offset:.1f}s)") results.append((idx, offset, segments)) finally: executor.shutdown(wait=True) results.sort(key=lambda x: x[0]) print("šŸ“ Writing final SRT with smart overlap dedupe...") global_idx = 1 with open(srt_path, "w", encoding="utf-8") as srt_file: for _, time_offset, segments in results: for seg in segments: actual_start = seg['start'] + time_offset actual_end = seg['end'] + time_offset srt_file.write(f"{global_idx}\n{format_srt_time(actual_start)} --> {format_srt_time(actual_end)}\n{seg['text'].strip()}\n\n") global_idx += 1 print(f"\nāœ… SRT saved: {srt_path} - {global_idx-1} lines") if temp_dir.exists(): shutil.rmtree(temp_dir) if __name__ == "__main__": target_video = "download/videos/2813112936/2813112936.mp4" transcribe_to_srt_remote(target_video, force=True)