import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { MediaRepos } from "./media.js"; import { createMediaRepos } from "./media.js"; import type { MediaTestDb } from "./media-fixtures.js"; import { BASE_TIME, openMediaTestDb, sourceItem, vectorBytes } from "./media-fixtures.js"; import { InvalidMediaInputError, MediaStateConflictError } from "./media-shared.js"; import { IsolationError } from "./scope.js"; const databaseUrl = process.env.DATABASE_URL; const describePostgres = process.env.VERIFY_DATABASE && databaseUrl ? describe.sequential : describe.skip; describePostgres("media library and organization (PostgreSQL)", () => { let db: MediaTestDb; let media: MediaRepos; beforeAll(() => { db = openMediaTestDb(databaseUrl!); media = createMediaRepos(db.prisma); }); afterAll(async () => { await db?.cleanup(); }); async function newLibrary(kind: "folder" | "ios_library" = "folder") { const user = await db.newUser(); const source = await media.createSource(user.spaceId, { kind, label: "Library", ownerUserId: user.userId, config: kind === "folder" ? { path: "/media/library" } : { deviceId: "device-1" }, }); return { ...user, sourceId: source.id }; } it("removes device-deleted items and filters by ingest state", async () => { const { spaceId, sourceId } = await newLibrary("ios_library"); const other = await newLibrary("ios_library"); const items = Array.from({ length: 5 }, (_, index) => sourceItem(index)); await media.upsertSourceItems(spaceId, sourceId, items); await media.upsertSourceItems(other.spaceId, other.sourceId, items); const awaiting = await media.searchAssets(spaceId, { ingestStates: ["awaiting_upload"] }); expect(awaiting.items).toHaveLength(5); expect(awaiting.items.map((asset) => asset.externalId).sort()).toEqual( items.map((item) => item.externalId).sort(), ); expect((await media.searchAssets(spaceId, { ingestStates: ["analyzed"] })).items).toEqual([]); const removed = await media.removeSourceItems(spaceId, sourceId, [ items[0]!.externalId, items[1]!.externalId, "unknown", ]); expect(removed.deleted).toBe(2); expect((await media.searchAssets(spaceId, {})).items).toHaveLength(3); expect((await media.searchAssets(other.spaceId, {})).items).toHaveLength(5); await expect( media.removeSourceItems(other.spaceId, sourceId, [items[2]!.externalId]), ).rejects.toBeInstanceOf(IsolationError); }); it("indexes source items once and reports only real changes on the next sync", async () => { const { spaceId, sourceId } = await newLibrary(); const items = Array.from({ length: 1000 }, (_, index) => sourceItem(index, { gpsLat: 48.208_49, gpsLon: 16.372_08 }), ); const first = await media.upsertSourceItems(spaceId, sourceId, items); expect(first).toMatchObject({ created: 1000, updated: 0, unchanged: 0 }); expect(new Set(first.changedAssetIds).size).toBe(1000); const second = await media.upsertSourceItems(spaceId, sourceId, items); expect(second).toEqual({ created: 0, updated: 0, unchanged: 1000, changedAssetIds: [] }); const edited = items.map((item, index) => index < 3 ? { ...item, revision: "r2", isFavorite: true } : item, ); const third = await media.upsertSourceItems(spaceId, sourceId, edited); expect(third).toMatchObject({ created: 0, updated: 3, unchanged: 997 }); expect(third.changedAssetIds).toEqual(first.changedAssetIds.slice(0, 3)); const asset = await media.getAssetRecord(spaceId, first.changedAssetIds[0] as string); expect(asset).toMatchObject({ gpsLat: 48.21, gpsLon: 16.37, isFavorite: true, revision: "r2", storage: "referenced", ingestState: "indexed", }); const sources = await media.listSources(spaceId); expect(sources).toHaveLength(1); expect(sources[0]).toMatchObject({ id: sourceId, assetCount: 1000, status: "idle" }); }); it("serializes concurrent syncs of one source without duplicate assets", async () => { const { spaceId, sourceId } = await newLibrary(); const items = Array.from({ length: 50 }, (_, index) => sourceItem(index)); const results = await Promise.all([ media.upsertSourceItems(spaceId, sourceId, items), media.upsertSourceItems(spaceId, sourceId, items), ]); expect(results.map((result) => result.created).sort()).toEqual([0, 50]); expect(await db.prisma.mediaAsset.count({ where: { sourceId } })).toBe(50); }); it("marks client-pushed assets as remote and waiting for their upload", async () => { const { spaceId, sourceId } = await newLibrary("ios_library"); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [sourceItem(1)]); const asset = await media.getAssetRecord(spaceId, changedAssetIds[0] as string); expect(asset).toMatchObject({ storage: "remote", ingestState: "awaiting_upload" }); }); it("keyset pagination covers 1,200 assets exactly once while new assets arrive", async () => { const { spaceId, sourceId } = await newLibrary(); // Repeated timestamps and undated assets exercise the (capturedAt, id) tie-break. const items = Array.from({ length: 1200 }, (_, index) => sourceItem(index, { capturedAt: index % 10 === 0 ? null : new Date(BASE_TIME + Math.floor(index / 4) * 1000), }), ); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, items); const original = new Set(changedAssetIds); const seen: string[] = []; const order: Array = []; let cursor: string | null = null; let page = 0; do { const result = await media.searchAssets(spaceId, { cursor, limit: 97 }); for (const asset of result.items) { seen.push(asset.id); order.push(asset.capturedAt ? Date.parse(asset.capturedAt) : null); } cursor = result.nextCursor; page += 1; // Newer, older and undated assets land between page reads. await media.upsertSourceItems(spaceId, sourceId, [ sourceItem(10_000 + page * 3, { capturedAt: new Date(BASE_TIME + 10_000_000 + page) }), sourceItem(10_001 + page * 3, { capturedAt: new Date(BASE_TIME - 10_000_000 - page) }), sourceItem(10_002 + page * 3, { capturedAt: null }), ]); } while (cursor); expect(new Set(seen).size).toBe(seen.length); for (const id of original) expect(seen).toContain(id); // Order: capturedAt descending, undated last. const firstNull = order.indexOf(null); expect(order.slice(firstNull).every((value) => value === null)).toBe(true); const dated = order.slice(0, firstNull) as number[]; for (let index = 1; index < dated.length; index++) { expect(dated[index - 1]).toBeGreaterThanOrEqual(dated[index] as number); } await expect(media.searchAssets(spaceId, { cursor: "not-a-cursor" })).rejects.toBeInstanceOf( InvalidMediaInputError, ); }); it("filters search by kind, time, favorites, quality, place, text, person and collection", async () => { const { spaceId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [ sourceItem(1, { kind: "video", gpsLat: 48.2085, gpsLon: 16.3721, albumNames: ["Summer Trip"], isFavorite: true, }), sourceItem(2, { kind: "photo", gpsLat: 48.137, gpsLon: 11.575, fileName: "beach_day.jpg" }), sourceItem(3, { kind: "video", gpsLat: 48.25, gpsLon: 16.4 }), sourceItem(4, { kind: "video", capturedAt: null, gpsLat: -16.5, gpsLon: 179.99 }), sourceItem(5, { kind: "photo", gpsLat: -16.5, gpsLon: -179.99, fileName: "100%_done.jpg" }), ]); const [vienna, munich, nearVienna, dateline, otherSide] = changedAssetIds as [ string, string, string, string, string, ]; const ids = async (query: Parameters[1]) => (await media.searchAssets(spaceId, query)).items.map((asset) => asset.id).sort(); expect(await ids({ kinds: ["photo"] })).toEqual([munich, otherSide].sort()); expect(await ids({ favoritesOnly: true })).toEqual([vienna]); expect( await ids({ from: new Date(BASE_TIME + 2 * 60_000).toISOString(), to: new Date(BASE_TIME + 3 * 60_000).toISOString(), }), ).toEqual([munich, nearVienna].sort()); expect(await ids({ near: { lat: 48.2082, lon: 16.3738, radiusKm: 2 } })).toEqual([vienna]); expect(await ids({ near: { lat: 48.2082, lon: 16.3738, radiusKm: 10 } })).toEqual( [vienna, nearVienna].sort(), ); expect(await ids({ near: { lat: -16.5, lon: 180, radiusKm: 5 } })).toEqual( [dateline, otherSide].sort(), ); await media.recordAnalysisVersion(spaceId, munich, 1, 0.9); await media.recordAnalysisVersion(spaceId, vienna, 1, 0.3); expect(await ids({ minQuality: 0.5 })).toEqual([munich]); expect(await ids({ text: "summer" })).toEqual([vienna]); expect(await ids({ text: "BEACH_DAY" })).toEqual([munich]); expect(await ids({ text: "100%" })).toEqual([otherSide]); expect(await ids({ text: "%" })).toEqual([otherSide]); await media.replaceSegments(spaceId, nearVienna, 1, [ { kind: "shot", startMs: 0, endMs: 1000, labels: ["Sunset"], transcriptExcerpt: "welcome" }, ]); expect(await ids({ text: "sunset" })).toEqual([nearVienna]); expect(await ids({ text: "WELCOME" })).toEqual([nearVienna]); const collection = await media.createManualCollection(spaceId, "Harbour walk", [dateline]); expect(await ids({ text: "harbour" })).toEqual([dateline]); expect(await ids({ collectionId: collection.id })).toEqual([dateline]); const [track] = await media.replaceFaceTracks(spaceId, munich, "faces-1", [ { startMs: 0, endMs: 1000, confidenceMin: 0.8, keyframesPath: "k.json", embedding: null }, ]); const [person] = await media.createPersonSuggestions(spaceId, [ { centroid: vectorBytes([1, 2]), trackIds: [track?.id as string] }, ]); expect(await ids({ personId: person?.id as string })).toEqual([munich]); expect(await ids({ sourceId })).toHaveLength(5); }); it("returns asset details and moves ingest state only from the expected state", async () => { const { spaceId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [sourceItem(1)]); const assetId = changedAssetIds[0] as string; const claims = await Promise.all([ media.transitionIngestState(spaceId, assetId, { from: ["indexed"], to: "proxying" }), media.transitionIngestState(spaceId, assetId, { from: ["indexed"], to: "proxying" }), ]); expect(claims.filter(Boolean)).toHaveLength(1); expect((await media.assetsNeedingWork(spaceId, "proxying", 10)).map((row) => row.id)).toEqual([ assetId, ]); const proxied = await media.recordProxy(spaceId, assetId, { contentHash: "f".repeat(64), proxyPath: "proxy.mp4", thumbPath: "thumb.jpg", durationMs: 12_000, }); expect(proxied).toMatchObject({ ingestState: "proxied", hasProxy: true, hasThumbnail: true }); expect( await media.transitionIngestState(spaceId, assetId, { from: ["proxied"], to: "failed", error: "sidecar offline", }), ).toBe(true); expect(await media.getAssetRecord(spaceId, assetId)).toMatchObject({ ingestState: "failed", ingestError: "sidecar offline", durationMs: 12_000, }); await media.upsertAnalysis(spaceId, assetId, { kind: "transcript", modelId: "whisper", modelVersion: "1", data: { language: "en", words: [{ text: "hello" }, { text: "there" }] }, }); await media.replaceSegments(spaceId, assetId, 1, [ { kind: "scene", startMs: 2000, endMs: 4000, scores: { motion: 0.5 } }, { kind: "shot", startMs: 0, endMs: 2000, teaserScore: 0.7 }, ]); await media.replaceFaceTracks(spaceId, assetId, "faces-1", [ { startMs: 0, endMs: 500, confidenceMin: 0.6, keyframesPath: "a.json", embedding: null }, ]); const detail = await media.getAsset(spaceId, assetId); expect(detail.transcript).toBe("hello there"); expect(detail.segments.map((segment) => segment.startMs)).toEqual([0, 2000]); expect(detail.segments[1]?.scores).toEqual({ motion: 0.5 }); expect(detail.faceTracks).toHaveLength(1); expect(await media.recordAnalysisVersion(spaceId, assetId, 2)).toMatchObject({ ingestState: "analyzed", }); }); it("keeps each analysis model version and returns the latest per kind", async () => { const { spaceId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [sourceItem(1)]); const assetId = changedAssetIds[0] as string; const v1 = await media.upsertAnalysis(spaceId, assetId, { kind: "embeddings", modelId: "siglip", modelVersion: "1", vectors: vectorBytes([1, 2, 3]), }); const again = await media.upsertAnalysis(spaceId, assetId, { kind: "embeddings", modelId: "siglip", modelVersion: "1", vectors: vectorBytes([4, 5, 6]), }); expect(again.id).toBe(v1.id); await new Promise((resolve) => setTimeout(resolve, 5)); await media.upsertAnalysis(spaceId, assetId, { kind: "embeddings", modelId: "siglip", modelVersion: "2", vectors: vectorBytes([7, 8, 9]), }); expect(await db.prisma.mediaAnalysis.count({ where: { assetId } })).toBe(2); const latest = await media.latestAnalysis(spaceId, assetId, "embeddings"); expect(latest?.modelVersion).toBe("2"); expect([...new Float32Array(new Uint8Array(latest?.vectors as Uint8Array).buffer)]).toEqual([ 7, 8, 9, ]); expect( (await media.latestAnalysesByKind(spaceId, "embeddings")).get(assetId)?.modelVersion, ).toBe("2"); }); it("replaces segments atomically and flags paid-only segments all or nothing", async () => { const { spaceId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [sourceItem(1)]); const assetId = changedAssetIds[0] as string; await media.replaceSegments(spaceId, assetId, 1, [ { kind: "shot", startMs: 0, endMs: 1000 }, { kind: "shot", startMs: 1000, endMs: 2000 }, ]); await expect( media.replaceSegments(spaceId, assetId, 2, [ { kind: "shot", startMs: 0, endMs: 1000 }, { kind: "shot", startMs: 1000, endMs: 1000 }, ]), ).rejects.toBeInstanceOf(InvalidMediaInputError); expect(await media.listSegments(spaceId, [assetId])).toHaveLength(2); await media.replaceSegments(spaceId, assetId, 2, [{ kind: "scene", startMs: 0, endMs: 3000 }]); const segments = await media.listSegments(spaceId, [assetId]); expect(segments).toHaveLength(1); const segmentId = segments[0]?.id as string; await expect( media.setSegmentsPaidOnly(spaceId, [segmentId, "missing"], true), ).rejects.toBeInstanceOf(IsolationError); expect((await media.listSegments(spaceId, [assetId]))[0]?.paidOnly).toBe(false); expect(await media.setSegmentsPaidOnly(spaceId, [segmentId], true)).toBe(1); expect((await media.listSegments(spaceId, [assetId]))[0]?.paidOnly).toBe(true); }); it("re-clustering keeps user titles and manual collections and drops stale clusters", async () => { const { spaceId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems( spaceId, sourceId, Array.from({ length: 6 }, (_, index) => sourceItem(index + 1)), ); const [a1, a2, a3, a4, a5, a6] = changedAssetIds as string[]; const cluster = (key: string, title: string, assetIds: string[]) => ({ key, title, labelSource: "rule" as const, startAt: new Date(BASE_TIME), endAt: new Date(BASE_TIME + 1000), centroid: { lat: 48.20849, lon: 16.37208 }, coverAssetId: assetIds[0] ?? null, assetIds, }); expect( await media.replaceClusters(spaceId, "session", [ cluster("s1", "Morning", [a1 as string, a2 as string]), cluster("s2", "Noon", [a3 as string]), cluster("s3", "Evening", [a4 as string]), ]), ).toEqual({ created: 3, updated: 0, deleted: 0 }); const manual = await media.createManualCollection(spaceId, "Favourites", [ a6 as string, a1 as string, ]); const first = (await media.listCollections(spaceId, { kind: "session" })).items; const morning = first.find((collection) => collection.title === "Morning"); const evening = first.find((collection) => collection.title === "Evening"); await media.renameCollection(spaceId, morning?.id as string, "Market visit"); await media.renameCollection(spaceId, evening?.id as string, "Dinner"); expect( await media.replaceClusters(spaceId, "session", [ cluster("s1", "Morning again", [a1 as string, a3 as string, a5 as string]), cluster("s4", "Night", [a2 as string]), ]), ).toEqual({ created: 1, updated: 1, deleted: 1 }); const after = await media.listCollections(spaceId, {}); const byTitle = new Map(after.items.map((collection) => [collection.title, collection])); expect([...byTitle.keys()].sort()).toEqual( ["Dinner", "Favourites", "Market visit", "Night"].sort(), ); expect(byTitle.get("Market visit")).toMatchObject({ id: morning?.id, labelSource: "user", assetCount: 3, videoCount: 2, totalDurationMs: 20_000, }); const manualAfter = await media.getCollection(spaceId, manual.id); expect(manualAfter.collection).toMatchObject({ kind: "manual", title: "Favourites" }); expect(manualAfter.assets.map((asset) => asset.id)).toEqual([a1, a6]); const stored = await db.prisma.mediaCollection.findUniqueOrThrow({ where: { id: morning?.id as string }, }); expect(stored).toMatchObject({ centroidLat: 48.21, centroidLon: 16.37 }); await expect( media.replaceClusters(spaceId, "session", [cluster("x", "Bad", ["foreign-asset"])]), ).rejects.toBeInstanceOf(IsolationError); }); it("lists, merges, splits and creates collections", async () => { const { spaceId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems( spaceId, sourceId, Array.from({ length: 5 }, (_, index) => sourceItem(index + 1)), ); const ids = changedAssetIds as string[]; const clusters = ids.map((assetId, index) => ({ key: `t${index}`, title: `Theme ${index}`, labelSource: "model" as const, startAt: index === 4 ? null : new Date(BASE_TIME + index * 1000), endAt: null, centroid: null, coverAssetId: assetId, assetIds: [assetId], })); await media.replaceClusters(spaceId, "theme", clusters); const seen: string[] = []; let cursor: string | null = null; do { const page = await media.listCollections(spaceId, { kind: "theme", cursor, limit: 2 }); seen.push(...page.items.map((collection) => collection.title)); cursor = page.nextCursor; } while (cursor); expect(seen).toEqual(["Theme 3", "Theme 2", "Theme 1", "Theme 0", "Theme 4"]); const all = (await media.listCollections(spaceId, { kind: "theme" })).items; const merged = await media.mergeCollections( spaceId, all.slice(0, 3).map((collection) => collection.id), ); expect(merged).toMatchObject({ id: all[0]?.id, assetCount: 3, labelSource: "user" }); expect((await media.listCollections(spaceId, { kind: "theme" })).items).toHaveLength(3); const mergedAssets = (await media.getCollection(spaceId, merged.id)).assets.map( (asset) => asset.id, ); expect(mergedAssets).toEqual([ids[1], ids[2], ids[3]]); await expect( media.splitCollection(spaceId, merged.id, ids[1] as string), ).rejects.toBeInstanceOf(InvalidMediaInputError); const [kept, split] = await media.splitCollection(spaceId, merged.id, ids[2] as string); expect(kept).toMatchObject({ id: merged.id, assetCount: 1 }); expect(split).toMatchObject({ assetCount: 2, kind: "theme" }); expect( await media.collectionAssetIds(spaceId, [split?.id as string, kept?.id as string]), ).toEqual([ids[2], ids[3], ids[1]]); }); it("suggests, merges and assigns people and records consent only for a member", async () => { const { spaceId, userId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [ sourceItem(1), sourceItem(2), ]); const [assetA, assetB] = changedAssetIds as [string, string]; const tracksA = await media.replaceFaceTracks(spaceId, assetA, "faces-1", [ { startMs: 0, endMs: 1000, confidenceMin: 0.9, keyframesPath: "a1", embedding: vectorBytes([1, 0]), }, { startMs: 0, endMs: 1000, confidenceMin: 0.7, keyframesPath: "a2", embedding: vectorBytes([0, 1]), }, ]); const tracksB = await media.replaceFaceTracks(spaceId, assetB, "faces-1", [ { startMs: 0, endMs: 1000, confidenceMin: 0.8, keyframesPath: "b1", embedding: vectorBytes([1, 0]), }, ]); expect(await media.faceTrackEmbeddings(spaceId, { unassignedOnly: true })).toHaveLength(3); const people = await media.createPersonSuggestions(spaceId, [ { centroid: vectorBytes([1, 0]), trackIds: [tracksA[0]?.id as string, tracksB[0]?.id as string], }, { centroid: vectorBytes([0, 1]), trackIds: [tracksA[1]?.id as string] }, ]); expect(people.map((person) => person.displayName)).toEqual(["Person 1", "Person 2"]); expect(people[0]).toMatchObject({ faceTrackCount: 2, confirmed: false, coverThumbAssetId: assetA, }); expect(await media.faceTrackEmbeddings(spaceId, { unassignedOnly: true })).toHaveLength(0); expect(await media.personCentroids(spaceId)).toHaveLength(2); const [first, second] = people as [(typeof people)[0], (typeof people)[0]]; await media.updatePerson(spaceId, first.id, { isSelf: true }); await media.updatePerson(spaceId, second.id, { isSelf: true, displayName: "Me" }); const listed = await media.listPeople(spaceId); expect(listed.filter((person) => person.isSelf).map((person) => person.id)).toEqual([ second.id, ]); const mark = await media.createMark(spaceId, { kind: "tattoo", label: null, personId: second.id, assetId: assetA, prompt: { tMs: 0, box: { x: 0.1, y: 0.1, w: 0.2, h: 0.2 } }, treatment: "inpaint", }); const merged = await media.mergePeople(spaceId, [first.id, second.id]); expect(merged).toMatchObject({ id: first.id, faceTrackCount: 3, confirmed: true }); expect((await media.getMark(spaceId, mark.id)).personId).toBe(first.id); expect(await media.listPeople(spaceId)).toHaveLength(1); const reassigned = await media.assignTrack(spaceId, tracksB[0]?.id as string, null); expect(reassigned.personId).toBeNull(); await expect( media.assignTrack(spaceId, tracksB[0]?.id as string, "missing-person"), ).rejects.toBeInstanceOf(IsolationError); const consent = { personId: first.id, consentArtifactId: "artifact-1", consentRecordedAt: new Date(BASE_TIME).toISOString(), ageVerificationMethod: "document_on_file" as const, ageVerifiedAt: new Date(BASE_TIME).toISOString(), }; await expect( media.recordConsent({ spaceId, recordedByUserId: "" }, consent), ).rejects.toBeInstanceOf(InvalidMediaInputError); await expect( media.recordConsent({ spaceId, recordedByUserId: "not-a-member" }, consent), ).rejects.toBeInstanceOf(IsolationError); await expect( media.recordConsent( { spaceId, recordedByUserId: userId }, { ...consent, ageVerifiedAt: null }, ), ).rejects.toBeInstanceOf(InvalidMediaInputError); const withConsent = await media.recordConsent({ spaceId, recordedByUserId: userId }, consent); expect(withConsent).toMatchObject({ consentArtifactId: "artifact-1", ageVerificationMethod: "document_on_file", consentRecordedAt: consent.consentRecordedAt, }); const record = await db.prisma.mediaPerson.findUniqueOrThrow({ where: { id: first.id } }); expect(record.consentRecordedBy).toBe(userId); }); it("removing a person leaves no biometric bytes behind", async () => { const { spaceId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [sourceItem(1)]); const assetId = changedAssetIds[0] as string; const tracks = await media.replaceFaceTracks(spaceId, assetId, "faces-1", [ { startMs: 0, endMs: 1000, confidenceMin: 0.9, keyframesPath: "k1", embedding: vectorBytes([1, 2]), }, { startMs: 0, endMs: 1000, confidenceMin: 0.9, keyframesPath: "k2", embedding: vectorBytes([3, 4]), }, { startMs: 0, endMs: 1000, confidenceMin: 0.9, keyframesPath: "k3", embedding: vectorBytes([5, 6]), }, ]); const [person, bystander] = await media.createPersonSuggestions(spaceId, [ { centroid: vectorBytes([2, 3]), trackIds: [tracks[0]?.id as string, tracks[1]?.id as string], }, { centroid: vectorBytes([5, 6]), trackIds: [tracks[2]?.id as string] }, ]); const personId = person?.id as string; const mark = await media.createMark(spaceId, { kind: "birthmark", label: null, personId, assetId, prompt: { tMs: 0 }, treatment: "blur", }); await media.recordMarkResult(spaceId, mark.id, { qualityScore: 0.4, suggestedFallback: "cover", maskDir: "masks/m1", }); const assetMark = await media.createMark(spaceId, { kind: "license_plate", label: null, personId: null, assetId, prompt: { tMs: 0 }, treatment: "blur", }); const removed = await media.removePersonCompletely(spaceId, personId); expect(removed).toEqual({ deletedFaceTracks: 2, deletedMarks: 1, keyframesPaths: expect.arrayContaining(["k1", "k2"]), maskDirs: ["masks/m1"], }); const people = await db.prisma.$queryRaw>` SELECT id, centroid FROM media_people WHERE "spaceId" = ${spaceId}`; expect(people).toEqual([{ id: bystander?.id, centroid: expect.anything() }]); const personRows = await db.prisma.$queryRaw>` SELECT count(*)::int AS count FROM media_people WHERE id = ${personId}`; expect(personRows[0]?.count).toBe(0); const linked = await db.prisma.$queryRaw>` SELECT count(*)::int AS count FROM media_face_tracks WHERE "personId" = ${personId} OR id IN (${tracks[0]?.id}, ${tracks[1]?.id})`; expect(linked[0]?.count).toBe(0); const embeddings = await db.prisma.$queryRaw>` SELECT id FROM media_face_tracks WHERE "spaceId" = ${spaceId} AND embedding IS NOT NULL`; expect(embeddings.map((row) => row.id)).toEqual([tracks[2]?.id]); const marks = await db.prisma.$queryRaw>` SELECT id FROM media_marks WHERE "spaceId" = ${spaceId}`; expect(marks.map((row) => row.id)).toEqual([assetMark.id]); await expect(media.removePersonCompletely(spaceId, personId)).rejects.toBeInstanceOf( IsolationError, ); }); it("manages marks per asset and per person", async () => { const { spaceId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [ sourceItem(1), sourceItem(2), ]); const [assetA, assetB] = changedAssetIds as [string, string]; const [trackB] = await media.replaceFaceTracks(spaceId, assetB, "faces-1", [ { startMs: 0, endMs: 1000, confidenceMin: 0.9, keyframesPath: "k", embedding: null }, ]); const [person] = await media.createPersonSuggestions(spaceId, [ { centroid: vectorBytes([1]), trackIds: [trackB?.id as string] }, ]); const personMark = await media.createMark(spaceId, { kind: "tattoo", label: "left forearm", personId: person?.id as string, assetId: assetA, prompt: { tMs: 500, point: { x: 0.2, y: 0.3 } }, treatment: "inpaint", }); const plate = await media.createMark(spaceId, { kind: "license_plate", label: null, personId: null, assetId: assetB, prompt: { tMs: 0, box: { x: 0, y: 0, w: 0.1, h: 0.1 } }, treatment: "blur", }); expect((await media.listMarks(spaceId, { personId: person?.id })).map((m) => m.id)).toEqual([ personMark.id, ]); expect((await media.listMarks(spaceId, { assetId: assetB })).map((m) => m.id)).toEqual([ plate.id, ]); // Person marks follow the person into every asset they appear in. expect((await media.marksForAsset(spaceId, assetB)).map((m) => m.id).sort()).toEqual( [personMark.id, plate.id].sort(), ); const updated = await media.updateMark(spaceId, plate.id, { treatment: "cover", label: "car" }); expect(updated).toMatchObject({ treatment: "cover", label: "car" }); const result = await media.recordMarkResult(spaceId, personMark.id, { qualityScore: 0.2, suggestedFallback: "cover", maskDir: "masks/tattoo", }); expect(result).toMatchObject({ qualityScore: 0.2, suggestedFallback: "cover", maskDir: "masks/tattoo", prompt: { tMs: 500, point: { x: 0.2, y: 0.3 } }, }); expect(await media.removeMark(spaceId, personMark.id)).toEqual({ maskDir: "masks/tattoo" }); expect(await media.listMarks(spaceId)).toHaveLength(1); }); it("uploads chunks idempotently, resumes, and completes only when every chunk arrived", async () => { const { spaceId, userId, sourceId } = await newLibrary("ios_library"); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [sourceItem(1)]); const assetId = changedAssetIds[0] as string; const begin = { assetId, userId, sizeBytes: 10, sha256: "c".repeat(64), fileName: "clip.mov", chunkBytes: 4, }; const upload = await media.beginUpload(spaceId, begin); expect(upload).toMatchObject({ sizeBytes: 10, chunkBytes: 4, receivedChunks: [], status: "open", }); expect((await media.beginUpload(spaceId, begin)).id).toBe(upload.id); await Promise.all( Array.from({ length: 6 }, () => media.markChunkReceived(spaceId, upload.id, 1)), ); await media.markChunkReceived(spaceId, upload.id, 0); expect((await media.uploadStatus(spaceId, upload.id)).receivedChunks).toEqual([0, 1]); await expect(media.markChunkReceived(spaceId, upload.id, 3)).rejects.toBeInstanceOf( InvalidMediaInputError, ); await expect( media.completeUpload(spaceId, upload.id, { storagePath: "uploads/clip.mov" }), ).rejects.toBeInstanceOf(MediaStateConflictError); await media.markChunkReceived(spaceId, upload.id, 2); const done = await media.completeUpload(spaceId, upload.id, { storagePath: "uploads/clip.mov", }); expect(done).toMatchObject({ status: "complete", receivedChunks: [0, 1, 2] }); expect(await media.getAssetRecord(spaceId, assetId)).toMatchObject({ storage: "uploaded", storagePath: "uploads/clip.mov", contentHash: "c".repeat(64), ingestState: "indexed", }); await expect(media.markChunkReceived(spaceId, upload.id, 0)).rejects.toBeInstanceOf( MediaStateConflictError, ); expect((await media.beginUpload(spaceId, begin)).id).not.toBe(upload.id); }); it("requests uploads only for remote assets", async () => { const remote = await newLibrary("ios_library"); const { changedAssetIds } = await media.upsertSourceItems(remote.spaceId, remote.sourceId, [ sourceItem(1), ]); const remoteId = changedAssetIds[0] as string; await media.transitionIngestState(remote.spaceId, remoteId, { from: ["awaiting_upload"], to: "failed", }); expect(await media.requestUpload(remote.spaceId, [remoteId])).toEqual([remoteId]); const folder = await newLibrary(); const local = await media.upsertSourceItems(folder.spaceId, folder.sourceId, [sourceItem(1)]); expect(await media.requestUpload(folder.spaceId, local.changedAssetIds)).toEqual([]); }); it("updates and removes a source with everything indexed from it", async () => { const { spaceId, sourceId } = await newLibrary(); const { changedAssetIds } = await media.upsertSourceItems(spaceId, sourceId, [sourceItem(1)]); const synced = await media.updateSourceSync(spaceId, sourceId, { status: "syncing", lastSyncCursor: "page-2", lastSyncedAt: new Date(BASE_TIME), error: null, }); expect(synced).toMatchObject({ status: "syncing", lastSyncCursor: "page-2", config: { path: "/media/library" }, assetCount: 1, }); await media.replaceSegments(spaceId, changedAssetIds[0] as string, 1, [ { kind: "shot", startMs: 0, endMs: 1 }, ]); await media.removeSource(spaceId, sourceId); expect(await media.listSources(spaceId)).toEqual([]); expect(await db.prisma.mediaAsset.count({ where: { sourceId } })).toBe(0); expect(await db.prisma.mediaSegment.count({ where: { spaceId } })).toBe(0); }); });