mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(cli): show output video length in render summary, not render time (#1812)
The render-complete summary printed `<fileSize> · <time> · completed` where `<time>` was the wall-clock render duration. Presented as a bare middle value, users read it as the video length and compared it to ffprobe, repeatedly reporting a "wrong duration". Show the actual output video length (from the perf summary's compositionDurationSeconds, which equals the rendered frame span) as the primary figure and label the render time explicitly: `<fileSize> · <videoLength> video · rendered in <renderTime>`. png-sequence (directory) output has no single muxed video, so it shows a frame count instead; when neither is known the summary falls back to render time only. Docker renders run the producer in a child process with no perf summary threaded back, so they show render time only rather than a misleading number.
This commit is contained in:
@@ -55,7 +55,7 @@ import { lintProject, shouldBlockRender } from "../utils/lintProject.js";
|
|||||||
import { formatLintFindings } from "../utils/lintFormat.js";
|
import { formatLintFindings } from "../utils/lintFormat.js";
|
||||||
import { loadProducer } from "../utils/producer.js";
|
import { loadProducer } from "../utils/producer.js";
|
||||||
import { c } from "../ui/colors.js";
|
import { c } from "../ui/colors.js";
|
||||||
import { formatBytes, formatDuration, errorBox } from "../ui/format.js";
|
import { formatBytes, formatRenderSummaryDetail, errorBox } from "../ui/format.js";
|
||||||
import { renderProgress } from "../ui/progress.js";
|
import { renderProgress } from "../ui/progress.js";
|
||||||
import {
|
import {
|
||||||
trackRenderComplete,
|
trackRenderComplete,
|
||||||
@@ -1227,6 +1227,9 @@ async function renderDocker(
|
|||||||
...getMemorySnapshot(),
|
...getMemorySnapshot(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ponytail: Docker runs the producer in a child process, so no perfSummary is
|
||||||
|
// threaded back here; the summary shows render time only (never a wrong video
|
||||||
|
// length). Probe the output with ffprobe if a duration figure is wanted here.
|
||||||
printRenderComplete(outputPath, elapsed, options.quiet);
|
printRenderComplete(outputPath, elapsed, options.quiet);
|
||||||
if (options.exitAfterComplete) scheduleRenderProcessExit();
|
if (options.exitAfterComplete) scheduleRenderProcessExit();
|
||||||
return { renderTimeMs: elapsed };
|
return { renderTimeMs: elapsed };
|
||||||
@@ -1323,7 +1326,13 @@ export async function renderLocal(
|
|||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
const elapsed = Date.now() - startTime;
|
||||||
trackRenderMetrics(job, elapsed, options, false);
|
trackRenderMetrics(job, elapsed, options, false);
|
||||||
printRenderComplete(outputPath, elapsed, options.quiet);
|
printRenderComplete(
|
||||||
|
outputPath,
|
||||||
|
elapsed,
|
||||||
|
options.quiet,
|
||||||
|
job.perfSummary?.compositionDurationSeconds,
|
||||||
|
job.perfSummary?.totalFrames,
|
||||||
|
);
|
||||||
if (!options.skipFeedback) {
|
if (!options.skipFeedback) {
|
||||||
await maybePromptRenderFeedback({
|
await maybePromptRenderFeedback({
|
||||||
renderDurationMs: elapsed,
|
renderDurationMs: elapsed,
|
||||||
@@ -1546,12 +1555,20 @@ function trackRenderMetrics(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function printRenderComplete(outputPath: string, elapsedMs: number, quiet: boolean): void {
|
function printRenderComplete(
|
||||||
|
outputPath: string,
|
||||||
|
elapsedMs: number,
|
||||||
|
quiet: boolean,
|
||||||
|
outputDurationSeconds?: number,
|
||||||
|
frameCount?: number,
|
||||||
|
): void {
|
||||||
if (quiet) return;
|
if (quiet) return;
|
||||||
|
|
||||||
let fileSize = "unknown";
|
let fileSize = "unknown";
|
||||||
|
let isDirectory = false;
|
||||||
try {
|
try {
|
||||||
const stat = statSync(outputPath);
|
const stat = statSync(outputPath);
|
||||||
|
isDirectory = stat.isDirectory();
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
// png-sequence output is a directory; sum the contained file sizes so
|
// png-sequence output is a directory; sum the contained file sizes so
|
||||||
// the user sees the on-disk footprint of the deliverable rather than
|
// the user sees the on-disk footprint of the deliverable rather than
|
||||||
@@ -1573,8 +1590,13 @@ function printRenderComplete(outputPath: string, elapsedMs: number, quiet: boole
|
|||||||
// file doesn't exist or is inaccessible
|
// file doesn't exist or is inaccessible
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = formatDuration(elapsedMs);
|
const detail = formatRenderSummaryDetail({
|
||||||
|
elapsedMs,
|
||||||
|
outputDurationSeconds,
|
||||||
|
isDirectory,
|
||||||
|
frameCount,
|
||||||
|
});
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log(c.success("\u25C7") + " " + c.accent(outputPath));
|
console.log(c.success("\u25C7") + " " + c.accent(outputPath));
|
||||||
console.log(" " + c.bold(fileSize) + c.dim(" \u00B7 " + duration + " \u00B7 completed"));
|
console.log(" " + c.bold(fileSize) + c.dim(" \u00B7 " + detail));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { formatRenderSummaryDetail } from "./format.js";
|
||||||
|
|
||||||
|
describe("formatRenderSummaryDetail", () => {
|
||||||
|
it("shows the output video length as the primary figure and labels render time", () => {
|
||||||
|
// Output is 71.7s of video but only took 34.2s of wall-clock to render:
|
||||||
|
// the two must be distinguishable so users stop comparing render time to ffprobe.
|
||||||
|
const detail = formatRenderSummaryDetail({
|
||||||
|
elapsedMs: 34_200,
|
||||||
|
outputDurationSeconds: 71.7,
|
||||||
|
isDirectory: false,
|
||||||
|
});
|
||||||
|
expect(detail).toBe("1m 11.7s video · rendered in 34.2s");
|
||||||
|
expect(detail).toContain("video");
|
||||||
|
expect(detail).toContain("rendered in");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits the video figure gracefully when the duration is unknown", () => {
|
||||||
|
expect(formatRenderSummaryDetail({ elapsedMs: 5_000, isDirectory: false })).toBe(
|
||||||
|
"rendered in 5.0s",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a frame count for png-sequence directory output instead of a video length", () => {
|
||||||
|
const detail = formatRenderSummaryDetail({
|
||||||
|
elapsedMs: 12_000,
|
||||||
|
isDirectory: true,
|
||||||
|
frameCount: 120,
|
||||||
|
// a stray duration must not leak into directory output
|
||||||
|
outputDurationSeconds: 4,
|
||||||
|
});
|
||||||
|
expect(detail).toBe("120 frames · rendered in 12.0s");
|
||||||
|
expect(detail).not.toContain("video");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not crash and shows only render time for a directory with no frame count", () => {
|
||||||
|
expect(formatRenderSummaryDetail({ elapsedMs: 1_000, isDirectory: true })).toBe(
|
||||||
|
"rendered in 1.0s",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,31 @@ export function formatDuration(ms: number): string {
|
|||||||
return `${minutes}m ${remaining.toFixed(1)}s`;
|
return `${minutes}m ${remaining.toFixed(1)}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the detail portion of the render-complete summary (everything after the
|
||||||
|
* file size). The output video length is shown as the primary figure, with the
|
||||||
|
* wall-clock render time explicitly labeled "rendered in" so the two are never
|
||||||
|
* confused (users were comparing the render time to ffprobe's media duration).
|
||||||
|
* Directory (png-sequence) output has no single muxed video, so it shows a frame
|
||||||
|
* count instead, or just the render time when neither is known.
|
||||||
|
*/
|
||||||
|
export function formatRenderSummaryDetail(input: {
|
||||||
|
elapsedMs: number;
|
||||||
|
outputDurationSeconds?: number;
|
||||||
|
isDirectory: boolean;
|
||||||
|
frameCount?: number;
|
||||||
|
}): string {
|
||||||
|
const middle = input.isDirectory
|
||||||
|
? input.frameCount != null
|
||||||
|
? `${input.frameCount} frames`
|
||||||
|
: undefined
|
||||||
|
: input.outputDurationSeconds != null && input.outputDurationSeconds > 0
|
||||||
|
? `${formatDuration(input.outputDurationSeconds * 1000)} video`
|
||||||
|
: undefined;
|
||||||
|
const renderTime = `rendered in ${formatDuration(input.elapsedMs)}`;
|
||||||
|
return [middle, renderTime].filter(Boolean).join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
export function label(name: string, value: string): string {
|
export function label(name: string, value: string): string {
|
||||||
const pad = 14 - name.length;
|
const pad = 14 - name.length;
|
||||||
return ` ${c.dim(name)}${" ".repeat(Math.max(1, pad))}${c.bold(value)}`;
|
return ` ${c.dim(name)}${" ".repeat(Math.max(1, pad))}${c.bold(value)}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user