import { defineCommand } from "citty"; import type { Example } from "./_examples.js"; import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, rmSync } from "node:fs"; import { reportVariableIssues, resolveVariablesArg, validateVariablesAgainstProject, } from "../utils/variables.js"; import { parseGifLoopArg, resolveBrowserTimeoutMsArg, resolveCompositionEntryArg, resolveDefaultFpsArg, } from "../utils/renderArgs.js"; export const examples: Example[] = [ ["Render to MP4", "hyperframes render --output output.mp4"], ["Render a specific composition", "hyperframes render -c compositions/intro.html -o intro.mp4"], [ "Upsample any composition to 4K (supersamples via Chrome DPR)", "hyperframes render --resolution 4k --output 4k.mp4", ], ["Render transparent overlay (ProRes)", "hyperframes render --format mov --output overlay.mov"], ["Render transparent WebM overlay", "hyperframes render --format webm --output overlay.webm"], [ "Render animated GIF for PRs/docs", "hyperframes render --format gif --fps 15 --gif-loop 0 --output demo.gif", ], [ "Render PNG sequence (RGBA frames for AE/Nuke/Fusion)", "hyperframes render --format png-sequence --output frames/", ], ["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"], ["Deterministic render via Docker", "hyperframes render --docker --output deterministic.mp4"], ["Parallel rendering with 6 workers", "hyperframes render --workers 6 --output fast.mp4"], ["Opt out of browser GPU render", "hyperframes render --no-browser-gpu --output cpu.mp4"], ["HDR output (auto-detected)", "hyperframes render --output hdr-output.mp4"], [ "Override composition variables (parametrized render)", 'hyperframes render --variables \'{"title":"Q4 Report","theme":"dark"}\' --output q4.mp4', ], [ "Variables from a JSON file", "hyperframes render --variables-file ./vars.json --output out.mp4", ], [ "Batch render one output per variables row", 'hyperframes render --batch rows.json --output "renders/{name}.mp4"', ], ]; import { cpus, freemem, tmpdir } from "node:os"; import { resolve, dirname, join, basename } from "node:path"; import { execFileSync, spawn } from "node:child_process"; import { resolveProject } from "../utils/project.js"; import { lintProject, shouldBlockRender } from "../utils/lintProject.js"; import { formatLintFindings } from "../utils/lintFormat.js"; import { loadProducer } from "../utils/producer.js"; import { c } from "../ui/colors.js"; import { formatBytes, formatRenderSummaryDetail, errorBox } from "../ui/format.js"; import { warnIfWebmAlphaDropped } from "../utils/webmAlphaCheck.js"; import { renderProgress } from "../ui/progress.js"; import { trackRenderComplete, trackRenderError, trackRenderObservation, trackRenderPreflightRejected, } from "../telemetry/events.js"; import { maybePromptRenderFeedback } from "../telemetry/feedback.js"; import { renderJobObservabilityTelemetryPayload } from "../telemetry/renderObservability.js"; import { normalizeSkillSlug } from "../telemetry/skill.js"; import { bytesToMb } from "../telemetry/system.js"; import { VERSION } from "../version.js"; import { isDevMode } from "../utils/env.js"; import { buildDockerRunArgs, resolveDockerPlatform } from "../utils/dockerRunArgs.js"; import { normalizeErrorMessage } from "../utils/errorMessage.js"; import { runEnvironmentChecks } from "../browser/preflight.js"; import { chromeLaunchRemediation } from "../browser/linuxDeps.js"; import type { ProducerLogger, RenderJob } from "@hyperframes/producer"; import { MAX_VP9_CPU_USED, MIN_VP9_CPU_USED, isVideoFrameFormat, type VideoFrameFormat, } from "@hyperframes/engine"; import { normalizeResolutionFlag, checkOutputResolutionCompatibility, parseFps, fpsToNumber, fpsToFfmpegArg, type CanvasResolution, type OutputResolutionIssueKind, type Fps, type FpsParseResult, } from "@hyperframes/core"; const VALID_QUALITY = new Set(["draft", "standard", "high"]); /** * Map a {@link FpsParseResult} failure reason to a human-friendly * error-box message. The empty / undefined / default-fallthrough case * shouldn't be reachable from the CLI flag (citty supplies a default of * "30") but the branch exists so this helper can be reused by other * fps-accepting CLI surfaces in the future. */ function formatFpsParseError( input: string, reason: Exclude["reason"], ): string { switch (reason) { case "empty": return "Frame rate must not be empty."; case "not-a-number": return `Got "${input}". Frame rate must be an integer (e.g. 30) or a rational (e.g. 30000/1001 for NTSC).`; case "non-positive": return `Got "${input}". Frame rate must be greater than zero.`; case "out-of-range": return `Got "${input}". Frame rate must be in the range 1–240.`; case "invalid-fraction": return `Got "${input}". Rational frame rates must be two positive integers separated by '/' (e.g. 30000/1001).`; case "ambiguous-decimal": return `Got "${input}". Decimal frame rates are ambiguous — use the exact rational form instead (e.g. 30000/1001 for 29.97).`; } } const RENDER_FORMATS = ["mp4", "webm", "mov", "png-sequence", "gif"] as const; type RenderFormat = (typeof RENDER_FORMATS)[number]; const VALID_FORMAT = new Set(RENDER_FORMATS); const RENDER_FORMAT_LABEL = "mp4, webm, mov, png-sequence, or gif"; // `png-sequence` writes a directory of frames rather than a single muxed file, // so its "extension" is empty — the auto-output path becomes a directory name. const FORMAT_EXT: Record = { mp4: ".mp4", webm: ".webm", mov: ".mov", "png-sequence": "", gif: ".gif", }; const CPU_CORE_COUNT = cpus().length; function parseRenderFormat(input: string): RenderFormat | undefined { if (!VALID_FORMAT.has(input)) return undefined; return RENDER_FORMATS.find((format) => format === input); } export default defineCommand({ meta: { name: "render", description: "Render a composition to MP4, WebM, MOV, GIF, or a PNG sequence", }, args: { dir: { type: "positional", description: "Project directory", required: false, }, composition: { type: "string", alias: "c", description: "Render a specific composition file instead of index.html (e.g. compositions/intro.html). " + "Sub-compositions using