evOo-local/packages/adapters/src/media-marks.test.ts
Claude 8d07ab17d4
feat(media): anonymization, marks, consent, and the render gate
prepareSources applies every face, mark, and region protection before the
compiler runs and fails closed when one cannot be honored: all face modes,
tracking with interpolation and grown holds over lost detections, reframe_out
crops with blur fallback, synthetic identity only through a licensed provider
(blur fallback otherwise), inpainting with a quality floor that falls back to
a cover, and person-bound marks re-found next to the face. The gate runs on
the render output: classifier at the profile rate plus cuts, face tracking
matched to people locally, pixel verification of every recorded protection,
consent and age proof (always for paid video), text and technical checks.
Auto-fix proposes only compliance repairs and stops after three rounds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLSCTnhge4umMnemGXWr1w
2026-10-03 18:57:04 +00:00

301 lines
11 KiB
TypeScript

import { copyFile, stat } from "node:fs/promises";
import path from "node:path";
import type { MediaInferenceProvider } from "@rakazo/adapter-kit";
import type { Edl } from "@rakazo/contracts";
import { afterEach, describe, expect, it, vi } from "vitest";
import { prepareSources } from "./media-anonymize.js";
import { ffmpegAvailable } from "./media-emulator-clips.js";
import { runGate } from "./media-gate.js";
import { anchorFromFace, createMark, placeAnchor, promptBox, updateMark } from "./media-marks.js";
import { createMediaPrivacyService } from "./media-privacy.js";
import type { PrivacyHarness } from "./media-privacy-fixtures.js";
import {
ACTOR,
cleanProbe,
createPrivacyHarness,
testEdl,
testProfile,
} from "./media-privacy-fixtures.js";
import { PrivacyUnavailableError } from "./media-privacy-io.js";
import { readPrivacyRecord } from "./media-privacy-plan.js";
const hasFfmpeg = ffmpegAvailable();
let harness: PrivacyHarness | null = null;
afterEach(async () => {
await harness?.cleanup();
harness = null;
});
const tattoo = { tMs: 500, box: { x: 0.6, y: 0.55, w: 0.2, h: 0.25 } };
async function render(h: PrivacyHarness, edl: Edl, name: string) {
const workDir = await h.workDir(name);
const prepared = await prepareSources(h.deps, {
actor: ACTOR,
projectId: "project-1",
edl,
quality: "final",
workDir,
});
return { workDir, prepared };
}
async function gate(h: PrivacyHarness, edl: Edl, workDir: string, prepared: string) {
const outputPath = path.join(workDir, "output.mp4");
await copyFile(prepared, outputPath);
return runGate(h.deps, {
actor: ACTOR,
projectId: "project-1",
edl,
profile: testProfile(),
outputPath,
ffprobe: cleanProbe(edl),
copy: { title: null, caption: null, tags: [] },
bundleHasDisclosure: false,
autoFixRound: 0,
});
}
describe("mark geometry", () => {
it("turns a point prompt into a small box inside the frame", () => {
expect(promptBox({ tMs: 0, point: { x: 0.99, y: 0.01 } })).toEqual({
x: 0.88,
y: 0,
w: 0.12,
h: 0.12,
});
expect(() => promptBox({ tMs: 0 })).toThrow(PrivacyUnavailableError);
});
it("keeps a mark's place relative to the face", () => {
const anchor = anchorFromFace(
{ x: 0.5, y: 0.5, w: 0.1, h: 0.1 },
{ x: 0.4, y: 0.3, w: 0.2, h: 0.2 },
);
const placed = placeAnchor(anchor, { x: 0.1, y: 0.1, w: 0.4, h: 0.4 })!;
expect(placed.x).toBeCloseTo(0.3);
expect(placed.y).toBeCloseTo(0.5);
expect(placed.w).toBeCloseTo(0.2);
expect(placeAnchor(anchor, { x: 0.95, y: 0.95, w: 0.4, h: 0.4 })).toBeNull();
});
});
describe.skipIf(!hasFfmpeg)("marks", () => {
it("segments, propagates, scores, and stores the mask under the media root", async () => {
harness = await createPrivacyHarness();
const asset = await harness.addAsset("clip.mp4");
const mark = await createMark(harness.deps, ACTOR, {
kind: "tattoo",
label: null,
personId: null,
assetId: asset.id,
prompt: tattoo,
treatment: "blur",
});
expect(mark).toMatchObject({ treatment: "blur", qualityScore: 0.9, suggestedFallback: null });
const stored = harness.store.marks[0]!;
expect(path.isAbsolute(stored.maskDir!)).toBe(false);
expect((await stat(path.join(harness.root, stored.maskDir!, "0.png"))).isFile()).toBe(true);
});
it("suggests a fallback when inpainting scores low and rescoring follows the treatment", async () => {
harness = await createPrivacyHarness();
const asset = await harness.addAsset("clip__inpaintq-0.3__.mp4");
const mark = await createMark(harness.deps, ACTOR, {
kind: "tattoo",
label: null,
personId: null,
assetId: asset.id,
prompt: tattoo,
treatment: "inpaint",
});
expect(mark.qualityScore).toBeLessThanOrEqual(0.3);
expect(mark.suggestedFallback).toBe("cover");
const blurred = await updateMark(harness.deps, ACTOR, { markId: mark.id, treatment: "blur" });
expect(blurred).toMatchObject({
treatment: "blur",
qualityScore: 0.9,
suggestedFallback: null,
});
});
it("keeps a mark without a worker and rendering it fails closed", async () => {
harness = await createPrivacyHarness(() => null);
const asset = await harness.addAsset("clip.mp4");
const mark = await createMark(harness.deps, ACTOR, {
kind: "license_plate",
label: null,
personId: null,
assetId: asset.id,
prompt: tattoo,
treatment: "cover",
});
expect(mark.qualityScore).toBeNull();
const edl = testEdl([{ id: "c1", assetId: asset.id, inMs: 0, outMs: 2000 }]);
await expect(render(harness, edl, "no-worker")).rejects.toBeInstanceOf(PrivacyUnavailableError);
});
it("treats a marked region through the video and the gate checks what is left", async () => {
harness = await createPrivacyHarness();
const asset = await harness.addAsset("clip.mp4");
const mark = await createMark(harness.deps, ACTOR, {
kind: "tattoo",
label: null,
personId: null,
assetId: asset.id,
prompt: tattoo,
treatment: "none",
});
// The plan asks for a cover even though the mark itself says none.
const edl = testEdl([{ id: "c1", assetId: asset.id, inMs: 0, outMs: 2000 }], {
redactions: { faces: [], marks: [{ markId: mark.id, treatment: "cover" }], regions: [] },
});
const { workDir, prepared } = await render(harness, edl, "cover");
const record = await readPrivacyRecord(workDir);
expect(record?.assets[asset.id]?.checks).toEqual([
expect.objectContaining({ kind: "mark", markId: mark.id }),
]);
const report = await gate(harness, edl, workDir, prepared.sources.get(asset.id)!.path);
expect(report.passed).toBe(true);
});
it("blocks when the treatment left the mark visible", async () => {
harness = await createPrivacyHarness((emulator) => {
const provider = Object.create(emulator) as MediaInferenceProvider;
provider.redact = async (request) => {
await copyFile(request.path, request.outPath);
return { outPath: request.outPath, framesProcessed: 50 };
};
return provider;
});
const asset = await harness.addAsset("clip.mp4");
const mark = await createMark(harness.deps, ACTOR, {
kind: "background_detail",
label: null,
personId: null,
assetId: asset.id,
prompt: tattoo,
treatment: "blur",
});
const edl = testEdl([{ id: "c1", assetId: asset.id, inMs: 0, outMs: 2000 }]);
const { workDir, prepared } = await render(harness, edl, "leak");
const report = await gate(harness, edl, workDir, prepared.sources.get(asset.id)!.path);
expect(report.passed).toBe(false);
expect(report.issues).toContainEqual(
expect.objectContaining({ code: "mark_visible", markId: mark.id }),
);
});
it("inpaints when quality holds and covers instead when it does not", async () => {
harness = await createPrivacyHarness();
const good = await harness.addAsset("good.mp4");
const poor = await harness.addAsset("poor__inpaintq-0.3__.mp4");
for (const asset of [good, poor]) {
await createMark(harness.deps, ACTOR, {
kind: "scar",
label: null,
personId: null,
assetId: asset.id,
prompt: tattoo,
treatment: "inpaint",
});
}
const edl = testEdl([
{ id: "c1", assetId: good.id, inMs: 0, outMs: 1000 },
{ id: "c2", assetId: poor.id, inMs: 0, outMs: 1000 },
]);
const { prepared } = await render(harness, edl, "inpaint");
expect(prepared.aiAlterations).toEqual(["inpaint"]);
expect(prepared.fallbacks).toEqual([
expect.stringMatching(/inpainting scored 0\.30; covered instead$/),
]);
});
it("re-finds a person's standing mark in another video relative to the face", async () => {
harness = await createPrivacyHarness();
const origin = await harness.addAsset("a__faces-1____person-alice__.mp4");
const other = await harness.addAsset("b__faces-1____person-alice__.mp4");
harness.addPerson("alice", "none");
await harness.analyseFaces(origin.id, ["alice"]);
await harness.analyseFaces(other.id, ["alice"]);
const segment = vi.spyOn(harness.inference, "segmentPrompted");
const mark = await createMark(harness.deps, ACTOR, {
kind: "tattoo",
label: "left forearm",
personId: "alice",
assetId: origin.id,
prompt: tattoo,
treatment: "blur",
});
const edl = testEdl([{ id: "c1", assetId: other.id, inMs: 0, outMs: 2000 }]);
const { workDir, prepared } = await render(harness, edl, "standing");
expect(segment).toHaveBeenCalledTimes(2);
const reprompt = segment.mock.calls[1]![0];
expect(reprompt.path).toContain("b__faces-1");
expect(reprompt.prompt.box).toBeDefined();
const record = await readPrivacyRecord(workDir);
expect(record?.reviews).toEqual([]);
expect(record?.assets[other.id]?.checks).toEqual([
expect.objectContaining({ kind: "mark", markId: mark.id }),
]);
const report = await gate(harness, edl, workDir, prepared.sources.get(other.id)!.path);
expect(report.passed).toBe(true);
});
it("sends a low-confidence re-find to review, which blocks the export", async () => {
harness = await createPrivacyHarness((emulator) => {
const provider = Object.create(emulator) as MediaInferenceProvider;
provider.segmentPrompted = async (request) => {
const result = await emulator.segmentPrompted(request);
return request.path.includes("b__") ? { ...result, score: 0.3 } : result;
};
return provider;
});
const origin = await harness.addAsset("a__faces-1____person-alice__.mp4");
const other = await harness.addAsset("b__faces-1____person-alice__.mp4");
harness.addPerson("alice", "none");
await harness.analyseFaces(origin.id, ["alice"]);
await harness.analyseFaces(other.id, ["alice"]);
const mark = await createMark(harness.deps, ACTOR, {
kind: "jewelry",
label: null,
personId: "alice",
assetId: origin.id,
prompt: tattoo,
treatment: "blur",
});
const edl = testEdl([{ id: "c1", assetId: other.id, inMs: 0, outMs: 2000 }]);
const { workDir, prepared } = await render(harness, edl, "review");
const report = await gate(harness, edl, workDir, prepared.sources.get(other.id)!.path);
expect(report.passed).toBe(false);
expect(report.issues).toContainEqual(
expect.objectContaining({ code: "mark_needs_review", markId: mark.id }),
);
});
it("removes a person with their tracks, marks, and files", async () => {
harness = await createPrivacyHarness();
const asset = await harness.addAsset("clip__faces-1____person-alice__.mp4");
harness.addPerson("alice", "blur");
await harness.analyseFaces(asset.id, ["alice"]);
await createMark(harness.deps, ACTOR, {
kind: "tattoo",
label: null,
personId: "alice",
assetId: asset.id,
prompt: tattoo,
treatment: "blur",
});
const files = [
path.join(harness.root, harness.store.tracks[0]!.keyframesPath),
path.join(harness.root, harness.store.marks[0]!.maskDir!),
];
const service = createMediaPrivacyService(harness.deps);
await service.removePerson(ACTOR, "alice");
expect(harness.store.people).toEqual([]);
expect(harness.store.tracks).toEqual([]);
for (const file of files) await expect(stat(file)).rejects.toThrow();
});
});