@kolbo/mcp 1.54.0 → 1.55.1
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 +3 -14
- package/package.json +5 -3
- package/skill/GENERATED.md +1 -1
- package/skill/SKILL.md +16 -28
- package/skill/VERSION +1 -1
- package/skill/references/models/music.md +1 -1
- package/skill/references/workflows/media-library.md +29 -1
- package/skill/references/workflows/troubleshooting.md +13 -0
- package/src/apps/widgets/upload.js +19 -2
- package/src/client.js +60 -6
- package/src/index.js +31 -2
- package/src/install.js +35 -3
- package/src/polling.js +7 -0
- package/src/progress.js +55 -0
- package/src/tools/_shared.js +137 -11
- package/src/tools/artifacts.js +23 -5
- package/src/tools/chat.js +3 -3
- package/src/tools/generate.js +21 -19
- package/src/tools/media.js +104 -12
- package/src/tools/models.js +55 -3
- package/src/tools/moodboards.js +1 -1
- package/src/tools/music_library.js +31 -4
- package/src/tools/presets.js +56 -17
- package/src/tools/projects.js +1 -1
- package/src/tools/stock_library.js +23 -3
- package/src/tools/visual_dna.js +9 -5
- package/src/tools/voices.js +10 -1
- package/skill/references/models/voice-tts.md +0 -85
- package/skill/references/workflows/app-builder.md +0 -160
- package/src/tools/app_builder.js +0 -253
- package/src/tools/shorts_creator.js +0 -404
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
/* ⛔ BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC
|
|
2
|
-
* CONTRACT. Never rename, remove, or break an existing tool/arg — old cached
|
|
3
|
-
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
|
-
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
|
-
|
|
6
|
-
const { z } = require('zod');
|
|
7
|
-
const { projectIdField, uiGenerating, appsEnabled } = require('./_shared');
|
|
8
|
-
const { UI, uiResult } = require('../apps');
|
|
9
|
-
|
|
10
|
-
/* ────────────────────────────────────────────────────────────────────────────
|
|
11
|
-
* Shorts Creator — two-phase job flow (NOT the generic generation state
|
|
12
|
-
* machine, so it doesn't use ../polling.js):
|
|
13
|
-
*
|
|
14
|
-
* 1. shorts_analyze → POST /v1/generate/shorts/analyze (flat 15 credits)
|
|
15
|
-
* job.phase: ANALYZING → AWAITING_SELECTION (moments ready to pick)
|
|
16
|
-
* 2. shorts_render → POST /v1/generate/shorts/:jobId/render
|
|
17
|
-
* job.phase: RENDERING → COMPLETED | PARTIALLY_COMPLETED | FAILED | CANCELLED
|
|
18
|
-
*
|
|
19
|
-
* All routes return { status: true, data: <job or payload> }.
|
|
20
|
-
* ──────────────────────────────────────────────────────────────────────────*/
|
|
21
|
-
|
|
22
|
-
const TERMINAL_PHASES = new Set(['COMPLETED', 'PARTIALLY_COMPLETED', 'FAILED', 'CANCELLED']);
|
|
23
|
-
|
|
24
|
-
// Same transient-tolerance philosophy as ../polling.js: a kolbo-api restart or
|
|
25
|
-
// network blip mid-poll must not abandon a job that's still rendering.
|
|
26
|
-
const TRANSIENT_STATUS_CODES = new Set([0, 408, 425, 429, 500, 502, 503, 504, 522, 524]);
|
|
27
|
-
function isTransientPollError(err) {
|
|
28
|
-
if (!err) return false;
|
|
29
|
-
if (err.name === 'TypeError') return true;
|
|
30
|
-
if (err.code === 'ECONNRESET' || err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.code === 'EPIPE') return true;
|
|
31
|
-
const status = err.status ?? err.options?.status;
|
|
32
|
-
if (typeof status === 'number' && TRANSIENT_STATUS_CODES.has(status)) return true;
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
class ShortsPollingTimeoutError extends Error {
|
|
37
|
-
constructor(jobId, timeoutMs, phase) {
|
|
38
|
-
const seconds = Math.round(timeoutMs / 1000);
|
|
39
|
-
super(
|
|
40
|
-
`Shorts job timed out after ${seconds}s of polling (last phase: ${phase || 'unknown'}). ` +
|
|
41
|
-
`The job may STILL be running on the server — call shorts_status with job_id="${jobId}" to check. ` +
|
|
42
|
-
`Analysis usually takes 1-3 min; rendering can take 5-20 min.`
|
|
43
|
-
);
|
|
44
|
-
this.name = 'ShortsPollingTimeoutError';
|
|
45
|
-
this.jobId = jobId;
|
|
46
|
-
this.timedOut = true;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Poll GET /v1/generate/shorts/:jobId/status until `until(job)` is true or
|
|
52
|
-
* a terminal phase is reached. Returns the raw job object.
|
|
53
|
-
*/
|
|
54
|
-
async function pollShortsJob(client, jobId, { until, interval = 10000, timeout = 300000 } = {}) {
|
|
55
|
-
const startTime = Date.now();
|
|
56
|
-
let transientFailures = 0;
|
|
57
|
-
let lastPhase = null;
|
|
58
|
-
|
|
59
|
-
while (true) {
|
|
60
|
-
if (Date.now() - startTime > timeout) {
|
|
61
|
-
throw new ShortsPollingTimeoutError(jobId, timeout, lastPhase);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let job;
|
|
65
|
-
try {
|
|
66
|
-
const result = await client.get(`/v1/generate/shorts/${encodeURIComponent(jobId)}/status`);
|
|
67
|
-
job = result.data || result;
|
|
68
|
-
transientFailures = 0;
|
|
69
|
-
} catch (err) {
|
|
70
|
-
if (isTransientPollError(err)) {
|
|
71
|
-
transientFailures++;
|
|
72
|
-
if (transientFailures > 30) throw err;
|
|
73
|
-
const backoff = Math.min(interval * Math.pow(1.5, Math.min(transientFailures - 1, 5)), 30000);
|
|
74
|
-
await new Promise((resolve) => setTimeout(resolve, backoff));
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
throw err;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
lastPhase = job.phase;
|
|
81
|
-
if (until(job) || TERMINAL_PHASES.has(job.phase)) return job;
|
|
82
|
-
|
|
83
|
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Compact summary of a job's rendered shorts (drop internal noise).
|
|
88
|
-
function summarizeShorts(shorts) {
|
|
89
|
-
return (shorts || []).map((s) => ({
|
|
90
|
-
moment_index: s.momentIndex,
|
|
91
|
-
status: s.status,
|
|
92
|
-
mode: s.mode,
|
|
93
|
-
preset: s.presetIdentifier,
|
|
94
|
-
final_url: s.finalUrl || null,
|
|
95
|
-
duration: s.duration ?? null,
|
|
96
|
-
error: s.error_message || null
|
|
97
|
-
}));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function summarizeMoments(moments) {
|
|
101
|
-
return (moments || []).map((m, i) => ({
|
|
102
|
-
moment_index: i,
|
|
103
|
-
start: m.start,
|
|
104
|
-
end: m.end,
|
|
105
|
-
title: m.title,
|
|
106
|
-
hook: m.hook,
|
|
107
|
-
score: m.score,
|
|
108
|
-
accent_beats: (m.accentBeats || []).map((b) => ({ start: b.start, end: b.end, reason: b.reason }))
|
|
109
|
-
}));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Shared zod schema for the shorts selection array used by both
|
|
113
|
-
// shorts_estimate and shorts_render. Snake_case args (MCP house style) are
|
|
114
|
-
// mapped to the backend's camelCase in buildSelectionBody().
|
|
115
|
-
const shortsSelectionField = z.array(z.object({
|
|
116
|
-
moment_index: z.number().int().min(0).describe('Index of the moment in the analysis.moments array returned by shorts_analyze / shorts_status.'),
|
|
117
|
-
mode: z.enum(['accents', 'full']).optional().describe('"accents" = restyle only the strongest beats of the moment (cheaper, 1-3 restyled chunks). "full" = restyle the entire clip (pricier, one chunk per ~10s). Omit to use the preset\'s default_mode.'),
|
|
118
|
-
preset_identifier: z.string().describe('Style preset identifier from shorts_list_presets (each preset has a name, description, and preview video).'),
|
|
119
|
-
subtitles_enabled: z.boolean().optional().describe('Burn subtitles into the short. The restyle itself NEVER adds text — subtitles come only from this step. Default: false.'),
|
|
120
|
-
subtitles_preset: z.string().optional().describe('VEED subtitle style preset name (default "glass"). Only used when subtitles_enabled is true.'),
|
|
121
|
-
start: z.number().optional().describe('Optional override of the moment\'s start time in seconds (trim/extend the suggested window). Final short must be 15-90s.'),
|
|
122
|
-
end: z.number().optional().describe('Optional override of the moment\'s end time in seconds. Final short must be 15-90s.'),
|
|
123
|
-
delete_ranges: z.array(z.object({
|
|
124
|
-
start: z.number().describe('Range start in ABSOLUTE source-video seconds.'),
|
|
125
|
-
end: z.number().describe('Range end in ABSOLUTE source-video seconds.')
|
|
126
|
-
})).optional().describe('Optional ranges to CUT from the short (dead air, filler, tangents). Times are absolute source seconds (same timeline as the moment\'s start/end). The server enforces at least 8s of remaining footage after cuts. Cuts shorten the effective duration, so shorts_estimate reflects a cheaper chunk count.'),
|
|
127
|
-
srt_content: z.string().optional().describe('Optional user-edited SRT subtitle content (max 200KB). Timestamps must be in the CUT timeline (after delete_ranges are applied), starting at 0. Build it from shorts_get_transcript word timings. Providing this implies burned-in subtitles unless subtitles_enabled is explicitly false.')
|
|
128
|
-
})).min(1).max(5).describe('Up to 5 shorts to price/render, each picking one analyzed moment + a style preset.');
|
|
129
|
-
|
|
130
|
-
function buildSelectionBody(shorts) {
|
|
131
|
-
return {
|
|
132
|
-
shorts: shorts.map((s) => {
|
|
133
|
-
const out = { momentIndex: s.moment_index, presetIdentifier: s.preset_identifier };
|
|
134
|
-
if (s.mode) out.mode = s.mode;
|
|
135
|
-
if (s.subtitles_enabled != null || s.subtitles_preset || s.srt_content) {
|
|
136
|
-
out.subtitles = {
|
|
137
|
-
// srt_content implies subtitles unless subtitles_enabled is explicitly false
|
|
138
|
-
enabled: s.subtitles_enabled != null ? !!s.subtitles_enabled : !!s.srt_content,
|
|
139
|
-
veedPreset: s.subtitles_preset || 'glass'
|
|
140
|
-
};
|
|
141
|
-
if (s.srt_content) out.subtitles.srtContent = s.srt_content;
|
|
142
|
-
}
|
|
143
|
-
if (s.start != null) out.start = s.start;
|
|
144
|
-
if (s.end != null) out.end = s.end;
|
|
145
|
-
if (Array.isArray(s.delete_ranges) && s.delete_ranges.length) {
|
|
146
|
-
out.deleteRanges = s.delete_ranges.map((r) => ({ start: r.start, end: r.end }));
|
|
147
|
-
}
|
|
148
|
-
return out;
|
|
149
|
-
})
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function registerShortsCreatorTools(server, client, options = {}) {
|
|
154
|
-
// MCP Apps hosts (claude.ai remote connector, Claude Desktop) get widget
|
|
155
|
-
// results; text-only hosts keep the exact blocking behavior below.
|
|
156
|
-
const ui = () => appsEnabled(server, options);
|
|
157
|
-
// ─── shorts_analyze ───────────────────────────────────────────
|
|
158
|
-
server.tool(
|
|
159
|
-
'shorts_analyze',
|
|
160
|
-
'PHASE 1 of the Shorts Creator: analyze a long video (up to 30 min) and get back the AI-picked best moments for short-form clips. Costs a flat 15 credits. The video URL MUST be a Kolbo media-library URL — upload local/external files first with upload_media. This tool submits the analysis and polls until the moments are ready (usually 1-3 min), then returns the moments list: each has start/end (seconds in the source), a title, a hook, a virality score, and accent beats. NEXT STEP: show the moments to the user, pick up to 5, choose a style preset (shorts_list_presets) + mode ("accents" = cheaper, restyles only the strongest beats; "full" = pricier, restyles everything), optionally price with shorts_estimate, then start PHASE 2 with shorts_render.',
|
|
161
|
-
{
|
|
162
|
-
video_url: z.string().describe('Kolbo media-library URL of the source video (from upload_media / list_media). External URLs are rejected — upload first. Source must be between ~30s and 30 min.'),
|
|
163
|
-
project_id: projectIdField
|
|
164
|
-
},
|
|
165
|
-
async ({ video_url, project_id }) => {
|
|
166
|
-
const submitted = await client.post('/v1/generate/shorts/analyze', { video_url, project_id });
|
|
167
|
-
const d = submitted.data || {};
|
|
168
|
-
const jobId = d.jobId;
|
|
169
|
-
|
|
170
|
-
let job;
|
|
171
|
-
try {
|
|
172
|
-
job = await pollShortsJob(client, jobId, {
|
|
173
|
-
until: (j) => j.phase === 'AWAITING_SELECTION',
|
|
174
|
-
interval: 10000,
|
|
175
|
-
timeout: 300000 // ~5 min — analysis usually takes 1-3 min
|
|
176
|
-
});
|
|
177
|
-
} catch (err) {
|
|
178
|
-
if (err.timedOut) {
|
|
179
|
-
return { content: [{ type: 'text', text: `${err.message}\n\njob_id: ${jobId}` }] };
|
|
180
|
-
}
|
|
181
|
-
throw err;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (job.phase === 'FAILED' || job.phase === 'CANCELLED') {
|
|
185
|
-
throw new Error(`Shorts analysis ${job.phase.toLowerCase()}: ${job.error_message || 'unknown error'} (job_id="${jobId}")`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const moments = summarizeMoments(job.analysis?.moments);
|
|
189
|
-
const text = JSON.stringify({
|
|
190
|
-
job_id: jobId,
|
|
191
|
-
phase: job.phase,
|
|
192
|
-
analysis_credits: d.analysisCredits,
|
|
193
|
-
source: d.source || job.source,
|
|
194
|
-
moments,
|
|
195
|
-
_followup_hint: 'Show these moments to the user and let them pick up to 5. Then call shorts_list_presets for styles, optionally shorts_estimate to price the selection (free), and shorts_render to produce the shorts. Each rendered short must be 15-90s.'
|
|
196
|
-
}, null, 2);
|
|
197
|
-
|
|
198
|
-
if (ui()) return uiResult(UI.mediaGrid, text, {
|
|
199
|
-
widget: 'media-grid',
|
|
200
|
-
title: 'Shorts — Best Moments',
|
|
201
|
-
items: moments.map((m) => {
|
|
202
|
-
const duration = (m.end != null && m.start != null) ? m.end - m.start : null;
|
|
203
|
-
return {
|
|
204
|
-
id: String(m.moment_index),
|
|
205
|
-
title: 'Moment ' + m.moment_index + (duration ? ' · ' + Math.round(duration) + 's' : ''),
|
|
206
|
-
subtitle: String(m.title || m.hook || '').slice(0, 80),
|
|
207
|
-
media_type: 'video',
|
|
208
|
-
use_hint: `Render short for moment {ID} of shorts job ${jobId} (shorts_render). First shorts_estimate it.`
|
|
209
|
-
};
|
|
210
|
-
}),
|
|
211
|
-
total: moments.length
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
return { content: [{ type: 'text', text }] };
|
|
215
|
-
}
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
// ─── shorts_estimate ──────────────────────────────────────────
|
|
219
|
-
server.tool(
|
|
220
|
-
'shorts_estimate',
|
|
221
|
-
'Price a Shorts Creator selection BEFORE rendering — free, no credits charged. Pass the job_id from shorts_analyze and the shorts selection (moments + presets + modes + subtitles, plus optional delete_ranges cuts / edited srt_content). delete_ranges shorten the effective duration, so the estimate gets cheaper. Returns total credits and a per-short breakdown with chunk counts. Pricing: each restyled chunk is a flat 200 credits ("accents" mode = 1-3 chunks per short, "full" mode = one chunk per ~10s of clip length); burned-in subtitles add 40 credits/min (60s minimum). Call this to confirm cost with the user before shorts_render.',
|
|
222
|
-
{
|
|
223
|
-
job_id: z.string().describe('The Shorts job id from shorts_analyze.'),
|
|
224
|
-
shorts: shortsSelectionField
|
|
225
|
-
},
|
|
226
|
-
async ({ job_id, shorts }) => {
|
|
227
|
-
const result = await client.post(`/v1/generate/shorts/${encodeURIComponent(job_id)}/estimate`, buildSelectionBody(shorts));
|
|
228
|
-
const d = result.data || {};
|
|
229
|
-
return {
|
|
230
|
-
content: [{
|
|
231
|
-
type: 'text',
|
|
232
|
-
text: JSON.stringify({
|
|
233
|
-
job_id,
|
|
234
|
-
total_credits: d.totalCredits,
|
|
235
|
-
per_short: (d.perShort || []).map((p) => ({ moment_index: p.momentIndex, credits: p.credits, chunk_count: p.chunkCount })),
|
|
236
|
-
_followup_hint: 'If the user approves the cost, call shorts_render with the SAME shorts selection.'
|
|
237
|
-
}, null, 2)
|
|
238
|
-
}]
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// ─── shorts_render ────────────────────────────────────────────
|
|
244
|
-
server.tool(
|
|
245
|
-
'shorts_render',
|
|
246
|
-
'PHASE 2 of the Shorts Creator: render the selected shorts (max 5, each 15-90s). Credits are reserved up-front (price with shorts_estimate first); any short that fails is auto-refunded. Each short takes one analyzed moment and applies a style preset in "accents" mode (restyle only the strongest beats, cheaper) or "full" mode (restyle everything, pricier), plus optional burned-in subtitles (VEED preset, default "glass" — the restyle itself NEVER adds text). Per short you can also pass delete_ranges (cut dead air / filler, absolute source seconds, ≥8s must remain) and srt_content (user-edited SRT, cut-timeline timestamps — build it with shorts_get_transcript). This tool submits the render and polls until done (can take 5-20 min), then returns each short\'s final video URL. On PARTIALLY_COMPLETED it returns both the successes (with URLs) and the failures (with errors, refunded).',
|
|
247
|
-
{
|
|
248
|
-
job_id: z.string().describe('The Shorts job id from shorts_analyze (must be in AWAITING_SELECTION phase).'),
|
|
249
|
-
shorts: shortsSelectionField
|
|
250
|
-
},
|
|
251
|
-
async ({ job_id, shorts }) => {
|
|
252
|
-
await client.post(`/v1/generate/shorts/${encodeURIComponent(job_id)}/render`, buildSelectionBody(shorts));
|
|
253
|
-
|
|
254
|
-
// UI hosts: return immediately — the generation widget polls shorts_status
|
|
255
|
-
// itself (shorts_status adds widget-friendly state/result fields when
|
|
256
|
-
// the job reaches a terminal phase).
|
|
257
|
-
if (ui()) return uiGenerating({
|
|
258
|
-
tool: 'generate_video', kind: 'video',
|
|
259
|
-
gen: { generation_id: job_id }, client,
|
|
260
|
-
model: 'Shorts Creator',
|
|
261
|
-
prompt: 'Rendering ' + shorts.length + ' short(s)',
|
|
262
|
-
count: Math.min(shorts.length, 4),
|
|
263
|
-
settings: { mode: 'shorts' },
|
|
264
|
-
poll_tool: 'shorts_status',
|
|
265
|
-
status_args: { job_id },
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
let job;
|
|
269
|
-
try {
|
|
270
|
-
job = await pollShortsJob(client, job_id, {
|
|
271
|
-
until: (j) => TERMINAL_PHASES.has(j.phase),
|
|
272
|
-
interval: 12000,
|
|
273
|
-
timeout: 1500000 // 25 min — rendering can take 5-20 min
|
|
274
|
-
});
|
|
275
|
-
} catch (err) {
|
|
276
|
-
if (err.timedOut) {
|
|
277
|
-
return { content: [{ type: 'text', text: `${err.message}\n\njob_id: ${job_id}` }] };
|
|
278
|
-
}
|
|
279
|
-
throw err;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (job.phase === 'FAILED') {
|
|
283
|
-
throw new Error(`Shorts render failed: ${job.error_message || 'all shorts failed'} (job_id="${job_id}"). Reserved credits for failed shorts are auto-refunded.`);
|
|
284
|
-
}
|
|
285
|
-
if (job.phase === 'CANCELLED') {
|
|
286
|
-
throw new Error(`Shorts job was cancelled (job_id="${job_id}"). Unused credits were refunded.`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const all = summarizeShorts(job.shorts);
|
|
290
|
-
const succeeded = all.filter((s) => s.final_url);
|
|
291
|
-
const failed = all.filter((s) => !s.final_url);
|
|
292
|
-
return {
|
|
293
|
-
content: [{
|
|
294
|
-
type: 'text',
|
|
295
|
-
text: JSON.stringify({
|
|
296
|
-
job_id,
|
|
297
|
-
phase: job.phase,
|
|
298
|
-
shorts: succeeded,
|
|
299
|
-
...(failed.length ? { failed_shorts: failed, _note: 'Credits for failed shorts are automatically refunded.' } : {}),
|
|
300
|
-
_followup_hint: 'Each final_url is a finished vertical short. To restyle differently, render again from the same job with a different preset/mode.'
|
|
301
|
-
}, null, 2)
|
|
302
|
-
}]
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
// ─── shorts_status ────────────────────────────────────────────
|
|
308
|
-
server.tool(
|
|
309
|
-
'shorts_status',
|
|
310
|
-
'Get the current state of a Shorts Creator job in one read (no polling). Use after a shorts_analyze / shorts_render timeout, or to resume a job later. Phases: ANALYZING → AWAITING_SELECTION (moments ready — pick and render) → RENDERING → COMPLETED / PARTIALLY_COMPLETED / FAILED / CANCELLED. Returns the moments list when awaiting selection and the shorts (with final URLs) when rendering/done.',
|
|
311
|
-
{
|
|
312
|
-
job_id: z.string().describe('The Shorts job id.')
|
|
313
|
-
},
|
|
314
|
-
async ({ job_id }) => {
|
|
315
|
-
const result = await client.get(`/v1/generate/shorts/${encodeURIComponent(job_id)}/status`);
|
|
316
|
-
const job = result.data || {};
|
|
317
|
-
const out = { job_id, phase: job.phase };
|
|
318
|
-
if (job.source) out.source = job.source;
|
|
319
|
-
if (job.analysis?.moments?.length) out.moments = summarizeMoments(job.analysis.moments);
|
|
320
|
-
if (job.shorts?.length) out.shorts = summarizeShorts(job.shorts);
|
|
321
|
-
if (job.error_message) out.error = job.error_message;
|
|
322
|
-
|
|
323
|
-
// ADDITIVE widget-compat fields (MCP Apps generation widget polls this
|
|
324
|
-
// tool and expects lowercase state + result.urls — see
|
|
325
|
-
// src/apps/widgets/generation.js poll()). Existing fields untouched.
|
|
326
|
-
if (job.phase === 'COMPLETED' || job.phase === 'PARTIALLY_COMPLETED') {
|
|
327
|
-
const urls = (out.shorts || []).map((s) => s.final_url).filter(Boolean);
|
|
328
|
-
if (urls.length) {
|
|
329
|
-
out.state = 'completed';
|
|
330
|
-
out.result = { urls };
|
|
331
|
-
} else {
|
|
332
|
-
out.state = 'failed';
|
|
333
|
-
}
|
|
334
|
-
} else if (job.phase === 'FAILED') {
|
|
335
|
-
out.state = 'failed';
|
|
336
|
-
} else if (job.phase === 'CANCELLED') {
|
|
337
|
-
out.state = 'cancelled';
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] };
|
|
341
|
-
}
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
// ─── shorts_get_transcript ────────────────────────────────────
|
|
345
|
-
server.tool(
|
|
346
|
-
'shorts_get_transcript',
|
|
347
|
-
'Get the word-level transcript of a Shorts Creator job\'s source video (from the analysis-phase Scribe transcription — free, no extra credits). Returns { words, language, sourceDuration } where each word has its text and start/end times in ABSOLUTE source seconds. Use this for the Review & Edit workflow: map words to the picked moment\'s window, decide delete_ranges (filler/dead air to cut, absolute source seconds), and build an edited SRT (srt_content, timestamps in the CUT timeline) to pass in the shorts_estimate / shorts_render selection.',
|
|
348
|
-
{
|
|
349
|
-
job_id: z.string().describe('The Shorts job id from shorts_analyze (analysis must be complete).')
|
|
350
|
-
},
|
|
351
|
-
async ({ job_id }) => {
|
|
352
|
-
const result = await client.get(`/v1/generate/shorts/${encodeURIComponent(job_id)}/transcript`);
|
|
353
|
-
const d = result.data || {};
|
|
354
|
-
return {
|
|
355
|
-
content: [{
|
|
356
|
-
type: 'text',
|
|
357
|
-
text: JSON.stringify({
|
|
358
|
-
job_id,
|
|
359
|
-
language: d.language || null,
|
|
360
|
-
source_duration: d.sourceDuration ?? null,
|
|
361
|
-
words: d.words || [],
|
|
362
|
-
_followup_hint: 'Word times are absolute source seconds. To edit a short: pick delete_ranges (absolute source seconds, ≥8s must remain) and/or build an edited SRT whose timestamps are in the cut timeline (after deletions), then pass delete_ranges / srt_content per short to shorts_estimate and shorts_render.'
|
|
363
|
-
}, null, 2)
|
|
364
|
-
}]
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
// ─── shorts_list_presets ──────────────────────────────────────
|
|
370
|
-
server.tool(
|
|
371
|
-
'shorts_list_presets',
|
|
372
|
-
'List the Shorts Creator style presets — each restyles the picked moment into a distinct visual style (identifier, name, description, thumbnail, preview video, default mode, default subtitle preset, category). Call before shorts_estimate / shorts_render so the user can pick a style; pass the `identifier` as preset_identifier.',
|
|
373
|
-
{},
|
|
374
|
-
async () => {
|
|
375
|
-
const result = await client.get('/v1/generate/shorts/presets');
|
|
376
|
-
const presets = (result.data || []).map((p) => ({
|
|
377
|
-
identifier: p.identifier,
|
|
378
|
-
name: p.name,
|
|
379
|
-
description: p.description,
|
|
380
|
-
category: p.category,
|
|
381
|
-
default_mode: p.default_mode,
|
|
382
|
-
default_subtitle_preset: p.default_veed_preset,
|
|
383
|
-
thumbnail_url: p.thumbnail_url,
|
|
384
|
-
preview_video_url: p.preview_video_url
|
|
385
|
-
}));
|
|
386
|
-
return { content: [{ type: 'text', text: JSON.stringify({ count: presets.length, presets }, null, 2) }] };
|
|
387
|
-
}
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
// ─── shorts_cancel ────────────────────────────────────────────
|
|
391
|
-
server.tool(
|
|
392
|
-
'shorts_cancel',
|
|
393
|
-
'Cancel a running Shorts Creator job (analysis or render) and refund all unused reserved credits. Use when the user changes their mind or a job is stuck.',
|
|
394
|
-
{
|
|
395
|
-
job_id: z.string().describe('The Shorts job id to cancel.')
|
|
396
|
-
},
|
|
397
|
-
async ({ job_id }) => {
|
|
398
|
-
const result = await client.post(`/v1/generate/shorts/${encodeURIComponent(job_id)}/cancel`, {});
|
|
399
|
-
return { content: [{ type: 'text', text: JSON.stringify({ job_id, cancelled: true, ...(result.data && typeof result.data === 'object' ? result.data : {}) }, null, 2) }] };
|
|
400
|
-
}
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
module.exports = { registerShortsCreatorTools };
|