Scene Summary

Export a summary of your Houdini scene as JSON

About

This Python script exports a summary of your Houdini scene as a JSON file, including all nodes, parameters, and code snippets.

Usage

To run this script, open Houdini and go to Windows > Python Shell. Then enter:

exec(open(r"path\	o\script\scene_summary.py").read())

Replace path\to\script with the folder path where you saved the script.

Script

# scene_summary.py — select HIP → load → write <hipname>_scene_summary.json next to it
import os, json, time, re
import hou

FILEY_HINTS = re.compile(r"(file|path|picture|output|cache)", re.I)
CODEY_HINTS = ("snippet", "vex", "code", "python", "script")

# ---------- JSON sanitizers for Houdini types ----------
def _ramp_to_dict(rp: hou.Ramp):
    try:
        n = rp.getKeyCount()
    except Exception:
        return {"error": "unreadable ramp"}
    keys = []
    for i in range(n):
        try:
            pos = rp.getKeyPosition(i)
            val = rp.getKeyValue(i)
            # val can be float or tuple-like (e.g., color ramps)
            try:
                val = list(val)  # if vector-like
            except TypeError:
                pass
            interp = str(rp.getKeyInterpolation(i))
            keys.append({"pos": float(pos), "val": val, "interp": interp})
        except Exception as e:
            keys.append({"error": str(e)})
    return {"keys": keys}

def _sanitize(obj):
    # Primitive JSON types pass through
    if obj is None or isinstance(obj, (bool, int, float, str)):
        return obj
    # Common Houdini math types → lists
    try:
        import numbers
        # hou.Vector2/3/4 behave like sequences
        if hasattr(obj, "__iter__") and not isinstance(obj, (str, bytes, dict)):
            return [_sanitize(x) for x in obj]
    except Exception:
        pass
    # hou.Ramp
    if isinstance(obj, hou.Ramp):
        return _ramp_to_dict(obj)
    # hou.Matrix, hou.Parm, hou.Node, etc. → string fallback
    try:
        return str(obj)
    except Exception:
        return "<unserializable>"

# ---------- helpers ----------
def _has_expression(parm):
    try:
        return bool(parm.isExpression())
    except Exception:
        try:
            _ = parm.expression()
            return True
        except Exception:
            return False

def _get_expression(parm):
    try:
        return parm.expression()
    except Exception:
        return None

def _maybe_ramp_dict(parm):
    # Detect ramp parm and return a dict; else None
    try:
        pt = parm.parmTemplate()
        if pt and pt.type() == hou.parmTemplateType.Ramp:
            try:
                rp = parm.evalAsRamp()
                return _ramp_to_dict(rp)
            except Exception:
                # Some ramp parms can be read via parm.eval() and will be hou.Ramp
                try:
                    v = parm.eval()
                    if isinstance(v, hou.Ramp):
                        return _ramp_to_dict(v)
                except Exception:
                    pass
    except Exception:
        pass
    return None

def _parm_value(p):
    rec = {"name": p.name()}
    # value (sanitized)
    try:
        v = p.eval()
        rec["value"] = _sanitize(v)
    except Exception as e:
        rec["value_error"] = str(e)

    # ramp (explicit)
    rd = _maybe_ramp_dict(p)
    if rd is not None:
        rec["ramp"] = rd

    # expression
    if _has_expression(p):
        rec["expression"] = _get_expression(p)
        try:
            rec["language"] = str(p.expressionLanguage())
        except Exception:
            pass

    # time dependency
    try:
        rec["is_time_dependent"] = p.isTimeDependent()
    except Exception:
        pass

    # keyframes
    try:
        kfs = []
        for k in p.keyframes():
            try:
                kfs.append({"frame": k.frame(), "value": _sanitize(p.evalAtFrame(k.frame()))})
            except Exception:
                kfs.append({"frame": k.frame()})
        if kfs:
            rec["keyframes"] = kfs
    except Exception:
        pass

    # heuristic file-like flag
    if FILEY_HINTS.search(p.name().lower()):
        rec["hint_file_param"] = True

    return rec

def _collect_node(n):
    # code-like parms (wrangles, python, scripts)
    code = {}
    for p in n.parms():
        nm = p.name().lower()
        if any(h in nm for h in CODEY_HINTS):
            try:
                v = p.eval()
                if isinstance(v, str) and v.strip():
                    code[p.name()] = v
            except Exception:
                pass

    return {
        "path": n.path(),
        "type": n.type().nameWithCategory(),
        "inputs": [i.path() if i else None for i in n.inputs()],
        "outputs": [o.path() for o in n.outputs()],
        "parms": [_parm_value(p) for p in n.parms()],
        "code": code or None,
    }

# ---------- File dialog ----------
hip_path = hou.ui.selectFile(
    title="Select HIP to summarize",
    file_type=hou.fileType.Hip,
    pattern="*.hip *.hiplc *.hipnc",
    multiple_select=False
)
if not hip_path:
    hou.ui.displayMessage("No file selected."); raise SystemExit

try:
    hip_path = hou.text.expandString(hip_path)  # new API
except Exception:
    hip_path = hou.expandString(hip_path)       # fallback (deprecated)
hip_path = os.path.normpath(hip_path)

if not os.path.isfile(hip_path):
    hou.ui.displayMessage("Invalid file:\n" + hip_path); raise SystemExit

# ---------- Open HIP (replaces session) ----------
hou.hipFile.load(hip_path)

# ---------- Gather ----------
root = hou.node("/")
nodes = [root] + list(root.allSubChildren())
data = {
    "hipfile": hou.hipFile.path(),
    "houdini_version": hou.applicationVersionString(),
    "saved_time": time.ctime(),
    "node_count": len(nodes),
    "nodes": [_collect_node(n) for n in nodes],
}

# ---------- Write JSON next to HIP ----------
out_path = os.path.splitext(hip_path)[0] + "_scene_summary.json"
with open(out_path, "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)

hou.ui.displayMessage("Scene summary written:\n" + out_path)
print("Wrote:", out_path)