Skip to content
Merged
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
14 changes: 14 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ A render stops when the process that started it exits. To keep one running after
its launcher exits, set `HYPERFRAMES_RENDER_DETACHED=1` and start it in its own
process group, e.g. `bash -c 'set -m; HYPERFRAMES_RENDER_DETACHED=1 npx hyperframes render -o out.mp4 < /dev/null > render.log 2>&1 &'`.

Piped, non-quiet renders emit a single `@hf-diagnostic ` JSON line when an audio
failure reaches the CLI error handler. Version 1 has `operation: "render"` and
at most 16 `audioFailures`: allowlisted stage, reason, owner, retryable, and
optional subprocess exit code, signal, termination reason and OS code. It omits
messages, stderr, source paths/URLs, element IDs and command arguments. Consumers
must ignore unknown versions and fields. This is local CLI output; uploading it
requires the caller's existing consent and privacy checks. Quiet mode (including
batch JSON mode) emits no diagnostic line.

This covers audio preparation and mix failures, including the typed external
interruption path. Probe failures retain their existing stage/reason but do not
gain subprocess evidence here; browser/FX failures remain classified as internal.
Desktop ingestion and live report delivery are separate changes.

### `publish`

Upload a project directory and get a hosted URL that keeps working after the CLI exits.
Expand Down
63 changes: 63 additions & 0 deletions packages/cli/src/commands/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,69 @@ describe("renderLocal browser GPU config", () => {
vi.restoreAllMocks();
});

it.each([
{ quiet: false, interrupted: false, emits: true },
{ quiet: false, interrupted: true, emits: true },
{ quiet: true, interrupted: false, emits: false },
])("writes the failure diagnostic only for piped non-JSON output: %j", async (mode) => {
const projectDir = mkdtempSync(join(tmpdir(), "hf-render-diagnostic-"));
const failures = [
{
stage: "prepare",
reason: "ffmpeg_failed",
owner: "system",
retryable: false,
subprocess: { exitCode: 7, terminationReason: "exit" },
detail: "private-detail",
elementId: "private-id",
},
];
producerState.executeImpl = async (job) => {
if (mode.interrupted) throw Object.assign(new Error("failed"), { audioFailures: failures });
job.warnings = [
{
code: "audio_processing_failed",
message: "private-warning",
details: { audioFailures: failures },
},
];
throw new Error("failed");
};
const originalIsTTY = process.stdout.isTTY;
Object.defineProperty(process.stdout, "isTTY", { value: false, configurable: true });
let output = "";
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
output += String(chunk);
return true;
});
try {
await expect(
renderLocal(projectDir, join(projectDir, "out.mp4"), {
fps: { num: 30, den: 1 },
quality: "standard",
format: "mp4",
gpu: false,
browserGpuMode: "software",
hdrMode: "auto",
quiet: mode.quiet,
throwOnError: true,
}),
).rejects.toThrow("failed");
const lines = output.split("\n").filter((line) => line.startsWith("@hf-diagnostic "));
expect(lines).toHaveLength(mode.emits ? 1 : 0);
if (mode.emits) {
expect(
JSON.parse((lines[0] ?? "").slice("@hf-diagnostic ".length)).audioFailures[0].subprocess
.exitCode,
).toBe(7);
expect(lines[0] ?? "").not.toMatch(/private-/);
}
} finally {
Object.defineProperty(process.stdout, "isTTY", { value: originalIsTTY, configurable: true });
rmSync(projectDir, { recursive: true, force: true });
}
});

it("logs only warnings and errors from the producer under --quiet", async () => {
const options = {
fps: { num: 30, den: 1 },
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
errorBox,
} from "../ui/format.js";
import { warnIfWebmAlphaDropped } from "../utils/webmAlphaCheck.js";
import { renderFailureDiagnostic } from "../ui/renderDiagnostic.js";
import { renderProgress, renderMachineProgress } from "../ui/progress.js";
import {
trackRenderComplete,
Expand Down Expand Up @@ -1705,6 +1706,7 @@ function handleRenderError(
job?: RenderJob,
): never {
const message = normalizeErrorMessage(error);
renderFailureDiagnostic(job, error, options.quiet);
trackRenderError({
fps: fpsToNumber(options.fps),
quality: options.quality,
Expand Down
Loading
Loading