Adding autorender
This commit is contained in:
Executable
+74
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user