evOo-local/packages/media/src/clustering.ts
Claude a663764ee8
feat(media): contracts and foundation for Media Studio
Freeze the shared contracts every Media Studio slice builds on: zod DTOs,
EDL, platform profile, brand kit, gate and render plan schemas; the media
RPC surface with a scaffold router; provider interfaces for sources,
inference, rendering, motion graphics and publishing; media background
jobs; the Prisma models and migration; and the @rakazo/media domain
package signatures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
2026-10-03 17:32:52 +00:00

88 lines
2.6 KiB
TypeScript

import type { MediaAssetKind } from "@rakazo/contracts";
import { notImplemented } from "./not-implemented.js";
export interface ClusterAsset {
id: string;
kind: MediaAssetKind;
capturedAt: Date | null;
gpsLat: number | null;
gpsLon: number | null;
deviceModel: string | null;
albumNames: string[];
/** Mean frame embedding; null when not analyzed yet. */
embedding: Float32Array | null;
}
export interface SessionOptions {
/** New session after a gap longer than this. Default 45. */
gapMinutes: number;
/** New session after a location jump larger than this. Default 2. */
maxDistanceKm: number;
/** New session on calendar-day change when the gap exceeds this. Default 3. */
dayBreakHours: number;
/** Merge adjacent sessions whose mean embeddings reach this cosine. Default 0.85. */
mergeSimilarity: number;
/** ...and whose gap is below this. Default 2. */
mergeMaxGapHours: number;
/** IANA zone for calendar-day boundaries. Default "UTC". */
timeZone: string;
}
export const DEFAULT_SESSION_OPTIONS: SessionOptions = {
gapMinutes: 45,
maxDistanceKm: 2,
dayBreakHours: 3,
mergeSimilarity: 0.85,
mergeMaxGapHours: 2,
timeZone: "UTC",
};
export interface SessionCluster {
/** Stable across re-clustering: derived from the earliest asset id of the session. */
key: string;
assetIds: string[];
startAt: Date | null;
endAt: Date | null;
centroid: { lat: number; lon: number } | null;
}
/**
* Deterministic session grouping (spec 4.4). Assets without capturedAt form
* one trailing "undated" session; exact duplicates (same id) are ignored.
*/
export function buildSessions(
_assets: ClusterAsset[],
_options?: Partial<SessionOptions>,
): SessionCluster[] {
return notImplemented("buildSessions");
}
export interface VectorItem {
id: string;
vector: Float32Array;
}
/** Agglomerative average-linkage clustering; returns groups with stable keys. */
export function clusterThemes(
_items: VectorItem[],
_options?: { similarity?: number; minSize?: number },
): Array<{ key: string; ids: string[] }> {
return notImplemented("clusterThemes");
}
/** Group face tracks into person suggestions by identity embedding. Local only. */
export function clusterFaces(
_tracks: VectorItem[],
_options?: { similarity?: number },
): Array<{ key: string; ids: string[]; centroid: Float32Array }> {
return notImplemented("clusterFaces");
}
/** Rule-based title from date range, place centroid, and dominant album. No model involved. */
export function ruleLabel(
_cluster: { startAt: Date | null; endAt: Date | null; albumNames: string[] },
_locale?: string,
): string {
return notImplemented("ruleLabel");
}