@sogni-ai/sogni-creative-agent-skill 2.2.0 → 2.3.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/README.md +40 -10
- package/SKILL.md +73 -17
- package/generated/creative-agent-runtime.mjs +4076 -148
- package/llm.txt +15 -5
- package/openclaw.plugin.json +25 -2
- package/package.json +6 -2
- package/scripts/check-creative-agent-source.mjs +104 -0
- package/sogni-agent.mjs +574 -75
- package/ssrf-guard.mjs +2 -1
- package/version.mjs +1 -1
|
@@ -1,10 +1,3052 @@
|
|
|
1
1
|
// Generated by sogni-creative-agent/scripts/sync-skill-runtime.mjs.
|
|
2
2
|
// Do not edit manually in sogni-creative-agent-skill.
|
|
3
3
|
|
|
4
|
+
export function formatWorkflowToolName(value) {
|
|
5
|
+
if (typeof value !== 'string' || !value.trim())
|
|
6
|
+
return 'tool';
|
|
7
|
+
return value
|
|
8
|
+
.trim()
|
|
9
|
+
.replace(/_/g, ' ')
|
|
10
|
+
.replace(/\b\w/g, char => char.toUpperCase());
|
|
11
|
+
}
|
|
12
|
+
function eventToolName(event) {
|
|
13
|
+
return formatWorkflowToolName(event.data?.toolName);
|
|
14
|
+
}
|
|
15
|
+
export function workflowStatusForDebugEvent(event) {
|
|
16
|
+
switch (event.event) {
|
|
17
|
+
case 'preproduction_script_stage_started':
|
|
18
|
+
return { label: 'Preparing creative brief', kind: 'info' };
|
|
19
|
+
case 'preproduction_planning_notes_started':
|
|
20
|
+
return { label: 'Planning storyboard structure', kind: 'info' };
|
|
21
|
+
case 'preproduction_script_draft_started':
|
|
22
|
+
return { label: 'Writing storyboard plan', kind: 'info' };
|
|
23
|
+
case 'preproduction_planning_contract_resolved':
|
|
24
|
+
return { label: 'Storyboard contract captured', kind: 'success' };
|
|
25
|
+
case 'llm_request_started':
|
|
26
|
+
return {
|
|
27
|
+
label: event.data?.toolsEnabled === false ? 'Creative writing pass started' : 'Planning next step',
|
|
28
|
+
kind: 'info',
|
|
29
|
+
};
|
|
30
|
+
case 'job_confirmation_waiting':
|
|
31
|
+
return { label: 'Waiting for job approval', kind: 'pending' };
|
|
32
|
+
case 'quality_audit_confirmation_waiting':
|
|
33
|
+
return { label: 'Waiting for quality approval', kind: 'pending' };
|
|
34
|
+
case 'job_confirmation_confirmed':
|
|
35
|
+
case 'quality_audit_confirmation_confirmed':
|
|
36
|
+
return { label: 'Approved', kind: 'success' };
|
|
37
|
+
case 'job_confirmation_overrides_applied':
|
|
38
|
+
return { label: 'Applied approved settings', kind: 'success' };
|
|
39
|
+
case 'job_confirmation_cancelled':
|
|
40
|
+
case 'quality_audit_confirmation_cancelled':
|
|
41
|
+
return { label: 'Approval cancelled', kind: 'warn' };
|
|
42
|
+
case 'job_confirmation_unavailable':
|
|
43
|
+
return { label: 'Approval unavailable', kind: 'error' };
|
|
44
|
+
case 'guardrail_blocked_tool':
|
|
45
|
+
return { label: 'Adjusted workflow before spending credits', kind: 'warn' };
|
|
46
|
+
case 'permission_gate_blocked':
|
|
47
|
+
return { label: 'Waiting for explicit permission', kind: 'warn' };
|
|
48
|
+
case 'contracts_dispatch_execute_with_repair':
|
|
49
|
+
return { label: 'Adjusted tool settings', kind: 'info' };
|
|
50
|
+
case 'contracts_dispatch_repair_followup':
|
|
51
|
+
return { label: 'Repairing workflow plan', kind: 'warn' };
|
|
52
|
+
case 'contracts_dispatch_ask_user':
|
|
53
|
+
return { label: 'Needs clarification', kind: 'pending' };
|
|
54
|
+
case 'contracts_dispatch_reject':
|
|
55
|
+
return { label: 'Tool call rejected by policy', kind: 'warn' };
|
|
56
|
+
case 'tool_call_received':
|
|
57
|
+
return { label: `Preparing ${eventToolName(event)}`, kind: 'info' };
|
|
58
|
+
case 'tool_progress_started':
|
|
59
|
+
return { label: `Running ${eventToolName(event)}`, kind: 'info' };
|
|
60
|
+
case 'tool_error_repairing':
|
|
61
|
+
return { label: 'Retrying with corrected settings', kind: 'warn' };
|
|
62
|
+
case 'internal_repair_no_tool_nudge':
|
|
63
|
+
return { label: 'Retrying workflow plan', kind: 'warn' };
|
|
64
|
+
case 'tool_error_repeated_stopped':
|
|
65
|
+
case 'tool_error_terminal_stopped':
|
|
66
|
+
case 'tool_error_user_message':
|
|
67
|
+
return { label: 'Tool could not continue', kind: 'error' };
|
|
68
|
+
case 'tool_wait_for_user_stopped':
|
|
69
|
+
return { label: 'Waiting for user input', kind: 'pending' };
|
|
70
|
+
case 'vision_analysis_failed':
|
|
71
|
+
return { label: 'Analysis failed', kind: 'error' };
|
|
72
|
+
default:
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (event.event.endsWith('_direct_tool_call')) {
|
|
76
|
+
return { label: `Routed to ${eventToolName(event)}`, kind: 'info' };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
export function streamingStatusForDebugEvent(event) {
|
|
81
|
+
switch (event.event) {
|
|
82
|
+
case 'llm_request_started':
|
|
83
|
+
return event.data?.toolsEnabled === false
|
|
84
|
+
? 'Running creative writing pass...'
|
|
85
|
+
: 'Planning the next step...';
|
|
86
|
+
case 'preproduction_script_stage_started':
|
|
87
|
+
return 'Preparing the creative brief...';
|
|
88
|
+
case 'preproduction_planning_notes_started':
|
|
89
|
+
return 'Planning the storyboard structure...';
|
|
90
|
+
case 'preproduction_script_draft_started':
|
|
91
|
+
return 'Writing the storyboard plan...';
|
|
92
|
+
case 'preproduction_script_continuation_started':
|
|
93
|
+
return 'Continuing the storyboard plan...';
|
|
94
|
+
case 'preproduction_script_beat_count_mismatch':
|
|
95
|
+
case 'preproduction_script_beat_count_second_repair_started':
|
|
96
|
+
return 'Correcting the storyboard beat count...';
|
|
97
|
+
case 'preproduction_script_beat_count_repair_mismatch':
|
|
98
|
+
case 'preproduction_script_beat_count_second_repair_mismatch':
|
|
99
|
+
return 'Checking the storyboard structure...';
|
|
100
|
+
case 'tool_call_received':
|
|
101
|
+
return `Preparing ${formatWorkflowToolName(event.data?.toolName)}...`;
|
|
102
|
+
case 'tool_progress_started':
|
|
103
|
+
return `Running ${formatWorkflowToolName(event.data?.toolName)}...`;
|
|
104
|
+
case 'tool_error_repairing':
|
|
105
|
+
return 'The tool rejected that attempt. Adjusting the request and trying again...';
|
|
106
|
+
case 'job_confirmation_waiting':
|
|
107
|
+
return 'Waiting for job approval before spending credits...';
|
|
108
|
+
case 'quality_audit_confirmation_waiting':
|
|
109
|
+
return 'Waiting for approval on a preflight quality warning...';
|
|
110
|
+
case 'job_confirmation_confirmed':
|
|
111
|
+
case 'quality_audit_confirmation_confirmed':
|
|
112
|
+
return 'Approved. Starting the job...';
|
|
113
|
+
case 'job_confirmation_overrides_applied':
|
|
114
|
+
return 'Applying approved generation settings...';
|
|
115
|
+
case 'job_confirmation_cancelled':
|
|
116
|
+
case 'quality_audit_confirmation_cancelled':
|
|
117
|
+
return 'Generation cancelled before spending credits.';
|
|
118
|
+
case 'tool_error_explaining':
|
|
119
|
+
return 'The tool could not continue. Preparing an update...';
|
|
120
|
+
case 'tool_error_user_message':
|
|
121
|
+
return 'The tool could not continue. Showing an update...';
|
|
122
|
+
case 'internal_repair_text_suppressed':
|
|
123
|
+
return 'Trying a corrected tool call...';
|
|
124
|
+
case 'internal_repair_no_tool_nudge':
|
|
125
|
+
return 'Still correcting the tool request...';
|
|
126
|
+
case 'tool_error_repeated_stopped':
|
|
127
|
+
return 'Stopped retrying after repeated tool errors.';
|
|
128
|
+
default:
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const TIME_TOKEN_PATTERN = String.raw `(?:(?:\d{1,2}:)?\d{1,2}:\d{2}(?:\.\d+)?|\d+(?:\.\d+)?\s*(?:seconds?|secs?|sec|s)\b)`;
|
|
133
|
+
const AUDIO_WINDOW_CONTEXT_PATTERN = /\b(?:audio file|uploaded audio|attached audio|reference audio|music clip|song clip|soundtrack|background music|music track|music)\b/i;
|
|
134
|
+
export function parseExplicitTimeTokenSeconds(value) {
|
|
135
|
+
const trimmed = value.trim().toLowerCase();
|
|
136
|
+
if (trimmed.includes(':')) {
|
|
137
|
+
const parts = trimmed.split(':').map(part => Number(part));
|
|
138
|
+
if (parts.length < 2 || parts.length > 3 || parts.some(part => !Number.isFinite(part) || part < 0)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const seconds = parts.length === 3
|
|
142
|
+
? (parts[0] * 3600) + (parts[1] * 60) + parts[2]
|
|
143
|
+
: (parts[0] * 60) + parts[1];
|
|
144
|
+
return Number.isFinite(seconds) ? seconds : null;
|
|
145
|
+
}
|
|
146
|
+
const numeric = Number(trimmed.replace(/\s*(?:seconds?|secs?|sec|s)\b/i, ''));
|
|
147
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
|
|
148
|
+
}
|
|
149
|
+
export function inferExplicitSeedanceAudioWindow(text) {
|
|
150
|
+
if (!AUDIO_WINDOW_CONTEXT_PATTERN.test(text))
|
|
151
|
+
return null;
|
|
152
|
+
const rangePattern = new RegExp(`(${TIME_TOKEN_PATTERN})\\s*(?:-|–|—|to|through|until)\\s*(${TIME_TOKEN_PATTERN})`, 'ig');
|
|
153
|
+
for (const match of text.matchAll(rangePattern)) {
|
|
154
|
+
const index = match.index ?? 0;
|
|
155
|
+
const contextSnippet = text.slice(Math.max(0, index - 180), Math.min(text.length, index + 220));
|
|
156
|
+
if (!AUDIO_WINDOW_CONTEXT_PATTERN.test(contextSnippet))
|
|
157
|
+
continue;
|
|
158
|
+
const start = parseExplicitTimeTokenSeconds(match[1] || '');
|
|
159
|
+
const end = parseExplicitTimeTokenSeconds(match[2] || '');
|
|
160
|
+
if (start === null || end === null || end <= start)
|
|
161
|
+
continue;
|
|
162
|
+
return {
|
|
163
|
+
startOffsetSeconds: Math.round(start * 1000) / 1000,
|
|
164
|
+
maxDurationSeconds: Math.round((end - start) * 1000) / 1000,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
export function applyExplicitSeedanceAudioWindowArgs(args, intentText) {
|
|
170
|
+
const window = inferExplicitSeedanceAudioWindow(intentText);
|
|
171
|
+
if (!window)
|
|
172
|
+
return false;
|
|
173
|
+
args.__seedanceReferenceAudioStartOffsetSeconds = window.startOffsetSeconds;
|
|
174
|
+
args.__seedanceReferenceAudioMaxDurationSeconds = window.maxDurationSeconds;
|
|
175
|
+
args.__seedanceReferenceAudioWindowIsExplicit = true;
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
const PHASE_3_GATING_POLICIES = [
|
|
179
|
+
{
|
|
180
|
+
"policyId": "DIAGNOSTIC_REQUEST",
|
|
181
|
+
"version": "1.0.0",
|
|
182
|
+
"trigger": {
|
|
183
|
+
"allOf": [
|
|
184
|
+
"asks_about_previous_error",
|
|
185
|
+
"has_prior_generation_context"
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
"effect": {
|
|
189
|
+
"forbid": [
|
|
190
|
+
"generate_image",
|
|
191
|
+
"edit_image",
|
|
192
|
+
"restore_photo",
|
|
193
|
+
"apply_style",
|
|
194
|
+
"refine_result",
|
|
195
|
+
"animate_photo",
|
|
196
|
+
"change_angle",
|
|
197
|
+
"generate_video",
|
|
198
|
+
"sound_to_video",
|
|
199
|
+
"video_to_video",
|
|
200
|
+
"generate_music",
|
|
201
|
+
"extend_video",
|
|
202
|
+
"replace_video_segment",
|
|
203
|
+
"overlay_video",
|
|
204
|
+
"add_subtitles",
|
|
205
|
+
"stitch_video",
|
|
206
|
+
"orbit_video",
|
|
207
|
+
"dance_montage"
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
"rationale": "The latest user message asks about a previous error. Do not retry media; answer from prior tool calls and context."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"policyId": "GENERATION_DETAILS_QUERY",
|
|
214
|
+
"version": "1.0.0",
|
|
215
|
+
"trigger": {
|
|
216
|
+
"allOf": [
|
|
217
|
+
"asks_about_generation_details",
|
|
218
|
+
"has_prior_generation_context"
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
"effect": {
|
|
222
|
+
"forbid": [
|
|
223
|
+
"generate_image",
|
|
224
|
+
"edit_image",
|
|
225
|
+
"restore_photo",
|
|
226
|
+
"apply_style",
|
|
227
|
+
"refine_result",
|
|
228
|
+
"animate_photo",
|
|
229
|
+
"change_angle",
|
|
230
|
+
"generate_video",
|
|
231
|
+
"sound_to_video",
|
|
232
|
+
"video_to_video",
|
|
233
|
+
"generate_music",
|
|
234
|
+
"extend_video",
|
|
235
|
+
"replace_video_segment",
|
|
236
|
+
"overlay_video",
|
|
237
|
+
"add_subtitles",
|
|
238
|
+
"stitch_video",
|
|
239
|
+
"orbit_video",
|
|
240
|
+
"dance_montage"
|
|
241
|
+
]
|
|
242
|
+
},
|
|
243
|
+
"rationale": "The latest user message asks about prior generation details. Do not create or retry media; answer only from prior tool calls."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"policyId": "TEXT_ONLY_REQUEST",
|
|
247
|
+
"version": "1.0.0",
|
|
248
|
+
"trigger": {
|
|
249
|
+
"allOf": [
|
|
250
|
+
"requests_text_only_response"
|
|
251
|
+
]
|
|
252
|
+
},
|
|
253
|
+
"effect": {
|
|
254
|
+
"forbid": [
|
|
255
|
+
"generate_image",
|
|
256
|
+
"edit_image",
|
|
257
|
+
"restore_photo",
|
|
258
|
+
"apply_style",
|
|
259
|
+
"refine_result",
|
|
260
|
+
"animate_photo",
|
|
261
|
+
"change_angle",
|
|
262
|
+
"generate_video",
|
|
263
|
+
"sound_to_video",
|
|
264
|
+
"video_to_video",
|
|
265
|
+
"generate_music",
|
|
266
|
+
"extend_video",
|
|
267
|
+
"replace_video_segment",
|
|
268
|
+
"overlay_video",
|
|
269
|
+
"add_subtitles",
|
|
270
|
+
"stitch_video",
|
|
271
|
+
"orbit_video",
|
|
272
|
+
"dance_montage"
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
"rationale": "The latest user message requests a text-only response. Do not call any media generation tools."
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"policyId": "DIAGNOSTIC_COMPLAINT",
|
|
279
|
+
"version": "1.0.0",
|
|
280
|
+
"trigger": {
|
|
281
|
+
"allOf": [
|
|
282
|
+
"requests_diagnostic_response",
|
|
283
|
+
"has_prior_generation_context"
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
"effect": {
|
|
287
|
+
"forbid": [
|
|
288
|
+
"generate_image",
|
|
289
|
+
"edit_image",
|
|
290
|
+
"restore_photo",
|
|
291
|
+
"apply_style",
|
|
292
|
+
"refine_result",
|
|
293
|
+
"animate_photo",
|
|
294
|
+
"change_angle",
|
|
295
|
+
"generate_video",
|
|
296
|
+
"sound_to_video",
|
|
297
|
+
"video_to_video",
|
|
298
|
+
"generate_music",
|
|
299
|
+
"extend_video",
|
|
300
|
+
"replace_video_segment",
|
|
301
|
+
"overlay_video",
|
|
302
|
+
"add_subtitles",
|
|
303
|
+
"stitch_video",
|
|
304
|
+
"orbit_video",
|
|
305
|
+
"dance_montage"
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
"rationale": "The latest user message is a diagnostic complaint about prior output. Do not create or retry media; explain what likely went wrong."
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"policyId": "UPLOADED_BASE_VIDEO_PRESENT",
|
|
312
|
+
"version": "1.0.0",
|
|
313
|
+
"trigger": {
|
|
314
|
+
"allOf": [
|
|
315
|
+
"has_uploaded_video",
|
|
316
|
+
"requests_video_modification"
|
|
317
|
+
],
|
|
318
|
+
"sources": {
|
|
319
|
+
"has_uploaded_video": "session_state",
|
|
320
|
+
"requests_video_modification": "planner"
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
"effect": {
|
|
324
|
+
"forbid": [
|
|
325
|
+
"generate_video",
|
|
326
|
+
"animate_photo"
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
"rationale": "A user-uploaded video is the base; modify that asset directly instead of rendering a fresh clip."
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"policyId": "UPLOADED_BASE_VIDEO_EXTEND",
|
|
333
|
+
"version": "1.0.0",
|
|
334
|
+
"trigger": {
|
|
335
|
+
"allOf": [
|
|
336
|
+
"has_uploaded_video",
|
|
337
|
+
"video_modification:extend"
|
|
338
|
+
],
|
|
339
|
+
"sources": {
|
|
340
|
+
"has_uploaded_video": "session_state",
|
|
341
|
+
"video_modification:extend": "planner"
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
"effect": {
|
|
345
|
+
"forbid": [
|
|
346
|
+
"generate_video",
|
|
347
|
+
"animate_photo"
|
|
348
|
+
],
|
|
349
|
+
"require": [
|
|
350
|
+
"extend_video"
|
|
351
|
+
]
|
|
352
|
+
},
|
|
353
|
+
"rationale": "The planner identified uploaded-video extension. Use extend_video on the uploaded base; do not create a separate fresh clip."
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"policyId": "UPLOADED_BASE_VIDEO_REPLACE",
|
|
357
|
+
"version": "1.0.0",
|
|
358
|
+
"trigger": {
|
|
359
|
+
"allOf": [
|
|
360
|
+
"has_uploaded_video",
|
|
361
|
+
"video_modification:replace_segment"
|
|
362
|
+
],
|
|
363
|
+
"sources": {
|
|
364
|
+
"has_uploaded_video": "session_state",
|
|
365
|
+
"video_modification:replace_segment": "planner"
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
"effect": {
|
|
369
|
+
"forbid": [
|
|
370
|
+
"generate_video",
|
|
371
|
+
"animate_photo"
|
|
372
|
+
],
|
|
373
|
+
"require": [
|
|
374
|
+
"replace_video_segment"
|
|
375
|
+
]
|
|
376
|
+
},
|
|
377
|
+
"rationale": "The planner identified an uploaded-video segment replacement. Use replace_video_segment on the uploaded base."
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"policyId": "UPLOADED_BASE_VIDEO_OVERLAY",
|
|
381
|
+
"version": "1.0.0",
|
|
382
|
+
"trigger": {
|
|
383
|
+
"allOf": [
|
|
384
|
+
"has_uploaded_video",
|
|
385
|
+
"video_modification:overlay"
|
|
386
|
+
],
|
|
387
|
+
"sources": {
|
|
388
|
+
"has_uploaded_video": "session_state",
|
|
389
|
+
"video_modification:overlay": "planner"
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
"effect": {
|
|
393
|
+
"forbid": [
|
|
394
|
+
"generate_video",
|
|
395
|
+
"animate_photo"
|
|
396
|
+
],
|
|
397
|
+
"require": [
|
|
398
|
+
"overlay_video"
|
|
399
|
+
]
|
|
400
|
+
},
|
|
401
|
+
"rationale": "The planner identified an uploaded-video overlay. Use overlay_video on the uploaded base."
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"policyId": "UPLOADED_BASE_VIDEO_SUBTITLES",
|
|
405
|
+
"version": "1.0.0",
|
|
406
|
+
"trigger": {
|
|
407
|
+
"allOf": [
|
|
408
|
+
"has_uploaded_video",
|
|
409
|
+
"video_modification:subtitles"
|
|
410
|
+
],
|
|
411
|
+
"sources": {
|
|
412
|
+
"has_uploaded_video": "session_state",
|
|
413
|
+
"video_modification:subtitles": "planner"
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
"effect": {
|
|
417
|
+
"forbid": [
|
|
418
|
+
"generate_video",
|
|
419
|
+
"animate_photo"
|
|
420
|
+
],
|
|
421
|
+
"require": [
|
|
422
|
+
"add_subtitles"
|
|
423
|
+
]
|
|
424
|
+
},
|
|
425
|
+
"rationale": "The planner identified uploaded-video subtitles. Use add_subtitles on the uploaded base."
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"policyId": "UPLOADED_BASE_VIDEO_TRANSFORM",
|
|
429
|
+
"version": "1.0.0",
|
|
430
|
+
"trigger": {
|
|
431
|
+
"allOf": [
|
|
432
|
+
"has_uploaded_video",
|
|
433
|
+
"video_modification:transform"
|
|
434
|
+
],
|
|
435
|
+
"sources": {
|
|
436
|
+
"has_uploaded_video": "session_state",
|
|
437
|
+
"video_modification:transform": "planner"
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
"effect": {
|
|
441
|
+
"forbid": [
|
|
442
|
+
"generate_video",
|
|
443
|
+
"animate_photo"
|
|
444
|
+
],
|
|
445
|
+
"require": [
|
|
446
|
+
"video_to_video"
|
|
447
|
+
]
|
|
448
|
+
},
|
|
449
|
+
"rationale": "The planner identified an uploaded-video transform. Use video_to_video on the uploaded base."
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"policyId": "UPLOADED_BASE_VIDEO_STITCH",
|
|
453
|
+
"version": "1.0.0",
|
|
454
|
+
"trigger": {
|
|
455
|
+
"allOf": [
|
|
456
|
+
"has_uploaded_video",
|
|
457
|
+
"video_modification:stitch"
|
|
458
|
+
],
|
|
459
|
+
"sources": {
|
|
460
|
+
"has_uploaded_video": "session_state",
|
|
461
|
+
"video_modification:stitch": "planner"
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
"effect": {
|
|
465
|
+
"forbid": [
|
|
466
|
+
"generate_video",
|
|
467
|
+
"animate_photo"
|
|
468
|
+
],
|
|
469
|
+
"require": [
|
|
470
|
+
"stitch_video"
|
|
471
|
+
]
|
|
472
|
+
},
|
|
473
|
+
"rationale": "The planner identified uploaded-video stitching. Use stitch_video with the uploaded clips instead of rendering a fresh clip."
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"policyId": "HAS_PERSONA_AND_REQUESTS_VIDEO",
|
|
477
|
+
"version": "1.0.0",
|
|
478
|
+
"trigger": {
|
|
479
|
+
"allOf": [
|
|
480
|
+
"has_active_persona",
|
|
481
|
+
"requests_video_generation",
|
|
482
|
+
"no_persona_image_in_session"
|
|
483
|
+
],
|
|
484
|
+
"sources": {
|
|
485
|
+
"has_active_persona": "session_state",
|
|
486
|
+
"requests_video_generation": "planner",
|
|
487
|
+
"no_persona_image_in_session": "session_state"
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
"effect": {
|
|
491
|
+
"forbid": [],
|
|
492
|
+
"require": [
|
|
493
|
+
"resolve_personas",
|
|
494
|
+
"edit_image"
|
|
495
|
+
]
|
|
496
|
+
},
|
|
497
|
+
"rationale": "Persona videos require an image stage first: resolve_personas, then edit_image to render the persona, then animate. Do not go text-to-video."
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"policyId": "HAS_PERSONA_AND_REQUESTS_PERSONA_IMAGE",
|
|
501
|
+
"version": "1.0.0",
|
|
502
|
+
"trigger": {
|
|
503
|
+
"allOf": [
|
|
504
|
+
"has_active_persona",
|
|
505
|
+
"requests_persona_image_generation"
|
|
506
|
+
],
|
|
507
|
+
"sources": {
|
|
508
|
+
"has_active_persona": "session_state",
|
|
509
|
+
"requests_persona_image_generation": "planner"
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
"effect": {
|
|
513
|
+
"forbid": [
|
|
514
|
+
"generate_image"
|
|
515
|
+
],
|
|
516
|
+
"require": [
|
|
517
|
+
"edit_image"
|
|
518
|
+
]
|
|
519
|
+
},
|
|
520
|
+
"rationale": "Persona images must use edit_image with the persona reference photo, never generate_image (which has no access to the persona identity)."
|
|
521
|
+
}
|
|
522
|
+
];
|
|
523
|
+
const PHASE_4_REPAIR_RECIPES = [
|
|
524
|
+
{
|
|
525
|
+
"recipeId": "extend_video.duration_clamp",
|
|
526
|
+
"version": "1.0.0",
|
|
527
|
+
"toolName": "extend_video",
|
|
528
|
+
"errorCode": "PARAMETER_INVALID",
|
|
529
|
+
"mode": "autoRepair",
|
|
530
|
+
"maxRetries": 1,
|
|
531
|
+
"repairNoteTemplate": "Seedance video segments cap at 15s; adjusted duration from {{requested}}s to {{clamped}}s.",
|
|
532
|
+
"autoRepairFields": [
|
|
533
|
+
"duration"
|
|
534
|
+
]
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"recipeId": "animate_photo.all_failed",
|
|
538
|
+
"version": "1.0.0",
|
|
539
|
+
"toolName": "animate_photo",
|
|
540
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
541
|
+
"mode": "stopAndAsk",
|
|
542
|
+
"maxRetries": 0,
|
|
543
|
+
"repairNoteTemplate": "All animation attempts failed. Want to try a different photo or rewrite the motion prompt?"
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"recipeId": "replace_video_segment.window_invalid",
|
|
547
|
+
"version": "1.0.0",
|
|
548
|
+
"toolName": "replace_video_segment",
|
|
549
|
+
"errorCode": "PARAMETER_INVALID",
|
|
550
|
+
"mode": "stopAndAsk",
|
|
551
|
+
"maxRetries": 0,
|
|
552
|
+
"repairNoteTemplate": "replace_video_segment needs a valid replacement window. {{message}} Please provide a shorter explicit start and end time, for example \"replace 5s to 9s\" or \"redo the last 4 seconds.\""
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
"recipeId": "generate_image.user_input_incomplete",
|
|
556
|
+
"version": "1.0.0",
|
|
557
|
+
"toolName": "generate_image",
|
|
558
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
559
|
+
"mode": "stopAndAsk",
|
|
560
|
+
"maxRetries": 0,
|
|
561
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
"recipeId": "edit_image.user_input_incomplete",
|
|
565
|
+
"version": "1.0.0",
|
|
566
|
+
"toolName": "edit_image",
|
|
567
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
568
|
+
"mode": "stopAndAsk",
|
|
569
|
+
"maxRetries": 0,
|
|
570
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
"recipeId": "restore_photo.user_input_incomplete",
|
|
574
|
+
"version": "1.0.0",
|
|
575
|
+
"toolName": "restore_photo",
|
|
576
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
577
|
+
"mode": "stopAndAsk",
|
|
578
|
+
"maxRetries": 0,
|
|
579
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
"recipeId": "apply_style.user_input_incomplete",
|
|
583
|
+
"version": "1.0.0",
|
|
584
|
+
"toolName": "apply_style",
|
|
585
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
586
|
+
"mode": "stopAndAsk",
|
|
587
|
+
"maxRetries": 0,
|
|
588
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
"recipeId": "refine_result.user_input_incomplete",
|
|
592
|
+
"version": "1.0.0",
|
|
593
|
+
"toolName": "refine_result",
|
|
594
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
595
|
+
"mode": "stopAndAsk",
|
|
596
|
+
"maxRetries": 0,
|
|
597
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
"recipeId": "animate_photo.user_input_incomplete",
|
|
601
|
+
"version": "1.0.0",
|
|
602
|
+
"toolName": "animate_photo",
|
|
603
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
604
|
+
"mode": "stopAndAsk",
|
|
605
|
+
"maxRetries": 0,
|
|
606
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
"recipeId": "change_angle.user_input_incomplete",
|
|
610
|
+
"version": "1.0.0",
|
|
611
|
+
"toolName": "change_angle",
|
|
612
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
613
|
+
"mode": "stopAndAsk",
|
|
614
|
+
"maxRetries": 0,
|
|
615
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
"recipeId": "generate_video.user_input_incomplete",
|
|
619
|
+
"version": "1.0.0",
|
|
620
|
+
"toolName": "generate_video",
|
|
621
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
622
|
+
"mode": "stopAndAsk",
|
|
623
|
+
"maxRetries": 0,
|
|
624
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
"recipeId": "sound_to_video.user_input_incomplete",
|
|
628
|
+
"version": "1.0.0",
|
|
629
|
+
"toolName": "sound_to_video",
|
|
630
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
631
|
+
"mode": "stopAndAsk",
|
|
632
|
+
"maxRetries": 0,
|
|
633
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
"recipeId": "video_to_video.user_input_incomplete",
|
|
637
|
+
"version": "1.0.0",
|
|
638
|
+
"toolName": "video_to_video",
|
|
639
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
640
|
+
"mode": "stopAndAsk",
|
|
641
|
+
"maxRetries": 0,
|
|
642
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
"recipeId": "generate_music.user_input_incomplete",
|
|
646
|
+
"version": "1.0.0",
|
|
647
|
+
"toolName": "generate_music",
|
|
648
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
649
|
+
"mode": "stopAndAsk",
|
|
650
|
+
"maxRetries": 0,
|
|
651
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
"recipeId": "extend_video.user_input_incomplete",
|
|
655
|
+
"version": "1.0.0",
|
|
656
|
+
"toolName": "extend_video",
|
|
657
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
658
|
+
"mode": "stopAndAsk",
|
|
659
|
+
"maxRetries": 0,
|
|
660
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"recipeId": "replace_video_segment.user_input_incomplete",
|
|
664
|
+
"version": "1.0.0",
|
|
665
|
+
"toolName": "replace_video_segment",
|
|
666
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
667
|
+
"mode": "stopAndAsk",
|
|
668
|
+
"maxRetries": 0,
|
|
669
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
"recipeId": "overlay_video.user_input_incomplete",
|
|
673
|
+
"version": "1.0.0",
|
|
674
|
+
"toolName": "overlay_video",
|
|
675
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
676
|
+
"mode": "stopAndAsk",
|
|
677
|
+
"maxRetries": 0,
|
|
678
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
"recipeId": "add_subtitles.user_input_incomplete",
|
|
682
|
+
"version": "1.0.0",
|
|
683
|
+
"toolName": "add_subtitles",
|
|
684
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
685
|
+
"mode": "stopAndAsk",
|
|
686
|
+
"maxRetries": 0,
|
|
687
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
"recipeId": "stitch_video.user_input_incomplete",
|
|
691
|
+
"version": "1.0.0",
|
|
692
|
+
"toolName": "stitch_video",
|
|
693
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
694
|
+
"mode": "stopAndAsk",
|
|
695
|
+
"maxRetries": 0,
|
|
696
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
"recipeId": "orbit_video.user_input_incomplete",
|
|
700
|
+
"version": "1.0.0",
|
|
701
|
+
"toolName": "orbit_video",
|
|
702
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
703
|
+
"mode": "stopAndAsk",
|
|
704
|
+
"maxRetries": 0,
|
|
705
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
"recipeId": "dance_montage.user_input_incomplete",
|
|
709
|
+
"version": "1.0.0",
|
|
710
|
+
"toolName": "dance_montage",
|
|
711
|
+
"errorCode": "USER_INPUT_INCOMPLETE",
|
|
712
|
+
"mode": "stopAndAsk",
|
|
713
|
+
"maxRetries": 0,
|
|
714
|
+
"repairNoteTemplate": "I need more details before I can run {{toolName}}. {{missingDetail}}"
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
"recipeId": "generate_image.cost_limit_exceeded",
|
|
718
|
+
"version": "1.0.0",
|
|
719
|
+
"toolName": "generate_image",
|
|
720
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
721
|
+
"mode": "stopAndAsk",
|
|
722
|
+
"maxRetries": 0,
|
|
723
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
"recipeId": "edit_image.cost_limit_exceeded",
|
|
727
|
+
"version": "1.0.0",
|
|
728
|
+
"toolName": "edit_image",
|
|
729
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
730
|
+
"mode": "stopAndAsk",
|
|
731
|
+
"maxRetries": 0,
|
|
732
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
"recipeId": "restore_photo.cost_limit_exceeded",
|
|
736
|
+
"version": "1.0.0",
|
|
737
|
+
"toolName": "restore_photo",
|
|
738
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
739
|
+
"mode": "stopAndAsk",
|
|
740
|
+
"maxRetries": 0,
|
|
741
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
"recipeId": "apply_style.cost_limit_exceeded",
|
|
745
|
+
"version": "1.0.0",
|
|
746
|
+
"toolName": "apply_style",
|
|
747
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
748
|
+
"mode": "stopAndAsk",
|
|
749
|
+
"maxRetries": 0,
|
|
750
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
"recipeId": "refine_result.cost_limit_exceeded",
|
|
754
|
+
"version": "1.0.0",
|
|
755
|
+
"toolName": "refine_result",
|
|
756
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
757
|
+
"mode": "stopAndAsk",
|
|
758
|
+
"maxRetries": 0,
|
|
759
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
"recipeId": "animate_photo.cost_limit_exceeded",
|
|
763
|
+
"version": "1.0.0",
|
|
764
|
+
"toolName": "animate_photo",
|
|
765
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
766
|
+
"mode": "stopAndAsk",
|
|
767
|
+
"maxRetries": 0,
|
|
768
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
"recipeId": "change_angle.cost_limit_exceeded",
|
|
772
|
+
"version": "1.0.0",
|
|
773
|
+
"toolName": "change_angle",
|
|
774
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
775
|
+
"mode": "stopAndAsk",
|
|
776
|
+
"maxRetries": 0,
|
|
777
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
"recipeId": "generate_video.cost_limit_exceeded",
|
|
781
|
+
"version": "1.0.0",
|
|
782
|
+
"toolName": "generate_video",
|
|
783
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
784
|
+
"mode": "stopAndAsk",
|
|
785
|
+
"maxRetries": 0,
|
|
786
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
"recipeId": "sound_to_video.cost_limit_exceeded",
|
|
790
|
+
"version": "1.0.0",
|
|
791
|
+
"toolName": "sound_to_video",
|
|
792
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
793
|
+
"mode": "stopAndAsk",
|
|
794
|
+
"maxRetries": 0,
|
|
795
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
"recipeId": "video_to_video.cost_limit_exceeded",
|
|
799
|
+
"version": "1.0.0",
|
|
800
|
+
"toolName": "video_to_video",
|
|
801
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
802
|
+
"mode": "stopAndAsk",
|
|
803
|
+
"maxRetries": 0,
|
|
804
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
"recipeId": "generate_music.cost_limit_exceeded",
|
|
808
|
+
"version": "1.0.0",
|
|
809
|
+
"toolName": "generate_music",
|
|
810
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
811
|
+
"mode": "stopAndAsk",
|
|
812
|
+
"maxRetries": 0,
|
|
813
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
"recipeId": "extend_video.cost_limit_exceeded",
|
|
817
|
+
"version": "1.0.0",
|
|
818
|
+
"toolName": "extend_video",
|
|
819
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
820
|
+
"mode": "stopAndAsk",
|
|
821
|
+
"maxRetries": 0,
|
|
822
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
"recipeId": "replace_video_segment.cost_limit_exceeded",
|
|
826
|
+
"version": "1.0.0",
|
|
827
|
+
"toolName": "replace_video_segment",
|
|
828
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
829
|
+
"mode": "stopAndAsk",
|
|
830
|
+
"maxRetries": 0,
|
|
831
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
"recipeId": "overlay_video.cost_limit_exceeded",
|
|
835
|
+
"version": "1.0.0",
|
|
836
|
+
"toolName": "overlay_video",
|
|
837
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
838
|
+
"mode": "stopAndAsk",
|
|
839
|
+
"maxRetries": 0,
|
|
840
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
"recipeId": "add_subtitles.cost_limit_exceeded",
|
|
844
|
+
"version": "1.0.0",
|
|
845
|
+
"toolName": "add_subtitles",
|
|
846
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
847
|
+
"mode": "stopAndAsk",
|
|
848
|
+
"maxRetries": 0,
|
|
849
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
"recipeId": "stitch_video.cost_limit_exceeded",
|
|
853
|
+
"version": "1.0.0",
|
|
854
|
+
"toolName": "stitch_video",
|
|
855
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
856
|
+
"mode": "stopAndAsk",
|
|
857
|
+
"maxRetries": 0,
|
|
858
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
"recipeId": "orbit_video.cost_limit_exceeded",
|
|
862
|
+
"version": "1.0.0",
|
|
863
|
+
"toolName": "orbit_video",
|
|
864
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
865
|
+
"mode": "stopAndAsk",
|
|
866
|
+
"maxRetries": 0,
|
|
867
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
"recipeId": "dance_montage.cost_limit_exceeded",
|
|
871
|
+
"version": "1.0.0",
|
|
872
|
+
"toolName": "dance_montage",
|
|
873
|
+
"errorCode": "COST_LIMIT_EXCEEDED",
|
|
874
|
+
"mode": "stopAndAsk",
|
|
875
|
+
"maxRetries": 0,
|
|
876
|
+
"repairNoteTemplate": "You have hit the credit limit for this turn. Top up credits or wait for the daily refill."
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
"recipeId": "generate_image.asset_not_found",
|
|
880
|
+
"version": "1.0.0",
|
|
881
|
+
"toolName": "generate_image",
|
|
882
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
883
|
+
"mode": "stopAndAsk",
|
|
884
|
+
"maxRetries": 0,
|
|
885
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
"recipeId": "edit_image.asset_not_found",
|
|
889
|
+
"version": "1.0.0",
|
|
890
|
+
"toolName": "edit_image",
|
|
891
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
892
|
+
"mode": "stopAndAsk",
|
|
893
|
+
"maxRetries": 0,
|
|
894
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
"recipeId": "restore_photo.asset_not_found",
|
|
898
|
+
"version": "1.0.0",
|
|
899
|
+
"toolName": "restore_photo",
|
|
900
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
901
|
+
"mode": "stopAndAsk",
|
|
902
|
+
"maxRetries": 0,
|
|
903
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
"recipeId": "apply_style.asset_not_found",
|
|
907
|
+
"version": "1.0.0",
|
|
908
|
+
"toolName": "apply_style",
|
|
909
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
910
|
+
"mode": "stopAndAsk",
|
|
911
|
+
"maxRetries": 0,
|
|
912
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
"recipeId": "refine_result.asset_not_found",
|
|
916
|
+
"version": "1.0.0",
|
|
917
|
+
"toolName": "refine_result",
|
|
918
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
919
|
+
"mode": "stopAndAsk",
|
|
920
|
+
"maxRetries": 0,
|
|
921
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
"recipeId": "animate_photo.asset_not_found",
|
|
925
|
+
"version": "1.0.0",
|
|
926
|
+
"toolName": "animate_photo",
|
|
927
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
928
|
+
"mode": "stopAndAsk",
|
|
929
|
+
"maxRetries": 0,
|
|
930
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
"recipeId": "change_angle.asset_not_found",
|
|
934
|
+
"version": "1.0.0",
|
|
935
|
+
"toolName": "change_angle",
|
|
936
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
937
|
+
"mode": "stopAndAsk",
|
|
938
|
+
"maxRetries": 0,
|
|
939
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
"recipeId": "generate_video.asset_not_found",
|
|
943
|
+
"version": "1.0.0",
|
|
944
|
+
"toolName": "generate_video",
|
|
945
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
946
|
+
"mode": "stopAndAsk",
|
|
947
|
+
"maxRetries": 0,
|
|
948
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
"recipeId": "sound_to_video.asset_not_found",
|
|
952
|
+
"version": "1.0.0",
|
|
953
|
+
"toolName": "sound_to_video",
|
|
954
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
955
|
+
"mode": "stopAndAsk",
|
|
956
|
+
"maxRetries": 0,
|
|
957
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
"recipeId": "video_to_video.asset_not_found",
|
|
961
|
+
"version": "1.0.0",
|
|
962
|
+
"toolName": "video_to_video",
|
|
963
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
964
|
+
"mode": "stopAndAsk",
|
|
965
|
+
"maxRetries": 0,
|
|
966
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
"recipeId": "generate_music.asset_not_found",
|
|
970
|
+
"version": "1.0.0",
|
|
971
|
+
"toolName": "generate_music",
|
|
972
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
973
|
+
"mode": "stopAndAsk",
|
|
974
|
+
"maxRetries": 0,
|
|
975
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
"recipeId": "extend_video.asset_not_found",
|
|
979
|
+
"version": "1.0.0",
|
|
980
|
+
"toolName": "extend_video",
|
|
981
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
982
|
+
"mode": "stopAndAsk",
|
|
983
|
+
"maxRetries": 0,
|
|
984
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
"recipeId": "replace_video_segment.asset_not_found",
|
|
988
|
+
"version": "1.0.0",
|
|
989
|
+
"toolName": "replace_video_segment",
|
|
990
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
991
|
+
"mode": "stopAndAsk",
|
|
992
|
+
"maxRetries": 0,
|
|
993
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
"recipeId": "overlay_video.asset_not_found",
|
|
997
|
+
"version": "1.0.0",
|
|
998
|
+
"toolName": "overlay_video",
|
|
999
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
1000
|
+
"mode": "stopAndAsk",
|
|
1001
|
+
"maxRetries": 0,
|
|
1002
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
"recipeId": "add_subtitles.asset_not_found",
|
|
1006
|
+
"version": "1.0.0",
|
|
1007
|
+
"toolName": "add_subtitles",
|
|
1008
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
1009
|
+
"mode": "stopAndAsk",
|
|
1010
|
+
"maxRetries": 0,
|
|
1011
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
"recipeId": "stitch_video.asset_not_found",
|
|
1015
|
+
"version": "1.0.0",
|
|
1016
|
+
"toolName": "stitch_video",
|
|
1017
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
1018
|
+
"mode": "stopAndAsk",
|
|
1019
|
+
"maxRetries": 0,
|
|
1020
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
"recipeId": "orbit_video.asset_not_found",
|
|
1024
|
+
"version": "1.0.0",
|
|
1025
|
+
"toolName": "orbit_video",
|
|
1026
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
1027
|
+
"mode": "stopAndAsk",
|
|
1028
|
+
"maxRetries": 0,
|
|
1029
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
"recipeId": "dance_montage.asset_not_found",
|
|
1033
|
+
"version": "1.0.0",
|
|
1034
|
+
"toolName": "dance_montage",
|
|
1035
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
1036
|
+
"mode": "stopAndAsk",
|
|
1037
|
+
"maxRetries": 0,
|
|
1038
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
"recipeId": "analyze_image.asset_not_found",
|
|
1042
|
+
"version": "1.0.0",
|
|
1043
|
+
"toolName": "analyze_image",
|
|
1044
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
1045
|
+
"mode": "stopAndAsk",
|
|
1046
|
+
"maxRetries": 0,
|
|
1047
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
"recipeId": "analyze_video.asset_not_found",
|
|
1051
|
+
"version": "1.0.0",
|
|
1052
|
+
"toolName": "analyze_video",
|
|
1053
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
1054
|
+
"mode": "stopAndAsk",
|
|
1055
|
+
"maxRetries": 0,
|
|
1056
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
"recipeId": "extract_metadata.asset_not_found",
|
|
1060
|
+
"version": "1.0.0",
|
|
1061
|
+
"toolName": "extract_metadata",
|
|
1062
|
+
"errorCode": "ASSET_NOT_FOUND",
|
|
1063
|
+
"mode": "stopAndAsk",
|
|
1064
|
+
"maxRetries": 0,
|
|
1065
|
+
"repairNoteTemplate": "I cannot find the asset that {{toolName}} needs. {{message}} Which uploaded or generated asset did you want?"
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
"recipeId": "generate_image.workflow_validation_failed",
|
|
1069
|
+
"version": "1.0.0",
|
|
1070
|
+
"toolName": "generate_image",
|
|
1071
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1072
|
+
"mode": "stopAndAsk",
|
|
1073
|
+
"maxRetries": 0,
|
|
1074
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
"recipeId": "edit_image.workflow_validation_failed",
|
|
1078
|
+
"version": "1.0.0",
|
|
1079
|
+
"toolName": "edit_image",
|
|
1080
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1081
|
+
"mode": "stopAndAsk",
|
|
1082
|
+
"maxRetries": 0,
|
|
1083
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
"recipeId": "restore_photo.workflow_validation_failed",
|
|
1087
|
+
"version": "1.0.0",
|
|
1088
|
+
"toolName": "restore_photo",
|
|
1089
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1090
|
+
"mode": "stopAndAsk",
|
|
1091
|
+
"maxRetries": 0,
|
|
1092
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
"recipeId": "apply_style.workflow_validation_failed",
|
|
1096
|
+
"version": "1.0.0",
|
|
1097
|
+
"toolName": "apply_style",
|
|
1098
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1099
|
+
"mode": "stopAndAsk",
|
|
1100
|
+
"maxRetries": 0,
|
|
1101
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
"recipeId": "refine_result.workflow_validation_failed",
|
|
1105
|
+
"version": "1.0.0",
|
|
1106
|
+
"toolName": "refine_result",
|
|
1107
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1108
|
+
"mode": "stopAndAsk",
|
|
1109
|
+
"maxRetries": 0,
|
|
1110
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
"recipeId": "animate_photo.workflow_validation_failed",
|
|
1114
|
+
"version": "1.0.0",
|
|
1115
|
+
"toolName": "animate_photo",
|
|
1116
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1117
|
+
"mode": "stopAndAsk",
|
|
1118
|
+
"maxRetries": 0,
|
|
1119
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
"recipeId": "change_angle.workflow_validation_failed",
|
|
1123
|
+
"version": "1.0.0",
|
|
1124
|
+
"toolName": "change_angle",
|
|
1125
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1126
|
+
"mode": "stopAndAsk",
|
|
1127
|
+
"maxRetries": 0,
|
|
1128
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
"recipeId": "generate_video.workflow_validation_failed",
|
|
1132
|
+
"version": "1.0.0",
|
|
1133
|
+
"toolName": "generate_video",
|
|
1134
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1135
|
+
"mode": "stopAndAsk",
|
|
1136
|
+
"maxRetries": 0,
|
|
1137
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
"recipeId": "sound_to_video.workflow_validation_failed",
|
|
1141
|
+
"version": "1.0.0",
|
|
1142
|
+
"toolName": "sound_to_video",
|
|
1143
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1144
|
+
"mode": "stopAndAsk",
|
|
1145
|
+
"maxRetries": 0,
|
|
1146
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
"recipeId": "video_to_video.workflow_validation_failed",
|
|
1150
|
+
"version": "1.0.0",
|
|
1151
|
+
"toolName": "video_to_video",
|
|
1152
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1153
|
+
"mode": "stopAndAsk",
|
|
1154
|
+
"maxRetries": 0,
|
|
1155
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
"recipeId": "generate_music.workflow_validation_failed",
|
|
1159
|
+
"version": "1.0.0",
|
|
1160
|
+
"toolName": "generate_music",
|
|
1161
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1162
|
+
"mode": "stopAndAsk",
|
|
1163
|
+
"maxRetries": 0,
|
|
1164
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
"recipeId": "extend_video.workflow_validation_failed",
|
|
1168
|
+
"version": "1.0.0",
|
|
1169
|
+
"toolName": "extend_video",
|
|
1170
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1171
|
+
"mode": "stopAndAsk",
|
|
1172
|
+
"maxRetries": 0,
|
|
1173
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
"recipeId": "replace_video_segment.workflow_validation_failed",
|
|
1177
|
+
"version": "1.0.0",
|
|
1178
|
+
"toolName": "replace_video_segment",
|
|
1179
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1180
|
+
"mode": "stopAndAsk",
|
|
1181
|
+
"maxRetries": 0,
|
|
1182
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
"recipeId": "overlay_video.workflow_validation_failed",
|
|
1186
|
+
"version": "1.0.0",
|
|
1187
|
+
"toolName": "overlay_video",
|
|
1188
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1189
|
+
"mode": "stopAndAsk",
|
|
1190
|
+
"maxRetries": 0,
|
|
1191
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
"recipeId": "add_subtitles.workflow_validation_failed",
|
|
1195
|
+
"version": "1.0.0",
|
|
1196
|
+
"toolName": "add_subtitles",
|
|
1197
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1198
|
+
"mode": "stopAndAsk",
|
|
1199
|
+
"maxRetries": 0,
|
|
1200
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
"recipeId": "stitch_video.workflow_validation_failed",
|
|
1204
|
+
"version": "1.0.0",
|
|
1205
|
+
"toolName": "stitch_video",
|
|
1206
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1207
|
+
"mode": "stopAndAsk",
|
|
1208
|
+
"maxRetries": 0,
|
|
1209
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
"recipeId": "orbit_video.workflow_validation_failed",
|
|
1213
|
+
"version": "1.0.0",
|
|
1214
|
+
"toolName": "orbit_video",
|
|
1215
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1216
|
+
"mode": "stopAndAsk",
|
|
1217
|
+
"maxRetries": 0,
|
|
1218
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
"recipeId": "dance_montage.workflow_validation_failed",
|
|
1222
|
+
"version": "1.0.0",
|
|
1223
|
+
"toolName": "dance_montage",
|
|
1224
|
+
"errorCode": "WORKFLOW_VALIDATION_FAILED",
|
|
1225
|
+
"mode": "stopAndAsk",
|
|
1226
|
+
"maxRetries": 0,
|
|
1227
|
+
"repairNoteTemplate": "{{toolName}} could not run: {{message}}"
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
"recipeId": "generate_image.parameter_invalid",
|
|
1231
|
+
"version": "1.0.0",
|
|
1232
|
+
"toolName": "generate_image",
|
|
1233
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1234
|
+
"mode": "stopAndAsk",
|
|
1235
|
+
"maxRetries": 0,
|
|
1236
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
"recipeId": "edit_image.parameter_invalid",
|
|
1240
|
+
"version": "1.0.0",
|
|
1241
|
+
"toolName": "edit_image",
|
|
1242
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1243
|
+
"mode": "stopAndAsk",
|
|
1244
|
+
"maxRetries": 0,
|
|
1245
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
"recipeId": "restore_photo.parameter_invalid",
|
|
1249
|
+
"version": "1.0.0",
|
|
1250
|
+
"toolName": "restore_photo",
|
|
1251
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1252
|
+
"mode": "stopAndAsk",
|
|
1253
|
+
"maxRetries": 0,
|
|
1254
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
"recipeId": "apply_style.parameter_invalid",
|
|
1258
|
+
"version": "1.0.0",
|
|
1259
|
+
"toolName": "apply_style",
|
|
1260
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1261
|
+
"mode": "stopAndAsk",
|
|
1262
|
+
"maxRetries": 0,
|
|
1263
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
"recipeId": "refine_result.parameter_invalid",
|
|
1267
|
+
"version": "1.0.0",
|
|
1268
|
+
"toolName": "refine_result",
|
|
1269
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1270
|
+
"mode": "stopAndAsk",
|
|
1271
|
+
"maxRetries": 0,
|
|
1272
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
"recipeId": "animate_photo.parameter_invalid",
|
|
1276
|
+
"version": "1.0.0",
|
|
1277
|
+
"toolName": "animate_photo",
|
|
1278
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1279
|
+
"mode": "stopAndAsk",
|
|
1280
|
+
"maxRetries": 0,
|
|
1281
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
"recipeId": "change_angle.parameter_invalid",
|
|
1285
|
+
"version": "1.0.0",
|
|
1286
|
+
"toolName": "change_angle",
|
|
1287
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1288
|
+
"mode": "stopAndAsk",
|
|
1289
|
+
"maxRetries": 0,
|
|
1290
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
"recipeId": "generate_video.parameter_invalid",
|
|
1294
|
+
"version": "1.0.0",
|
|
1295
|
+
"toolName": "generate_video",
|
|
1296
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1297
|
+
"mode": "stopAndAsk",
|
|
1298
|
+
"maxRetries": 0,
|
|
1299
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
"recipeId": "sound_to_video.parameter_invalid",
|
|
1303
|
+
"version": "1.0.0",
|
|
1304
|
+
"toolName": "sound_to_video",
|
|
1305
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1306
|
+
"mode": "stopAndAsk",
|
|
1307
|
+
"maxRetries": 0,
|
|
1308
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
"recipeId": "video_to_video.parameter_invalid",
|
|
1312
|
+
"version": "1.0.0",
|
|
1313
|
+
"toolName": "video_to_video",
|
|
1314
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1315
|
+
"mode": "stopAndAsk",
|
|
1316
|
+
"maxRetries": 0,
|
|
1317
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
"recipeId": "generate_music.parameter_invalid",
|
|
1321
|
+
"version": "1.0.0",
|
|
1322
|
+
"toolName": "generate_music",
|
|
1323
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1324
|
+
"mode": "stopAndAsk",
|
|
1325
|
+
"maxRetries": 0,
|
|
1326
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
"recipeId": "overlay_video.parameter_invalid",
|
|
1330
|
+
"version": "1.0.0",
|
|
1331
|
+
"toolName": "overlay_video",
|
|
1332
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1333
|
+
"mode": "stopAndAsk",
|
|
1334
|
+
"maxRetries": 0,
|
|
1335
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
"recipeId": "add_subtitles.parameter_invalid",
|
|
1339
|
+
"version": "1.0.0",
|
|
1340
|
+
"toolName": "add_subtitles",
|
|
1341
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1342
|
+
"mode": "stopAndAsk",
|
|
1343
|
+
"maxRetries": 0,
|
|
1344
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
"recipeId": "stitch_video.parameter_invalid",
|
|
1348
|
+
"version": "1.0.0",
|
|
1349
|
+
"toolName": "stitch_video",
|
|
1350
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1351
|
+
"mode": "stopAndAsk",
|
|
1352
|
+
"maxRetries": 0,
|
|
1353
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
"recipeId": "orbit_video.parameter_invalid",
|
|
1357
|
+
"version": "1.0.0",
|
|
1358
|
+
"toolName": "orbit_video",
|
|
1359
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1360
|
+
"mode": "stopAndAsk",
|
|
1361
|
+
"maxRetries": 0,
|
|
1362
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
"recipeId": "dance_montage.parameter_invalid",
|
|
1366
|
+
"version": "1.0.0",
|
|
1367
|
+
"toolName": "dance_montage",
|
|
1368
|
+
"errorCode": "PARAMETER_INVALID",
|
|
1369
|
+
"mode": "stopAndAsk",
|
|
1370
|
+
"maxRetries": 0,
|
|
1371
|
+
"repairNoteTemplate": "{{toolName}} rejected the arguments: {{message}}"
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
"recipeId": "generate_image.gpu_worker_failed",
|
|
1375
|
+
"version": "1.0.0",
|
|
1376
|
+
"toolName": "generate_image",
|
|
1377
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1378
|
+
"mode": "stopAndAsk",
|
|
1379
|
+
"maxRetries": 0,
|
|
1380
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
"recipeId": "edit_image.gpu_worker_failed",
|
|
1384
|
+
"version": "1.0.0",
|
|
1385
|
+
"toolName": "edit_image",
|
|
1386
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1387
|
+
"mode": "stopAndAsk",
|
|
1388
|
+
"maxRetries": 0,
|
|
1389
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
"recipeId": "restore_photo.gpu_worker_failed",
|
|
1393
|
+
"version": "1.0.0",
|
|
1394
|
+
"toolName": "restore_photo",
|
|
1395
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1396
|
+
"mode": "stopAndAsk",
|
|
1397
|
+
"maxRetries": 0,
|
|
1398
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
"recipeId": "apply_style.gpu_worker_failed",
|
|
1402
|
+
"version": "1.0.0",
|
|
1403
|
+
"toolName": "apply_style",
|
|
1404
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1405
|
+
"mode": "stopAndAsk",
|
|
1406
|
+
"maxRetries": 0,
|
|
1407
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
"recipeId": "refine_result.gpu_worker_failed",
|
|
1411
|
+
"version": "1.0.0",
|
|
1412
|
+
"toolName": "refine_result",
|
|
1413
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1414
|
+
"mode": "stopAndAsk",
|
|
1415
|
+
"maxRetries": 0,
|
|
1416
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1419
|
+
"recipeId": "change_angle.gpu_worker_failed",
|
|
1420
|
+
"version": "1.0.0",
|
|
1421
|
+
"toolName": "change_angle",
|
|
1422
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1423
|
+
"mode": "stopAndAsk",
|
|
1424
|
+
"maxRetries": 0,
|
|
1425
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
"recipeId": "generate_video.gpu_worker_failed",
|
|
1429
|
+
"version": "1.0.0",
|
|
1430
|
+
"toolName": "generate_video",
|
|
1431
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1432
|
+
"mode": "stopAndAsk",
|
|
1433
|
+
"maxRetries": 0,
|
|
1434
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
"recipeId": "sound_to_video.gpu_worker_failed",
|
|
1438
|
+
"version": "1.0.0",
|
|
1439
|
+
"toolName": "sound_to_video",
|
|
1440
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1441
|
+
"mode": "stopAndAsk",
|
|
1442
|
+
"maxRetries": 0,
|
|
1443
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
"recipeId": "video_to_video.gpu_worker_failed",
|
|
1447
|
+
"version": "1.0.0",
|
|
1448
|
+
"toolName": "video_to_video",
|
|
1449
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1450
|
+
"mode": "stopAndAsk",
|
|
1451
|
+
"maxRetries": 0,
|
|
1452
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
"recipeId": "generate_music.gpu_worker_failed",
|
|
1456
|
+
"version": "1.0.0",
|
|
1457
|
+
"toolName": "generate_music",
|
|
1458
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1459
|
+
"mode": "stopAndAsk",
|
|
1460
|
+
"maxRetries": 0,
|
|
1461
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
"recipeId": "extend_video.gpu_worker_failed",
|
|
1465
|
+
"version": "1.0.0",
|
|
1466
|
+
"toolName": "extend_video",
|
|
1467
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1468
|
+
"mode": "stopAndAsk",
|
|
1469
|
+
"maxRetries": 0,
|
|
1470
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
"recipeId": "replace_video_segment.gpu_worker_failed",
|
|
1474
|
+
"version": "1.0.0",
|
|
1475
|
+
"toolName": "replace_video_segment",
|
|
1476
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1477
|
+
"mode": "stopAndAsk",
|
|
1478
|
+
"maxRetries": 0,
|
|
1479
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
"recipeId": "overlay_video.gpu_worker_failed",
|
|
1483
|
+
"version": "1.0.0",
|
|
1484
|
+
"toolName": "overlay_video",
|
|
1485
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1486
|
+
"mode": "stopAndAsk",
|
|
1487
|
+
"maxRetries": 0,
|
|
1488
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
"recipeId": "add_subtitles.gpu_worker_failed",
|
|
1492
|
+
"version": "1.0.0",
|
|
1493
|
+
"toolName": "add_subtitles",
|
|
1494
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1495
|
+
"mode": "stopAndAsk",
|
|
1496
|
+
"maxRetries": 0,
|
|
1497
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1498
|
+
},
|
|
1499
|
+
{
|
|
1500
|
+
"recipeId": "stitch_video.gpu_worker_failed",
|
|
1501
|
+
"version": "1.0.0",
|
|
1502
|
+
"toolName": "stitch_video",
|
|
1503
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1504
|
+
"mode": "stopAndAsk",
|
|
1505
|
+
"maxRetries": 0,
|
|
1506
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
"recipeId": "orbit_video.gpu_worker_failed",
|
|
1510
|
+
"version": "1.0.0",
|
|
1511
|
+
"toolName": "orbit_video",
|
|
1512
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1513
|
+
"mode": "stopAndAsk",
|
|
1514
|
+
"maxRetries": 0,
|
|
1515
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
"recipeId": "dance_montage.gpu_worker_failed",
|
|
1519
|
+
"version": "1.0.0",
|
|
1520
|
+
"toolName": "dance_montage",
|
|
1521
|
+
"errorCode": "GPU_WORKER_FAILED",
|
|
1522
|
+
"mode": "stopAndAsk",
|
|
1523
|
+
"maxRetries": 0,
|
|
1524
|
+
"repairNoteTemplate": "The {{toolName}} worker failed. {{message}} Want me to try again or change the request?"
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
"recipeId": "generate_image.model_unavailable",
|
|
1528
|
+
"version": "1.0.0",
|
|
1529
|
+
"toolName": "generate_image",
|
|
1530
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1531
|
+
"mode": "stopAndAsk",
|
|
1532
|
+
"maxRetries": 0,
|
|
1533
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
"recipeId": "edit_image.model_unavailable",
|
|
1537
|
+
"version": "1.0.0",
|
|
1538
|
+
"toolName": "edit_image",
|
|
1539
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1540
|
+
"mode": "stopAndAsk",
|
|
1541
|
+
"maxRetries": 0,
|
|
1542
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
"recipeId": "restore_photo.model_unavailable",
|
|
1546
|
+
"version": "1.0.0",
|
|
1547
|
+
"toolName": "restore_photo",
|
|
1548
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1549
|
+
"mode": "stopAndAsk",
|
|
1550
|
+
"maxRetries": 0,
|
|
1551
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
"recipeId": "apply_style.model_unavailable",
|
|
1555
|
+
"version": "1.0.0",
|
|
1556
|
+
"toolName": "apply_style",
|
|
1557
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1558
|
+
"mode": "stopAndAsk",
|
|
1559
|
+
"maxRetries": 0,
|
|
1560
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
"recipeId": "refine_result.model_unavailable",
|
|
1564
|
+
"version": "1.0.0",
|
|
1565
|
+
"toolName": "refine_result",
|
|
1566
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1567
|
+
"mode": "stopAndAsk",
|
|
1568
|
+
"maxRetries": 0,
|
|
1569
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
"recipeId": "animate_photo.model_unavailable",
|
|
1573
|
+
"version": "1.0.0",
|
|
1574
|
+
"toolName": "animate_photo",
|
|
1575
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1576
|
+
"mode": "stopAndAsk",
|
|
1577
|
+
"maxRetries": 0,
|
|
1578
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
"recipeId": "change_angle.model_unavailable",
|
|
1582
|
+
"version": "1.0.0",
|
|
1583
|
+
"toolName": "change_angle",
|
|
1584
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1585
|
+
"mode": "stopAndAsk",
|
|
1586
|
+
"maxRetries": 0,
|
|
1587
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
"recipeId": "generate_video.model_unavailable",
|
|
1591
|
+
"version": "1.0.0",
|
|
1592
|
+
"toolName": "generate_video",
|
|
1593
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1594
|
+
"mode": "stopAndAsk",
|
|
1595
|
+
"maxRetries": 0,
|
|
1596
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
"recipeId": "sound_to_video.model_unavailable",
|
|
1600
|
+
"version": "1.0.0",
|
|
1601
|
+
"toolName": "sound_to_video",
|
|
1602
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1603
|
+
"mode": "stopAndAsk",
|
|
1604
|
+
"maxRetries": 0,
|
|
1605
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
"recipeId": "video_to_video.model_unavailable",
|
|
1609
|
+
"version": "1.0.0",
|
|
1610
|
+
"toolName": "video_to_video",
|
|
1611
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1612
|
+
"mode": "stopAndAsk",
|
|
1613
|
+
"maxRetries": 0,
|
|
1614
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
"recipeId": "generate_music.model_unavailable",
|
|
1618
|
+
"version": "1.0.0",
|
|
1619
|
+
"toolName": "generate_music",
|
|
1620
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1621
|
+
"mode": "stopAndAsk",
|
|
1622
|
+
"maxRetries": 0,
|
|
1623
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
"recipeId": "extend_video.model_unavailable",
|
|
1627
|
+
"version": "1.0.0",
|
|
1628
|
+
"toolName": "extend_video",
|
|
1629
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1630
|
+
"mode": "stopAndAsk",
|
|
1631
|
+
"maxRetries": 0,
|
|
1632
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
"recipeId": "replace_video_segment.model_unavailable",
|
|
1636
|
+
"version": "1.0.0",
|
|
1637
|
+
"toolName": "replace_video_segment",
|
|
1638
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1639
|
+
"mode": "stopAndAsk",
|
|
1640
|
+
"maxRetries": 0,
|
|
1641
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
"recipeId": "overlay_video.model_unavailable",
|
|
1645
|
+
"version": "1.0.0",
|
|
1646
|
+
"toolName": "overlay_video",
|
|
1647
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1648
|
+
"mode": "stopAndAsk",
|
|
1649
|
+
"maxRetries": 0,
|
|
1650
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
"recipeId": "add_subtitles.model_unavailable",
|
|
1654
|
+
"version": "1.0.0",
|
|
1655
|
+
"toolName": "add_subtitles",
|
|
1656
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1657
|
+
"mode": "stopAndAsk",
|
|
1658
|
+
"maxRetries": 0,
|
|
1659
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
"recipeId": "stitch_video.model_unavailable",
|
|
1663
|
+
"version": "1.0.0",
|
|
1664
|
+
"toolName": "stitch_video",
|
|
1665
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1666
|
+
"mode": "stopAndAsk",
|
|
1667
|
+
"maxRetries": 0,
|
|
1668
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
"recipeId": "orbit_video.model_unavailable",
|
|
1672
|
+
"version": "1.0.0",
|
|
1673
|
+
"toolName": "orbit_video",
|
|
1674
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1675
|
+
"mode": "stopAndAsk",
|
|
1676
|
+
"maxRetries": 0,
|
|
1677
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
"recipeId": "dance_montage.model_unavailable",
|
|
1681
|
+
"version": "1.0.0",
|
|
1682
|
+
"toolName": "dance_montage",
|
|
1683
|
+
"errorCode": "MODEL_UNAVAILABLE",
|
|
1684
|
+
"mode": "stopAndAsk",
|
|
1685
|
+
"maxRetries": 0,
|
|
1686
|
+
"repairNoteTemplate": "The model {{toolName}} wanted is offline. {{message}} Pick a different model or try again later."
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
"recipeId": "generate_image.permission_required",
|
|
1690
|
+
"version": "1.0.0",
|
|
1691
|
+
"toolName": "generate_image",
|
|
1692
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1693
|
+
"mode": "stopAndAsk",
|
|
1694
|
+
"maxRetries": 0,
|
|
1695
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
"recipeId": "edit_image.permission_required",
|
|
1699
|
+
"version": "1.0.0",
|
|
1700
|
+
"toolName": "edit_image",
|
|
1701
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1702
|
+
"mode": "stopAndAsk",
|
|
1703
|
+
"maxRetries": 0,
|
|
1704
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
"recipeId": "restore_photo.permission_required",
|
|
1708
|
+
"version": "1.0.0",
|
|
1709
|
+
"toolName": "restore_photo",
|
|
1710
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1711
|
+
"mode": "stopAndAsk",
|
|
1712
|
+
"maxRetries": 0,
|
|
1713
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
"recipeId": "apply_style.permission_required",
|
|
1717
|
+
"version": "1.0.0",
|
|
1718
|
+
"toolName": "apply_style",
|
|
1719
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1720
|
+
"mode": "stopAndAsk",
|
|
1721
|
+
"maxRetries": 0,
|
|
1722
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
"recipeId": "refine_result.permission_required",
|
|
1726
|
+
"version": "1.0.0",
|
|
1727
|
+
"toolName": "refine_result",
|
|
1728
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1729
|
+
"mode": "stopAndAsk",
|
|
1730
|
+
"maxRetries": 0,
|
|
1731
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
"recipeId": "animate_photo.permission_required",
|
|
1735
|
+
"version": "1.0.0",
|
|
1736
|
+
"toolName": "animate_photo",
|
|
1737
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1738
|
+
"mode": "stopAndAsk",
|
|
1739
|
+
"maxRetries": 0,
|
|
1740
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1741
|
+
},
|
|
1742
|
+
{
|
|
1743
|
+
"recipeId": "change_angle.permission_required",
|
|
1744
|
+
"version": "1.0.0",
|
|
1745
|
+
"toolName": "change_angle",
|
|
1746
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1747
|
+
"mode": "stopAndAsk",
|
|
1748
|
+
"maxRetries": 0,
|
|
1749
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
"recipeId": "generate_video.permission_required",
|
|
1753
|
+
"version": "1.0.0",
|
|
1754
|
+
"toolName": "generate_video",
|
|
1755
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1756
|
+
"mode": "stopAndAsk",
|
|
1757
|
+
"maxRetries": 0,
|
|
1758
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1759
|
+
},
|
|
1760
|
+
{
|
|
1761
|
+
"recipeId": "sound_to_video.permission_required",
|
|
1762
|
+
"version": "1.0.0",
|
|
1763
|
+
"toolName": "sound_to_video",
|
|
1764
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1765
|
+
"mode": "stopAndAsk",
|
|
1766
|
+
"maxRetries": 0,
|
|
1767
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
"recipeId": "video_to_video.permission_required",
|
|
1771
|
+
"version": "1.0.0",
|
|
1772
|
+
"toolName": "video_to_video",
|
|
1773
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1774
|
+
"mode": "stopAndAsk",
|
|
1775
|
+
"maxRetries": 0,
|
|
1776
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
"recipeId": "generate_music.permission_required",
|
|
1780
|
+
"version": "1.0.0",
|
|
1781
|
+
"toolName": "generate_music",
|
|
1782
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1783
|
+
"mode": "stopAndAsk",
|
|
1784
|
+
"maxRetries": 0,
|
|
1785
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
"recipeId": "extend_video.permission_required",
|
|
1789
|
+
"version": "1.0.0",
|
|
1790
|
+
"toolName": "extend_video",
|
|
1791
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1792
|
+
"mode": "stopAndAsk",
|
|
1793
|
+
"maxRetries": 0,
|
|
1794
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
"recipeId": "replace_video_segment.permission_required",
|
|
1798
|
+
"version": "1.0.0",
|
|
1799
|
+
"toolName": "replace_video_segment",
|
|
1800
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1801
|
+
"mode": "stopAndAsk",
|
|
1802
|
+
"maxRetries": 0,
|
|
1803
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
"recipeId": "overlay_video.permission_required",
|
|
1807
|
+
"version": "1.0.0",
|
|
1808
|
+
"toolName": "overlay_video",
|
|
1809
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1810
|
+
"mode": "stopAndAsk",
|
|
1811
|
+
"maxRetries": 0,
|
|
1812
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
"recipeId": "add_subtitles.permission_required",
|
|
1816
|
+
"version": "1.0.0",
|
|
1817
|
+
"toolName": "add_subtitles",
|
|
1818
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1819
|
+
"mode": "stopAndAsk",
|
|
1820
|
+
"maxRetries": 0,
|
|
1821
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
"recipeId": "stitch_video.permission_required",
|
|
1825
|
+
"version": "1.0.0",
|
|
1826
|
+
"toolName": "stitch_video",
|
|
1827
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1828
|
+
"mode": "stopAndAsk",
|
|
1829
|
+
"maxRetries": 0,
|
|
1830
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
"recipeId": "orbit_video.permission_required",
|
|
1834
|
+
"version": "1.0.0",
|
|
1835
|
+
"toolName": "orbit_video",
|
|
1836
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1837
|
+
"mode": "stopAndAsk",
|
|
1838
|
+
"maxRetries": 0,
|
|
1839
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
"recipeId": "dance_montage.permission_required",
|
|
1843
|
+
"version": "1.0.0",
|
|
1844
|
+
"toolName": "dance_montage",
|
|
1845
|
+
"errorCode": "PERMISSION_REQUIRED",
|
|
1846
|
+
"mode": "stopAndAsk",
|
|
1847
|
+
"maxRetries": 0,
|
|
1848
|
+
"repairNoteTemplate": "{{toolName}} needs permission you have not granted yet. {{message}}"
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
"recipeId": "generate_image.safety_rewrite",
|
|
1852
|
+
"version": "1.0.0",
|
|
1853
|
+
"toolName": "generate_image",
|
|
1854
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1855
|
+
"mode": "suggestFollowup",
|
|
1856
|
+
"maxRetries": 1,
|
|
1857
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1858
|
+
"suggestedFollowupTool": "generate_image"
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
"recipeId": "edit_image.safety_rewrite",
|
|
1862
|
+
"version": "1.0.0",
|
|
1863
|
+
"toolName": "edit_image",
|
|
1864
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1865
|
+
"mode": "suggestFollowup",
|
|
1866
|
+
"maxRetries": 1,
|
|
1867
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1868
|
+
"suggestedFollowupTool": "edit_image"
|
|
1869
|
+
},
|
|
1870
|
+
{
|
|
1871
|
+
"recipeId": "restore_photo.safety_rewrite",
|
|
1872
|
+
"version": "1.0.0",
|
|
1873
|
+
"toolName": "restore_photo",
|
|
1874
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1875
|
+
"mode": "suggestFollowup",
|
|
1876
|
+
"maxRetries": 1,
|
|
1877
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1878
|
+
"suggestedFollowupTool": "restore_photo"
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
"recipeId": "apply_style.safety_rewrite",
|
|
1882
|
+
"version": "1.0.0",
|
|
1883
|
+
"toolName": "apply_style",
|
|
1884
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1885
|
+
"mode": "suggestFollowup",
|
|
1886
|
+
"maxRetries": 1,
|
|
1887
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1888
|
+
"suggestedFollowupTool": "apply_style"
|
|
1889
|
+
},
|
|
1890
|
+
{
|
|
1891
|
+
"recipeId": "refine_result.safety_rewrite",
|
|
1892
|
+
"version": "1.0.0",
|
|
1893
|
+
"toolName": "refine_result",
|
|
1894
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1895
|
+
"mode": "suggestFollowup",
|
|
1896
|
+
"maxRetries": 1,
|
|
1897
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1898
|
+
"suggestedFollowupTool": "refine_result"
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
"recipeId": "animate_photo.safety_rewrite",
|
|
1902
|
+
"version": "1.0.0",
|
|
1903
|
+
"toolName": "animate_photo",
|
|
1904
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1905
|
+
"mode": "suggestFollowup",
|
|
1906
|
+
"maxRetries": 1,
|
|
1907
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1908
|
+
"suggestedFollowupTool": "animate_photo"
|
|
1909
|
+
},
|
|
1910
|
+
{
|
|
1911
|
+
"recipeId": "generate_video.safety_rewrite",
|
|
1912
|
+
"version": "1.0.0",
|
|
1913
|
+
"toolName": "generate_video",
|
|
1914
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1915
|
+
"mode": "suggestFollowup",
|
|
1916
|
+
"maxRetries": 1,
|
|
1917
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1918
|
+
"suggestedFollowupTool": "generate_video"
|
|
1919
|
+
},
|
|
1920
|
+
{
|
|
1921
|
+
"recipeId": "sound_to_video.safety_rewrite",
|
|
1922
|
+
"version": "1.0.0",
|
|
1923
|
+
"toolName": "sound_to_video",
|
|
1924
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1925
|
+
"mode": "suggestFollowup",
|
|
1926
|
+
"maxRetries": 1,
|
|
1927
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1928
|
+
"suggestedFollowupTool": "sound_to_video"
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
"recipeId": "video_to_video.safety_rewrite",
|
|
1932
|
+
"version": "1.0.0",
|
|
1933
|
+
"toolName": "video_to_video",
|
|
1934
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1935
|
+
"mode": "suggestFollowup",
|
|
1936
|
+
"maxRetries": 1,
|
|
1937
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1938
|
+
"suggestedFollowupTool": "video_to_video"
|
|
1939
|
+
},
|
|
1940
|
+
{
|
|
1941
|
+
"recipeId": "generate_music.safety_rewrite",
|
|
1942
|
+
"version": "1.0.0",
|
|
1943
|
+
"toolName": "generate_music",
|
|
1944
|
+
"errorCode": "SAFETY_REJECTED",
|
|
1945
|
+
"mode": "suggestFollowup",
|
|
1946
|
+
"maxRetries": 1,
|
|
1947
|
+
"repairNoteTemplate": "Content filter rejected the prompt for {{toolName}}. Try a softer phrasing or different scene.",
|
|
1948
|
+
"suggestedFollowupTool": "generate_music"
|
|
1949
|
+
},
|
|
1950
|
+
{
|
|
1951
|
+
"recipeId": "generate_image.provider_timeout",
|
|
1952
|
+
"version": "1.0.0",
|
|
1953
|
+
"toolName": "generate_image",
|
|
1954
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
1955
|
+
"mode": "stopAndAsk",
|
|
1956
|
+
"maxRetries": 0,
|
|
1957
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
"recipeId": "edit_image.provider_timeout",
|
|
1961
|
+
"version": "1.0.0",
|
|
1962
|
+
"toolName": "edit_image",
|
|
1963
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
1964
|
+
"mode": "stopAndAsk",
|
|
1965
|
+
"maxRetries": 0,
|
|
1966
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
1967
|
+
},
|
|
1968
|
+
{
|
|
1969
|
+
"recipeId": "restore_photo.provider_timeout",
|
|
1970
|
+
"version": "1.0.0",
|
|
1971
|
+
"toolName": "restore_photo",
|
|
1972
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
1973
|
+
"mode": "stopAndAsk",
|
|
1974
|
+
"maxRetries": 0,
|
|
1975
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
1976
|
+
},
|
|
1977
|
+
{
|
|
1978
|
+
"recipeId": "apply_style.provider_timeout",
|
|
1979
|
+
"version": "1.0.0",
|
|
1980
|
+
"toolName": "apply_style",
|
|
1981
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
1982
|
+
"mode": "stopAndAsk",
|
|
1983
|
+
"maxRetries": 0,
|
|
1984
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
"recipeId": "refine_result.provider_timeout",
|
|
1988
|
+
"version": "1.0.0",
|
|
1989
|
+
"toolName": "refine_result",
|
|
1990
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
1991
|
+
"mode": "stopAndAsk",
|
|
1992
|
+
"maxRetries": 0,
|
|
1993
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
"recipeId": "animate_photo.provider_timeout",
|
|
1997
|
+
"version": "1.0.0",
|
|
1998
|
+
"toolName": "animate_photo",
|
|
1999
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2000
|
+
"mode": "stopAndAsk",
|
|
2001
|
+
"maxRetries": 0,
|
|
2002
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
"recipeId": "change_angle.provider_timeout",
|
|
2006
|
+
"version": "1.0.0",
|
|
2007
|
+
"toolName": "change_angle",
|
|
2008
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2009
|
+
"mode": "stopAndAsk",
|
|
2010
|
+
"maxRetries": 0,
|
|
2011
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
"recipeId": "generate_video.provider_timeout",
|
|
2015
|
+
"version": "1.0.0",
|
|
2016
|
+
"toolName": "generate_video",
|
|
2017
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2018
|
+
"mode": "stopAndAsk",
|
|
2019
|
+
"maxRetries": 0,
|
|
2020
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
"recipeId": "sound_to_video.provider_timeout",
|
|
2024
|
+
"version": "1.0.0",
|
|
2025
|
+
"toolName": "sound_to_video",
|
|
2026
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2027
|
+
"mode": "stopAndAsk",
|
|
2028
|
+
"maxRetries": 0,
|
|
2029
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
"recipeId": "video_to_video.provider_timeout",
|
|
2033
|
+
"version": "1.0.0",
|
|
2034
|
+
"toolName": "video_to_video",
|
|
2035
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2036
|
+
"mode": "stopAndAsk",
|
|
2037
|
+
"maxRetries": 0,
|
|
2038
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
"recipeId": "generate_music.provider_timeout",
|
|
2042
|
+
"version": "1.0.0",
|
|
2043
|
+
"toolName": "generate_music",
|
|
2044
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2045
|
+
"mode": "stopAndAsk",
|
|
2046
|
+
"maxRetries": 0,
|
|
2047
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
"recipeId": "extend_video.provider_timeout",
|
|
2051
|
+
"version": "1.0.0",
|
|
2052
|
+
"toolName": "extend_video",
|
|
2053
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2054
|
+
"mode": "stopAndAsk",
|
|
2055
|
+
"maxRetries": 0,
|
|
2056
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2057
|
+
},
|
|
2058
|
+
{
|
|
2059
|
+
"recipeId": "replace_video_segment.provider_timeout",
|
|
2060
|
+
"version": "1.0.0",
|
|
2061
|
+
"toolName": "replace_video_segment",
|
|
2062
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2063
|
+
"mode": "stopAndAsk",
|
|
2064
|
+
"maxRetries": 0,
|
|
2065
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
"recipeId": "overlay_video.provider_timeout",
|
|
2069
|
+
"version": "1.0.0",
|
|
2070
|
+
"toolName": "overlay_video",
|
|
2071
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2072
|
+
"mode": "stopAndAsk",
|
|
2073
|
+
"maxRetries": 0,
|
|
2074
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
"recipeId": "add_subtitles.provider_timeout",
|
|
2078
|
+
"version": "1.0.0",
|
|
2079
|
+
"toolName": "add_subtitles",
|
|
2080
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2081
|
+
"mode": "stopAndAsk",
|
|
2082
|
+
"maxRetries": 0,
|
|
2083
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2084
|
+
},
|
|
2085
|
+
{
|
|
2086
|
+
"recipeId": "stitch_video.provider_timeout",
|
|
2087
|
+
"version": "1.0.0",
|
|
2088
|
+
"toolName": "stitch_video",
|
|
2089
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2090
|
+
"mode": "stopAndAsk",
|
|
2091
|
+
"maxRetries": 0,
|
|
2092
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
"recipeId": "orbit_video.provider_timeout",
|
|
2096
|
+
"version": "1.0.0",
|
|
2097
|
+
"toolName": "orbit_video",
|
|
2098
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2099
|
+
"mode": "stopAndAsk",
|
|
2100
|
+
"maxRetries": 0,
|
|
2101
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
"recipeId": "dance_montage.provider_timeout",
|
|
2105
|
+
"version": "1.0.0",
|
|
2106
|
+
"toolName": "dance_montage",
|
|
2107
|
+
"errorCode": "PROVIDER_TIMEOUT",
|
|
2108
|
+
"mode": "stopAndAsk",
|
|
2109
|
+
"maxRetries": 0,
|
|
2110
|
+
"repairNoteTemplate": "{{toolName}} timed out. {{message}} Want me to retry, or simplify the request?"
|
|
2111
|
+
},
|
|
2112
|
+
{
|
|
2113
|
+
"recipeId": "generate_image.user_cancelled",
|
|
2114
|
+
"version": "1.0.0",
|
|
2115
|
+
"toolName": "generate_image",
|
|
2116
|
+
"errorCode": "USER_CANCELLED",
|
|
2117
|
+
"mode": "stopAndAsk",
|
|
2118
|
+
"maxRetries": 0,
|
|
2119
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2120
|
+
},
|
|
2121
|
+
{
|
|
2122
|
+
"recipeId": "edit_image.user_cancelled",
|
|
2123
|
+
"version": "1.0.0",
|
|
2124
|
+
"toolName": "edit_image",
|
|
2125
|
+
"errorCode": "USER_CANCELLED",
|
|
2126
|
+
"mode": "stopAndAsk",
|
|
2127
|
+
"maxRetries": 0,
|
|
2128
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2129
|
+
},
|
|
2130
|
+
{
|
|
2131
|
+
"recipeId": "restore_photo.user_cancelled",
|
|
2132
|
+
"version": "1.0.0",
|
|
2133
|
+
"toolName": "restore_photo",
|
|
2134
|
+
"errorCode": "USER_CANCELLED",
|
|
2135
|
+
"mode": "stopAndAsk",
|
|
2136
|
+
"maxRetries": 0,
|
|
2137
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2138
|
+
},
|
|
2139
|
+
{
|
|
2140
|
+
"recipeId": "apply_style.user_cancelled",
|
|
2141
|
+
"version": "1.0.0",
|
|
2142
|
+
"toolName": "apply_style",
|
|
2143
|
+
"errorCode": "USER_CANCELLED",
|
|
2144
|
+
"mode": "stopAndAsk",
|
|
2145
|
+
"maxRetries": 0,
|
|
2146
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
"recipeId": "refine_result.user_cancelled",
|
|
2150
|
+
"version": "1.0.0",
|
|
2151
|
+
"toolName": "refine_result",
|
|
2152
|
+
"errorCode": "USER_CANCELLED",
|
|
2153
|
+
"mode": "stopAndAsk",
|
|
2154
|
+
"maxRetries": 0,
|
|
2155
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2156
|
+
},
|
|
2157
|
+
{
|
|
2158
|
+
"recipeId": "animate_photo.user_cancelled",
|
|
2159
|
+
"version": "1.0.0",
|
|
2160
|
+
"toolName": "animate_photo",
|
|
2161
|
+
"errorCode": "USER_CANCELLED",
|
|
2162
|
+
"mode": "stopAndAsk",
|
|
2163
|
+
"maxRetries": 0,
|
|
2164
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2165
|
+
},
|
|
2166
|
+
{
|
|
2167
|
+
"recipeId": "change_angle.user_cancelled",
|
|
2168
|
+
"version": "1.0.0",
|
|
2169
|
+
"toolName": "change_angle",
|
|
2170
|
+
"errorCode": "USER_CANCELLED",
|
|
2171
|
+
"mode": "stopAndAsk",
|
|
2172
|
+
"maxRetries": 0,
|
|
2173
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2174
|
+
},
|
|
2175
|
+
{
|
|
2176
|
+
"recipeId": "generate_video.user_cancelled",
|
|
2177
|
+
"version": "1.0.0",
|
|
2178
|
+
"toolName": "generate_video",
|
|
2179
|
+
"errorCode": "USER_CANCELLED",
|
|
2180
|
+
"mode": "stopAndAsk",
|
|
2181
|
+
"maxRetries": 0,
|
|
2182
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
"recipeId": "sound_to_video.user_cancelled",
|
|
2186
|
+
"version": "1.0.0",
|
|
2187
|
+
"toolName": "sound_to_video",
|
|
2188
|
+
"errorCode": "USER_CANCELLED",
|
|
2189
|
+
"mode": "stopAndAsk",
|
|
2190
|
+
"maxRetries": 0,
|
|
2191
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
"recipeId": "video_to_video.user_cancelled",
|
|
2195
|
+
"version": "1.0.0",
|
|
2196
|
+
"toolName": "video_to_video",
|
|
2197
|
+
"errorCode": "USER_CANCELLED",
|
|
2198
|
+
"mode": "stopAndAsk",
|
|
2199
|
+
"maxRetries": 0,
|
|
2200
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2201
|
+
},
|
|
2202
|
+
{
|
|
2203
|
+
"recipeId": "generate_music.user_cancelled",
|
|
2204
|
+
"version": "1.0.0",
|
|
2205
|
+
"toolName": "generate_music",
|
|
2206
|
+
"errorCode": "USER_CANCELLED",
|
|
2207
|
+
"mode": "stopAndAsk",
|
|
2208
|
+
"maxRetries": 0,
|
|
2209
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
"recipeId": "extend_video.user_cancelled",
|
|
2213
|
+
"version": "1.0.0",
|
|
2214
|
+
"toolName": "extend_video",
|
|
2215
|
+
"errorCode": "USER_CANCELLED",
|
|
2216
|
+
"mode": "stopAndAsk",
|
|
2217
|
+
"maxRetries": 0,
|
|
2218
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2219
|
+
},
|
|
2220
|
+
{
|
|
2221
|
+
"recipeId": "replace_video_segment.user_cancelled",
|
|
2222
|
+
"version": "1.0.0",
|
|
2223
|
+
"toolName": "replace_video_segment",
|
|
2224
|
+
"errorCode": "USER_CANCELLED",
|
|
2225
|
+
"mode": "stopAndAsk",
|
|
2226
|
+
"maxRetries": 0,
|
|
2227
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
"recipeId": "overlay_video.user_cancelled",
|
|
2231
|
+
"version": "1.0.0",
|
|
2232
|
+
"toolName": "overlay_video",
|
|
2233
|
+
"errorCode": "USER_CANCELLED",
|
|
2234
|
+
"mode": "stopAndAsk",
|
|
2235
|
+
"maxRetries": 0,
|
|
2236
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
"recipeId": "add_subtitles.user_cancelled",
|
|
2240
|
+
"version": "1.0.0",
|
|
2241
|
+
"toolName": "add_subtitles",
|
|
2242
|
+
"errorCode": "USER_CANCELLED",
|
|
2243
|
+
"mode": "stopAndAsk",
|
|
2244
|
+
"maxRetries": 0,
|
|
2245
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
"recipeId": "stitch_video.user_cancelled",
|
|
2249
|
+
"version": "1.0.0",
|
|
2250
|
+
"toolName": "stitch_video",
|
|
2251
|
+
"errorCode": "USER_CANCELLED",
|
|
2252
|
+
"mode": "stopAndAsk",
|
|
2253
|
+
"maxRetries": 0,
|
|
2254
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
"recipeId": "orbit_video.user_cancelled",
|
|
2258
|
+
"version": "1.0.0",
|
|
2259
|
+
"toolName": "orbit_video",
|
|
2260
|
+
"errorCode": "USER_CANCELLED",
|
|
2261
|
+
"mode": "stopAndAsk",
|
|
2262
|
+
"maxRetries": 0,
|
|
2263
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
"recipeId": "dance_montage.user_cancelled",
|
|
2267
|
+
"version": "1.0.0",
|
|
2268
|
+
"toolName": "dance_montage",
|
|
2269
|
+
"errorCode": "USER_CANCELLED",
|
|
2270
|
+
"mode": "stopAndAsk",
|
|
2271
|
+
"maxRetries": 0,
|
|
2272
|
+
"repairNoteTemplate": "The {{toolName}} run was cancelled by the user. I will stop here unless you ask me to try again."
|
|
2273
|
+
}
|
|
2274
|
+
];
|
|
2275
|
+
const PHASE_5_PROMPT_CONTRACTS = [
|
|
2276
|
+
{
|
|
2277
|
+
"contractId": "restore_photo_v1",
|
|
2278
|
+
"version": "1.0.0",
|
|
2279
|
+
"toolName": "restore_photo",
|
|
2280
|
+
"baseDescription": "restore_photo edits or restores the ORIGINAL uploaded photograph. Use this for the first\nrestoration/edit on an upload, or when the user explicitly asks to start over from the\noriginal/source photo. Use refine_result for follow-up edits to an existing generated result.\n\nFor photos with people, front-load identity preservation before the restoration or edit.\nUse positive constraints such as preserve exact facial likeness, face structure, apparent\nage, pose, and composition. End with preservation of unmentioned details.\n\nUse Dynamic Prompt syntax only when the user explicitly asks to compare multiple restoration\napproaches. Default restoration batches should keep the same prompt and vary only by seed.",
|
|
2281
|
+
"parameterDocs": {
|
|
2282
|
+
"prompt": "Natural-language edit/restoration prompt. Start with identity preservation for people, then requested restoration/edit, then preserve unmentioned details.",
|
|
2283
|
+
"numberOfVariations": "Use 1 unless the user explicitly asks for multiple outputs or comparison options.",
|
|
2284
|
+
"scale": "Set only when the user asks to upscale, enlarge, or increase resolution.",
|
|
2285
|
+
"quality": "Omit unless the user explicitly asks for fast or high quality."
|
|
2286
|
+
}
|
|
2287
|
+
},
|
|
2288
|
+
{
|
|
2289
|
+
"contractId": "apply_style_v1",
|
|
2290
|
+
"version": "1.0.0",
|
|
2291
|
+
"toolName": "apply_style",
|
|
2292
|
+
"baseDescription": "apply_style transfers an artistic style, era, franchise, medium, or photographic look onto\nan uploaded or generated image. It automatically uses the latest result unless the user\nspecifies a result number or asks for the original upload.\n\nTransfer visual style only. Do not let a style reference override identity, pose, or\ncomposition unless the user asked for those changes. For people, state identity preservation\nbefore the style instructions and finish by preserving pose and composition.\n\nUse refine_result instead when the user wants a targeted non-style edit to an existing\nresult. Use restore_photo only when they explicitly want to restart from the original upload.",
|
|
2293
|
+
"parameterDocs": {
|
|
2294
|
+
"prompt": "Style-transfer prompt. Name the style/era/artist/franchise and preserve identity, pose, and composition for people.",
|
|
2295
|
+
"sourceImageIndex": "Omit for the latest result. Use -1 only when the user explicitly says original/source upload.",
|
|
2296
|
+
"scale": "Set only when the user asks to upscale, enlarge, or increase resolution."
|
|
2297
|
+
}
|
|
2298
|
+
},
|
|
2299
|
+
{
|
|
2300
|
+
"contractId": "refine_result_v1",
|
|
2301
|
+
"version": "1.0.0",
|
|
2302
|
+
"toolName": "refine_result",
|
|
2303
|
+
"baseDescription": "refine_result is the default follow-up image-edit tool after generated results exist. Use it\nfor targeted changes to a prior result: color, brightness, sharpening, background changes,\nobject edits, further restoration, or small creative adjustments.\n\nDescribe only the delta. The source image already contains the subject, composition, and\nmost details. Do not rewrite the whole image unless the user asks for a broad transformation.\nFor people, front-load exact identity preservation and end with preserving all unmentioned\ndetails.\n\nUse restore_photo instead only when the user explicitly asks to start over from the original\nupload. Use sourceImageIndex only when the user names a specific result.",
|
|
2304
|
+
"parameterDocs": {
|
|
2305
|
+
"prompt": "Targeted delta instruction. Preserve identity first for people, then specify the change, then preserve everything else.",
|
|
2306
|
+
"sourceImageIndex": "Omit for the latest relevant result unless the user names a specific image number.",
|
|
2307
|
+
"numberOfVariations": "Use 1 unless the user asks to compare multiple refinement options."
|
|
2308
|
+
}
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
"contractId": "orbit_video_v1",
|
|
2312
|
+
"version": "1.0.0",
|
|
2313
|
+
"toolName": "orbit_video",
|
|
2314
|
+
"baseDescription": "orbit_video is a self-contained pipeline that handles angle generation, video transitions,\nand stitching internally. If the user uploaded an image, call orbit_video directly — it uses\nthe upload as the front view. If no image exists yet, generate ONE front-view image first,\nthen call orbit_video. Never pre-generate multiple angles or variations for orbit_video.\n\nORBIT DIALOGUE: When the user wants spoken dialogue in an orbit video, ALWAYS use the\ndialogue parameter (NOT prompt). Dialogue goes in ONLY the specified segment — put\nmotion/foley in prompt. If the user says \"only in the first segment\" or \"just at the start\",\nset dialogueSegment=0 (default). Never put dialogue text in the prompt parameter — it will\nbe duplicated across all segments.\n\nORBIT ANGLES: Do NOT send the angles parameter for standard 360° orbits — omit it entirely.\nThe default (right side view, back view, left side view at 90° increments) is correct for all\nnormal orbit requests. Only send angles when the user explicitly asks for specific azimuth\npositions (e.g. \"show me from the front-right and back-left only\") or a partial orbit.\n\nORBIT DIALOGUE UPDATE: For dialogue in multiple/every orbit segment, before every 90-degree\nturn, or with per-turn sequence numbers, use the dialogues array instead of the single dialogue\nparameter. Default 360-degree orbit has 4 transitions, so provide 4 short lines in order; leave\nprompt for subject, action, ambient audio, and foley only. Preserve the real names from the\nrequest/prior result; never invent placeholder speaker tags. For a couple/persona request\nphrased as \"us\", \"we\", or \"my wife and I\", each per-turn line should make the named people\nspeak together. When the user picks a generated image by 1-based number (\"number 3\",\n\"use #3\"), pass sourceImageIndex as that number minus one (number 3 -> sourceImageIndex=2)\ninstead of omitting it.",
|
|
2315
|
+
"parameterDocs": {
|
|
2316
|
+
"dialogue": "Spoken dialogue for the first/default orbit segment. Do NOT put dialogue in prompt — it repeats across all segments.",
|
|
2317
|
+
"dialogues": "Per-segment dialogue lines array. Use for multi-segment dialogue (4 lines for full 360° orbit).",
|
|
2318
|
+
"dialogueSegment": "Which orbit segment gets the dialogue (0-based). Default 0 = first segment.",
|
|
2319
|
+
"angles": "Omit for standard 360° orbits. Only set for explicit azimuth positions or partial orbits.",
|
|
2320
|
+
"sourceImageIndex": "Use uploaded image if present. If user picks by 1-based number, subtract 1."
|
|
2321
|
+
}
|
|
2322
|
+
},
|
|
2323
|
+
{
|
|
2324
|
+
"contractId": "animate_photo_v1",
|
|
2325
|
+
"version": "1.0.0",
|
|
2326
|
+
"toolName": "animate_photo",
|
|
2327
|
+
"baseDescription": "animate_photo produces video from one or more source images using LTX 2.3.\n\nVIDEO PROMPT QUOTING: In video prompts, ONLY use double quotes for spoken dialogue.\nSpeaker tags are allowed outside the quotes for screenplay-style dialogue, e.g.\nCHARACTER: \"We made it.\" Never put on-screen text, overlay text, titles, captions, signs,\nwatermarks, or any visual text in quotes — describe them without quotes (e.g. bold white text\nreading CONGRATULATIONS overlays the lower third). Quotes signal speech to the model;\nquoting non-speech text confuses audio generation.\n\nDIALOGUE DURATION: Spoken dialogue in video prompts must fit the clip duration. Estimate\nat 2.5 words per second for natural cinematic delivery, plus ~1 second per acting beat\n(pauses, gestures, glances between lines). If the user did NOT explicitly request a specific\nduration (using default 5s), extend the duration to fit the dialogue (max 20s). If the user\nexplicitly requested a specific duration, condense the dialogue to fit while preserving meaning.\nAlways check: total dialogue words ÷ 2.5 + beat count ≤ clip duration.\n\nLATEST GENERATED IMAGE FOLLOW-UP: When the newest user turn asks to animate, make a video,\nor make a clip from a generated image/result (for example \"the apple\", \"this one\",\n\"the latest image\"), use animate_photo with that latest generated image. Do not inherit an\nolder Seedance model, resolution, or duration from an unrelated prior turn unless the newest\nuser turn explicitly says Seedance or confirms an immediately suggested Seedance video stage.\nLTX supports exact 2-20s durations, so honor requests like 3s exactly.\n\nWORD BUDGET PER CLIP: The handler REJECTS clips whose spoken dialogue exceeds the budget\n— there is NO auto-trim, so plan dialogue lengths up-front. Hard maximum is 3.75 spoken\nwords per second. Ceilings: 5s = 18 words, 6s = 22 words, 8s = 30 words, 10s = 37 words,\n15s = 56 words, 20s = 75 words. Aim below these ceilings. If a scene's dialogue won't fit,\ntighten the lines, raise the per-clip duration, or split into two segments — do NOT submit\nand hope it works. Spoken words inside double quotes count toward the budget; speaker tags\nand visual/action prose are free.\n\nBATCH VIDEO PER-CLIP DURATION: For a multi-segment animate_photo batch\n(sourceImageIndices + prompts) when the user states a TOTAL video length but NO per-clip\nlength, target 15 seconds per clip when dialogue is involved, and pass that duration\nexplicitly. Example: 60s total → 4 segments × 15s, NOT 6×10s or 12×5s. There is NO 3-clip\nbatch cap: sourceImageIndices supports up to 16 clips, so never split one planned batch into\n\"first 3\" and \"remaining clips\" calls. Do NOT split a planned 15s dialogue scene into multiple\nshorter clips just because a retry complains about word budget; keep duration=15 and tighten\nthe line. Use 5s clips only for single short motion beats or one very short spoken phrase.\nIf the user explicitly specifies a per-clip duration, honor that instead.\n\nN-VERSIONS-OF-A-VIDEO PATTERN: NEVER call animate_photo N times sequentially — ALWAYS\nuse sourceImageIndices in ONE call so all N projects run in parallel. Two flavors:\n(A) SHARED CONTENT — one edit_image/generate_image call with numberOfVariations=N + {|}\nDynamic Prompts to make N distinct source images, then ONE animate_photo call with\nsourceImageIndices=[start..start+N-1] and a single shared prompt.\n(B) PER-CLIP CONTENT — when each clip has DIFFERENT dialogue, jokes, narration, or motion,\npass BOTH sourceImageIndices AND prompts (array of N strings, one per clip) in the SAME\nsingle animate_photo call. The top-level prompt is still required — pass a brief batch summary.\n\nCRITICAL: sourceImageIndices values MUST be read from the latest edit_image/generate_image\ntool result's startIndex field — if startIndex=3 and 4 images were generated, pass\nsourceImageIndices=[3,4,5,6], NOT [0,1,2,3]. Negative indices refer to uploaded images:\n-1 first upload, -2 second upload, -3 third upload. Use repeated -1 entries only when\nintentionally reusing the primary uploaded image. When prompts is supplied, prompts.length\nMUST equal sourceImageIndices.length.\n\nSEEDANCE UPLOADED STORYBOARD DEFAULT: If the user uploaded a storyboard, shot sheet,\nor visual trailer board and asks to make a trailer/video/movie/clip from it, do NOT use\nanimate_photo on the board image and do NOT split it into four LTX clips. Use generate_video\nwith Seedance referenceImageIndices for one continuous clip unless the user explicitly asks\nfor separate LTX clips or first-frame/last-frame animation.\n\nSCREENPLAY / STORYBOARD ANIMATE RULE: For full storyboard projects, use one\nanimate_photo batch with sourceImageIndices + prompts so each clip keeps its own exact\nscene text, stable cast anchors, and screenplay-style speaker-tagged dialogue, and all video\nclips render in parallel. Every speaking clip's video prompt must include that clip's actual\nquoted dialogue, not placeholders such as \"while speaking\", \"dialogue begins\", \"explaining\",\nor \"final line lands\". If each generated scene keyframe should be both the first and last frame\nof its own stitched segment, call animate_photo with sourceImageIndices=[start..end],\nframeRole=\"both\", prompts=[...], and OMIT endImageIndex/endImageIndices so the handler\nuses each source as its own end frame.\n\nUPLOADED REFERENCE LOOPED SKITS: When the user supplies one uploaded reference image and\nasks for several scripted/storyboard/dialogue segments to reuse that same image as BOTH the\nfirst frame and last frame of each segment before stitching, do it in ONE animate_photo call:\nsourceImageIndices=[-1,-1,...], frameRole=\"both\", endImageIndex=-1 (or matching\nendImageIndices=[-1,-1,...]), duration equal to the requested per-segment duration, and\nprompts=[one full scene prompt per segment]. Each prompt must preserve the exact screenplay\nspeaker tags and quoted dialogue from that scene, e.g. HOST: \"...\" GUEST: \"...\". Do not\ndrop speaker tags, convert them to generic narration, omit the last-frame contract, analyze\nthe image first, generate new keyframes first, or split the batch into serial calls. After\nthe single animate_photo batch completes, call stitch_video with the returned video indices.\n\nFor adjacent transition chains: N images create N-1 clips — call animate_photo with\nframeRole=\"both\", sourceImageIndices=[start..end-1], endImageIndices=[start+1..end],\nprompts=[one transition prompt per adjacent pair], then stitch_video. If 5 uploaded images\nare the keyframe sequence, use sourceImageIndices=[-1,-2,-3,-4],\nendImageIndices=[-2,-3,-4,-5], frameRole=\"both\", prompts length 4, then stitch_video.\nDo NOT set endImageIndex=-1 in generated-keyframe patterns — that means every clip ends\non the primary uploaded image.\n\nUPLOADED FIRST-FRAME/LAST-FRAME TRANSITION CHAINS: If the user uploads multiple images\nand asks for a video that transitions from image to image, changes country/version every\nN seconds, or says to use first-frame/last-frame for each pair, call animate_photo directly.\nDo not call edit_image, generate_image, analyze_image, or map_assets_for_model first — the\nuploaded images are already the keyframes. For N uploaded images, create N-1 adjacent clips\nunless the user explicitly asks for a loop back to the first image. Use per-clip duration\nfrom \"every N seconds\" when present; otherwise divide the requested total by the number of\nadjacent clips. After animate_photo returns the batch videos, always call stitch_video with\nthose video indices before finalizing.",
|
|
2328
|
+
"parameterDocs": {
|
|
2329
|
+
"sourceImageIndices": "Batch source image indices. Read startIndex from prior generate_image/edit_image result. Negative = uploaded images (-1 = first upload).",
|
|
2330
|
+
"prompts": "Per-clip prompt array. Length MUST equal sourceImageIndices.length when both are set.",
|
|
2331
|
+
"duration": "Per-clip duration in seconds. Target 15s when dialogue is involved and total length is given without per-clip spec.",
|
|
2332
|
+
"frameRole": "Set to \"both\" for first+last frame transitions using sourceImageIndices + endImageIndices.",
|
|
2333
|
+
"endImageIndices": "End frames for adjacent-chain transitions. N images → N-1 clips."
|
|
2334
|
+
}
|
|
2335
|
+
},
|
|
2336
|
+
{
|
|
2337
|
+
"contractId": "change_angle_v1",
|
|
2338
|
+
"version": "1.0.0",
|
|
2339
|
+
"toolName": "change_angle",
|
|
2340
|
+
"baseDescription": "change_angle creates a new still image from a different camera perspective. Use when the\nuser asks for a left/right/back/three-quarter/front view, elevated/low-angle view, close-up,\nmedium shot, or wide shot of an existing image subject.\n\nThe description must be exactly one azimuth, one elevation, and one distance phrase in the\ntool schema format. Default unspecified elevation to eye-level shot and distance to medium\nshot. Use the latest result unless the user specifies another source or says original.\n\nUse orbit_video for a full video orbit. Do not pre-generate angles for orbit_video; that tool\nowns its own angle pipeline.",
|
|
2341
|
+
"parameterDocs": {
|
|
2342
|
+
"description": "Exact format: \"[azimuth] [elevation] [distance]\". Pick one value from each schema category.",
|
|
2343
|
+
"sourceImageIndex": "Omit for latest result. Use -1 only when the user explicitly says original/source upload.",
|
|
2344
|
+
"loraStrength": "Omit unless the user explicitly asks to control the strength of the angle change."
|
|
2345
|
+
}
|
|
2346
|
+
},
|
|
2347
|
+
{
|
|
2348
|
+
"contractId": "generate_video_v1",
|
|
2349
|
+
"version": "1.1.0",
|
|
2350
|
+
"toolName": "generate_video",
|
|
2351
|
+
"baseDescription": "generate_video produces text-to-video clips and Seedance multimodal reference videos.\nUse for text-only video generation with no source image input. For Seedance, also use this\ntool when uploaded/generated images, videos, or audio are loose references. Use animate_photo\nonly when a non-Seedance source image must become the first frame of an LTX/WAN animation.\n\nSEEDANCE UPLOADED STORYBOARD DEFAULT: When the user uploads a storyboard, shot sheet,\nmood board, or trailer concept image and asks to make a movie trailer/video/clip from it,\ndefault to one Seedance generate_video call with referenceImageIndices=[-1]. Do not first\nextract panels with edit_image, do not generate replacement keyframes, and do not make four\nseparate LTX animate_photo clips unless the user explicitly asks for separate clips or LTX.\nUse seedance2 when premium Spark access is available; if premium access is unavailable,\nexplain the limitation or use the best non-Seedance fallback the user accepts.\n\nSTORYTELLING / COMMERCIAL / TRAILER PROMPTS: For creative video requests, turn the brief\ninto timed, causally connected visual beats before writing the final prompt. Default social\nvideo is 15s 9:16 with a strong first 1-2s, visible escalation, payoff, and brand/CTA/final\nimage. Commercials should show audience desire/problem, transformation, proof/benefit, and\nCTA. Trailers should follow hook → world → disruption → escalation → reveal → title/CTA.\nEvery beat must be generatable: subject, setting, action, camera, lighting, audio, and text\nrole where relevant. Avoid vague \"cinematic\" filler, feature dumps, and beautiful images with\nno visible change.\n\nVIDEO PROMPT QUOTING: ONLY use double quotes for spoken dialogue in video prompts. Never\nquote on-screen text, titles, captions, or visual text elements — describe them without\nquotes. Quotes signal speech to the model and confuse audio generation.\n\nSTORYBOARD TEXT: Structural headings, section numbers, slide titles, panel titles, and\ncaptions in storyboard references may become short audio-only narration/VO or\nkey-message beats, but they are not subtitles, title cards, lower thirds, or visible\noverlays unless the user explicitly asks for visible text, on-screen text, a title\ncard, subtitle, lower third, signage, or CTA. Keep narration as separate brief phrases\nwith pauses; do not concatenate storyboard labels into run-on voiceover.\n\nDIALOGUE DURATION: Spoken dialogue must fit the clip. Estimate 2.5 words per second\nnatural delivery plus ~1s per acting beat. Hard maximum 3.75 words/second.\nCheck: dialogue words ÷ 2.5 + beats ≤ duration. Do not submit oversized dialogue.\n\nLATEST USER DURATION WINS: In follow-up turns, use the newest duration the user states,\neven if a previous assistant message mentioned a longer script/runtime. For example, if\nhistory says \"the full script is 66 seconds\" but the user now says \"do a 30 second version\",\ngenerate the 30 second version. Do not ask a clarification question just because history\ncontains another duration; treat the latest user request as the override.\n\nSEEDANCE SHORT-DURATION LIMIT: Seedance supports 4-15s clips. If the user explicitly asks\nfor Seedance below 4s, do not silently round up. Ask whether they prefer a 4s Seedance clip\nor an exact-duration LTX clip. If the user did not explicitly ask for Seedance, choose the\nmodel/tool that can satisfy the requested duration exactly.",
|
|
2352
|
+
"parameterDocs": {
|
|
2353
|
+
"prompt": "Video prompt. Use double quotes ONLY for spoken dialogue. Describe visual text without quotes.",
|
|
2354
|
+
"duration": "Clip duration in seconds. Plan dialogue word count against the 3.75 words/second ceiling."
|
|
2355
|
+
}
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
"contractId": "edit_image_v1",
|
|
2359
|
+
"version": "1.0.0",
|
|
2360
|
+
"toolName": "edit_image",
|
|
2361
|
+
"baseDescription": "edit_image applies instruction-based edits to uploaded or generated images. Use when\nuploaded or reference images must guide identity or likeness.\n\nImage-to-Image prompt order: [IDENTITY LOCK] → [REQUESTED EDIT] → [REFERENCE ROLE\nMAPPING] → [POSE/COMPOSITION] → [STYLE] → [LIGHTING/REALISM] → [PRESERVE ALL\nUNMENTIONED DETAILS]. GOLDEN RULE: When editing a person, always state which image owns\nidentity — never leave identity ambiguous. Describe only the DELTA — what changes. Don't\nrewrite the entire image; the base image already contains most of the truth. Default to minimal\nchange. For multi-image edits, assign ONE primary role per reference image (identity, pose,\noutfit, style, environment). Never let a style/pose/clothing reference silently override the face.\nUse positive constraints — \"preserve exact facial likeness, face structure, eye shape, nose\nshape, mouth shape, jawline, skin tone, hairline, apparent age, and overall recognizability\"\n— not vague negatives like \"don't mess up the face\".\n\nUPLOADED IMAGE VARIANT SETS: When the user supplies a photo/portrait/reference image and\nasks for N distinct generated images deriving from that source while changing paired\nper-output details, call edit_image exactly once with sourceImageIndex=-1,\nnumberOfVariations=N, and ONE Dynamic Prompt branch with N complete options. Each option\nmust be a full concrete image prompt for one output, including the uploaded subject/reference\nanchor, requested pose or placement preservation, the specific changed appearance/style/role,\nclothing or surface details when relevant, setting/background, and any requested label text or\nvisual symbol. If one option is a remade original/preserved source and the rest are themed\nvariants, the original option must explicitly say to preserve the original clothing/wardrobe/outfit\nand background/setting, plus any requested added label, flag, logo, symbol, or prop.\nDo not call generate_image, analyze_image, or multiple serial edit_image calls first.\n\nSELECTION-GATED IMAGE STAGES: If the user asks for N image options and says they will pick\none before a later dance/video/animation, call edit_image exactly once with numberOfVariations=N\nand one Dynamic Prompt branch. After images are created, stop and ask the user to choose;\ndo not call dance_montage, animate_photo, or generate_video until they select.\n\nMULTI-PERSONA (COMBINED): When multiple personas must appear in the SAME scene, make\nONE edit_image call with ALL persona faces in one prompt and DO NOT pass personaName.\nPer-persona splits (one call each with personaName set) are RARE — only when the user\nexplicitly asks for solo images of each person individually.\n\nSTORYBOARD IMAGE BATCH RULE: When rendering scene keyframes from a screenplay/storyboard,\nnumberOfVariations is only the count; the prompt MUST be one Dynamic Prompt branch with one\nfull keyframe prompt per scene:\n{scene 1 full keyframe prompt|scene 2 full keyframe prompt|...|scene N full keyframe prompt}.\nNEVER set numberOfVariations=N with only the first scene prompt — that creates N versions of\nscene 1. For full project requests, one edit_image batch for all scene keyframes, then one\nanimate_photo batch for all video clips in parallel.\nException: if the storyboard/shot sheet is already uploaded and the user asks to make a\ntrailer/video/movie/clip from that uploaded board, do not extract panels or redraw keyframes.\nUse generate_video with Seedance references for one continuous clip unless the user explicitly\nasks for separate image keyframes or a storyboard sheet output.\n\nDIRECT UPLOADED GPT IMAGE 2 STORYBOARD SHEETS: If the user uploaded reference images and\nasks for one finished GPT Image 2 storyboard/keyframe sheet now, call edit_image directly\nwith sourceImageIndex=-1, model=\"gpt-image-2\", numberOfVariations=1, and the requested\ncanvas/aspect settings. If the user did not explicitly specify a storyboard page/canvas/sheet\nshape, default the GPT Image 2 storyboard sheet itself to landscape 2560x1440 while keeping\nthe individual scene-cell/frame areas at the target video aspect ratio. Do not call map_assets_for_model,\nanalyze_image, generate_image, or a separate planning tool first. The uploaded files are already\navailable as references; describe their roles plainly in the edit_image prompt and generate the\nsheet in that call.\n\nDO NOT USE edit_image FOR UPLOADED REFERENCE LOOPED VIDEO SEGMENTS: If the user says the\nsame uploaded image/reference should be reused as the first frame and last frame of each\nscripted segment/scene/clip before stitching, they are explicitly asking to animate the\nuploaded image, not to generate new storyboard keyframes. Do not call edit_image for that\nrequest. Call animate_photo once with repeated uploaded source indices and per-scene prompts.",
|
|
2362
|
+
"parameterDocs": {
|
|
2363
|
+
"sourceImageIndex": "Index of uploaded/generated image. Use -1 for the first uploaded image.",
|
|
2364
|
+
"numberOfVariations": "Number of output variants. When > 1, use a Dynamic Prompt branch with one complete prompt per output.",
|
|
2365
|
+
"prompt": "Edit instruction. Start with identity lock (who owns the face), then describe only the delta."
|
|
2366
|
+
}
|
|
2367
|
+
},
|
|
2368
|
+
{
|
|
2369
|
+
"contractId": "generate_image_v1",
|
|
2370
|
+
"version": "1.1.0",
|
|
2371
|
+
"toolName": "generate_image",
|
|
2372
|
+
"baseDescription": "generate_image creates images from text descriptions. Use for text-only image generation;\nuse edit_image when uploaded or reference images must guide identity/likeness.\nException: Z-image and Z-image Turbo image-to-image/enhancement requests use generate_image\nwith model=\"z-turbo\" or model=\"z-image\", sourceImageIndex=-1, and starting_image_strength;\ndo not route explicit Z-image Turbo uploaded-image enhancement to edit_image because\nedit_image does not expose Z-image models.\n\nFLUX.2 PROMPT ORDER: [SUBJECT] → [ATTRIBUTES] → [ACTION/POSE] → [CAMERA/FRAMING]\n→ [ENVIRONMENT] → [LIGHTING] → [STYLE/MEDIUM] → [MATERIALS/TEXTURES] →\n[SECONDARY DETAILS]. Always start with the main subject, never mood or atmosphere.\nUse concrete nouns and observable adjectives — \"soft overcast daylight\" not \"nice lighting\".\nGood defaults when user is underspecified: medium shot for portraits, wide shot for\nenvironments, eye-level angle, soft natural light for realism.\n\nDYNAMIC PROMPTS: When numberOfVariations > 1, use Dynamic Prompt syntax to make each\nvariation meaningfully different — not just seed-different. Syntax: {a|b|c} cycles\nsequentially, {@a|b|c} picks randomly, {~a|b} paired cycling across groups. Rules: (1) Vary\nONLY what the user left unspecified — lock in everything they specified. (2) Match option\ncount to numberOfVariations so every result is unique. (3) Briefly tell the user what you're\nvarying — never show raw {|} syntax. (4) Skip when: user wants consistency, prompt is fully\nspecified, user typed their own {|} syntax, or iterating on a specific result. (5) NEVER put\nthe count or the word \"versions\"/\"variations\" inside the prompt — the prompt always describes\na single image. The multiplicity comes ONLY from numberOfVariations + the {|} syntax.\nLINKED VARIANTS: when multiple attributes must stay paired per result, use ONE top-level\nDynamic Prompt branch with one complete self-contained prompt per output. Do NOT split\nlinked fields into separate Dynamic Prompt groups.\n\nSELECTION-GATED IMAGE STAGES: If the user asks for N image options and says they will pick\none before a later dance/video/animation, call generate_image once with numberOfVariations=N.\nAfter images are created, stop and ask the user to choose; do not call dance_montage,\nanimate_photo, or generate_video until they select.\n\nIMAGE→VIDEO DIMENSION RULE: When generating an image that will feed into a video tool\n(animate_photo, sound_to_video, etc.), the image MUST be generated at the SAME aspect\nratio and dimensions as the target video. Default video aspect ratio is 16:9 landscape —\npass aspectRatio=\"16:9\" (or the user's specified/reference ratio) so the source image\nmatches the video output. Never generate a square image for a widescreen video. Exception:\na composite GPT Image 2 storyboard/keyframe sheet for a later Seedance video is a board,\nnot a single source frame; unless the user explicitly specifies a storyboard page/canvas/sheet\nshape, default the sheet image itself to landscape 2560x1440 while each scene-cell/frame\narea preserves the target video aspect ratio.\n\nSTORYBOARD IMAGE BATCH RULE: When rendering scene keyframes from a screenplay/storyboard,\nnumberOfVariations is only the count; the prompt MUST be one Dynamic Prompt branch with one\nfull keyframe prompt per scene:\n{scene 1 full keyframe prompt|scene 2 full keyframe prompt|...|scene N full keyframe prompt}.\nNEVER set numberOfVariations=N with only the first scene prompt — that creates N versions of\nscene 1. For full project requests, one generate_image batch for all scene keyframes, then\none animate_photo batch for all video clips in parallel.\n\nSTORYTELLING / BRAND / SOCIAL IMAGE PROMPTS: If generating a storyboard, ad concept,\ntrailer sheet, meme, creator post, or provocative social concept, make the first frame or\npanel immediately legible. Preserve the user's requested tone and audience. Use concrete\ncomposition, persona, product/brand role, caption placement, readable required text, and a\nclear visual transformation or punchline. For provocative adult social content, keep subjects\nclearly adult and consensual, PG-13/non-explicit, and avoid minor-coded styling or school-coded\nsettings while still optimizing visual magnet, persona, caption bait, and replay/comment value.\n\nGPT IMAGE 2 STORYBOARD SHEET → SEEDANCE AUTO-PROCEED: If the user asks to run the whole\nGPT Image 2 storyboard/keyframe sheet plus Seedance workflow without approval, the FIRST\ngenerate_image call must create ONE composite storyboard/keyframe sheet, not loose concept\nart and not separate keyframes. Use model=\"gpt-image-2\", numberOfVariations=1, and a\ncompiled storyboard prompt that literally includes: \"Create exactly N sequential video\nstoryboard frames as one composite storyboard image\", \"Target final video aspect ratio: X\",\nand Audio/SFX directions in the scene notes. Unless the user explicitly specifies another\nstoryboard page/canvas/sheet shape, default the GPT Image 2 storyboard sheet itself to\nlandscape 2560x1440 / aspectRatio=\"2560x1440\", even when the final video cells are portrait\nor landscape. Preserve the requested final video aspect ratio for every frame area. After\nthat image completes, call generate_video once using the generated storyboard board as\n@Image1/referenceImageIndices=[0], with skipPromptProcessing=false only when the user\nexplicitly wants the storyboard text rewritten; otherwise preserve the compiled shot guide\nand use skipPromptProcessing=true, expandPrompt=false.\n\nDO NOT USE generate_image FOR UPLOADED REFERENCE LOOPED VIDEO SEGMENTS: If the user says\nthe same uploaded image/reference should be reused as the first frame and last frame of each\nscripted segment/scene/clip before stitching, they are explicitly asking to animate the\nuploaded image, not to generate new storyboard keyframes. Do not call generate_image for\nthat request. Call animate_photo once with repeated uploaded source indices and per-scene\nprompts.\n\nREUSING RESULTS: When the user asks to redo, retry, or revise (e.g., \"try a new version\",\n\"redo the video with X\"), reuse the existing source images — do NOT regenerate them unless\nthe user explicitly asks for new images or describes changes to the images themselves.\nReference the existing result indices from the prior generation. If unsure whether the user\nwants new images, ask — don't regenerate by default.",
|
|
2373
|
+
"parameterDocs": {
|
|
2374
|
+
"prompt": "Text description. Follow FLUX.2 prompt order: subject first. Use Dynamic Prompt syntax when numberOfVariations > 1.",
|
|
2375
|
+
"numberOfVariations": "Number of distinct outputs. Use Dynamic Prompt {|} syntax to vary one attribute per image. Never put the count in the prompt itself.",
|
|
2376
|
+
"aspectRatio": "For ordinary images feeding a video tool, set to match the target video aspect ratio. For composite GPT Image 2 storyboard sheets, default the sheet canvas to landscape 2560x1440 unless the user explicitly specifies a storyboard page/canvas/sheet shape; keep the target video ratio inside each frame area."
|
|
2377
|
+
}
|
|
2378
|
+
},
|
|
2379
|
+
{
|
|
2380
|
+
"contractId": "video_to_video_v1",
|
|
2381
|
+
"version": "1.0.0",
|
|
2382
|
+
"toolName": "video_to_video",
|
|
2383
|
+
"baseDescription": "video_to_video transforms an uploaded video. Use for uploaded-video restyling, enhancement,\nupscaling/remastering, motion transfer from video to image, subject replacement, edge/pose/\ndepth-guided restyle, or explicit Seedance V2V transforms.\n\nThis tool requires an uploaded video source. Do not use it for generated video indices. For\ngenerated or uploaded partial edits use replace_video_segment; for appended time use\nextend_video; for logos/text overlays use overlay_video; for stitching use stitch_video.\n\nChoose controlMode by intent. Use detailer for quality-only enhancement without restyling.\nUse seedance-v2v only when the user asks to transform/enhance/remaster an uploaded video\nwith Seedance. For detailer, describe the original scene plus quality terms, not new content.",
|
|
2384
|
+
"parameterDocs": {
|
|
2385
|
+
"prompt": "Describe the target appearance in present tense. For detailer, describe the original content plus quality qualifiers only.",
|
|
2386
|
+
"videoSourceIndex": "Uploaded video index. Omit when there is one uploaded video; use 0 for first uploaded video or -1 if using negative upload notation.",
|
|
2387
|
+
"controlMode": "Pick from intent: detailer for enhance, seedance-v2v for explicit Seedance V2V, canny/depth/pose for control-net restyles, animate-move/replace for WAN Animate.",
|
|
2388
|
+
"sourceImageIndex": "Required for animate-move and animate-replace. Ignored by canny, depth, and detailer.",
|
|
2389
|
+
"duration": "Set only when the user requests a different output length; otherwise let the tool match/cap the source duration."
|
|
2390
|
+
}
|
|
2391
|
+
},
|
|
2392
|
+
{
|
|
2393
|
+
"contractId": "extend_video_v1",
|
|
2394
|
+
"version": "1.0.0",
|
|
2395
|
+
"toolName": "extend_video",
|
|
2396
|
+
"baseDescription": "Use extend_video when a video already exists in the session — whether previously rendered OR\nuploaded — and the user asks to make it longer, add a segment, or append a bumper, outro,\nintro, tag, or sting to the end (or start). Do NOT call generate_video, animate_photo, or build\na new bumper from scratch via edit_image+animate_photo+stitch_video — those render fresh\nclips and either waste the previous render or ignore the uploaded base.\n\nFor uploaded base videos, set videoIndex to a negative number (-1 for first uploaded video).\nSet duration to the ADDITIONAL seconds (not the new total).\n\nTrigger phrases: \"make it longer\", \"extend the video\", \"add another N seconds\", \"continue the\nscene\", \"add a bumper/outro/intro/tag/sting to the end (or start)\".\n\nBoth extend_video and replace_video_segment auto-detect the base video's model (Seedance\nsource → Seedance continuation; LTX source → LTX continuation; Wan source → Wan continuation), so OMIT videoModel unless\nthe user explicitly demands a different model. This applies regardless of whether the prior\nrender came from generate_video, animate_photo, sound_to_video, or video_to_video.",
|
|
2397
|
+
"parameterDocs": {
|
|
2398
|
+
"videoIndex": "Index of the existing video. Use -1 for first uploaded video, non-negative for generated videos.",
|
|
2399
|
+
"duration": "ADDITIONAL seconds to add — not the new total length.",
|
|
2400
|
+
"videoModel": "Omit to auto-detect from source video. Only set if user explicitly requests a different model."
|
|
2401
|
+
}
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
"contractId": "replace_video_segment_v1",
|
|
2405
|
+
"version": "1.0.0",
|
|
2406
|
+
"toolName": "replace_video_segment",
|
|
2407
|
+
"baseDescription": "Use replace_video_segment when the user wants to regenerate a specific time range of an\nexisting video: \"regenerate from Xs to Ys\", \"redo the last N seconds\", \"swap out the middle\",\n\"fix the [start/middle/end] of the video\", or \"replace the [bumper/intro/outro/end card/\ntag/sting] at the [start/end] of the video\". Use explicit startSeconds and endSeconds; use\n-1 sentinels when exact base duration is unknown — the handler probes and resolves.\n\nWhen the replacement is already another uploaded or generated video clip, still use\nreplace_video_segment but pass replacementVideoIndex. Example: \"splice video 2 into video 1\nat 5s\" means videoIndex=-1, replacementVideoIndex=-2, startSeconds=5, endSeconds=5.\nUse endSeconds=startSeconds for insertion; use a wider endSeconds only when the user says to\nreplace/remove that base-video range. Do not use stitch_video for \"into the middle\"/\"insert\"\nrequests, because stitch_video only concatenates full clips end-to-end.\n\nFor time-sliced interleaving from existing videos — \"alternate 1s from each video\", \"weave\none-second clips from video 1 and video 2\", \"cut back and forth every N seconds\" — do NOT\nuse stitch_video and do NOT omit replacementVideoIndex. Start with the first requested video\nas the base, then call replace_video_segment once for each window that should come from the\nother video. Set replacementVideoIndex to that other existing video and set\nreplacementStartSeconds/replacementEndSeconds to the next source slice from that\nreplacement video. For ordinary\nalternation, preserve the base duration: set endSeconds=startSeconds+sliceDuration, not\nendSeconds=startSeconds insertion, unless the user explicitly asks to lengthen the output by\ninserting extra slices. Skip no-op windows that already come from the base video; only splice\nwindows that should come from a different source. Example for two 10s uploads alternating every 1s starting with video\n1: replace base windows 1..2, 3..4,\n5..6, 7..8, and 9..10 with slices 0..1, 1..2, 2..3, 3..4, and 4..5 from video 2. After\neach successful splice, target the newest composite video index for the next splice.\nThe -1 time sentinel applies only to base startSeconds/endSeconds when the base duration is\nunknown. Never use -1 for replacementStartSeconds or replacementEndSeconds; source windows\nmust use concrete non-negative seconds. For uploaded/generated videos with duration metadata,\nuse that known duration directly; do not call analyze_video just to learn the clip length for\nroutine alternating slices. Do not add a final tail splice with an unknown source end — stop at\nthe known clip duration or skip a no-op tail window.\n\nDo NOT call generate_video or animate_photo to re-render an existing video just to change\npart of it (the bumper, the intro, the end card, a single scene, the last few seconds, etc.).\nUse replace_video_segment — it preserves the unchanged portion, keeps the original audio\noutside the replaced window, and costs far less.\n\nAuto-detects the base video's model, so OMIT videoModel unless the user explicitly demands\na different model. Short requested windows are supported by rendering with model-specific\nhandles and trimming the rendered clip before splicing, so still pass the user's exact\nstartSeconds/endSeconds.",
|
|
2408
|
+
"parameterDocs": {
|
|
2409
|
+
"startSeconds": "Start of segment to replace in seconds. Use -1 sentinel if exact base duration is unknown.",
|
|
2410
|
+
"endSeconds": "End of segment to replace in seconds. Use the same value as startSeconds for insertion with replacementVideoIndex.",
|
|
2411
|
+
"replacementVideoIndex": "Existing uploaded/generated replacement clip. Use negative uploaded-video indices, e.g. -2 for the second uploaded video.",
|
|
2412
|
+
"replacementStartSeconds": "Optional start time inside replacementVideoIndex. Use with replacementEndSeconds for time-sliced interleaving. Must be concrete and >= 0; never use -1 here.",
|
|
2413
|
+
"replacementEndSeconds": "Optional end time inside replacementVideoIndex. Must be concrete, >= 0, and greater than replacementStartSeconds; never use -1 here.",
|
|
2414
|
+
"videoModel": "Omit to auto-detect from source. Only set if user explicitly requests a different model."
|
|
2415
|
+
}
|
|
2416
|
+
},
|
|
2417
|
+
{
|
|
2418
|
+
"contractId": "overlay_video_v1",
|
|
2419
|
+
"version": "1.0.0",
|
|
2420
|
+
"toolName": "overlay_video",
|
|
2421
|
+
"baseDescription": "Use overlay_video when the user wants to overlay/place/show a logo, text, caption, or\nwatermark ON TOP OF existing video frames.\n\nFor uploaded base videos, set sourceVideoIndex=-1; for generated videos, use their\nnon-negative video index. If the overlay should last only part of the video, set the overlay\nitem's startSeconds/endSeconds (e.g. a 2s middle overlay on a 20s video: ~9s to ~11s).\nNegative startSeconds/endSeconds are relative to the end of the base video, so startSeconds=-2\nwith omitted endSeconds means the last 2 seconds.\n\nWhen the user asks to replace a video time window with an uploaded still image, screenshot,\nphoto, frame, logo, or graphic, use an image overlay for that window with widthPct=100 and\nfit=\"cover\"; do not regenerate the video segment.\n\nDo NOT use overlay_video for intro/outro/bumper/end-card/start-card requests — those add or\nregenerate video time and should use extend_video or replace_video_segment. After a successful\noverlay_video call, finalize the turn; do not call overlay_video again just to tweak default\nsize or placement unless the user asks.",
|
|
2422
|
+
"parameterDocs": {
|
|
2423
|
+
"sourceVideoIndex": "Use -1 for first uploaded video, non-negative index for generated videos.",
|
|
2424
|
+
"overlays": "Use startSeconds/endSeconds for time windows. For still/screenshot replacement windows, use kind=\"image\", widthPct=100, fit=\"cover\"."
|
|
2425
|
+
}
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
"contractId": "add_subtitles_v1",
|
|
2429
|
+
"version": "1.0.0",
|
|
2430
|
+
"toolName": "add_subtitles",
|
|
2431
|
+
"baseDescription": "add_subtitles burns caller-supplied subtitles, captions, lyrics, or on-screen dialogue into\nan existing video. Use this when the user provides cue text/timing, pastes SRT/VTT, or asks\nto add known caption lines to a generated or uploaded video.\n\nDo not use auto_transcribe. Speech-to-text is not enabled; when the user has not supplied\nsubtitle text or timing, ask for the cue text/timing instead of calling this tool.\n\nException: if the user explicitly authorizes invented caption copy with language like\n\"make them up\", \"write captions\", \"invent subtitles\", or \"add funny captions\", create a few\nshort generic cue lines yourself and call add_subtitles. Do not ask for exact wording when\nthe user has asked you to author the captions.\n\nSplit subtitles into multiple short cues. Do not burn one paragraph across the whole clip.\nUse overlay_video instead for static title cards, labels, logos, watermarks, or non-timed text.",
|
|
2432
|
+
"parameterDocs": {
|
|
2433
|
+
"sourceVideoIndex": "Non-negative generated video index or negative uploaded video index. Omit/default to the latest relevant video.",
|
|
2434
|
+
"cues": "Multiple short cues with startSeconds, endSeconds, and text. Prefer 1.5-4 seconds per cue.",
|
|
2435
|
+
"srt": "Full SRT/VTT string. Provide either srt or cues, not both.",
|
|
2436
|
+
"auto_transcribe": "Do not set. Ask the user for subtitle text/timing until STT is available."
|
|
2437
|
+
}
|
|
2438
|
+
},
|
|
2439
|
+
{
|
|
2440
|
+
"contractId": "stitch_video_v1",
|
|
2441
|
+
"version": "1.0.0",
|
|
2442
|
+
"toolName": "stitch_video",
|
|
2443
|
+
"baseDescription": "stitch_video joins multiple video clips into one. Reference generated videos by their\n0-based video indices (from videoStartIndex in prior tool results). Reference uploaded\nvideos with negative indices in current UI order: -1 = first uploaded video, -2 = second, etc.\nWhen the user asks to stitch these/all uploaded videos and does not name a different order,\nuse the current UI order exactly: [-1,-2,...].\nIf the user explicitly names a different playback order, preserve that requested order exactly;\ndo not rewrite it back to UI order.\n\nNEVER tell the user to \"upload\" a video that was already generated in this conversation —\nstitch_video can reference them directly by index.\n\nWhen stitching results from a batch video tool, use ONLY the video indices actually returned\nby that tool. Do not infer the stitch list from the number of source images, keyframes, or\nstoryboard panels. Example: if 5 uploaded keyframes create 4 adjacent animate_photo\ntransition clips and the tool result returns videos at indices 0,1,2,3, call stitch_video\nwith videoIndices=[0,1,2,3] — never include index 4 unless a fifth video was returned.\n\nDo not use stitch_video for alternating/interleaved time slices such as \"alternate\n1s from each video\"; stitch_video joins whole clips end-to-end, while interleaving existing\nvideo slices belongs to repeated replace_video_segment calls with replacementVideoIndex and\nreplacementStartSeconds/replacementEndSeconds.\n\nNote: video_to_video requires an actual uploaded video file and cannot use generated video\nindices. Do not claim exact final runtime, dimensions, or aspect ratio after a tool finishes\nunless that value was explicitly requested by the user or explicitly returned by the tool.",
|
|
2444
|
+
"parameterDocs": {
|
|
2445
|
+
"videoIndices": "Ordered source video indices. Use non-negative generated-video indices from prior tool results; use negative uploaded-video indices in current UI order (-1 first uploaded video, -2 second, etc.)."
|
|
2446
|
+
}
|
|
2447
|
+
},
|
|
2448
|
+
{
|
|
2449
|
+
"contractId": "sound_to_video_v1",
|
|
2450
|
+
"version": "1.0.0",
|
|
2451
|
+
"toolName": "sound_to_video",
|
|
2452
|
+
"baseDescription": "sound_to_video creates audio-synced video from an audio source. Works with uploaded audio\nfiles (mp3, m4a, wav) OR previously generated music from generate_music (auto-detected).\n\nWhen the user asks to \"turn that song/music into a video\" after generate_music, use\nsound_to_video — it will automatically find the generated audio.\n\nFor music visualization (syncing video to a specific song or audio track), use the\ngenerate_music → sound_to_video pipeline. Do NOT use animate_photo or generate_video for\naudio-driven visualization.\n\nanimate_photo and generate_video produce audio natively via LTX 2.3 — never pre-generate\naudio for those tools. sound_to_video is only for when the audio IS the primary creative\ninput driving the video output.",
|
|
2453
|
+
"parameterDocs": {
|
|
2454
|
+
"audioSource": "Uploaded audio file or reference to a prior generate_music result. Auto-detected when omitted after generate_music."
|
|
2455
|
+
}
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
"contractId": "dance_montage_v1",
|
|
2459
|
+
"version": "1.0.0",
|
|
2460
|
+
"toolName": "dance_montage",
|
|
2461
|
+
"baseDescription": "dance_montage creates dance videos from an uploaded photo. Dance video requests from an\nuploaded photo go directly to dance_montage; do not call edit_image/generate_image first\nunless the user explicitly asks for a new look, outfit, generated character/image, variations,\nor persona identity prep.\n\nDance preset/vibe words like \"Barbie\", \"Metric\", \"Black Sheep\", \"Rasputin\", or \"TikTok\" are\nnot image-prep requests by themselves.\n\nSELECTION-GATED DANCE FLOW: If the user asks for N image options and says they will pick\none before the dance video, generate the image options first (generate_image or edit_image\nwith numberOfVariations=N), then stop and wait for the user to choose before calling\ndance_montage.",
|
|
2462
|
+
"parameterDocs": {
|
|
2463
|
+
"sourceImageIndex": "Use the uploaded photo directly (-1). Only use a generated image index if the user explicitly requested a new image first."
|
|
2464
|
+
}
|
|
2465
|
+
},
|
|
2466
|
+
{
|
|
2467
|
+
"contractId": "generate_music_v1",
|
|
2468
|
+
"version": "1.1.0",
|
|
2469
|
+
"toolName": "generate_music",
|
|
2470
|
+
"baseDescription": "generate_music creates music tracks with optional lyrics, BPM, key, and style control.\n\nMUSIC CREATIVE BRIEF: Identify purpose before composing: full song, short social hook,\njingle, trailer score, background underscore, sonic logo, music video cue, or lyric video.\nDefine genre, mood, tempo/BPM, energy curve, instrumentation, vocal style, lyrical point of\nview, hook phrase, section structure, and production notes. Lyrics should be original,\nsingable, sectioned, rhythmically clear, and have a memorable hook. Brand music should make\nthe brand easier to remember without stuffing the name into every line.\n\nFor music visualization (syncing the generated track to video), chain generate_music →\nsound_to_video. Do NOT use animate_photo or generate_video for audio-driven visualization.\n\nAfter generate_music, if the user asks to \"turn that song into a video\" or similar, call\nsound_to_video next — it auto-detects the latest generated music track.",
|
|
2471
|
+
"parameterDocs": {}
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
"contractId": "analyze_image_v1",
|
|
2475
|
+
"version": "1.0.0",
|
|
2476
|
+
"toolName": "analyze_image",
|
|
2477
|
+
"baseDescription": "analyze_image answers questions about image content, OCR, objects, style, documents, or\ncomparisons. It does not generate or modify media.\n\nUse only when the user asks to inspect, describe, read, compare, or reason about an image.\nDo not insert it as a quality-control step inside generation workflows unless the user\nexplicitly asks for analysis before continuing.\n\nFor uploaded images, negative indices refer to the upload order. For compare mode, provide\nboth sourceImageIndex and compareImageIndex.",
|
|
2478
|
+
"parameterDocs": {
|
|
2479
|
+
"query": "Specific question/request about the image. Preserve the user question directly.",
|
|
2480
|
+
"analysisType": "Pick describe, ocr, objects, document, compare, or general based on the user request.",
|
|
2481
|
+
"sourceImageIndex": "Omit to auto-select latest result or original upload. Use -1 for first uploaded image.",
|
|
2482
|
+
"compareImageIndex": "Second image for compare mode. Use negative uploaded-image indices when comparing uploads."
|
|
2483
|
+
}
|
|
2484
|
+
},
|
|
2485
|
+
{
|
|
2486
|
+
"contractId": "analyze_video_v1",
|
|
2487
|
+
"version": "1.0.0",
|
|
2488
|
+
"toolName": "analyze_video",
|
|
2489
|
+
"baseDescription": "analyze_video uses sampled frames to answer questions about an uploaded or generated video:\nsummary, timeline, visual scene description, action breakdown, or visible text.\n\nUse only when the user asks to inspect or understand a video. Do not call it as automatic\nverification after generation, and do not use it before stitch_video when the requested\ngeneration pipeline should continue.\n\nThis is visual sampled-frame analysis only. It does not transcribe audio and does not inspect\nevery frame.",
|
|
2490
|
+
"parameterDocs": {
|
|
2491
|
+
"query": "Specific question/request about the video. State whether summary, timeline, OCR, scene, or action detail is needed.",
|
|
2492
|
+
"analysisType": "Pick summary, timeline, scene, action, ocr, or general based on the user request.",
|
|
2493
|
+
"sourceVideoIndex": "Generated video index or negative uploaded-video index. Omit to auto-select latest generated video or first upload."
|
|
2494
|
+
}
|
|
2495
|
+
},
|
|
2496
|
+
{
|
|
2497
|
+
"contractId": "set_content_filter_v1",
|
|
2498
|
+
"version": "1.0.0",
|
|
2499
|
+
"toolName": "set_content_filter",
|
|
2500
|
+
"baseDescription": "set_content_filter enables or disables the Safe Content Filter. Call only when the user\nexplicitly asks to change this setting. Do not toggle it as part of ordinary generation.\n\nIf disabling requires host-side confirmation, let the handler surface that permission flow;\ndo not claim the setting changed unless the tool result says it did.",
|
|
2501
|
+
"parameterDocs": {
|
|
2502
|
+
"enabled": "true to enable the filter, false to disable it. Only set from explicit user intent."
|
|
2503
|
+
}
|
|
2504
|
+
},
|
|
2505
|
+
{
|
|
2506
|
+
"contractId": "extract_metadata_v1",
|
|
2507
|
+
"version": "1.0.0",
|
|
2508
|
+
"toolName": "extract_metadata",
|
|
2509
|
+
"baseDescription": "extract_metadata reads prompt/model/settings metadata from an uploaded media file. Use when\nthe user asks what prompt, model, seed, dimensions, or generation settings are embedded in\nan uploaded file, or when they ask to recreate/remix an uploaded file from its metadata.\n\nThis indexes uploaded files only, not prior generated results already known to chat. Do not\ncall it for ordinary visual analysis; use analyze_image or analyze_video for content.",
|
|
2510
|
+
"parameterDocs": {
|
|
2511
|
+
"file_index": "0-based uploaded-file index. Omit for the first uploaded file."
|
|
2512
|
+
}
|
|
2513
|
+
},
|
|
2514
|
+
{
|
|
2515
|
+
"contractId": "resolve_personas_v1",
|
|
2516
|
+
"version": "1.0.0",
|
|
2517
|
+
"toolName": "resolve_personas",
|
|
2518
|
+
"baseDescription": "resolve_personas is the required first step when the user explicitly names a saved Persona\nor says to use a Persona Image, Persona reference photo, Persona Voice, registered voice,\nor voice clone. Do not answer in prose, ask a follow-up, or finalize before calling this\ntool when a listed Persona name is present.\n\nDIRECT PERSONA IMAGE / VOICE VIDEO: If the user says to use the Persona image/reference\ndirectly/originally, call resolve_personas first, then call animate_photo using the injected\npersona photo as an uploaded image index. For one named Persona, use sourceImageIndex=-1\nor sourceImageIndices=[-1,...] for a multi-clip batch. If Persona Voice was explicitly\nrequested, set voicePersonaName to the exact resolved Persona name and use an LTX model.\nDo not call generate_video for Persona image/voice videos. Do not generate a new image first\nwhen the user explicitly requested the existing Persona image directly.\n\nMULTI-CLIP PERSONA BATCHES: If the user asks for several separate clips from the same\nPersona Image, make one animate_photo call after resolve_personas with repeated persona\nsource indices, one prompt per clip, and the requested per-clip duration. If the user asks\nto stitch the clips, call stitch_video with the returned video indices after animate_photo.",
|
|
2519
|
+
"parameterDocs": {
|
|
2520
|
+
"names": "Persona names to load. Use the exact listed Persona name; call this before any Persona image/voice video or image generation."
|
|
2521
|
+
}
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
"contractId": "manage_memory_v1",
|
|
2525
|
+
"version": "1.0.0",
|
|
2526
|
+
"toolName": "manage_memory",
|
|
2527
|
+
"baseDescription": "manage_memory reads, writes, or deletes persistent user preferences and facts. Use write\nonly when the user states a durable preference/fact or explicitly asks you to remember\nsomething. Use read when persistent preferences are relevant before generation.\n\nDo not save transient one-off creative instructions as memory. Delete/clear actions require\nexplicit user intent; never infer memory deletion from vague dissatisfaction.",
|
|
2528
|
+
"parameterDocs": {
|
|
2529
|
+
"action": "read, write, or delete. Use write only for durable preferences/facts; delete only on explicit request.",
|
|
2530
|
+
"key": "Stable concise memory key. Required for write/delete.",
|
|
2531
|
+
"value": "Concise durable value. Required for write.",
|
|
2532
|
+
"category": "preference for style/defaults, fact for user facts, context for project context."
|
|
2533
|
+
}
|
|
2534
|
+
},
|
|
2535
|
+
{
|
|
2536
|
+
"contractId": "create_asset_manifest_v1",
|
|
2537
|
+
"version": "1.0.0",
|
|
2538
|
+
"toolName": "create_asset_manifest",
|
|
2539
|
+
"baseDescription": "create_asset_manifest resets and seeds the session asset manifest with stable asset_id\nrecords. Use at the start of workflows with multiple named uploaded/generated assets that\nwill need durable references across later prompts.\n\nDo not call this just to use ordinary uploaded images in edit_image/generate_image; those\ntools can already reference uploads. Resetting discards prior manifest entries.",
|
|
2540
|
+
"parameterDocs": {
|
|
2541
|
+
"assets": "Ordered assets to register. Each needs user_label and type; include must_preserve/avoid only when they matter later."
|
|
2542
|
+
}
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
"contractId": "inspect_asset_v1",
|
|
2546
|
+
"version": "1.0.0",
|
|
2547
|
+
"toolName": "inspect_asset",
|
|
2548
|
+
"baseDescription": "inspect_asset returns one manifest asset or the whole manifest. Use when the current\nmanifest state is unclear before referring to a generated asset by asset_id/user_label.\n\nDo not call it for ordinary uploaded references that the generation tools can use directly,\nand do not use it as a substitute for analyze_image/analyze_video content inspection.",
|
|
2549
|
+
"parameterDocs": {
|
|
2550
|
+
"asset_id": "Stable internal asset id. Prefer this when known.",
|
|
2551
|
+
"user_label": "Human label to look up case-insensitively when asset_id is not known."
|
|
2552
|
+
}
|
|
2553
|
+
},
|
|
2554
|
+
{
|
|
2555
|
+
"contractId": "label_asset_v1",
|
|
2556
|
+
"version": "1.0.0",
|
|
2557
|
+
"toolName": "label_asset",
|
|
2558
|
+
"baseDescription": "label_asset updates the label, description, URL, must_preserve, or avoid fields for an\nexisting manifest asset. Use when the user renames an asset, assigns a role, or when a\nprevious tool result adds durable reference constraints.\n\nDo not invent labels or preservation constraints that the user did not provide or that are\nnot present in a tool result.",
|
|
2559
|
+
"parameterDocs": {
|
|
2560
|
+
"asset_id": "Existing asset_id to update.",
|
|
2561
|
+
"user_label": "New human-readable label when the user renames the asset.",
|
|
2562
|
+
"description": "Replacement description. Keep it factual and concise.",
|
|
2563
|
+
"must_preserve": "Replacement preservation list. Use only explicit or tool-result-backed constraints.",
|
|
2564
|
+
"avoid": "Replacement avoid list. Use only explicit or tool-result-backed constraints."
|
|
2565
|
+
}
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
"contractId": "map_assets_for_model_v1",
|
|
2569
|
+
"version": "1.0.0",
|
|
2570
|
+
"toolName": "map_assets_for_model",
|
|
2571
|
+
"baseDescription": "map_assets_for_model is an inspection helper for previously generated asset-manifest entries\nwhen you need exact model_ref tokens for a later prompt.\n\nDo NOT call this for ordinary uploaded image references. If the user uploaded images and\nasks GPT Image 2 to use all uploaded assets as visual references, call edit_image directly\nwith sourceImageIndex=-1 and describe the uploaded assets/roles in the prompt. Uploaded\nreference images are already available to edit_image/generate_image; mapping them first\nwastes a tool round and may violate direct-generation requests.\n\nUse this helper only when a previous tool result produced assets in the manifest and the\nnext prompt must name those prior generated assets with provider-specific tokens.",
|
|
2572
|
+
"parameterDocs": {
|
|
2573
|
+
"model_id": "Target model for prior generated manifest refs. Do not use for plain uploaded references."
|
|
2574
|
+
}
|
|
2575
|
+
},
|
|
2576
|
+
{
|
|
2577
|
+
"contractId": "validate_asset_references_v1",
|
|
2578
|
+
"version": "1.0.0",
|
|
2579
|
+
"toolName": "validate_asset_references",
|
|
2580
|
+
"baseDescription": "validate_asset_references checks a prompt for provider-specific manifest reference tokens\nand reports which resolve or dangle. Call right before dispatching a prompt that names\nmanifest assets with model_ref tokens.\n\nDo not use this for plain uploaded references. If validation finds dangling refs, repair the\nprompt or register/map the asset before running an expensive generation tool.",
|
|
2581
|
+
"parameterDocs": {
|
|
2582
|
+
"model_id": "Target provider/model id whose reference token format should be scanned.",
|
|
2583
|
+
"prompt": "Prompt text about to be sent to the model."
|
|
2584
|
+
}
|
|
2585
|
+
},
|
|
2586
|
+
{
|
|
2587
|
+
"contractId": "ask_clarifying_question_v1",
|
|
2588
|
+
"version": "1.1.0",
|
|
2589
|
+
"toolName": "ask_clarifying_question",
|
|
2590
|
+
"baseDescription": "ask_clarifying_question pauses the workflow and asks the user for missing required input.\nUse only when no reasonable safe default exists, when required media/content is missing, or\nwhen confirmation is required for a destructive or credit-sensitive action.\n\nFor creative briefs, do not over-question. Ask at most 3 concise high-leverage questions only\nwhen the answer would materially change audience, product promise, format/duration/platform,\ntone, required assets, dialogue/no-dialogue, safety/legal/factual claims, or credit-sensitive\nexecution. Otherwise make reasonable assumptions and continue.\n\nThis ends the turn. Do not call another tool after it. Ask short concrete question(s) and\navoid preamble.",
|
|
2591
|
+
"parameterDocs": {
|
|
2592
|
+
"question": "One short concrete question surfaced verbatim to the user.",
|
|
2593
|
+
"reason": "Short telemetry tag such as missing_source_asset, ambiguous_subject, or destructive_confirm."
|
|
2594
|
+
}
|
|
2595
|
+
},
|
|
2596
|
+
{
|
|
2597
|
+
"contractId": "finalize_response_v1",
|
|
2598
|
+
"version": "1.1.0",
|
|
2599
|
+
"toolName": "finalize_response",
|
|
2600
|
+
"baseDescription": "finalize_response marks the turn complete and stops the tool loop. Use after the requested\nworkflow succeeds, partially succeeds, fails with a surfaced error, or needs no tool action.\n\nWhen the user asked for a script, storyboard, ad concept, trailer, creator video, meme/parody,\nor music prompt and no media tool is required, deliver the final creative in a clean Markdown\ncontract: title, concept/objective, audience if relevant, timed beats or script, audio/text\nnotes, generation prompt(s), CTA, and brief assumptions. For revisions, apply the feedback\ndirectly while preserving approved elements and rejected constraints.\n\nDo not call any other tool after finalize_response. Keep the summary short and grounded in\nactual tool results; do not claim exact metadata that no tool returned.",
|
|
2601
|
+
"parameterDocs": {
|
|
2602
|
+
"summary": "Short user-visible closeout. Mention produced media or the concrete blocker; avoid duplicating prior tool output.",
|
|
2603
|
+
"outcome": "success, partial, asked_user, failed, or no_action based on the actual turn outcome."
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
];
|
|
4
2607
|
function isLtxWorkflow(workflow) {
|
|
5
2608
|
return workflow === 't2v' || workflow === 'i2v' || workflow === 'ia2v' || workflow === 'a2v' || workflow === 'v2v';
|
|
6
2609
|
}
|
|
7
|
-
export const SKILL_RUNTIME_VERSION = '2026-05-
|
|
2610
|
+
export const SKILL_RUNTIME_VERSION = '2026-05-13.1';
|
|
2611
|
+
function publicSkillPromptContractFragment(contract) {
|
|
2612
|
+
const parameterDocs = Object.entries(contract.parameterDocs ?? {})
|
|
2613
|
+
.map(([name, description]) => `${name}: ${description}`);
|
|
2614
|
+
const sections = [
|
|
2615
|
+
contract.baseDescription ?? '',
|
|
2616
|
+
parameterDocs.length ? `Parameter notes:\n${parameterDocs.join('\n')}` : '',
|
|
2617
|
+
contract.voiceExamples?.length ? `Examples:\n${contract.voiceExamples.join('\n')}` : '',
|
|
2618
|
+
Object.values(contract.conditionalNotes ?? {}).join('\n'),
|
|
2619
|
+
].filter(Boolean);
|
|
2620
|
+
return sections.join('\n\n');
|
|
2621
|
+
}
|
|
2622
|
+
function publicSkillRepairMode(mode) {
|
|
2623
|
+
if (mode === 'autoRepair')
|
|
2624
|
+
return 'execute_with_repair';
|
|
2625
|
+
if (mode === 'stopAndAsk')
|
|
2626
|
+
return 'ask_user';
|
|
2627
|
+
if (mode === 'suggestFollowup')
|
|
2628
|
+
return 'repair';
|
|
2629
|
+
return 'passthrough';
|
|
2630
|
+
}
|
|
2631
|
+
export const PUBLIC_SKILL_DEFAULT_POLICIES = PHASE_3_GATING_POLICIES.map((policy) => ({
|
|
2632
|
+
policyId: policy.policyId,
|
|
2633
|
+
trigger: {
|
|
2634
|
+
allOf: [...policy.trigger.allOf],
|
|
2635
|
+
...(policy.trigger.noneOf ? { noneOf: [...policy.trigger.noneOf] } : {}),
|
|
2636
|
+
...(policy.trigger.sources
|
|
2637
|
+
? { sources: Object.fromEntries(Object.entries(policy.trigger.sources).map(([kind, source]) => [
|
|
2638
|
+
kind,
|
|
2639
|
+
Array.isArray(source) ? [...source] : source,
|
|
2640
|
+
])) }
|
|
2641
|
+
: {}),
|
|
2642
|
+
},
|
|
2643
|
+
effect: {
|
|
2644
|
+
forbid: [...policy.effect.forbid],
|
|
2645
|
+
...(policy.effect.require ? { require: [...policy.effect.require] } : {}),
|
|
2646
|
+
},
|
|
2647
|
+
rationale: policy.rationale,
|
|
2648
|
+
}));
|
|
2649
|
+
export const PUBLIC_SKILL_DEFAULT_PROMPT_CONTRACTS = PHASE_5_PROMPT_CONTRACTS.map((contract) => ({
|
|
2650
|
+
toolName: contract.toolName,
|
|
2651
|
+
fragment: publicSkillPromptContractFragment(contract),
|
|
2652
|
+
}));
|
|
2653
|
+
export const PUBLIC_SKILL_DEFAULT_REPAIR_RECIPES = PHASE_4_REPAIR_RECIPES.map((recipe) => ({
|
|
2654
|
+
toolName: recipe.toolName,
|
|
2655
|
+
errorCode: recipe.errorCode,
|
|
2656
|
+
mode: publicSkillRepairMode(recipe.mode),
|
|
2657
|
+
message: recipe.repairNoteTemplate,
|
|
2658
|
+
...(recipe.suggestedFollowupTool ? { suggestedArgs: { toolName: recipe.suggestedFollowupTool } } : {}),
|
|
2659
|
+
}));
|
|
2660
|
+
export const PUBLIC_SKILL_DEFAULT_TOOL_NAMES = [
|
|
2661
|
+
...new Set(PHASE_5_PROMPT_CONTRACTS.map((contract) => contract.toolName)),
|
|
2662
|
+
];
|
|
2663
|
+
export const PUBLIC_SKILL_DEFAULT_TOOL_DEFINITIONS = PUBLIC_SKILL_DEFAULT_TOOL_NAMES.map((name) => ({
|
|
2664
|
+
type: 'function',
|
|
2665
|
+
function: {
|
|
2666
|
+
name,
|
|
2667
|
+
description: name.replace(/_/g, ' '),
|
|
2668
|
+
},
|
|
2669
|
+
}));
|
|
2670
|
+
export function createPublicSkillContractRuntime(input = {}) {
|
|
2671
|
+
return {
|
|
2672
|
+
policies: [...(input.policies ?? [])],
|
|
2673
|
+
promptContracts: [...(input.promptContracts ?? [])],
|
|
2674
|
+
repairRecipes: [...(input.repairRecipes ?? [])],
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
export function createPublicSkillDefaultContractRuntime(input = {}) {
|
|
2678
|
+
return createPublicSkillContractRuntime({
|
|
2679
|
+
policies: [
|
|
2680
|
+
...PUBLIC_SKILL_DEFAULT_POLICIES,
|
|
2681
|
+
...(input.policies ?? []),
|
|
2682
|
+
],
|
|
2683
|
+
promptContracts: [
|
|
2684
|
+
...PUBLIC_SKILL_DEFAULT_PROMPT_CONTRACTS,
|
|
2685
|
+
...(input.promptContracts ?? []),
|
|
2686
|
+
],
|
|
2687
|
+
repairRecipes: [
|
|
2688
|
+
...PUBLIC_SKILL_DEFAULT_REPAIR_RECIPES,
|
|
2689
|
+
...(input.repairRecipes ?? []),
|
|
2690
|
+
],
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
function publicSkillSignalSourcesByKind(signals) {
|
|
2694
|
+
const sourcesByKind = new Map();
|
|
2695
|
+
for (const signal of signals) {
|
|
2696
|
+
const sources = sourcesByKind.get(signal.kind) ?? new Set();
|
|
2697
|
+
sources.add(signal.source);
|
|
2698
|
+
sourcesByKind.set(signal.kind, sources);
|
|
2699
|
+
}
|
|
2700
|
+
return sourcesByKind;
|
|
2701
|
+
}
|
|
2702
|
+
function publicSkillSignalMatchesSourceConstraint(sourcesByKind, kind, allowed) {
|
|
2703
|
+
const sources = sourcesByKind.get(kind);
|
|
2704
|
+
if (!sources)
|
|
2705
|
+
return false;
|
|
2706
|
+
if (!allowed)
|
|
2707
|
+
return true;
|
|
2708
|
+
const allowedSources = Array.isArray(allowed) ? allowed : [allowed];
|
|
2709
|
+
return allowedSources.some((source) => sources.has(source));
|
|
2710
|
+
}
|
|
2711
|
+
function publicSkillSessionSignals(sessionState) {
|
|
2712
|
+
if (!sessionState)
|
|
2713
|
+
return [];
|
|
2714
|
+
const signalNames = [
|
|
2715
|
+
['hasUploadedImage', 'has_uploaded_image'],
|
|
2716
|
+
['hasUploadedVideo', 'has_uploaded_video'],
|
|
2717
|
+
['hasUploadedAudio', 'has_uploaded_audio'],
|
|
2718
|
+
['hasGeneratedImage', 'has_generated_image'],
|
|
2719
|
+
['hasGeneratedVideo', 'has_generated_video'],
|
|
2720
|
+
['hasGeneratedAudio', 'has_generated_audio'],
|
|
2721
|
+
['hasActivePersona', 'has_active_persona'],
|
|
2722
|
+
['awaitingImageSelection', 'awaiting_image_selection'],
|
|
2723
|
+
['pendingStitchAfterBatch', 'pending_stitch_after_batch'],
|
|
2724
|
+
['completedWorkflow', 'completed_workflow'],
|
|
2725
|
+
];
|
|
2726
|
+
return signalNames
|
|
2727
|
+
.filter(([key]) => sessionState[key] === true)
|
|
2728
|
+
.map(([, kind]) => ({ kind, source: 'session_state' }));
|
|
2729
|
+
}
|
|
2730
|
+
export function classifyPublicSkillTurn(input) {
|
|
2731
|
+
const signals = [
|
|
2732
|
+
...(input.signals ?? []),
|
|
2733
|
+
...publicSkillSessionSignals(input.sessionState),
|
|
2734
|
+
];
|
|
2735
|
+
const sourcesByKind = publicSkillSignalSourcesByKind(signals);
|
|
2736
|
+
const forbidden = new Set();
|
|
2737
|
+
const required = new Set();
|
|
2738
|
+
const appliedPolicies = [];
|
|
2739
|
+
const rationales = [];
|
|
2740
|
+
const policies = input.policies ?? input.runtime?.policies ?? [];
|
|
2741
|
+
for (const policy of policies) {
|
|
2742
|
+
const allOfMatch = policy.trigger.allOf.every((kind) => publicSkillSignalMatchesSourceConstraint(sourcesByKind, kind, policy.trigger.sources?.[kind]));
|
|
2743
|
+
const noneOfMatch = !policy.trigger.noneOf
|
|
2744
|
+
|| policy.trigger.noneOf.every((kind) => !sourcesByKind.has(kind));
|
|
2745
|
+
if (!allOfMatch || !noneOfMatch)
|
|
2746
|
+
continue;
|
|
2747
|
+
appliedPolicies.push(policy.policyId);
|
|
2748
|
+
if (policy.rationale)
|
|
2749
|
+
rationales.push(policy.rationale);
|
|
2750
|
+
for (const tool of policy.effect.forbid)
|
|
2751
|
+
forbidden.add(tool);
|
|
2752
|
+
for (const tool of policy.effect.require ?? [])
|
|
2753
|
+
required.add(tool);
|
|
2754
|
+
}
|
|
2755
|
+
return {
|
|
2756
|
+
visibleTools: input.availableTools.filter((tool) => !forbidden.has(tool)),
|
|
2757
|
+
forbiddenTools: [...forbidden],
|
|
2758
|
+
requiredTools: [...required],
|
|
2759
|
+
appliedPolicies,
|
|
2760
|
+
signals,
|
|
2761
|
+
rationale: rationales.join(' '),
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
export function compilePublicSkillToolSurface(input) {
|
|
2765
|
+
const availableTools = input.tools.map((tool) => tool.function.name);
|
|
2766
|
+
const turnPolicy = input.turnPolicy ?? classifyPublicSkillTurn({
|
|
2767
|
+
availableTools,
|
|
2768
|
+
signals: input.signals,
|
|
2769
|
+
sessionState: input.sessionState,
|
|
2770
|
+
policies: input.policies,
|
|
2771
|
+
runtime: input.runtime,
|
|
2772
|
+
});
|
|
2773
|
+
const promptContracts = input.promptContracts ?? input.runtime?.promptContracts ?? [];
|
|
2774
|
+
const contractsByTool = new Map();
|
|
2775
|
+
for (const contract of promptContracts) {
|
|
2776
|
+
const fragments = contractsByTool.get(contract.toolName) ?? [];
|
|
2777
|
+
fragments.push(contract.fragment.trim());
|
|
2778
|
+
contractsByTool.set(contract.toolName, fragments);
|
|
2779
|
+
}
|
|
2780
|
+
const visible = new Set(turnPolicy.visibleTools);
|
|
2781
|
+
const tools = input.tools
|
|
2782
|
+
.filter((tool) => visible.has(tool.function.name))
|
|
2783
|
+
.map((tool) => {
|
|
2784
|
+
const fragments = contractsByTool.get(tool.function.name)?.filter(Boolean) ?? [];
|
|
2785
|
+
if (fragments.length === 0)
|
|
2786
|
+
return tool;
|
|
2787
|
+
return {
|
|
2788
|
+
...tool,
|
|
2789
|
+
function: {
|
|
2790
|
+
...tool.function,
|
|
2791
|
+
description: [tool.function.description, ...fragments].filter(Boolean).join('\n\n'),
|
|
2792
|
+
},
|
|
2793
|
+
};
|
|
2794
|
+
});
|
|
2795
|
+
const contextLines = [
|
|
2796
|
+
turnPolicy.forbiddenTools.length ? `Forbidden tools: ${turnPolicy.forbiddenTools.join(', ')}` : '',
|
|
2797
|
+
turnPolicy.requiredTools.length ? `Required tools: ${turnPolicy.requiredTools.join(', ')}` : '',
|
|
2798
|
+
turnPolicy.rationale,
|
|
2799
|
+
].filter(Boolean);
|
|
2800
|
+
return {
|
|
2801
|
+
tools,
|
|
2802
|
+
contextBlock: contextLines.join('\n'),
|
|
2803
|
+
turnPolicy,
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
export function dispatchPublicSkillToolCall(input) {
|
|
2807
|
+
if (input.turnPolicy?.forbiddenTools.includes(input.toolName)) {
|
|
2808
|
+
return {
|
|
2809
|
+
allowed: false,
|
|
2810
|
+
mode: 'reject',
|
|
2811
|
+
reason: `Tool ${input.toolName} is forbidden by the current turn policy.`,
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
const recipes = input.repairRecipes ?? input.runtime?.repairRecipes ?? [];
|
|
2815
|
+
const recipe = input.errorCode
|
|
2816
|
+
? recipes.find((candidate) => candidate.toolName === input.toolName && candidate.errorCode === input.errorCode)
|
|
2817
|
+
: undefined;
|
|
2818
|
+
if (!recipe)
|
|
2819
|
+
return { allowed: true, mode: 'passthrough' };
|
|
2820
|
+
return {
|
|
2821
|
+
allowed: recipe.mode !== 'reject',
|
|
2822
|
+
mode: recipe.mode,
|
|
2823
|
+
reason: recipe.message,
|
|
2824
|
+
repairRecipe: recipe,
|
|
2825
|
+
suggestedArgs: recipe.suggestedArgs,
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
function skillErrorMessage(error) {
|
|
2829
|
+
if (!error)
|
|
2830
|
+
return 'Unknown error';
|
|
2831
|
+
if (error instanceof Error)
|
|
2832
|
+
return error.message || 'Unknown error';
|
|
2833
|
+
if (typeof error === 'string')
|
|
2834
|
+
return error;
|
|
2835
|
+
if (typeof error === 'object' && error !== null) {
|
|
2836
|
+
const record = error;
|
|
2837
|
+
if (typeof record.message === 'string')
|
|
2838
|
+
return record.message;
|
|
2839
|
+
if (typeof record.error === 'string')
|
|
2840
|
+
return record.error;
|
|
2841
|
+
}
|
|
2842
|
+
return String(error);
|
|
2843
|
+
}
|
|
2844
|
+
function skillErrorCode(error) {
|
|
2845
|
+
if (typeof error !== 'object' || error === null)
|
|
2846
|
+
return undefined;
|
|
2847
|
+
const record = error;
|
|
2848
|
+
const code = record.code ?? record.errorCode ?? record.status ?? record.statusCode;
|
|
2849
|
+
if (typeof code === 'string' || typeof code === 'number')
|
|
2850
|
+
return code;
|
|
2851
|
+
return undefined;
|
|
2852
|
+
}
|
|
2853
|
+
function codeEquals(code, expected) {
|
|
2854
|
+
if (code === undefined)
|
|
2855
|
+
return false;
|
|
2856
|
+
return String(code).toLowerCase() === String(expected).toLowerCase();
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Map public skill / CLI failures onto the canonical `ToolErrorCode`
|
|
2860
|
+
* taxonomy used by the shared creative-agent runtime. This intentionally
|
|
2861
|
+
* accepts only broad producer-owned fields (`message`, `error`, `code`,
|
|
2862
|
+
* `errorCode`, HTTP status) and otherwise keeps unknown payloads opaque.
|
|
2863
|
+
*/
|
|
2864
|
+
export function classifySkillError(error) {
|
|
2865
|
+
const message = skillErrorMessage(error);
|
|
2866
|
+
const lower = message.toLowerCase();
|
|
2867
|
+
const code = skillErrorCode(error);
|
|
2868
|
+
if (codeEquals(code, 4024)
|
|
2869
|
+
|| codeEquals(code, 'INSUFFICIENT_BALANCE')
|
|
2870
|
+
|| lower.includes('insufficient balance')
|
|
2871
|
+
|| lower.includes('insufficient credits')
|
|
2872
|
+
|| lower.includes('insufficient_credits')) {
|
|
2873
|
+
return { error_type: 'COST_LIMIT_EXCEEDED', category: 'insufficient_credits', message, retryable: true };
|
|
2874
|
+
}
|
|
2875
|
+
if (codeEquals(code, 'USER_CANCELLED')
|
|
2876
|
+
|| codeEquals(code, 'CANCELLED')
|
|
2877
|
+
|| codeEquals(code, 'ABORT_ERR')
|
|
2878
|
+
|| lower.includes('cancelled')
|
|
2879
|
+
|| lower.includes('canceled')
|
|
2880
|
+
|| lower.includes('aborted')) {
|
|
2881
|
+
return { error_type: 'USER_CANCELLED', category: 'cancelled', message, retryable: false };
|
|
2882
|
+
}
|
|
2883
|
+
if (codeEquals(code, 408)
|
|
2884
|
+
|| codeEquals(code, 'ETIMEDOUT')
|
|
2885
|
+
|| lower.includes('timed out')
|
|
2886
|
+
|| lower.includes('timeout')
|
|
2887
|
+
|| lower.includes('no events received')
|
|
2888
|
+
|| lower.includes('no activity')
|
|
2889
|
+
|| lower.includes('inactivity')) {
|
|
2890
|
+
return { error_type: 'PROVIDER_TIMEOUT', category: 'timeout', message, retryable: true };
|
|
2891
|
+
}
|
|
2892
|
+
if (codeEquals(code, 401)
|
|
2893
|
+
|| codeEquals(code, 403)
|
|
2894
|
+
|| codeEquals(code, 'MISSING_CREDENTIALS')
|
|
2895
|
+
|| codeEquals(code, 'UNSAFE_API_BASE_URL')
|
|
2896
|
+
|| lower.includes('api key')
|
|
2897
|
+
|| lower.includes('credentials')
|
|
2898
|
+
|| lower.includes('permission')
|
|
2899
|
+
|| lower.includes('unauthorized')
|
|
2900
|
+
|| lower.includes('forbidden')) {
|
|
2901
|
+
return { error_type: 'PERMISSION_REQUIRED', category: 'permission_required', message, retryable: false };
|
|
2902
|
+
}
|
|
2903
|
+
if (codeEquals(code, 'MODEL_UNAVAILABLE')
|
|
2904
|
+
|| lower.includes('model unavailable')
|
|
2905
|
+
|| lower.includes('model not available')
|
|
2906
|
+
|| lower.includes('no worker')
|
|
2907
|
+
|| lower.includes('no gpu worker')) {
|
|
2908
|
+
return { error_type: 'MODEL_UNAVAILABLE', category: 'model_unavailable', message, retryable: true };
|
|
2909
|
+
}
|
|
2910
|
+
if (codeEquals(code, 'ASSET_NOT_FOUND')
|
|
2911
|
+
|| codeEquals(code, 'FILE_NOT_FOUND')
|
|
2912
|
+
|| lower.includes('file not found')
|
|
2913
|
+
|| lower.includes('asset not found')
|
|
2914
|
+
|| lower.includes('missing asset')) {
|
|
2915
|
+
return { error_type: 'ASSET_NOT_FOUND', category: 'asset_not_found', message, retryable: false };
|
|
2916
|
+
}
|
|
2917
|
+
if (codeEquals(code, 'WORKFLOW_VALIDATION_FAILED')
|
|
2918
|
+
|| codeEquals(code, 'INVALID_WORKFLOW_INPUT')
|
|
2919
|
+
|| lower.includes('workflow validation')
|
|
2920
|
+
|| lower.includes('invalid workflow')) {
|
|
2921
|
+
return { error_type: 'WORKFLOW_VALIDATION_FAILED', category: 'workflow_validation', message, retryable: false };
|
|
2922
|
+
}
|
|
2923
|
+
if (codeEquals(code, 'USER_INPUT_INCOMPLETE')
|
|
2924
|
+
|| codeEquals(code, 'MISSING_WORKFLOW_INPUT')
|
|
2925
|
+
|| lower.includes('requires a prompt')
|
|
2926
|
+
|| lower.includes('requires --')
|
|
2927
|
+
|| lower.includes('missing required input')) {
|
|
2928
|
+
return { error_type: 'USER_INPUT_INCOMPLETE', category: 'user_input_incomplete', message, retryable: false };
|
|
2929
|
+
}
|
|
2930
|
+
if (codeEquals(code, 'PARAMETER_INVALID')
|
|
2931
|
+
|| codeEquals(code, 'INVALID_ARGUMENT')
|
|
2932
|
+
|| codeEquals(code, 'INVALID_VIDEO_SIZE')
|
|
2933
|
+
|| codeEquals(code, 'INVALID_PATH')
|
|
2934
|
+
|| lower.includes('parse')
|
|
2935
|
+
|| lower.includes('malformed')
|
|
2936
|
+
|| lower.includes('missing required')
|
|
2937
|
+
|| lower.includes('must be')
|
|
2938
|
+
|| lower.includes('invalid ')) {
|
|
2939
|
+
return { error_type: 'PARAMETER_INVALID', category: 'schema_validation', message, retryable: false };
|
|
2940
|
+
}
|
|
2941
|
+
if (codeEquals(code, 'SAFETY_REJECTED')
|
|
2942
|
+
|| lower.includes('content policy')
|
|
2943
|
+
|| lower.includes('sensitive content')
|
|
2944
|
+
|| lower.includes('sensitivecontent')
|
|
2945
|
+
|| lower.includes('nsfw')
|
|
2946
|
+
|| lower.includes('refused')
|
|
2947
|
+
|| lower.includes('not appropriate')
|
|
2948
|
+
|| lower.includes('safety')) {
|
|
2949
|
+
return { error_type: 'SAFETY_REJECTED', category: 'content_refused', message, retryable: false };
|
|
2950
|
+
}
|
|
2951
|
+
if (codeEquals(code, 502)
|
|
2952
|
+
|| codeEquals(code, 503)
|
|
2953
|
+
|| codeEquals(code, 504)
|
|
2954
|
+
|| codeEquals(code, 'ECONNRESET')
|
|
2955
|
+
|| codeEquals(code, 'ECONNREFUSED')
|
|
2956
|
+
|| lower.includes('network')
|
|
2957
|
+
|| lower.includes('failed to fetch')
|
|
2958
|
+
|| lower.includes('websocket')
|
|
2959
|
+
|| lower.includes('econnreset')
|
|
2960
|
+
|| lower.includes('econnrefused')
|
|
2961
|
+
|| lower.includes('socket hang up')
|
|
2962
|
+
|| lower.includes('server restarting')
|
|
2963
|
+
|| lower.includes('worker disconnected')) {
|
|
2964
|
+
return { error_type: 'GPU_WORKER_FAILED', category: 'transient_failure', message, retryable: true };
|
|
2965
|
+
}
|
|
2966
|
+
return { error_type: 'UNKNOWN_ERROR', category: 'permanent_failure', message, retryable: false };
|
|
2967
|
+
}
|
|
2968
|
+
const TOOL_RESULT_BEGIN = '[[TOOL_RESULT_BEGIN]]';
|
|
2969
|
+
const TOOL_RESULT_END = '[[TOOL_RESULT_END]]';
|
|
2970
|
+
const HARD_STRIP_PATTERNS = [
|
|
2971
|
+
/<\|im_start\|>/gi,
|
|
2972
|
+
/<\|im_end\|>/gi,
|
|
2973
|
+
/<\|user\|>/gi,
|
|
2974
|
+
/<\|system\|>/gi,
|
|
2975
|
+
/<\|assistant\|>/gi,
|
|
2976
|
+
/<\|tool\|>/gi,
|
|
2977
|
+
/<\|tool_call\|>/gi,
|
|
2978
|
+
/<\|begin\u2581of\u2581sentence\|>/gi,
|
|
2979
|
+
/<\|end\u2581of\u2581sentence\|>/gi,
|
|
2980
|
+
/\[INST\]/gi,
|
|
2981
|
+
/\[\/INST\]/gi,
|
|
2982
|
+
/<<SYS>>/gi,
|
|
2983
|
+
/<<\/SYS>>/gi,
|
|
2984
|
+
/<system>[\s\S]*?<\/system>/gi,
|
|
2985
|
+
/<tool_call>[\s\S]*?<\/tool_call>/gi,
|
|
2986
|
+
/<\/?(?:user|assistant|tool)>/gi,
|
|
2987
|
+
];
|
|
2988
|
+
const SUSPICIOUS_PHRASE_PATTERNS = [
|
|
2989
|
+
/\bignore\s+(?:all\s+)?(?:previous|prior|the\s+above)\s+instructions?\b/gi,
|
|
2990
|
+
/\bdisregard\s+(?:all\s+)?(?:previous|prior|the\s+above)\b/gi,
|
|
2991
|
+
/\bforget\s+(?:your|the)\s+(?:role|instructions?|rules?|system)\b/gi,
|
|
2992
|
+
/\byou\s+are\s+now\s+(?:a|an)\s+/gi,
|
|
2993
|
+
/\b(?:override|bypass)\s+(?:safety|content|filter)/gi,
|
|
2994
|
+
];
|
|
2995
|
+
export function sanitizeToolMessageContent(input) {
|
|
2996
|
+
if (!input)
|
|
2997
|
+
return { cleaned: input, flagged: false, signals: [] };
|
|
2998
|
+
let cleaned = input;
|
|
2999
|
+
const signals = [];
|
|
3000
|
+
for (const pattern of HARD_STRIP_PATTERNS) {
|
|
3001
|
+
if (pattern.test(cleaned)) {
|
|
3002
|
+
pattern.lastIndex = 0;
|
|
3003
|
+
cleaned = cleaned.replace(pattern, ' ');
|
|
3004
|
+
signals.push(`stripped:${pattern.source.replace(/\\\|/g, '|').slice(0, 40)}`);
|
|
3005
|
+
}
|
|
3006
|
+
pattern.lastIndex = 0;
|
|
3007
|
+
}
|
|
3008
|
+
for (const pattern of SUSPICIOUS_PHRASE_PATTERNS) {
|
|
3009
|
+
if (pattern.test(cleaned)) {
|
|
3010
|
+
signals.push(`flagged:${pattern.source.slice(0, 40)}`);
|
|
3011
|
+
}
|
|
3012
|
+
pattern.lastIndex = 0;
|
|
3013
|
+
}
|
|
3014
|
+
cleaned = cleaned.replace(/[ \t]{2,}/g, ' ');
|
|
3015
|
+
return {
|
|
3016
|
+
cleaned,
|
|
3017
|
+
flagged: signals.length > 0,
|
|
3018
|
+
signals,
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
export function wrapToolResultForLlm(content) {
|
|
3022
|
+
if (content.startsWith(TOOL_RESULT_BEGIN))
|
|
3023
|
+
return content;
|
|
3024
|
+
return `${TOOL_RESULT_BEGIN}\n${content}\n${TOOL_RESULT_END}`;
|
|
3025
|
+
}
|
|
3026
|
+
export function sanitizeMessagesForLlm(messages, onSignal) {
|
|
3027
|
+
return messages.map((message) => {
|
|
3028
|
+
if (message.role !== 'tool')
|
|
3029
|
+
return message;
|
|
3030
|
+
const raw = message.content;
|
|
3031
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
3032
|
+
return message;
|
|
3033
|
+
const { cleaned, flagged, signals } = sanitizeToolMessageContent(raw);
|
|
3034
|
+
if (flagged && onSignal) {
|
|
3035
|
+
onSignal({
|
|
3036
|
+
tool_call_id: message.tool_call_id,
|
|
3037
|
+
signals,
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
return {
|
|
3041
|
+
...message,
|
|
3042
|
+
content: wrapToolResultForLlm(cleaned),
|
|
3043
|
+
};
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
export const TOOL_RESULT_DELIMITERS = {
|
|
3047
|
+
begin: TOOL_RESULT_BEGIN,
|
|
3048
|
+
end: TOOL_RESULT_END,
|
|
3049
|
+
};
|
|
8
3050
|
const SEEDANCE_MODEL_REF_FORMAT = {
|
|
9
3051
|
format(index, type) {
|
|
10
3052
|
if (type === 'video')
|
|
@@ -203,38 +3245,256 @@ export const ALL_BUILT_IN_SKILLS = [
|
|
|
203
3245
|
export function toolOk(fields) {
|
|
204
3246
|
return { ok: true, status: 'completed', output_assets: [], ...fields };
|
|
205
3247
|
}
|
|
206
|
-
export function toolErr(fields) {
|
|
207
|
-
return { ok: false, ...fields };
|
|
3248
|
+
export function toolErr(fields) {
|
|
3249
|
+
return { ok: false, ...fields };
|
|
3250
|
+
}
|
|
3251
|
+
export function isToolResultOk(result) {
|
|
3252
|
+
return result.ok === true;
|
|
3253
|
+
}
|
|
3254
|
+
export function isToolResultErr(result) {
|
|
3255
|
+
return result.ok === false;
|
|
3256
|
+
}
|
|
3257
|
+
export class StoryboardAdapterUnsupportedStageError extends Error {
|
|
3258
|
+
modelId;
|
|
3259
|
+
stage;
|
|
3260
|
+
constructor(modelId, stage) {
|
|
3261
|
+
super(`Adapter "${modelId}" does not support stage "${stage}".`);
|
|
3262
|
+
this.modelId = modelId;
|
|
3263
|
+
this.stage = stage;
|
|
3264
|
+
this.name = 'StoryboardAdapterUnsupportedStageError';
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
function requireStoryboardScene(adapterId, input) {
|
|
3268
|
+
if (!input.scene)
|
|
3269
|
+
throw new Error(`${adapterId} ${input.stage} requires a scene argument.`);
|
|
3270
|
+
return input.scene;
|
|
3271
|
+
}
|
|
3272
|
+
function compileStoryboardImagePromptFromProject(project) {
|
|
3273
|
+
const frameCount = project.scenes.length || 1;
|
|
3274
|
+
const layout = storyboardLayoutSpecFromProject(project, frameCount);
|
|
3275
|
+
const boardSizeLine = layout.boardDimensions
|
|
3276
|
+
? `Overall storyboard canvas: ${layout.boardDimensions} pixels (${layout.boardAspectRatio}).`
|
|
3277
|
+
: `Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}.`;
|
|
3278
|
+
return [
|
|
3279
|
+
'CREATE:',
|
|
3280
|
+
`Create exactly ${frameCount} sequential video storyboard frames as one composite storyboard image.`,
|
|
3281
|
+
`Project title: ${project.title}.`,
|
|
3282
|
+
project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
|
|
3283
|
+
'',
|
|
3284
|
+
...compileStoryboardCountContractSection(project, layout),
|
|
3285
|
+
'',
|
|
3286
|
+
...compileStoryboardReferenceSection(project),
|
|
3287
|
+
'',
|
|
3288
|
+
'CANVAS / LAYOUT:',
|
|
3289
|
+
boardSizeLine,
|
|
3290
|
+
`Individual scene-cell/frame aspect ratio: ${layout.cellAspectRatio}.`,
|
|
3291
|
+
`Target final video aspect ratio: ${layout.targetVideoAspectRatio}.`,
|
|
3292
|
+
`Layout preset: ${layout.layoutKind} - ${layout.layoutDescription}.`,
|
|
3293
|
+
'',
|
|
3294
|
+
...compileStoryboardStoryContinuitySection(project),
|
|
3295
|
+
'',
|
|
3296
|
+
'GLOBAL STYLE:',
|
|
3297
|
+
project.creativeBrief.visualQualityBar,
|
|
3298
|
+
'',
|
|
3299
|
+
'CRITICAL REQUIREMENTS:',
|
|
3300
|
+
...compileStoryboardCriticalRequirements().map((item, index) => `${index + 1}. ${item}`),
|
|
3301
|
+
'',
|
|
3302
|
+
...compileStoryboardTimingValidationSection(project),
|
|
3303
|
+
'',
|
|
3304
|
+
...compileStoryboardScenesSection(project),
|
|
3305
|
+
'TEXT RENDERING:',
|
|
3306
|
+
'Keep production labels outside video-frame artwork. Only user-required diegetic or brand text belongs inside a frame.',
|
|
3307
|
+
'Visible text listed on a scene belongs only in that scene; do not repeat earlier scene text on later panels or the final frame unless that later scene lists it too.',
|
|
3308
|
+
...storyboardRequiredVisibleText(project).map(text => `Required exact visible text: "${text}".`),
|
|
3309
|
+
'',
|
|
3310
|
+
'NEGATIVE / AVOID:',
|
|
3311
|
+
...compileStoryboardAvoidSection(project.creativeBrief.concept).map(item => `- ${item}`),
|
|
3312
|
+
].join('\n');
|
|
3313
|
+
}
|
|
3314
|
+
function scenePromptText(scene) {
|
|
3315
|
+
return [
|
|
3316
|
+
scene.visual,
|
|
3317
|
+
scene.action,
|
|
3318
|
+
scene.camera,
|
|
3319
|
+
scene.lighting,
|
|
3320
|
+
].filter((value) => typeof value === 'string' && value.trim().length > 0).join(' — ');
|
|
208
3321
|
}
|
|
209
|
-
|
|
210
|
-
return
|
|
3322
|
+
function compileStoryboardKeyframePrompt(project, scene) {
|
|
3323
|
+
return [
|
|
3324
|
+
`Create a cinematic keyframe for scene "${scene.title}" in project "${project.title}".`,
|
|
3325
|
+
`Visual: ${scenePromptText(scene) || scene.purpose || scene.id}.`,
|
|
3326
|
+
scene.referenceUsage.length > 0 ? `Reference usage: ${scene.referenceUsage.join('; ')}.` : '',
|
|
3327
|
+
scene.textInImage.length > 0 ? `Visible text: ${scene.textInImage.join('; ')}.` : 'Visible text: none.',
|
|
3328
|
+
`Style: ${project.creativeBrief.visualQualityBar}.`,
|
|
3329
|
+
'Do not include storyboard panel labels, timecodes, production notes, or metadata labels inside the frame.',
|
|
3330
|
+
].filter(Boolean).join('\n');
|
|
211
3331
|
}
|
|
212
|
-
|
|
213
|
-
return
|
|
3332
|
+
function compileSeedanceSceneClipPromptFromProject(project, scene, referenceTag) {
|
|
3333
|
+
return [
|
|
3334
|
+
`Create a full-screen cinematic video clip from ${referenceTag}.`,
|
|
3335
|
+
`Project: ${project.title}.`,
|
|
3336
|
+
`Scene: ${scene.title}.`,
|
|
3337
|
+
`Purpose: ${scene.purpose || 'Advance the approved story spine.'}`,
|
|
3338
|
+
`Visual/action/camera: ${scenePromptText(scene) || scene.id}.`,
|
|
3339
|
+
scene.transitionIn || scene.transitionOut ? `Transition: ${[scene.transitionIn, scene.transitionOut].filter(Boolean).join('; ')}.` : '',
|
|
3340
|
+
scene.dialogue ? `Dialogue/VO: ${scene.dialogue}.` : '',
|
|
3341
|
+
scene.audioSfx.length > 0 ? `Audio/SFX: ${scene.audioSfx.join(', ')}.` : '',
|
|
3342
|
+
scene.music ? `Music: ${scene.music}.` : '',
|
|
3343
|
+
scene.textInImage.length > 0 ? `Required visible text: ${scene.textInImage.join('; ')}.` : 'Visible text: none.',
|
|
3344
|
+
`Style: ${project.creativeBrief.visualQualityBar}.`,
|
|
3345
|
+
'Use the reference as identity and composition guidance. Do not render storyboard labels, panel numbers, timecodes, or metadata.',
|
|
3346
|
+
].filter(Boolean).join('\n');
|
|
214
3347
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
3348
|
+
function compactSceneVideoPrompt(project, scene, referenceTag) {
|
|
3349
|
+
const lines = [];
|
|
3350
|
+
lines.push(`[VISUAL] ${scenePromptText(scene) || scene.purpose || scene.id}`);
|
|
3351
|
+
if (scene.audioSfx.length > 0)
|
|
3352
|
+
lines.push(`[SOUNDS] ${scene.audioSfx.join(', ')}`);
|
|
3353
|
+
if (scene.music)
|
|
3354
|
+
lines.push(`[MUSIC] ${scene.music}`);
|
|
3355
|
+
if (scene.dialogue)
|
|
3356
|
+
lines.push(`[SPEECH] ${scene.dialogue}`);
|
|
3357
|
+
lines.push(`Reference: ${referenceTag} (use as keyframe identity).`);
|
|
3358
|
+
if (project.creativeBrief.visualQualityBar)
|
|
3359
|
+
lines.push(`Style: ${project.creativeBrief.visualQualityBar}`);
|
|
3360
|
+
return lines.join('\n');
|
|
3361
|
+
}
|
|
3362
|
+
const PUBLIC_SEEDANCE_ADAPTER = {
|
|
3363
|
+
modelId: 'seedance',
|
|
3364
|
+
name: 'Seedance 2.x',
|
|
3365
|
+
supportedStages: ['storyboard_image', 'scene_clip'],
|
|
3366
|
+
compile(storyboard, input) {
|
|
3367
|
+
if (input.stage === 'storyboard_image') {
|
|
3368
|
+
return {
|
|
3369
|
+
stage: 'storyboard_image',
|
|
3370
|
+
prompt: compileStoryboardImagePromptFromProject(storyboard),
|
|
3371
|
+
args: {
|
|
3372
|
+
videoModel: 'seedance2-fast',
|
|
3373
|
+
aspectRatio: storyboard.targetVideoAspectRatio,
|
|
3374
|
+
skipPromptProcessing: true,
|
|
3375
|
+
expandPrompt: false,
|
|
3376
|
+
},
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
if (input.stage === 'scene_clip') {
|
|
3380
|
+
const scene = requireStoryboardScene('SEEDANCE_ADAPTER', input);
|
|
3381
|
+
const referenceTag = input.primaryReferenceTag ?? formatModelRef('seedance', 1, 'image');
|
|
3382
|
+
const duration = clampSeedanceStoryboardDuration(scene.durationSec ?? 5);
|
|
3383
|
+
return {
|
|
3384
|
+
stage: 'scene_clip',
|
|
3385
|
+
prompt: compileSeedanceSceneClipPromptFromProject(storyboard, scene, referenceTag),
|
|
3386
|
+
args: {
|
|
3387
|
+
videoModel: 'seedance2-fast',
|
|
3388
|
+
duration,
|
|
3389
|
+
aspectRatio: storyboard.targetVideoAspectRatio,
|
|
3390
|
+
skipPromptProcessing: true,
|
|
3391
|
+
expandPrompt: false,
|
|
3392
|
+
},
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
throw new StoryboardAdapterUnsupportedStageError('seedance', input.stage);
|
|
3396
|
+
},
|
|
3397
|
+
getSystemPromptGuidance() {
|
|
3398
|
+
return `SEEDANCE STORYBOARD REFERENCES: If exactly one uploaded image is an ordered storyboard/sequence sheet and the user asks for a Seedance video with only a sparse/casual prompt, use generate_video with referenceImageIndices=[-1], prompt="${SEEDANCE_STORYBOARD_REFERENCE_PROMPT}", videoModel="seedance2", skipPromptProcessing=true, and expandPrompt=false. Also use videoModel="seedance2" when a generated storyboard image becomes the Seedance reference, regardless of requested resolution, unless the user explicitly asks for a draft or the Seedance fast model/version. Do not use this fallback when the user provides a literal prompt, their own script, shot list, timecoded beats, VO/SFX notes, or other substantive video instructions.`;
|
|
3399
|
+
},
|
|
3400
|
+
};
|
|
3401
|
+
const PUBLIC_GPT_IMAGE_2_ADAPTER = {
|
|
3402
|
+
modelId: 'gpt-image-2',
|
|
3403
|
+
name: 'GPT Image 2',
|
|
3404
|
+
supportedStages: ['storyboard_image', 'keyframe'],
|
|
3405
|
+
compile(storyboard, input) {
|
|
3406
|
+
if (input.stage === 'storyboard_image') {
|
|
3407
|
+
return {
|
|
3408
|
+
stage: 'storyboard_image',
|
|
3409
|
+
prompt: compileStoryboardImagePromptFromProject(storyboard),
|
|
3410
|
+
args: {
|
|
3411
|
+
model: 'gpt-image-2',
|
|
3412
|
+
...buildStoryboardCanvasArgs(storyboard.outputAspectRatio, true),
|
|
3413
|
+
numberOfVariations: 1,
|
|
3414
|
+
},
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
if (input.stage === 'keyframe') {
|
|
3418
|
+
const scene = input.scene ?? storyboard.scenes[0];
|
|
3419
|
+
if (!scene)
|
|
3420
|
+
throw new Error('GPT_IMAGE_2_ADAPTER keyframe requires at least one storyboard scene.');
|
|
3421
|
+
return {
|
|
3422
|
+
stage: 'keyframe',
|
|
3423
|
+
prompt: compileStoryboardKeyframePrompt(storyboard, scene),
|
|
3424
|
+
args: {
|
|
3425
|
+
model: 'gpt-image-2',
|
|
3426
|
+
aspectRatio: storyboard.frameAspectRatio,
|
|
3427
|
+
numberOfVariations: 1,
|
|
3428
|
+
sceneId: scene.id,
|
|
3429
|
+
},
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
throw new StoryboardAdapterUnsupportedStageError('gpt-image-2', input.stage);
|
|
3433
|
+
},
|
|
3434
|
+
getSystemPromptGuidance() {
|
|
3435
|
+
return 'GPT IMAGE 2 ROUTING: When the user asks for a ChatGPT, OpenAI, GPT, GPT-2, GPT Image, or gpt-image-2 image/model, use model="gpt-image-2". Use generate_image for text-to-image requests. If uploaded/reference/persona images must guide identity, likeness, composition, style, or objects, use edit_image with model="gpt-image-2" instead of forcing generate_image.';
|
|
3436
|
+
},
|
|
3437
|
+
};
|
|
3438
|
+
const PUBLIC_LTX23_ADAPTER = {
|
|
3439
|
+
modelId: 'ltx23',
|
|
3440
|
+
name: 'LTX-2.3',
|
|
3441
|
+
supportedStages: ['scene_clip'],
|
|
3442
|
+
compile(storyboard, input) {
|
|
3443
|
+
if (input.stage !== 'scene_clip')
|
|
3444
|
+
throw new StoryboardAdapterUnsupportedStageError('ltx23', input.stage);
|
|
3445
|
+
const scene = requireStoryboardScene('LTX23_ADAPTER', input);
|
|
3446
|
+
const referenceTag = input.primaryReferenceTag ?? 'context_image_0';
|
|
3447
|
+
return {
|
|
3448
|
+
stage: 'scene_clip',
|
|
3449
|
+
prompt: compactSceneVideoPrompt(storyboard, scene, referenceTag),
|
|
3450
|
+
args: {
|
|
3451
|
+
videoModel: 'ltx23',
|
|
3452
|
+
duration: clampSeedanceStoryboardDuration(scene.durationSec ?? 5),
|
|
3453
|
+
aspectRatio: storyboard.targetVideoAspectRatio,
|
|
3454
|
+
},
|
|
3455
|
+
};
|
|
3456
|
+
},
|
|
3457
|
+
getSystemPromptGuidance() {
|
|
3458
|
+
return 'LTX-2.3 VIDEO PROMPTING: For image-to-video, describe only motion, action, camera, and sound that are not already obvious in the reference frame. Keep recurring character names and visual anchors stable across scenes.';
|
|
3459
|
+
},
|
|
3460
|
+
};
|
|
3461
|
+
const PUBLIC_WAN_ADAPTER = {
|
|
3462
|
+
modelId: 'wan',
|
|
3463
|
+
name: 'WAN 2.x',
|
|
3464
|
+
supportedStages: ['scene_clip'],
|
|
3465
|
+
compile(storyboard, input) {
|
|
3466
|
+
if (input.stage !== 'scene_clip')
|
|
3467
|
+
throw new StoryboardAdapterUnsupportedStageError('wan', input.stage);
|
|
3468
|
+
const scene = requireStoryboardScene('WAN_ADAPTER', input);
|
|
3469
|
+
const referenceTag = input.primaryReferenceTag ?? 'context_image_0';
|
|
3470
|
+
return {
|
|
3471
|
+
stage: 'scene_clip',
|
|
3472
|
+
prompt: [
|
|
3473
|
+
`[VISUAL] ${scenePromptText(scene) || scene.purpose || scene.id}`,
|
|
3474
|
+
`Reference: ${referenceTag} (use as the visual identity/keyframe anchor).`,
|
|
3475
|
+
scene.dialogue ? '[PERFORMANCE] Show implied speaking beats through expression and body motion; WAN does not render audio.' : '',
|
|
3476
|
+
storyboard.creativeBrief.visualQualityBar ? `Style: ${storyboard.creativeBrief.visualQualityBar}` : '',
|
|
3477
|
+
].filter(Boolean).join('\n'),
|
|
3478
|
+
args: {
|
|
3479
|
+
videoModel: 'wan22',
|
|
3480
|
+
duration: clampSeedanceStoryboardDuration(scene.durationSec ?? 5),
|
|
3481
|
+
aspectRatio: storyboard.targetVideoAspectRatio,
|
|
3482
|
+
},
|
|
3483
|
+
};
|
|
3484
|
+
},
|
|
3485
|
+
};
|
|
3486
|
+
const PUBLIC_STORYBOARD_ADAPTERS = [
|
|
3487
|
+
PUBLIC_SEEDANCE_ADAPTER,
|
|
3488
|
+
PUBLIC_GPT_IMAGE_2_ADAPTER,
|
|
3489
|
+
PUBLIC_LTX23_ADAPTER,
|
|
3490
|
+
PUBLIC_WAN_ADAPTER,
|
|
3491
|
+
];
|
|
3492
|
+
export function composeAdapterPromptGuidance(adapters = PUBLIC_STORYBOARD_ADAPTERS) {
|
|
3493
|
+
return adapters
|
|
3494
|
+
.map(adapter => adapter.getSystemPromptGuidance?.())
|
|
3495
|
+
.filter((guidance) => typeof guidance === 'string' && guidance.trim().length > 0)
|
|
3496
|
+
.map(guidance => guidance.trim())
|
|
3497
|
+
.join('\n\n');
|
|
238
3498
|
}
|
|
239
3499
|
export class SkillRegistry {
|
|
240
3500
|
manifests = new Map();
|
|
@@ -274,22 +3534,28 @@ export class SkillRegistry {
|
|
|
274
3534
|
export const storyboardAdapterRegistry = {
|
|
275
3535
|
getAdapter(modelId) {
|
|
276
3536
|
const trimmed = modelId.trim().toLowerCase();
|
|
3537
|
+
const exact = PUBLIC_STORYBOARD_ADAPTERS.find(adapter => adapter.modelId === trimmed);
|
|
3538
|
+
if (exact)
|
|
3539
|
+
return exact;
|
|
277
3540
|
if (trimmed.startsWith('seedance'))
|
|
278
|
-
return
|
|
3541
|
+
return PUBLIC_SEEDANCE_ADAPTER;
|
|
279
3542
|
if (trimmed.startsWith('gpt-image'))
|
|
280
|
-
return
|
|
3543
|
+
return PUBLIC_GPT_IMAGE_2_ADAPTER;
|
|
281
3544
|
if (trimmed.startsWith('ltx'))
|
|
282
|
-
return
|
|
3545
|
+
return PUBLIC_LTX23_ADAPTER;
|
|
283
3546
|
if (trimmed.startsWith('wan'))
|
|
284
|
-
return
|
|
3547
|
+
return PUBLIC_WAN_ADAPTER;
|
|
285
3548
|
return null;
|
|
286
3549
|
},
|
|
3550
|
+
list() {
|
|
3551
|
+
return [...PUBLIC_STORYBOARD_ADAPTERS];
|
|
3552
|
+
},
|
|
287
3553
|
};
|
|
288
|
-
export function compileForModel(modelId) {
|
|
3554
|
+
export function compileForModel(modelId, storyboard, input) {
|
|
289
3555
|
const adapter = storyboardAdapterRegistry.getAdapter(modelId);
|
|
290
3556
|
if (!adapter)
|
|
291
3557
|
throw new Error(`No storyboard adapter registered for model_id "${modelId}".`);
|
|
292
|
-
return
|
|
3558
|
+
return adapter.compile(storyboard, input);
|
|
293
3559
|
}
|
|
294
3560
|
const PUBLIC_SEEDANCE_PRIMARY_IMAGE_REF = formatModelRef('seedance', 1, 'image');
|
|
295
3561
|
export const SEEDANCE_STORYBOARD_REFERENCE_PROMPT = `Create a full-screen cinematic video from the storyboard in ${PUBLIC_SEEDANCE_PRIMARY_IMAGE_REF}. Treat ${PUBLIC_SEEDANCE_PRIMARY_IMAGE_REF} as the controlling source for shot order and intent, and as a source layout reference: use the thumbnails, timing, Dialogue/VO, Audio/SFX, timecodes, camera/motion notes, transitions, and scene order as instructions, not as a visual board to reproduce. Do not display the storyboard grid, borders, caption bars, storyboard title/footer text, panel numbers, section labels, slide titles, headings, or transcribed narration. Convert the ordered thumbnails into full-screen chronological beats; do not reuse only one or two motifs while skipping panels. When the board has panel titles, captions, section numbers, slide titles, or headings but no formal Dialogue/VO labels, treat those labels as short audio-only narration/voiceover or key-message beats in order unless they are clearly visual-only metadata. Voice each label as its own brief phrase with a pause; do not concatenate labels into run-on sentences and do not speak panel numbers. Show storyboard labels as visible text only when the user explicitly asks for visible text, subtitles, a title card, lower third, signage, or CTA. Preserve the story spine, character/product/reference continuity, and cause-and-effect progression between beats. Treat transitions as motion instructions, not unrelated hard cuts unless the storyboard explicitly asks for hard cuts. Use brand color, lighting, product imagery, and composition instead of invented typography. Keep visible text limited to exact copy the user or storyboard explicitly marks as on-screen text, CTA, signage, or end-card text. Use a music/SFX arc that follows the storyboard audio notes and lands the final brand/CTA hit. Keep unrelated UI, extra logos, microtext, subtitles, and extra scenes out of the frame.`;
|
|
@@ -1090,52 +4356,87 @@ export function inferExplicitPixelDimensionsFromText(text) {
|
|
|
1090
4356
|
}
|
|
1091
4357
|
return null;
|
|
1092
4358
|
}
|
|
4359
|
+
function ratioMatchLooksLikeMediaTimecodeRange(text, match) {
|
|
4360
|
+
const rawWidth = match[1] ?? '';
|
|
4361
|
+
const rawHeight = match[2] ?? '';
|
|
4362
|
+
if (!/^\d{1,2}$/.test(rawWidth) || !/^\d{2}$/.test(rawHeight))
|
|
4363
|
+
return false;
|
|
4364
|
+
const width = Number(rawWidth);
|
|
4365
|
+
const height = Number(rawHeight);
|
|
4366
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || height < 0 || height > 59)
|
|
4367
|
+
return false;
|
|
4368
|
+
const start = match.index ?? 0;
|
|
4369
|
+
const end = start + match[0].length;
|
|
4370
|
+
const before = text.slice(Math.max(0, start - 40), start);
|
|
4371
|
+
const after = text.slice(end, Math.min(text.length, end + 40));
|
|
4372
|
+
const adjacentRange = /^\s*(?:-|–|—|\bto\b)\s*\d{1,2}:[0-5]\d\b/i.test(after)
|
|
4373
|
+
|| /\b\d{1,2}:[0-5]\d\s*(?:-|–|—|\bto\b)\s*$/i.test(before);
|
|
4374
|
+
if (adjacentRange)
|
|
4375
|
+
return true;
|
|
4376
|
+
const timeCueContext = `${before} ${after}`;
|
|
4377
|
+
return /^0\d$/.test(rawHeight)
|
|
4378
|
+
&& /\b(?:timecode|timestamp|audio|music|song|track|clip|file|source|window|segment|range)\b/i.test(timeCueContext);
|
|
4379
|
+
}
|
|
1093
4380
|
export function inferExplicitAspectRatioFromText(text) {
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
4381
|
+
const ratioPattern = /\b(\d{1,4}(?:\.\d+)?)\s*:\s*(\d{1,4}(?:\.\d+)?)\b/g;
|
|
4382
|
+
for (const match of text.matchAll(ratioPattern)) {
|
|
4383
|
+
if (ratioMatchLooksLikeMediaTimecodeRange(text, match))
|
|
4384
|
+
continue;
|
|
4385
|
+
const width = Number(match[1]);
|
|
4386
|
+
const height = Number(match[2]);
|
|
4387
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
4388
|
+
continue;
|
|
4389
|
+
}
|
|
4390
|
+
const index = match.index ?? 0;
|
|
4391
|
+
const context = text.slice(Math.max(0, index - 48), Math.min(text.length, index + match[0].length + 48));
|
|
4392
|
+
const hasExplicitRatioContext = /\b(?:aspect|ratio|format|resolution|size|dimensions?|portrait|landscape|widescreen|horizontal|vertical)\b/i.test(context);
|
|
4393
|
+
if (!hasExplicitRatioContext && width <= 3 && height >= 10 && height < 60) {
|
|
4394
|
+
continue;
|
|
4395
|
+
}
|
|
4396
|
+
if (/\b(?:ratio|aspect|format|resolution|size|dimensions?|portrait|landscape|vertical|horizontal|widescreen|frame|image|photo|picture|video|clip|ad|advert|commercial|promo|story\s*board|storyboard|tiktok|tik\s*tok|reels?|shorts?|instagram|output)\b/i.test(context)) {
|
|
4397
|
+
return { width, height, text: match[0] };
|
|
4398
|
+
}
|
|
1110
4399
|
}
|
|
1111
4400
|
return null;
|
|
1112
4401
|
}
|
|
4402
|
+
function timeRangeLooksLikeSourceMediaWindow(text, match) {
|
|
4403
|
+
const start = match.index ?? 0;
|
|
4404
|
+
const end = start + match[0].length;
|
|
4405
|
+
const context = text.slice(Math.max(0, start - 96), Math.min(text.length, end + 96));
|
|
4406
|
+
return /\b(?:audio|music|song|sound|track)\s+(?:file|clip|track|source|upload|reference)\b/i.test(context)
|
|
4407
|
+
|| /\b(?:file|clip|track|source|upload|reference)\s+(?:audio|music|song|sound|track)\b/i.test(context)
|
|
4408
|
+
|| /\b(?:from|using|between|inside|within|in)\b[\s\S]{0,60}\b(?:audio|music|song|sound|track|clip|file)\b/i.test(context)
|
|
4409
|
+
|| /\b(?:audio|music|song|sound|track|clip|file)\b[\s\S]{0,60}\b(?:from|using|between|inside|within|in)\b/i.test(context);
|
|
4410
|
+
}
|
|
1113
4411
|
export function inferRequestedTotalVideoDurationSeconds(text) {
|
|
1114
|
-
const
|
|
1115
|
-
for (const match of text.matchAll(/\b(\d{1,3}(?:\.\d+)?)\s*(?:minutes?|mins?)\b/gi)) {
|
|
4412
|
+
const explicitDurations = [];
|
|
4413
|
+
for (const match of text.matchAll(/\b(\d{1,3}(?:\.\d+)?)\s*[- ]?\s*(?:minutes?|mins?)\b/gi)) {
|
|
1116
4414
|
if (match.index !== undefined && text[match.index - 1] === '%')
|
|
1117
4415
|
continue;
|
|
1118
4416
|
const minutes = Number(match[1]);
|
|
1119
4417
|
if (Number.isFinite(minutes) && minutes > 0)
|
|
1120
|
-
|
|
4418
|
+
explicitDurations.push(Math.ceil(minutes * 60));
|
|
1121
4419
|
}
|
|
1122
|
-
for (const match of text.matchAll(/\b(\d{1,3}(?:\.\d+)?)\s*(?:s|sec|secs|seconds?)\b/gi)) {
|
|
4420
|
+
for (const match of text.matchAll(/\b(\d{1,3}(?:\.\d+)?)\s*[- ]?\s*(?:s|sec|secs|seconds?)\b/gi)) {
|
|
1123
4421
|
if (match.index !== undefined && text[match.index - 1] === '%')
|
|
1124
4422
|
continue;
|
|
1125
4423
|
const seconds = Number(match[1]);
|
|
1126
4424
|
if (Number.isFinite(seconds) && seconds > 0)
|
|
1127
|
-
|
|
4425
|
+
explicitDurations.push(seconds);
|
|
1128
4426
|
}
|
|
4427
|
+
if (explicitDurations.length > 0)
|
|
4428
|
+
return Math.max(...explicitDurations);
|
|
4429
|
+
const timeRangeDurations = [];
|
|
1129
4430
|
for (const match of text.matchAll(/\b(\d{1,2}):([0-5]\d)(?:\.\d+)?\s*(?:-|–|—|\bto\b)\s*(\d{1,2}):([0-5]\d)(?:\.\d+)?\b/gi)) {
|
|
1130
4431
|
if (match.index !== undefined && text[match.index - 1] === '%')
|
|
1131
4432
|
continue;
|
|
1132
4433
|
const start = (Number(match[1]) * 60) + Number(match[2]);
|
|
1133
4434
|
const end = (Number(match[3]) * 60) + Number(match[4]);
|
|
1134
4435
|
if (Number.isFinite(start) && Number.isFinite(end) && end > start && end <= 600) {
|
|
1135
|
-
|
|
4436
|
+
timeRangeDurations.push(timeRangeLooksLikeSourceMediaWindow(text, match) ? end - start : end);
|
|
1136
4437
|
}
|
|
1137
4438
|
}
|
|
1138
|
-
return
|
|
4439
|
+
return timeRangeDurations.length > 0 ? Math.max(...timeRangeDurations) : null;
|
|
1139
4440
|
}
|
|
1140
4441
|
export function textProvidesLiteralVideoPrompt(text) {
|
|
1141
4442
|
return /\b(?:full|exact|literal)\s+prompt\b/i.test(text)
|
|
@@ -1420,6 +4721,21 @@ export function textRequestsProfessionalCharacterSheetImage(text) {
|
|
|
1420
4721
|
|| new RegExp(String.raw `\b${sheetArtifact}\b[\s\S]{0,120}\b(?:image|illustration|artwork|render|board|layout)\b`, 'i').test(normalized)
|
|
1421
4722
|
|| new RegExp(String.raw `\b(?:need|want|would\s+like|looking\s+for|please|can\s+you|could\s+you)\b[\s\S]{0,120}\b${sheetArtifact}\b`, 'i').test(normalized);
|
|
1422
4723
|
}
|
|
4724
|
+
function textRequestsDirectVideoOutput(text) {
|
|
4725
|
+
const videoGenerationVerbs = String.raw `(?:generate|create|make|render|produce|turn|animate|convert|transform)`;
|
|
4726
|
+
const videoOutputNouns = String.raw `(?:videos?|clips?|animations?|movies?|films?)`;
|
|
4727
|
+
return new RegExp(String.raw `\b${videoGenerationVerbs}\b[\s\S]{0,140}\b${videoOutputNouns}\b`, 'i').test(text)
|
|
4728
|
+
|| new RegExp(String.raw `\b${videoOutputNouns}\b[\s\S]{0,140}\b${videoGenerationVerbs}\b`, 'i').test(text);
|
|
4729
|
+
}
|
|
4730
|
+
function textHasExplicitStoryboardPlanningWorkflow(text) {
|
|
4731
|
+
const planningNoun = String.raw `(?:script|screenplay|story\s*board|storyboard|shot\s*list|beat\s*sheet|treatment|story\s+beats?|video\s+plan|creative\s+brief)`;
|
|
4732
|
+
const planningVerb = String.raw `(?:write|draft|develop|outline|plan|map\s*out|break\s*down)`;
|
|
4733
|
+
return new RegExp(String.raw `\b${planningVerb}\b[\s\S]{0,140}\b${planningNoun}\b`, 'i').test(text)
|
|
4734
|
+
|| new RegExp(String.raw `\b${planningNoun}\b[\s\S]{0,120}\b(?:to\s+develop|for\s+review|for\s+approval|before|first|then|next|subsequent|subsequently|later)\b`, 'i').test(text)
|
|
4735
|
+
|| /\bnew\s+script\s+to\s+develop\b/i.test(text)
|
|
4736
|
+
|| /\b(?:construct|build|develop|map\s*out|plan)\b[\s\S]{0,100}\b(?:using|with)\s+(?:\d{1,2}|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s+(?:beats?|scenes?|panels?)\b/i.test(text)
|
|
4737
|
+
|| /\b(?:video\s+story\s*board|video\s+storyboard|story\s*board\s+sequence|storyboard\s+sequence)\b[\s\S]{0,140}\b(?:model[-\s]?ready|prompt[-\s]?ready|production[-\s]?ready|enough\s+details?|generate\s+the\s+entire\s+video|story\s+spine|for\s+the\s+model|script|plan|breakdown)\b/i.test(text);
|
|
4738
|
+
}
|
|
1423
4739
|
export function textRequestsSingleCompositeImageOutput(text) {
|
|
1424
4740
|
const normalized = text
|
|
1425
4741
|
.replace(/[“”]/g, '"')
|
|
@@ -1429,9 +4745,7 @@ export function textRequestsSingleCompositeImageOutput(text) {
|
|
|
1429
4745
|
if (!normalized)
|
|
1430
4746
|
return false;
|
|
1431
4747
|
const generationVerbs = String.raw `(?:generate|create|make|render|produce|design|build|develop|draw)`;
|
|
1432
|
-
const videoGenerationVerbs = String.raw `(?:generate|create|make|render|produce|turn|animate|convert|transform)`;
|
|
1433
4748
|
const imageOutputNouns = String.raw `(?:images?|photos?|pictures?|portraits?|posters?|artwork|illustrations?)`;
|
|
1434
|
-
const videoOutputNouns = String.raw `(?:videos?|clips?|animations?|movies?|films?)`;
|
|
1435
4749
|
const compositeNouns = String.raw `(?:story\s*board|storyboard|collage|contact\s+sheet|mood\s*board|moodboard|grid|board)`;
|
|
1436
4750
|
const adCreativeNouns = String.raw `(?:ads?|advertisements?|banners?|flyers?|posters?|social\s+posts?|campaign\s+creative|marketing\s+(?:creative|graphic)|promo\s+graphic|product\s+graphic)`;
|
|
1437
4751
|
const characterSheetImageStage = textRequestsProfessionalCharacterSheetImage(normalized);
|
|
@@ -1454,8 +4768,7 @@ export function textRequestsSingleCompositeImageOutput(text) {
|
|
|
1454
4768
|
&& !/\b(?:story\s*board|storyboard)\b[\s\S]{0,60}\bfirst\b/i.test(normalized)) {
|
|
1455
4769
|
return false;
|
|
1456
4770
|
}
|
|
1457
|
-
const directVideoOutput =
|
|
1458
|
-
|| new RegExp(String.raw `\b${videoOutputNouns}\b[\s\S]{0,140}\b${videoGenerationVerbs}\b`, 'i').test(normalized);
|
|
4771
|
+
const directVideoOutput = textRequestsDirectVideoOutput(normalized);
|
|
1459
4772
|
const rejectsStoryboardPanelOutput = /\b(?:no|without)\s+(?:extra\s+|random\s+|visible\s+|generated\s+|output\s+)?(?:story\s*board|storyboard)\s+panels?\b/i.test(normalized)
|
|
1460
4773
|
|| /\b(?:avoid|exclude|never|don't|do\s+not)\b[\s\S]{0,80}\b(?:story\s*board|storyboard)\s+panels?\b/i.test(normalized)
|
|
1461
4774
|
|| /\bnot\s+(?:a\s+|the\s+|as\s+)?(?:story\s*board|storyboard)\s+panels?\b/i.test(normalized)
|
|
@@ -1472,6 +4785,13 @@ export function textRequestsSingleCompositeImageOutput(text) {
|
|
|
1472
4785
|
const referenceGuidedAdCreative = new RegExp(String.raw `\b${generationVerbs}\b[\s\S]{0,140}\b${adCreativeNouns}\b`, 'i').test(normalized)
|
|
1473
4786
|
&& /\b(?:referenc(?:e|es|ed|ing)|use|using|include|incorporate|based\s+on|guided\s+by|with)\b[\s\S]{0,120}\b(?:uploaded|attached|provided|reference|source|input|assets?|images?|photos?|pictures?)\b/i.test(normalized)
|
|
1474
4787
|
&& !/\b(?:videos?|clips?|animations?|movies?|films?|commercials?)\b/i.test(normalized);
|
|
4788
|
+
if (directVideoOutput
|
|
4789
|
+
&& !explicitStoryboardImageOrSheetRequest
|
|
4790
|
+
&& !textHasExplicitStoryboardPlanningWorkflow(normalized)
|
|
4791
|
+
&& !referenceGuidedAdCreative
|
|
4792
|
+
&& !characterSheetImageStage) {
|
|
4793
|
+
return false;
|
|
4794
|
+
}
|
|
1475
4795
|
const storyboardImageMentionIsCaptionSource = standaloneCompositeImageMention
|
|
1476
4796
|
&& /\b(?:voice\s*over|voiceover|captions?|text|copy|dialogue|lines?)\b[\s\S]{0,180}\b(?:read|shown|displayed|under|below|from|in|on)\b[\s\S]{0,120}\b(?:story\s*board|storyboard)\s+images?\b/i.test(normalized);
|
|
1477
4797
|
if (directVideoOutput
|
|
@@ -1547,8 +4867,16 @@ export function textRequestsPreproductionScriptStage(text) {
|
|
|
1547
4867
|
const asksForPlanning = new RegExp(String.raw `\b${planningVerb}\b[\s\S]{0,140}\b${planningNoun}\b`, 'i').test(normalized)
|
|
1548
4868
|
|| new RegExp(String.raw `\b${planningNoun}\b[\s\S]{0,120}\b(?:to\s+develop|for\s+review|for\s+approval|before|first|then|next|subsequent|subsequently|later)\b`, 'i').test(normalized)
|
|
1549
4869
|
|| /\bnew\s+script\s+to\s+develop\b/i.test(normalized);
|
|
1550
|
-
|
|
4870
|
+
const creativeStoryTerm = String.raw `(?:storyline|monologue|dialogue|script|skit|comedy|jokes?|punchline|payoff|twist|beats?)`;
|
|
4871
|
+
const asksForCreativeStoryDevelopment = new RegExp(String.raw `\b(?:story\s*board|storyboard)\b[\s\S]{0,220}\b${creativeStoryTerm}\b`, 'i').test(normalized)
|
|
4872
|
+
|| new RegExp(String.raw `\b${creativeStoryTerm}\b[\s\S]{0,220}\b(?:story\s*board|storyboard)\b`, 'i').test(normalized);
|
|
4873
|
+
if (!asksForPlanning && !asksForCreativeStoryDevelopment)
|
|
1551
4874
|
return false;
|
|
4875
|
+
if (textRequestsDirectVideoOutput(normalized)
|
|
4876
|
+
&& !textHasExplicitStoryboardPlanningWorkflow(normalized)
|
|
4877
|
+
&& !asksForCreativeStoryDevelopment) {
|
|
4878
|
+
return false;
|
|
4879
|
+
}
|
|
1552
4880
|
const downstreamMediaContext = /\b(?:video|clip|animation|movie|film|seedance|ltx|image|keyframe|storyboard\s+image|storyboard\s+sheet|model|generation|generate|render|animate)\b/i.test(normalized)
|
|
1553
4881
|
|| /\b(?:used\s+by|enough\s+details?|production\s+ready|prompt-ready|model-ready)\b/i.test(normalized);
|
|
1554
4882
|
return downstreamMediaContext;
|
|
@@ -1759,6 +5087,7 @@ export const STORYBOARD_PLANNING_CONTRACT_JSON_SCHEMA = {
|
|
|
1759
5087
|
storyboardCellAspectRatio: { type: 'string' },
|
|
1760
5088
|
targetVideoAspectRatio: { type: 'string' },
|
|
1761
5089
|
boardDimensions: { type: 'string' },
|
|
5090
|
+
storyboardCanvasSpecifiedByUser: { type: 'boolean' },
|
|
1762
5091
|
},
|
|
1763
5092
|
required: [
|
|
1764
5093
|
'source',
|
|
@@ -1766,6 +5095,7 @@ export const STORYBOARD_PLANNING_CONTRACT_JSON_SCHEMA = {
|
|
|
1766
5095
|
'storyboardCellAspectRatio',
|
|
1767
5096
|
'targetVideoAspectRatio',
|
|
1768
5097
|
'boardDimensions',
|
|
5098
|
+
'storyboardCanvasSpecifiedByUser',
|
|
1769
5099
|
],
|
|
1770
5100
|
},
|
|
1771
5101
|
scenes: {
|
|
@@ -1832,8 +5162,8 @@ export function buildStoryboardPlanningResponseFormat(name = 'preproduction_stor
|
|
|
1832
5162
|
},
|
|
1833
5163
|
};
|
|
1834
5164
|
}
|
|
1835
|
-
export const STORYBOARD_DEFAULT_MIN_FRAMES =
|
|
1836
|
-
export const STORYBOARD_DEFAULT_MAX_FRAMES =
|
|
5165
|
+
export const STORYBOARD_DEFAULT_MIN_FRAMES = 6;
|
|
5166
|
+
export const STORYBOARD_DEFAULT_MAX_FRAMES = 16;
|
|
1837
5167
|
const STORYBOARD_COUNT_WORDS = {
|
|
1838
5168
|
one: 1,
|
|
1839
5169
|
two: 2,
|
|
@@ -1923,6 +5253,15 @@ function inferStoryboardAspectNearUnit(text, unitPattern, rejectBetweenPattern)
|
|
|
1923
5253
|
const candidates = [];
|
|
1924
5254
|
for (const match of text.matchAll(aspectPattern)) {
|
|
1925
5255
|
const aspect = match[1];
|
|
5256
|
+
const numericAspect = aspect.match(/^(\d{1,4})\s*:\s*(\d{1,4})$/);
|
|
5257
|
+
if (numericAspect && ratioMatchLooksLikeMediaTimecodeRange(text, {
|
|
5258
|
+
...match,
|
|
5259
|
+
0: match[0],
|
|
5260
|
+
1: numericAspect[1],
|
|
5261
|
+
2: numericAspect[2],
|
|
5262
|
+
})) {
|
|
5263
|
+
continue;
|
|
5264
|
+
}
|
|
1926
5265
|
const ratio = ratioFromStoryboardAspectWords(aspect);
|
|
1927
5266
|
if (!ratio)
|
|
1928
5267
|
continue;
|
|
@@ -1984,6 +5323,14 @@ function inferExplicitStoryboardTargetVideoAspectRatio(text) {
|
|
|
1984
5323
|
const between = explicitTarget[1] ?? '';
|
|
1985
5324
|
if (/\b(?:story\s*board|storyboard|board|canvas|page|sheet|poster)\b/i.test(between))
|
|
1986
5325
|
continue;
|
|
5326
|
+
const fullMatch = explicitTarget[0] ?? '';
|
|
5327
|
+
const ratioInMatch = fullMatch.match(/(\d{1,4})\s*:\s*(\d{1,4})\s*$/);
|
|
5328
|
+
if (ratioInMatch?.index !== undefined && ratioMatchLooksLikeMediaTimecodeRange(text, {
|
|
5329
|
+
...ratioInMatch,
|
|
5330
|
+
index: (explicitTarget.index ?? 0) + ratioInMatch.index,
|
|
5331
|
+
})) {
|
|
5332
|
+
continue;
|
|
5333
|
+
}
|
|
1987
5334
|
const width = Number(explicitTarget[2]);
|
|
1988
5335
|
const height = Number(explicitTarget[3]);
|
|
1989
5336
|
if (width > 0 && height > 0)
|
|
@@ -2107,20 +5454,30 @@ function normalizeStoryboardBoardDimensions(value) {
|
|
|
2107
5454
|
function storyboardPlanningSourceFromContract(contract) {
|
|
2108
5455
|
return contract?.layout?.source ?? contract?.source ?? 'fallback_text';
|
|
2109
5456
|
}
|
|
2110
|
-
function applyStoryboardPlanningLayoutContract(fallback, contract, frameCount) {
|
|
5457
|
+
function applyStoryboardPlanningLayoutContract(fallback, contract, frameCount, userDefinedCanvas) {
|
|
2111
5458
|
const layoutContract = contract?.layout;
|
|
2112
5459
|
if (!layoutContract)
|
|
2113
5460
|
return fallback;
|
|
2114
|
-
const
|
|
2115
|
-
|
|
5461
|
+
const layoutSource = storyboardPlanningSourceFromContract(contract);
|
|
5462
|
+
const contractUserDefinedCanvas = typeof layoutContract.storyboardCanvasSpecifiedByUser === 'boolean'
|
|
5463
|
+
? layoutContract.storyboardCanvasSpecifiedByUser
|
|
5464
|
+
: userDefinedCanvas;
|
|
5465
|
+
const contractOwnsCanvas = contractUserDefinedCanvas
|
|
5466
|
+
|| layoutSource === 'assistant_metadata'
|
|
5467
|
+
|| layoutSource === 'user_schema';
|
|
5468
|
+
const boardAspectRatio = contractOwnsCanvas
|
|
5469
|
+
? normalizeAspectRatio(layoutContract.storyboardCanvasAspectRatio) ?? fallback.boardAspectRatio
|
|
5470
|
+
: fallback.boardAspectRatio;
|
|
2116
5471
|
const cellAspectRatio = normalizeAspectRatio(layoutContract.storyboardCellAspectRatio)
|
|
2117
5472
|
?? fallback.cellAspectRatio;
|
|
2118
5473
|
const targetVideoAspectRatio = normalizeAspectRatio(layoutContract.targetVideoAspectRatio)
|
|
2119
5474
|
?? cellAspectRatio
|
|
2120
5475
|
?? fallback.targetVideoAspectRatio;
|
|
2121
5476
|
const layout = describeStoryboardLayout(boardAspectRatio, cellAspectRatio, frameCount);
|
|
2122
|
-
const boardDimensions =
|
|
2123
|
-
|
|
5477
|
+
const boardDimensions = contractOwnsCanvas
|
|
5478
|
+
? normalizeStoryboardBoardDimensions(layoutContract.boardDimensions)
|
|
5479
|
+
?? (userDefinedCanvas ? fallback.boardDimensions : undefined)
|
|
5480
|
+
: fallback.boardDimensions;
|
|
2124
5481
|
return {
|
|
2125
5482
|
boardAspectRatio,
|
|
2126
5483
|
cellAspectRatio,
|
|
@@ -2130,8 +5487,14 @@ function applyStoryboardPlanningLayoutContract(fallback, contract, frameCount) {
|
|
|
2130
5487
|
};
|
|
2131
5488
|
}
|
|
2132
5489
|
export function inferStoryboardLayoutSpec(userIntentText, frameCount, planningContract) {
|
|
2133
|
-
const
|
|
2134
|
-
const
|
|
5490
|
+
const planningLayout = planningContract?.layout;
|
|
5491
|
+
const userDefinedCanvas = typeof planningLayout?.storyboardCanvasSpecifiedByUser === 'boolean'
|
|
5492
|
+
? planningLayout.storyboardCanvasSpecifiedByUser
|
|
5493
|
+
: userDefinedStoryboardCanvas(userIntentText);
|
|
5494
|
+
const explicitPixels = userDefinedCanvas ? inferExplicitStoryboardCanvasPixelDimensions(userIntentText) : null;
|
|
5495
|
+
const boardAspectRatio = userDefinedCanvas
|
|
5496
|
+
? inferStoryboardBoardAspectRatio(userIntentText)
|
|
5497
|
+
: '16:9';
|
|
2135
5498
|
const explicitTargetVideoAspectRatio = inferExplicitStoryboardTargetVideoAspectRatio(userIntentText);
|
|
2136
5499
|
const inferredTargetVideoAspectRatio = explicitTargetVideoAspectRatio
|
|
2137
5500
|
?? inferStoryboardTargetVideoAspectRatio(userIntentText, boardAspectRatio);
|
|
@@ -2143,13 +5506,20 @@ export function inferStoryboardLayoutSpec(userIntentText, frameCount, planningCo
|
|
|
2143
5506
|
cellAspectRatio,
|
|
2144
5507
|
targetVideoAspectRatio,
|
|
2145
5508
|
...layout,
|
|
2146
|
-
...(explicitPixels
|
|
5509
|
+
...(explicitPixels
|
|
5510
|
+
? { boardDimensions: `${explicitPixels.width}x${explicitPixels.height}` }
|
|
5511
|
+
: userDefinedCanvas
|
|
5512
|
+
? {}
|
|
5513
|
+
: { boardDimensions: GPT_IMAGE_STORYBOARD_DEFAULTS.storyboardLandscape.aspectRatio }),
|
|
2147
5514
|
};
|
|
2148
|
-
return applyStoryboardPlanningLayoutContract(fallback, planningContract, frameCount);
|
|
5515
|
+
return applyStoryboardPlanningLayoutContract(fallback, planningContract, frameCount, userDefinedCanvas);
|
|
2149
5516
|
}
|
|
2150
5517
|
function clampStoryboardDefaultFrameCount(value) {
|
|
2151
5518
|
return Math.max(STORYBOARD_DEFAULT_MIN_FRAMES, Math.min(STORYBOARD_DEFAULT_MAX_FRAMES, Math.round(value)));
|
|
2152
5519
|
}
|
|
5520
|
+
function defaultStoryboardFrameCountForDuration(durationSeconds) {
|
|
5521
|
+
return clampStoryboardDefaultFrameCount(Math.ceil(durationSeconds / 2));
|
|
5522
|
+
}
|
|
2153
5523
|
function normalizeStoryboardCountToken(value) {
|
|
2154
5524
|
if (!value)
|
|
2155
5525
|
return null;
|
|
@@ -2239,20 +5609,13 @@ export function inferDefaultStoryboardFrameCountFromText(text) {
|
|
|
2239
5609
|
if (scriptCount)
|
|
2240
5610
|
return clampStoryboardDefaultFrameCount(scriptCount);
|
|
2241
5611
|
const duration = inferRequestedTotalVideoDurationSeconds(canonicalText);
|
|
5612
|
+
const durationFrameFloor = duration !== null
|
|
5613
|
+
? defaultStoryboardFrameCountForDuration(duration)
|
|
5614
|
+
: null;
|
|
2242
5615
|
const words = storyboardWordCount(canonicalText);
|
|
2243
5616
|
const complexity = storyboardComplexityScore(canonicalText);
|
|
2244
|
-
let count =
|
|
2245
|
-
?
|
|
2246
|
-
? 4
|
|
2247
|
-
: duration <= 15
|
|
2248
|
-
? 5
|
|
2249
|
-
: duration <= 20
|
|
2250
|
-
? 6
|
|
2251
|
-
: duration <= 30
|
|
2252
|
-
? 8
|
|
2253
|
-
: duration <= 45
|
|
2254
|
-
? 10
|
|
2255
|
-
: 12
|
|
5617
|
+
let count = durationFrameFloor !== null
|
|
5618
|
+
? durationFrameFloor
|
|
2256
5619
|
: words <= 28
|
|
2257
5620
|
? 4
|
|
2258
5621
|
: words <= 70
|
|
@@ -2268,6 +5631,8 @@ export function inferDefaultStoryboardFrameCountFromText(text) {
|
|
|
2268
5631
|
}
|
|
2269
5632
|
if (duration === null && words > 180)
|
|
2270
5633
|
count += 1;
|
|
5634
|
+
if (durationFrameFloor !== null)
|
|
5635
|
+
count = Math.max(count, durationFrameFloor);
|
|
2271
5636
|
return clampStoryboardDefaultFrameCount(count);
|
|
2272
5637
|
}
|
|
2273
5638
|
function inferReferencedStoryboardImageCount(text) {
|
|
@@ -2696,7 +6061,7 @@ function buildStoryboardSourceBriefForPrompt(prompt, userIntentText, approvedScr
|
|
|
2696
6061
|
if (canonicalApprovedScriptContext) {
|
|
2697
6062
|
parts.push(`APPROVED STORYBOARD SCRIPT CONTEXT TO PRESERVE:\n${canonicalApprovedScriptContext}`);
|
|
2698
6063
|
}
|
|
2699
|
-
return parts.filter(Boolean).join('\n\n');
|
|
6064
|
+
return stripGenericStoryboardVisibleTextMetadata(parts.filter(Boolean).join('\n\n'));
|
|
2700
6065
|
}
|
|
2701
6066
|
function buildStoryboardUserConstraintSource(userIntentText, primarySourceBrief, options) {
|
|
2702
6067
|
const canonicalApprovedScriptContext = canonicalStoryboardScriptContext(options.approvedScriptContext);
|
|
@@ -2757,8 +6122,25 @@ function storyboardTextCandidateLooksLikeGenericProductionLabel(value) {
|
|
|
2757
6122
|
.trim();
|
|
2758
6123
|
return /^(?:visible\s+text|on[-\s]?screen\s+text|onscreen\s+text|text\s+overlay|title\s+card|caption|subtitle|super|copy|cta|tagline|headline)$/i.test(withoutTiming);
|
|
2759
6124
|
}
|
|
6125
|
+
function stripGenericStoryboardVisibleTextMetadata(value) {
|
|
6126
|
+
if (!value)
|
|
6127
|
+
return '';
|
|
6128
|
+
return value
|
|
6129
|
+
.replace(/\s*(?:<br\s*\/?>\s*)?\b(?:visible\s+text|on[-\s]?screen\s+text|onscreen\s+text|text)\s*:\s*([^|\n\r<]+)/gi, (match, candidate) => storyboardTextCandidateLooksLikeGenericProductionLabel(candidate)
|
|
6130
|
+
? ''
|
|
6131
|
+
: match)
|
|
6132
|
+
.replace(/[ \t]+(?=\n)/g, '')
|
|
6133
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
6134
|
+
.trim();
|
|
6135
|
+
}
|
|
6136
|
+
function normalizeStoryboardVisibleText(value) {
|
|
6137
|
+
const compact = compactStoryboardLine(stripGenericStoryboardVisibleTextMetadata(value));
|
|
6138
|
+
if (!compact || storyboardTextCandidateLooksLikeGenericProductionLabel(compact))
|
|
6139
|
+
return '';
|
|
6140
|
+
return compact;
|
|
6141
|
+
}
|
|
2760
6142
|
function normalizeStoryboardDialogue(value) {
|
|
2761
|
-
const compact = compactStoryboardLine(value);
|
|
6143
|
+
const compact = compactStoryboardLine(stripGenericStoryboardVisibleTextMetadata(value));
|
|
2762
6144
|
if (!compact)
|
|
2763
6145
|
return '';
|
|
2764
6146
|
if (/^[-\u2013\u2014]\.?$/.test(compact))
|
|
@@ -2769,6 +6151,9 @@ function normalizeStoryboardDialogue(value) {
|
|
|
2769
6151
|
const visibleTextField = compact.match(/^(?:visible\s+text|on[-\s]?screen\s+text|onscreen\s+text|text|cta|tagline|headline|title\s+card|copy)\s*:\s*(.+)$/i)?.[1];
|
|
2770
6152
|
if (visibleTextField !== undefined)
|
|
2771
6153
|
return '';
|
|
6154
|
+
const audioField = compact.match(/^(?:audio\s*\/\s*sfx|audio\s*\/\s*foley|foley\s*\/\s*sfx|audio|sfx|fx|foley|sound|sounds|music)\s*:\s*(.+)$/i)?.[1];
|
|
6155
|
+
if (audioField !== undefined)
|
|
6156
|
+
return '';
|
|
2772
6157
|
if (/^text\s+only\s*:/i.test(compact))
|
|
2773
6158
|
return '';
|
|
2774
6159
|
const quoted = extractQuotedDialogueSegments(compact)
|
|
@@ -3035,8 +6420,8 @@ function splitStoryboardTableSections(text) {
|
|
|
3035
6420
|
: storyboardTableHeaderMatches(dialogueHeader, /\b(?:dialogue|vo|v\.o\.|voiceover|speech|narration)\b/i)
|
|
3036
6421
|
? normalizeStoryboardDialogue(extractQuotedDialogueSegments(dialogueCell || audioCell)[0] || dialogueCell || audioCell)
|
|
3037
6422
|
: '';
|
|
3038
|
-
const visibleText = extractStoryboardField(audioCell, ['Visible text', 'On-screen text', 'Onscreen text', 'Text', 'CTA'])
|
|
3039
|
-
|| (visibleTextHeaderIndex >= 0 ? compactStoryboardLine(visibleTextCell) : '');
|
|
6423
|
+
const visibleText = normalizeStoryboardVisibleText(extractStoryboardField(audioCell, ['Visible text', 'On-screen text', 'Onscreen text', 'Text', 'CTA'])
|
|
6424
|
+
|| (visibleTextHeaderIndex >= 0 ? compactStoryboardLine(visibleTextCell) : ''));
|
|
3040
6425
|
const audio = extractStoryboardField(audioCell, ['Audio/SFX', 'Audio', 'SFX', 'FX', 'Foley', 'Sound', 'Sounds'])
|
|
3041
6426
|
|| storyboardTableCellWithoutDialogue(soundCell || audioCell, dialogue);
|
|
3042
6427
|
const number = sections.length + 1;
|
|
@@ -3094,6 +6479,10 @@ function splitStoryboardSceneSections(text) {
|
|
|
3094
6479
|
function splitStoryboardSections(text) {
|
|
3095
6480
|
const sectionHeadings = splitStoryboardSceneSections(text);
|
|
3096
6481
|
const tableSections = splitStoryboardTableSections(text);
|
|
6482
|
+
const explicitFrameCount = inferExplicitStoryboardFrameCountFromText(text);
|
|
6483
|
+
if (tableSections.length > 0 && explicitFrameCount !== null && tableSections.length === explicitFrameCount) {
|
|
6484
|
+
return tableSections;
|
|
6485
|
+
}
|
|
3097
6486
|
if (tableSections.length > 0 && tableSections.length >= sectionHeadings.length) {
|
|
3098
6487
|
return tableSections;
|
|
3099
6488
|
}
|
|
@@ -3351,23 +6740,14 @@ function applyStoryboardScenePlanningContract(scene, planningContract) {
|
|
|
3351
6740
|
metadataLabels,
|
|
3352
6741
|
};
|
|
3353
6742
|
}
|
|
3354
|
-
function buildFallbackScenes(frameCount,
|
|
3355
|
-
const sceneDuration = durationSec && frameCount > 0
|
|
3356
|
-
? Math.round((durationSec / frameCount) * 100) / 100
|
|
3357
|
-
: null;
|
|
6743
|
+
function buildFallbackScenes(frameCount, _durationSec, sourceText) {
|
|
3358
6744
|
return Array.from({ length: frameCount }, (_, index) => {
|
|
3359
|
-
const startSec = sceneDuration !== null ? Math.round(index * sceneDuration * 100) / 100 : null;
|
|
3360
|
-
const endSec = sceneDuration !== null
|
|
3361
|
-
? index === frameCount - 1 && durationSec !== null
|
|
3362
|
-
? durationSec
|
|
3363
|
-
: Math.round(((index + 1) * sceneDuration) * 100) / 100
|
|
3364
|
-
: null;
|
|
3365
6745
|
return {
|
|
3366
6746
|
id: `scene_${String(index + 1).padStart(2, '0')}`,
|
|
3367
6747
|
title: `Frame ${String(index + 1).padStart(2, '0')}`,
|
|
3368
|
-
startSec,
|
|
3369
|
-
endSec,
|
|
3370
|
-
durationSec:
|
|
6748
|
+
startSec: null,
|
|
6749
|
+
endSec: null,
|
|
6750
|
+
durationSec: null,
|
|
3371
6751
|
purpose: index === 0
|
|
3372
6752
|
? 'Establish the hook and the first clear story beat from the source brief.'
|
|
3373
6753
|
: 'Advance the same story spine with a distinct sequential beat.',
|
|
@@ -3753,18 +7133,60 @@ function inferEndCard(projectText, references, requiredText) {
|
|
|
3753
7133
|
composition: extractStoryboardField(endBlock, ['Composition', 'Layout', 'Camera']) || compactStoryboardLine(endBlock.slice(0, 180)),
|
|
3754
7134
|
};
|
|
3755
7135
|
}
|
|
7136
|
+
function storyboardSceneLooksLikeEndCard(scene) {
|
|
7137
|
+
return /\b(?:end\s*card|final\s*(?:card|frame|scene)|closing|outro|stinger|cta|call\s*to\s*action|logo\s*(?:lockup|reveal|hold)|brand\s*(?:resolve|lockup))\b/i.test([
|
|
7138
|
+
scene.title,
|
|
7139
|
+
scene.purpose,
|
|
7140
|
+
scene.productFeature,
|
|
7141
|
+
scene.visual,
|
|
7142
|
+
scene.action,
|
|
7143
|
+
].join(' '));
|
|
7144
|
+
}
|
|
7145
|
+
function finalStoryboardSceneVisibleText(scenes) {
|
|
7146
|
+
const finalScene = scenes[scenes.length - 1];
|
|
7147
|
+
if (finalScene?.textInImage.length)
|
|
7148
|
+
return uniqueStoryboardStrings(finalScene.textInImage);
|
|
7149
|
+
for (let index = scenes.length - 1; index >= 0; index -= 1) {
|
|
7150
|
+
const scene = scenes[index];
|
|
7151
|
+
if (scene.textInImage.length > 0 && storyboardSceneLooksLikeEndCard(scene)) {
|
|
7152
|
+
return uniqueStoryboardStrings(scene.textInImage);
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7155
|
+
return [];
|
|
7156
|
+
}
|
|
7157
|
+
function storyboardRequiredTextForProject(options, userConstraintSource, scenes) {
|
|
7158
|
+
const contractEndCardText = normalizeStoryboardContractTextArray(options.planningContract?.endCard?.visibleText);
|
|
7159
|
+
const extractedRequiredText = extractStoryboardRequiredText(userConstraintSource);
|
|
7160
|
+
const sceneVisibleText = uniqueStoryboardStrings(scenes.flatMap(scene => scene.textInImage));
|
|
7161
|
+
if (options.promptAuthorship === 'assistant' && sceneVisibleText.length > 0) {
|
|
7162
|
+
const scopedEndCardText = finalStoryboardSceneVisibleText(scenes);
|
|
7163
|
+
const endCardText = scopedEndCardText.length > 0 ? scopedEndCardText : contractEndCardText;
|
|
7164
|
+
return {
|
|
7165
|
+
mustIncludeText: uniqueStoryboardStrings([...sceneVisibleText, ...contractEndCardText]),
|
|
7166
|
+
endCardText,
|
|
7167
|
+
};
|
|
7168
|
+
}
|
|
7169
|
+
const requiredText = uniqueStoryboardStrings([
|
|
7170
|
+
...extractedRequiredText,
|
|
7171
|
+
...contractEndCardText,
|
|
7172
|
+
]);
|
|
7173
|
+
return {
|
|
7174
|
+
mustIncludeText: requiredText,
|
|
7175
|
+
endCardText: requiredText,
|
|
7176
|
+
};
|
|
7177
|
+
}
|
|
3756
7178
|
export function buildStoryboardProject(options) {
|
|
3757
7179
|
const prompt = options.prompt.trim();
|
|
3758
7180
|
const rawUserIntentText = options.userIntentText.trim();
|
|
3759
7181
|
const userIntentText = canonicalStoryboardScriptContext(rawUserIntentText) || rawUserIntentText;
|
|
3760
7182
|
const approvedScriptContext = canonicalStoryboardScriptContext(options.approvedScriptContext);
|
|
3761
7183
|
const primarySourceBrief = selectStoryboardSourceBrief(prompt, userIntentText);
|
|
3762
|
-
const sourceText = sanitizeStoryboardExternalAudioReferences([
|
|
7184
|
+
const sourceText = stripGenericStoryboardVisibleTextMetadata(sanitizeStoryboardExternalAudioReferences([
|
|
3763
7185
|
primarySourceBrief,
|
|
3764
7186
|
approvedScriptContext
|
|
3765
7187
|
? `APPROVED STORYBOARD SCRIPT CONTEXT TO PRESERVE:\n${approvedScriptContext}`
|
|
3766
7188
|
: '',
|
|
3767
|
-
].filter(Boolean).join('\n\n'));
|
|
7189
|
+
].filter(Boolean).join('\n\n')));
|
|
3768
7190
|
const allText = `${userIntentText}\n${sourceText}`;
|
|
3769
7191
|
const layoutTextParts = [userIntentText].filter(Boolean);
|
|
3770
7192
|
if (primarySourceBrief
|
|
@@ -3790,6 +7212,9 @@ export function buildStoryboardProject(options) {
|
|
|
3790
7212
|
const sourceSections = splitStoryboardSections(sourceText);
|
|
3791
7213
|
const approvedSectionsHaveExplicitTiming = storyboardSectionsHavePreservableExplicitTiming(approvedSections);
|
|
3792
7214
|
const sourceSectionsHaveExplicitTiming = storyboardSectionsHavePreservableExplicitTiming(sourceSections);
|
|
7215
|
+
const exactRequestedFrameCount = inferExplicitStoryboardFrameCountFromText(userIntentText);
|
|
7216
|
+
const assistantMustHonorExactFrameCount = options.promptAuthorship === 'assistant'
|
|
7217
|
+
&& exactRequestedFrameCount === options.frameCount;
|
|
3793
7218
|
const assistantApprovedDraftUndercounted = options.promptAuthorship === 'assistant'
|
|
3794
7219
|
&& approvedSections.length > 0
|
|
3795
7220
|
&& approvedSections.length < options.frameCount
|
|
@@ -3804,37 +7229,29 @@ export function buildStoryboardProject(options) {
|
|
|
3804
7229
|
: assistantDraftUndercounted || assistantApprovedDraftUndercounted
|
|
3805
7230
|
? []
|
|
3806
7231
|
: sourceSections;
|
|
3807
|
-
const
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
: null;
|
|
3811
|
-
const scenes = sections.length > 0
|
|
3812
|
-
? sections.map((section, index) => {
|
|
3813
|
-
const fallbackTiming = equalDuration !== null
|
|
3814
|
-
? {
|
|
3815
|
-
startSec: Math.round(index * equalDuration * 100) / 100,
|
|
3816
|
-
endSec: index === sceneCountForTiming - 1 && durationSec !== null
|
|
3817
|
-
? durationSec
|
|
3818
|
-
: Math.round((index + 1) * equalDuration * 100) / 100,
|
|
3819
|
-
durationSec: index === sceneCountForTiming - 1 && durationSec !== null
|
|
3820
|
-
? Math.round((durationSec - index * equalDuration) * 100) / 100
|
|
3821
|
-
: equalDuration,
|
|
3822
|
-
}
|
|
3823
|
-
: null;
|
|
3824
|
-
return buildSceneFromSection(section, references, fallbackTiming, storyboardScenePlanningContractForIndex(options.planningContract, section.number));
|
|
7232
|
+
const parsedScenes = sections.length > 0
|
|
7233
|
+
? sections.map((section) => {
|
|
7234
|
+
return buildSceneFromSection(section, references, null, storyboardScenePlanningContractForIndex(options.planningContract, section.number));
|
|
3825
7235
|
})
|
|
3826
7236
|
: buildFallbackScenes(options.frameCount, durationSec, sourceText)
|
|
3827
7237
|
.map((scene, index) => applyStoryboardScenePlanningContract(scene, storyboardScenePlanningContractForIndex(options.planningContract, index + 1)));
|
|
7238
|
+
const scenes = assistantMustHonorExactFrameCount && parsedScenes.length !== options.frameCount
|
|
7239
|
+
? parsedScenes.length < options.frameCount
|
|
7240
|
+
? [
|
|
7241
|
+
...parsedScenes,
|
|
7242
|
+
...buildFallbackScenes(options.frameCount, durationSec, sourceText)
|
|
7243
|
+
.map((scene, index) => applyStoryboardScenePlanningContract(scene, storyboardScenePlanningContractForIndex(options.planningContract, index + 1)))
|
|
7244
|
+
.slice(parsedScenes.length),
|
|
7245
|
+
]
|
|
7246
|
+
: parsedScenes.slice(0, options.frameCount)
|
|
7247
|
+
: parsedScenes;
|
|
3828
7248
|
const timingNormalizedScenes = normalizeAssistantStoryboardSceneTiming(scenes, durationSec, options.promptAuthorship);
|
|
3829
7249
|
const dialogueAlignment = alignAssistantStoryboardDialogueWithUserSource(timingNormalizedScenes, userIntentText, options.promptAuthorship);
|
|
3830
7250
|
const normalizedScenes = dialogueAlignment.shouldRetime
|
|
3831
7251
|
? retimeStoryboardScenesForDialogue(dialogueAlignment.scenes, durationSec)
|
|
3832
7252
|
: dialogueAlignment.scenes;
|
|
3833
7253
|
const userConstraintSource = buildStoryboardUserConstraintSource(userIntentText, primarySourceBrief, options);
|
|
3834
|
-
const
|
|
3835
|
-
...extractStoryboardRequiredText(userConstraintSource),
|
|
3836
|
-
...normalizeStoryboardContractTextArray(options.planningContract?.endCard?.visibleText),
|
|
3837
|
-
]);
|
|
7254
|
+
const { mustIncludeText, endCardText } = storyboardRequiredTextForProject(options, userConstraintSource, normalizedScenes);
|
|
3838
7255
|
const voiceLines = assignVoiceLinesToScenes(normalizedScenes, sourceText);
|
|
3839
7256
|
const storySpineFallback = approvedScriptContext
|
|
3840
7257
|
|| (options.promptAuthorship === 'assistant' ? userIntentText : primarySourceBrief)
|
|
@@ -3844,6 +7261,11 @@ export function buildStoryboardProject(options) {
|
|
|
3844
7261
|
const productFeatureMap = inferStoryboardProductFeatureMap(allText, scenes);
|
|
3845
7262
|
return {
|
|
3846
7263
|
title: inferStoryboardTitle(allText),
|
|
7264
|
+
sourceProvenance: approvedScriptContext
|
|
7265
|
+
? 'approved_assistant'
|
|
7266
|
+
: options.promptAuthorship === 'assistant'
|
|
7267
|
+
? 'assistant_draft'
|
|
7268
|
+
: 'user',
|
|
3847
7269
|
durationSec,
|
|
3848
7270
|
outputAspectRatio: layout.boardAspectRatio,
|
|
3849
7271
|
frameAspectRatio: layout.cellAspectRatio,
|
|
@@ -3860,9 +7282,9 @@ export function buildStoryboardProject(options) {
|
|
|
3860
7282
|
storySpine,
|
|
3861
7283
|
toneProgression: inferStoryboardToneProgression(allText),
|
|
3862
7284
|
productFeatureMap,
|
|
3863
|
-
mustInclude:
|
|
7285
|
+
mustInclude: mustIncludeText,
|
|
3864
7286
|
mustAvoid: extractStoryboardAvoidConstraints(userConstraintSource),
|
|
3865
|
-
brandRules:
|
|
7287
|
+
brandRules: mustIncludeText.length > 0 ? mustIncludeText.map(text => `Preserve exact visible text: "${text}"`) : [],
|
|
3866
7288
|
visualQualityBar: /production[-\s]?ready|premium|commercial|cinematic|high[-\s]?end/i.test(allText)
|
|
3867
7289
|
? 'production-ready commercial storyboard sheet'
|
|
3868
7290
|
: 'clean readable storyboard sheet',
|
|
@@ -3872,7 +7294,7 @@ export function buildStoryboardProject(options) {
|
|
|
3872
7294
|
lines: voiceLines,
|
|
3873
7295
|
},
|
|
3874
7296
|
scenes: normalizedScenes,
|
|
3875
|
-
endCard: inferEndCard(allText, references,
|
|
7297
|
+
endCard: inferEndCard(allText, references, endCardText),
|
|
3876
7298
|
};
|
|
3877
7299
|
}
|
|
3878
7300
|
export function validateStoryboardProjectTiming(project, rules = DEFAULT_STORYBOARD_TIMING_RULES) {
|
|
@@ -4010,6 +7432,7 @@ function compileStoryboardCriticalRequirements() {
|
|
|
4010
7432
|
'When a product, feature, brand, capability, or CTA is part of the brief, keep its scene-level purpose legible in the non-frame notes without inventing unsupported product claims.',
|
|
4011
7433
|
'Use explicit transition logic between adjacent beats: object motion, light/color handoff, match cut, camera move, wipe, reaction, or another concrete edit idea.',
|
|
4012
7434
|
'Use shaped pacing when timings are flexible: fast hook, escalating middle, readable reveal, and a final CTA/end card held long enough for critical text/logo recognition.',
|
|
7435
|
+
'If the storyboard count was inferred from the final video duration, treat that count only as keyframe density for the still sheet. Do not force every storyboard cell to last 2 seconds or use uniform slices unless the source explicitly requires equal-duration panels. Cell timing must be driven by the underlying story timeline, important visual keyframes, dialogue/VO pacing, transitions, and readable CTA/end-card holds.',
|
|
4013
7436
|
'Divide each scene cell into a clean video-frame artwork area plus a clearly associated note/header/footer area. Put Time, scene/frame numbers, Visual/Action, Camera/Motion, Lighting/Style, Dialogue/VO, and Audio/SFX outside the video frame artwork, never overlaid on top of the cinematic frame.',
|
|
4014
7437
|
'Every cinematic video-frame artwork area must preserve the requested Individual scene-cell/frame aspect ratio. Keep all frame artwork areas locked to the same W:H unless the source explicitly requests mixed ratios; do not make individual stills square or let one or two cells drift to a different crop.',
|
|
4015
7438
|
'Use concise readable storyboard labels in the non-frame note areas. Do not place long paragraphs of production notes inside every cell.',
|
|
@@ -4063,6 +7486,7 @@ function compileStoryboardCountContractSection(project, layout) {
|
|
|
4063
7486
|
return [
|
|
4064
7487
|
'COUNT / GRID CONTRACT:',
|
|
4065
7488
|
`Required scene count: exactly ${project.scenes.length} numbered storyboard scene slots; do not render fewer slots and do not add extra scene slots.`,
|
|
7489
|
+
'Scene count does not imply equal duration per slot. Use each slot for the next important visual keyframe in the story and preserve any source time ranges, dialogue pacing, transition needs, and readable end-card/CTA holds.',
|
|
4066
7490
|
`Allocate the full ${project.scenes.length}-slot storyboard grid before drawing details. Fill slots in reading order, left-to-right then top-to-bottom: ${sceneSlots}.`,
|
|
4067
7491
|
`Each allocated scene slot gets one distinct ${layout.cellAspectRatio} cinematic video-frame rectangle plus its own compact notes outside that rectangle. Do not merge adjacent scenes, combine two beats into one slot, duplicate slots, or place thumbnail/inset panels inside a slot.`,
|
|
4068
7492
|
`Use the layout preset exactly: ${layout.layoutKind} - ${layout.layoutDescription}. The final sheet should be visibly countable as ${project.scenes.length} numbered scene slots at a glance.`,
|
|
@@ -4145,7 +7569,9 @@ function compileStoryboardScenesSection(project) {
|
|
|
4145
7569
|
for (const scene of project.scenes) {
|
|
4146
7570
|
const timing = scene.startSec !== null && scene.endSec !== null
|
|
4147
7571
|
? `${formatStoryboardSeconds(scene.startSec)}-${formatStoryboardSeconds(scene.endSec)}`
|
|
4148
|
-
:
|
|
7572
|
+
: project.durationSec !== null
|
|
7573
|
+
? `timing flexible within ${project.durationSec} seconds total; set from story/dialogue pacing, not equal-duration slots`
|
|
7574
|
+
: 'timing flexible; set from story/dialogue pacing, not equal-duration slots';
|
|
4149
7575
|
lines.push(`${scene.id.toUpperCase()} - ${scene.title} - ${timing}`);
|
|
4150
7576
|
if (scene.purpose)
|
|
4151
7577
|
lines.push(`Scene purpose: ${scene.purpose}`);
|
|
@@ -4175,6 +7601,27 @@ function compileStoryboardScenesSection(project) {
|
|
|
4175
7601
|
}
|
|
4176
7602
|
return lines;
|
|
4177
7603
|
}
|
|
7604
|
+
function storyboardRequiredVisibleText(project) {
|
|
7605
|
+
const sceneVisibleText = uniqueStoryboardStrings(project.scenes.flatMap(scene => [
|
|
7606
|
+
...scene.textInImage,
|
|
7607
|
+
...extractStoryboardRequiredText([
|
|
7608
|
+
scene.visual,
|
|
7609
|
+
scene.action,
|
|
7610
|
+
scene.productFeature,
|
|
7611
|
+
].filter(Boolean).join('\n')),
|
|
7612
|
+
]));
|
|
7613
|
+
const globalVisibleText = project.creativeBrief.mustInclude.filter(text => !sceneVisibleText.includes(text));
|
|
7614
|
+
const finalVisibleText = finalStoryboardSceneVisibleText(project.scenes);
|
|
7615
|
+
const endCardText = finalVisibleText.length > 0
|
|
7616
|
+
? project.endCard.requiredText.filter(text => finalVisibleText.includes(text) || !sceneVisibleText.includes(text))
|
|
7617
|
+
: project.endCard.requiredText;
|
|
7618
|
+
const requiredSceneVisibleText = project.sourceProvenance === 'assistant_draft' ? [] : sceneVisibleText;
|
|
7619
|
+
return uniqueStoryboardStrings([
|
|
7620
|
+
...requiredSceneVisibleText,
|
|
7621
|
+
...globalVisibleText,
|
|
7622
|
+
...endCardText,
|
|
7623
|
+
]);
|
|
7624
|
+
}
|
|
4178
7625
|
function storyboardLayoutSpecFromProject(project, frameCount) {
|
|
4179
7626
|
const layout = describeStoryboardLayout(project.outputAspectRatio, project.frameAspectRatio, frameCount);
|
|
4180
7627
|
return {
|
|
@@ -4249,7 +7696,8 @@ export function compileVideoStoryboardImagePrompt(options) {
|
|
|
4249
7696
|
...compileStoryboardScenesSection(project),
|
|
4250
7697
|
'TEXT RENDERING:',
|
|
4251
7698
|
'Place scene number, timing, scene title, beat title, and compact production labels outside each video frame in a clearly associated header, footer strip, side rail, or table. Do not overlay scene numbers, timecodes, production notes, Dialogue/VO labels, Audio/SFX text, or SFX/action callout words such as Whoosh!, Impact!, Boom!, Thud!, Slash!, Crack!, or Pop! on top of the video-frame artwork. Also do not overlay scene/beat titles on top of the video-frame artwork. Project titles are metadata, not in-frame text, unless the source explicitly marks them as visible on-screen text. Only user-required diegetic or brand text belongs inside a frame. Quote and spell any required visible text exactly.',
|
|
4252
|
-
|
|
7699
|
+
'Visible text listed on a scene belongs only in that scene; do not repeat earlier scene text on later panels or the final frame unless that later scene lists it too.',
|
|
7700
|
+
...storyboardRequiredVisibleText(project).map(text => `Required exact visible text: "${text}".`),
|
|
4253
7701
|
project.endCard.logoUsage ? `Logo usage: ${project.endCard.logoUsage}` : '',
|
|
4254
7702
|
'',
|
|
4255
7703
|
'SOURCE BRIEF TO FOLLOW:',
|
|
@@ -4359,6 +7807,242 @@ export function lintStoryboardImagePrompt(prompt, layout, project) {
|
|
|
4359
7807
|
warnings,
|
|
4360
7808
|
};
|
|
4361
7809
|
}
|
|
7810
|
+
function inferCompiledStoryboardPromptFrameCount(prompt) {
|
|
7811
|
+
const match = prompt.match(/\bCreate\s+exactly\s+(\d{1,3})\s+sequential video storyboard frames\b/i);
|
|
7812
|
+
if (!match)
|
|
7813
|
+
return null;
|
|
7814
|
+
const count = Number(match[1]);
|
|
7815
|
+
return Number.isInteger(count) && count > 0 ? count : null;
|
|
7816
|
+
}
|
|
7817
|
+
function inferCompiledStoryboardPromptDurationSec(prompt) {
|
|
7818
|
+
const match = prompt.match(/\bTarget duration:\s*(\d{1,3}(?:\.\d+)?)\s*seconds?\b/i);
|
|
7819
|
+
if (!match)
|
|
7820
|
+
return null;
|
|
7821
|
+
const duration = Number(match[1]);
|
|
7822
|
+
return Number.isFinite(duration) && duration > 0 ? duration : null;
|
|
7823
|
+
}
|
|
7824
|
+
function extractCompiledStoryboardScenesBlock(prompt) {
|
|
7825
|
+
const match = prompt.match(/\nSCENES:\s*\n([\s\S]*?)(?:\nTEXT RENDERING:|\nSOURCE BRIEF TO FOLLOW:|\nNEGATIVE \/ AVOID:|$)/i);
|
|
7826
|
+
return match?.[1] ?? '';
|
|
7827
|
+
}
|
|
7828
|
+
function parseCompiledStoryboardHeadingTiming(heading) {
|
|
7829
|
+
const timing = heading.match(/(?:^|\s+-\s+)(\d{1,2}:\d{2}(?:\.\d+)?|\d{1,3}(?:\.\d+)?)\s*(?:s|sec|secs|seconds?)?\s*-\s*(\d{1,2}:\d{2}(?:\.\d+)?|\d{1,3}(?:\.\d+)?)\s*(?:s|sec|secs|seconds?)?\s*$/i);
|
|
7830
|
+
if (!timing)
|
|
7831
|
+
return { startSec: null, endSec: null };
|
|
7832
|
+
return {
|
|
7833
|
+
startSec: parseStoryboardTimeValue(timing[1]),
|
|
7834
|
+
endSec: parseStoryboardTimeValue(timing[2]),
|
|
7835
|
+
};
|
|
7836
|
+
}
|
|
7837
|
+
function extractCompiledStoryboardScenes(prompt) {
|
|
7838
|
+
const block = extractCompiledStoryboardScenesBlock(prompt);
|
|
7839
|
+
if (!block.trim())
|
|
7840
|
+
return [];
|
|
7841
|
+
const headings = Array.from(block.matchAll(/^SCENE_(\d{1,3})\s+-\s+([^\n]+)$/gim));
|
|
7842
|
+
return headings.map((match, i) => {
|
|
7843
|
+
const headingStart = match.index ?? 0;
|
|
7844
|
+
const headingEnd = headingStart + match[0].length;
|
|
7845
|
+
const nextStart = headings[i + 1]?.index ?? block.length;
|
|
7846
|
+
const heading = match[0].trim();
|
|
7847
|
+
const body = block.slice(headingEnd, nextStart).trim();
|
|
7848
|
+
const dialogueMatch = body.match(/^\s*Dialogue\/VO:\s*(.+)$/im);
|
|
7849
|
+
const timing = parseCompiledStoryboardHeadingTiming(heading);
|
|
7850
|
+
return {
|
|
7851
|
+
index: Number(match[1]),
|
|
7852
|
+
heading,
|
|
7853
|
+
body,
|
|
7854
|
+
startSec: timing.startSec,
|
|
7855
|
+
endSec: timing.endSec,
|
|
7856
|
+
dialogue: normalizeStoryboardDialogue(dialogueMatch?.[1] ?? ''),
|
|
7857
|
+
};
|
|
7858
|
+
});
|
|
7859
|
+
}
|
|
7860
|
+
function sourceStoryboardDialogueRequirements(sourceText) {
|
|
7861
|
+
const raw = (sourceText || '').trim();
|
|
7862
|
+
if (!raw)
|
|
7863
|
+
return { requirements: [], sourceSectionCount: 0 };
|
|
7864
|
+
const source = canonicalStoryboardScriptContext(raw) || raw;
|
|
7865
|
+
const requirements = [];
|
|
7866
|
+
const sections = splitStoryboardSections(source);
|
|
7867
|
+
sections.forEach((section, sectionIndex) => {
|
|
7868
|
+
const dialogue = normalizeStoryboardDialogue(extractStoryboardField(section.body, [
|
|
7869
|
+
'Dialogue/VO',
|
|
7870
|
+
'VO/Dialogue',
|
|
7871
|
+
'Dialogue',
|
|
7872
|
+
'VO',
|
|
7873
|
+
'V.O.',
|
|
7874
|
+
'Voiceover',
|
|
7875
|
+
'Voice-over',
|
|
7876
|
+
'Speech',
|
|
7877
|
+
'Narration',
|
|
7878
|
+
]));
|
|
7879
|
+
if (dialogue)
|
|
7880
|
+
requirements.push({ dialogue, sectionIndex });
|
|
7881
|
+
});
|
|
7882
|
+
if (requirements.length === 0) {
|
|
7883
|
+
requirements.push(...sourceQuotedStoryboardDialogueSegments(source).map(dialogue => ({
|
|
7884
|
+
dialogue,
|
|
7885
|
+
sectionIndex: null,
|
|
7886
|
+
})));
|
|
7887
|
+
}
|
|
7888
|
+
const unique = [];
|
|
7889
|
+
const seen = new Set();
|
|
7890
|
+
for (const requirement of requirements) {
|
|
7891
|
+
const compact = compactStoryboardLine(requirement.dialogue);
|
|
7892
|
+
if (!compact)
|
|
7893
|
+
continue;
|
|
7894
|
+
const tokenCount = storyboardDialogueWordSpans(compact).length;
|
|
7895
|
+
if (tokenCount === 0 || compact.length < 2)
|
|
7896
|
+
continue;
|
|
7897
|
+
const key = storyboardDialogueWordSpans(compact).map(span => span.token).join(' ');
|
|
7898
|
+
if (!key || seen.has(key))
|
|
7899
|
+
continue;
|
|
7900
|
+
seen.add(key);
|
|
7901
|
+
unique.push({ ...requirement, dialogue: compact });
|
|
7902
|
+
}
|
|
7903
|
+
return { requirements: unique, sourceSectionCount: sections.length };
|
|
7904
|
+
}
|
|
7905
|
+
export function auditCompiledStoryboardImagePrompt(options) {
|
|
7906
|
+
const prompt = options.prompt.trim();
|
|
7907
|
+
const fatalIssues = [];
|
|
7908
|
+
const warnings = [];
|
|
7909
|
+
if (!prompt) {
|
|
7910
|
+
fatalIssues.push({
|
|
7911
|
+
code: 'storyboard_prompt_empty',
|
|
7912
|
+
message: 'Storyboard image prompt is empty.',
|
|
7913
|
+
field: 'prompt',
|
|
7914
|
+
});
|
|
7915
|
+
return { ok: false, fatalIssues, warnings };
|
|
7916
|
+
}
|
|
7917
|
+
const expectedFrameCount = options.expectedFrameCount
|
|
7918
|
+
?? inferCompiledStoryboardPromptFrameCount(prompt);
|
|
7919
|
+
const expectedDurationSec = options.expectedDurationSec
|
|
7920
|
+
?? inferCompiledStoryboardPromptDurationSec(prompt);
|
|
7921
|
+
const scenes = extractCompiledStoryboardScenes(prompt);
|
|
7922
|
+
if (expectedFrameCount !== null && scenes.length !== expectedFrameCount) {
|
|
7923
|
+
fatalIssues.push({
|
|
7924
|
+
code: 'storyboard_prompt_scene_count_mismatch',
|
|
7925
|
+
message: `Compiled storyboard prompt asks for ${expectedFrameCount} frame(s) but defines ${scenes.length} scene(s).`,
|
|
7926
|
+
field: 'prompt',
|
|
7927
|
+
metadata: { expectedFrameCount, actualSceneCount: scenes.length },
|
|
7928
|
+
});
|
|
7929
|
+
}
|
|
7930
|
+
if (scenes.length === 0) {
|
|
7931
|
+
fatalIssues.push({
|
|
7932
|
+
code: 'storyboard_prompt_missing_scenes',
|
|
7933
|
+
message: 'Compiled storyboard prompt has no SCENES entries.',
|
|
7934
|
+
field: 'prompt',
|
|
7935
|
+
});
|
|
7936
|
+
}
|
|
7937
|
+
const timedScenes = scenes.filter(scene => scene.startSec !== null && scene.endSec !== null);
|
|
7938
|
+
if (timedScenes.length > 0) {
|
|
7939
|
+
const backwards = timedScenes.find(scene => scene.endSec <= scene.startSec);
|
|
7940
|
+
if (backwards) {
|
|
7941
|
+
fatalIssues.push({
|
|
7942
|
+
code: 'storyboard_prompt_invalid_scene_timing',
|
|
7943
|
+
message: `Scene ${backwards.index} has a non-forward time range.`,
|
|
7944
|
+
field: 'prompt',
|
|
7945
|
+
metadata: {
|
|
7946
|
+
sceneIndex: backwards.index,
|
|
7947
|
+
startSec: backwards.startSec,
|
|
7948
|
+
endSec: backwards.endSec,
|
|
7949
|
+
},
|
|
7950
|
+
});
|
|
7951
|
+
}
|
|
7952
|
+
for (let i = 1; i < timedScenes.length; i += 1) {
|
|
7953
|
+
const previous = timedScenes[i - 1];
|
|
7954
|
+
const current = timedScenes[i];
|
|
7955
|
+
if (current.startSec < previous.endSec - 0.05) {
|
|
7956
|
+
fatalIssues.push({
|
|
7957
|
+
code: 'storyboard_prompt_timing_overlap',
|
|
7958
|
+
message: `Scene ${current.index} starts before scene ${previous.index} ends.`,
|
|
7959
|
+
field: 'prompt',
|
|
7960
|
+
metadata: {
|
|
7961
|
+
previousSceneIndex: previous.index,
|
|
7962
|
+
previousEndSec: previous.endSec,
|
|
7963
|
+
sceneIndex: current.index,
|
|
7964
|
+
startSec: current.startSec,
|
|
7965
|
+
},
|
|
7966
|
+
});
|
|
7967
|
+
break;
|
|
7968
|
+
}
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7971
|
+
if (expectedDurationSec !== null && timedScenes.length > 0) {
|
|
7972
|
+
const maxEndSec = Math.max(...timedScenes.map(scene => scene.endSec));
|
|
7973
|
+
const firstStartSec = Math.min(...timedScenes.map(scene => scene.startSec));
|
|
7974
|
+
const allowedLateStartSec = Math.min(2, expectedDurationSec * 0.2);
|
|
7975
|
+
if (maxEndSec > expectedDurationSec + 0.75) {
|
|
7976
|
+
fatalIssues.push({
|
|
7977
|
+
code: 'storyboard_prompt_timing_exceeds_duration',
|
|
7978
|
+
message: `Compiled storyboard prompt scene timings end at ${formatStoryboardSeconds(maxEndSec)}, beyond the ${formatStoryboardSeconds(expectedDurationSec)} target duration.`,
|
|
7979
|
+
field: 'prompt',
|
|
7980
|
+
metadata: { expectedDurationSec, maxEndSec },
|
|
7981
|
+
});
|
|
7982
|
+
}
|
|
7983
|
+
if (firstStartSec > allowedLateStartSec) {
|
|
7984
|
+
fatalIssues.push({
|
|
7985
|
+
code: 'storyboard_prompt_timing_starts_late',
|
|
7986
|
+
message: `Compiled storyboard prompt starts at ${formatStoryboardSeconds(firstStartSec)} instead of near 0s.`,
|
|
7987
|
+
field: 'prompt',
|
|
7988
|
+
metadata: { expectedDurationSec, firstStartSec },
|
|
7989
|
+
});
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
const dialogueRequirementSet = sourceStoryboardDialogueRequirements(options.sourceText);
|
|
7993
|
+
const requiredDialogue = dialogueRequirementSet.requirements;
|
|
7994
|
+
if (requiredDialogue.length > 0) {
|
|
7995
|
+
const compiledDialogue = scenes.map(scene => scene.dialogue).filter(Boolean).join(' ');
|
|
7996
|
+
const canCheckSceneAlignment = dialogueRequirementSet.sourceSectionCount > 0
|
|
7997
|
+
&& dialogueRequirementSet.sourceSectionCount === scenes.length;
|
|
7998
|
+
for (const requirement of requiredDialogue) {
|
|
7999
|
+
const dialogue = requirement.dialogue;
|
|
8000
|
+
const tokenCount = storyboardDialogueWordSpans(dialogue).length;
|
|
8001
|
+
const minCoverage = tokenCount >= 8 ? 0.7 : 0.9;
|
|
8002
|
+
const sceneDialogue = canCheckSceneAlignment && requirement.sectionIndex !== null
|
|
8003
|
+
? scenes[requirement.sectionIndex]?.dialogue ?? ''
|
|
8004
|
+
: '';
|
|
8005
|
+
const sceneCoverage = sceneDialogue
|
|
8006
|
+
? storyboardDialogueCoverage(dialogue, sceneDialogue)
|
|
8007
|
+
: 0;
|
|
8008
|
+
const coverage = storyboardDialogueCoverage(dialogue, compiledDialogue);
|
|
8009
|
+
if (canCheckSceneAlignment && requirement.sectionIndex !== null && sceneCoverage < minCoverage) {
|
|
8010
|
+
fatalIssues.push({
|
|
8011
|
+
code: coverage >= minCoverage
|
|
8012
|
+
? 'storyboard_prompt_misassigned_source_dialogue'
|
|
8013
|
+
: 'storyboard_prompt_missing_source_dialogue',
|
|
8014
|
+
message: coverage >= minCoverage
|
|
8015
|
+
? 'Compiled storyboard prompt preserves source Dialogue/VO but assigns it to the wrong scene.'
|
|
8016
|
+
: 'Compiled storyboard prompt does not preserve required Dialogue/VO from the approved source script.',
|
|
8017
|
+
field: 'prompt',
|
|
8018
|
+
metadata: {
|
|
8019
|
+
requiredDialogue: dialogue,
|
|
8020
|
+
sceneIndex: requirement.sectionIndex + 1,
|
|
8021
|
+
sceneCoverage,
|
|
8022
|
+
coverage,
|
|
8023
|
+
},
|
|
8024
|
+
});
|
|
8025
|
+
continue;
|
|
8026
|
+
}
|
|
8027
|
+
if (coverage < minCoverage) {
|
|
8028
|
+
fatalIssues.push({
|
|
8029
|
+
code: 'storyboard_prompt_missing_source_dialogue',
|
|
8030
|
+
message: 'Compiled storyboard prompt does not preserve required Dialogue/VO from the approved source script.',
|
|
8031
|
+
field: 'prompt',
|
|
8032
|
+
metadata: {
|
|
8033
|
+
requiredDialogue: dialogue,
|
|
8034
|
+
coverage,
|
|
8035
|
+
},
|
|
8036
|
+
});
|
|
8037
|
+
}
|
|
8038
|
+
}
|
|
8039
|
+
}
|
|
8040
|
+
return {
|
|
8041
|
+
ok: fatalIssues.length === 0,
|
|
8042
|
+
fatalIssues,
|
|
8043
|
+
warnings,
|
|
8044
|
+
};
|
|
8045
|
+
}
|
|
4362
8046
|
function parseStoryboardDimensionText(value) {
|
|
4363
8047
|
if (!value)
|
|
4364
8048
|
return null;
|
|
@@ -4407,7 +8091,7 @@ export function userDefinedStoryboardCanvas(text) {
|
|
|
4407
8091
|
return true;
|
|
4408
8092
|
if (inferExplicitStoryboardCanvasPixelDimensions(source))
|
|
4409
8093
|
return true;
|
|
4410
|
-
const pageUnit = String.raw `(?:board|canvas|page|sheet|poster|story\s*board\s+(?:
|
|
8094
|
+
const pageUnit = String.raw `(?:board|canvas|page|sheet|poster|story\s*board\s+(?:sheet|canvas|board|page|poster|output)|storyboard\s+(?:sheet|canvas|board|page|poster|output)|(?:sheet|canvas|board|page|poster|output)\s+(?:story\s*board|storyboard))`;
|
|
4411
8095
|
const aspectToken = String.raw `(?:\d{1,4}\s*:\s*\d{1,4}|\d{3,5}\s*x\s*\d{3,5}|portrait|vertical|landscape|horizontal|widescreen)`;
|
|
4412
8096
|
return new RegExp(String.raw `\b${pageUnit}\b[\s\S]{0,80}\b${aspectToken}\b`, 'i').test(source)
|
|
4413
8097
|
|| new RegExp(String.raw `\b${aspectToken}\b[\s\S]{0,80}\b${pageUnit}\b`, 'i').test(source);
|
|
@@ -4441,10 +8125,9 @@ function parseAspectRatioOrientation(aspectRatio) {
|
|
|
4441
8125
|
}
|
|
4442
8126
|
function defaultStoryboardCanvasForVideoAspectRatio(targetVideoAspectRatio) {
|
|
4443
8127
|
const orientation = parseAspectRatioOrientation(targetVideoAspectRatio);
|
|
4444
|
-
if (orientation === 'portrait')
|
|
8128
|
+
if (orientation === 'portrait' || orientation === 'landscape' || orientation === 'square') {
|
|
4445
8129
|
return GPT_IMAGE_STORYBOARD_DEFAULTS.storyboardLandscape;
|
|
4446
|
-
|
|
4447
|
-
return GPT_IMAGE_STORYBOARD_DEFAULTS.storyboardPortrait;
|
|
8130
|
+
}
|
|
4448
8131
|
return null;
|
|
4449
8132
|
}
|
|
4450
8133
|
function storyboardCanvasHintText(canvas, targetVideoAspectRatio) {
|
|
@@ -4709,38 +8392,38 @@ export function buildStoryboardVideoHostedToolSequenceInput(options) {
|
|
|
4709
8392
|
steps: [
|
|
4710
8393
|
{
|
|
4711
8394
|
id: 'storyboard_image',
|
|
4712
|
-
toolName: '
|
|
8395
|
+
toolName: 'generate_image',
|
|
4713
8396
|
arguments: {
|
|
4714
8397
|
prompt: storyboardImagePrompt,
|
|
4715
8398
|
model: imageModel,
|
|
4716
8399
|
width: imageWidth,
|
|
4717
8400
|
height: imageHeight,
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
8401
|
+
numberOfVariations: 1,
|
|
8402
|
+
gptImageQuality: imageQuality,
|
|
8403
|
+
outputFormat: imageOutputFormat,
|
|
4721
8404
|
},
|
|
4722
8405
|
},
|
|
4723
8406
|
{
|
|
4724
8407
|
id: 'seedance_video',
|
|
4725
|
-
toolName: '
|
|
8408
|
+
toolName: 'generate_video',
|
|
4726
8409
|
arguments: {
|
|
4727
8410
|
prompt: seedanceVideoPrompt,
|
|
4728
|
-
|
|
8411
|
+
videoModel,
|
|
4729
8412
|
width: videoDimensions.width,
|
|
4730
8413
|
height: videoDimensions.height,
|
|
4731
8414
|
duration: videoDuration,
|
|
4732
8415
|
fps: 24,
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
8416
|
+
numberOfVariations: 1,
|
|
8417
|
+
generateAudio,
|
|
8418
|
+
expandPrompt: false,
|
|
4736
8419
|
},
|
|
4737
8420
|
dependsOn: [
|
|
4738
8421
|
{
|
|
4739
8422
|
sourceStepId: 'storyboard_image',
|
|
4740
8423
|
sourceArtifactIndex: 0,
|
|
4741
|
-
targetArgument: '
|
|
8424
|
+
targetArgument: 'referenceImageIndices',
|
|
4742
8425
|
mediaType: 'image',
|
|
4743
|
-
transform: '
|
|
8426
|
+
transform: 'image_index',
|
|
4744
8427
|
required: true,
|
|
4745
8428
|
},
|
|
4746
8429
|
],
|
|
@@ -4868,3 +8551,248 @@ export function sanitizeBatchPrompt(prompt) {
|
|
|
4868
8551
|
}
|
|
4869
8552
|
return result;
|
|
4870
8553
|
}
|
|
8554
|
+
export const CROSS_SURFACE_PARITY_SURFACES = [
|
|
8555
|
+
'chat',
|
|
8556
|
+
'api_chat_completions',
|
|
8557
|
+
'api_creative_agent_workflows',
|
|
8558
|
+
'public_skill',
|
|
8559
|
+
];
|
|
8560
|
+
export const CROSS_SURFACE_PARITY_FIXTURES = [
|
|
8561
|
+
{
|
|
8562
|
+
id: 'uploaded-video-subtitles',
|
|
8563
|
+
focus: 'subtitles',
|
|
8564
|
+
description: 'Quoted subtitle lines burn into the uploaded video instead of regenerating it.',
|
|
8565
|
+
userText: 'Add subtitles to this uploaded clip: "Fresh coffee." "Ready when you are."',
|
|
8566
|
+
mediaReferences: [{ kind: 'video', filename: 'cafe.mp4' }],
|
|
8567
|
+
expectations: [
|
|
8568
|
+
{
|
|
8569
|
+
surface: 'chat',
|
|
8570
|
+
entrypoint: 'workflow fixture subtitles-on-uploaded-video',
|
|
8571
|
+
expectedTools: ['add_subtitles'],
|
|
8572
|
+
expectedBehavior: ['sourceVideoIndex uses the uploaded video'],
|
|
8573
|
+
},
|
|
8574
|
+
{
|
|
8575
|
+
surface: 'api_chat_completions',
|
|
8576
|
+
entrypoint: '/v1/chat/completions',
|
|
8577
|
+
expectedTools: ['add_subtitles'],
|
|
8578
|
+
expectedRequest: { sogni_tools: 'creative-agent', api_media_references: true },
|
|
8579
|
+
},
|
|
8580
|
+
{
|
|
8581
|
+
surface: 'api_creative_agent_workflows',
|
|
8582
|
+
entrypoint: '/v1/creative-agent/workflows',
|
|
8583
|
+
expectedTools: ['add_subtitles'],
|
|
8584
|
+
expectedRequest: { kind: 'hosted_tool_sequence' },
|
|
8585
|
+
},
|
|
8586
|
+
{
|
|
8587
|
+
surface: 'public_skill',
|
|
8588
|
+
entrypoint: '--api-chat and --api-workflow hosted-tool-sequence',
|
|
8589
|
+
expectedTools: ['add_subtitles'],
|
|
8590
|
+
expectedBehavior: ['forwards video refs as api_media_references without prompt base64 leakage'],
|
|
8591
|
+
},
|
|
8592
|
+
],
|
|
8593
|
+
},
|
|
8594
|
+
{
|
|
8595
|
+
id: 'uploaded-video-logo-overlay',
|
|
8596
|
+
focus: 'overlay',
|
|
8597
|
+
description: 'A static logo/image overlay targets the uploaded base video.',
|
|
8598
|
+
userText: 'Put this logo in the top left of the uploaded video as a small watermark.',
|
|
8599
|
+
mediaReferences: [
|
|
8600
|
+
{ kind: 'video', filename: 'source.mp4' },
|
|
8601
|
+
{ kind: 'image', filename: 'logo.png' },
|
|
8602
|
+
],
|
|
8603
|
+
expectations: [
|
|
8604
|
+
{ surface: 'chat', entrypoint: 'workflow fixture uploaded-video-logo-static-overlay', expectedTools: ['overlay_video'] },
|
|
8605
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['overlay_video'] },
|
|
8606
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['overlay_video'] },
|
|
8607
|
+
{ surface: 'public_skill', entrypoint: '--api-workflow hosted-tool-sequence', expectedTools: ['overlay_video'] },
|
|
8608
|
+
],
|
|
8609
|
+
},
|
|
8610
|
+
{
|
|
8611
|
+
id: 'uploaded-video-segment-replace',
|
|
8612
|
+
focus: 'uploaded_video_edits',
|
|
8613
|
+
description: 'A bounded uploaded-video window routes to replace_video_segment.',
|
|
8614
|
+
userText: 'Regenerate the 2s-4s window of this uploaded video and keep the original audio.',
|
|
8615
|
+
mediaReferences: [{ kind: 'video', filename: 'source.mp4' }],
|
|
8616
|
+
expectations: [
|
|
8617
|
+
{ surface: 'chat', entrypoint: 'workflow fixture replace-video-segment-uploaded', expectedTools: ['replace_video_segment'] },
|
|
8618
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['replace_video_segment'] },
|
|
8619
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['replace_video_segment'] },
|
|
8620
|
+
{ surface: 'public_skill', entrypoint: '--api-workflow hosted-tool-sequence', expectedTools: ['replace_video_segment'] },
|
|
8621
|
+
],
|
|
8622
|
+
},
|
|
8623
|
+
{
|
|
8624
|
+
id: 'uploaded-video-extend',
|
|
8625
|
+
focus: 'uploaded_video_edits',
|
|
8626
|
+
description: 'Uploaded-video extension targets the uploaded base clip instead of regenerating from scratch.',
|
|
8627
|
+
userText: 'Extend this uploaded video by 5 seconds.',
|
|
8628
|
+
mediaReferences: [{ kind: 'video', filename: 'source.mp4' }],
|
|
8629
|
+
expectations: [
|
|
8630
|
+
{ surface: 'chat', entrypoint: 'workflow fixture extend-video-uploaded-five-seconds', expectedTools: ['extend_video'] },
|
|
8631
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['extend_video'] },
|
|
8632
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['extend_video'] },
|
|
8633
|
+
{ surface: 'public_skill', entrypoint: '--api-workflow hosted-tool-sequence', expectedTools: ['extend_video'] },
|
|
8634
|
+
],
|
|
8635
|
+
},
|
|
8636
|
+
{
|
|
8637
|
+
id: 'uploaded-video-transform',
|
|
8638
|
+
focus: 'uploaded_video_edits',
|
|
8639
|
+
description: 'Uploaded-video restyling routes to video_to_video and does not generate new source media first.',
|
|
8640
|
+
userText: 'Transform this uploaded video into watercolor anime style while preserving the motion.',
|
|
8641
|
+
mediaReferences: [{ kind: 'video', filename: 'street.mp4' }],
|
|
8642
|
+
expectations: [
|
|
8643
|
+
{ surface: 'chat', entrypoint: 'workflow fixture uploaded-video-restyle', expectedTools: ['video_to_video'] },
|
|
8644
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['video_to_video'] },
|
|
8645
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['video_to_video'] },
|
|
8646
|
+
{ surface: 'public_skill', entrypoint: '--api-workflow hosted-tool-sequence', expectedTools: ['video_to_video'] },
|
|
8647
|
+
],
|
|
8648
|
+
},
|
|
8649
|
+
{
|
|
8650
|
+
id: 'uploaded-video-stitch',
|
|
8651
|
+
focus: 'uploaded_video_edits',
|
|
8652
|
+
description: 'Simple uploaded-video stitch requests preserve upload/UI order and default to hard cuts.',
|
|
8653
|
+
userText: 'Combine these 2 clips.',
|
|
8654
|
+
mediaReferences: [
|
|
8655
|
+
{ kind: 'video', filename: 'first.mp4' },
|
|
8656
|
+
{ kind: 'video', filename: 'second.mp4' },
|
|
8657
|
+
],
|
|
8658
|
+
expectations: [
|
|
8659
|
+
{ surface: 'chat', entrypoint: 'workflow fixture uploaded-videos-simple-stitch-hard-cut', expectedTools: ['stitch_video'] },
|
|
8660
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['stitch_video'] },
|
|
8661
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['stitch_video'] },
|
|
8662
|
+
{ surface: 'public_skill', entrypoint: '--api-workflow hosted-tool-sequence', expectedTools: ['stitch_video'] },
|
|
8663
|
+
],
|
|
8664
|
+
},
|
|
8665
|
+
{
|
|
8666
|
+
id: 'uploaded-video-alternating-splice',
|
|
8667
|
+
focus: 'uploaded_video_edits',
|
|
8668
|
+
description: 'Alternating uploaded-video splices compile into repeated replace_video_segment existing-clip calls.',
|
|
8669
|
+
userText: 'Create a video where you alternate 1s from each of these videos one after another.',
|
|
8670
|
+
mediaReferences: [
|
|
8671
|
+
{ kind: 'video', filename: 'first.mp4' },
|
|
8672
|
+
{ kind: 'video', filename: 'second.mp4' },
|
|
8673
|
+
],
|
|
8674
|
+
expectations: [
|
|
8675
|
+
{ surface: 'chat', entrypoint: 'workflow fixture uploaded-videos-alternating-one-second-splices', expectedTools: ['replace_video_segment'] },
|
|
8676
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['replace_video_segment'] },
|
|
8677
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['replace_video_segment'] },
|
|
8678
|
+
{ surface: 'public_skill', entrypoint: '--api-workflow hosted-tool-sequence', expectedTools: ['replace_video_segment'] },
|
|
8679
|
+
],
|
|
8680
|
+
},
|
|
8681
|
+
{
|
|
8682
|
+
id: 'asset-manifest-model-map-sequence',
|
|
8683
|
+
focus: 'asset_manifest_sequence',
|
|
8684
|
+
description: 'Generated assets are labeled, mapped for the target model, and validated through the shared manifest tools.',
|
|
8685
|
+
userText: 'Create a product hero, label it as hero product, map it for Seedance, then validate the reference.',
|
|
8686
|
+
expectations: [
|
|
8687
|
+
{
|
|
8688
|
+
surface: 'chat',
|
|
8689
|
+
entrypoint: 'asset manifest workflow',
|
|
8690
|
+
expectedTools: ['create_asset_manifest', 'label_asset', 'map_assets_for_model', 'validate_asset_references'],
|
|
8691
|
+
},
|
|
8692
|
+
{
|
|
8693
|
+
surface: 'api_chat_completions',
|
|
8694
|
+
entrypoint: '/v1/chat/completions',
|
|
8695
|
+
expectedTools: ['create_asset_manifest', 'label_asset', 'map_assets_for_model', 'validate_asset_references'],
|
|
8696
|
+
},
|
|
8697
|
+
{
|
|
8698
|
+
surface: 'api_creative_agent_workflows',
|
|
8699
|
+
entrypoint: '/v1/creative-agent/workflows',
|
|
8700
|
+
expectedTools: ['create_asset_manifest', 'label_asset', 'map_assets_for_model', 'validate_asset_references'],
|
|
8701
|
+
},
|
|
8702
|
+
{
|
|
8703
|
+
surface: 'public_skill',
|
|
8704
|
+
entrypoint: 'generated creative-agent runtime',
|
|
8705
|
+
expectedTools: ['create_asset_manifest', 'label_asset', 'map_assets_for_model', 'validate_asset_references'],
|
|
8706
|
+
},
|
|
8707
|
+
],
|
|
8708
|
+
},
|
|
8709
|
+
{
|
|
8710
|
+
id: 'durable-workflow-cancel',
|
|
8711
|
+
focus: 'cancellation',
|
|
8712
|
+
description: 'Cancellation is exposed as the same durable workflow state transition across clients.',
|
|
8713
|
+
userText: 'Cancel workflow wf_test.',
|
|
8714
|
+
expectations: [
|
|
8715
|
+
{ surface: 'chat', entrypoint: 'workflow status controls', expectedBehavior: ['cancelled workflow is terminal'] },
|
|
8716
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions tool execution', expectedBehavior: ['USER_CANCELLED maps to cancelled'] },
|
|
8717
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows/:id/cancel', expectedBehavior: ['idempotent cancel response'] },
|
|
8718
|
+
{ surface: 'public_skill', entrypoint: '--cancel-workflow', expectedBehavior: ['posts to durable cancel endpoint'] },
|
|
8719
|
+
],
|
|
8720
|
+
},
|
|
8721
|
+
{
|
|
8722
|
+
id: 'durable-workflow-duplicate-start',
|
|
8723
|
+
focus: 'duplicate_start',
|
|
8724
|
+
description: 'Repeated workflow starts with an idempotency key return the existing run instead of launching a duplicate.',
|
|
8725
|
+
userText: 'Start the same durable workflow twice with idempotency key idem_123.',
|
|
8726
|
+
expectations: [
|
|
8727
|
+
{ surface: 'chat', entrypoint: 'hosted tool workflow tracking', expectedBehavior: ['workflowId remains stable for duplicate start'] },
|
|
8728
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedBehavior: ['hosted tool workflow tracking keeps one parent run'] },
|
|
8729
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedRequest: { idempotency_key: 'idem_123' } },
|
|
8730
|
+
{ surface: 'public_skill', entrypoint: '--api-workflow', expectedBehavior: ['forwards workflow idempotency metadata when provided'] },
|
|
8731
|
+
],
|
|
8732
|
+
},
|
|
8733
|
+
{
|
|
8734
|
+
id: 'generated-video-rerender-latest',
|
|
8735
|
+
focus: 'generated_video_rerender',
|
|
8736
|
+
description: 'Latest generated-video rerenders reuse the prior video arguments rather than starting a new unrelated render.',
|
|
8737
|
+
userText: 'Rerender the latest video with the same prompt, but make it vertical.',
|
|
8738
|
+
expectations: [
|
|
8739
|
+
{ surface: 'chat', entrypoint: 'workflow fixture seedance-uploaded-video-rerender-same-prompt', expectedTools: ['generate_video'] },
|
|
8740
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['generate_video'] },
|
|
8741
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['generate_video'] },
|
|
8742
|
+
{ surface: 'public_skill', entrypoint: '--api-chat', expectedTools: ['generate_video'] },
|
|
8743
|
+
],
|
|
8744
|
+
},
|
|
8745
|
+
{
|
|
8746
|
+
id: 'seedance-storyboard-rerender',
|
|
8747
|
+
focus: 'seedance_storyboard_rerender',
|
|
8748
|
+
description: 'Seedance storyboard rerenders keep the storyboard reference and compile into one generate_video rerender.',
|
|
8749
|
+
userText: 'Try that Seedance storyboard video again with the same script, but make the camera movement smoother.',
|
|
8750
|
+
mediaReferences: [{ kind: 'image', filename: 'storyboard.png' }],
|
|
8751
|
+
expectations: [
|
|
8752
|
+
{ surface: 'chat', entrypoint: 'workflow fixture seedance-storyboard-rerender-same-script', expectedTools: ['generate_video'] },
|
|
8753
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['generate_video'] },
|
|
8754
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['generate_video'] },
|
|
8755
|
+
{ surface: 'public_skill', entrypoint: '--api-workflow storyboard-video', expectedTools: ['generate_video'] },
|
|
8756
|
+
],
|
|
8757
|
+
},
|
|
8758
|
+
{
|
|
8759
|
+
id: 'image-selection-wait-before-video',
|
|
8760
|
+
focus: 'image_selection_wait',
|
|
8761
|
+
description: 'Image-option batches requested for later video pause after image generation until the user selects one.',
|
|
8762
|
+
userText: "Generate 4 image takes and I'll pick the best one before you make the dance video.",
|
|
8763
|
+
expectations: [
|
|
8764
|
+
{ surface: 'chat', entrypoint: 'workflow fixture snow-white-pick-before-dance', expectedTools: ['generate_image'], expectedBehavior: ['waits for user image selection before video tools'] },
|
|
8765
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['generate_image'], expectedBehavior: ['imageSelectionPolicy=wait_for_user_selection'] },
|
|
8766
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['generate_image'], expectedBehavior: ['stores pending selection state before video continuation'] },
|
|
8767
|
+
{ surface: 'public_skill', entrypoint: '--api-chat', expectedTools: ['generate_image'], expectedBehavior: ['uses public skill default contract runtime selection policy'] },
|
|
8768
|
+
],
|
|
8769
|
+
},
|
|
8770
|
+
{
|
|
8771
|
+
id: 'stitch-after-generated-batch',
|
|
8772
|
+
focus: 'stitch_after_batch',
|
|
8773
|
+
description: 'Generated clip batches that must become one video require stitch_video before finalization.',
|
|
8774
|
+
userText: 'Animate these generated keyframes as separate clips, then stitch the clips into one video.',
|
|
8775
|
+
expectations: [
|
|
8776
|
+
{ surface: 'chat', entrypoint: 'workflow fixture uploaded-reference-skit-loop-stitch', expectedTools: ['animate_photo', 'stitch_video'] },
|
|
8777
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedTools: ['animate_photo', 'stitch_video'] },
|
|
8778
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedTools: ['animate_photo', 'stitch_video'] },
|
|
8779
|
+
{ surface: 'public_skill', entrypoint: '--api-workflow hosted-tool-sequence', expectedTools: ['animate_photo', 'stitch_video'] },
|
|
8780
|
+
],
|
|
8781
|
+
},
|
|
8782
|
+
{
|
|
8783
|
+
id: 'skill-audio-video-media-refs',
|
|
8784
|
+
focus: 'skill_media_refs',
|
|
8785
|
+
description: 'Public skill uploads local audio and video refs as stored media references instead of embedding base64 in prompts.',
|
|
8786
|
+
userText: 'Make a music video from this audio and source video.',
|
|
8787
|
+
mediaReferences: [
|
|
8788
|
+
{ kind: 'audio', filename: 'music.mp3' },
|
|
8789
|
+
{ kind: 'video', filename: 'source.mp4' },
|
|
8790
|
+
],
|
|
8791
|
+
expectations: [
|
|
8792
|
+
{ surface: 'chat', entrypoint: 'uploaded file context', expectedBehavior: ['audio and video are distinct uploaded media refs'] },
|
|
8793
|
+
{ surface: 'api_chat_completions', entrypoint: '/v1/chat/completions', expectedRequest: { api_media_references: true } },
|
|
8794
|
+
{ surface: 'api_creative_agent_workflows', entrypoint: '/v1/creative-agent/workflows', expectedRequest: { api_media_references: true } },
|
|
8795
|
+
{ surface: 'public_skill', entrypoint: '--api-chat and --api-workflow', expectedBehavior: ['local files are uploaded to Sogni media URLs before durable execution'] },
|
|
8796
|
+
],
|
|
8797
|
+
},
|
|
8798
|
+
];
|