diff --git a/packages/cli/src/commands/media-treatment.test.ts b/packages/cli/src/commands/media-treatment.test.ts index bbcecbaf5..d525a6c64 100644 --- a/packages/cli/src/commands/media-treatment.test.ts +++ b/packages/cli/src/commands/media-treatment.test.ts @@ -1,6 +1,6 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { basename, join } from "node:path"; import { runCommand } from "citty"; import { describe, expect, it, vi } from "vitest"; import { @@ -211,11 +211,13 @@ describe("applyMediaTreatmentToHtml", () => { it("resolves nested composition media through the shared project-root contract", () => { const project = mkdtempSync(join(tmpdir(), "hf-media-treatment-assets-")); + const escapedAsset = join(project, "..", `${basename(project)}-escape.mp4`); mkdirSync(join(project, "capture"), { recursive: true }); mkdirSync(join(project, "assets"), { recursive: true }); writeFileSync(join(project, "capture", "talking-head.mp4"), ""); writeFileSync(join(project, "assets", "photo.webp"), ""); writeFileSync(join(project, "assets", "My Clip.mp4"), ""); + writeFileSync(escapedAsset, ""); try { expect( @@ -248,8 +250,16 @@ describe("applyMediaTreatmentToHtml", () => { expect(() => resolveMediaTreatmentSource(project, "compositions/scene.html", "missing.mp4"), ).toThrow(/Media file not found/); + expect(() => + resolveMediaTreatmentSource( + project, + "compositions/scene.html", + `../../${basename(escapedAsset)}`, + ), + ).toThrow(/Media file not found/); } finally { rmSync(project, { recursive: true, force: true }); + rmSync(escapedAsset, { force: true }); } }); diff --git a/packages/core/src/mediaGradeAnalyzer.vendoredParity.test.ts b/packages/core/src/mediaGradeAnalyzer.vendoredParity.test.ts index b5d186129..7f0c55f69 100644 --- a/packages/core/src/mediaGradeAnalyzer.vendoredParity.test.ts +++ b/packages/core/src/mediaGradeAnalyzer.vendoredParity.test.ts @@ -28,6 +28,26 @@ lavfi.signalstats.UAVG=125 lavfi.signalstats.VAVG=135 lavfi.signalstats.SATAVG=60`; +const UNDEREXPOSED_SIGNALSTATS = `frame:0 pts:0 pts_time:0 +lavfi.signalstats.YMIN=2 +lavfi.signalstats.YLOW=8 +lavfi.signalstats.YAVG=50 +lavfi.signalstats.YHIGH=120 +lavfi.signalstats.YMAX=135 +lavfi.signalstats.UAVG=128 +lavfi.signalstats.VAVG=128 +lavfi.signalstats.SATAVG=30`; + +const BLOWN_HIGHLIGHT_SIGNALSTATS = `frame:0 pts:0 pts_time:0 +lavfi.signalstats.YMIN=70 +lavfi.signalstats.YLOW=90 +lavfi.signalstats.YAVG=200 +lavfi.signalstats.YHIGH=248 +lavfi.signalstats.YMAX=255 +lavfi.signalstats.UAVG=128 +lavfi.signalstats.VAVG=128 +lavfi.signalstats.SATAVG=30`; + const PROBE: GradeMediaProbe = { duration: 4, colorSpace: "bt2020nc", @@ -36,8 +56,8 @@ const PROBE: GradeMediaProbe = { pixelFormat: "yuv420p10le", }; -function productResult() { - const frames = parseMediaTreatmentSignalStats(SIGNALSTATS); +function productResult(signalStats = SIGNALSTATS) { + const frames = parseMediaTreatmentSignalStats(signalStats); const summary = summarizeMediaTreatmentAnalysis(PROBE, frames); return { frames, @@ -47,13 +67,13 @@ function productResult() { }; } -async function vendoredResult() { +async function vendoredResult(signalStats = SIGNALSTATS) { const moduleUrl = new URL( "../../../skills/media-use/scripts/lib/grade-analyzer.mjs", import.meta.url, ); const analyzer = await import(moduleUrl.href); - const frames = analyzer.parseMediaTreatmentSignalStats(SIGNALSTATS); + const frames = analyzer.parseMediaTreatmentSignalStats(signalStats); const summary = analyzer.summarizeMediaTreatmentAnalysis(PROBE, frames); return { frames, @@ -67,4 +87,14 @@ describe("vendored media grade analyzer parity", () => { it("keeps the standalone skill copy aligned with the typed product module", async () => { expect(await vendoredResult()).toEqual(productResult()); }); + + it.each([ + ["underexposed", UNDEREXPOSED_SIGNALSTATS, 1], + ["blown-highlight", BLOWN_HIGHLIGHT_SIGNALSTATS, -1], + ])("covers the %s exposure branch", async (_, signalStats, expectedSign) => { + const product = productResult(signalStats); + + expect(Math.sign(product.adjust.adjust.exposure)).toBe(expectedSign); + expect(await vendoredResult(signalStats)).toEqual(product); + }); });