feat(tools): 2.0 phase 5 wave 3b - audio depth (14 tools) (#222)

This commit is contained in:
SnapOtter
2026-06-13 10:19:06 +08:00
parent 5f98b48593
commit 638288e196
81 changed files with 7646 additions and 718 deletions
+6 -4
View File
@@ -13,12 +13,14 @@ const STDERR_RING_MAX = 16 * 1024;
/**
* Runs ffmpeg with `-progress pipe:1` appended, parsing progress blocks from
* stdout. Rejects with the tail of stderr on non-zero exit, timeout or abort.
* Output must go to a FILE path in args (no stdout piping of media data).
* Resolves with the captured stderr tail (filters like silencedetect report
* there). Output must go to a FILE path in args (no stdout piping of media
* data).
*/
export async function runFfmpeg(args: string[], opts: RunFfmpegOptions = {}): Promise<void> {
export async function runFfmpeg(args: string[], opts: RunFfmpegOptions = {}): Promise<string> {
const bin = resolveFfmpeg();
if (!bin) throw new Error("ffmpeg binary not found (set FFMPEG_PATH or install ffmpeg)");
await new Promise<void>((resolvePromise, reject) => {
return new Promise<string>((resolvePromise, reject) => {
const child = spawn(bin, ["-hide_banner", "-nostdin", "-y", ...args, "-progress", "pipe:1"], {
stdio: ["ignore", "pipe", "pipe"],
});
@@ -73,7 +75,7 @@ export async function runFfmpeg(args: string[], opts: RunFfmpegOptions = {}): Pr
if (settled) return;
settled = true;
cleanup();
if (code === 0) resolvePromise();
if (code === 0) resolvePromise(stderrTail);
else reject(new Error(`ffmpeg exited ${code ?? signal}: ${stderrTail.slice(-2000)}`));
});
});
+16 -1
View File
@@ -7,6 +7,7 @@ export interface MediaStreamInfo {
width?: number;
height?: number;
sampleRate?: number;
channels?: number;
}
export interface MediaInfo {
@@ -14,6 +15,7 @@ export interface MediaInfo {
durationS: number | null;
bitrateKbps: number | null;
streams: MediaStreamInfo[];
tags?: Record<string, string>;
}
export interface ProbeOptions {
@@ -70,17 +72,28 @@ export async function probeMedia(filePath: string, opts: ProbeOptions = {}): Pro
});
});
const parsed = JSON.parse(stdout) as {
format?: { format_name?: string; duration?: string; bit_rate?: string };
format?: {
format_name?: string;
duration?: string;
bit_rate?: string;
tags?: Record<string, unknown>;
};
streams?: Array<{
codec_type?: string;
codec_name?: string;
width?: number;
height?: number;
sample_rate?: string;
channels?: number;
}>;
};
const duration = parsed.format?.duration ? Number(parsed.format.duration) : null;
const bitRate = parsed.format?.bit_rate ? Number(parsed.format.bit_rate) : null;
const rawTags = (parsed.format?.tags ?? {}) as Record<string, unknown>;
const tags: Record<string, string> = {};
for (const [k, v] of Object.entries(rawTags)) {
if (typeof v === "string" && v.length > 0) tags[k.toLowerCase()] = v;
}
return {
container: parsed.format?.format_name ?? "unknown",
durationS: Number.isFinite(duration as number) ? (duration as number) : null,
@@ -93,7 +106,9 @@ export async function probeMedia(filePath: string, opts: ProbeOptions = {}): Pro
width: s.width,
height: s.height,
sampleRate: Number.isFinite(sr) && (sr as number) > 0 ? sr : undefined,
channels: typeof s.channels === "number" && s.channels > 0 ? s.channels : undefined,
} as MediaStreamInfo;
}),
tags: Object.keys(tags).length > 0 ? tags : undefined,
};
}