import 'reflect-metadata' import { z } from 'zod' export const VideoResourceKindSchema = z.enum(['scene', 'transition', 'theme']) export type VideoResourceKind = z.infer export const BaseVideoResourceMetadataSchema = z.object({ resourceKind: VideoResourceKindSchema, name: z.string().min(1), description: z.string().min(1), sourceFile: z.string().min(1), pluginKey: z.string().min(1), tags: z.array(z.string().min(1)).default([]), }) export const SceneSlotHintSchema = z.object({ name: z.string().min(1), description: z.string().min(1).optional(), required: z.boolean().optional(), accepts: z.enum(['text', 'image', 'video', 'component', 'any']).optional(), }) export const SceneSlotsSchema = z.union([ z.array(z.string().min(1)), z.array(SceneSlotHintSchema), ]) const SceneFamilySchema = z.enum(['slide', 'canvas-2d', 'game-2d', 'three-3d', 'custom']) export const SceneMotionSchema = z.enum(['static', 'animated']) export const ScenePluginMetadataSchema = BaseVideoResourceMetadataSchema.extend({ resourceKind: z.literal('scene'), aspectRatio: z.enum(['9:16', '16:9']), sceneType: z.enum(['intro', 'scene', 'outro']).optional(), motion: SceneMotionSchema, sceneFamily: SceneFamilySchema.default('custom'), themePreset: z.string().min(1).optional(), slots: SceneSlotsSchema.optional(), propsTypeName: z.string().min(1).optional(), rootLayout: z.literal('absolute-fill').default('absolute-fill'), }) export const TransitionPluginMetadataSchema = BaseVideoResourceMetadataSchema.extend({ resourceKind: z.literal('transition'), transitionKind: z.enum(['transition', 'overlay']), }) export const ThemePluginMetadataSchema = BaseVideoResourceMetadataSchema.extend({ resourceKind: z.literal('theme'), supportedSceneFamilies: z.array(SceneFamilySchema).default([]), cssVariables: z.array(z.string().regex(/^--scene-/)).default([]), }) export const VideoResourceMetadataSchema = z.discriminatedUnion('resourceKind', [ ScenePluginMetadataSchema, TransitionPluginMetadataSchema, ThemePluginMetadataSchema, ]) export type ScenePluginMetadata = z.infer export type ScenePluginMetadataInput = z.input export type SceneSlotHint = z.infer export type TransitionPluginMetadata = z.infer export type ThemePluginMetadata = z.infer export type VideoResourceMetadata = z.infer export const VIDEO_RESOURCE_METADATA_KEY = Symbol.for( '@vibecuting/component-project-helper/video-resource-metadata', ) export type VideoResourceDescriptor = { kind: TMetadata['resourceKind'] annotationName: string metadataFactoryName: string schema: z.ZodType } export type VideoResourceAnnotation = { defineMetadata(input: TMetadata): TMetadata annotate(metadata: TMetadata): (target: T) => T getMetadata(target: unknown): TMetadata | undefined } export function createVideoResourceAnnotation( descriptor: VideoResourceDescriptor, ): VideoResourceAnnotation { return { defineMetadata(input) { return descriptor.schema.parse(input) }, annotate(metadata) { const normalized = descriptor.schema.parse(metadata) return function annotateTarget(target: T): T { Reflect.defineMetadata(VIDEO_RESOURCE_METADATA_KEY, normalized, target) return target } }, getMetadata(target) { if (!target || (typeof target !== 'object' && typeof target !== 'function')) { return undefined } const metadata = Reflect.getMetadata(VIDEO_RESOURCE_METADATA_KEY, target) const parsed = descriptor.schema.safeParse(metadata) return parsed.success ? parsed.data : undefined }, } } export function getVideoResourceMetadata( target: unknown, expectedKind?: TMetadata['resourceKind'], ): TMetadata | undefined { if (!target || (typeof target !== 'object' && typeof target !== 'function')) { return undefined } const metadata = Reflect.getMetadata(VIDEO_RESOURCE_METADATA_KEY, target) const parsed = VideoResourceMetadataSchema.safeParse(metadata) if (!parsed.success) { return undefined } if (expectedKind && parsed.data.resourceKind !== expectedKind) { return undefined } return parsed.data as TMetadata } export function defineScenePluginMetadata(input: ScenePluginMetadataInput): ScenePluginMetadata { return ScenePluginMetadataSchema.parse(input) } export function defineTransitionPluginMetadata( input: TransitionPluginMetadata, ): TransitionPluginMetadata { return TransitionPluginMetadataSchema.parse(input) } export function defineThemePluginMetadata(input: ThemePluginMetadata): ThemePluginMetadata { return ThemePluginMetadataSchema.parse(input) }