mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
feat(render): auto-detect HDR from media probes, add --sdr flag (#526)
* feat(render): auto-detect HDR from media probes, add --sdr flag Replace the --hdr opt-in model with automatic detection. When no flags are passed, the renderer probes all video/image sources and enables HDR output if any HDR color space is detected. Existing --hdr flag becomes a force override. New --sdr flag forces SDR output. Behavior matrix: (no flags) + HDR content → HDR output (no flags) + SDR content → SDR output --hdr → force HDR (defaults to HLG if no HDR sources) --sdr → force SDR (skips probing) --hdr --sdr → error Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: align HDR auto-detect docs and tests --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4d0e262eb3
commit
8e5593b6ba
@@ -9,7 +9,7 @@ export const examples: Example[] = [
|
||||
["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"],
|
||||
["HDR output (H.265 10-bit)", "hyperframes render --hdr --output hdr-output.mp4"],
|
||||
["HDR output (auto-detected)", "hyperframes render --output hdr-output.mp4"],
|
||||
];
|
||||
import { cpus, freemem, tmpdir } from "node:os";
|
||||
import { resolve, dirname, join, basename } from "node:path";
|
||||
@@ -82,7 +82,12 @@ export default defineCommand({
|
||||
},
|
||||
hdr: {
|
||||
type: "boolean",
|
||||
description: "Enable HDR: probe sources for PQ/HLG, output H.265 10-bit BT.2020",
|
||||
description: "Force HDR output even if no HDR sources are detected",
|
||||
default: false,
|
||||
},
|
||||
sdr: {
|
||||
type: "boolean",
|
||||
description: "Force SDR output even if HDR sources are detected",
|
||||
default: false,
|
||||
},
|
||||
crf: {
|
||||
@@ -293,6 +298,12 @@ export default defineCommand({
|
||||
}
|
||||
}
|
||||
|
||||
// ── Validate HDR/SDR mutual exclusion ────────────────────────────────
|
||||
if (args.hdr && args.sdr) {
|
||||
console.error("Error: --hdr and --sdr are mutually exclusive.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────
|
||||
if (useDocker) {
|
||||
await renderDocker(project.dir, outputPath, {
|
||||
@@ -301,7 +312,7 @@ export default defineCommand({
|
||||
format,
|
||||
workers,
|
||||
gpu: useGpu,
|
||||
hdr: args.hdr ?? false,
|
||||
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
|
||||
crf,
|
||||
videoBitrate,
|
||||
quiet,
|
||||
@@ -313,7 +324,7 @@ export default defineCommand({
|
||||
format,
|
||||
workers,
|
||||
gpu: useGpu,
|
||||
hdr: args.hdr ?? false,
|
||||
hdrMode: args.sdr ? "force-sdr" : args.hdr ? "force-hdr" : "auto",
|
||||
crf,
|
||||
videoBitrate,
|
||||
quiet,
|
||||
@@ -329,7 +340,7 @@ interface RenderOptions {
|
||||
format: "mp4" | "webm" | "mov";
|
||||
workers?: number;
|
||||
gpu: boolean;
|
||||
hdr: boolean;
|
||||
hdrMode: "auto" | "force-hdr" | "force-sdr";
|
||||
crf?: number;
|
||||
videoBitrate?: string;
|
||||
quiet: boolean;
|
||||
@@ -453,7 +464,7 @@ async function renderDocker(
|
||||
format: options.format,
|
||||
workers: options.workers,
|
||||
gpu: options.gpu,
|
||||
hdr: options.hdr,
|
||||
hdrMode: options.hdrMode,
|
||||
crf: options.crf,
|
||||
videoBitrate: options.videoBitrate,
|
||||
quiet: options.quiet,
|
||||
@@ -519,7 +530,7 @@ async function renderLocal(
|
||||
format: options.format,
|
||||
workers: options.workers,
|
||||
useGpu: options.gpu,
|
||||
hdr: options.hdr,
|
||||
hdrMode: options.hdrMode,
|
||||
crf: options.crf,
|
||||
videoBitrate: options.videoBitrate,
|
||||
});
|
||||
|
||||
@@ -58,9 +58,14 @@ function readCache<T>(path: string): T | undefined {
|
||||
}
|
||||
|
||||
function writeCache<T>(path: string, data: T): void {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
const entry: CacheEntry<T> = { fetchedAt: Date.now(), data };
|
||||
writeFileSync(path, JSON.stringify(entry), "utf-8");
|
||||
try {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
const entry: CacheEntry<T> = { fetchedAt: Date.now(), data };
|
||||
writeFileSync(path, JSON.stringify(entry), "utf-8");
|
||||
} catch {
|
||||
// Cache writes are opportunistic. A read-only home directory or sandboxed
|
||||
// environment should not make the registry appear unreachable.
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fetchers ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -6,7 +6,7 @@ const BASE: DockerRenderOptions = {
|
||||
quality: "standard",
|
||||
format: "mp4",
|
||||
gpu: false,
|
||||
hdr: false,
|
||||
hdrMode: "auto",
|
||||
crf: undefined,
|
||||
videoBitrate: undefined,
|
||||
quiet: false,
|
||||
@@ -57,9 +57,8 @@ describe("buildDockerRunArgs", () => {
|
||||
...FIXED_INPUT,
|
||||
options: {
|
||||
...BASE,
|
||||
workers: 4,
|
||||
gpu: true,
|
||||
hdr: true,
|
||||
hdrMode: "force-hdr",
|
||||
crf: 18,
|
||||
videoBitrate: undefined,
|
||||
quiet: true,
|
||||
@@ -88,8 +87,6 @@ describe("buildDockerRunArgs", () => {
|
||||
"standard",
|
||||
"--format",
|
||||
"mp4",
|
||||
"--workers",
|
||||
"4",
|
||||
"--crf",
|
||||
"18",
|
||||
"--quiet",
|
||||
@@ -102,17 +99,28 @@ describe("buildDockerRunArgs", () => {
|
||||
// Regression for the original PR feedback: --hdr was silently dropped from
|
||||
// the docker arg array. Keep this assertion explicit (in addition to the
|
||||
// snapshot above) so the failure message points directly at the flag.
|
||||
it("forwards --hdr to the container when hdr is enabled", () => {
|
||||
it("forwards --hdr to the container when hdrMode is force-hdr", () => {
|
||||
const args = buildDockerRunArgs({
|
||||
...FIXED_INPUT,
|
||||
options: { ...BASE, hdr: true },
|
||||
options: { ...BASE, hdrMode: "force-hdr" },
|
||||
});
|
||||
expect(args).toContain("--hdr");
|
||||
expect(args).not.toContain("--sdr");
|
||||
});
|
||||
|
||||
it("omits --hdr when hdr is disabled", () => {
|
||||
it("forwards --sdr to the container when hdrMode is force-sdr", () => {
|
||||
const args = buildDockerRunArgs({
|
||||
...FIXED_INPUT,
|
||||
options: { ...BASE, hdrMode: "force-sdr" },
|
||||
});
|
||||
expect(args).toContain("--sdr");
|
||||
expect(args).not.toContain("--hdr");
|
||||
});
|
||||
|
||||
it("omits --hdr and --sdr when hdrMode is auto", () => {
|
||||
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
|
||||
expect(args).not.toContain("--hdr");
|
||||
expect(args).not.toContain("--sdr");
|
||||
});
|
||||
|
||||
it("requests host GPU passthrough only when gpu is enabled", () => {
|
||||
@@ -140,7 +148,7 @@ describe("buildDockerRunArgs", () => {
|
||||
format: "webm",
|
||||
workers: 8,
|
||||
gpu: true,
|
||||
hdr: true,
|
||||
hdrMode: "force-hdr",
|
||||
crf: 16,
|
||||
videoBitrate: undefined,
|
||||
quiet: true,
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface DockerRenderOptions {
|
||||
format: "mp4" | "webm" | "mov";
|
||||
workers?: number;
|
||||
gpu: boolean;
|
||||
hdr: boolean;
|
||||
hdrMode: "auto" | "force-hdr" | "force-sdr";
|
||||
crf?: number;
|
||||
videoBitrate?: string;
|
||||
quiet: boolean;
|
||||
@@ -59,6 +59,7 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
|
||||
...(options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : []),
|
||||
...(options.quiet ? ["--quiet"] : []),
|
||||
...(options.gpu ? ["--gpu"] : []),
|
||||
...(options.hdr ? ["--hdr"] : []),
|
||||
...(options.hdrMode === "force-hdr" ? ["--hdr"] : []),
|
||||
...(options.hdrMode === "force-sdr" ? ["--sdr"] : []),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user