@pixerate/flair 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  standaloneHtml,
10
10
  standaloneSceneHtml,
11
11
  validateScene
12
- } from "./chunk-ZULFHADW.js";
12
+ } from "./chunk-OLTY4APV.js";
13
13
  import {
14
14
  calculateShotTimings,
15
15
  createLottieEase,
@@ -870,11 +870,164 @@ function sampleMotionPathAtTime(path, time, duration, ease) {
870
870
  return sampleMotionPath(path, distanceFraction);
871
871
  }
872
872
 
873
+ // src/agent/batch.ts
874
+ function getByPath(obj, path) {
875
+ if (!obj || typeof obj !== "object") return void 0;
876
+ const parts = path.replace(/\[(\w+)\]/g, ".$1").split(".");
877
+ let curr = obj;
878
+ for (const part of parts) {
879
+ if (curr === void 0 || curr === null) return void 0;
880
+ curr = curr[part];
881
+ }
882
+ return curr;
883
+ }
884
+ var REFERENCE = /\$(\d+)\.([A-Za-z_][\w.[\]]*)/g;
885
+ function resolve(stepResults, stepStr, path, text) {
886
+ const stepIdx = parseInt(stepStr, 10);
887
+ const prevResult = stepResults.get(stepIdx);
888
+ if (!prevResult) {
889
+ throw new Error(`"${text}" refers to step ${stepIdx}, which has not run before this step.`);
890
+ }
891
+ const resolved = getByPath(prevResult, path);
892
+ if (resolved === void 0) {
893
+ throw new Error(`"${text}" does not resolve: step ${stepIdx}'s result has nothing at "${path}".`);
894
+ }
895
+ return resolved;
896
+ }
897
+ function substituteVariables(value, stepResults) {
898
+ if (typeof value === "string") {
899
+ const match = /^\$(\d+)\.([A-Za-z_][\w.[\]]*)$/.exec(value);
900
+ if (match) return resolve(stepResults, match[1], match[2], value);
901
+ return value.replace(
902
+ REFERENCE,
903
+ (fullMatch, stepStr, path) => String(resolve(stepResults, stepStr, path, fullMatch))
904
+ );
905
+ }
906
+ if (Array.isArray(value)) {
907
+ return value.map((item) => substituteVariables(item, stepResults));
908
+ }
909
+ if (value !== null && typeof value === "object") {
910
+ const copy = {};
911
+ for (const [k, v] of Object.entries(value)) {
912
+ copy[k] = substituteVariables(v, stepResults);
913
+ }
914
+ return copy;
915
+ }
916
+ return value;
917
+ }
918
+ async function applySceneBatch(scene, commands) {
919
+ let currentScene = scene;
920
+ const stepResults = /* @__PURE__ */ new Map();
921
+ const executedSteps = [];
922
+ for (let i = 0; i < commands.length; i++) {
923
+ const stepNumber = i + 1;
924
+ const cmd = commands[i];
925
+ if (cmd.tool === "batch_scene") {
926
+ return {
927
+ ok: false,
928
+ scene,
929
+ // Rollback
930
+ steps: executedSteps,
931
+ error: `Step ${stepNumber} is a batch_scene; batches cannot be nested, so list its steps here instead.`,
932
+ failedStep: stepNumber
933
+ };
934
+ }
935
+ let resolvedParams;
936
+ try {
937
+ resolvedParams = substituteVariables(cmd.params || {}, stepResults);
938
+ } catch (err) {
939
+ return {
940
+ ok: false,
941
+ scene,
942
+ // Rollback
943
+ steps: executedSteps,
944
+ error: `Step ${stepNumber} (${cmd.tool}) failed: ${err.message}`,
945
+ failedStep: stepNumber
946
+ };
947
+ }
948
+ const stepResult = await applySceneTool(currentScene, cmd.tool, resolvedParams);
949
+ stepResults.set(stepNumber, stepResult);
950
+ executedSteps.push({
951
+ step: stepNumber,
952
+ tool: cmd.tool,
953
+ result: stepResult
954
+ });
955
+ if (!stepResult.ok) {
956
+ return {
957
+ ok: false,
958
+ scene,
959
+ // Rollback to initial
960
+ steps: executedSteps,
961
+ error: `Step ${stepNumber} (${cmd.tool}) failed: ${stepResult.error}`,
962
+ failedStep: stepNumber
963
+ };
964
+ }
965
+ currentScene = stepResult.scene;
966
+ }
967
+ return {
968
+ ok: true,
969
+ scene: currentScene,
970
+ steps: executedSteps
971
+ };
972
+ }
973
+
974
+ // src/agent/schema.ts
975
+ function typeOf(value) {
976
+ if (value === null) return "null";
977
+ if (Array.isArray(value)) return "array";
978
+ if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
979
+ return typeof value;
980
+ }
981
+ function described(type) {
982
+ if (type === "integer") return "a whole number";
983
+ if (type === "null") return "null";
984
+ return `${/^[aeiou]/.test(type) ? "an" : "a"} ${type}`;
985
+ }
986
+ function fits(value, type) {
987
+ const actual = typeOf(value);
988
+ return actual === type || type === "number" && actual === "integer";
989
+ }
990
+ function checkSchema(schema, value, at = "arguments") {
991
+ const problems = [];
992
+ if (schema.type !== void 0) {
993
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
994
+ if (!types.some((t) => fits(value, t))) {
995
+ const actual = typeOf(value) === "integer" ? "number" : typeOf(value);
996
+ problems.push(`${at} must be ${types.map(described).join(" or ")}, not ${described(actual)}.`);
997
+ return problems;
998
+ }
999
+ }
1000
+ if (schema.enum && !schema.enum.includes(value)) {
1001
+ problems.push(`${at} must be one of ${schema.enum.map((v) => JSON.stringify(v)).join(", ")}.`);
1002
+ }
1003
+ if (typeof value === "number" && typeof schema.minimum === "number" && value < schema.minimum) {
1004
+ problems.push(`${at} must be at least ${schema.minimum}.`);
1005
+ }
1006
+ if (Array.isArray(value) && schema.items) {
1007
+ value.forEach((item, i) => problems.push(...checkSchema(schema.items, item, `${at}[${i}]`)));
1008
+ }
1009
+ if (typeOf(value) === "object") {
1010
+ const obj = value;
1011
+ const props = schema.properties ?? {};
1012
+ for (const name of schema.required ?? []) {
1013
+ if (obj[name] === void 0) problems.push(`${at}.${name} is required.`);
1014
+ }
1015
+ for (const [name, v] of Object.entries(obj)) {
1016
+ if (props[name]) problems.push(...checkSchema(props[name], v, `${at}.${name}`));
1017
+ else if (schema.additionalProperties === false) {
1018
+ const known = Object.keys(props);
1019
+ problems.push(`${at}.${name} is not a parameter${known.length ? `; expected ${known.map((k) => `"${k}"`).join(", ")}` : ""}.`);
1020
+ }
1021
+ }
1022
+ }
1023
+ return problems;
1024
+ }
1025
+
873
1026
  // src/agent/tools.ts
874
1027
  var FLAIR_AGENT_TOOLS = [
875
1028
  {
876
1029
  name: "get_scene",
877
- description: "Returns the current scene JSON (or a concise structural outline) and any static validation issues.",
1030
+ description: 'Returns the current scene JSON, or with view "outline" a concise structural outline instead, and any static validation issues. Start here.',
878
1031
  input_schema: {
879
1032
  type: "object",
880
1033
  properties: {
@@ -889,7 +1042,7 @@ var FLAIR_AGENT_TOOLS = [
889
1042
  },
890
1043
  {
891
1044
  name: "write_scene",
892
- description: "Overwrites the entire scene with a new specification and validates it.",
1045
+ description: "Overwrites the entire scene with a new specification and validates it. Prefer update_shot, insert_shots and delete_shots for smaller changes.",
893
1046
  input_schema: {
894
1047
  type: "object",
895
1048
  properties: {
@@ -909,7 +1062,10 @@ var FLAIR_AGENT_TOOLS = [
909
1062
  type: "object",
910
1063
  properties: {
911
1064
  shot: { type: "integer", minimum: 1, description: "1-based index of the shot to update." },
912
- set: { type: "object", description: "Fields to set or replace on this shot." },
1065
+ set: {
1066
+ type: "object",
1067
+ description: "Fields to set or replace on this shot: caption, title, camera, hold (ms), run, effects, stopEffects, behaviors."
1068
+ },
913
1069
  remove: {
914
1070
  type: "array",
915
1071
  items: { type: "string" },
@@ -934,7 +1090,7 @@ var FLAIR_AGENT_TOOLS = [
934
1090
  shots: {
935
1091
  type: "array",
936
1092
  items: { type: "object" },
937
- description: "Array of Shot objects to insert."
1093
+ description: "Shot objects to insert (fields as for update_shot)."
938
1094
  }
939
1095
  },
940
1096
  required: ["after", "shots"],
@@ -966,7 +1122,7 @@ var FLAIR_AGENT_TOOLS = [
966
1122
  id: { type: "string", description: "Media identifier." },
967
1123
  media: {
968
1124
  type: ["object", "null"],
969
- description: 'SceneMedia object (e.g. { type: "shader", source: "..." }), or null to remove.'
1125
+ description: 'SceneMedia object, or null to remove. A shader is { type: "shader", source } with a GLSL ES 3.00 Shadertoy-style void mainImage(out vec4 fragColor, in vec2 fragCoord); it is only compiled when the scene is rendered on a GPU.'
970
1126
  }
971
1127
  },
972
1128
  required: ["id", "media"],
@@ -975,7 +1131,7 @@ var FLAIR_AGENT_TOOLS = [
975
1131
  },
976
1132
  {
977
1133
  name: "preview_scene",
978
- description: "Simulates the scene fast and off-screen, returning timing, layout checks, and review report.",
1134
+ description: "Simulates the scene fast and off-screen, returning its total length, layout and timing warnings, and notes. Run it after a round of edits.",
979
1135
  input_schema: {
980
1136
  type: "object",
981
1137
  properties: {},
@@ -997,208 +1153,174 @@ var FLAIR_AGENT_TOOLS = [
997
1153
  required: ["name", "value"],
998
1154
  additionalProperties: false
999
1155
  }
1156
+ },
1157
+ {
1158
+ name: "batch_scene",
1159
+ description: `Runs several of these tools in order as one all-or-nothing change: if a step fails, none of the steps apply. A string param "$N.path" is replaced by that path in step N's result (1-based; e.g. "$1.output.outline.shots[0].hold" or "$2.scene.shots[1].title"), and "$N.path" inside a longer string is filled in; a reference that does not resolve fails its step.`,
1160
+ input_schema: {
1161
+ type: "object",
1162
+ properties: {
1163
+ steps: {
1164
+ type: "array",
1165
+ items: {
1166
+ type: "object",
1167
+ properties: {
1168
+ tool: { type: "string", description: "A tool name (not batch_scene)." },
1169
+ params: { type: "object", description: "That tool's arguments." }
1170
+ },
1171
+ required: ["tool"],
1172
+ additionalProperties: false
1173
+ },
1174
+ description: "The tool calls to run, in order."
1175
+ }
1176
+ },
1177
+ required: ["steps"],
1178
+ additionalProperties: false
1179
+ }
1000
1180
  }
1001
1181
  ];
1182
+ var TOOL_NAMES = FLAIR_AGENT_TOOLS.map((t) => t.name);
1183
+ function fail(error, failedStep) {
1184
+ return { ok: false, error, ...failedStep !== void 0 && { failedStep }, output: { ok: false, error } };
1185
+ }
1186
+ function edited(scene, summary) {
1187
+ const issues = validateScene(scene);
1188
+ return { ok: true, scene, issues, output: { ok: true, summary, shotCount: scene.shots.length, issues } };
1189
+ }
1190
+ var shotsWord = (n) => `${n} shot${n === 1 ? "" : "s"}`;
1191
+ function outlineScene(scene) {
1192
+ return {
1193
+ name: scene.name,
1194
+ title: scene.title,
1195
+ description: scene.description,
1196
+ stage: scene.stage,
1197
+ target: scene.target,
1198
+ mediaIds: Object.keys(scene.media || {}),
1199
+ shots: scene.shots.map((s, i) => ({
1200
+ shot: i + 1,
1201
+ caption: s.caption,
1202
+ title: s.title,
1203
+ hold: s.hold,
1204
+ camera: s.camera,
1205
+ commandCount: Array.isArray(s.run) ? s.run.length : 0,
1206
+ effects: Array.isArray(s.effects) ? s.effects.map((e) => e.type) : void 0
1207
+ }))
1208
+ };
1209
+ }
1002
1210
  async function applySceneTool(scene, toolName, input = {}) {
1211
+ const definition = FLAIR_AGENT_TOOLS.find((t) => t.name === toolName);
1212
+ if (!definition) {
1213
+ return fail(`There is no tool "${toolName}"; the tools are ${TOOL_NAMES.join(", ")}.`);
1214
+ }
1215
+ const problems = checkSchema(definition.input_schema, input ?? {});
1216
+ if (problems.length) return fail(`Invalid arguments for ${toolName}: ${problems.join(" ")}`);
1217
+ const shotCount = scene.shots.length;
1218
+ const shotRange = `the scene has ${shotsWord(shotCount)} (1 to ${shotCount})`;
1003
1219
  switch (toolName) {
1004
1220
  case "get_scene": {
1005
1221
  const issues = validateScene(scene);
1006
1222
  if (input.view === "outline") {
1007
- const outline = {
1008
- name: scene.name,
1009
- title: scene.title,
1010
- description: scene.description,
1011
- stage: scene.stage,
1012
- target: scene.target,
1013
- mediaIds: Object.keys(scene.media || {}),
1014
- shots: scene.shots.map((s, i) => ({
1015
- shot: i + 1,
1016
- caption: s.caption,
1017
- title: s.title,
1018
- hold: s.hold,
1019
- camera: s.camera,
1020
- commandCount: s.run?.length ?? 0,
1021
- effects: s.effects?.map((e) => e.type)
1022
- }))
1023
- };
1024
- return { ok: true, scene, outline, issues };
1223
+ const outline = outlineScene(scene);
1224
+ return { ok: true, scene, outline, issues, output: { ok: true, outline, issues } };
1025
1225
  }
1026
- return { ok: true, scene, issues };
1226
+ return { ok: true, scene, issues, output: { ok: true, scene, issues } };
1027
1227
  }
1028
1228
  case "write_scene": {
1029
- if (!input.scene || typeof input.scene !== "object") {
1030
- return { ok: false, error: "Expected scene object in arguments." };
1229
+ const next = input.scene;
1230
+ if (!Array.isArray(next.shots) || next.shots.some((s) => !s || typeof s !== "object" || Array.isArray(s))) {
1231
+ return fail("The scene needs a shots array of shot objects; nothing was written.");
1031
1232
  }
1032
- const newScene = input.scene;
1033
- const issues = validateScene(newScene);
1034
- return { ok: true, scene: newScene, issues };
1233
+ if (next.shots.length === 0) return fail("The scene needs at least one shot; nothing was written.");
1234
+ return edited(next, `Wrote the scene "${next.name}" with ${shotsWord(next.shots.length)}.`);
1035
1235
  }
1036
1236
  case "update_shot": {
1037
1237
  const idx = input.shot - 1;
1038
- if (idx < 0 || idx >= scene.shots.length) {
1039
- return { ok: false, error: `Invalid shot number ${input.shot}; scene has ${scene.shots.length} shots.` };
1040
- }
1041
- const existing = scene.shots[idx];
1042
- const updated = { ...existing, ...input.set || {} };
1043
- if (Array.isArray(input.remove)) {
1044
- for (const k of input.remove) {
1045
- delete updated[k];
1046
- }
1238
+ if (idx >= shotCount) return fail(`There is no shot ${input.shot}; ${shotRange}.`);
1239
+ if (input.set === void 0 && input.remove === void 0) {
1240
+ return fail('Give fields to change in "set", field names to delete in "remove", or both.');
1047
1241
  }
1242
+ const updated = { ...scene.shots[idx], ...input.set || {} };
1243
+ for (const k of input.remove ?? []) delete updated[k];
1048
1244
  const shots = [...scene.shots];
1049
1245
  shots[idx] = updated;
1050
- const newScene = { ...scene, shots };
1051
- return { ok: true, scene: newScene, issues: validateScene(newScene) };
1246
+ const changed = [...Object.keys(input.set ?? {}), ...(input.remove ?? []).map((k) => `-${k}`)];
1247
+ return edited({ ...scene, shots }, `Updated shot ${input.shot} (${changed.join(", ")}).`);
1052
1248
  }
1053
1249
  case "insert_shots": {
1054
- const after = input.after ?? scene.shots.length;
1055
- if (after < 0 || after > scene.shots.length) {
1056
- return { ok: false, error: `Invalid after index ${after}.` };
1057
- }
1058
- if (!Array.isArray(input.shots) || input.shots.length === 0) {
1059
- return { ok: false, error: "Expected non-empty shots array to insert." };
1060
- }
1250
+ const after = input.after;
1251
+ if (after > shotCount) return fail(`Cannot insert after shot ${after}; ${shotRange}, so "after" is 0 to ${shotCount}.`);
1252
+ if (input.shots.length === 0) return fail("Give at least one shot to insert.");
1061
1253
  const shots = [...scene.shots.slice(0, after), ...input.shots, ...scene.shots.slice(after)];
1062
- const newScene = { ...scene, shots };
1063
- return { ok: true, scene: newScene, issues: validateScene(newScene) };
1254
+ const first = after + 1;
1255
+ const last = after + input.shots.length;
1256
+ return edited(
1257
+ { ...scene, shots },
1258
+ `Inserted ${shotsWord(input.shots.length)} as ${first === last ? `shot ${first}` : `shots ${first}-${last}`}; later shots moved down.`
1259
+ );
1064
1260
  }
1065
1261
  case "delete_shots": {
1066
- if (!Array.isArray(input.shots) || input.shots.length === 0) {
1067
- return { ok: false, error: "Expected non-empty shots array to delete." };
1068
- }
1069
- const toDelete = new Set(input.shots.map((n) => n - 1));
1262
+ const numbers = input.shots;
1263
+ if (numbers.length === 0) return fail("Give the shot numbers to delete.");
1264
+ const missing = numbers.filter((n) => n > shotCount);
1265
+ if (missing.length) return fail(`There is no shot ${missing.join(", ")}; ${shotRange}. Nothing was deleted.`);
1266
+ const toDelete = new Set(numbers.map((n) => n - 1));
1070
1267
  const shots = scene.shots.filter((_, i) => !toDelete.has(i));
1071
- if (shots.length === 0) {
1072
- return { ok: false, error: "Cannot delete all shots in scene." };
1073
- }
1074
- const newScene = { ...scene, shots };
1075
- return { ok: true, scene: newScene, issues: validateScene(newScene) };
1268
+ if (shots.length === 0) return fail("Cannot delete every shot; a scene needs at least one. Nothing was deleted.");
1269
+ return edited({ ...scene, shots }, `Deleted ${shotsWord(toDelete.size)}; the scene has ${shotsWord(shots.length)} now.`);
1076
1270
  }
1077
1271
  case "set_media": {
1078
- if (!input.id || typeof input.id !== "string") {
1079
- return { ok: false, error: "Expected string media id." };
1080
- }
1081
1272
  const media = { ...scene.media || {} };
1082
1273
  if (input.media === null) {
1274
+ if (!(input.id in media)) {
1275
+ const ids = Object.keys(media);
1276
+ return fail(`There is no media "${input.id}" to remove; ${ids.length ? `the media are ${ids.join(", ")}` : "the scene has no media"}.`);
1277
+ }
1083
1278
  delete media[input.id];
1084
- } else {
1085
- media[input.id] = input.media;
1279
+ return edited({ ...scene, media }, `Removed media "${input.id}".`);
1086
1280
  }
1087
- const newScene = { ...scene, media };
1088
- return { ok: true, scene: newScene, issues: validateScene(newScene) };
1281
+ if (typeof input.media.type !== "string") return fail('Media needs a string type, e.g. { type: "shader", source: "..." }.');
1282
+ const existed = input.id in media;
1283
+ media[input.id] = input.media;
1284
+ return edited({ ...scene, media }, `${existed ? "Replaced" : "Added"} ${input.media.type} media "${input.id}".`);
1089
1285
  }
1090
1286
  case "preview_scene": {
1091
1287
  const { review } = await simulateScene(scene);
1092
- return { ok: true, scene, issues: validateScene(scene), review };
1288
+ const issues = validateScene(scene);
1289
+ const { markdown: _markdown, ...summary } = review;
1290
+ return { ok: true, scene, issues, review, output: { ok: true, issues, review: summary } };
1093
1291
  }
1094
1292
  case "set_rig_widget": {
1095
- if (!scene.rig || !scene.rig.widgets || !scene.rig.widgets[input.name]) {
1096
- return { ok: false, error: `Scene does not have a rig widget named "${input.name}".` };
1293
+ const widgets = scene.rig?.widgets ?? {};
1294
+ if (!scene.rig || !widgets[input.name]) {
1295
+ const names = Object.keys(widgets);
1296
+ return fail(`Scene does not have a rig widget named "${input.name}"; ${names.length ? `its widgets are ${names.join(", ")}` : "it has no rig widgets"}.`);
1097
1297
  }
1098
1298
  try {
1099
1299
  const updatedRig = setRigWidget(scene.rig, input.name, input.value);
1100
- const newScene = applyRig(updatedRig, scene);
1101
- return { ok: true, scene: newScene, issues: validateScene(newScene) };
1300
+ return edited(applyRig(updatedRig, scene), `Set rig widget "${input.name}" to ${JSON.stringify(input.value)}.`);
1102
1301
  } catch (err) {
1103
- return { ok: false, error: err.message || String(err) };
1302
+ return fail(err.message || String(err));
1104
1303
  }
1105
1304
  }
1106
- default:
1107
- return { ok: false, error: `Unknown tool name: ${toolName}` };
1108
- }
1109
- }
1110
-
1111
- // src/agent/batch.ts
1112
- function getByPath(obj, path) {
1113
- if (!obj || typeof obj !== "object") return void 0;
1114
- const parts = path.replace(/\[(\w+)\]/g, ".$1").split(".");
1115
- let curr = obj;
1116
- for (const part of parts) {
1117
- if (curr === void 0 || curr === null) return void 0;
1118
- curr = curr[part];
1119
- }
1120
- return curr;
1121
- }
1122
- function substituteVariables(value, stepResults) {
1123
- if (typeof value === "string") {
1124
- const match = /^\$(\d+)\.(.+)$/.exec(value);
1125
- if (match) {
1126
- const stepIdx = parseInt(match[1], 10);
1127
- const path = match[2];
1128
- const prevResult = stepResults.get(stepIdx);
1129
- if (prevResult) {
1130
- const resolved = getByPath(prevResult, path);
1131
- if (resolved !== void 0) {
1132
- return resolved;
1133
- }
1134
- }
1135
- }
1136
- return value.replace(/\$(\d+)\.([a-zA-Z0-9_.[\]]+)/g, (fullMatch, stepStr, path) => {
1137
- const stepIdx = parseInt(stepStr, 10);
1138
- const prevResult = stepResults.get(stepIdx);
1139
- if (prevResult) {
1140
- const resolved = getByPath(prevResult, path);
1141
- if (resolved !== void 0) {
1142
- return String(resolved);
1143
- }
1144
- }
1145
- return fullMatch;
1146
- });
1147
- }
1148
- if (Array.isArray(value)) {
1149
- return value.map((item) => substituteVariables(item, stepResults));
1150
- }
1151
- if (value !== null && typeof value === "object") {
1152
- const copy = {};
1153
- for (const [k, v] of Object.entries(value)) {
1154
- copy[k] = substituteVariables(v, stepResults);
1155
- }
1156
- return copy;
1157
- }
1158
- return value;
1159
- }
1160
- async function applySceneBatch(scene, commands) {
1161
- let currentScene = scene;
1162
- const stepResults = /* @__PURE__ */ new Map();
1163
- const executedSteps = [];
1164
- for (let i = 0; i < commands.length; i++) {
1165
- const stepNumber = i + 1;
1166
- const cmd = commands[i];
1167
- const resolvedParams = substituteVariables(cmd.params || {}, stepResults);
1168
- if (cmd.tool === "batch_scene_tool") {
1169
- return {
1170
- ok: false,
1171
- scene,
1172
- // Rollback
1173
- steps: executedSteps,
1174
- error: `Nested batch_scene_tool is not allowed at step ${stepNumber}.`,
1175
- failedStep: stepNumber
1176
- };
1177
- }
1178
- const stepResult = await applySceneTool(currentScene, cmd.tool, resolvedParams);
1179
- stepResults.set(stepNumber, stepResult);
1180
- executedSteps.push({
1181
- step: stepNumber,
1182
- tool: cmd.tool,
1183
- result: stepResult
1184
- });
1185
- if (!stepResult.ok) {
1305
+ case "batch_scene": {
1306
+ const batch = await applySceneBatch(scene, input.steps);
1307
+ if (!batch.ok) return fail(`${batch.error} No steps were applied.`, batch.failedStep);
1308
+ const issues = validateScene(batch.scene);
1186
1309
  return {
1187
- ok: false,
1188
- scene,
1189
- // Rollback to initial
1190
- steps: executedSteps,
1191
- error: `Step ${stepNumber} (${cmd.tool}) failed: ${stepResult.error}`,
1192
- failedStep: stepNumber
1310
+ ok: true,
1311
+ scene: batch.scene,
1312
+ issues,
1313
+ output: {
1314
+ ok: true,
1315
+ summary: `Ran ${batch.steps.length} step${batch.steps.length === 1 ? "" : "s"}.`,
1316
+ steps: batch.steps.map((s) => ({ step: s.step, tool: s.tool, ...s.result.output })),
1317
+ shotCount: batch.scene.shots.length,
1318
+ issues
1319
+ }
1193
1320
  };
1194
1321
  }
1195
- currentScene = stepResult.scene;
1196
1322
  }
1197
- return {
1198
- ok: true,
1199
- scene: currentScene,
1200
- steps: executedSteps
1201
- };
1323
+ return fail(`There is no tool "${toolName}".`);
1202
1324
  }
1203
1325
  export {
1204
1326
  ALL_BLEND_MODES,
@@ -1353,6 +1475,7 @@ export {
1353
1475
  oklabToOklch,
1354
1476
  oklabToSrgb,
1355
1477
  oklchToOklab,
1478
+ outlineScene,
1356
1479
  overlapArea,
1357
1480
  pageHidden,
1358
1481
  parseColor,