schema.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { defineSchema, type PromptContext } from "@json-render/core";
  2. /**
  3. * Prompt template for Remotion timeline generation
  4. *
  5. * Uses JSONL patch format (same as React) but builds up a timeline spec structure.
  6. */
  7. function remotionPromptTemplate(context: PromptContext): string {
  8. const { catalog, options } = context;
  9. const { system = "You are a video timeline generator.", customRules = [] } =
  10. options;
  11. const lines: string[] = [];
  12. lines.push(system);
  13. lines.push("");
  14. // Output format - JSONL patches
  15. lines.push("OUTPUT FORMAT:");
  16. lines.push(
  17. "Output JSONL (one JSON object per line) with patches to build a timeline spec.",
  18. );
  19. lines.push(
  20. "Each line is a JSON patch operation. Build the timeline incrementally.",
  21. );
  22. lines.push("");
  23. lines.push("Example output (each line is a separate JSON object):");
  24. lines.push("");
  25. lines.push(`{"op":"add","path":"/composition","value":{"id":"intro","fps":30,"width":1920,"height":1080,"durationInFrames":300}}
  26. {"op":"add","path":"/tracks","value":[{"id":"main","name":"Main","type":"video","enabled":true},{"id":"overlay","name":"Overlay","type":"overlay","enabled":true}]}
  27. {"op":"add","path":"/clips","value":[]}
  28. {"op":"add","path":"/clips/-","value":{"id":"clip-1","trackId":"main","component":"TitleCard","props":{"title":"Welcome","subtitle":"Getting Started"},"from":0,"durationInFrames":90,"transitionIn":{"type":"fade","durationInFrames":15},"transitionOut":{"type":"fade","durationInFrames":15},"motion":{"enter":{"opacity":0,"y":50,"scale":0.9,"duration":25},"spring":{"damping":15}}}}
  29. {"op":"add","path":"/clips/-","value":{"id":"clip-2","trackId":"main","component":"TitleCard","props":{"title":"Features"},"from":90,"durationInFrames":90,"motion":{"enter":{"opacity":0,"x":-100,"duration":20},"exit":{"opacity":0,"x":100,"duration":15}}}}
  30. {"op":"add","path":"/audio","value":{"tracks":[]}}`);
  31. lines.push("");
  32. // Components
  33. const catalogData = catalog as {
  34. components?: Record<
  35. string,
  36. { description?: string; defaultDuration?: number }
  37. >;
  38. transitions?: Record<string, { description?: string }>;
  39. effects?: Record<string, { description?: string }>;
  40. };
  41. if (catalogData.components) {
  42. lines.push(
  43. `AVAILABLE COMPONENTS (${Object.keys(catalogData.components).length}):`,
  44. );
  45. lines.push("");
  46. for (const [name, def] of Object.entries(catalogData.components)) {
  47. const duration = def.defaultDuration
  48. ? ` [default: ${def.defaultDuration} frames]`
  49. : "";
  50. lines.push(
  51. `- ${name}: ${def.description || "No description"}${duration}`,
  52. );
  53. }
  54. lines.push("");
  55. }
  56. // Transitions
  57. if (
  58. catalogData.transitions &&
  59. Object.keys(catalogData.transitions).length > 0
  60. ) {
  61. lines.push("AVAILABLE TRANSITIONS:");
  62. lines.push("");
  63. for (const [name, def] of Object.entries(catalogData.transitions)) {
  64. lines.push(`- ${name}: ${def.description || "No description"}`);
  65. }
  66. lines.push("");
  67. }
  68. // Motion system documentation
  69. lines.push("MOTION SYSTEM:");
  70. lines.push(
  71. "Clips can have a 'motion' field for declarative animations (optional, use for dynamic/engaging videos):",
  72. );
  73. lines.push("");
  74. lines.push(
  75. "- enter: {opacity?, scale?, x?, y?, rotate?, duration?} - animate FROM these values TO normal when clip starts",
  76. );
  77. lines.push(
  78. "- exit: {opacity?, scale?, x?, y?, rotate?, duration?} - animate FROM normal TO these values when clip ends",
  79. );
  80. lines.push(
  81. "- spring: {damping?, stiffness?, mass?} - physics config (lower damping = more bounce)",
  82. );
  83. lines.push(
  84. '- loop: {property, from, to, duration, easing?} - continuous animation (property: "scale"|"rotate"|"x"|"y"|"opacity")',
  85. );
  86. lines.push("");
  87. lines.push("Example motion configs:");
  88. lines.push(' Fade up: {"enter":{"opacity":0,"y":30,"duration":20}}');
  89. lines.push(
  90. ' Scale pop: {"enter":{"scale":0.5,"opacity":0,"duration":15},"spring":{"damping":10}}',
  91. );
  92. lines.push(
  93. ' Slide in/out: {"enter":{"x":-100,"duration":20},"exit":{"x":100,"duration":15}}',
  94. );
  95. lines.push(
  96. ' Gentle pulse: {"loop":{"property":"scale","from":1,"to":1.05,"duration":60,"easing":"ease"}}',
  97. );
  98. lines.push("");
  99. // Rules
  100. lines.push("RULES:");
  101. const baseRules = [
  102. "Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
  103. 'First add /composition with {id, fps:30, width:1920, height:1080, durationInFrames}: {"op":"add","path":"/composition","value":{...}}',
  104. 'Then add /tracks array with video/overlay tracks: {"op":"add","path":"/tracks","value":[...]}',
  105. 'Then add each clip by appending to the array: {"op":"add","path":"/clips/-","value":{...}}',
  106. 'Finally add /audio with {tracks:[]}: {"op":"add","path":"/audio","value":{...}}',
  107. "ONLY use components listed above",
  108. "fps is always 30 (1 second = 30 frames, 10 seconds = 300 frames)",
  109. 'Clips on "main" track flow sequentially (from = previous clip\'s from + durationInFrames)',
  110. 'Overlay clips (LowerThird, TextOverlay) go on "overlay" track',
  111. "Use motion.enter for engaging clip entrances, motion.exit for smooth departures",
  112. "Spring damping: 20=smooth, 10=bouncy, 5=very bouncy",
  113. ];
  114. const allRules = [...baseRules, ...customRules];
  115. allRules.forEach((rule, i) => {
  116. lines.push(`${i + 1}. ${rule}`);
  117. });
  118. return lines.join("\n");
  119. }
  120. /**
  121. * The schema for @json-render/remotion
  122. *
  123. * This schema is fundamentally different from the React element tree schema.
  124. * It's timeline-based, designed for video composition:
  125. *
  126. * - Spec: A composition with tracks containing timed clips
  127. * - Catalog: Video components (scenes, overlays, etc.) and effects
  128. *
  129. * This demonstrates that json-render is truly agnostic - different renderers
  130. * can have completely different spec formats.
  131. */
  132. export const schema = defineSchema(
  133. (s) => ({
  134. // What the AI-generated SPEC looks like (timeline-based)
  135. spec: s.object({
  136. /** Composition settings */
  137. composition: s.object({
  138. /** Unique composition ID */
  139. id: s.string(),
  140. /** Frames per second */
  141. fps: s.number(),
  142. /** Width in pixels */
  143. width: s.number(),
  144. /** Height in pixels */
  145. height: s.number(),
  146. /** Total duration in frames */
  147. durationInFrames: s.number(),
  148. }),
  149. /** Timeline tracks (like layers in video editing) */
  150. tracks: s.array(
  151. s.object({
  152. /** Unique track ID */
  153. id: s.string(),
  154. /** Track name for organization */
  155. name: s.string(),
  156. /** Track type: "video" | "audio" | "overlay" | "text" */
  157. type: s.string(),
  158. /** Whether track is muted/hidden */
  159. enabled: s.boolean(),
  160. }),
  161. ),
  162. /** Clips placed on the timeline */
  163. clips: s.array(
  164. s.object({
  165. /** Unique clip ID */
  166. id: s.string(),
  167. /** Which track this clip belongs to */
  168. trackId: s.string(),
  169. /** Component type from catalog */
  170. component: s.ref("catalog.components"),
  171. /** Component props */
  172. props: s.propsOf("catalog.components"),
  173. /** Start frame (when clip begins) */
  174. from: s.number(),
  175. /** Duration in frames */
  176. durationInFrames: s.number(),
  177. /** Transition in effect */
  178. transitionIn: s.object({
  179. type: s.ref("catalog.transitions"),
  180. durationInFrames: s.number(),
  181. }),
  182. /** Transition out effect */
  183. transitionOut: s.object({
  184. type: s.ref("catalog.transitions"),
  185. durationInFrames: s.number(),
  186. }),
  187. /** Declarative motion configuration for custom animations */
  188. motion: s.object({
  189. /** Enter animation - animates FROM these values TO neutral */
  190. enter: s.object({
  191. /** Starting opacity (0-1), animates to 1 */
  192. opacity: s.number(),
  193. /** Starting scale (e.g., 0.8 = 80%), animates to 1 */
  194. scale: s.number(),
  195. /** Starting X offset in pixels, animates to 0 */
  196. x: s.number(),
  197. /** Starting Y offset in pixels, animates to 0 */
  198. y: s.number(),
  199. /** Starting rotation in degrees, animates to 0 */
  200. rotate: s.number(),
  201. /** Duration of enter animation in frames (default: 20) */
  202. duration: s.number(),
  203. }),
  204. /** Exit animation - animates FROM neutral TO these values */
  205. exit: s.object({
  206. /** Ending opacity (0-1), animates from 1 */
  207. opacity: s.number(),
  208. /** Ending scale, animates from 1 */
  209. scale: s.number(),
  210. /** Ending X offset in pixels, animates from 0 */
  211. x: s.number(),
  212. /** Ending Y offset in pixels, animates from 0 */
  213. y: s.number(),
  214. /** Ending rotation in degrees, animates from 0 */
  215. rotate: s.number(),
  216. /** Duration of exit animation in frames (default: 20) */
  217. duration: s.number(),
  218. }),
  219. /** Spring physics configuration */
  220. spring: s.object({
  221. /** Damping coefficient (default: 20) */
  222. damping: s.number(),
  223. /** Stiffness (default: 100) */
  224. stiffness: s.number(),
  225. /** Mass (default: 1) */
  226. mass: s.number(),
  227. }),
  228. /** Continuous looping animation */
  229. loop: s.object({
  230. /** Property to animate: "scale" | "rotate" | "x" | "y" | "opacity" */
  231. property: s.string(),
  232. /** Starting value */
  233. from: s.number(),
  234. /** Ending value */
  235. to: s.number(),
  236. /** Duration of one cycle in frames */
  237. duration: s.number(),
  238. /** Easing type: "linear" | "ease" | "spring" (default: "ease") */
  239. easing: s.string(),
  240. }),
  241. }),
  242. }),
  243. ),
  244. /** Audio configuration */
  245. audio: s.object({
  246. /** Background music/audio clips */
  247. tracks: s.array(
  248. s.object({
  249. id: s.string(),
  250. src: s.string(),
  251. from: s.number(),
  252. durationInFrames: s.number(),
  253. volume: s.number(),
  254. }),
  255. ),
  256. }),
  257. }),
  258. // What the CATALOG must provide
  259. catalog: s.object({
  260. /** Video component definitions (scenes, overlays, etc.) */
  261. components: s.map({
  262. /** Zod schema for component props */
  263. props: s.zod(),
  264. /** Component type: "scene" | "overlay" | "text" | "image" | "video" */
  265. type: s.string(),
  266. /** Default duration in frames (can be overridden per clip) */
  267. defaultDuration: s.number(),
  268. /** Description for AI generation hints */
  269. description: s.string(),
  270. }),
  271. /** Transition effect definitions */
  272. transitions: s.map({
  273. /** Default duration in frames */
  274. defaultDuration: s.number(),
  275. /** Description for AI generation hints */
  276. description: s.string(),
  277. }),
  278. /** Effect definitions (filters, animations, etc.) */
  279. effects: s.map({
  280. /** Zod schema for effect params */
  281. params: s.zod(),
  282. /** Description for AI generation hints */
  283. description: s.string(),
  284. }),
  285. }),
  286. }),
  287. {
  288. promptTemplate: remotionPromptTemplate,
  289. },
  290. );
  291. /**
  292. * Type for the Remotion schema
  293. */
  294. export type RemotionSchema = typeof schema;
  295. /**
  296. * Infer the spec type from a catalog
  297. */
  298. export type RemotionSpec<TCatalog> = typeof schema extends {
  299. createCatalog: (catalog: TCatalog) => { _specType: infer S };
  300. }
  301. ? S
  302. : never;