OmniSub / src /omnisub /scene_context.py
STBack23's picture
Upload OmniSub source
b5c8312 verified
Raw
History Blame Contribute Delete
4.79 kB
"""Trích ngữ cảnh đa phương thức cho từng cảnh: frame video + audio đoạn cảnh.
Dùng ffmpeg/ffprobe (gọi qua subprocess). Trên Colab L4 đã có sẵn ffmpeg.
Tất cả file tạm ghi vào `work_dir` để backend Omni nạp ở Bước 3 & 4.
"""
from __future__ import annotations
import shutil
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional
from .scenes import Scene
def _require(tool: str) -> str:
path = shutil.which(tool)
if not path:
raise RuntimeError(
f"Không tìm thấy `{tool}` trong PATH. Cần ffmpeg để trích frame/audio."
)
return path
def extract_audio(
video: str | Path,
out_path: str | Path,
*,
start: Optional[float] = None,
end: Optional[float] = None,
sample_rate: int = 16000,
) -> Path:
"""Trích audio mono 16 kHz (mặc định) cho [start, end]. Trả về path WAV."""
ffmpeg = _require("ffmpeg")
out = Path(out_path)
out.parent.mkdir(parents=True, exist_ok=True)
cmd = [ffmpeg, "-y", "-loglevel", "error"]
if start is not None:
cmd += ["-ss", f"{start:.3f}"]
if end is not None and start is not None:
cmd += ["-t", f"{max(0.0, end - start):.3f}"]
cmd += [
"-i", str(video),
"-vn",
"-ac", "1",
"-ar", str(sample_rate),
"-c:a", "pcm_s16le",
str(out),
]
subprocess.run(cmd, check=True)
return out
def extract_frame(
video: str | Path,
out_path: str | Path,
*,
timestamp: float,
width: Optional[int] = 768,
) -> Path:
"""Trích 1 frame tại `timestamp` (giây). Trả về path ảnh JPG."""
ffmpeg = _require("ffmpeg")
out = Path(out_path)
out.parent.mkdir(parents=True, exist_ok=True)
vf = f"scale={width}:-2" if width else "scale=iw:ih"
cmd = [
ffmpeg, "-y", "-loglevel", "error",
"-ss", f"{timestamp:.3f}",
"-i", str(video),
"-frames:v", "1",
"-vf", vf,
str(out),
]
subprocess.run(cmd, check=True)
return out
@dataclass
class SceneContext:
"""Tài nguyên đa phương thức đã trích cho một cảnh."""
scene_id: int
audio_path: Optional[Path] = None
frame_paths: List[Path] = field(default_factory=list)
def build_scene_context(
video: str | Path,
scene: Scene,
work_dir: str | Path,
*,
n_frames: int = 2,
sample_rate: int = 16000,
frame_width: int = 768,
) -> SceneContext:
"""Trích audio toàn cảnh + `n_frames` frame rải đều trong cảnh."""
work = Path(work_dir)
work.mkdir(parents=True, exist_ok=True)
audio_path = extract_audio(
video,
work / f"scene_{scene.scene_id:04d}.wav",
start=scene.start,
end=scene.end,
sample_rate=sample_rate,
)
frames: List[Path] = []
dur = max(0.001, scene.duration)
for i in range(max(1, n_frames)):
# rải đều, tránh đúng biên cắt cảnh
frac = (i + 1) / (n_frames + 1)
ts = scene.start + frac * dur
frames.append(
extract_frame(
video,
work / f"scene_{scene.scene_id:04d}_f{i}.jpg",
timestamp=ts,
width=frame_width,
)
)
return SceneContext(scene_id=scene.scene_id, audio_path=audio_path, frame_paths=frames)
def extract_speaker_clips(
video: str | Path,
turns: List["SpeakerTurn"], # type: ignore[name-defined]
work_dir: str | Path,
*,
clips_per_speaker: int = 5,
max_seconds_per_clip: float = 8.0,
sample_rate: int = 16000,
) -> dict[str, List[Path]]:
"""Trích các clip audio đại diện cho từng speaker (cho Bước 3 — hồ sơ giọng).
Chọn các turn dài nhất của mỗi speaker, cắt ≤ `max_seconds_per_clip`.
"""
from collections import defaultdict
work = Path(work_dir)
work.mkdir(parents=True, exist_ok=True)
by_speaker: dict[str, list] = defaultdict(list)
for t in turns:
by_speaker[t.speaker].append(t)
result: dict[str, List[Path]] = {}
for speaker, sp_turns in by_speaker.items():
sp_turns = sorted(sp_turns, key=lambda x: x.duration, reverse=True)
clips: List[Path] = []
for i, t in enumerate(sp_turns[:clips_per_speaker]):
end = min(t.end, t.start + max_seconds_per_clip)
clips.append(
extract_audio(
video,
work / f"{speaker}_clip{i}.wav",
start=t.start,
end=end,
sample_rate=sample_rate,
)
)
result[speaker] = clips
return result