File size: 2,423 Bytes
85b485a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"""Video helpers: audio extraction, duration probing, single-frame grabbing.

All heavy lifting is delegated to ffmpeg/ffprobe (already on PATH). ffmpeg's
``-ss`` before ``-i`` is both fast and frame-accurate in modern builds, which we
rely on for precise frame extraction at a given timestamp.
"""
from __future__ import annotations

import subprocess
from pathlib import Path

from . import config


class FFmpegError(RuntimeError):
    """Raised when an ffmpeg/ffprobe subprocess fails."""


def _run(cmd: list[str]) -> subprocess.CompletedProcess:
    proc = subprocess.run(cmd, capture_output=True, text=True)
    if proc.returncode != 0:
        raise FFmpegError(
            f"Command failed ({proc.returncode}): {' '.join(cmd)}\n{proc.stderr.strip()}"
        )
    return proc


def get_duration(video_path: str | Path) -> float:
    """Return the media duration in seconds (0.0 if it cannot be determined)."""
    try:
        proc = _run(
            [
                config.FFPROBE_BIN,
                "-v", "error",
                "-show_entries", "format=duration",
                "-of", "default=noprint_wrappers=1:nokey=1",
                str(video_path),
            ]
        )
        return float(proc.stdout.strip())
    except (FFmpegError, ValueError):
        return 0.0


def extract_audio(video_path: str | Path, out_wav: str | Path) -> Path:
    """Extract a 16 kHz mono WAV (the format faster-whisper expects)."""
    out_wav = Path(out_wav)
    out_wav.parent.mkdir(parents=True, exist_ok=True)
    _run(
        [
            config.FFMPEG_BIN,
            "-y",
            "-i", str(video_path),
            "-vn",            # drop video
            "-ac", "1",       # mono
            "-ar", "16000",   # 16 kHz
            "-f", "wav",
            str(out_wav),
        ]
    )
    return out_wav


def extract_frame(video_path: str | Path, timestamp: float, out_png: str | Path) -> Path:
    """Save a single frame at ``timestamp`` seconds as a PNG.

    ``-ss`` is placed before ``-i`` for fast, frame-accurate seeking.
    """
    out_png = Path(out_png)
    out_png.parent.mkdir(parents=True, exist_ok=True)
    _run(
        [
            config.FFMPEG_BIN,
            "-y",
            "-ss", f"{max(timestamp, 0.0):.3f}",
            "-i", str(video_path),
            "-frames:v", "1",
            "-q:v", "2",
            str(out_png),
        ]
    )
    return out_png