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
1519 lines
49 KiB
TypeScript
1519 lines
49 KiB
TypeScript
import * as z from "zod";
|
|
import { Id, IsoDate } from "./ids.js";
|
|
|
|
/*
|
|
* Media Studio contracts: the single source of truth for media DTOs, the edit
|
|
* decision list (EDL), platform profiles, brand kits, gate reports, render
|
|
* plans, and the media_* agent tool inputs.
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Enums
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const MediaSourceKindSchema = z.enum(["apple_photos", "ios_library", "folder", "emulator"]);
|
|
export type MediaSourceKind = z.infer<typeof MediaSourceKindSchema>;
|
|
|
|
/** Sources whose files live on a client device and are pushed to the server. */
|
|
export const CLIENT_PUSHED_MEDIA_SOURCES = ["apple_photos", "ios_library"] as const;
|
|
|
|
export const MediaSourceStatusSchema = z.enum(["idle", "syncing", "error", "unavailable"]);
|
|
export type MediaSourceStatus = z.infer<typeof MediaSourceStatusSchema>;
|
|
|
|
export const MediaAssetKindSchema = z.enum(["video", "photo", "live_photo"]);
|
|
export type MediaAssetKind = z.infer<typeof MediaAssetKindSchema>;
|
|
|
|
/** Where the original bytes are: referenced in place, uploaded to DATA_DIR, or still on a device. */
|
|
export const MediaAssetStorageSchema = z.enum(["referenced", "uploaded", "remote"]);
|
|
export type MediaAssetStorage = z.infer<typeof MediaAssetStorageSchema>;
|
|
|
|
export const MediaIngestStateSchema = z.enum([
|
|
"indexed",
|
|
"awaiting_upload",
|
|
"proxying",
|
|
"proxied",
|
|
"analyzing",
|
|
"analyzed",
|
|
"failed",
|
|
]);
|
|
export type MediaIngestState = z.infer<typeof MediaIngestStateSchema>;
|
|
|
|
export const MediaSegmentKindSchema = z.enum(["shot", "scene"]);
|
|
export type MediaSegmentKind = z.infer<typeof MediaSegmentKindSchema>;
|
|
|
|
export const MediaAnalysisKindSchema = z.enum([
|
|
"scenes",
|
|
"transcript",
|
|
"faces",
|
|
"embeddings",
|
|
"classifier",
|
|
"audio",
|
|
"saliency",
|
|
]);
|
|
export type MediaAnalysisKind = z.infer<typeof MediaAnalysisKindSchema>;
|
|
|
|
export const MediaCollectionKindSchema = z.enum(["session", "theme", "manual"]);
|
|
export type MediaCollectionKind = z.infer<typeof MediaCollectionKindSchema>;
|
|
|
|
export const MediaLabelSourceSchema = z.enum(["rule", "model", "user"]);
|
|
export type MediaLabelSource = z.infer<typeof MediaLabelSourceSchema>;
|
|
|
|
export const AnonymizeModeSchema = z.enum([
|
|
"none",
|
|
"blur",
|
|
"pixelate",
|
|
"solid",
|
|
"sticker",
|
|
"partial_eyes",
|
|
"partial_upper",
|
|
"partial_lower",
|
|
"reframe_out",
|
|
"synthetic_identity",
|
|
]);
|
|
export type AnonymizeMode = z.infer<typeof AnonymizeModeSchema>;
|
|
|
|
export const AgeVerificationMethodSchema = z.enum([
|
|
"platform_verified",
|
|
"document_on_file",
|
|
"none",
|
|
]);
|
|
export type AgeVerificationMethod = z.infer<typeof AgeVerificationMethodSchema>;
|
|
|
|
export const MediaMarkKindSchema = z.enum([
|
|
"tattoo",
|
|
"birthmark",
|
|
"scar",
|
|
"jewelry",
|
|
"background_detail",
|
|
"license_plate",
|
|
"custom",
|
|
]);
|
|
export type MediaMarkKind = z.infer<typeof MediaMarkKindSchema>;
|
|
|
|
export const MarkTreatmentSchema = z.enum(["inpaint", "blur", "cover", "reframe_out", "none"]);
|
|
export type MarkTreatment = z.infer<typeof MarkTreatmentSchema>;
|
|
|
|
export const MediaProjectGoalSchema = z.enum([
|
|
"teaser",
|
|
"full_release",
|
|
"trailer",
|
|
"highlight",
|
|
"story",
|
|
"custom",
|
|
]);
|
|
export type MediaProjectGoal = z.infer<typeof MediaProjectGoalSchema>;
|
|
|
|
export const MediaProjectStatusSchema = z.enum([
|
|
"draft",
|
|
"planning",
|
|
"rendering",
|
|
"review",
|
|
"ready",
|
|
"archived",
|
|
]);
|
|
export type MediaProjectStatus = z.infer<typeof MediaProjectStatusSchema>;
|
|
|
|
export const MediaRenderStatusSchema = z.enum([
|
|
"queued",
|
|
"rendering",
|
|
"gating",
|
|
"blocked",
|
|
"done",
|
|
"failed",
|
|
"canceled",
|
|
]);
|
|
export type MediaRenderStatus = z.infer<typeof MediaRenderStatusSchema>;
|
|
|
|
export const MediaRenderQualitySchema = z.enum(["draft", "final"]);
|
|
export type MediaRenderQuality = z.infer<typeof MediaRenderQualitySchema>;
|
|
|
|
export const MediaPublicationStatusSchema = z.enum([
|
|
"draft",
|
|
"scheduled",
|
|
"bundled",
|
|
"published",
|
|
"failed",
|
|
"canceled",
|
|
]);
|
|
export type MediaPublicationStatus = z.infer<typeof MediaPublicationStatusSchema>;
|
|
|
|
export const MediaMetricSourceSchema = z.enum(["csv_import", "manual", "connector"]);
|
|
export type MediaMetricSource = z.infer<typeof MediaMetricSourceSchema>;
|
|
|
|
export const PlatformProfileKindSchema = z.enum([
|
|
"free_social",
|
|
"paid_video",
|
|
"own_site",
|
|
"messenger",
|
|
]);
|
|
export type PlatformProfileKind = z.infer<typeof PlatformProfileKindSchema>;
|
|
|
|
export const ContentCategorySchema = z.enum([
|
|
"explicit_nudity",
|
|
"suggestive",
|
|
"violence",
|
|
"minors_risk",
|
|
"self_harm",
|
|
"drugs",
|
|
"weapons",
|
|
]);
|
|
export type ContentCategory = z.infer<typeof ContentCategorySchema>;
|
|
export const CONTENT_CATEGORIES = ContentCategorySchema.options;
|
|
|
|
export const LinkPolicySchema = z.enum(["allowed", "bio_only", "forbidden"]);
|
|
export type LinkPolicy = z.infer<typeof LinkPolicySchema>;
|
|
|
|
export const MusicPolicySchema = z.enum(["any", "licensed_only", "platform_library_only"]);
|
|
export type MusicPolicy = z.infer<typeof MusicPolicySchema>;
|
|
|
|
export const TransitionKindSchema = z.enum(["cut", "crossfade", "dip", "whip", "match_cut"]);
|
|
export type TransitionKind = z.infer<typeof TransitionKindSchema>;
|
|
|
|
export const CaptionModeSchema = z.enum(["burned", "sidecar", "both", "none"]);
|
|
export type CaptionMode = z.infer<typeof CaptionModeSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Primitives
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Milliseconds on a media timeline. */
|
|
export const Ms = z.number().int().min(0);
|
|
/** A normalized coordinate in [0, 1] relative to frame width or height. */
|
|
export const Unit = z.number().min(0).max(1);
|
|
/** Identifiers that may reach a filtergraph label or file name. */
|
|
export const SafeId = z
|
|
.string()
|
|
.min(1)
|
|
.max(64)
|
|
.regex(/^[a-z0-9_-]+$/);
|
|
|
|
export const NormalizedBoxSchema = z.object({ x: Unit, y: Unit, w: Unit, h: Unit });
|
|
export type NormalizedBox = z.infer<typeof NormalizedBoxSchema>;
|
|
|
|
export const NormalizedPointSchema = z.object({ x: Unit, y: Unit });
|
|
export type NormalizedPoint = z.infer<typeof NormalizedPointSchema>;
|
|
|
|
/** Hex color for brand kits and render output. Never a UI token. */
|
|
export const BrandColor = z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
|
|
|
export const BoxKeyframeSchema = z.object({ tMs: Ms, box: NormalizedBoxSchema });
|
|
export type BoxKeyframe = z.infer<typeof BoxKeyframeSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Platform profiles
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const AspectRatioSchema = z.enum(["9:16", "4:5", "1:1", "16:9", "4:3", "21:9"]);
|
|
export type AspectRatio = z.infer<typeof AspectRatioSchema>;
|
|
|
|
export const PlatformProfileSchema = z
|
|
.object({
|
|
id: SafeId,
|
|
name: z.string().min(1).max(80),
|
|
kind: PlatformProfileKindSchema,
|
|
sourceUrls: z.array(z.string().url()).default([]),
|
|
reviewedAt: z.string().date(),
|
|
/** True when at least one value could not be verified against official documentation. */
|
|
unverified: z.boolean().default(false),
|
|
video: z.object({
|
|
aspectRatios: z.array(AspectRatioSchema).min(1),
|
|
width: z.number().int().min(16).max(7680),
|
|
height: z.number().int().min(16).max(7680),
|
|
fps: z.number().min(1).max(120),
|
|
codec: z.enum(["h264", "hevc"]),
|
|
maxBitrateKbps: z.number().int().positive(),
|
|
maxFileSizeMb: z.number().positive(),
|
|
}),
|
|
duration: z.object({
|
|
hardMinMs: Ms,
|
|
hardMaxMs: Ms,
|
|
recommended: z.array(Ms).default([]),
|
|
hookWindowMs: Ms.default(2000),
|
|
}),
|
|
safeZones: z.object({
|
|
topPct: z.number().min(0).max(50),
|
|
bottomPct: z.number().min(0).max(50),
|
|
leftPct: z.number().min(0).max(50),
|
|
rightPct: z.number().min(0).max(50),
|
|
}),
|
|
captions: z.object({
|
|
recommended: z.boolean(),
|
|
required: z.boolean().default(false),
|
|
maxLines: z.number().int().min(1).max(4).default(2),
|
|
}),
|
|
audio: z.object({
|
|
targetLufs: z.number().min(-30).max(-5).default(-14),
|
|
truePeakDb: z.number().min(-9).max(0).default(-1),
|
|
music: MusicPolicySchema.default("licensed_only"),
|
|
}),
|
|
text: z.object({
|
|
titleMaxChars: z.number().int().positive().optional(),
|
|
captionMaxChars: z.number().int().positive(),
|
|
hashtagLimit: z.number().int().min(0),
|
|
linkPolicy: LinkPolicySchema,
|
|
bannedPatterns: z
|
|
.array(z.object({ pattern: z.string().min(1).max(200), reason: z.string().min(1) }))
|
|
.default([]),
|
|
}),
|
|
content: z.object({
|
|
/** Maximum allowed classifier score per category, 0..1. Missing categories are unrestricted. */
|
|
thresholds: z.partialRecord(ContentCategorySchema, z.number().min(0).max(1)),
|
|
sampleFps: z.number().min(0.5).max(10).default(2),
|
|
blockOn: z.enum(["any_frame", "ratio"]).default("any_frame"),
|
|
/** With blockOn "ratio": fraction of sampled frames above threshold that blocks. */
|
|
blockRatio: z.number().min(0).max(1).optional(),
|
|
}),
|
|
disclosure: z.object({
|
|
aiAlteredLabelRequired: z.boolean(),
|
|
paidPartnershipLabel: z.boolean().default(false),
|
|
ageRestrictedFlag: z.boolean().default(false),
|
|
}),
|
|
consent: z.object({
|
|
requirePerformerConsent: z.boolean(),
|
|
requireAgeVerification: z.boolean(),
|
|
}),
|
|
export: z.object({
|
|
poster: z.boolean().default(true),
|
|
subtitles: z.boolean().default(true),
|
|
captionText: z.boolean().default(true),
|
|
uploadChecklist: z.boolean().default(true),
|
|
previewClipMs: Ms.optional(),
|
|
}),
|
|
})
|
|
.superRefine((profile, ctx) => {
|
|
if (profile.duration.hardMinMs > profile.duration.hardMaxMs) {
|
|
ctx.addIssue({ code: "custom", path: ["duration"], message: "hardMinMs > hardMaxMs" });
|
|
}
|
|
});
|
|
export type PlatformProfile = z.infer<typeof PlatformProfileSchema>;
|
|
|
|
/** Profile summary as the API exposes it. */
|
|
export const PlatformProfileSummarySchema = z.object({
|
|
id: SafeId,
|
|
name: z.string(),
|
|
kind: PlatformProfileKindSchema,
|
|
origin: z.enum(["builtin", "private"]),
|
|
unverified: z.boolean(),
|
|
stale: z.boolean(),
|
|
aspectRatios: z.array(AspectRatioSchema),
|
|
hardMinMs: Ms,
|
|
hardMaxMs: Ms,
|
|
recommendedMs: z.array(Ms),
|
|
});
|
|
export type PlatformProfileSummary = z.infer<typeof PlatformProfileSummarySchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Brand kits
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const WatermarkSchema = z.object({
|
|
artifactId: Id.optional(),
|
|
position: z.enum(["top_left", "top_right", "bottom_left", "bottom_right", "center"]),
|
|
opacity: z.number().min(0).max(1),
|
|
/** Width relative to frame width. */
|
|
scale: z.number().min(0.02).max(0.5),
|
|
});
|
|
export type Watermark = z.infer<typeof WatermarkSchema>;
|
|
|
|
export const CaptionStylePresetSchema = z.object({
|
|
id: SafeId,
|
|
name: z.string().min(1).max(40),
|
|
template: z.enum(["clean", "karaoke", "bold_social"]),
|
|
fontFamily: z.string().min(1).max(80),
|
|
sizePct: z.number().min(2).max(12),
|
|
textColor: BrandColor,
|
|
highlightColor: BrandColor.optional(),
|
|
backgroundColor: BrandColor.optional(),
|
|
uppercase: z.boolean().default(false),
|
|
});
|
|
export type CaptionStylePreset = z.infer<typeof CaptionStylePresetSchema>;
|
|
|
|
export const MotionTemplateRefSchema = z.object({
|
|
templateId: SafeId,
|
|
params: z.record(z.string(), z.union([z.string().max(200), z.number(), z.boolean()])).default({}),
|
|
durationMs: Ms,
|
|
});
|
|
export type MotionTemplateRef = z.infer<typeof MotionTemplateRefSchema>;
|
|
|
|
export const BrandKitSchema = z.object({
|
|
name: z.string().min(1).max(60),
|
|
colors: z.object({
|
|
primary: BrandColor,
|
|
secondary: BrandColor,
|
|
accent: BrandColor,
|
|
text: BrandColor,
|
|
background: BrandColor,
|
|
}),
|
|
fonts: z
|
|
.array(
|
|
z.object({
|
|
family: z.string().min(1).max(80),
|
|
artifactId: Id,
|
|
role: z.enum(["heading", "body", "caption"]),
|
|
license: z.string().max(200).optional(),
|
|
}),
|
|
)
|
|
.default([]),
|
|
logoArtifactIds: z.array(Id).max(4).default([]),
|
|
lutArtifactId: Id.optional(),
|
|
watermark: WatermarkSchema.optional(),
|
|
captionPresets: z.array(CaptionStylePresetSchema).default([]),
|
|
musicBeds: z
|
|
.array(
|
|
z.object({
|
|
artifactId: Id,
|
|
title: z.string().min(1).max(80),
|
|
license: z.string().min(1).max(200),
|
|
commercialUse: z.boolean(),
|
|
}),
|
|
)
|
|
.default([]),
|
|
voicePreset: z.object({ provider: z.string(), voiceId: z.string() }).optional(),
|
|
intro: MotionTemplateRefSchema.optional(),
|
|
outro: MotionTemplateRefSchema.optional(),
|
|
endCard: MotionTemplateRefSchema.optional(),
|
|
tone: z.string().max(200).optional(),
|
|
audience: z.string().max(200).optional(),
|
|
});
|
|
export type BrandKit = z.infer<typeof BrandKitSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Edit decision list
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const ReframeKeyframeSchema = z.object({
|
|
tMs: Ms,
|
|
/** Crop center, normalized to source frame. */
|
|
cx: Unit,
|
|
cy: Unit,
|
|
/** 1 = tightest crop that still fills the output aspect; > 1 zooms in. */
|
|
scale: z.number().min(1).max(4),
|
|
});
|
|
export type ReframeKeyframe = z.infer<typeof ReframeKeyframeSchema>;
|
|
|
|
export const ColorAdjustSchema = z.object({
|
|
exposure: z.number().min(-2).max(2).default(0),
|
|
temperature: z.number().min(-1).max(1).default(0),
|
|
saturation: z.number().min(0).max(2).default(1),
|
|
contrast: z.number().min(0.5).max(2).default(1),
|
|
useBrandLut: z.boolean().default(false),
|
|
});
|
|
export type ColorAdjust = z.infer<typeof ColorAdjustSchema>;
|
|
|
|
export const EdlVideoClipSchema = z.object({
|
|
id: SafeId,
|
|
assetId: Id,
|
|
inMs: Ms,
|
|
outMs: Ms,
|
|
/** Position on the output timeline. */
|
|
atMs: Ms,
|
|
speed: z.number().min(0.25).max(4).default(1),
|
|
reframe: z.union([z.literal("auto"), z.literal("none"), z.array(ReframeKeyframeSchema).min(1)]),
|
|
color: ColorAdjustSchema.optional(),
|
|
stabilize: z.boolean().default(false),
|
|
muted: z.boolean().default(false),
|
|
segmentId: Id.optional(),
|
|
});
|
|
export type EdlVideoClip = z.infer<typeof EdlVideoClipSchema>;
|
|
|
|
export const EdlTransitionSchema = z.object({
|
|
fromClipId: SafeId,
|
|
toClipId: SafeId,
|
|
kind: TransitionKindSchema,
|
|
durationMs: Ms.max(2000),
|
|
});
|
|
export type EdlTransition = z.infer<typeof EdlTransitionSchema>;
|
|
|
|
export const EdlAudioClipSchema = z.object({
|
|
id: SafeId,
|
|
kind: z.enum(["music", "voiceover", "sfx"]),
|
|
artifactId: Id,
|
|
inMs: Ms,
|
|
outMs: Ms,
|
|
atMs: Ms,
|
|
gainDb: z.number().min(-40).max(12).default(0),
|
|
fadeInMs: Ms.default(0),
|
|
fadeOutMs: Ms.default(0),
|
|
/** Lower this clip under speech from video clips. */
|
|
duckUnderSpeech: z.boolean().default(false),
|
|
/** Snap cuts to this clip's beat grid. */
|
|
beatGridMs: z.array(Ms).optional(),
|
|
});
|
|
export type EdlAudioClip = z.infer<typeof EdlAudioClipSchema>;
|
|
|
|
export const EdlCaptionsSchema = z.object({
|
|
mode: CaptionModeSchema,
|
|
presetId: SafeId.optional(),
|
|
maxCharsPerLine: z.number().int().min(10).max(60).default(32),
|
|
wordHighlight: z.boolean().default(false),
|
|
language: z.string().min(2).max(10).optional(),
|
|
/** Vertical anchor; the compiler keeps the block inside the profile safe zone. */
|
|
position: z.enum(["lower", "middle", "upper"]).default("lower"),
|
|
});
|
|
export type EdlCaptions = z.infer<typeof EdlCaptionsSchema>;
|
|
|
|
export const EdlOverlaySchema = z.object({
|
|
id: SafeId,
|
|
kind: z.enum([
|
|
"text",
|
|
"lower_third",
|
|
"logo",
|
|
"watermark",
|
|
"cta_end_card",
|
|
"progress_bar",
|
|
"motion",
|
|
]),
|
|
atMs: Ms,
|
|
durationMs: Ms,
|
|
/** Plain text only. The compiler escapes it; it never reaches a shell. */
|
|
text: z.string().max(200).optional(),
|
|
box: NormalizedBoxSchema.optional(),
|
|
motion: MotionTemplateRefSchema.optional(),
|
|
});
|
|
export type EdlOverlay = z.infer<typeof EdlOverlaySchema>;
|
|
|
|
export const FaceRedactionSchema = z.object({
|
|
id: SafeId,
|
|
target: z.union([
|
|
z.object({ trackId: Id }),
|
|
z.object({ personId: Id }),
|
|
z.object({ unassignedFaces: z.literal(true) }),
|
|
]),
|
|
mode: AnonymizeModeSchema.exclude(["none"]),
|
|
paddingPct: z.number().min(0).max(100).default(20),
|
|
featherPct: z.number().min(0).max(50).default(10),
|
|
minSizePct: z.number().min(0).max(50).default(2),
|
|
});
|
|
export type FaceRedaction = z.infer<typeof FaceRedactionSchema>;
|
|
|
|
export const RegionRedactionSchema = z.object({
|
|
id: SafeId,
|
|
clipId: SafeId,
|
|
keyframes: z.array(BoxKeyframeSchema).min(1),
|
|
treatment: MarkTreatmentSchema.exclude(["none", "inpaint"]),
|
|
});
|
|
export type RegionRedaction = z.infer<typeof RegionRedactionSchema>;
|
|
|
|
export const VoiceDisguiseSchema = z.object({
|
|
method: z.enum(["pitch", "formant"]),
|
|
semitones: z.number().min(-12).max(12),
|
|
});
|
|
export type VoiceDisguise = z.infer<typeof VoiceDisguiseSchema>;
|
|
|
|
export const AiAlterationSchema = z.enum([
|
|
"synthetic_identity",
|
|
"inpaint",
|
|
"voice_disguise",
|
|
"generated_voiceover",
|
|
]);
|
|
export type AiAlteration = z.infer<typeof AiAlterationSchema>;
|
|
|
|
export const EDL_VERSION = 1;
|
|
|
|
export const EdlSchema = z.object({
|
|
edlVersion: z.literal(EDL_VERSION),
|
|
output: z.object({
|
|
profileId: SafeId,
|
|
width: z.number().int().min(16).max(7680),
|
|
height: z.number().int().min(16).max(7680),
|
|
fps: z.number().min(1).max(120),
|
|
durationTargetMs: Ms,
|
|
durationHardMinMs: Ms,
|
|
durationHardMaxMs: Ms,
|
|
}),
|
|
tracks: z.object({
|
|
video: z.array(EdlVideoClipSchema).min(1).max(400),
|
|
audio: z.array(EdlAudioClipSchema).max(40).default([]),
|
|
}),
|
|
transitions: z.array(EdlTransitionSchema).max(400).default([]),
|
|
targetLufs: z.number().min(-30).max(-5).optional(),
|
|
captions: EdlCaptionsSchema.optional(),
|
|
overlays: z.array(EdlOverlaySchema).max(100).default([]),
|
|
intro: MotionTemplateRefSchema.optional(),
|
|
outro: MotionTemplateRefSchema.optional(),
|
|
redactions: z
|
|
.object({
|
|
faces: z.array(FaceRedactionSchema).max(50).default([]),
|
|
marks: z
|
|
.array(z.object({ markId: Id, treatment: MarkTreatmentSchema.exclude(["none"]) }))
|
|
.max(100)
|
|
.default([]),
|
|
regions: z.array(RegionRedactionSchema).max(100).default([]),
|
|
voice: VoiceDisguiseSchema.optional(),
|
|
})
|
|
.default({ faces: [], marks: [], regions: [] }),
|
|
metadata: z.object({
|
|
title: z.string().max(200).optional(),
|
|
description: z.string().max(5000).optional(),
|
|
tags: z.array(z.string().max(60)).max(50).default([]),
|
|
aiAltered: z.boolean().default(false),
|
|
aiAlterations: z.array(AiAlterationSchema).default([]),
|
|
}),
|
|
});
|
|
export type Edl = z.infer<typeof EdlSchema>;
|
|
|
|
/** Machine-readable EDL validation error so an agent can correct its plan. */
|
|
export const EdlErrorSchema = z.object({
|
|
code: z.enum([
|
|
"schema",
|
|
"unknown_asset",
|
|
"unknown_clip",
|
|
"unknown_artifact",
|
|
"unknown_mark",
|
|
"unknown_person",
|
|
"unknown_track",
|
|
"unknown_template",
|
|
"range_out_of_asset",
|
|
"range_empty",
|
|
"overlap",
|
|
"duration_below_min",
|
|
"duration_above_max",
|
|
"overlay_outside_safe_zone",
|
|
"paid_only_segment",
|
|
"profile_mismatch",
|
|
"music_license",
|
|
]),
|
|
path: z.string(),
|
|
message: z.string(),
|
|
});
|
|
export type EdlError = z.infer<typeof EdlErrorSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Render plan (compiled from an EDL; executed by MediaRenderer)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const RenderStepSchema = z.discriminatedUnion("kind", [
|
|
z.object({
|
|
kind: z.literal("ffmpeg"),
|
|
id: SafeId,
|
|
/** argv after the ffmpeg binary. Never joined into a shell string. */
|
|
argv: z.array(z.string()),
|
|
produces: z.array(z.string()),
|
|
/** Output timeline length for progress reporting. */
|
|
durationMs: Ms,
|
|
}),
|
|
z.object({
|
|
kind: z.literal("motion"),
|
|
id: SafeId,
|
|
template: MotionTemplateRefSchema,
|
|
width: z.number().int(),
|
|
height: z.number().int(),
|
|
fps: z.number(),
|
|
outPath: z.string(),
|
|
}),
|
|
z.object({
|
|
kind: z.literal("inpaint"),
|
|
id: SafeId,
|
|
markId: Id,
|
|
inputPath: z.string(),
|
|
outPath: z.string(),
|
|
}),
|
|
z.object({
|
|
kind: z.literal("write_file"),
|
|
id: SafeId,
|
|
path: z.string(),
|
|
content: z.string(),
|
|
}),
|
|
z.object({
|
|
kind: z.literal("synthetic_identity"),
|
|
id: SafeId,
|
|
personId: Id,
|
|
seed: z.number().int(),
|
|
inputPath: z.string(),
|
|
outPath: z.string(),
|
|
}),
|
|
]);
|
|
export type RenderStep = z.infer<typeof RenderStepSchema>;
|
|
|
|
export const RenderOutputKindSchema = z.enum(["video", "poster", "subtitles_srt", "subtitles_vtt"]);
|
|
export type RenderOutputKind = z.infer<typeof RenderOutputKindSchema>;
|
|
|
|
export const RenderPlanSchema = z.object({
|
|
quality: MediaRenderQualitySchema,
|
|
workDir: z.string(),
|
|
steps: z.array(RenderStepSchema),
|
|
outputs: z.array(z.object({ kind: RenderOutputKindSchema, path: z.string() })),
|
|
});
|
|
export type RenderPlan = z.infer<typeof RenderPlanSchema>;
|
|
|
|
export const FfprobeReportSchema = z.object({
|
|
durationMs: Ms,
|
|
width: z.number().int(),
|
|
height: z.number().int(),
|
|
fps: z.number(),
|
|
videoCodec: z.string(),
|
|
audioCodec: z.string().nullable(),
|
|
sizeBytes: z.number().int(),
|
|
integratedLufs: z.number().nullable(),
|
|
truePeakDb: z.number().nullable(),
|
|
/** Container and stream tags exactly as found, for metadata-clean checks. */
|
|
tags: z.record(z.string(), z.string()),
|
|
});
|
|
export type FfprobeReport = z.infer<typeof FfprobeReportSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Gate
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const GateCheckSchema = z.enum([
|
|
"technical",
|
|
"content",
|
|
"anonymization",
|
|
"consent",
|
|
"text",
|
|
"disclosure",
|
|
"brand",
|
|
]);
|
|
export type GateCheck = z.infer<typeof GateCheckSchema>;
|
|
|
|
export const GateIssueSchema = z.object({
|
|
check: GateCheckSchema,
|
|
severity: z.enum(["block", "warn"]),
|
|
code: z.string().min(1).max(64),
|
|
message: z.string(),
|
|
atMs: Ms.optional(),
|
|
endMs: Ms.optional(),
|
|
category: ContentCategorySchema.optional(),
|
|
score: z.number().optional(),
|
|
personId: Id.optional(),
|
|
markId: Id.optional(),
|
|
});
|
|
export type GateIssue = z.infer<typeof GateIssueSchema>;
|
|
|
|
export const GateReportSchema = z.object({
|
|
profileId: SafeId,
|
|
passed: z.boolean(),
|
|
checkedAt: IsoDate,
|
|
sampledFrames: z.number().int().min(0),
|
|
issues: z.array(GateIssueSchema),
|
|
autoFixRound: z.number().int().min(0).max(3).default(0),
|
|
});
|
|
export type GateReport = z.infer<typeof GateReportSchema>;
|
|
|
|
export const MAX_AUTO_FIX_ROUNDS = 3;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Teaser scoring
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const TEASER_FEATURES = [
|
|
"motion",
|
|
"sharpness",
|
|
"faces",
|
|
"gaze",
|
|
"audioPeak",
|
|
"speechHook",
|
|
"novelty",
|
|
"beatProximity",
|
|
"aesthetics",
|
|
] as const;
|
|
export const TeaserFeatureSchema = z.enum(TEASER_FEATURES);
|
|
export type TeaserFeature = z.infer<typeof TeaserFeatureSchema>;
|
|
|
|
export const TeaserWeightsSchema = z.record(TeaserFeatureSchema, z.number());
|
|
export type TeaserWeights = z.infer<typeof TeaserWeightsSchema>;
|
|
|
|
/** Minimum publications with metrics before learned weights replace defaults. */
|
|
export const TEASER_LEARNING_MIN_SAMPLES = 20;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DTOs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const MediaSourceSchema = z.object({
|
|
id: Id,
|
|
kind: MediaSourceKindSchema,
|
|
label: z.string(),
|
|
status: MediaSourceStatusSchema,
|
|
assetCount: z.number().int(),
|
|
lastSyncedAt: IsoDate.nullable(),
|
|
error: z.string().nullable(),
|
|
createdAt: IsoDate,
|
|
});
|
|
export type MediaSource = z.infer<typeof MediaSourceSchema>;
|
|
|
|
export const MediaAssetSchema = z.object({
|
|
id: Id,
|
|
sourceId: Id,
|
|
kind: MediaAssetKindSchema,
|
|
storage: MediaAssetStorageSchema,
|
|
capturedAt: IsoDate.nullable(),
|
|
durationMs: Ms.nullable(),
|
|
width: z.number().int().nullable(),
|
|
height: z.number().int().nullable(),
|
|
fps: z.number().nullable(),
|
|
/** Rounded to about 1 km. */
|
|
gpsLat: z.number().nullable(),
|
|
gpsLon: z.number().nullable(),
|
|
deviceModel: z.string().nullable(),
|
|
albumNames: z.array(z.string()),
|
|
isFavorite: z.boolean(),
|
|
ingestState: MediaIngestStateSchema,
|
|
hasProxy: z.boolean(),
|
|
hasThumbnail: z.boolean(),
|
|
qualityScore: z.number().nullable(),
|
|
createdAt: IsoDate,
|
|
});
|
|
export type MediaAsset = z.infer<typeof MediaAssetSchema>;
|
|
|
|
export const MediaSegmentSchema = z.object({
|
|
id: Id,
|
|
assetId: Id,
|
|
kind: MediaSegmentKindSchema,
|
|
startMs: Ms,
|
|
endMs: Ms,
|
|
scores: z.partialRecord(TeaserFeatureSchema, z.number()),
|
|
teaserScore: z.number().nullable(),
|
|
transcriptExcerpt: z.string().nullable(),
|
|
labels: z.array(z.string()),
|
|
paidOnly: z.boolean(),
|
|
});
|
|
export type MediaSegment = z.infer<typeof MediaSegmentSchema>;
|
|
|
|
export const MediaCollectionSchema = z.object({
|
|
id: Id,
|
|
kind: MediaCollectionKindSchema,
|
|
title: z.string(),
|
|
labelSource: MediaLabelSourceSchema,
|
|
startAt: IsoDate.nullable(),
|
|
endAt: IsoDate.nullable(),
|
|
coverAssetId: Id.nullable(),
|
|
assetCount: z.number().int(),
|
|
videoCount: z.number().int(),
|
|
totalDurationMs: Ms,
|
|
});
|
|
export type MediaCollection = z.infer<typeof MediaCollectionSchema>;
|
|
|
|
export const MediaPersonSchema = z.object({
|
|
id: Id,
|
|
displayName: z.string(),
|
|
isSelf: z.boolean(),
|
|
confirmed: z.boolean(),
|
|
anonymizeByDefault: AnonymizeModeSchema,
|
|
consentRecordedAt: IsoDate.nullable(),
|
|
consentArtifactId: Id.nullable(),
|
|
ageVerifiedAt: IsoDate.nullable(),
|
|
ageVerificationMethod: AgeVerificationMethodSchema,
|
|
faceTrackCount: z.number().int(),
|
|
coverThumbAssetId: Id.nullable(),
|
|
});
|
|
export type MediaPerson = z.infer<typeof MediaPersonSchema>;
|
|
|
|
export const MediaFaceTrackSchema = z.object({
|
|
id: Id,
|
|
assetId: Id,
|
|
personId: Id.nullable(),
|
|
startMs: Ms,
|
|
endMs: Ms,
|
|
confidenceMin: z.number(),
|
|
});
|
|
export type MediaFaceTrack = z.infer<typeof MediaFaceTrackSchema>;
|
|
|
|
export const MediaMarkPromptSchema = z.object({
|
|
tMs: Ms,
|
|
box: NormalizedBoxSchema.optional(),
|
|
point: NormalizedPointSchema.optional(),
|
|
});
|
|
export type MediaMarkPrompt = z.infer<typeof MediaMarkPromptSchema>;
|
|
|
|
export const MediaMarkSchema = z.object({
|
|
id: Id,
|
|
kind: MediaMarkKindSchema,
|
|
label: z.string().nullable(),
|
|
personId: Id.nullable(),
|
|
assetId: Id.nullable(),
|
|
prompt: MediaMarkPromptSchema.nullable(),
|
|
treatment: MarkTreatmentSchema,
|
|
qualityScore: z.number().nullable(),
|
|
suggestedFallback: MarkTreatmentSchema.nullable(),
|
|
createdAt: IsoDate,
|
|
});
|
|
export type MediaMark = z.infer<typeof MediaMarkSchema>;
|
|
|
|
export const BrandKitRecordSchema = z.object({
|
|
id: Id,
|
|
kit: BrandKitSchema,
|
|
createdAt: IsoDate,
|
|
updatedAt: IsoDate,
|
|
});
|
|
export type BrandKitRecord = z.infer<typeof BrandKitRecordSchema>;
|
|
|
|
export const MediaProjectSchema = z.object({
|
|
id: Id,
|
|
title: z.string(),
|
|
goal: MediaProjectGoalSchema,
|
|
status: MediaProjectStatusSchema,
|
|
brandKitId: Id.nullable(),
|
|
collectionIds: z.array(Id),
|
|
profileIds: z.array(SafeId),
|
|
createdAt: IsoDate,
|
|
updatedAt: IsoDate,
|
|
});
|
|
export type MediaProject = z.infer<typeof MediaProjectSchema>;
|
|
|
|
export const MediaEditPlanSchema = z.object({
|
|
id: Id,
|
|
projectId: Id,
|
|
profileId: SafeId,
|
|
version: z.number().int().min(1),
|
|
edl: EdlSchema,
|
|
createdBy: z.enum(["agent", "user"]),
|
|
rationale: z.string().nullable(),
|
|
createdAt: IsoDate,
|
|
});
|
|
export type MediaEditPlan = z.infer<typeof MediaEditPlanSchema>;
|
|
|
|
export const MediaRenderOutputSchema = z.object({
|
|
kind: z.enum(["video", "poster", "subtitles_srt", "subtitles_vtt", "thumbnail", "bundle"]),
|
|
/** Small outputs are artifacts; videos are streamed from the media file route. */
|
|
artifactId: Id.nullable(),
|
|
fileName: z.string(),
|
|
sizeBytes: z.number().int(),
|
|
});
|
|
export type MediaRenderOutput = z.infer<typeof MediaRenderOutputSchema>;
|
|
|
|
export const MediaRenderSchema = z.object({
|
|
id: Id,
|
|
planId: Id,
|
|
projectId: Id,
|
|
profileId: SafeId,
|
|
quality: MediaRenderQualitySchema,
|
|
status: MediaRenderStatusSchema,
|
|
progress: z.number().min(0).max(1),
|
|
outputs: z.array(MediaRenderOutputSchema),
|
|
gateReport: GateReportSchema.nullable(),
|
|
ffprobe: FfprobeReportSchema.nullable(),
|
|
error: z.string().nullable(),
|
|
createdAt: IsoDate,
|
|
updatedAt: IsoDate,
|
|
});
|
|
export type MediaRender = z.infer<typeof MediaRenderSchema>;
|
|
|
|
export const UtmSchema = z.object({
|
|
source: z.string().min(1).max(60),
|
|
medium: z.string().min(1).max(60),
|
|
campaign: z.string().min(1).max(100),
|
|
content: z.string().max(100).optional(),
|
|
term: z.string().max(100).optional(),
|
|
});
|
|
export type Utm = z.infer<typeof UtmSchema>;
|
|
|
|
export const MediaPublicationSchema = z.object({
|
|
id: Id,
|
|
renderId: Id,
|
|
profileId: SafeId,
|
|
destination: z.string(),
|
|
status: MediaPublicationStatusSchema,
|
|
scheduledAt: IsoDate.nullable(),
|
|
publishedAt: IsoDate.nullable(),
|
|
externalUrl: z.string().nullable(),
|
|
targetUrl: z.string().nullable(),
|
|
trackingUrl: z.string().nullable(),
|
|
utm: UtmSchema.nullable(),
|
|
title: z.string().nullable(),
|
|
caption: z.string().nullable(),
|
|
tags: z.array(z.string()),
|
|
thumbnailVariant: z.string().nullable(),
|
|
bundleArtifactId: Id.nullable(),
|
|
createdAt: IsoDate,
|
|
});
|
|
export type MediaPublication = z.infer<typeof MediaPublicationSchema>;
|
|
|
|
export const MediaMetricSchema = z.object({
|
|
id: Id,
|
|
publicationId: Id,
|
|
observedAt: IsoDate,
|
|
views: z.number().int().min(0),
|
|
clicks: z.number().int().min(0),
|
|
conversions: z.number().int().min(0),
|
|
revenueCents: z.number().int().min(0),
|
|
currency: z.string().length(3),
|
|
source: MediaMetricSourceSchema,
|
|
});
|
|
export type MediaMetric = z.infer<typeof MediaMetricSchema>;
|
|
|
|
export const MetricCsvMappingSchema = z.object({
|
|
id: Id.optional(),
|
|
name: z.string().min(1).max(60),
|
|
delimiter: z.enum([",", ";", "\t"]).default(","),
|
|
columns: z.object({
|
|
publication: z.string().min(1),
|
|
observedAt: z.string().min(1),
|
|
views: z.string().optional(),
|
|
clicks: z.string().optional(),
|
|
conversions: z.string().optional(),
|
|
revenue: z.string().optional(),
|
|
currency: z.string().optional(),
|
|
}),
|
|
/** How the publication column identifies a publication. */
|
|
publicationKey: z.enum(["id", "tracking_url", "utm_content", "external_url"]),
|
|
revenueUnit: z.enum(["cents", "major"]).default("major"),
|
|
defaultCurrency: z.string().length(3).default("EUR"),
|
|
});
|
|
export type MetricCsvMapping = z.infer<typeof MetricCsvMappingSchema>;
|
|
|
|
export const FunnelRowSchema = z.object({
|
|
publicationId: Id,
|
|
projectId: Id,
|
|
profileId: SafeId,
|
|
variant: z.string().nullable(),
|
|
thumbnailVariant: z.string().nullable(),
|
|
views: z.number().int(),
|
|
clicks: z.number().int(),
|
|
conversions: z.number().int(),
|
|
revenueCents: z.number().int(),
|
|
currency: z.string(),
|
|
});
|
|
export type FunnelRow = z.infer<typeof FunnelRowSchema>;
|
|
|
|
export const MotionTemplateInfoSchema = z.object({
|
|
id: SafeId,
|
|
kind: z.enum(["intro", "outro", "lower_third", "caption", "end_card"]),
|
|
name: z.string(),
|
|
minDurationMs: Ms,
|
|
maxDurationMs: Ms,
|
|
params: z.array(
|
|
z.object({
|
|
name: z.string(),
|
|
type: z.enum(["string", "number", "boolean", "color"]),
|
|
required: z.boolean(),
|
|
}),
|
|
),
|
|
});
|
|
export type MotionTemplateInfo = z.infer<typeof MotionTemplateInfoSchema>;
|
|
|
|
export const IntroVariantSchema = z.object({
|
|
id: Id,
|
|
template: MotionTemplateRefSchema,
|
|
rationale: z.string(),
|
|
previewArtifactId: Id.nullable(),
|
|
});
|
|
export type IntroVariant = z.infer<typeof IntroVariantSchema>;
|
|
|
|
export const MarketingCopySchema = z.object({
|
|
profileId: SafeId,
|
|
language: z.string().min(2).max(10),
|
|
title: z.string().max(200),
|
|
hook: z.string().max(200),
|
|
caption: z.string().max(5000),
|
|
hashtags: z.array(z.string().max(60)).max(50),
|
|
description: z.string().max(5000).optional(),
|
|
chapters: z.array(z.object({ atMs: Ms, title: z.string().max(100) })).optional(),
|
|
altText: z.string().max(500).optional(),
|
|
issues: z.array(GateIssueSchema),
|
|
});
|
|
export type MarketingCopy = z.infer<typeof MarketingCopySchema>;
|
|
|
|
export const MediaInferenceOperationSchema = z.enum([
|
|
"detectScenes",
|
|
"transcribe",
|
|
"detectFaces",
|
|
"trackFaces",
|
|
"faceLandmarks",
|
|
"embedFrames",
|
|
"classifyContent",
|
|
"segmentPrompted",
|
|
"inpaintVideo",
|
|
"redact",
|
|
"synthesizeIdentity",
|
|
"audioFeatures",
|
|
"saliency",
|
|
]);
|
|
export type MediaInferenceOperation = z.infer<typeof MediaInferenceOperationSchema>;
|
|
|
|
export const MediaCapabilitiesSchema = z.object({
|
|
configured: z.boolean(),
|
|
operations: z.record(
|
|
MediaInferenceOperationSchema,
|
|
z.object({
|
|
available: z.boolean(),
|
|
experimental: z.boolean(),
|
|
model: z.string().nullable(),
|
|
license: z.string().nullable(),
|
|
commercialUse: z.boolean().nullable(),
|
|
}),
|
|
),
|
|
accelerator: z.enum(["cpu", "cuda", "rocm", "metal", "none"]),
|
|
hardwareEncoders: z.array(z.string()),
|
|
});
|
|
export type MediaCapabilities = z.infer<typeof MediaCapabilitiesSchema>;
|
|
|
|
/** Metadata a client-pushed source (Apple Photos, iOS library) reports per asset. */
|
|
export const ClientMediaItemSchema = z.object({
|
|
externalId: z.string().min(1).max(200),
|
|
kind: MediaAssetKindSchema,
|
|
capturedAt: IsoDate.nullable(),
|
|
durationMs: Ms.nullable(),
|
|
width: z.number().int().nullable(),
|
|
height: z.number().int().nullable(),
|
|
fps: z.number().nullable(),
|
|
gpsLat: z.number().min(-90).max(90).nullable(),
|
|
gpsLon: z.number().min(-180).max(180).nullable(),
|
|
deviceModel: z.string().max(100).nullable(),
|
|
albumNames: z.array(z.string().max(200)).max(50).default([]),
|
|
isFavorite: z.boolean().default(false),
|
|
fileName: z.string().max(255).nullable(),
|
|
});
|
|
export type ClientMediaItem = z.infer<typeof ClientMediaItemSchema>;
|
|
|
|
/** Upload chunks are sent as raw bytes to the media upload route. */
|
|
export const MEDIA_UPLOAD_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
export const MEDIA_UPLOAD_MAX_BYTES = 20 * 1024 * 1024 * 1024;
|
|
|
|
export const MediaUploadSchema = z.object({
|
|
id: Id,
|
|
assetId: Id,
|
|
sizeBytes: z.number().int().positive(),
|
|
chunkBytes: z.number().int().positive(),
|
|
receivedChunks: z.array(z.number().int().min(0)),
|
|
status: z.enum(["open", "complete", "failed"]),
|
|
});
|
|
export type MediaUpload = z.infer<typeof MediaUploadSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RPC inputs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const PageInput = z.object({
|
|
cursor: z.string().max(200).nullable().default(null),
|
|
limit: z.number().int().min(1).max(500).default(100),
|
|
});
|
|
|
|
export const MediaAssetQuerySchema = PageInput.extend({
|
|
text: z.string().max(200).optional(),
|
|
kinds: z.array(MediaAssetKindSchema).optional(),
|
|
from: IsoDate.optional(),
|
|
to: IsoDate.optional(),
|
|
near: z
|
|
.object({ lat: z.number(), lon: z.number(), radiusKm: z.number().positive().max(500) })
|
|
.optional(),
|
|
personId: Id.optional(),
|
|
collectionId: Id.optional(),
|
|
sourceId: Id.optional(),
|
|
minQuality: z.number().min(0).max(1).optional(),
|
|
favoritesOnly: z.boolean().optional(),
|
|
});
|
|
export type MediaAssetQuery = z.infer<typeof MediaAssetQuerySchema>;
|
|
|
|
export const ConsentInputSchema = z.object({
|
|
personId: Id,
|
|
consentArtifactId: Id.nullable(),
|
|
consentRecordedAt: IsoDate,
|
|
ageVerificationMethod: AgeVerificationMethodSchema,
|
|
ageVerifiedAt: IsoDate.nullable(),
|
|
});
|
|
export type ConsentInput = z.infer<typeof ConsentInputSchema>;
|
|
|
|
export const CreateMediaProjectInput = z.object({
|
|
title: z.string().trim().min(1).max(120),
|
|
goal: MediaProjectGoalSchema,
|
|
collectionIds: z.array(Id).max(50).default([]),
|
|
assetIds: z.array(Id).max(2000).default([]),
|
|
profileIds: z.array(SafeId).min(1).max(12),
|
|
brandKitId: Id.nullable().default(null),
|
|
});
|
|
export type CreateMediaProjectInputType = z.infer<typeof CreateMediaProjectInput>;
|
|
|
|
export const UpdateMediaProjectInput = z.object({
|
|
projectId: Id,
|
|
title: z.string().trim().min(1).max(120).optional(),
|
|
goal: MediaProjectGoalSchema.optional(),
|
|
collectionIds: z.array(Id).max(50).optional(),
|
|
profileIds: z.array(SafeId).min(1).max(12).optional(),
|
|
brandKitId: Id.nullable().optional(),
|
|
status: MediaProjectStatusSchema.optional(),
|
|
});
|
|
|
|
export const CreateMarkInput = z.object({
|
|
kind: MediaMarkKindSchema,
|
|
label: z.string().max(80).nullable().default(null),
|
|
personId: Id.nullable().default(null),
|
|
assetId: Id,
|
|
prompt: MediaMarkPromptSchema,
|
|
treatment: MarkTreatmentSchema,
|
|
});
|
|
|
|
const ok = z.object({ ok: z.literal(true) });
|
|
const page = <T extends z.ZodTypeAny>(item: T) =>
|
|
z.object({ items: z.array(item), nextCursor: z.string().nullable() });
|
|
|
|
/**
|
|
* Plain-object RPC definitions. rpc.ts wraps them with `oc` so this module
|
|
* stays free of the oRPC dependency and can be imported by packages/media.
|
|
*/
|
|
export const mediaRpcShapes = {
|
|
capabilities: { input: z.object({}).default({}), output: MediaCapabilitiesSchema },
|
|
sources: {
|
|
list: { input: z.object({}).default({}), output: z.array(MediaSourceSchema) },
|
|
createFolder: {
|
|
input: z.object({
|
|
label: z.string().trim().min(1).max(60),
|
|
path: z.string().min(1).max(1000),
|
|
}),
|
|
output: MediaSourceSchema,
|
|
},
|
|
registerClient: {
|
|
input: z.object({
|
|
kind: z.enum(CLIENT_PUSHED_MEDIA_SOURCES),
|
|
label: z.string().trim().min(1).max(60),
|
|
deviceId: z.string().min(1).max(200),
|
|
}),
|
|
output: MediaSourceSchema,
|
|
},
|
|
pushItems: {
|
|
input: z.object({
|
|
sourceId: Id,
|
|
items: z.array(ClientMediaItemSchema).min(1).max(500),
|
|
/** Opaque change token from the device for the next incremental push. */
|
|
syncToken: z.string().max(2000).nullable(),
|
|
complete: z.boolean(),
|
|
}),
|
|
output: z.object({
|
|
created: z.number().int(),
|
|
updated: z.number().int(),
|
|
unchanged: z.number().int(),
|
|
}),
|
|
},
|
|
sync: { input: z.object({ sourceId: Id }), output: ok },
|
|
remove: { input: z.object({ sourceId: Id }), output: ok },
|
|
},
|
|
assets: {
|
|
search: { input: MediaAssetQuerySchema, output: page(MediaAssetSchema) },
|
|
get: {
|
|
input: z.object({ assetId: Id }),
|
|
output: z.object({
|
|
asset: MediaAssetSchema,
|
|
segments: z.array(MediaSegmentSchema),
|
|
faceTracks: z.array(MediaFaceTrackSchema),
|
|
transcript: z.string().nullable(),
|
|
}),
|
|
},
|
|
setPaidOnly: {
|
|
input: z.object({ segmentIds: z.array(Id).min(1).max(500), paidOnly: z.boolean() }),
|
|
output: ok,
|
|
},
|
|
requestUpload: {
|
|
input: z.object({ assetIds: z.array(Id).min(1).max(2000) }),
|
|
output: z.object({ awaiting: z.array(Id) }),
|
|
},
|
|
},
|
|
uploads: {
|
|
begin: {
|
|
input: z.object({
|
|
assetId: Id,
|
|
sizeBytes: z.number().int().positive().max(MEDIA_UPLOAD_MAX_BYTES),
|
|
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
fileName: z.string().min(1).max(255),
|
|
}),
|
|
output: MediaUploadSchema,
|
|
},
|
|
status: { input: z.object({ uploadId: Id }), output: MediaUploadSchema },
|
|
complete: { input: z.object({ uploadId: Id }), output: MediaUploadSchema },
|
|
},
|
|
collections: {
|
|
list: {
|
|
input: PageInput.extend({ kind: MediaCollectionKindSchema.optional() }),
|
|
output: page(MediaCollectionSchema),
|
|
},
|
|
get: {
|
|
input: z.object({ collectionId: Id }),
|
|
output: z.object({ collection: MediaCollectionSchema, assets: z.array(MediaAssetSchema) }),
|
|
},
|
|
rename: {
|
|
input: z.object({ collectionId: Id, title: z.string().trim().min(1).max(120) }),
|
|
output: MediaCollectionSchema,
|
|
},
|
|
merge: {
|
|
input: z.object({ collectionIds: z.array(Id).min(2).max(20) }),
|
|
output: MediaCollectionSchema,
|
|
},
|
|
split: {
|
|
input: z.object({ collectionId: Id, atAssetId: Id }),
|
|
output: z.array(MediaCollectionSchema),
|
|
},
|
|
createManual: {
|
|
input: z.object({
|
|
title: z.string().trim().min(1).max(120),
|
|
assetIds: z.array(Id).min(1).max(2000),
|
|
}),
|
|
output: MediaCollectionSchema,
|
|
},
|
|
recluster: { input: z.object({}).default({}), output: ok },
|
|
},
|
|
people: {
|
|
list: { input: z.object({}).default({}), output: z.array(MediaPersonSchema) },
|
|
update: {
|
|
input: z.object({
|
|
personId: Id,
|
|
displayName: z.string().trim().min(1).max(60).optional(),
|
|
isSelf: z.boolean().optional(),
|
|
confirmed: z.boolean().optional(),
|
|
anonymizeByDefault: AnonymizeModeSchema.optional(),
|
|
}),
|
|
output: MediaPersonSchema,
|
|
},
|
|
merge: {
|
|
input: z.object({ personIds: z.array(Id).min(2).max(20) }),
|
|
output: MediaPersonSchema,
|
|
},
|
|
assignTrack: {
|
|
input: z.object({ trackId: Id, personId: Id.nullable() }),
|
|
output: MediaFaceTrackSchema,
|
|
},
|
|
/** User-only. Agent tools never reach this procedure. */
|
|
recordConsent: { input: ConsentInputSchema, output: MediaPersonSchema },
|
|
/** Deletes the person and every biometric record tied to them. */
|
|
remove: { input: z.object({ personId: Id }), output: ok },
|
|
},
|
|
marks: {
|
|
list: {
|
|
input: z.object({ personId: Id.optional(), assetId: Id.optional() }),
|
|
output: z.array(MediaMarkSchema),
|
|
},
|
|
create: { input: CreateMarkInput, output: MediaMarkSchema },
|
|
update: {
|
|
input: z.object({
|
|
markId: Id,
|
|
treatment: MarkTreatmentSchema.optional(),
|
|
label: z.string().max(80).nullable().optional(),
|
|
personId: Id.nullable().optional(),
|
|
}),
|
|
output: MediaMarkSchema,
|
|
},
|
|
remove: { input: z.object({ markId: Id }), output: ok },
|
|
},
|
|
brandKits: {
|
|
list: { input: z.object({}).default({}), output: z.array(BrandKitRecordSchema) },
|
|
upsert: {
|
|
input: z.object({ id: Id.optional(), kit: BrandKitSchema }),
|
|
output: BrandKitRecordSchema,
|
|
},
|
|
remove: { input: z.object({ id: Id }), output: ok },
|
|
templates: { input: z.object({}).default({}), output: z.array(MotionTemplateInfoSchema) },
|
|
introVariants: {
|
|
input: z.object({ id: Id, count: z.number().int().min(1).max(5).default(3) }),
|
|
output: z.array(IntroVariantSchema),
|
|
},
|
|
},
|
|
profiles: {
|
|
list: { input: z.object({}).default({}), output: z.array(PlatformProfileSummarySchema) },
|
|
get: { input: z.object({ profileId: SafeId }), output: PlatformProfileSchema },
|
|
},
|
|
projects: {
|
|
list: { input: PageInput, output: page(MediaProjectSchema) },
|
|
get: {
|
|
input: z.object({ projectId: Id }),
|
|
output: z.object({
|
|
project: MediaProjectSchema,
|
|
plans: z.array(MediaEditPlanSchema),
|
|
renders: z.array(MediaRenderSchema),
|
|
}),
|
|
},
|
|
create: { input: CreateMediaProjectInput, output: MediaProjectSchema },
|
|
update: { input: UpdateMediaProjectInput, output: MediaProjectSchema },
|
|
remove: { input: z.object({ projectId: Id }), output: ok },
|
|
/** Deterministic first-cut plans per profile (agent refines them through media_plan). */
|
|
autoPlan: {
|
|
input: z.object({ projectId: Id, profileIds: z.array(SafeId).max(12).optional() }),
|
|
output: z.array(MediaEditPlanSchema),
|
|
},
|
|
},
|
|
plans: {
|
|
submit: {
|
|
input: z.object({
|
|
projectId: Id,
|
|
edl: z.unknown(),
|
|
rationale: z.string().max(4000).nullable().default(null),
|
|
}),
|
|
output: z.union([
|
|
z.object({ ok: z.literal(true), plan: MediaEditPlanSchema }),
|
|
z.object({ ok: z.literal(false), errors: z.array(EdlErrorSchema) }),
|
|
]),
|
|
},
|
|
},
|
|
renders: {
|
|
start: {
|
|
input: z.object({ planId: Id, quality: MediaRenderQualitySchema }),
|
|
output: MediaRenderSchema,
|
|
},
|
|
get: { input: z.object({ renderId: Id }), output: MediaRenderSchema },
|
|
cancel: { input: z.object({ renderId: Id }), output: MediaRenderSchema },
|
|
thumbnails: {
|
|
input: z.object({ renderId: Id, count: z.number().int().min(1).max(6).default(3) }),
|
|
output: z.array(MediaRenderOutputSchema),
|
|
},
|
|
},
|
|
marketing: {
|
|
copy: {
|
|
input: z.object({ renderId: Id, language: z.string().min(2).max(10).default("en") }),
|
|
output: MarketingCopySchema,
|
|
},
|
|
},
|
|
publications: {
|
|
list: {
|
|
input: PageInput.extend({ projectId: Id.optional() }),
|
|
output: page(MediaPublicationSchema),
|
|
},
|
|
create: {
|
|
input: z.object({
|
|
renderId: Id,
|
|
destination: z.string().min(1).max(60).default("bundle"),
|
|
targetUrl: z.string().url().max(2000).nullable().default(null),
|
|
utm: UtmSchema.nullable().default(null),
|
|
title: z.string().max(200).nullable().default(null),
|
|
caption: z.string().max(5000).nullable().default(null),
|
|
tags: z.array(z.string().max(60)).max(50).default([]),
|
|
thumbnailVariant: z.string().max(40).nullable().default(null),
|
|
scheduledAt: IsoDate.nullable().default(null),
|
|
}),
|
|
output: MediaPublicationSchema,
|
|
},
|
|
bundle: { input: z.object({ publicationId: Id }), output: MediaPublicationSchema },
|
|
markPublished: {
|
|
input: z.object({ publicationId: Id, externalUrl: z.string().url().max(2000).nullable() }),
|
|
output: MediaPublicationSchema,
|
|
},
|
|
cancel: { input: z.object({ publicationId: Id }), output: MediaPublicationSchema },
|
|
},
|
|
metrics: {
|
|
mappings: { input: z.object({}).default({}), output: z.array(MetricCsvMappingSchema) },
|
|
saveMapping: { input: MetricCsvMappingSchema, output: MetricCsvMappingSchema },
|
|
importCsv: {
|
|
input: z.object({ mappingId: Id, csv: z.string().min(1).max(5_000_000) }),
|
|
output: z.object({
|
|
imported: z.number().int(),
|
|
skipped: z.array(z.object({ row: z.number().int(), reason: z.string() })),
|
|
}),
|
|
},
|
|
record: {
|
|
input: MediaMetricSchema.omit({ id: true, source: true }),
|
|
output: MediaMetricSchema,
|
|
},
|
|
funnel: {
|
|
input: z.object({ projectId: Id.optional() }),
|
|
output: z.array(FunnelRowSchema),
|
|
},
|
|
},
|
|
} as const;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Agent tools
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const MEDIA_TOOL_NAMES = [
|
|
"media_search",
|
|
"media_collections",
|
|
"media_project",
|
|
"media_plan",
|
|
"media_render",
|
|
"media_gate_report",
|
|
"media_people",
|
|
"media_marks",
|
|
"media_brand_kit",
|
|
"media_copy",
|
|
"media_thumbnails",
|
|
"media_publish",
|
|
"media_metrics",
|
|
] as const;
|
|
export type MediaToolName = (typeof MEDIA_TOOL_NAMES)[number];
|
|
|
|
const help = { help: z.boolean().optional() };
|
|
|
|
export const MediaToolInputSchemas = {
|
|
media_search: MediaAssetQuerySchema.extend({
|
|
...help,
|
|
themeText: z.string().max(200).optional(),
|
|
}),
|
|
media_collections: z.object({
|
|
...help,
|
|
action: z.enum(["list", "get", "rename", "merge", "split"]),
|
|
kind: MediaCollectionKindSchema.optional(),
|
|
collectionId: Id.optional(),
|
|
collectionIds: z.array(Id).max(20).optional(),
|
|
atAssetId: Id.optional(),
|
|
title: z.string().max(120).optional(),
|
|
}),
|
|
media_project: z.object({
|
|
...help,
|
|
action: z.enum(["create", "update", "get", "list", "auto_plan"]),
|
|
projectId: Id.optional(),
|
|
title: z.string().max(120).optional(),
|
|
goal: MediaProjectGoalSchema.optional(),
|
|
collectionIds: z.array(Id).max(50).optional(),
|
|
assetIds: z.array(Id).max(2000).optional(),
|
|
profileIds: z.array(SafeId).max(12).optional(),
|
|
brandKitId: Id.nullable().optional(),
|
|
}),
|
|
media_plan: z.object({
|
|
...help,
|
|
projectId: Id.optional(),
|
|
edl: z.unknown().optional(),
|
|
rationale: z.string().max(4000).optional(),
|
|
}),
|
|
media_render: z.object({
|
|
...help,
|
|
action: z.enum(["start", "status", "cancel"]),
|
|
planId: Id.optional(),
|
|
renderId: Id.optional(),
|
|
quality: MediaRenderQualitySchema.optional(),
|
|
}),
|
|
media_gate_report: z.object({ ...help, renderId: Id.optional() }),
|
|
media_people: z.object({
|
|
...help,
|
|
action: z.enum(["list", "update", "merge"]),
|
|
personId: Id.optional(),
|
|
personIds: z.array(Id).max(20).optional(),
|
|
displayName: z.string().max(60).optional(),
|
|
anonymizeByDefault: AnonymizeModeSchema.optional(),
|
|
}),
|
|
media_marks: z.object({
|
|
...help,
|
|
action: z.enum(["list", "create", "update", "remove"]),
|
|
markId: Id.optional(),
|
|
kind: MediaMarkKindSchema.optional(),
|
|
personId: Id.nullable().optional(),
|
|
assetId: Id.optional(),
|
|
prompt: MediaMarkPromptSchema.optional(),
|
|
treatment: MarkTreatmentSchema.optional(),
|
|
label: z.string().max(80).optional(),
|
|
}),
|
|
media_brand_kit: z.object({
|
|
...help,
|
|
action: z.enum(["list", "upsert", "intro_variants", "templates"]),
|
|
id: Id.optional(),
|
|
kit: BrandKitSchema.optional(),
|
|
count: z.number().int().min(1).max(5).optional(),
|
|
}),
|
|
media_copy: z.object({
|
|
...help,
|
|
renderId: Id.optional(),
|
|
language: z.string().max(10).optional(),
|
|
}),
|
|
media_thumbnails: z.object({
|
|
...help,
|
|
renderId: Id.optional(),
|
|
count: z.number().int().min(1).max(6).optional(),
|
|
}),
|
|
media_publish: z.object({
|
|
...help,
|
|
action: z.enum(["create", "bundle", "list", "cancel"]),
|
|
renderId: Id.optional(),
|
|
publicationId: Id.optional(),
|
|
projectId: Id.optional(),
|
|
targetUrl: z.string().url().max(2000).optional(),
|
|
utm: UtmSchema.optional(),
|
|
title: z.string().max(200).optional(),
|
|
caption: z.string().max(5000).optional(),
|
|
tags: z.array(z.string().max(60)).max(50).optional(),
|
|
thumbnailVariant: z.string().max(40).optional(),
|
|
scheduledAt: IsoDate.optional(),
|
|
destination: z.string().max(60).optional(),
|
|
}),
|
|
media_metrics: z.object({
|
|
...help,
|
|
action: z.enum(["funnel", "record"]),
|
|
projectId: Id.optional(),
|
|
metric: MediaMetricSchema.omit({ id: true, source: true }).optional(),
|
|
}),
|
|
} satisfies Record<MediaToolName, z.ZodTypeAny>;
|