Adding autorender

This commit is contained in:
2026-07-30 20:34:01 +02:00
parent 4fbb6425df
commit 9d29d2e2ed
3 changed files with 138 additions and 6 deletions
Executable
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
#
# autorender.sh — self-updating nightly render driver for the render rig.
#
# Invoked by Windows Task Scheduler (run only when user is logged on):
# wsl.exe -d Ubuntu -u glitchhunter -- bash -lc "/home/glitchhunter/Projects/gnommo/autorender.sh"
#
# Flow:
# 1. DEPLOY CODE — git fetch + reset --hard origin/<branch>, then re-exec the
# freshly pulled script once. This is how code changes ship from the Mac:
# push to origin, and the next run picks them up. SAFE — reset --hard only
# rewrites TRACKED files; gitignored project data (video*/) and secrets
# (.env) are left untouched. This script NEVER runs `git clean`.
# 2. RENDER — (pending) `gnommo auto`: per-project down -> gated render -> handoff.
#
# Config via environment (set once in ~/.profile / ~/.bash_profile on the rig,
# so a login shell — bash -lc — picks them up):
# GNOMMO_DIR repo clone on the rig (default: this script's own dir)
# BRANCH branch to track (default: main)
# NTFY_URL failure ping endpoint, e.g. https://ntfy.sh/your-secret-topic
#
set -uo pipefail
# Default GNOMMO_DIR to the repo this script lives in, so it works regardless of
# username or checkout path (rig: /home/glitchhunter/Projects/gnommo).
GNOMMO_DIR="${GNOMMO_DIR:-$(cd "$(dirname "$(readlink -f "$0")")" && pwd)}"
BRANCH="${AUTORENDER_BRANCH:-main}"
LOG="${AUTORENDER_LOG:-$GNOMMO_DIR/autorender.log}"
LOCK="${AUTORENDER_LOCK:-/tmp/gnommo-autorender.lock}"
log() { printf '%s | %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG"; }
notify() { [ -n "${NTFY_URL:-}" ] && curl -fsS -m 10 -d "$*" "$NTFY_URL" >/dev/null 2>&1 || true; }
# ── Step 1: deploy latest code from origin, then re-exec the fresh script once ──
# The re-exec is essential: reset --hard rewrites this very file mid-run, so we
# must restart from the updated copy rather than keep executing the old bytes.
if [ -z "${AUTORENDER_UPDATED:-}" ]; then
if ! cd "$GNOMMO_DIR" 2>/dev/null; then
log "FATAL: GNOMMO_DIR not found: $GNOMMO_DIR"
notify "autorender: GNOMMO_DIR missing ($GNOMMO_DIR)"
exit 1
fi
if git fetch --quiet origin 2>>"$LOG"; then
before="$(git rev-parse --short HEAD 2>/dev/null || echo '?')"
git reset --hard "origin/$BRANCH" >>"$LOG" 2>&1
after="$(git rev-parse --short HEAD 2>/dev/null || echo '?')"
./venv/bin/pip install -e . -q >>"$LOG" 2>&1 || true # catch new deps/entry points
[ "$before" != "$after" ] && log "code deployed: $before -> $after"
else
log "git fetch failed — running existing code"
notify "autorender: git fetch failed on the rig"
fi
export AUTORENDER_UPDATED=1
exec "$0" "$@"
fi
# ── everything below runs on the freshly-deployed code ─────────────────────────
# Single-run lock — renders take hours; a second scheduled firing must bail.
exec 9>"$LOCK"
if ! flock -n 9; then
log "another autorender run is active — exiting"
exit 0
fi
log "autorender start (HEAD $(git -C "$GNOMMO_DIR" rev-parse --short HEAD 2>/dev/null), user $(whoami))"
# ── Step 2: render loop — pending `gnommo auto` ────────────────────────────────
# When gnommo auto lands, replace the placeholder with:
# cd "$GNOMMO_DIR" && ./venv/bin/python -m gnommo auto 2>&1 | tee -a "$LOG" \
# || notify "autorender: gnommo auto reported failures"
log "render step not wired yet (gnommo auto pending) — code-deploy path is live"
log "autorender done"
+55 -6
View File
@@ -11,6 +11,7 @@ Files are looked up first locally, then in the cache at:
""" """
import configparser import configparser
import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple from typing import Optional, Tuple
@@ -18,6 +19,11 @@ from typing import Optional, Tuple
_cache_config: Optional[dict] = None _cache_config: Optional[dict] = None
_assets_config: Optional[dict] = None _assets_config: Optional[dict] = None
_perf_config: Optional[dict] = None _perf_config: Optional[dict] = None
# Per-project performance overrides (project.json "performance" block). Set at the
# start of preprocess/render via set_active_project(). These OVERRIDE ~/.gnommo.conf
# and — unlike that per-machine file — travel with the project over up/down, so the
# render rig's chunk size / CPU limits can be tuned remotely by editing project.json.
_active_project_perf: dict = {}
def _load_perf_config() -> dict: def _load_perf_config() -> dict:
@@ -47,6 +53,44 @@ def _load_perf_config() -> dict:
return _perf_config return _perf_config
def set_active_project(project_path) -> None:
"""Load a project's optional "performance" overrides from its project.json.
project.json syncs via up/down and isn't secret, so its "performance" block is
the remotely-editable home for the render rig's tunables (render_chunk_slides,
cpu_limit_preprocess, cpu_limit_render). Keys present here override
~/.gnommo.conf; anything absent falls back to the machine config. Call once at
the start of a per-project preprocess/render.
"""
global _active_project_perf
_active_project_perf = {}
if project_path is None:
return
pj = Path(project_path) / "project.json"
if not pj.exists():
return
try:
perf = json.loads(pj.read_text(encoding="utf-8")).get("performance")
except (ValueError, OSError):
return
if isinstance(perf, dict):
_active_project_perf = perf
def _perf_get(key: str, default_key: Optional[str] = None):
"""Resolve a performance value: active project.json overrides ~/.gnommo.conf.
Within each source the stage-specific `key` wins over the legacy `default_key`
(e.g. `cpu_limit`); the project source is consulted before the machine config.
"""
conf = _load_perf_config()
for src in (_active_project_perf, conf):
for k in (key, default_key):
if k and src.get(k) is not None:
return src[k]
return None
def get_ffmpeg_thread_count(stage: str = "preprocess") -> int: def get_ffmpeg_thread_count(stage: str = "preprocess") -> int:
"""Return the FFmpeg thread count for a pipeline stage from ~/.gnommo.conf. """Return the FFmpeg thread count for a pipeline stage from ~/.gnommo.conf.
@@ -59,15 +103,19 @@ def get_ffmpeg_thread_count(stage: str = "preprocess") -> int:
# layer and OOMs at high core counts # layer and OOMs at high core counts
`stage` is "preprocess" or "render". The legacy single `cpu_limit` key is the `stage` is "preprocess" or "render". The legacy single `cpu_limit` key is the
fallback for either stage when its specific key is absent. Each value is a fallback for either stage when its specific key is absent. A project.json
fraction of logical CPUs (0.8 = 80%); defaults to 1 thread when nothing is "performance" block (see set_active_project) overrides these per project. Each
configured, which is safe on memory-constrained machines. value is a fraction of logical CPUs (0.8 = 80%); defaults to 1 thread when
nothing is configured, which is safe on memory-constrained machines.
""" """
cfg = _load_perf_config()
key = "cpu_limit_render" if stage == "render" else "cpu_limit_preprocess" key = "cpu_limit_render" if stage == "render" else "cpu_limit_preprocess"
cpu_limit = cfg.get(key, cfg.get("cpu_limit")) cpu_limit = _perf_get(key, "cpu_limit")
if cpu_limit is None: if cpu_limit is None:
return 1 return 1
try:
cpu_limit = float(cpu_limit)
except (TypeError, ValueError):
return 1
cpu_count = os.cpu_count() or 1 cpu_count = os.cpu_count() or 1
return max(1, int(cpu_count * cpu_limit)) return max(1, int(cpu_count * cpu_limit))
@@ -77,12 +125,13 @@ def get_render_chunk_size() -> Optional[int]:
When set, cmd_render splits the filter graph into chunks of this many slides When set, cmd_render splits the filter graph into chunks of this many slides
to avoid OOM from allocating filter buffers for the entire video at once. to avoid OOM from allocating filter buffers for the entire video at once.
A project.json "performance" block overrides ~/.gnommo.conf per project.
Example ~/.gnommo.conf: Example ~/.gnommo.conf:
[performance] [performance]
render_chunk_slides = 15 render_chunk_slides = 15
""" """
val = _load_perf_config().get("render_chunk_slides") val = _perf_get("render_chunk_slides")
if val is None: if val is None:
return None return None
try: try:
+9
View File
@@ -2565,6 +2565,10 @@ def cmd_preprocess(
from .parser import parse_project_config, parse_videos from .parser import parse_project_config, parse_videos
from .preprocessor import preprocess_video, RES_CONFIGS from .preprocessor import preprocess_video, RES_CONFIGS
from .models import VideoSource as _VideoSource from .models import VideoSource as _VideoSource
from .cache import set_active_project
# Apply this project's project.json "performance" overrides (cpu limits etc.).
set_active_project(project_path)
mode_str = f" ({res.upper()})" if res != "full" else "" mode_str = f" ({res.upper()})" if res != "full" else ""
print(f"Preprocessing narration: {project_path.name}{mode_str}") print(f"Preprocessing narration: {project_path.name}{mode_str}")
@@ -4799,6 +4803,11 @@ def _cmd_render_impl(
from .transformer import build_render_plan from .transformer import build_render_plan
from .renderer import render, generate_ffmpeg_command_string from .renderer import render, generate_ffmpeg_command_string
from .preprocessor import RES_CONFIGS, ensure_downscaled_files_exist from .preprocessor import RES_CONFIGS, ensure_downscaled_files_exist
from .cache import set_active_project
# Apply this project's project.json "performance" overrides (chunk size, cpu
# limits) before any chunk-size or thread-count decisions below.
set_active_project(project_path)
# ffmpeg version guard — only for an actual encode (not dry-run/build, and only # ffmpeg version guard — only for an actual encode (not dry-run/build, and only
# once at the top level, not per chunk sub-render). Older builds silently # once at the top level, not per chunk sub-render). Older builds silently