import { rm } from "node:fs/promises"; import path from "node:path"; import { prepareSources } from "./media-anonymize.js"; import type { MediaPrivacyService, MediaServiceDeps } from "./media-context.js"; import { runGate } from "./media-gate.js"; import { createMark, updateMark } from "./media-marks.js"; import type { PrivacyToolOptions } from "./media-privacy-io.js"; import { resolveStored } from "./media-privacy-io.js"; /* * MediaPrivacyService: anonymization before rendering, the gate after it, * identifying marks, and deleting a person with their biometric data. Face * data never leaves this host: matching, tracking, and clustering run against * the local inference provider and database only. */ export function createMediaPrivacyService( deps: MediaServiceDeps, tools: PrivacyToolOptions = {}, ): MediaPrivacyService { const withTools = { ...deps, ...tools }; return { prepareSources: (input) => prepareSources(withTools, input), gate: (input) => runGate(withTools, input), createMark: (actor, input) => createMark(withTools, actor, input), updateMark: (actor, input) => updateMark(withTools, actor, input), async removePerson(actor, personId) { const removed = await deps.repos.removePersonCompletely(actor.spaceId, personId); // Rows are gone; now the files: track keyframes and each mark's directory // (masks plus previews). A path outside the media root throws untouched. const markDirs = removed.maskDirs.map((dir) => path.basename(dir) === "masks" ? path.dirname(dir) : dir, ); for (const stored of [...removed.keyframesPaths, ...markDirs]) { await rm(await resolveStored(deps.mediaRoot, stored), { recursive: true, force: true }); } }, }; }