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 { loadProducer } from "../utils/producer.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 {
|
||||
trackRenderComplete,
|
||||
@@ -1227,6 +1227,9 @@ async function renderDocker(
|
||||
...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);
|
||||
if (options.exitAfterComplete) scheduleRenderProcessExit();
|
||||
return { renderTimeMs: elapsed };
|
||||
@@ -1323,7 +1326,13 @@ export async function renderLocal(
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
trackRenderMetrics(job, elapsed, options, false);
|
||||
printRenderComplete(outputPath, elapsed, options.quiet);
|
||||
printRenderComplete(
|
||||
outputPath,
|
||||
elapsed,
|
||||
options.quiet,
|
||||
job.perfSummary?.compositionDurationSeconds,
|
||||
job.perfSummary?.totalFrames,
|
||||
);
|
||||
if (!options.skipFeedback) {
|
||||
await maybePromptRenderFeedback({
|
||||
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;
|
||||
|
||||
let fileSize = "unknown";
|
||||
let isDirectory = false;
|
||||
try {
|
||||
const stat = statSync(outputPath);
|
||||
isDirectory = stat.isDirectory();
|
||||
if (stat.isDirectory()) {
|
||||
// png-sequence output is a directory; sum the contained file sizes so
|
||||
// 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
|
||||
}
|
||||
|
||||
const duration = formatDuration(elapsedMs);
|
||||
const detail = formatRenderSummaryDetail({
|
||||
elapsedMs,
|
||||
outputDurationSeconds,
|
||||
isDirectory,
|
||||
frameCount,
|
||||
});
|
||||
console.log("");
|
||||
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`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
const pad = 14 - name.length;
|
||||
return ` ${c.dim(name)}${" ".repeat(Math.max(1, pad))}${c.bold(value)}`;
|
||||
|
||||
Reference in New Issue
Block a user