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; /** 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; export const MediaAssetKindSchema = z.enum(["video", "photo", "live_photo"]); export type MediaAssetKind = z.infer; /** 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; export const MediaIngestStateSchema = z.enum([ "indexed", "awaiting_upload", "proxying", "proxied", "analyzing", "analyzed", "failed", ]); export type MediaIngestState = z.infer; export const MediaSegmentKindSchema = z.enum(["shot", "scene"]); export type MediaSegmentKind = z.infer; export const MediaAnalysisKindSchema = z.enum([ "scenes", "transcript", "faces", "embeddings", "classifier", "audio", "saliency", ]); export type MediaAnalysisKind = z.infer; export const MediaCollectionKindSchema = z.enum(["session", "theme", "manual"]); export type MediaCollectionKind = z.infer; export const MediaLabelSourceSchema = z.enum(["rule", "model", "user"]); export type MediaLabelSource = z.infer; 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; export const AgeVerificationMethodSchema = z.enum([ "platform_verified", "document_on_file", "none", ]); export type AgeVerificationMethod = z.infer; export const MediaMarkKindSchema = z.enum([ "tattoo", "birthmark", "scar", "jewelry", "background_detail", "license_plate", "custom", ]); export type MediaMarkKind = z.infer; export const MarkTreatmentSchema = z.enum(["inpaint", "blur", "cover", "reframe_out", "none"]); export type MarkTreatment = z.infer; export const MediaProjectGoalSchema = z.enum([ "teaser", "full_release", "trailer", "highlight", "story", "custom", ]); export type MediaProjectGoal = z.infer; export const MediaProjectStatusSchema = z.enum([ "draft", "planning", "rendering", "review", "ready", "archived", ]); export type MediaProjectStatus = z.infer; export const MediaRenderStatusSchema = z.enum([ "queued", "rendering", "gating", "blocked", "done", "failed", "canceled", ]); export type MediaRenderStatus = z.infer; export const MediaRenderQualitySchema = z.enum(["draft", "final"]); export type MediaRenderQuality = z.infer; export const MediaPublicationStatusSchema = z.enum([ "draft", "scheduled", "bundled", "published", "failed", "canceled", ]); export type MediaPublicationStatus = z.infer; export const MediaMetricSourceSchema = z.enum(["csv_import", "manual", "connector"]); export type MediaMetricSource = z.infer; export const PlatformProfileKindSchema = z.enum([ "free_social", "paid_video", "own_site", "messenger", ]); export type PlatformProfileKind = z.infer; export const ContentCategorySchema = z.enum([ "explicit_nudity", "suggestive", "violence", "minors_risk", "self_harm", "drugs", "weapons", ]); export type ContentCategory = z.infer; export const CONTENT_CATEGORIES = ContentCategorySchema.options; export const LinkPolicySchema = z.enum(["allowed", "bio_only", "forbidden"]); export type LinkPolicy = z.infer; export const MusicPolicySchema = z.enum(["any", "licensed_only", "platform_library_only"]); export type MusicPolicy = z.infer; export const TransitionKindSchema = z.enum(["cut", "crossfade", "dip", "whip", "match_cut"]); export type TransitionKind = z.infer; export const CaptionModeSchema = z.enum(["burned", "sidecar", "both", "none"]); export type CaptionMode = z.infer; // --------------------------------------------------------------------------- // 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; export const NormalizedPointSchema = z.object({ x: Unit, y: Unit }); export type NormalizedPoint = z.infer; /** 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; // --------------------------------------------------------------------------- // Platform profiles // --------------------------------------------------------------------------- export const AspectRatioSchema = z.enum(["9:16", "4:5", "1:1", "16:9", "4:3", "21:9"]); export type AspectRatio = z.infer; 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; /** 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; // --------------------------------------------------------------------------- // 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; 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; 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; 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; // --------------------------------------------------------------------------- // 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; 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; 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; export const EdlTransitionSchema = z.object({ fromClipId: SafeId, toClipId: SafeId, kind: TransitionKindSchema, durationMs: Ms.max(2000), }); export type EdlTransition = z.infer; 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; 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; 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; 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; 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; export const VoiceDisguiseSchema = z.object({ method: z.enum(["pitch", "formant"]), semitones: z.number().min(-12).max(12), }); export type VoiceDisguise = z.infer; export const AiAlterationSchema = z.enum([ "synthetic_identity", "inpaint", "voice_disguise", "generated_voiceover", ]); export type AiAlteration = z.infer; 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; /** 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; // --------------------------------------------------------------------------- // 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; export const RenderOutputKindSchema = z.enum(["video", "poster", "subtitles_srt", "subtitles_vtt"]); export type RenderOutputKind = z.infer; 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; 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; // --------------------------------------------------------------------------- // Gate // --------------------------------------------------------------------------- export const GateCheckSchema = z.enum([ "technical", "content", "anonymization", "consent", "text", "disclosure", "brand", ]); export type GateCheck = z.infer; 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; 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; 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; export const TeaserWeightsSchema = z.record(TeaserFeatureSchema, z.number()); export type TeaserWeights = z.infer; /** 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; 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; 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; 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; 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; export const MediaFaceTrackSchema = z.object({ id: Id, assetId: Id, personId: Id.nullable(), startMs: Ms, endMs: Ms, confidenceMin: z.number(), }); export type MediaFaceTrack = z.infer; export const MediaMarkPromptSchema = z.object({ tMs: Ms, box: NormalizedBoxSchema.optional(), point: NormalizedPointSchema.optional(), }); export type MediaMarkPrompt = z.infer; 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; export const BrandKitRecordSchema = z.object({ id: Id, kit: BrandKitSchema, createdAt: IsoDate, updatedAt: IsoDate, }); export type BrandKitRecord = z.infer; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; export const IntroVariantSchema = z.object({ id: Id, template: MotionTemplateRefSchema, rationale: z.string(), previewArtifactId: Id.nullable(), }); export type IntroVariant = z.infer; 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; export const MediaInferenceOperationSchema = z.enum([ "detectScenes", "transcribe", "detectFaces", "trackFaces", "faceLandmarks", "embedFrames", "classifyContent", "segmentPrompted", "inpaintVideo", "redact", "synthesizeIdentity", "audioFeatures", "saliency", ]); export type MediaInferenceOperation = z.infer; 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; /** 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; /** 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; // --------------------------------------------------------------------------- // 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; export const ConsentInputSchema = z.object({ personId: Id, consentArtifactId: Id.nullable(), consentRecordedAt: IsoDate, ageVerificationMethod: AgeVerificationMethodSchema, ageVerifiedAt: IsoDate.nullable(), }); export type ConsentInput = z.infer; 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; 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 = (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;