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:
parent
3c3961f242
commit
a663764ee8
@ -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
|
||||
|
||||
@ -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
113
apps/api/src/media.ts
Normal 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),
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
325
packages/adapter-kit/src/media.ts
Normal file
325
packages/adapter-kit/src/media.ts
Normal 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>;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
1518
packages/contracts/src/media.ts
Normal file
1518
packages/contracts/src/media.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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")
|
||||
}
|
||||
|
||||
30
packages/media/package.json
Normal file
30
packages/media/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
87
packages/media/src/clustering.ts
Normal file
87
packages/media/src/clustering.ts
Normal 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");
|
||||
}
|
||||
117
packages/media/src/compile.ts
Normal file
117
packages/media/src/compile.ts
Normal 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
45
packages/media/src/edl.ts
Normal 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");
|
||||
}
|
||||
87
packages/media/src/gate.ts
Normal file
87
packages/media/src/gate.ts
Normal 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
28
packages/media/src/geo.ts
Normal 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");
|
||||
}
|
||||
8
packages/media/src/index.ts
Normal file
8
packages/media/src/index.ts
Normal 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";
|
||||
66
packages/media/src/marketing.ts
Normal file
66
packages/media/src/marketing.ts
Normal 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");
|
||||
}
|
||||
18
packages/media/src/node.ts
Normal file
18
packages/media/src/node.ts
Normal 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");
|
||||
}
|
||||
4
packages/media/src/not-implemented.ts
Normal file
4
packages/media/src/not-implemented.ts
Normal 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}`);
|
||||
}
|
||||
38
packages/media/src/profiles.ts
Normal file
38
packages/media/src/profiles.ts
Normal 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");
|
||||
}
|
||||
98
packages/media/src/teaser.ts
Normal file
98
packages/media/src/teaser.ts
Normal 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");
|
||||
}
|
||||
8
packages/media/tsconfig.json
Normal file
8
packages/media/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
202
pnpm-lock.yaml
generated
202
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user