Add error handling for ffmpg

This commit is contained in:
2026-07-23 21:56:24 +02:00
parent 09620c4e61
commit 78e3aef0f2
+163
View File
@@ -4221,6 +4221,97 @@ def cmd_build(
) )
# ── Render logging ────────────────────────────────────────────────────────────
# A hard crash on Windows/Linux (the OS OOM-killing ffmpeg or python, a native
# segfault) leaves no Python traceback and just prints "Terminated". So we tee the
# whole render to <project>/<project>.log with line-flushing: the log keeps a
# header (platform / ffmpeg / memory / args) and the exact ffmpeg command, so a
# run that dies mid-encode can still be diagnosed from the last lines written.
_RENDER_LOGFILE = None
class _TeeStream:
"""Write to the real stream and mirror completed lines into a log file.
Progress-bar redraws (carriage returns with no newline) are dropped from the
log; only whole lines are kept, so the log stays greppable. Everything is
flushed immediately so a hard kill still leaves the trail on disk.
"""
def __init__(self, stream, logfile):
self._stream = stream
self._logfile = logfile
self._buf = ""
def write(self, data):
self._stream.write(data)
self._buf += data
while "\n" in self._buf:
line, self._buf = self._buf.split("\n", 1)
try:
self._logfile.write(line.split("\r")[-1] + "\n")
except Exception:
pass
try:
self._logfile.flush()
except Exception:
pass
def flush(self):
self._stream.flush()
try:
self._logfile.flush()
except Exception:
pass
def __getattr__(self, name):
return getattr(self._stream, name)
def _render_log(msg: str) -> None:
"""Write a line only to the render log (not the terminal)."""
if _RENDER_LOGFILE is not None:
try:
_RENDER_LOGFILE.write(msg + "\n")
_RENDER_LOGFILE.flush()
except Exception:
pass
def _write_render_log_header(logfile, project_path, res, slides_arg, force, chunk_slides):
import os
import platform as _platform
try:
_ff = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True)
ffmpeg_ver = _ff.stdout.splitlines()[0] if _ff.stdout else "unknown"
except Exception:
ffmpeg_ver = "unavailable"
try:
import psutil # optional dependency
vm = psutil.virtual_memory()
mem = f"{vm.available / 1e9:.1f} GB free / {vm.total / 1e9:.1f} GB total"
except Exception:
mem = "unknown (install psutil for memory info)"
logfile.write("=" * 70 + "\n")
logfile.write(f"gnommo render log — {project_path.name}\n")
logfile.write(f"time : {datetime.now().isoformat(timespec='seconds')}\n")
logfile.write(f"platform : {_platform.platform()}\n")
logfile.write(f"python : {sys.version.split()[0]}\n")
logfile.write(f"ffmpeg : {ffmpeg_ver}\n")
logfile.write(f"cpu_count : {os.cpu_count()}\n")
logfile.write(f"memory : {mem}\n")
logfile.write(
f"args : res={res} slides={slides_arg} force={force} chunk_slides={chunk_slides}\n"
)
logfile.write("=" * 70 + "\n\n")
logfile.flush()
def cmd_render( def cmd_render(
project_path: Path, project_path: Path,
verbose: bool, verbose: bool,
@@ -4232,6 +4323,72 @@ def cmd_render(
_output_path_override: Path = None, _output_path_override: Path = None,
plan_only: bool = False, plan_only: bool = False,
realign: bool = False, realign: bool = False,
) -> int:
"""Render entry point — opens <project>/<project>.log, then runs the render.
Internal chunk sub-renders (_output_path_override set) and any nested call
while a log is already open reuse the parent log instead of clobbering it.
"""
global _RENDER_LOGFILE
passthrough = dict(
slides_arg=slides_arg,
res=res,
force=force,
chunk_slides=chunk_slides,
_output_path_override=_output_path_override,
plan_only=plan_only,
realign=realign,
)
if _output_path_override is not None or _RENDER_LOGFILE is not None:
return _cmd_render_impl(project_path, verbose, dry_run, **passthrough)
log_path = project_path / f"{project_path.name}.log"
try:
logfile = open(log_path, "w", encoding="utf-8", buffering=1)
except OSError:
return _cmd_render_impl(project_path, verbose, dry_run, **passthrough)
_write_render_log_header(logfile, project_path, res, slides_arg, force, chunk_slides)
_orig_out, _orig_err = sys.stdout, sys.stderr
sys.stdout = _TeeStream(_orig_out, logfile)
sys.stderr = _TeeStream(_orig_err, logfile)
_RENDER_LOGFILE = logfile
try:
return _cmd_render_impl(project_path, verbose, dry_run, **passthrough)
except BaseException:
import traceback
logfile.write("\n=== EXCEPTION / ABORT ===\n")
traceback.print_exc(file=logfile)
logfile.flush()
raise
finally:
sys.stdout = _orig_out
sys.stderr = _orig_err
_RENDER_LOGFILE = None
try:
logfile.write(
f"\n[render log closed {datetime.now().isoformat(timespec='seconds')}]\n"
)
logfile.close()
except Exception:
pass
print(f" (render log: {log_path})")
def _cmd_render_impl(
project_path: Path,
verbose: bool,
dry_run: bool,
slides_arg: str = None,
res: str = "full",
force: bool = False,
chunk_slides: int = 0,
_output_path_override: Path = None,
plan_only: bool = False,
realign: bool = False,
) -> int: ) -> int:
"""Render final video. """Render final video.
@@ -4662,6 +4819,12 @@ def cmd_render(
return 0 return 0
print("\n[4/4] Rendering...") print("\n[4/4] Rendering...")
# Record the exact ffmpeg command in the log only (not the terminal), so a
# render that gets hard-killed mid-encode can still be reproduced/diagnosed.
try:
_render_log("FFmpeg command:\n" + generate_ffmpeg_command_string(plan, output_path))
except Exception as _e:
_render_log(f"(could not serialize ffmpeg command: {_e})")
render(plan, output_path, verbose=verbose) render(plan, output_path, verbose=verbose)
print(f" Output: {output_path}") print(f" Output: {output_path}")