#!/usr/bin/env python3 import os import shutil import subprocess from pathlib import Path import wave from faster_whisper import WhisperModel os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" CHUNK_DURATION_SEC = 300 def extract_audio_and_chunk(video_path: str, output_dir: Path) -> list[tuple[Path, float]]: """Returns list of (chunk_path, start_offset_seconds)""" print("šŸš€ Extracting and chunking audio with FFmpeg...") output_dir.mkdir(parents=True, exist_ok=True) chunk_pattern = str(output_dir / "chunk_%03d.wav") command = [ "ffmpeg", "-y", "-i", video_path, "-vn", "-ac", "1", "-ar", "16000", "-acodec", "pcm_s16le", "-f", "segment", "-segment_time", str(CHUNK_DURATION_SEC), "-reset_timestamps", "0", # keep each chunk starting at 0 chunk_pattern ] result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True) if result.returncode != 0: print(f"āŒ FFmpeg Error:\n{result.stderr}") raise RuntimeError("FFmpeg failed") chunks = sorted(output_dir.glob("chunk_*.wav")) # FIX: Calculate real offset from actual file durations, not idx*300 offsets = [] current_offset = 0.0 for chunk_path in chunks: offsets.append((chunk_path, current_offset)) with wave.open(str(chunk_path), 'rb') as wf: duration = wf.getnframes() / wf.getframerate() current_offset += duration return offsets 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 transcribe_to_srt(video_path: str, force: bool = False): if not hasattr(transcribe_to_srt, "model"): print("šŸ”§ Initializing C++ Engine...") transcribe_to_srt.model = WhisperModel( "distil-small.en", device="cpu", compute_type="int8", cpu_threads=4, # 4 = physical cores on E3-1280 v5 = fastest num_workers=1 ) print("āœ… Model loaded.") 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() try: audio_chunks = extract_audio_and_chunk(str(video_path_obj), temp_dir) print(f"šŸ“¦ {len(audio_chunks)} chunks.") print("šŸŽ™ Transcribing...") global_idx = 1 with open(srt_path, "w", encoding="utf-8") as srt_file: for chunk_idx, (chunk_path, time_offset) in enumerate(audio_chunks): print(f"\nā³ Chunk {chunk_idx+1}/{len(audio_chunks)} offset={time_offset:.1f}s") segments, info = transcribe_to_srt.model.transcribe( str(chunk_path), language="en", beam_size=1, vad_filter=True, vad_parameters=dict(min_silence_duration_ms=500), condition_on_previous_text=False # important for chunked mode ) for seg in segments: actual_start = seg.start + time_offset actual_end = seg.end + time_offset # Use comma for SRT, not dot 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 chunk_path.unlink() print(f"\nāœ… SRT saved: {srt_path}") except Exception as e: print(f"\nāŒ Error: {e}") import traceback; traceback.print_exc() finally: if temp_dir.exists(): shutil.rmtree(temp_dir) if __name__ == "__main__": target_video = "download/videos/2813112936/2813112936.mp4" if not os.path.exists(target_video): print(f"āŒ Not found: {target_video}") else: transcribe_to_srt(target_video, force=True)