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

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

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>;