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(
|
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}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user