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
This commit is contained in:
Claude 2026-10-03 17:32:52 +00:00
parent 3c3961f242
commit a663764ee8
No known key found for this signature in database
36 changed files with 3958 additions and 1 deletions

View File

@ -236,3 +236,10 @@ EVOO_COCKPIT_CORS_ORIGIN=
# Expo: English or Simplified Chinese (en | zh-CN). Overridden by SecureStore key
# rakazo.uiLocale from Account → Language.
# EXPO_PUBLIC_DEFAULT_UI_LOCALE=en
# Optional Media Studio worker (local ffmpeg + ML sidecar, compose profile "media").
# Without it, Media Studio stays hidden and media jobs are skipped.
# MEDIA_WORKER_URL=http://127.0.0.1:8790
# MEDIA_WORKER_TOKEN=REPLACE_WITH_RANDOM_TOKEN
# Private platform profiles that override the shipped ones. Keep outside the repo.
# MEDIA_PROFILES_DIR=~/.evoo/media-profiles

View File

@ -19,6 +19,7 @@
"@rakazo/core": "workspace:*",
"@rakazo/db": "workspace:*",
"@rakazo/logging": "workspace:*",
"@rakazo/media": "workspace:*",
"@rakazo/memory": "workspace:*",
"hono": "4.13.7"
},

113
apps/api/src/media.ts Normal file
View File

@ -0,0 +1,113 @@
import { implement, ORPCError } from "@orpc/server";
import type { Actor } from "@rakazo/contracts";
import { appContract } from "@rakazo/contracts";
import type { PrismaClient } from "@rakazo/db";
export interface MediaRouterDeps {
prisma: PrismaClient;
dataDir: string;
}
// Phase A scaffold: every procedure is replaced by its real implementation.
function notImplemented(): never {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Media Studio is not available yet" });
}
export function createMediaRouter(_deps: MediaRouterDeps) {
const os = implement(appContract.media).$context<{ actor: Actor | null; signal?: AbortSignal }>();
const authed = os.use(async ({ context, next }) => {
if (!context.actor) throw new ORPCError("UNAUTHORIZED");
return next({ context: { ...context, actor: context.actor } });
});
return os.router({
capabilities: authed.capabilities.handler(notImplemented),
sources: {
list: authed.sources.list.handler(notImplemented),
createFolder: authed.sources.createFolder.handler(notImplemented),
registerClient: authed.sources.registerClient.handler(notImplemented),
pushItems: authed.sources.pushItems.handler(notImplemented),
sync: authed.sources.sync.handler(notImplemented),
remove: authed.sources.remove.handler(notImplemented),
},
assets: {
search: authed.assets.search.handler(notImplemented),
get: authed.assets.get.handler(notImplemented),
setPaidOnly: authed.assets.setPaidOnly.handler(notImplemented),
requestUpload: authed.assets.requestUpload.handler(notImplemented),
},
uploads: {
begin: authed.uploads.begin.handler(notImplemented),
status: authed.uploads.status.handler(notImplemented),
complete: authed.uploads.complete.handler(notImplemented),
},
collections: {
list: authed.collections.list.handler(notImplemented),
get: authed.collections.get.handler(notImplemented),
rename: authed.collections.rename.handler(notImplemented),
merge: authed.collections.merge.handler(notImplemented),
split: authed.collections.split.handler(notImplemented),
createManual: authed.collections.createManual.handler(notImplemented),
recluster: authed.collections.recluster.handler(notImplemented),
},
people: {
list: authed.people.list.handler(notImplemented),
update: authed.people.update.handler(notImplemented),
merge: authed.people.merge.handler(notImplemented),
assignTrack: authed.people.assignTrack.handler(notImplemented),
recordConsent: authed.people.recordConsent.handler(notImplemented),
remove: authed.people.remove.handler(notImplemented),
},
marks: {
list: authed.marks.list.handler(notImplemented),
create: authed.marks.create.handler(notImplemented),
update: authed.marks.update.handler(notImplemented),
remove: authed.marks.remove.handler(notImplemented),
},
brandKits: {
list: authed.brandKits.list.handler(notImplemented),
upsert: authed.brandKits.upsert.handler(notImplemented),
remove: authed.brandKits.remove.handler(notImplemented),
templates: authed.brandKits.templates.handler(notImplemented),
introVariants: authed.brandKits.introVariants.handler(notImplemented),
},
profiles: {
list: authed.profiles.list.handler(notImplemented),
get: authed.profiles.get.handler(notImplemented),
},
projects: {
list: authed.projects.list.handler(notImplemented),
get: authed.projects.get.handler(notImplemented),
create: authed.projects.create.handler(notImplemented),
update: authed.projects.update.handler(notImplemented),
remove: authed.projects.remove.handler(notImplemented),
autoPlan: authed.projects.autoPlan.handler(notImplemented),
},
plans: {
submit: authed.plans.submit.handler(notImplemented),
},
renders: {
start: authed.renders.start.handler(notImplemented),
get: authed.renders.get.handler(notImplemented),
cancel: authed.renders.cancel.handler(notImplemented),
thumbnails: authed.renders.thumbnails.handler(notImplemented),
},
marketing: {
copy: authed.marketing.copy.handler(notImplemented),
},
publications: {
list: authed.publications.list.handler(notImplemented),
create: authed.publications.create.handler(notImplemented),
bundle: authed.publications.bundle.handler(notImplemented),
markPublished: authed.publications.markPublished.handler(notImplemented),
cancel: authed.publications.cancel.handler(notImplemented),
},
metrics: {
mappings: authed.metrics.mappings.handler(notImplemented),
saveMapping: authed.metrics.saveMapping.handler(notImplemented),
importCsv: authed.metrics.importCsv.handler(notImplemented),
record: authed.metrics.record.handler(notImplemented),
funnel: authed.metrics.funnel.handler(notImplemented),
},
});
}

View File

@ -166,6 +166,7 @@ import {
renameMainConversation,
} from "./main-conversation.js";
import { buildMcpUpdateMaterial } from "./mcp-material.js";
import { createMediaRouter } from "./media.js";
import {
disconnectMemoryProvider,
getMemoryProviderConfig,
@ -526,6 +527,7 @@ export function createRouter(deps: RouterDeps) {
});
return os.router({
media: createMediaRouter({ prisma: deps.prisma, dataDir: deps.dataDir }),
aiConsent: {
status: authed.aiConsent.status.handler(({ context, input }) =>
aiConsentStatus(deps, context.actor, input),

View File

@ -35,6 +35,7 @@
"expo-image-picker": "57.0.20",
"expo-linking": "57.0.11",
"expo-localization": "57.0.2",
"expo-media-library": "57.0.5",
"expo-modules-core": "57.0.20",
"expo-notifications": "57.0.21",
"expo-router": "57.0.24",
@ -44,6 +45,7 @@
"expo-status-bar": "57.0.1",
"expo-symbols": "57.0.3",
"expo-updates": "57.0.24",
"expo-video": "57.0.5",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.86.3",

View File

@ -24,6 +24,7 @@
"@rakazo/core": "workspace:*",
"@rakazo/ui-tokens": "workspace:*",
"@rakazo/ui-web": "workspace:*",
"@tanstack/react-virtual": "3.14.13",
"better-auth": "1.7.4",
"lucide-react": "1.44.0",
"react": "19.2.3",

View File

@ -22,6 +22,12 @@ function handlers(): BackgroundJobHandlers {
"messaging.deliver": vi.fn(async () => undefined),
"cloud_agent.poll": vi.fn(async () => undefined),
"profile.consolidate": vi.fn(async () => undefined),
"media.sync": vi.fn(async () => undefined),
"media.proxy": vi.fn(async () => undefined),
"media.analyze": vi.fn(async () => undefined),
"media.cluster": vi.fn(async () => undefined),
"media.render": vi.fn(async () => undefined),
"media.publish": vi.fn(async () => undefined),
};
}

View File

@ -1,3 +1,5 @@
import type { MediaAnalysisKind } from "@rakazo/contracts";
import { MediaAnalysisKindSchema } from "@rakazo/contracts";
import { z } from "zod";
import type {
BackgroundJob,
@ -23,6 +25,15 @@ const payloadSchemas = {
"messaging.deliver": z.object({ runId: z.string().min(1).optional() }),
"cloud_agent.poll": z.object({ agentId: z.string().min(1) }),
"profile.consolidate": z.object({ userId: z.string().min(1) }),
"media.sync": z.object({ sourceId: z.string().min(1) }),
"media.proxy": z.object({ assetId: z.string().min(1) }),
"media.analyze": z.object({
assetId: z.string().min(1),
kinds: z.array(MediaAnalysisKindSchema).optional(),
}),
"media.cluster": z.object({ spaceId: z.string().min(1) }),
"media.render": z.object({ renderId: z.string().min(1) }),
"media.publish": z.object({ publicationId: z.string().min(1) }),
} satisfies { [Name in BackgroundJobName]: z.ZodType<BackgroundJobPayloads[Name]> };
export function parseBackgroundJob(name: string, payload: unknown): BackgroundJob {
@ -160,3 +171,41 @@ export function profileConsolidateJob(userId: string): BackgroundJob {
replaceKey: profileConsolidateJobKey(userId),
};
}
export function mediaSyncJob(sourceId: string): BackgroundJob {
return { name: "media.sync", payload: { sourceId }, replaceKey: `media.sync:${sourceId}` };
}
export function mediaProxyJob(assetId: string): BackgroundJob {
return { name: "media.proxy", payload: { assetId }, replaceKey: `media.proxy:${assetId}` };
}
export function mediaAnalyzeJob(assetId: string, kinds?: MediaAnalysisKind[]): BackgroundJob {
return {
name: "media.analyze",
payload: kinds ? { assetId, kinds } : { assetId },
replaceKey: `media.analyze:${assetId}`,
};
}
export function mediaClusterJob(spaceId: string, availableAt?: Date): BackgroundJob {
return {
name: "media.cluster",
payload: { spaceId },
replaceKey: `media.cluster:${spaceId}`,
...(availableAt ? { availableAt } : {}),
};
}
export function mediaRenderJob(renderId: string): BackgroundJob {
return { name: "media.render", payload: { renderId }, replaceKey: `media.render:${renderId}` };
}
export function mediaPublishJob(publicationId: string, availableAt?: Date): BackgroundJob {
return {
name: "media.publish",
payload: { publicationId },
replaceKey: `media.publish:${publicationId}`,
...(availableAt ? { availableAt } : {}),
};
}

View File

@ -1,5 +1,6 @@
export * from "./background-jobs.js";
export * from "./cloud-agents.js";
export * from "./interfaces.js";
export * from "./media.js";
export * from "./registry.js";
export * from "./types.js";

View File

@ -0,0 +1,325 @@
import type {
AnonymizeMode,
BoxKeyframe,
BrandKit,
ContentCategory,
FfprobeReport,
MediaAssetKind,
MediaCapabilities,
MediaInferenceOperation,
MediaMarkPrompt,
MediaSourceKind,
MotionTemplateInfo,
MotionTemplateRef,
NormalizedBox,
NormalizedPoint,
PlatformProfile,
RenderPlan,
} from "@rakazo/contracts";
/*
* Media Studio provider boundaries. Every implementation (local sidecar,
* emulator, optional cloud adapters) satisfies the same contracts, which
* media-conformance tests exercise offline.
*
* Paths are absolute host paths inside the configured media root. Providers
* must reject any path outside that root.
*/
// ---------------------------------------------------------------------------
// Sources
// ---------------------------------------------------------------------------
export interface MediaSourceItem {
externalId: string;
kind: MediaAssetKind;
capturedAt: Date | null;
durationMs: number | null;
width: number | null;
height: number | null;
fps: number | null;
gpsLat: number | null;
gpsLon: number | null;
deviceModel: string | null;
albumNames: string[];
isFavorite: boolean;
fileName: string | null;
sizeBytes: number | null;
/** Cheap change detector (mtime + size or provider revision); not a content hash. */
revision: string;
}
export interface MediaSourceListRequest {
config: Record<string, unknown>;
cursor: string | null;
limit: number;
signal?: AbortSignal;
}
export interface MediaSourceListResult {
items: MediaSourceItem[];
nextCursor: string | null;
}
/** Server-side sources the worker can enumerate and read directly. */
export interface MediaSourceProvider {
readonly kind: Extract<MediaSourceKind, "folder" | "emulator">;
list(request: MediaSourceListRequest): Promise<MediaSourceListResult>;
/** Absolute readable path of the original file. Throws when it escaped the source root. */
resolve(config: Record<string, unknown>, externalId: string): Promise<string>;
}
// ---------------------------------------------------------------------------
// Inference
// ---------------------------------------------------------------------------
export class UnsupportedMediaOperationError extends Error {
constructor(
readonly operation: MediaInferenceOperation,
readonly reason: string,
) {
super(`${operation} is not available: ${reason}`);
this.name = "UnsupportedMediaOperationError";
}
}
export interface MediaTimeSpan {
startMs: number;
endMs: number;
}
export interface DetectScenesResult {
shots: Array<MediaTimeSpan & { score: number }>;
scenes: MediaTimeSpan[];
}
export interface TranscriptWord {
startMs: number;
endMs: number;
text: string;
confidence: number;
}
export interface TranscribeResult {
language: string | null;
words: TranscriptWord[];
}
export interface FaceObservation {
box: NormalizedBox;
confidence: number;
/** Five-point landmarks: eyes, nose, mouth corners. */
landmarks?: NormalizedPoint[];
}
export interface DetectFacesResult {
frames: Array<{ tMs: number; faces: FaceObservation[] }>;
}
export interface FaceTrackResult {
trackId: string;
keyframes: Array<BoxKeyframe & { confidence: number }>;
/** Identity embedding for local person clustering. Never leaves the host. */
embedding: number[] | null;
}
export interface TrackFacesResult {
sampleFps: number;
tracks: FaceTrackResult[];
}
export interface FaceLandmarksResult {
frames: Array<{
tMs: number;
faces: Array<{ box: NormalizedBox; landmarks: NormalizedPoint[] }>;
}>;
}
export interface EmbedFramesResult {
model: string;
dim: number;
vectors: number[][];
}
export interface ClassifyContentResult {
model: string;
frames: Array<{ tMs: number; scores: Partial<Record<ContentCategory, number>> }>;
}
export interface SegmentPromptedResult {
/** Directory of per-frame PNG masks named by timestamp in ms. */
maskDir: string;
keyframes: Array<BoxKeyframe & { confidence: number }>;
score: number;
}
export interface InpaintQuality {
/** 1 = no flicker inside the mask region between neighbouring frames. */
temporalStability: number;
/** 1 = no visible seams at the mask edge. */
edgeQuality: number;
score: number;
}
export interface InpaintVideoResult {
outPath: string;
quality: InpaintQuality;
}
export interface AudioFeaturesResult {
integratedLufs: number | null;
truePeakDb: number | null;
tempoBpm: number | null;
beatsMs: number[];
energy: Array<{ tMs: number; value: number }>;
silences: MediaTimeSpan[];
}
export interface SaliencyResult {
points: Array<{ tMs: number; cx: number; cy: number; strength: number }>;
}
export interface RedactionInstruction {
keyframes: BoxKeyframe[];
mode: Exclude<AnonymizeMode, "none" | "reframe_out" | "synthetic_identity"> | "cover";
paddingPct: number;
featherPct: number;
/** Solid and cover fill. */
color?: string;
/** Landmarks per keyframe for partial modes. */
landmarks?: Array<{ tMs: number; points: NormalizedPoint[] }>;
/** Mask directory for region marks instead of boxes. */
maskDir?: string;
}
interface MediaPathRequest {
path: string;
signal?: AbortSignal;
}
/**
* Local-first ML operations. Callers check `capabilities()` and fall back
* (blur, cover, reframe) when an operation reports unavailable; providers
* throw UnsupportedMediaOperationError instead of guessing.
*/
export interface MediaInferenceProvider {
readonly id: string;
capabilities(signal?: AbortSignal): Promise<MediaCapabilities>;
detectScenes(request: MediaPathRequest): Promise<DetectScenesResult>;
transcribe(request: MediaPathRequest & { language?: string }): Promise<TranscribeResult>;
detectFaces(
request: MediaPathRequest & { sampleFps: number; timesMs?: number[] },
): Promise<DetectFacesResult>;
trackFaces(request: MediaPathRequest & { sampleFps: number }): Promise<TrackFacesResult>;
faceLandmarks(request: MediaPathRequest & { timesMs: number[] }): Promise<FaceLandmarksResult>;
embedFrames(request: MediaPathRequest & { timesMs: number[] }): Promise<EmbedFramesResult>;
classifyContent(
request: MediaPathRequest & { sampleFps: number; extraTimesMs?: number[] },
): Promise<ClassifyContentResult>;
segmentPrompted(
request: MediaPathRequest & { prompt: MediaMarkPrompt; propagate: boolean; outDir: string },
): Promise<SegmentPromptedResult>;
inpaintVideo(
request: MediaPathRequest & { maskDir: string; outPath: string },
): Promise<InpaintVideoResult>;
/**
* Apply face and region redactions frame-accurately and write a near-lossless
* intermediate. Fails closed: a frame it cannot process is an error, not a pass.
*/
redact(
request: MediaPathRequest & { redactions: RedactionInstruction[]; outPath: string },
): Promise<{ outPath: string; framesProcessed: number }>;
/**
* Replace a tracked face with a generated, non-existent identity derived from
* `seed`. Deliberately takes no target-face image.
*/
synthesizeIdentity(
request: MediaPathRequest & { track: BoxKeyframe[]; seed: number; outPath: string },
): Promise<{ outPath: string }>;
audioFeatures(request: MediaPathRequest): Promise<AudioFeaturesResult>;
saliency(request: MediaPathRequest & { timesMs: number[] }): Promise<SaliencyResult>;
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
export interface MediaRenderProgress {
stepId: string;
/** Overall progress 0..1. */
progress: number;
}
export interface MediaRenderer {
readonly id: string;
/** Hardware encoders usable on this host, e.g. h264_vaapi, h264_videotoolbox. */
hardwareEncoders(): Promise<string[]>;
/**
* Run every ffmpeg step of the plan. Motion, inpaint, and synthetic identity
* steps must already be satisfied (their outPath exists) or the renderer throws.
*/
render(
plan: RenderPlan,
options: { signal?: AbortSignal; onProgress?: (progress: MediaRenderProgress) => void },
): Promise<void>;
probe(
path: string,
options?: { measureLoudness?: boolean; signal?: AbortSignal },
): Promise<FfprobeReport>;
/** Extract a still frame as PNG (EXIF-free). */
extractFrame(input: {
path: string;
tMs: number;
outPath: string;
width?: number;
signal?: AbortSignal;
}): Promise<void>;
}
export interface MotionRenderRequest {
template: MotionTemplateRef;
brandKit: BrandKit;
/** Resolved font and logo files from the brand kit artifacts. */
assets: { fonts: Array<{ family: string; path: string }>; logoPaths: string[] };
width: number;
height: number;
fps: number;
outPath: string;
signal?: AbortSignal;
}
export interface MotionGraphicsRenderer {
readonly id: string;
templates(): MotionTemplateInfo[];
/** Render an overlay clip with alpha (MOV, qtrle or prores 4444). */
render(request: MotionRenderRequest): Promise<{ path: string; hasAlpha: boolean }>;
}
// ---------------------------------------------------------------------------
// Publishing
// ---------------------------------------------------------------------------
export interface MediaPublishRequest {
bundleDir: string;
profile: PlatformProfile;
publicationId: string;
title: string | null;
caption: string | null;
tags: string[];
signal?: AbortSignal;
}
export interface MediaPublishResult {
status: "published" | "bundled";
externalUrl: string | null;
}
export interface MediaPublisher {
readonly id: string;
publish(request: MediaPublishRequest): Promise<MediaPublishResult>;
}
export interface LinkShortener {
readonly id: string;
shorten(url: string, signal?: AbortSignal): Promise<string>;
}

View File

@ -36,4 +36,8 @@ export const slots = {
web: "web",
browser: "browser",
cloudAgent: "cloud-agent",
mediaInference: "media-inference",
mediaRenderer: "media-renderer",
motionGraphics: "motion-graphics",
mediaPublisher: "media-publisher",
} as const;

View File

@ -1,4 +1,9 @@
import type { ConnectionCatalogItem, ModelTier, SandboxKind } from "@rakazo/contracts";
import type {
ConnectionCatalogItem,
MediaAnalysisKind,
ModelTier,
SandboxKind,
} from "@rakazo/contracts";
export interface AdapterContext {
operationId: string;
@ -524,9 +529,22 @@ export interface BackgroundJobPayloads {
"cloud_agent.poll": { agentId: string };
/** Learn durable user facts from conversations since the profile's last consolidation. */
"profile.consolidate": { userId: string };
/** Enumerate a server-side media source incrementally (metadata only). */
"media.sync": { sourceId: string };
/** Hash the original and build proxy, thumbnails, and keyframes. */
"media.proxy": { assetId: string };
/** Run sidecar analyses; omitted kinds means every analysis not yet at the current version. */
"media.analyze": { assetId: string; kinds?: MediaAnalysisKind[] };
/** Rebuild sessions, themes, and person suggestions for a space. */
"media.cluster": { spaceId: string };
/** Render, probe, and gate one MediaRender. */
"media.render": { renderId: string };
/** Build the export bundle and publish a scheduled MediaPublication. */
"media.publish": { publicationId: string };
}
export type BackgroundJobName = keyof BackgroundJobPayloads;
export type MediaJobName = Extract<BackgroundJobName, `media.${string}`>;
export type BackgroundJob = {
[Name in BackgroundJobName]: {
@ -541,6 +559,8 @@ export type BackgroundJobHandlers = {
[Name in BackgroundJobName]: (payload: BackgroundJobPayloads[Name]) => Promise<void>;
};
export type MediaJobHandlers = Pick<BackgroundJobHandlers, MediaJobName>;
export interface SecretRecord {
id: string;
ciphertext: string;

View File

@ -29,6 +29,8 @@
"@rakazo/core": "workspace:*",
"@rakazo/db": "workspace:*",
"@rakazo/logging": "workspace:*",
"@rakazo/media": "workspace:*",
"@resvg/resvg-js": "2.6.2",
"chat": "4.40.0",
"chat-adapter-lark": "0.3.1",
"chat-adapter-sendblue": "0.2.0",

View File

@ -3,6 +3,7 @@ import type {
AgentRuntime,
BackgroundJobHandlers,
JobPublisher,
MediaJobHandlers,
MessagingSurface,
SandboxProvider,
} from "@rakazo/adapter-kit";
@ -36,6 +37,8 @@ export function createBackgroundJobHandlers(deps: {
deploymentModelKey?: string;
messaging?: MessagingSurface;
cloudAgent?: CloudAgentConnection | null;
/** Media Studio job handlers; absent when media is not configured for this worker. */
media?: MediaJobHandlers;
}): BackgroundJobHandlers {
const deliverMessaging = async (runId?: string) => {
if (!deps.messaging) return;
@ -53,6 +56,7 @@ export function createBackgroundJobHandlers(deps: {
};
return {
...(deps.media ?? mediaDisabledHandlers()),
"run.continue": async (payload) => {
await deps.executor.continueRun(payload.runId, deps.workerId);
// Automatic messaging mirror: once the run's bot messages are durable,
@ -126,3 +130,17 @@ export function createBackgroundJobHandlers(deps: {
},
};
}
function mediaDisabledHandlers(): MediaJobHandlers {
const skip = async () => {
getLogger().warn("media job skipped: Media Studio is not configured on this worker");
};
return {
"media.sync": skip,
"media.proxy": skip,
"media.analyze": skip,
"media.cluster": skip,
"media.render": skip,
"media.publish": skip,
};
}

View File

@ -24,6 +24,12 @@ function handlers(): BackgroundJobHandlers {
"messaging.deliver": vi.fn(async () => undefined),
"cloud_agent.poll": vi.fn(async () => undefined),
"profile.consolidate": vi.fn(async () => undefined),
"media.sync": vi.fn(async () => undefined),
"media.proxy": vi.fn(async () => undefined),
"media.analyze": vi.fn(async () => undefined),
"media.cluster": vi.fn(async () => undefined),
"media.render": vi.fn(async () => undefined),
"media.publish": vi.fn(async () => undefined),
};
}

View File

@ -29,6 +29,12 @@ function handlers(overrides: Partial<BackgroundJobHandlers> = {}): BackgroundJob
"messaging.deliver": vi.fn(async () => undefined),
"cloud_agent.poll": vi.fn(async () => undefined),
"profile.consolidate": vi.fn(async () => undefined),
"media.sync": vi.fn(async () => undefined),
"media.proxy": vi.fn(async () => undefined),
"media.analyze": vi.fn(async () => undefined),
"media.cluster": vi.fn(async () => undefined),
"media.render": vi.fn(async () => undefined),
"media.publish": vi.fn(async () => undefined),
...overrides,
};
}

View File

@ -14,6 +14,12 @@ function handlers(): BackgroundJobHandlers {
"messaging.deliver": vi.fn(async () => undefined),
"cloud_agent.poll": vi.fn(async () => undefined),
"profile.consolidate": vi.fn(async () => undefined),
"media.sync": vi.fn(async () => undefined),
"media.proxy": vi.fn(async () => undefined),
"media.analyze": vi.fn(async () => undefined),
"media.cluster": vi.fn(async () => undefined),
"media.render": vi.fn(async () => undefined),
"media.publish": vi.fn(async () => undefined),
};
}

View File

@ -9,6 +9,7 @@ export * from "./inbox.js";
export * from "./integration-settings.js";
export * from "./local-settings.js";
export * from "./mcp.js";
export * from "./media.js";
export * from "./model-tiers.js";
export * from "./openai-compatible-ui.js";
export * from "./project-goals.js";

File diff suppressed because it is too large Load Diff

View File

@ -82,6 +82,7 @@ import {
IntegrationProviderConfigSchema,
IntegrationSetupStateSchema,
} from "./integration-settings.js";
import { mediaRpcShapes } from "./media.js";
import { ModelTierMappingSchema, SetModelTierInput } from "./model-tiers.js";
import {
CreateProjectGoalInput,
@ -157,6 +158,10 @@ const threadSendInput = threadTarget
}
});
function rpc<I extends z.ZodType, O extends z.ZodType>(shape: { input: I; output: O }) {
return oc.input(shape.input).output(shape.output);
}
export const appContract = {
aiConsent: {
status: oc.input(AiConsentQuerySchema).output(AiConsentStatusSchema),
@ -861,6 +866,96 @@ export const appContract = {
put: oc.input(AgentSecretInputSchema).output(AgentSecretSchema),
remove: oc.input(z.object({ id: Id })).output(z.object({ ok: z.literal(true) })),
},
media: {
capabilities: rpc(mediaRpcShapes.capabilities),
sources: {
list: rpc(mediaRpcShapes.sources.list),
createFolder: rpc(mediaRpcShapes.sources.createFolder),
registerClient: rpc(mediaRpcShapes.sources.registerClient),
pushItems: rpc(mediaRpcShapes.sources.pushItems),
sync: rpc(mediaRpcShapes.sources.sync),
remove: rpc(mediaRpcShapes.sources.remove),
},
assets: {
search: rpc(mediaRpcShapes.assets.search),
get: rpc(mediaRpcShapes.assets.get),
setPaidOnly: rpc(mediaRpcShapes.assets.setPaidOnly),
requestUpload: rpc(mediaRpcShapes.assets.requestUpload),
},
uploads: {
begin: rpc(mediaRpcShapes.uploads.begin),
status: rpc(mediaRpcShapes.uploads.status),
complete: rpc(mediaRpcShapes.uploads.complete),
},
collections: {
list: rpc(mediaRpcShapes.collections.list),
get: rpc(mediaRpcShapes.collections.get),
rename: rpc(mediaRpcShapes.collections.rename),
merge: rpc(mediaRpcShapes.collections.merge),
split: rpc(mediaRpcShapes.collections.split),
createManual: rpc(mediaRpcShapes.collections.createManual),
recluster: rpc(mediaRpcShapes.collections.recluster),
},
people: {
list: rpc(mediaRpcShapes.people.list),
update: rpc(mediaRpcShapes.people.update),
merge: rpc(mediaRpcShapes.people.merge),
assignTrack: rpc(mediaRpcShapes.people.assignTrack),
recordConsent: rpc(mediaRpcShapes.people.recordConsent),
remove: rpc(mediaRpcShapes.people.remove),
},
marks: {
list: rpc(mediaRpcShapes.marks.list),
create: rpc(mediaRpcShapes.marks.create),
update: rpc(mediaRpcShapes.marks.update),
remove: rpc(mediaRpcShapes.marks.remove),
},
brandKits: {
list: rpc(mediaRpcShapes.brandKits.list),
upsert: rpc(mediaRpcShapes.brandKits.upsert),
remove: rpc(mediaRpcShapes.brandKits.remove),
templates: rpc(mediaRpcShapes.brandKits.templates),
introVariants: rpc(mediaRpcShapes.brandKits.introVariants),
},
profiles: {
list: rpc(mediaRpcShapes.profiles.list),
get: rpc(mediaRpcShapes.profiles.get),
},
projects: {
list: rpc(mediaRpcShapes.projects.list),
get: rpc(mediaRpcShapes.projects.get),
create: rpc(mediaRpcShapes.projects.create),
update: rpc(mediaRpcShapes.projects.update),
remove: rpc(mediaRpcShapes.projects.remove),
autoPlan: rpc(mediaRpcShapes.projects.autoPlan),
},
plans: {
submit: rpc(mediaRpcShapes.plans.submit),
},
renders: {
start: rpc(mediaRpcShapes.renders.start),
get: rpc(mediaRpcShapes.renders.get),
cancel: rpc(mediaRpcShapes.renders.cancel),
thumbnails: rpc(mediaRpcShapes.renders.thumbnails),
},
marketing: {
copy: rpc(mediaRpcShapes.marketing.copy),
},
publications: {
list: rpc(mediaRpcShapes.publications.list),
create: rpc(mediaRpcShapes.publications.create),
bundle: rpc(mediaRpcShapes.publications.bundle),
markPublished: rpc(mediaRpcShapes.publications.markPublished),
cancel: rpc(mediaRpcShapes.publications.cancel),
},
metrics: {
mappings: rpc(mediaRpcShapes.metrics.mappings),
saveMapping: rpc(mediaRpcShapes.metrics.saveMapping),
importCsv: rpc(mediaRpcShapes.metrics.importCsv),
record: rpc(mediaRpcShapes.metrics.record),
funnel: rpc(mediaRpcShapes.metrics.funnel),
},
},
};
export type AppContract = typeof appContract;

View File

@ -0,0 +1,513 @@
-- CreateTable
CREATE TABLE "media_sources" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"ownerUserId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"label" TEXT NOT NULL,
"config" JSONB NOT NULL DEFAULT '{}',
"status" TEXT NOT NULL DEFAULT 'idle',
"error" TEXT,
"lastSyncCursor" TEXT,
"lastSyncedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_sources_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_assets" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"sourceId" TEXT NOT NULL,
"externalId" TEXT NOT NULL,
"revision" TEXT,
"contentHash" TEXT,
"kind" TEXT NOT NULL,
"storage" TEXT NOT NULL,
"storagePath" TEXT,
"fileName" TEXT,
"sizeBytes" BIGINT,
"capturedAt" TIMESTAMP(3),
"durationMs" INTEGER,
"width" INTEGER,
"height" INTEGER,
"fps" DOUBLE PRECISION,
"codec" TEXT,
"orientation" INTEGER,
"gpsLat" DOUBLE PRECISION,
"gpsLon" DOUBLE PRECISION,
"deviceModel" TEXT,
"albumNames" TEXT[] DEFAULT ARRAY[]::TEXT[],
"isFavorite" BOOLEAN NOT NULL DEFAULT false,
"proxyPath" TEXT,
"thumbPath" TEXT,
"spritePath" TEXT,
"qualityScore" DOUBLE PRECISION,
"analysisVersion" INTEGER NOT NULL DEFAULT 0,
"ingestState" TEXT NOT NULL DEFAULT 'indexed',
"ingestError" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_assets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_uploads" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"assetId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"sizeBytes" BIGINT NOT NULL,
"chunkBytes" INTEGER NOT NULL,
"sha256" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"receivedChunks" INTEGER[] DEFAULT ARRAY[]::INTEGER[],
"status" TEXT NOT NULL DEFAULT 'open',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_uploads_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_segments" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"assetId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"startMs" INTEGER NOT NULL,
"endMs" INTEGER NOT NULL,
"scores" JSONB NOT NULL DEFAULT '{}',
"teaserScore" DOUBLE PRECISION,
"transcriptExcerpt" TEXT,
"labels" TEXT[] DEFAULT ARRAY[]::TEXT[],
"paidOnly" BOOLEAN NOT NULL DEFAULT false,
"analysisVersion" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "media_segments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_analyses" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"assetId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"modelId" TEXT NOT NULL,
"modelVersion" TEXT NOT NULL,
"data" JSONB,
"dataPath" TEXT,
"vectors" BYTEA,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "media_analyses_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_collections" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"title" TEXT NOT NULL,
"labelSource" TEXT NOT NULL DEFAULT 'rule',
"startAt" TIMESTAMP(3),
"endAt" TIMESTAMP(3),
"centroidLat" DOUBLE PRECISION,
"centroidLon" DOUBLE PRECISION,
"coverAssetId" TEXT,
"clusterKey" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_collections_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_collection_items" (
"collectionId" TEXT NOT NULL,
"assetId" TEXT NOT NULL,
"position" INTEGER NOT NULL,
CONSTRAINT "media_collection_items_pkey" PRIMARY KEY ("collectionId","assetId")
);
-- CreateTable
CREATE TABLE "media_people" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"isSelf" BOOLEAN NOT NULL DEFAULT false,
"confirmed" BOOLEAN NOT NULL DEFAULT false,
"anonymizeByDefault" TEXT NOT NULL DEFAULT 'none',
"centroid" BYTEA,
"consentArtifactId" TEXT,
"consentRecordedAt" TIMESTAMP(3),
"consentRecordedBy" TEXT,
"ageVerifiedAt" TIMESTAMP(3),
"ageVerificationMethod" TEXT NOT NULL DEFAULT 'none',
"syntheticSeed" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_people_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_face_tracks" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"assetId" TEXT NOT NULL,
"personId" TEXT,
"startMs" INTEGER NOT NULL,
"endMs" INTEGER NOT NULL,
"confidenceMin" DOUBLE PRECISION NOT NULL,
"keyframesPath" TEXT NOT NULL,
"embedding" BYTEA,
"modelVersion" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "media_face_tracks_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_marks" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"label" TEXT,
"personId" TEXT,
"assetId" TEXT,
"prompt" JSONB,
"treatment" TEXT NOT NULL,
"maskDir" TEXT,
"qualityScore" DOUBLE PRECISION,
"suggestedFallback" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_marks_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_brand_kits" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"kit" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_brand_kits_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_projects" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"createdBy" TEXT NOT NULL,
"title" TEXT NOT NULL,
"goal" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'draft',
"brandKitId" TEXT,
"collectionIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
"assetIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
"profileIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_projects_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_edit_plans" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"edl" JSONB NOT NULL,
"createdBy" TEXT NOT NULL,
"rationale" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "media_edit_plans_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_renders" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"planId" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"quality" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'queued',
"progress" DOUBLE PRECISION NOT NULL DEFAULT 0,
"outputs" JSONB NOT NULL DEFAULT '[]',
"gateReport" JSONB,
"ffprobe" JSONB,
"autoFixRound" INTEGER NOT NULL DEFAULT 0,
"error" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_renders_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_publications" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"renderId" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"destination" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'draft',
"scheduledAt" TIMESTAMP(3),
"publishedAt" TIMESTAMP(3),
"externalUrl" TEXT,
"targetUrl" TEXT,
"trackingUrl" TEXT,
"utm" JSONB,
"title" TEXT,
"caption" TEXT,
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
"thumbnailVariant" TEXT,
"bundleArtifactId" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_publications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_metrics" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"publicationId" TEXT NOT NULL,
"observedAt" TIMESTAMP(3) NOT NULL,
"views" INTEGER NOT NULL DEFAULT 0,
"clicks" INTEGER NOT NULL DEFAULT 0,
"conversions" INTEGER NOT NULL DEFAULT 0,
"revenueCents" INTEGER NOT NULL DEFAULT 0,
"currency" TEXT NOT NULL DEFAULT 'EUR',
"source" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "media_metrics_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_metric_mappings" (
"id" TEXT NOT NULL,
"spaceId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"mapping" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_metric_mappings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "media_teaser_models" (
"spaceId" TEXT NOT NULL,
"weights" JSONB NOT NULL,
"sampleCount" INTEGER NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "media_teaser_models_pkey" PRIMARY KEY ("spaceId")
);
-- CreateIndex
CREATE INDEX "media_sources_spaceId_createdAt_idx" ON "media_sources"("spaceId", "createdAt");
-- CreateIndex
CREATE INDEX "media_assets_spaceId_capturedAt_idx" ON "media_assets"("spaceId", "capturedAt");
-- CreateIndex
CREATE INDEX "media_assets_spaceId_kind_capturedAt_idx" ON "media_assets"("spaceId", "kind", "capturedAt");
-- CreateIndex
CREATE INDEX "media_assets_spaceId_ingestState_idx" ON "media_assets"("spaceId", "ingestState");
-- CreateIndex
CREATE INDEX "media_assets_contentHash_idx" ON "media_assets"("contentHash");
-- CreateIndex
CREATE UNIQUE INDEX "media_assets_sourceId_externalId_key" ON "media_assets"("sourceId", "externalId");
-- CreateIndex
CREATE INDEX "media_uploads_assetId_idx" ON "media_uploads"("assetId");
-- CreateIndex
CREATE INDEX "media_uploads_spaceId_status_idx" ON "media_uploads"("spaceId", "status");
-- CreateIndex
CREATE INDEX "media_segments_assetId_startMs_idx" ON "media_segments"("assetId", "startMs");
-- CreateIndex
CREATE INDEX "media_segments_spaceId_teaserScore_idx" ON "media_segments"("spaceId", "teaserScore");
-- CreateIndex
CREATE INDEX "media_analyses_spaceId_kind_idx" ON "media_analyses"("spaceId", "kind");
-- CreateIndex
CREATE UNIQUE INDEX "media_analyses_assetId_kind_modelVersion_key" ON "media_analyses"("assetId", "kind", "modelVersion");
-- CreateIndex
CREATE INDEX "media_collections_spaceId_kind_startAt_idx" ON "media_collections"("spaceId", "kind", "startAt");
-- CreateIndex
CREATE UNIQUE INDEX "media_collections_spaceId_kind_clusterKey_key" ON "media_collections"("spaceId", "kind", "clusterKey");
-- CreateIndex
CREATE INDEX "media_collection_items_assetId_idx" ON "media_collection_items"("assetId");
-- CreateIndex
CREATE INDEX "media_people_spaceId_idx" ON "media_people"("spaceId");
-- CreateIndex
CREATE INDEX "media_face_tracks_assetId_idx" ON "media_face_tracks"("assetId");
-- CreateIndex
CREATE INDEX "media_face_tracks_personId_idx" ON "media_face_tracks"("personId");
-- CreateIndex
CREATE INDEX "media_marks_spaceId_personId_idx" ON "media_marks"("spaceId", "personId");
-- CreateIndex
CREATE INDEX "media_marks_assetId_idx" ON "media_marks"("assetId");
-- CreateIndex
CREATE INDEX "media_brand_kits_spaceId_idx" ON "media_brand_kits"("spaceId");
-- CreateIndex
CREATE INDEX "media_projects_spaceId_updatedAt_idx" ON "media_projects"("spaceId", "updatedAt");
-- CreateIndex
CREATE UNIQUE INDEX "media_edit_plans_projectId_profileId_version_key" ON "media_edit_plans"("projectId", "profileId", "version");
-- CreateIndex
CREATE INDEX "media_renders_projectId_createdAt_idx" ON "media_renders"("projectId", "createdAt");
-- CreateIndex
CREATE INDEX "media_renders_spaceId_status_idx" ON "media_renders"("spaceId", "status");
-- CreateIndex
CREATE INDEX "media_publications_spaceId_createdAt_idx" ON "media_publications"("spaceId", "createdAt");
-- CreateIndex
CREATE INDEX "media_publications_renderId_idx" ON "media_publications"("renderId");
-- CreateIndex
CREATE INDEX "media_metrics_spaceId_observedAt_idx" ON "media_metrics"("spaceId", "observedAt");
-- CreateIndex
CREATE UNIQUE INDEX "media_metrics_publicationId_observedAt_source_key" ON "media_metrics"("publicationId", "observedAt", "source");
-- CreateIndex
CREATE UNIQUE INDEX "media_metric_mappings_spaceId_name_key" ON "media_metric_mappings"("spaceId", "name");
-- AddForeignKey
ALTER TABLE "media_sources" ADD CONSTRAINT "media_sources_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_assets" ADD CONSTRAINT "media_assets_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_assets" ADD CONSTRAINT "media_assets_sourceId_fkey" FOREIGN KEY ("sourceId") REFERENCES "media_sources"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_uploads" ADD CONSTRAINT "media_uploads_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_uploads" ADD CONSTRAINT "media_uploads_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "media_assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_segments" ADD CONSTRAINT "media_segments_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_segments" ADD CONSTRAINT "media_segments_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "media_assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_analyses" ADD CONSTRAINT "media_analyses_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_analyses" ADD CONSTRAINT "media_analyses_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "media_assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_collections" ADD CONSTRAINT "media_collections_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_collection_items" ADD CONSTRAINT "media_collection_items_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "media_collections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_collection_items" ADD CONSTRAINT "media_collection_items_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "media_assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_people" ADD CONSTRAINT "media_people_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_face_tracks" ADD CONSTRAINT "media_face_tracks_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_face_tracks" ADD CONSTRAINT "media_face_tracks_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "media_assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_face_tracks" ADD CONSTRAINT "media_face_tracks_personId_fkey" FOREIGN KEY ("personId") REFERENCES "media_people"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_marks" ADD CONSTRAINT "media_marks_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_marks" ADD CONSTRAINT "media_marks_personId_fkey" FOREIGN KEY ("personId") REFERENCES "media_people"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_marks" ADD CONSTRAINT "media_marks_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "media_assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_brand_kits" ADD CONSTRAINT "media_brand_kits_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_projects" ADD CONSTRAINT "media_projects_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_projects" ADD CONSTRAINT "media_projects_brandKitId_fkey" FOREIGN KEY ("brandKitId") REFERENCES "media_brand_kits"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_edit_plans" ADD CONSTRAINT "media_edit_plans_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_edit_plans" ADD CONSTRAINT "media_edit_plans_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "media_projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_renders" ADD CONSTRAINT "media_renders_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_renders" ADD CONSTRAINT "media_renders_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "media_projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_renders" ADD CONSTRAINT "media_renders_planId_fkey" FOREIGN KEY ("planId") REFERENCES "media_edit_plans"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_publications" ADD CONSTRAINT "media_publications_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_publications" ADD CONSTRAINT "media_publications_renderId_fkey" FOREIGN KEY ("renderId") REFERENCES "media_renders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_metrics" ADD CONSTRAINT "media_metrics_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_metrics" ADD CONSTRAINT "media_metrics_publicationId_fkey" FOREIGN KEY ("publicationId") REFERENCES "media_publications"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_metric_mappings" ADD CONSTRAINT "media_metric_mappings_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "media_teaser_models" ADD CONSTRAINT "media_teaser_models_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -216,6 +216,23 @@ model Space {
userFactVisibility UserFactSpace[]
delegatedTasks DelegatedTask[]
semanticMemoryItems SemanticMemoryItem[]
mediaSources MediaSource[]
mediaAssets MediaAsset[]
mediaUploads MediaUpload[]
mediaSegments MediaSegment[]
mediaAnalyses MediaAnalysis[]
mediaCollections MediaCollection[]
mediaPeople MediaPerson[]
mediaFaceTracks MediaFaceTrack[]
mediaMarks MediaMark[]
mediaBrandKits MediaBrandKit[]
mediaProjects MediaProject[]
mediaEditPlans MediaEditPlan[]
mediaRenders MediaRender[]
mediaPublications MediaPublication[]
mediaMetrics MediaMetric[]
mediaMetricMappings MediaMetricMapping[]
mediaTeaserModel MediaTeaserModel?
@@unique([id, organizationId])
@@index([organizationId, createdAt])
@ -1497,3 +1514,411 @@ model DelegatedTask {
// CHECK constraint delegated_tasks_status_chk is created in SQL.
@@map("delegated_tasks")
}
// ---------------------------------------------------------------------------
// Media Studio
// ---------------------------------------------------------------------------
model MediaSource {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
ownerUserId String
kind String
label String
/// Folder: { path }. Client-pushed: { deviceId }. Never secrets.
config Json @default("{}")
status String @default("idle")
error String?
lastSyncCursor String?
lastSyncedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assets MediaAsset[]
@@index([spaceId, createdAt])
@@map("media_sources")
}
model MediaAsset {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
sourceId String
source MediaSource @relation(fields: [sourceId], references: [id], onDelete: Cascade)
externalId String
revision String?
contentHash String?
kind String
storage String
/// Uploaded originals: path relative to the media root. Referenced originals resolve through the source.
storagePath String?
fileName String?
sizeBytes BigInt?
capturedAt DateTime?
durationMs Int?
width Int?
height Int?
fps Float?
codec String?
orientation Int?
/// Rounded to two decimals (about 1 km).
gpsLat Float?
gpsLon Float?
deviceModel String?
albumNames String[] @default([])
isFavorite Boolean @default(false)
proxyPath String?
thumbPath String?
spritePath String?
qualityScore Float?
analysisVersion Int @default(0)
ingestState String @default("indexed")
ingestError String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
segments MediaSegment[]
analyses MediaAnalysis[]
collectionItems MediaCollectionItem[]
faceTracks MediaFaceTrack[]
marks MediaMark[]
uploads MediaUpload[]
@@unique([sourceId, externalId])
@@index([spaceId, capturedAt])
@@index([spaceId, kind, capturedAt])
@@index([spaceId, ingestState])
@@index([contentHash])
@@map("media_assets")
}
model MediaUpload {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
assetId String
asset MediaAsset @relation(fields: [assetId], references: [id], onDelete: Cascade)
userId String
sizeBytes BigInt
chunkBytes Int
sha256 String
fileName String
receivedChunks Int[] @default([])
status String @default("open")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([assetId])
@@index([spaceId, status])
@@map("media_uploads")
}
model MediaSegment {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
assetId String
asset MediaAsset @relation(fields: [assetId], references: [id], onDelete: Cascade)
kind String
startMs Int
endMs Int
/// Partial<Record<TeaserFeature, number>>
scores Json @default("{}")
teaserScore Float?
transcriptExcerpt String?
labels String[] @default([])
paidOnly Boolean @default(false)
analysisVersion Int
createdAt DateTime @default(now())
@@index([assetId, startMs])
@@index([spaceId, teaserScore])
@@map("media_segments")
}
model MediaAnalysis {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
assetId String
asset MediaAsset @relation(fields: [assetId], references: [id], onDelete: Cascade)
kind String
modelId String
modelVersion String
/// Small results inline; large results as a path relative to the media root.
data Json?
dataPath String?
/// Float32 little-endian vectors for embeddings.
vectors Bytes?
createdAt DateTime @default(now())
@@unique([assetId, kind, modelVersion])
@@index([spaceId, kind])
@@map("media_analyses")
}
model MediaCollection {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
kind String
title String
labelSource String @default("rule")
startAt DateTime?
endAt DateTime?
centroidLat Float?
centroidLon Float?
coverAssetId String?
/// Stable key so re-clustering updates instead of recreating user-renamed collections.
clusterKey String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items MediaCollectionItem[]
@@unique([spaceId, kind, clusterKey])
@@index([spaceId, kind, startAt])
@@map("media_collections")
}
model MediaCollectionItem {
collectionId String
collection MediaCollection @relation(fields: [collectionId], references: [id], onDelete: Cascade)
assetId String
asset MediaAsset @relation(fields: [assetId], references: [id], onDelete: Cascade)
position Int
@@id([collectionId, assetId])
@@index([assetId])
@@map("media_collection_items")
}
model MediaPerson {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
displayName String
isSelf Boolean @default(false)
confirmed Boolean @default(false)
anonymizeByDefault String @default("none")
/// Mean identity embedding (Float32). Biometric: local only, deleted with the person.
centroid Bytes?
consentArtifactId String?
consentRecordedAt DateTime?
consentRecordedBy String?
ageVerifiedAt DateTime?
ageVerificationMethod String @default("none")
/// Seed for a stable synthetic identity.
syntheticSeed Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
faceTracks MediaFaceTrack[]
marks MediaMark[]
@@index([spaceId])
@@map("media_people")
}
model MediaFaceTrack {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
assetId String
asset MediaAsset @relation(fields: [assetId], references: [id], onDelete: Cascade)
personId String?
person MediaPerson? @relation(fields: [personId], references: [id], onDelete: SetNull)
startMs Int
endMs Int
confidenceMin Float
/// Box keyframes JSON relative to the media root.
keyframesPath String
/// Biometric (Float32). Local only.
embedding Bytes?
modelVersion String
createdAt DateTime @default(now())
@@index([assetId])
@@index([personId])
@@map("media_face_tracks")
}
model MediaMark {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
kind String
label String?
personId String?
person MediaPerson? @relation(fields: [personId], references: [id], onDelete: Cascade)
assetId String?
asset MediaAsset? @relation(fields: [assetId], references: [id], onDelete: Cascade)
/// MediaMarkPrompt
prompt Json?
treatment String
maskDir String?
qualityScore Float?
suggestedFallback String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([spaceId, personId])
@@index([assetId])
@@map("media_marks")
}
model MediaBrandKit {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
/// BrandKit
kit Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projects MediaProject[]
@@index([spaceId])
@@map("media_brand_kits")
}
model MediaProject {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
createdBy String
title String
goal String
status String @default("draft")
brandKitId String?
brandKit MediaBrandKit? @relation(fields: [brandKitId], references: [id], onDelete: SetNull)
collectionIds String[] @default([])
assetIds String[] @default([])
profileIds String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
plans MediaEditPlan[]
renders MediaRender[]
@@index([spaceId, updatedAt])
@@map("media_projects")
}
model MediaEditPlan {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
projectId String
project MediaProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
profileId String
version Int
/// Edl, validated before insert.
edl Json
createdBy String
rationale String?
createdAt DateTime @default(now())
renders MediaRender[]
@@unique([projectId, profileId, version])
@@map("media_edit_plans")
}
model MediaRender {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
projectId String
project MediaProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
planId String
plan MediaEditPlan @relation(fields: [planId], references: [id], onDelete: Cascade)
profileId String
quality String
status String @default("queued")
progress Float @default(0)
/// MediaRenderOutput[] plus internal relative paths.
outputs Json @default("[]")
gateReport Json?
ffprobe Json?
autoFixRound Int @default(0)
error String?
createdBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
publications MediaPublication[]
@@index([projectId, createdAt])
@@index([spaceId, status])
@@map("media_renders")
}
model MediaPublication {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
renderId String
render MediaRender @relation(fields: [renderId], references: [id], onDelete: Cascade)
profileId String
destination String
status String @default("draft")
scheduledAt DateTime?
publishedAt DateTime?
externalUrl String?
targetUrl String?
trackingUrl String?
utm Json?
title String?
caption String?
tags String[] @default([])
thumbnailVariant String?
bundleArtifactId String?
createdBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
metrics MediaMetric[]
@@index([spaceId, createdAt])
@@index([renderId])
@@map("media_publications")
}
model MediaMetric {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
publicationId String
publication MediaPublication @relation(fields: [publicationId], references: [id], onDelete: Cascade)
observedAt DateTime
views Int @default(0)
clicks Int @default(0)
conversions Int @default(0)
revenueCents Int @default(0)
currency String @default("EUR")
source String
createdAt DateTime @default(now())
@@unique([publicationId, observedAt, source])
@@index([spaceId, observedAt])
@@map("media_metrics")
}
model MediaMetricMapping {
id String @id @default(cuid())
spaceId String
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
name String
/// MetricCsvMapping without id
mapping Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([spaceId, name])
@@map("media_metric_mappings")
}
model MediaTeaserModel {
spaceId String @id
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
/// TeaserWeights
weights Json
sampleCount Int
updatedAt DateTime @updatedAt
@@map("media_teaser_models")
}

View File

@ -0,0 +1,30 @@
{
"name": "@rakazo/media",
"version": "0.1.0",
"license": "Apache-2.0",
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./src/index.ts"
},
"./node": {
"types": "./src/node.ts",
"development": "./src/node.ts",
"default": "./src/node.ts"
}
},
"scripts": {
"check": "tsc --noEmit -p tsconfig.json",
"test": "vitest run --root ../.. packages/media/src"
},
"dependencies": {
"@rakazo/contracts": "workspace:*",
"zod": "4.6.2"
},
"devDependencies": {
"typescript": "7.0.2",
"vitest": "4.1.11"
}
}

View File

@ -0,0 +1,87 @@
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");
}

View File

@ -0,0 +1,117 @@
import type {
BrandKit,
Edl,
MediaRenderQuality,
NormalizedBox,
PlatformProfile,
ReframeKeyframe,
RenderPlan,
} from "@rakazo/contracts";
import { notImplemented } from "./not-implemented.js";
export interface CompileSource {
/** Redacted intermediate when redactions apply, otherwise original or proxy. */
path: string;
width: number;
height: number;
durationMs: number;
fps: number;
hasAudio: boolean;
rotation: 0 | 90 | 180 | 270;
}
export interface CaptionWord {
startMs: number;
endMs: number;
text: string;
}
export interface CompileInput {
edl: Edl;
profile: PlatformProfile;
brandKit: BrandKit | null;
quality: MediaRenderQuality;
/** Absolute work directory for intermediates and outputs. */
workDir: string;
sources: Map<string, CompileSource>;
/** Source-time words per asset for captions. */
transcripts: Map<string, CaptionWord[]>;
/** Saliency or subject points per asset for "auto" reframe. */
subjects: Map<string, Array<{ tMs: number; cx: number; cy: number; strength: number }>>;
/** Pre-rendered motion clips keyed by overlay id, "intro", "outro". */
motionClips: Map<string, string>;
files: {
lutPath: string | null;
watermarkPath: string | null;
fonts: Map<string, string>;
/** artifactId → absolute path for audio clips. */
audio: Map<string, string>;
};
/** ffmpeg video encoder name, e.g. libx264, h264_vaapi, h264_videotoolbox. */
videoEncoder: string;
}
/**
* Pure compiler from a validated EDL to ffmpeg argv steps plus caption files.
* No string ever reaches a shell; text is escaped for filtergraph and ASS,
* every label comes from SafeId values.
*/
export function compileRenderPlan(_input: CompileInput): RenderPlan {
return notImplemented("compileRenderPlan");
}
export interface CaptionCue {
startMs: number;
endMs: number;
text: string;
/** Word timings inside the cue for karaoke highlight. */
words: CaptionWord[];
}
export function buildCaptionCues(
_words: CaptionWord[],
_options: { maxCharsPerLine: number; maxLines: number },
): CaptionCue[] {
return notImplemented("buildCaptionCues");
}
export function toSrt(_cues: CaptionCue[]): string {
return notImplemented("toSrt");
}
export function toVtt(_cues: CaptionCue[]): string {
return notImplemented("toVtt");
}
/** Smoothed crop path that keeps the subject in frame for the output aspect. */
export function autoReframe(
_subjects: Array<{ tMs: number; cx: number; cy: number; strength: number }>,
_input: {
sourceWidth: number;
sourceHeight: number;
outWidth: number;
outHeight: number;
durationMs: number;
},
): ReframeKeyframe[] {
return notImplemented("autoReframe");
}
/** Crop path that keeps every given face box outside the output frame, or null if impossible. */
export function reframeExcluding(
_faces: Array<{ tMs: number; box: NormalizedBox }>,
_input: {
sourceWidth: number;
sourceHeight: number;
outWidth: number;
outHeight: number;
durationMs: number;
},
): ReframeKeyframe[] | null {
return notImplemented("reframeExcluding");
}
/** Escape text for ffmpeg drawtext/filtergraph values. */
export function escapeFilterText(_text: string): string {
return notImplemented("escapeFilterText");
}

45
packages/media/src/edl.ts Normal file
View File

@ -0,0 +1,45 @@
import type { BrandKit, Edl, EdlError, MediaAssetKind, PlatformProfile } from "@rakazo/contracts";
import { notImplemented } from "./not-implemented.js";
export interface EdlContext {
profile: PlatformProfile;
brandKit: BrandKit | null;
assets: Map<
string,
{ kind: MediaAssetKind; durationMs: number | null; width: number | null; height: number | null }
>;
/** Artifact ids the plan may reference (music, voice-over, fonts, logos). */
artifactIds: Set<string>;
/** Music artifacts and whether their license allows commercial use. */
music: Map<string, { commercialUse: boolean }>;
markIds: Set<string>;
personIds: Set<string>;
faceTrackIds: Set<string>;
templateIds: Set<string>;
/** Segments the user reserved for the paid release. */
paidOnly: Array<{ assetId: string; startMs: number; endMs: number }>;
}
export type EdlValidation = { ok: true; edl: Edl } | { ok: false; errors: EdlError[] };
/**
* Parse with EdlSchema, then check references, ranges, overlaps, durations,
* safe zones, paid-only use (teaser-style profiles), and music licensing.
* Every failure is reported; nothing is silently corrected.
*/
export function validateEdl(_input: unknown, _context: EdlContext): EdlValidation {
return notImplemented("validateEdl");
}
/** Output length in ms including intro, outro, speed, and transition overlap. */
export function edlDurationMs(_edl: Edl): number {
return notImplemented("edlDurationMs");
}
/** Map an output timestamp back to the clip and source time it shows. */
export function locateOutputTime(
_edl: Edl,
_outputMs: number,
): { clipId: string; assetId: string; sourceMs: number } | null {
return notImplemented("locateOutputTime");
}

View File

@ -0,0 +1,87 @@
import type {
AiAlteration,
ContentCategory,
Edl,
FfprobeReport,
GateIssue,
GateReport,
NormalizedBox,
PlatformProfile,
} from "@rakazo/contracts";
import { notImplemented } from "./not-implemented.js";
/** Tags a clean export may carry. Everything else is a metadata leak. */
export const METADATA_ALLOWED_TAGS: readonly string[] = [];
export interface GateInput {
profile: PlatformProfile;
edl: Edl;
ffprobe: FfprobeReport;
/** Classifier scores on the rendered output. */
content: Array<{ tMs: number; scores: Partial<Record<ContentCategory, number>> }>;
/**
* Faces re-detected on the rendered output. personId null means the face
* could not be matched to a known person.
*/
faces: Array<{ tMs: number; box: NormalizedBox; personId: string | null; coverage: number }>;
/** Frames where tracking lost a face that must stay anonymized for longer than allowed. */
trackingGaps: Array<{ startMs: number; endMs: number; personId: string | null }>;
/** Residual visibility 0..1 of treated marks in the output. */
marks: Array<{ markId: string; tMs: number; residualVisibility: number }>;
/** People visible in the output with their protection and consent state. */
people: Array<{
id: string;
anonymizeByDefault: string;
consentRecordedAt: string | null;
ageVerifiedAt: string | null;
}>;
copy: { title: string | null; caption: string | null; tags: string[] };
aiAlterations: AiAlteration[];
bundleHasDisclosure: boolean;
watermarkBox: NormalizedBox | null;
sampledFrames: number;
autoFixRound: number;
now: Date;
}
/** Deterministic gate (spec 6). Any blocking issue fails the report. */
export function evaluateGate(_input: GateInput): GateReport {
return notImplemented("evaluateGate");
}
export function checkTechnical(
_profile: PlatformProfile,
_edl: Edl,
_probe: FfprobeReport,
): GateIssue[] {
return notImplemented("checkTechnical");
}
export function findMetadataLeaks(_tags: Record<string, string>): string[] {
return notImplemented("findMetadataLeaks");
}
export function checkCopy(
_profile: PlatformProfile,
_copy: { title: string | null; caption: string | null; tags: string[] },
): GateIssue[] {
return notImplemented("checkCopy");
}
export interface AutoFixResult {
edl: Edl;
changes: string[];
}
/**
* Compliance-only repairs: trim or drop offending clips, change crop, widen
* redaction padding, swap to another segment. Never adds obfuscation aimed at
* classifiers. Returns null when the round limit is reached or nothing safe applies.
*/
export function planAutoFix(
_edl: Edl,
_report: GateReport,
_alternatives: Array<{ segmentId: string; assetId: string; startMs: number; endMs: number }>,
): AutoFixResult | null {
return notImplemented("planAutoFix");
}

28
packages/media/src/geo.ts Normal file
View File

@ -0,0 +1,28 @@
import { notImplemented } from "./not-implemented.js";
/** Great-circle distance in kilometres. */
export function haversineKm(
_a: { lat: number; lon: number },
_b: { lat: number; lon: number },
): number {
return notImplemented("haversineKm");
}
/** Round a coordinate to two decimals (about 1 km) before storage or display. */
export function roundCoordinate(_value: number): number {
return notImplemented("roundCoordinate");
}
/** Cosine similarity of two equal-length vectors. */
export function cosineSimilarity(_a: ArrayLike<number>, _b: ArrayLike<number>): number {
return notImplemented("cosineSimilarity");
}
/** Encode Float32 vectors as little-endian bytes for MediaAnalysis.vectors and centroids. */
export function encodeVectors(_vectors: number[][]): Uint8Array {
return notImplemented("encodeVectors");
}
export function decodeVectors(_bytes: Uint8Array, _dim: number): Float32Array[] {
return notImplemented("decodeVectors");
}

View File

@ -0,0 +1,8 @@
export * from "./clustering.js";
export * from "./compile.js";
export * from "./edl.js";
export * from "./gate.js";
export * from "./geo.js";
export * from "./marketing.js";
export * from "./profiles.js";
export * from "./teaser.js";

View File

@ -0,0 +1,66 @@
import type {
BrandKit,
MetricCsvMapping,
MotionTemplateInfo,
MotionTemplateRef,
PlatformProfile,
Utm,
} from "@rakazo/contracts";
import { notImplemented } from "./not-implemented.js";
/** Append UTM parameters to an http(s) target. Rejects other schemes. */
export function buildTrackingUrl(_targetUrl: string, _utm: Utm): string {
return notImplemented("buildTrackingUrl");
}
export function normalizeHashtags(_tags: string[], _limit: number): string[] {
return notImplemented("normalizeHashtags");
}
export interface ParsedMetricRow {
row: number;
publicationKey: string;
observedAt: Date;
views: number;
clicks: number;
conversions: number;
revenueCents: number;
currency: string;
}
/** RFC 4180 CSV parsing with the saved column mapping. */
export function parseMetricsCsv(
_csv: string,
_mapping: MetricCsvMapping,
): { rows: ParsedMetricRow[]; skipped: Array<{ row: number; reason: string }> } {
return notImplemented("parseMetricsCsv");
}
/** Whether the brand intro fits before the hook window; shorts put the hook first. */
export function introFits(_profile: PlatformProfile, _introMs: number): boolean {
return notImplemented("introFits");
}
/** Deterministic intro proposals from the brand kit, tone, and available templates. */
export function proposeIntroVariants(
_kit: BrandKit,
_templates: MotionTemplateInfo[],
_count: number,
): Array<{ template: MotionTemplateRef; rationale: string }> {
return notImplemented("proposeIntroVariants");
}
/** Structural brand-kit checks beyond the schema (contrast, duplicate presets). */
export function validateBrandKit(_kit: BrandKit): string[] {
return notImplemented("validateBrandKit");
}
/** Upload checklist markdown for an export bundle. */
export function uploadChecklist(_input: {
profile: PlatformProfile;
aiAltered: boolean;
hasCaptions: boolean;
trackingUrl: string | null;
}): string {
return notImplemented("uploadChecklist");
}

View File

@ -0,0 +1,18 @@
import type { PlatformProfile } from "@rakazo/contracts";
import { notImplemented } from "./not-implemented.js";
import type { ProfileLoadError } from "./profiles.js";
/** Directory of the profile JSON files shipped with this package. */
export const BUILTIN_PROFILES_DIR = new URL("../profiles/", import.meta.url);
/** Read every *.json in a directory; missing directory yields no profiles. */
export async function loadProfilesFromDir(
_dir: string | URL,
): Promise<{ profiles: PlatformProfile[]; errors: ProfileLoadError[] }> {
return notImplemented("loadProfilesFromDir");
}
/** Resolve a path inside root; rejects traversal and symlinks that escape it. */
export async function resolveInside(_root: string, _relative: string): Promise<string> {
return notImplemented("resolveInside");
}

View File

@ -0,0 +1,4 @@
/** Phase A scaffold marker. No call site may remain once Media Studio ships. */
export function notImplemented(name: string): never {
throw new Error(`not implemented: ${name}`);
}

View File

@ -0,0 +1,38 @@
import type { PlatformProfile, PlatformProfileSummary } from "@rakazo/contracts";
import { notImplemented } from "./not-implemented.js";
export const PROFILE_STALE_DAYS = 90;
export interface ProfileLoadError {
file: string;
message: string;
}
export interface ProfileSet {
profiles: Map<string, { profile: PlatformProfile; origin: "builtin" | "private" }>;
errors: ProfileLoadError[];
}
/** Parse raw JSON documents; invalid ones become errors and are skipped whole. */
export function parseProfiles(_documents: Array<{ file: string; json: unknown }>): {
profiles: PlatformProfile[];
errors: ProfileLoadError[];
} {
return notImplemented("parseProfiles");
}
/** Private profiles override builtin ones with the same id. */
export function mergeProfiles(
_builtin: PlatformProfile[],
_private: PlatformProfile[],
): ProfileSet {
return notImplemented("mergeProfiles");
}
export function summarizeProfile(
_profile: PlatformProfile,
_origin: "builtin" | "private",
_now: Date,
): PlatformProfileSummary {
return notImplemented("summarizeProfile");
}

View File

@ -0,0 +1,98 @@
import type {
BrandKit,
Edl,
MediaProjectGoal,
PlatformProfile,
TeaserFeature,
TeaserWeights,
} from "@rakazo/contracts";
import { notImplemented } from "./not-implemented.js";
export type SegmentFeatures = Partial<Record<TeaserFeature, number>>;
export function defaultTeaserWeights(_goal: MediaProjectGoal): TeaserWeights {
return notImplemented("defaultTeaserWeights");
}
export interface ScoredSegmentInput {
id: string;
assetId: string;
startMs: number;
endMs: number;
capturedAt: Date | null;
features: SegmentFeatures;
paidOnly: boolean;
transcript: string | null;
}
export function scoreSegment(_features: SegmentFeatures, _weights: TeaserWeights): number {
return notImplemented("scoreSegment");
}
/** 0..1 speech-hook strength from a transcript excerpt (questions, exclamations, numbers, announcements). */
export function speechHookScore(_text: string, _language?: string): number {
return notImplemented("speechHookScore");
}
export interface TeaserSelection {
clips: Array<{ segmentId: string; assetId: string; inMs: number; outMs: number }>;
hookSegmentId: string | null;
/** Why each clip was chosen, for plan rationale. */
notes: string[];
}
/**
* Pick clips for a goal and profile: a hook inside hookWindowMs, rising
* tension, cut before the payoff (open loop), never paid-only segments,
* total within the profile's duration bounds.
*/
export function selectTeaserSegments(_input: {
segments: ScoredSegmentInput[];
profile: PlatformProfile;
goal: MediaProjectGoal;
targetMs: number;
weights: TeaserWeights;
endCardMs: number;
}): TeaserSelection {
return notImplemented("selectTeaserSegments");
}
/** Regularized logistic regression of conversion on features; null below TEASER_LEARNING_MIN_SAMPLES. */
export function fitTeaserWeights(
_samples: Array<{ features: SegmentFeatures; views: number; conversions: number }>,
_prior: TeaserWeights,
): TeaserWeights | null {
return notImplemented("fitTeaserWeights");
}
/** Deterministic first-cut EDL for a project and profile; the agent refines it. */
export function autoPlanEdl(_input: {
goal: MediaProjectGoal;
profile: PlatformProfile;
brandKit: BrandKit | null;
segments: ScoredSegmentInput[];
weights: TeaserWeights;
assets: Map<
string,
{ durationMs: number | null; width: number | null; height: number | null; hasSpeech: boolean }
>;
/** Person anonymization defaults to carry into redactions. */
people: Array<{ id: string; anonymizeByDefault: string }>;
marks: Array<{ id: string; treatment: string; assetId: string | null; personId: string | null }>;
ctaText: string | null;
}): Edl {
return notImplemented("autoPlanEdl");
}
export function rankThumbnailFrames(
_candidates: Array<{
tMs: number;
sharpness: number;
face: number;
thirds: number;
contrast: number;
}>,
_count: number,
): number[] {
return notImplemented("rankThumbnailFrames");
}

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"noEmit": true
},
"include": ["src"]
}

202
pnpm-lock.yaml generated
View File

@ -78,6 +78,9 @@ importers:
'@rakazo/logging':
specifier: workspace:*
version: link:../../packages/logging
'@rakazo/media':
specifier: workspace:*
version: link:../../packages/media
'@rakazo/memory':
specifier: workspace:*
version: link:../../packages/memory
@ -176,6 +179,9 @@ importers:
expo-localization:
specifier: 57.0.2
version: 57.0.2(expo@57.0.26)(react@19.2.3)
expo-media-library:
specifier: 57.0.5
version: 57.0.5(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))
expo-modules-core:
specifier: 57.0.20
version: 57.0.20(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)
@ -203,6 +209,9 @@ importers:
expo-updates:
specifier: 57.0.24
version: 57.0.24(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)
expo-video:
specifier: 57.0.5
version: 57.0.5(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)
react:
specifier: 19.2.3
version: 19.2.3
@ -288,6 +297,9 @@ importers:
'@rakazo/ui-web':
specifier: workspace:*
version: link:../../packages/ui-web
'@tanstack/react-virtual':
specifier: 3.14.13
version: 3.14.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
better-auth:
specifier: 1.7.4
version: 1.7.4(@opentelemetry/api@1.9.1)(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2))(typescript@7.0.2))(mysql2@3.15.3)(pg@8.23.0)(prisma@7.10.0(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(magicast@0.5.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@7.0.2))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.0)(jsdom@30.0.1(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)))
@ -579,6 +591,12 @@ importers:
'@rakazo/logging':
specifier: workspace:*
version: link:../logging
'@rakazo/media':
specifier: workspace:*
version: link:../media
'@resvg/resvg-js':
specifier: 2.6.2
version: 2.6.2
chat:
specifier: 4.40.0
version: 4.40.0(zod@4.6.2)
@ -800,6 +818,22 @@ importers:
specifier: 4.1.11
version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.0)(jsdom@30.0.1(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))
packages/media:
dependencies:
'@rakazo/contracts':
specifier: workspace:*
version: link:../contracts
zod:
specifier: 4.6.2
version: 4.6.2
devDependencies:
typescript:
specifier: 7.0.2
version: 7.0.2
vitest:
specifier: 4.1.11
version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.0)(jsdom@30.0.1(@noble/hashes@2.3.0))(vite@8.3.0(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))
packages/memory:
dependencies:
'@rakazo/adapter-kit':
@ -3800,6 +3834,82 @@ packages:
'@types/react':
optional: true
'@resvg/resvg-js-android-arm-eabi@2.6.2':
resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
'@resvg/resvg-js-android-arm64@2.6.2':
resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@resvg/resvg-js-darwin-arm64@2.6.2':
resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@resvg/resvg-js-darwin-x64@2.6.2':
resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@resvg/resvg-js-linux-x64-musl@2.6.2':
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@resvg/resvg-js@2.6.2':
resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==}
engines: {node: '>= 10'}
'@rolldown/binding-android-arm-eabi@1.2.8':
resolution: {integrity: sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4116,6 +4226,15 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
'@tanstack/react-virtual@3.14.13':
resolution: {integrity: sha512-JbDTAwtzZ99aOeCrAfW5EsE5KSq5RWh6Af2dtFwyLIIk48Ja7vm6n4axu/43T3vfjFnEGJalxJ1wyYjSQD6bSg==}
peerDependencies:
react: 19.2.3
react-dom: 19.2.3
'@tanstack/virtual-core@3.17.11':
resolution: {integrity: sha512-+ILjvtHup6Y2hzQ6YzwMgX1Q+oQpxEGOXCEsCNaPoIP0VxMbizIBTmYTDtkerkIQS8/CbP1BRuyt8V/8BCsy1g==}
'@testcontainers/postgresql@12.1.0':
resolution: {integrity: sha512-Pjf2VSVNirEPfz36nidyrVAnZvc2YhajOznY4VgyEsvfTd5qiMNOuPq96drREvxAUtXl5SFLX7vXj7sSq4aTcA==}
@ -6165,6 +6284,12 @@ packages:
peerDependencies:
expo: '*'
expo-media-library@57.0.5:
resolution: {integrity: sha512-amSLrLsS7Gl4nCqbc2PlVbHj5jK9kQLsSOQgJ2QasHCSHzYXw6Ui7aolxX5APg9P4TgYPE0yWn46ndifh52bRw==}
peerDependencies:
expo: '*'
react-native: '*'
expo-modules-autolinking@57.0.13:
resolution: {integrity: sha512-Hj5NjZRlfccazhKab+wK19leRVUr1Y/PUERaRvegIbUSkq4GVxKq6lU9QHlKQyC16MdD/VS4iwsiLivB4o7lvQ==}
hasBin: true
@ -6279,6 +6404,13 @@ packages:
expo-dev-client:
optional: true
expo-video@57.0.5:
resolution: {integrity: sha512-VK2o+d6rb+tVeL2DI6d0F58pRV8d8bctLpOrMJEHRYtMIK5iwvjjrRx2mciYN8/d04SgWC8TkI+wuZ8Sbp/yuA==}
peerDependencies:
expo: '*'
react: 19.2.3
react-native: '*'
expo@57.0.26:
resolution: {integrity: sha512-U1rsFwcuZNnU61Z/YLnnVC/ZqP0wpxh/rCKUMff3ot/rhLpxFY1zxEqu4vyaexyvBAB1mwpl9lA/ltcHBoQGyg==}
hasBin: true
@ -13559,6 +13691,57 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.18
'@resvg/resvg-js-android-arm-eabi@2.6.2':
optional: true
'@resvg/resvg-js-android-arm64@2.6.2':
optional: true
'@resvg/resvg-js-darwin-arm64@2.6.2':
optional: true
'@resvg/resvg-js-darwin-x64@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
optional: true
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
optional: true
'@resvg/resvg-js-linux-x64-musl@2.6.2':
optional: true
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
optional: true
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
optional: true
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
optional: true
'@resvg/resvg-js@2.6.2':
optionalDependencies:
'@resvg/resvg-js-android-arm-eabi': 2.6.2
'@resvg/resvg-js-android-arm64': 2.6.2
'@resvg/resvg-js-darwin-arm64': 2.6.2
'@resvg/resvg-js-darwin-x64': 2.6.2
'@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2
'@resvg/resvg-js-linux-arm64-gnu': 2.6.2
'@resvg/resvg-js-linux-arm64-musl': 2.6.2
'@resvg/resvg-js-linux-x64-gnu': 2.6.2
'@resvg/resvg-js-linux-x64-musl': 2.6.2
'@resvg/resvg-js-win32-arm64-msvc': 2.6.2
'@resvg/resvg-js-win32-ia32-msvc': 2.6.2
'@resvg/resvg-js-win32-x64-msvc': 2.6.2
'@rolldown/binding-android-arm-eabi@1.2.8':
optional: true
@ -13847,6 +14030,14 @@ snapshots:
tailwindcss: 4.3.3
vite: 8.3.0(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)
'@tanstack/react-virtual@3.14.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tanstack/virtual-core': 3.17.11
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@tanstack/virtual-core@3.17.11': {}
'@testcontainers/postgresql@12.1.0':
dependencies:
testcontainers: 12.1.0
@ -16233,6 +16424,11 @@ snapshots:
expo: 57.0.26(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3)
expo-json-utils: 57.0.2
expo-media-library@57.0.5(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)):
dependencies:
expo: 57.0.26(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3)
react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)
expo-modules-autolinking@57.0.13(typescript@6.0.3):
dependencies:
'@expo/require-utils': 57.0.5(typescript@6.0.3)
@ -16391,6 +16587,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
expo-video@57.0.5(expo@57.0.26)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3):
dependencies:
expo: 57.0.26(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3)
react: 19.2.3
react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3)
expo@57.0.26(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.3))(react@19.2.3)(typescript@6.0.3):
dependencies:
'@babel/runtime': 7.29.7