Add error handling for ffmpg
This commit is contained in:
+163
@@ -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(
|
||||
project_path: Path,
|
||||
verbose: bool,
|
||||
@@ -4232,6 +4323,72 @@ def cmd_render(
|
||||
_output_path_override: Path = None,
|
||||
plan_only: 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:
|
||||
"""Render final video.
|
||||
|
||||
@@ -4662,6 +4819,12 @@ def cmd_render(
|
||||
return 0
|
||||
|
||||
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)
|
||||
print(f" Output: {output_path}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user