fix(cli,core,lint,producer): terminate ffprobe options at every call site

#2740 added `--` to one of nine independent ffprobe invocations, so the
bug class it closed stayed open everywhere else while CI reported it
fixed — the regression test asserts the argv of that single site.

Reproduced on ffprobe 8.1.1: an asset named `-intro.mp4` probes fine
through extractMediaMetadata but fails with "Missing argument for option
'intro.mp4'" in audio pad/trim (mid-render), `hyperframes init`, whisper
duration probing and webmAlphaCheck. hevcPreviewLint catches and returns
false, so a dash-prefixed HEVC preview silently passes the lint rule.

Terminated at all of them:
  producer/services/render/audioPadTrim.ts (x2)
  producer/plan-parity-analysis.ts
  cli/commands/init.ts
  cli/utils/webmAlphaCheck.ts
  cli/whisper/transcribe.ts (x2)
  core/mediaGradeAnalyzer.ts
  lint/hevcPreviewLint.ts

audioPadTrim's runFfprobeJson is a near-verbatim clone of the engine's
runFfprobe and structurally cannot add the terminator itself, because
callers bake the input path into `args`. It now asserts the terminator
is present rather than letting a dash-prefixed path through, takes the
same stdio ["ignore", ...] as the engine helper, and redacts its stderr
— it was throwing raw ffprobe output, which echoes the input path, into
logs and telemetry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-08-04 02:23:34 -07:00
co-authored by Claude Opus 5
parent 8e41fa17b9
commit 47564ab94c
7 changed files with 20 additions and 4 deletions
+1 -1
View File
@@ -109,7 +109,7 @@ function probeVideo(filePath: string): VideoMeta | undefined {
if (!ffprobePath) return undefined; if (!ffprobePath) return undefined;
const raw = execFileSync( const raw = execFileSync(
ffprobePath, ffprobePath,
["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath], ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "--", filePath],
{ encoding: "utf-8", timeout: 15_000 }, { encoding: "utf-8", timeout: 15_000 },
); );
+1
View File
@@ -86,6 +86,7 @@ function probeWebmAlpha(filePath: string): WebmAlphaProbe {
"stream=codec_name:stream_tags=alpha_mode", "stream=codec_name:stream_tags=alpha_mode",
"-of", "-of",
"json", "json",
"--",
filePath, filePath,
], ],
{ encoding: "utf-8", timeout: 15_000 }, { encoding: "utf-8", timeout: 15_000 },
+2 -1
View File
@@ -171,6 +171,7 @@ function getMediaDurationSeconds(filePath: string): number | null {
"format=duration", "format=duration",
"-of", "-of",
"default=noprint_wrappers=1:nokey=1", "default=noprint_wrappers=1:nokey=1",
"--",
filePath, filePath,
], ],
{ encoding: "utf-8", timeout: 10_000 }, { encoding: "utf-8", timeout: 10_000 },
@@ -329,7 +330,7 @@ function isWav16kMono(filePath: string): boolean {
if (!ffprobePath) return false; if (!ffprobePath) return false;
const raw = execFileSync( const raw = execFileSync(
ffprobePath, ffprobePath,
["-v", "quiet", "-print_format", "json", "-show_streams", filePath], ["-v", "quiet", "-print_format", "json", "-show_streams", "--", filePath],
{ encoding: "utf-8", timeout: 10_000 }, { encoding: "utf-8", timeout: 10_000 },
); );
const parsed: { const parsed: {
+1
View File
@@ -121,6 +121,7 @@ function probeMedia(mediaPath: string, ffprobePath: string): GradeMediaProbe {
"stream=color_space,color_transfer,color_primaries,pix_fmt,duration:format=duration", "stream=color_space,color_transfer,color_primaries,pix_fmt,duration:format=duration",
"-of", "-of",
"json", "json",
"--",
mediaPath, mediaPath,
], ],
{ encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"] }, { encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"] },
+1
View File
@@ -57,6 +57,7 @@ async function probeIsHevc(ffprobePath: string, filePath: string): Promise<boole
"stream=codec_name", "stream=codec_name",
"-of", "-of",
"json", "json",
"--",
filePath, filePath,
]); ]);
return hasHevcStream(JSON.parse(stdout)); return hasHevcStream(JSON.parse(stdout));
@@ -119,6 +119,7 @@ function probeMetadata(outputPath: string): PlanParityStreamMetadata {
].join(":"), ].join(":"),
"-of", "-of",
"json", "json",
"--",
outputPath, outputPath,
]); ]);
return normalizeFfprobeMetadata(JSON.parse(bytes.toString("utf-8")) as unknown); return normalizeFfprobeMetadata(JSON.parse(bytes.toString("utf-8")) as unknown);
@@ -30,6 +30,7 @@ import {
trackChildProcess, trackChildProcess,
type AudioMetadata, type AudioMetadata,
} from "@hyperframes/engine"; } from "@hyperframes/engine";
import { redactTelemetryString } from "@hyperframes/core";
/** /**
* Tolerance used to decide whether an audio file is already short enough to * Tolerance used to decide whether an audio file is already short enough to
@@ -361,6 +362,7 @@ async function defaultProbeVideoFrameInfo(
"stream=nb_frames,r_frame_rate", "stream=nb_frames,r_frame_rate",
"-of", "-of",
"json", "json",
"--",
videoPath, videoPath,
], ],
signal, signal,
@@ -381,6 +383,7 @@ async function defaultProbeVideoFrameInfo(
"stream=nb_read_packets,r_frame_rate", "stream=nb_read_packets,r_frame_rate",
"-of", "-of",
"json", "json",
"--",
videoPath, videoPath,
], ],
signal, signal,
@@ -434,7 +437,13 @@ async function defaultRunFfmpeg(
// ── ffprobe JSON runner (shared between fast/slow video probe paths) ───── // ── ffprobe JSON runner (shared between fast/slow video probe paths) ─────
async function runFfprobeJson<T>(args: string[], signal?: AbortSignal): Promise<T> { async function runFfprobeJson<T>(args: string[], signal?: AbortSignal): Promise<T> {
const proc = spawn(getFfprobeBinary(), args); // Callers bake the input path into `args` (terminated with "--"), so this
// helper cannot add the terminator itself — assert they did rather than
// let a dash-prefixed path silently reach ffprobe as an option.
if (!args.includes("--")) {
throw new Error('[audioPadTrim] ffprobe args must terminate options with "--".');
}
const proc = spawn(getFfprobeBinary(), args, { stdio: ["ignore", "pipe", "pipe"] });
trackChildProcess(proc); trackChildProcess(proc);
let stdout = ""; let stdout = "";
proc.stdout.on("data", (data: Buffer) => { proc.stdout.on("data", (data: Buffer) => {
@@ -452,7 +461,9 @@ async function runFfprobeJson<T>(args: string[], signal?: AbortSignal): Promise<
throw outcome.error ?? new Error(outcome.stderr); throw outcome.error ?? new Error(outcome.stderr);
} }
if (outcome.reason !== "exit" || outcome.exitCode !== 0) { if (outcome.reason !== "exit" || outcome.exitCode !== 0) {
throw new Error(`ffprobe ${outcome.reason}: ${outcome.stderr}`); // Redacted: raw ffprobe stderr echoes the input path, and this message
// reaches logs and telemetry.
throw new Error(`ffprobe ${outcome.reason}: ${redactTelemetryString(outcome.stderr, 2000)}`);
} }
try { try {
return JSON.parse(stdout) as T; return JSON.parse(stdout) as T;