173 lines
5.9 KiB
Python
173 lines
5.9 KiB
Python
"""
|
|
process.py — Nuke terminal-mode batch renderer.
|
|
|
|
This script runs INSIDE Nuke and must not import anything outside the
|
|
Python standard library or Nuke's own modules.
|
|
|
|
Usage (called by nuke_runner.py):
|
|
Nuke.exe -t process.py <jobs.json>
|
|
|
|
jobs.json schema
|
|
----------------
|
|
{
|
|
"template_script": "/abs/path/to/proxy_template.nk",
|
|
"jobs": [
|
|
{
|
|
"basename": "SHOW_EP_SEQ_SHOT_ELEM_TASK_v001",
|
|
"input_sequence": "/path/SHOW_..._%05d.exr",
|
|
"first_frame": 1001,
|
|
"last_frame": 1100,
|
|
"output_folder": "/output/SHOW/SHOW_EP/basename",
|
|
"output_movie": "/output/.../basename.mov",
|
|
"output_exr": "/output/.../basename.%05d.exr"
|
|
},
|
|
...
|
|
]
|
|
}
|
|
|
|
Expected template nodes
|
|
-----------------------
|
|
Read_Input — source EXR sequence
|
|
Overlay — text burn-in; knob 'bottomleft' is set to the shot basename
|
|
Write_4k — EXR sequence output (file = output_exr)
|
|
Write_h264 — MOV / H.264 output (file = output_movie)
|
|
|
|
Both Write nodes are executed in a single nuke.execute() call so the
|
|
graph is only evaluated once per frame.
|
|
|
|
Progress lines printed to stdout (parsed by nuke_runner.py):
|
|
[1/10] Rendering: <basename>
|
|
[1/10] Done: <basename>
|
|
[1/10] ERROR: <basename>: <message>
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
|
|
def _log(msg: str) -> None:
|
|
"""Print *msg* and flush immediately so the parent process receives it."""
|
|
print(msg)
|
|
sys.stdout.flush()
|
|
|
|
|
|
def main() -> None:
|
|
# ── Locate jobs file ──────────────────────────────────────────────
|
|
# When launched as: Nuke.exe -t process.py jobs.json
|
|
# sys.argv is typically: ['process.py', 'jobs.json']
|
|
if len(sys.argv) < 2:
|
|
_log("ERROR: Usage: Nuke.exe -t process.py <jobs.json>")
|
|
sys.exit(1)
|
|
|
|
jobs_path = sys.argv[1]
|
|
if not os.path.isfile(jobs_path):
|
|
_log(f"ERROR: Jobs file not found: {jobs_path}")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
with open(jobs_path, encoding="utf-8") as fh:
|
|
payload = json.load(fh)
|
|
except Exception as exc:
|
|
_log(f"ERROR: Cannot parse jobs file: {exc}")
|
|
sys.exit(1)
|
|
|
|
template_script = payload.get("template_script", "")
|
|
jobs = payload.get("jobs", [])
|
|
total = len(jobs)
|
|
|
|
if not jobs:
|
|
_log("No jobs to process.")
|
|
return
|
|
|
|
# ── Import Nuke ───────────────────────────────────────────────────
|
|
try:
|
|
import nuke # noqa: PLC0415
|
|
except ImportError:
|
|
_log("ERROR: 'nuke' module not available. This script must run inside Nuke.")
|
|
sys.exit(1)
|
|
|
|
# ── Open template ─────────────────────────────────────────────────
|
|
if not os.path.isfile(template_script):
|
|
_log(f"ERROR: Template script not found: {template_script}")
|
|
sys.exit(1)
|
|
|
|
_log(f"Loading template: {template_script}")
|
|
nuke.scriptOpen(template_script)
|
|
|
|
read_node = nuke.toNode("Read_Input")
|
|
overlay_node = nuke.toNode("Overlay")
|
|
write_4k = nuke.toNode("Write_4k")
|
|
write_h264 = nuke.toNode("Write_h264")
|
|
|
|
if read_node is None:
|
|
_log("ERROR: Node 'Read_Input' not found in template.")
|
|
sys.exit(1)
|
|
if write_4k is None:
|
|
_log("ERROR: Node 'Write_4k' not found in template.")
|
|
sys.exit(1)
|
|
if write_h264 is None:
|
|
_log("ERROR: Node 'Write_h264' not found in template.")
|
|
sys.exit(1)
|
|
if overlay_node is None:
|
|
_log("WARNING: Node 'Overlay' not found in template — bottomleft will not be set.")
|
|
|
|
# ── Process jobs ──────────────────────────────────────────────────
|
|
for idx, job in enumerate(jobs, start=1):
|
|
basename = job.get("basename", f"job_{idx}")
|
|
_log(f"[{idx}/{total}] Rendering: {basename}")
|
|
|
|
try:
|
|
first = int(job["first_frame"])
|
|
last = int(job["last_frame"])
|
|
input_seq = job["input_sequence"]
|
|
output_mov = job["output_movie"]
|
|
output_exr = job["output_exr"]
|
|
output_folder = job.get("output_folder", "")
|
|
|
|
# Ensure output directory exists
|
|
target_dir = output_folder or os.path.dirname(output_mov)
|
|
if target_dir:
|
|
os.makedirs(target_dir, exist_ok=True)
|
|
|
|
# Configure Read node
|
|
read_node["file"].setValue(input_seq)
|
|
read_node["first"].setValue(first)
|
|
read_node["last"].setValue(last)
|
|
read_node["origfirst"].setValue(first)
|
|
read_node["origlast"].setValue(last)
|
|
|
|
# Set burn-in text on the Overlay node
|
|
if overlay_node is not None:
|
|
overlay_node["bottomleft"].setValue(basename)
|
|
|
|
# Configure both Write nodes
|
|
write_4k["file"].setValue(output_exr)
|
|
write_h264["file"].setValue(output_mov)
|
|
|
|
# Render both Write nodes (sequential calls; list form not supported in all versions)
|
|
nuke.execute(write_4k, first, last, 1)
|
|
nuke.execute(write_h264, first, last, 1)
|
|
|
|
_log(f"[{idx}/{total}] Done: {basename}")
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
_log(f"[{idx}/{total}] ERROR: {basename}: {exc}")
|
|
|
|
# Free memory caches between jobs to avoid RAM build-up
|
|
for fn in ("clearRAMCache", "memory2", "memory"):
|
|
func = getattr(nuke, fn, None)
|
|
if func is not None:
|
|
try:
|
|
func("free")
|
|
except Exception:
|
|
pass
|
|
break
|
|
|
|
_log("All jobs complete.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|