Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,7 @@
"group": "Rendering paths",
"pages": [
"guides/rendering",
"reference/render-provenance",
"deploy/overview",
"deploy/cloud",
"guides/deploy"
Expand Down
117 changes: 117 additions & 0 deletions docs/reference/render-provenance.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: "Render provenance sidecar"
sidebarTitle: "Render provenance"
description: "The portable JSON receipt written next to every rendered output."
---

Every successful `hyperframes render` writes a small JSON receipt next to the
output file: `out.mp4` gets `out.mp4.hf-render.json`. The sidecar records what
produced the file — tool versions, input hashes, fonts, format, encoder, stage
timings, and warning codes — so agents, CI pipelines, and support tooling can
answer "what rendered this, from what, and how" without re-running anything.

```bash
npx hyperframes render --output out.mp4
# → out.mp4
# → out.mp4.hf-render.json
```

```json
{
"$schema": "https://hyperframes.heygen.com/schema/hf-render-sidecar.json",
"schemaVersion": 1,
"kind": "hf-render-sidecar",
"createdAt": "2026-09-08T01:10:29.640Z",
"versions": {
"producer": "0.8.31",
"node": "v22.22.2",
"ffmpeg": "ffmpeg version 6.1.1-3ubuntu5"
},
"render": {
"jobId": "render-1757294992655-h2y0iqk3d",
"outcome": "completed",
"warningCodes": [],
"totalElapsedMs": 36980,
"stages": { "compileMs": 189, "captureFrameMs": 33212, "encodeMs": 941 },
"workers": 1,
"quality": "standard"
},
"input": {
"entryFile": "index.html",
"entrySha256": "9917f7d1…",
"compositionHash": "d59fa8f6a2f95627",
"fonts": ["Inter", "Space Grotesk"],
"variables": { "count": 2, "sha256": "0b26e313…" }
},
"output": {
"file": "out.mp4",
"format": "mp4",
"fps": { "num": 30, "den": 1 },
"width": 1920,
"height": 1080,
"durationSeconds": 4,
"totalFrames": 120,
"sizeBytes": 693352,
"sha256": "6cbb43c1…",
"hdr": false,
"encoder": { "codec": "h264", "preset": "medium", "pixelFormat": "yuv420p" }
},
"host": { "platform": "linux", "arch": "x64" }
}
```

## Controlling the sidecar

The sidecar is on by default for every render, including `--docker` and
`--batch` renders (each batch row writes its own
`<row output>.hf-render.json`).

```bash
npx hyperframes render --output out.mp4 --no-provenance # disable
npx hyperframes render --output out.mp4 --provenance receipts/out.json # relocate
```

Two combinations are rejected up front: a custom `--provenance <path>` with
`--batch` (one fixed path cannot serve N row outputs) and with `--docker`
(an arbitrary host path is not visible from the render container). Disabling
works everywhere.

Programmatic callers set the same tri-state on the render request:
`provenance` omitted (default path), `false` (disabled), or a custom path
string.

## What the fields mean

| Field | Meaning |
| --- | --- |
| `versions` | `@hyperframes/producer` package version, Node.js version, and the first line of `ffmpeg -version`. |
| `render.outcome` | `completed` or `completed_with_warnings` — sidecars only exist for renders whose artifact committed. |
| `render.warningCodes` | Sorted capture-readiness warning codes (empty on a clean render). |
| `render.stages` | Per-stage wall-clock timings in milliseconds (`compileMs`, `captureMs`, `encodeMs`, …). |
| `input.entrySha256` | sha256 of the entry HTML source bytes. |
| `input.compositionHash` | Content hash of the compiled composition — the same value render telemetry reports. |
| `input.fonts` | `@font-face` family names baked into the compiled composition. |
| `input.variables` | Count and sha256 of the render-time variable overrides — the hash proves *which* parametrization produced the output without disclosing values. `null` when no variables were passed. |
| `output.sha256` / `output.sizeBytes` | Digest and size of the committed artifact. Omitted for `png-sequence` directory outputs. |
| `output.encoder` | Codec, preset, and pixel format used by the encode stage. `null` for `png-sequence` and `gif`. |

The full contract is published as a JSON Schema at
[`hf-render-sidecar.json`](https://hyperframes.heygen.com/schema/hf-render-sidecar.json)
(source: `packages/core/schemas/hf-render-sidecar.json`).

## What the sidecar deliberately omits

Variable **values** (they routinely carry user text and tokens — only a hash
is recorded), environment variables, absolute host paths, usernames, and
machine names. Once a receipt travels with a shared file, metadata leaks are
hard to walk back.

## Sidecar vs. embedded container tags

HyperFrames also stamps MP4/MOV/WebM containers with two unsigned metadata
tags (`hyperframes_renderer`, `hyperframes_version`). The two are
complementary: the embedded tags survive file moves but hold only the
renderer name and version; the sidecar carries the full receipt but is a
separate file. Both are unauthenticated hints — any tool can write either —
so use them for diagnostics and CI bookkeeping, never as an authenticity or
attribution boundary.
138 changes: 138 additions & 0 deletions packages/cli/src/commands/render.provenance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CliUsageError } from "../utils/commandResult.js";
import { buildDockerRunArgs } from "../utils/dockerRunArgs.js";
import { createRenderPlan, parseProvenanceArg } from "./render/plan.js";

describe("parseProvenanceArg", () => {
it("defaults to on (undefined) when the flag is absent or bare", () => {
expect(parseProvenanceArg(undefined)).toBeUndefined();
expect(parseProvenanceArg(true)).toBeUndefined();
expect(parseProvenanceArg(" ")).toBeUndefined();
});

it("disables on --no-provenance and on disable-alias values", () => {
expect(parseProvenanceArg(false)).toBe(false);
expect(parseProvenanceArg("false")).toBe(false);
expect(parseProvenanceArg("OFF")).toBe(false);
expect(parseProvenanceArg("0")).toBe(false);
expect(parseProvenanceArg("none")).toBe(false);
});

it("resolves a custom sidecar path", () => {
expect(parseProvenanceArg("receipts/out.json")).toBe(resolve("receipts/out.json"));
});
});

/** Minimal renderable project fixture — one composition root, no clips. */
function makeProvenanceProjectDir(): string {
const dir = mkdtempSync(join(tmpdir(), "hf-render-provenance-"));
const root =
'<main data-composition-id="main" data-width="1920" data-height="1080" data-fps="30"></main>';
writeFileSync(join(dir, "index.html"), root);
return dir;
}

describe("createRenderPlan provenance", () => {
let projectDir: string;

beforeEach(() => {
projectDir = makeProvenanceProjectDir();
vi.spyOn(console, "error").mockImplementation(() => {});
});

afterEach(() => {
vi.restoreAllMocks();
rmSync(projectDir, { recursive: true, force: true });
});

it("leaves provenance on by default", () => {
const plan = createRenderPlan({ dir: projectDir, output: "out.mp4" });
expect(plan.provenance).toBeUndefined();
});

it("threads --no-provenance through the plan", () => {
const plan = createRenderPlan({ dir: projectDir, output: "out.mp4", provenance: false });
expect(plan.provenance).toBe(false);
});

it("threads a custom sidecar path through the plan", () => {
const plan = createRenderPlan({
dir: projectDir,
output: "out.mp4",
provenance: "receipts/out.json",
});
expect(plan.provenance).toBe(resolve("receipts/out.json"));
});

it("rejects a custom sidecar path with --batch (rows write their own sidecars)", () => {
expect(() =>
createRenderPlan({
dir: projectDir,
batch: "rows.json",
provenance: "receipts/out.json",
}),
).toThrow(CliUsageError);
});

it("allows disabling provenance batch-wide", () => {
const plan = createRenderPlan({ dir: projectDir, batch: "rows.json", provenance: false });
expect(plan.provenance).toBe(false);
});

it("rejects a custom sidecar path with --docker (path is not container-visible)", () => {
expect(() =>
createRenderPlan({
dir: projectDir,
output: "out.mp4",
docker: true,
provenance: "receipts/out.json",
}),
).toThrow(CliUsageError);
});

it("allows disabling provenance with --docker", () => {
const plan = createRenderPlan({
dir: projectDir,
output: "out.mp4",
docker: true,
provenance: "false",
});
expect(plan.provenance).toBe(false);
});
});

describe("buildDockerRunArgs provenance forwarding", () => {
const base = {
imageTag: "hyperframes-renderer:test",
projectDir: "/host/project",
outputDir: "/host/renders",
outputFilename: "out.mp4",
platform: "linux/amd64",
options: {
fps: { num: 30, den: 1 },
quality: "standard" as const,
format: "mp4" as const,
gpu: false,
browserGpu: false,
hdrMode: "auto" as const,
quiet: true,
},
};

it("forwards --no-provenance into the container CLI", () => {
const args = buildDockerRunArgs({
...base,
options: { ...base.options, provenance: false as const },
});
expect(args).toContain("--no-provenance");
});

it("does not forward anything for the default-on setting", () => {
const args = buildDockerRunArgs(base);
expect(args).not.toContain("--no-provenance");
expect(args).not.toContain("--provenance");
});
});
16 changes: 16 additions & 0 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,15 @@ export default defineCommand({
// guard below leaves PRODUCER_EXPERIMENTAL_FAST_CAPTURE untouched and the
// env fallback survives (matches the --low-memory-mode idiom).
},
provenance: {
type: "string",
description:
"Render provenance sidecar path (default: <output>.hf-render.json). " +
"The sidecar is a portable JSON receipt written next to the output " +
"after a successful render: tool versions, input hashes, variables " +
"hash, fonts, format/fps/resolution, encoder, stage timings, and " +
"warning codes. Pass false (or use --no-provenance) to disable.",
},
"frames-cache-dir": {
type: "string",
description:
Expand Down Expand Up @@ -426,6 +435,11 @@ export interface RenderOptions {
protocolTimeout?: number;
/** Player-ready timeout override (ms). */
playerReadyTimeout?: number;
/**
* Provenance sidecar setting: `undefined` = default sidecar next to the
* output, `false` = disabled, string = custom sidecar path.
*/
provenance?: string | false;
/** Throw render failures to the caller instead of printing and exiting. */
throwOnError?: boolean;
/** Skip the interactive feedback prompt after a successful render. */
Expand Down Expand Up @@ -728,6 +742,7 @@ async function renderDocker(
pageNavigationTimeoutMs: options.pageNavigationTimeoutMs,
protocolTimeoutMs: options.protocolTimeout,
playerReadyTimeoutMs: options.playerReadyTimeout,
provenance: options.provenance,
},
});

Expand Down Expand Up @@ -894,6 +909,7 @@ export async function renderLocal(
entryFile: options.entryFile,
outputResolution: options.outputResolution,
outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic,
provenance: options.provenance,
debug: options.debug,
strictness: options.bestEffort === false ? "strict" : "best-effort",
},
Expand Down
Loading
Loading