update minio port
This commit is contained in:
276
backend/app/algo_host/transcode.py
Normal file
276
backend/app/algo_host/transcode.py
Normal file
@@ -0,0 +1,276 @@
|
||||
"""FFmpeg/ffprobe helpers for batch uploads and browser-ready MP4 (infrastructure only)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
VISUALIZATION_MAX_WIDTH = 1920
|
||||
|
||||
|
||||
def visualization_ffmpeg_scale_filter() -> str:
|
||||
return f"scale='min({VISUALIZATION_MAX_WIDTH},iw)':-2"
|
||||
|
||||
|
||||
def browser_transcode_tmp_path(output_path: Path) -> Path:
|
||||
return output_path.with_name(f"{output_path.stem}.part{output_path.suffix}")
|
||||
|
||||
|
||||
def is_readable_mp4(path: Path) -> bool:
|
||||
ffprobe = shutil.which("ffprobe")
|
||||
if ffprobe is None or not path.is_file() or path.stat().st_size < 4096:
|
||||
return False
|
||||
proc = subprocess.run(
|
||||
[
|
||||
ffprobe,
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=codec_name",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
str(path),
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return proc.returncode == 0 and bool((proc.stdout or "").strip())
|
||||
|
||||
|
||||
def ffprobe_fields(path: Path, entries: str) -> dict[str, str]:
|
||||
ffprobe = shutil.which("ffprobe")
|
||||
if ffprobe is None or not path.is_file():
|
||||
return {}
|
||||
proc = subprocess.run(
|
||||
[
|
||||
ffprobe,
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
entries,
|
||||
"-of",
|
||||
"default=noprint_wrappers=1",
|
||||
str(path),
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
return {}
|
||||
fields: dict[str, str] = {}
|
||||
for line in proc.stdout.splitlines():
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
fields[key.strip().lower()] = value.strip().lower()
|
||||
return fields
|
||||
|
||||
|
||||
def ffprobe_container_format(path: Path) -> str:
|
||||
ffprobe = shutil.which("ffprobe")
|
||||
if ffprobe is None or not path.is_file():
|
||||
return ""
|
||||
proc = subprocess.run(
|
||||
[
|
||||
ffprobe,
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=format_name",
|
||||
"-of",
|
||||
"default=noprint_wrappers=1:nokey=1",
|
||||
str(path),
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
return ""
|
||||
return (proc.stdout or "").strip().lower()
|
||||
|
||||
|
||||
def is_browser_compatible_mp4(path: Path) -> bool:
|
||||
fields = ffprobe_fields(path, "stream=codec_name,pix_fmt")
|
||||
return fields.get("codec_name") == "h264" and fields.get("pix_fmt") in {"yuv420p", "yuvj420p"}
|
||||
|
||||
|
||||
def batch_input_needs_normalize(path: Path) -> bool:
|
||||
if not is_readable_mp4(path):
|
||||
return True
|
||||
if not is_browser_compatible_mp4(path):
|
||||
return True
|
||||
container = ffprobe_container_format(path)
|
||||
if container and "mpeg" in container:
|
||||
return True
|
||||
fields = ffprobe_fields(path, "stream=codec_name,width,height")
|
||||
try:
|
||||
width = int(fields.get("width") or "0")
|
||||
except ValueError:
|
||||
width = 0
|
||||
return width > 1920
|
||||
|
||||
|
||||
def normalize_batch_input_video(source_path: Path, output_path: Path) -> bool:
|
||||
ffmpeg = shutil.which("ffmpeg")
|
||||
if ffmpeg is None or not source_path.is_file():
|
||||
return False
|
||||
if not is_readable_mp4(source_path):
|
||||
logger.warning("skip batch input normalize: unreadable source {}", source_path)
|
||||
return False
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = browser_transcode_tmp_path(output_path)
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
logger.info("ffmpeg batch input normalize starting: {} -> {}", source_path, output_path)
|
||||
proc = subprocess.run(
|
||||
[
|
||||
ffmpeg,
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-fflags",
|
||||
"+genpts",
|
||||
"-i",
|
||||
str(source_path),
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-an",
|
||||
"-f",
|
||||
"mp4",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-preset",
|
||||
"veryfast",
|
||||
"-crf",
|
||||
"23",
|
||||
"-vf",
|
||||
"scale='min(1920,iw)':-2",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
str(tmp_path),
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
stderr = (proc.stderr or "").strip()
|
||||
logger.warning("ffmpeg batch input normalize failed: {}", stderr[-3000:])
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
return False
|
||||
if not tmp_path.is_file() or tmp_path.stat().st_size <= 0:
|
||||
logger.warning("ffmpeg batch input normalize produced empty file: {}", tmp_path)
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
return False
|
||||
tmp_path.replace(output_path)
|
||||
if not is_browser_compatible_mp4(output_path):
|
||||
logger.warning("ffmpeg batch input normalize output not h264/yuv420p: {}", output_path)
|
||||
output_path.unlink(missing_ok=True)
|
||||
return False
|
||||
logger.info(
|
||||
"ffmpeg batch input normalize complete: {} ({} bytes)",
|
||||
output_path,
|
||||
output_path.stat().st_size,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def ensure_batch_pipeline_input_video(*, source_path: Path, dest_path: Path) -> None:
|
||||
import shutil as sh
|
||||
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if dest_path.is_file() and dest_path.stat().st_size > 0 and not batch_input_needs_normalize(dest_path):
|
||||
return
|
||||
if batch_input_needs_normalize(source_path):
|
||||
if normalize_batch_input_video(source_path, dest_path):
|
||||
return
|
||||
logger.warning(
|
||||
"batch input normalize failed, falling back to raw copy: {} -> {}",
|
||||
source_path,
|
||||
dest_path,
|
||||
)
|
||||
if not dest_path.is_file():
|
||||
sh.copy2(source_path, dest_path)
|
||||
|
||||
|
||||
def transcode_visualization_for_browser(source_path: Path, output_path: Path) -> bool:
|
||||
ffmpeg = shutil.which("ffmpeg")
|
||||
if ffmpeg is None or not source_path.is_file():
|
||||
return False
|
||||
if not is_readable_mp4(source_path):
|
||||
logger.warning("skip visualization transcode: unreadable source {}", source_path)
|
||||
return False
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = browser_transcode_tmp_path(output_path)
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
logger.info("ffmpeg visualization transcode starting: {} -> {}", source_path, output_path)
|
||||
proc = subprocess.run(
|
||||
[
|
||||
ffmpeg,
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-i",
|
||||
str(source_path),
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-an",
|
||||
"-f",
|
||||
"mp4",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
"-crf",
|
||||
"23",
|
||||
"-vf",
|
||||
visualization_ffmpeg_scale_filter(),
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
str(tmp_path),
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
stderr = (proc.stderr or "").strip()
|
||||
logger.warning("ffmpeg visualization transcode failed: {}", stderr[-3000:])
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
return False
|
||||
if not tmp_path.is_file() or tmp_path.stat().st_size <= 0:
|
||||
logger.warning("ffmpeg visualization transcode produced empty file: {}", tmp_path)
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
return False
|
||||
tmp_path.replace(output_path)
|
||||
if not is_browser_compatible_mp4(output_path):
|
||||
logger.warning("ffmpeg output is not browser-compatible h264/yuv420p: {}", output_path)
|
||||
output_path.unlink(missing_ok=True)
|
||||
return False
|
||||
logger.info(
|
||||
"ffmpeg visualization transcode complete: {} ({} bytes)",
|
||||
output_path,
|
||||
output_path.stat().st_size,
|
||||
)
|
||||
return True
|
||||
Reference in New Issue
Block a user