created remote transcriber

This commit is contained in:
SumGuyV5 committed 2026-09-24 19:53:05 +00:00
1 parent 856ab439f7
commit 38e0cc6764
10 files changed
+718 -68

No files matched your search

+56 -56
View File
@@ -3,117 +3,117 @@ import os
import shutil
import subprocess
from pathlib import Path
import wave
from faster_whisper import WhisperModel
from faster_whisper.utils import format_timestamp
# Prevent OpenMP thread conflicts from crashing the script
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
# 5 minutes per chunk (300 seconds) keeps RAM usage low and stable
CHUNK_DURATION_SEC = 300
def extract_audio_and_chunk(video_path: str, output_dir: Path) -> list:
"""Extracts and splits audio into 5-minute chunks using a single FFmpeg pass."""
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)
# Segment format output: chunk_000.wav, chunk_001.wav, etc.
chunk_pattern = str(output_dir / "chunk_%03d.wav")
command = [
"ffmpeg", "-y", "-i", video_path,
"-vn", "-ac", "1", "-ar", "16000",
"-acodec", "pcm_s16le", "-sn", "-map_chapters", "-1",
"-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 Output:\n{result.stderr}")
raise RuntimeError("FFmpeg extraction and chunking failed.")
# Return sorted list of generated chunk files
return sorted(list(output_dir.glob("chunk_*.wav")))
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(
"base",
device="cpu",
compute_type="int8",
cpu_threads=0, # Let CTranslate2 auto-detect safe core counts
num_workers=1
"distil-small.en",
device="cpu",
compute_type="int8",
cpu_threads=4, # 4 = physical cores on E3-1280 v5 = fastest
num_workers=1
)
print("✅ C++ Model loaded successfully.")
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():
if not force:
print(f"❌ Error: Transcription file already exists: {srt_path}")
return
else:
srt_path.unlink()
srt_path.unlink()
try:
# Step 1: Split audio into bite-sized pieces
audio_chunks = extract_audio_and_chunk(str(video_path_obj), temp_dir)
if not audio_chunks:
print("❌ Error: No audio chunks were generated.")
return
print(f"📦 {len(audio_chunks)} chunks.")
print("🎙 Transcribing...")
print(f"📦 Successfully split audio into {len(audio_chunks)} chunks.")
print("🎙️ Starting safe chunk-by-chunk transcription...")
global_segment_index = 1
global_idx = 1
with open(srt_path, "w", encoding="utf-8") as srt_file:
for chunk_idx, chunk_path in enumerate(audio_chunks):
# Calculate the time offset for the current chunk
time_offset = chunk_idx * CHUNK_DURATION_SEC
print(f"\n⏳ Processing chunk {chunk_idx + 1}/{len(audio_chunks)} ({chunk_path.name})...")
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_generator, info = transcribe_to_srt.model.transcribe(
segments, info = transcribe_to_srt.model.transcribe(
str(chunk_path),
language="en",
beam_size=1,
vad_filter=True,
temperature=0.0
vad_parameters=dict(min_silence_duration_ms=500),
condition_on_previous_text=False # important for chunked mode
)
# Consume chunk generator and shift timestamps instantly
for segment in segments_generator:
# Shift timestamps relative to the original video timeline
actual_start = segment.start + time_offset
actual_end = segment.end + time_offset
for seg in segments:
actual_start = seg.start + time_offset
actual_end = seg.end + time_offset
start_str = format_timestamp(actual_start, always_include_hours=True)
end_str = format_timestamp(actual_end, always_include_hours=True)
# 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
srt_file.write(f"{global_segment_index}\n{start_str} --> {end_str}\n{segment.text.strip()}\n\n")
global_segment_index += 1
# Free up space as we go by deleting the processed chunk
chunk_path.unlink()
print(f"\n✅ All chunks combined! SRT subtitle file saved in: {srt_path}")
print(f"\n✅ SRT saved: {srt_path}")
except Exception as e:
print(f"\n❌ Execution Error: {e}")
print(f"\n❌ Error: {e}")
import traceback; traceback.print_exc()
finally:
# Clean up the temporary folder entirely
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"❌ System Error: Target video file does not exist at path: {target_video}")
print(f"❌ Not found: {target_video}")
else:
transcribe_to_srt(target_video, force=True)
transcribe_to_srt(target_video, force=True)