Adding GPU transcoding possibility
This commit is contained in:
112
app/worker.py
112
app/worker.py
@@ -544,7 +544,7 @@ def reuse_repo_transcript(media_path: Path, repo_json: Path) -> Path | None:
|
||||
|
||||
# copy or synthesize TXT
|
||||
if src_txt.exists():
|
||||
shutil.copy2(src_txt, new_base.with_suffix(".txt"))
|
||||
_safe_copy(src_txt, new_base.with_suffix(".txt"))
|
||||
else:
|
||||
# fallback: concatenate segments
|
||||
txt = " ".join(s.get("text", "") for s in data.get("segments", []))
|
||||
@@ -552,7 +552,7 @@ def reuse_repo_transcript(media_path: Path, repo_json: Path) -> Path | None:
|
||||
|
||||
# copy SRT/VTT if present; otherwise synthesize SRT from segments
|
||||
if src_srt.exists():
|
||||
shutil.copy2(src_srt, new_base.with_suffix(".srt"))
|
||||
_safe_copy(src_srt, new_base.with_suffix(".srt"))
|
||||
else:
|
||||
# synthesize SRT
|
||||
def fmt_ts(t):
|
||||
@@ -562,7 +562,7 @@ def reuse_repo_transcript(media_path: Path, repo_json: Path) -> Path | None:
|
||||
for i, s in enumerate(data.get("segments", []), 1):
|
||||
srt.write(f"{i}\n{fmt_ts(s.get('start',0.0))} --> {fmt_ts(s.get('end',0.0))}\n{s.get('text','').strip()}\n\n")
|
||||
if src_vtt.exists():
|
||||
shutil.copy2(src_vtt, new_base.with_suffix(".vtt"))
|
||||
_safe_copy(src_vtt, new_base.with_suffix(".vtt"))
|
||||
else:
|
||||
# synthesize VTT from segments
|
||||
def fmt_ts_vtt(t):
|
||||
@@ -642,7 +642,7 @@ def ensure_sidecar_next_to_media(sidecar: Path, media_path: Path, lang: str = "e
|
||||
return
|
||||
if sidecar.suffix.lower() == ".srt":
|
||||
dst = media_path.with_suffix(f".{lang}.srt")
|
||||
shutil.copy2(sidecar, dst)
|
||||
_safe_copy(sidecar, dst)
|
||||
elif sidecar.suffix.lower() == ".vtt":
|
||||
tmp_srt = sidecar.with_suffix(".srt")
|
||||
subprocess.run(["ffmpeg", "-nostdin", "-y", "-threads", str(FFMPEG_THREADS), "-i", str(sidecar), str(tmp_srt)], check=True)
|
||||
@@ -708,7 +708,7 @@ def save_episode_artwork(image_url: str | None, media_path: Path, show_title: st
|
||||
try:
|
||||
show_poster = media_path.parent / "poster.jpg"
|
||||
if not show_poster.exists():
|
||||
shutil.copy2(episode_jpg, show_poster)
|
||||
_safe_copy(episode_jpg, show_poster)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1187,7 +1187,7 @@ def transcribe(media_path: Path):
|
||||
lang_code = (info.language or (WHISPER_LANGUAGE if WHISPER_LANGUAGE.lower() != 'auto' else 'en')).lower()
|
||||
srt_src = base.with_suffix(".srt")
|
||||
srt_dst = media_path.with_suffix(f".{lang_code}.srt")
|
||||
shutil.copy2(srt_src, srt_dst)
|
||||
_safe_copy(srt_src, srt_dst)
|
||||
except Exception as e:
|
||||
print(f"[post] could not copy srt -> {srt_dst}: {e}", flush=True)
|
||||
|
||||
@@ -1500,6 +1500,17 @@ def _unique_backup_path(path: Path) -> Path:
|
||||
counter += 1
|
||||
|
||||
|
||||
def _safe_copy(src: Path, dst: Path) -> None:
|
||||
try:
|
||||
if src.resolve(strict=False) == dst.resolve(strict=False):
|
||||
return
|
||||
except Exception:
|
||||
if os.path.abspath(src) == os.path.abspath(dst):
|
||||
return
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
|
||||
def _is_sidecar_name(name: str, base_stem: str, base_name: str) -> bool:
|
||||
exact_suffixes = [".info.json", ".nfo", ".jpg", ".jpeg", ".png", ".webp", ".prov.json"]
|
||||
for suf in exact_suffixes:
|
||||
@@ -1585,41 +1596,68 @@ def _normalize_video_file(path: Path, info: dict[str, str]) -> Path:
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
|
||||
cmd = [
|
||||
"ffmpeg", "-nostdin", "-y",
|
||||
"-i", str(path),
|
||||
"-map", "0",
|
||||
"-c:v", encoder,
|
||||
]
|
||||
if VIDEO_NORMALIZE_PRESET:
|
||||
cmd.extend(["-preset", VIDEO_NORMALIZE_PRESET])
|
||||
if VIDEO_NORMALIZE_TUNE:
|
||||
cmd.extend(["-tune", VIDEO_NORMALIZE_TUNE])
|
||||
if VIDEO_NORMALIZE_CRF:
|
||||
cmd.extend(["-crf", VIDEO_NORMALIZE_CRF])
|
||||
def build_cmd(v_encoder: str) -> list[str]:
|
||||
cmd = [
|
||||
"ffmpeg", "-nostdin", "-y",
|
||||
"-i", str(path),
|
||||
"-map", "0",
|
||||
"-c:v", v_encoder,
|
||||
]
|
||||
if VIDEO_NORMALIZE_PRESET:
|
||||
cmd.extend(["-preset", VIDEO_NORMALIZE_PRESET])
|
||||
if VIDEO_NORMALIZE_TUNE:
|
||||
cmd.extend(["-tune", VIDEO_NORMALIZE_TUNE])
|
||||
if VIDEO_NORMALIZE_CRF:
|
||||
cmd.extend(["-crf", VIDEO_NORMALIZE_CRF])
|
||||
|
||||
if info.get("audio"):
|
||||
if VIDEO_NORMALIZE_AUDIO_CODEC == "copy":
|
||||
cmd.extend(["-c:a", "copy"])
|
||||
if info.get("audio"):
|
||||
if VIDEO_NORMALIZE_AUDIO_CODEC == "copy":
|
||||
cmd.extend(["-c:a", "copy"])
|
||||
else:
|
||||
cmd.extend(["-c:a", _resolve_audio_encoder(VIDEO_NORMALIZE_AUDIO_CODEC)])
|
||||
if VIDEO_NORMALIZE_AUDIO_BITRATE:
|
||||
cmd.extend(["-b:a", VIDEO_NORMALIZE_AUDIO_BITRATE])
|
||||
else:
|
||||
cmd.extend(["-c:a", _resolve_audio_encoder(VIDEO_NORMALIZE_AUDIO_CODEC)])
|
||||
if VIDEO_NORMALIZE_AUDIO_BITRATE:
|
||||
cmd.extend(["-b:a", VIDEO_NORMALIZE_AUDIO_BITRATE])
|
||||
else:
|
||||
cmd.append("-an")
|
||||
cmd.append("-an")
|
||||
|
||||
cmd.extend(["-c:s", "copy", str(tmp_path)])
|
||||
cmd.extend(["-c:s", "copy", str(tmp_path)])
|
||||
return cmd
|
||||
|
||||
print(f"[normalize] video -> {final_path.name} codec={VIDEO_NORMALIZE_CODEC}", flush=True)
|
||||
try:
|
||||
subprocess.check_call(cmd)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
raise RuntimeError(f"ffmpeg video normalize failed: {e}")
|
||||
def fallback_encoder() -> str:
|
||||
base = VIDEO_NORMALIZE_CODEC.lower()
|
||||
if base.startswith("hevc") or base.startswith("h265"):
|
||||
return "libx265"
|
||||
if base.startswith("h264") or base.startswith("avc"):
|
||||
return "libx264"
|
||||
if base.startswith("av1"):
|
||||
return "libaom-av1"
|
||||
return "libx265"
|
||||
|
||||
rename_media_sidecars(path, final_path, skip={tmp_path})
|
||||
return _finalize_normalized_output(path, final_path, tmp_path)
|
||||
attempted = []
|
||||
encoders_to_try = [encoder]
|
||||
lower_encoder = encoder.lower()
|
||||
if any(token in lower_encoder for token in ("nvenc", "qsv", "cuda", "amf")):
|
||||
cpu_encoder = fallback_encoder()
|
||||
if cpu_encoder not in encoders_to_try:
|
||||
encoders_to_try.append(cpu_encoder)
|
||||
|
||||
for idx, enc in enumerate(encoders_to_try):
|
||||
try:
|
||||
print(f"[normalize] video -> {final_path.name} codec={enc}", flush=True)
|
||||
subprocess.check_call(build_cmd(enc))
|
||||
rename_media_sidecars(path, final_path, skip={tmp_path})
|
||||
return _finalize_normalized_output(path, final_path, tmp_path)
|
||||
except subprocess.CalledProcessError as e:
|
||||
attempted.append((enc, e))
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
if idx == len(encoders_to_try) - 1:
|
||||
details = ", ".join(f"{enc}: {err}" for enc, err in attempted)
|
||||
raise RuntimeError(f"ffmpeg video normalize failed ({details})")
|
||||
else:
|
||||
print(f"[normalize] encoder {enc} failed ({e}); retrying with CPU fallback", flush=True)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def _normalize_audio_file(path: Path, info: dict[str, str]) -> Path:
|
||||
@@ -2054,7 +2092,7 @@ def refresh_media(path_str: str):
|
||||
for s in p.parent.glob(f"{p.stem}*.srt"):
|
||||
# If it's already lang-suffixed, keep; also copy to .en.srt when only plain .srt exists
|
||||
if s.name == f"{p.stem}.srt":
|
||||
shutil.copy2(s, p.with_suffix(".en.srt"))
|
||||
_safe_copy(s, p.with_suffix(".en.srt"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
Reference in New Issue
Block a user