cerevox 0.3.3 → 0.3.5

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.
@@ -17,6 +17,45 @@ const constants_1 = require("../../utils/constants");
17
17
  const videokit_1 = require("../../utils/videokit");
18
18
  const promises_1 = require("node:fs/promises");
19
19
  const node_path_1 = require("node:path");
20
+ // 获取当前文件的目录路径
21
+ const __dirname = (0, node_path_1.dirname)(__filename);
22
+ // 错误处理工具函数
23
+ function createErrorResponse(error, operation) {
24
+ const errorMessage = error instanceof Error ? error.message : String(error);
25
+ console.error(`[${operation}] Error:`, error);
26
+ return {
27
+ content: [
28
+ {
29
+ type: 'text',
30
+ text: JSON.stringify({
31
+ success: false,
32
+ error: errorMessage,
33
+ operation,
34
+ timestamp: new Date().toISOString(),
35
+ }),
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ // Session 状态检查
41
+ function validateSession(operation) {
42
+ if (!session) {
43
+ throw new Error(`Session not initialized. Please call 'zerocut-project-open' first before using ${operation}.`);
44
+ }
45
+ return session;
46
+ }
47
+ // 文件名验证
48
+ function validateFileName(fileName) {
49
+ if (!fileName || fileName.trim() === '') {
50
+ throw new Error('File name cannot be empty');
51
+ }
52
+ if (fileName.includes('..') ||
53
+ fileName.includes('/') ||
54
+ fileName.includes('\\')) {
55
+ throw new Error('Invalid file name: contains illegal characters');
56
+ }
57
+ return fileName.trim();
58
+ }
20
59
  /* Configuration
21
60
  {
22
61
  "mcpServers": {
@@ -105,85 +144,266 @@ server.registerTool('zerocut-project-open', {
105
144
  .describe('The path of the file to upload.'),
106
145
  },
107
146
  }, async ({ localDir }, context) => {
108
- session = await cerevox.launch({
109
- browser: {
110
- liveview: true,
111
- },
112
- keepAliveMS: 10000,
113
- timeoutMS: 3600000,
114
- });
115
- const workDir = await initProject(session);
116
- projectLocalDir = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), localDir);
117
- const syncDir = (0, node_path_1.resolve)(projectLocalDir, 'materials');
118
- await (0, promises_1.mkdir)(syncDir, { recursive: true });
119
- const materials = await listFiles(syncDir);
120
- const files = session.files;
121
- let progress = 0;
122
- for (const material of materials) {
123
- await files.upload(material, `${workDir}/materials/${(0, node_path_1.basename)(material)}`);
124
- sendProgress(context, ++progress, materials.length, material);
125
- }
126
- return {
127
- content: [
128
- {
129
- type: 'text',
130
- text: JSON.stringify({
131
- sessionId: session.id,
132
- workDir,
133
- projectLocalDir,
134
- materials,
135
- }),
147
+ try {
148
+ // 检查是否已有活跃session
149
+ if (session) {
150
+ console.warn('Session already exists, closing previous session');
151
+ try {
152
+ await session.close();
153
+ }
154
+ catch (closeError) {
155
+ console.warn('Failed to close previous session:', closeError);
156
+ }
157
+ }
158
+ // 验证API密钥
159
+ if (!process.env.CEREVOX_API_KEY) {
160
+ throw new Error('CEREVOX_API_KEY environment variable is required');
161
+ }
162
+ console.log('Launching new Cerevox session...');
163
+ session = await cerevox.launch({
164
+ browser: {
165
+ liveview: true,
136
166
  },
137
- ],
138
- };
167
+ keepAliveMS: 10000,
168
+ timeoutMS: 3600000,
169
+ });
170
+ if (!session) {
171
+ throw new Error('Failed to create Cerevox session');
172
+ }
173
+ console.log('Initializing project...');
174
+ const workDir = await initProject(session);
175
+ projectLocalDir = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), localDir || '.');
176
+ const syncDir = (0, node_path_1.resolve)(projectLocalDir, 'materials');
177
+ try {
178
+ await (0, promises_1.mkdir)(syncDir, { recursive: true });
179
+ }
180
+ catch (mkdirError) {
181
+ console.warn('Failed to create materials directory:', mkdirError);
182
+ // 继续执行,可能目录已存在
183
+ }
184
+ let materials = [];
185
+ try {
186
+ materials = await listFiles(syncDir);
187
+ }
188
+ catch (listError) {
189
+ console.warn('Failed to list materials:', listError);
190
+ materials = [];
191
+ }
192
+ const files = session.files;
193
+ let progress = 0;
194
+ const uploadErrors = [];
195
+ for (const material of materials) {
196
+ try {
197
+ await files.upload(material, `${workDir}/materials/${(0, node_path_1.basename)(material)}`);
198
+ await sendProgress(context, ++progress, materials.length, material);
199
+ }
200
+ catch (uploadError) {
201
+ const errorMsg = `Failed to upload ${material}: ${uploadError}`;
202
+ console.error(errorMsg);
203
+ uploadErrors.push(errorMsg);
204
+ }
205
+ }
206
+ const result = {
207
+ success: true,
208
+ sessionId: session.id,
209
+ workDir,
210
+ projectLocalDir,
211
+ materials,
212
+ uploadErrors: uploadErrors.length > 0 ? uploadErrors : undefined,
213
+ };
214
+ return {
215
+ content: [
216
+ {
217
+ type: 'text',
218
+ text: JSON.stringify(result),
219
+ },
220
+ ],
221
+ };
222
+ }
223
+ catch (error) {
224
+ // 不自动关闭session,让agent根据异常信息自行处理
225
+ return createErrorResponse(error, 'zerocut-project-open');
226
+ }
139
227
  });
140
228
  server.registerTool('zerocut-project-close', {
141
229
  title: 'Close Project',
142
230
  description: 'Close the current Cerevox session and release all resources.',
143
231
  inputSchema: {},
144
232
  }, async () => {
145
- if (session) {
146
- await session.close();
233
+ try {
234
+ if (session) {
235
+ console.log('Closing Cerevox session...');
236
+ await session.close();
237
+ session = null;
238
+ console.log('Session closed successfully');
239
+ }
240
+ else {
241
+ console.warn('No active session to close');
242
+ }
243
+ return {
244
+ content: [
245
+ {
246
+ type: 'text',
247
+ text: JSON.stringify({
248
+ success: true,
249
+ message: 'Project closed successfully.',
250
+ timestamp: new Date().toISOString(),
251
+ }),
252
+ },
253
+ ],
254
+ };
255
+ }
256
+ catch (error) {
257
+ // 即使关闭失败,也要清理session引用
147
258
  session = null;
259
+ return createErrorResponse(error, 'zerocut-project-close');
148
260
  }
149
- return {
150
- content: [{ type: 'text', text: 'Cerevox session closed' }],
151
- };
152
261
  });
153
262
  // 将完成的成品下载到本地
263
+ // server.registerTool(
264
+ // 'download-outputs',
265
+ // {
266
+ // title: 'Download Outputs',
267
+ // description: 'Download the output files from the server.',
268
+ // inputSchema: {},
269
+ // },
270
+ // async (_, context) => {
271
+ // const terminal = session!.terminal;
272
+ // const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/output`;
273
+ // const outputDir = resolve(
274
+ // process.env.ZEROCUT_PROJECT_CWD || process.cwd(),
275
+ // projectLocalDir,
276
+ // 'output'
277
+ // );
278
+ // await mkdir(outputDir, { recursive: true });
279
+ // let progress = 0;
280
+ // const outputs = (await session?.files.listFiles(workDir)) || [];
281
+ // const files = session!.files;
282
+ // for (const output of outputs) {
283
+ // await files.download(`${workDir}/${output}`, `${outputDir}/${output}`);
284
+ // sendProgress(context, ++progress, outputs.length, output);
285
+ // }
286
+ // return {
287
+ // content: [
288
+ // { type: 'text', text: JSON.stringify({ success: true, outputs }) },
289
+ // ],
290
+ // };
291
+ // }
292
+ // );
154
293
  server.registerTool('download-outputs', {
155
- title: 'Download Outputs',
294
+ title: 'Download Outputs from HTTP',
156
295
  description: 'Download the output files from the server.',
157
296
  inputSchema: {},
158
297
  }, async (_, context) => {
159
- const terminal = session.terminal;
160
- const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/output`;
161
- const outputDir = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'output');
162
- await (0, promises_1.mkdir)(outputDir, { recursive: true });
163
- let progress = 0;
164
- const outputs = (await session?.files.listFiles(workDir)) || [];
165
- const files = session.files;
166
- for (const output of outputs) {
167
- await files.download(`${workDir}/${output}`, `${outputDir}/${output}`);
168
- sendProgress(context, ++progress, outputs.length, output);
298
+ try {
299
+ // 验证session状态
300
+ const currentSession = validateSession('download-outputs');
301
+ console.log('Starting download outputs process...');
302
+ const terminal = currentSession.terminal;
303
+ if (!terminal) {
304
+ throw new Error('Terminal not available in current session');
305
+ }
306
+ const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/output`;
307
+ const host = currentSession.getHost();
308
+ if (!host) {
309
+ throw new Error('Host information not available from session');
310
+ }
311
+ console.log(`Listing output files from: ${workDir}`);
312
+ let outputs = [];
313
+ try {
314
+ outputs = (await currentSession.files.listFiles(workDir)) || [];
315
+ }
316
+ catch (listError) {
317
+ console.warn('Failed to list output files:', listError);
318
+ outputs = [];
319
+ }
320
+ if (outputs.length === 0) {
321
+ console.log('No output files found');
322
+ return {
323
+ content: [
324
+ {
325
+ type: 'text',
326
+ text: JSON.stringify({
327
+ success: true,
328
+ status: 'no_files',
329
+ message: 'No output files found to download',
330
+ sources: [],
331
+ timestamp: new Date().toISOString(),
332
+ }),
333
+ },
334
+ ],
335
+ };
336
+ }
337
+ console.log(`Found ${outputs.length} output files`);
338
+ const publicDir = `/home/user/public/zerocut`;
339
+ try {
340
+ const copyCommand = `mkdir -p ${publicDir} && cp -R ${workDir} ${publicDir}`;
341
+ console.log(`Executing: ${copyCommand}`);
342
+ const copyResult = await terminal.run(copyCommand);
343
+ await copyResult.end();
344
+ console.log('Files copied to public directory successfully');
345
+ }
346
+ catch (copyError) {
347
+ console.error('Failed to copy files to public directory:', copyError);
348
+ throw new Error(`Failed to prepare files for download: ${copyError}`);
349
+ }
350
+ let progress = 0;
351
+ const downloadErrors = [];
352
+ const successfulDownloads = [];
353
+ const outputDir = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'output');
354
+ await (0, promises_1.mkdir)(outputDir, { recursive: true });
355
+ const promises = outputs.map(async (output) => {
356
+ try {
357
+ const url = `https://${host}/public/zerocut/output/${output}`;
358
+ console.log(`Downloading: ${url}`);
359
+ const res = await fetch(url);
360
+ if (!res.ok) {
361
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
362
+ }
363
+ const content = await res.arrayBuffer();
364
+ const buffer = Buffer.from(content);
365
+ await (0, promises_1.writeFile)(`${outputDir}/${output}`, buffer);
366
+ try {
367
+ await sendProgress(context, ++progress, outputs.length, output);
368
+ }
369
+ catch (progressError) {
370
+ console.warn('Failed to send progress update:', progressError);
371
+ }
372
+ successfulDownloads.push(url);
373
+ return url;
374
+ }
375
+ catch (downloadError) {
376
+ const errorMsg = `Failed to download ${output}: ${downloadError}`;
377
+ console.error(errorMsg);
378
+ downloadErrors.push(errorMsg);
379
+ return null;
380
+ }
381
+ });
382
+ const results = await Promise.all(promises);
383
+ const sources = results.filter(url => url !== null);
384
+ const result = {
385
+ success: downloadErrors.length === 0,
386
+ status: 'downloaded',
387
+ sources,
388
+ totalFiles: outputs.length,
389
+ successfulDownloads: sources.length,
390
+ failedDownloads: downloadErrors.length,
391
+ downloadErrors: downloadErrors.length > 0 ? downloadErrors : undefined,
392
+ timestamp: new Date().toISOString(),
393
+ };
394
+ console.log(`Download completed: ${sources.length}/${outputs.length} files successful`);
395
+ return {
396
+ content: [
397
+ {
398
+ type: 'text',
399
+ text: JSON.stringify(result),
400
+ },
401
+ ],
402
+ };
403
+ }
404
+ catch (error) {
405
+ return createErrorResponse(error, 'download-outputs');
169
406
  }
170
- // await session!.files.syncDownloadsDirectory(
171
- // outputDir,
172
- // workDir,
173
- // async metaData => {
174
- // await sendProgress(
175
- // context,
176
- // ++progress,
177
- // outputs.length,
178
- // JSON.stringify(metaData)
179
- // );
180
- // }
181
- // );
182
- return {
183
- content: [
184
- { type: 'text', text: JSON.stringify({ success: true, outputs }) },
185
- ],
186
- };
187
407
  });
188
408
  // 列出项目下的所有文件
189
409
  server.registerTool('list-project-files', {
@@ -191,67 +411,63 @@ server.registerTool('list-project-files', {
191
411
  description: 'List all files in the materials directory.',
192
412
  inputSchema: {},
193
413
  }, async () => {
194
- const terminal = session.terminal;
195
- if (!terminal) {
414
+ try {
415
+ // 验证session状态
416
+ const currentSession = validateSession('list-project-files');
417
+ console.log('Listing project files...');
418
+ const terminal = currentSession.terminal;
419
+ if (!terminal) {
420
+ throw new Error('Terminal not available in current session');
421
+ }
422
+ let cwd;
423
+ try {
424
+ cwd = await terminal.getCwd();
425
+ }
426
+ catch (cwdError) {
427
+ console.error('Failed to get current working directory:', cwdError);
428
+ throw new Error('Failed to get current working directory');
429
+ }
430
+ console.log(`Current working directory: ${cwd}`);
431
+ // 安全地列出各目录文件,失败时返回空数组
432
+ const listFilesWithFallback = async (path, dirName) => {
433
+ try {
434
+ const files = await currentSession.files.listFiles(path);
435
+ console.log(`Found ${files?.length || 0} files in ${dirName}`);
436
+ return files || [];
437
+ }
438
+ catch (error) {
439
+ console.warn(`Failed to list files in ${dirName} (${path}):`, error);
440
+ return [];
441
+ }
442
+ };
443
+ const [rootFiles, materialsFiles, outputFiles] = await Promise.all([
444
+ listFilesWithFallback(cwd, 'root'),
445
+ listFilesWithFallback(`${cwd}/materials`, 'materials'),
446
+ listFilesWithFallback(`${cwd}/output`, 'output'),
447
+ ]);
448
+ const result = {
449
+ success: true,
450
+ cwd,
451
+ root: rootFiles,
452
+ materials: materialsFiles,
453
+ output: outputFiles,
454
+ totalFiles: rootFiles.length + materialsFiles.length + outputFiles.length,
455
+ timestamp: new Date().toISOString(),
456
+ };
457
+ console.log(`Total files found: ${result.totalFiles}`);
196
458
  return {
197
- content: [{ type: 'text', text: 'No session found' }],
459
+ content: [
460
+ {
461
+ type: 'text',
462
+ text: JSON.stringify(result),
463
+ },
464
+ ],
198
465
  };
199
466
  }
200
- const cwd = await terminal.getCwd();
201
- const rootFiles = await session?.files.listFiles(cwd);
202
- const materialsFiles = await session?.files.listFiles(`${cwd}/materials`);
203
- const outputFiles = await session?.files.listFiles(`${cwd}/output`);
204
- return {
205
- content: [
206
- {
207
- type: 'text',
208
- text: JSON.stringify({
209
- root: rootFiles,
210
- materials: materialsFiles,
211
- output: outputFiles,
212
- }),
213
- },
214
- ],
215
- };
467
+ catch (error) {
468
+ return createErrorResponse(error, 'list-project-files');
469
+ }
216
470
  });
217
- // server.registerTool(
218
- // 'read-file',
219
- // {
220
- // title: 'Read File',
221
- // description: 'Read the content of a file.',
222
- // inputSchema: {
223
- // path: z.string().describe('The path of the file to read.'),
224
- // },
225
- // },
226
- // async ({ path }) => {
227
- // const files = session!.files;
228
- // const content: string = await files.read(path, { encoding: 'utf8' });
229
- // return {
230
- // content: [{ type: 'text', text: content }],
231
- // };
232
- // }
233
- // );
234
- // server.registerTool(
235
- // 'write-file',
236
- // {
237
- // title: 'Write File',
238
- // description: 'Write the content to a file.',
239
- // inputSchema: {
240
- // path: z.string().describe('The path of the file to write.'),
241
- // content: z.string().describe('The content to write.'),
242
- // },
243
- // },
244
- // async ({ path, content }) => {
245
- // const files = session!.files;
246
- // await files.write(path, content);
247
- // assetsCount++;
248
- // return {
249
- // content: [
250
- // { type: 'text', text: JSON.stringify({ path, success: true }) },
251
- // ],
252
- // };
253
- // }
254
- // );
255
471
  server.registerTool('search-context', {
256
472
  title: 'Search Context',
257
473
  description: 'Search the context.',
@@ -299,22 +515,59 @@ server.registerTool('generate-image', {
299
515
  saveToFileName: zod_1.z.string().describe('The filename to save.'),
300
516
  },
301
517
  }, async ({ prompt, size, saveToFileName }) => {
302
- const ai = session.ai;
303
- const res = await ai.generateImage({
304
- prompt,
305
- size,
306
- });
307
- if (res.url) {
308
- const uri = await saveMertial(session, res.url, saveToFileName);
309
- return {
310
- content: [
311
- { type: 'text', text: JSON.stringify({ source: res.url, uri }) },
312
- ],
313
- };
518
+ try {
519
+ // 验证session状态
520
+ const currentSession = validateSession('generate-image');
521
+ const validatedFileName = validateFileName(saveToFileName);
522
+ console.log(`Generating image with prompt: ${prompt.substring(0, 100)}...`);
523
+ const ai = currentSession.ai;
524
+ const res = await ai.generateImage({
525
+ prompt: prompt.trim(),
526
+ size,
527
+ });
528
+ if (!res) {
529
+ throw new Error('Failed to generate image: no response from AI service');
530
+ }
531
+ if (res.url) {
532
+ console.log('Image generated successfully, saving to materials...');
533
+ const uri = await saveMertial(currentSession, res.url, validatedFileName);
534
+ const result = {
535
+ success: true,
536
+ source: res.url,
537
+ uri,
538
+ prompt: prompt.substring(0, 100),
539
+ size,
540
+ timestamp: new Date().toISOString(),
541
+ };
542
+ return {
543
+ content: [
544
+ {
545
+ type: 'text',
546
+ text: JSON.stringify(result),
547
+ },
548
+ ],
549
+ };
550
+ }
551
+ else {
552
+ console.warn('Image generation completed but no URL returned');
553
+ return {
554
+ content: [
555
+ {
556
+ type: 'text',
557
+ text: JSON.stringify({
558
+ success: false,
559
+ error: 'No image URL returned from AI service',
560
+ response: res,
561
+ timestamp: new Date().toISOString(),
562
+ }),
563
+ },
564
+ ],
565
+ };
566
+ }
567
+ }
568
+ catch (error) {
569
+ return createErrorResponse(error, 'generate-image');
314
570
  }
315
- return {
316
- content: [{ type: 'text', text: JSON.stringify(res) }],
317
- };
318
571
  });
319
572
  server.registerTool('generate-video', {
320
573
  title: 'Generate Video',
@@ -335,38 +588,73 @@ server.registerTool('generate-video', {
335
588
  .describe('The end frame image URL.'),
336
589
  },
337
590
  }, async ({ prompt, saveToFileName, start_frame, end_frame, duration }, context) => {
338
- const ai = session.ai;
339
- let progress = 0;
340
- const res = await ai.framesToVideo({
341
- prompt,
342
- start_frame,
343
- end_frame,
344
- duration,
345
- resolution: '720p',
346
- onProgress: async (metaData) => {
347
- await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
348
- },
349
- });
350
- if (res.url) {
351
- const uri = await saveMertial(session, res.url, saveToFileName);
352
- const { url, duration, ...opts } = res;
353
- return {
354
- content: [
355
- {
356
- type: 'text',
357
- text: JSON.stringify({
358
- source: url,
359
- uri,
360
- durationMs: Math.floor(duration * 1000),
361
- ...opts,
362
- }),
363
- },
364
- ],
365
- };
591
+ try {
592
+ // 验证session状态
593
+ const currentSession = validateSession('generate-video');
594
+ const validatedFileName = validateFileName(saveToFileName);
595
+ console.log(`Generating video with prompt: ${prompt.substring(0, 100)}...`);
596
+ const ai = currentSession.ai;
597
+ let progress = 0;
598
+ const res = await ai.framesToVideo({
599
+ prompt: prompt.trim(),
600
+ start_frame,
601
+ end_frame,
602
+ duration,
603
+ resolution: '720p',
604
+ onProgress: async (metaData) => {
605
+ try {
606
+ await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
607
+ }
608
+ catch (progressError) {
609
+ console.warn('Failed to send progress update:', progressError);
610
+ }
611
+ },
612
+ });
613
+ if (!res) {
614
+ throw new Error('Failed to generate video: no response from AI service');
615
+ }
616
+ if (res.url) {
617
+ console.log('Video generated successfully, saving to materials...');
618
+ const uri = await saveMertial(currentSession, res.url, validatedFileName);
619
+ const { url, duration: videoDuration, ...opts } = res;
620
+ const result = {
621
+ success: true,
622
+ source: url,
623
+ uri,
624
+ durationMs: Math.floor((videoDuration || duration) * 1000),
625
+ prompt: prompt.substring(0, 100),
626
+ timestamp: new Date().toISOString(),
627
+ ...opts,
628
+ };
629
+ return {
630
+ content: [
631
+ {
632
+ type: 'text',
633
+ text: JSON.stringify(result),
634
+ },
635
+ ],
636
+ };
637
+ }
638
+ else {
639
+ console.warn('Video generation completed but no URL returned');
640
+ return {
641
+ content: [
642
+ {
643
+ type: 'text',
644
+ text: JSON.stringify({
645
+ success: false,
646
+ error: 'No video URL returned from AI service',
647
+ response: res,
648
+ timestamp: new Date().toISOString(),
649
+ }),
650
+ },
651
+ ],
652
+ };
653
+ }
654
+ }
655
+ catch (error) {
656
+ return createErrorResponse(error, 'generate-video');
366
657
  }
367
- return {
368
- content: [{ type: 'text', text: JSON.stringify(res) }],
369
- };
370
658
  });
371
659
  server.registerTool('generate-video-fallback', {
372
660
  title: 'Generate Ken Burns Motion as Video Fallback',
@@ -394,35 +682,94 @@ server.registerTool('generate-video-fallback', {
394
682
  saveToFileName: zod_1.z.string().describe('The filename to save.'),
395
683
  },
396
684
  }, async ({ frame_index, image_path, duration, size, saveToFileName }) => {
397
- const files = session.files;
398
- const terminal = session.terminal;
399
- const saveToPath = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials/${saveToFileName}`;
400
- const saveLocalPath = (0, node_path_1.resolve)(projectLocalDir, 'materials', saveToFileName);
401
- const [width, height] = size.split('x') ?? ['1280', '720'];
402
- const command = await (0, videokit_1.compileKenBurnsMotion)(frame_index, image_path, duration, {
403
- output: saveToPath,
404
- width: Number(width),
405
- height: Number(height),
406
- });
407
- const res = await terminal.run(command);
408
- const result = await res.json();
409
- if (result.exitCode !== 0) {
685
+ try {
686
+ // 验证session状态
687
+ const currentSession = validateSession('generate-video-fallback');
688
+ const validatedFileName = validateFileName(saveToFileName);
689
+ console.log(`Generating Ken Burns motion for frame ${frame_index}: ${image_path} (${duration}s, ${size})`);
690
+ const files = currentSession.files;
691
+ const terminal = currentSession.terminal;
692
+ if (!terminal) {
693
+ throw new Error('Terminal not available in current session');
694
+ }
695
+ const saveToPath = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials/${validatedFileName}`;
696
+ const saveLocalPath = (0, node_path_1.resolve)(projectLocalDir, 'materials', validatedFileName);
697
+ // 解析尺寸参数
698
+ const sizeArray = size.split('x');
699
+ if (sizeArray.length !== 2) {
700
+ throw new Error(`Invalid size format: ${size}. Expected format: WIDTHxHEIGHT`);
701
+ }
702
+ const [widthStr, heightStr] = sizeArray;
703
+ const width = Number(widthStr);
704
+ const height = Number(heightStr);
705
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
706
+ throw new Error(`Invalid dimensions: ${width}x${height}. Both width and height must be positive numbers`);
707
+ }
708
+ console.log(`Compiling Ken Burns motion command...`);
709
+ let command;
710
+ try {
711
+ command = await (0, videokit_1.compileKenBurnsMotion)(frame_index, image_path, duration, {
712
+ output: saveToPath,
713
+ width,
714
+ height,
715
+ });
716
+ }
717
+ catch (compileError) {
718
+ console.error('Failed to compile Ken Burns motion command:', compileError);
719
+ throw new Error(`Failed to compile Ken Burns motion: ${compileError}`);
720
+ }
721
+ console.log(`Executing FFmpeg command: ${command.substring(0, 100)}...`);
722
+ const res = await terminal.run(command);
723
+ const result = await res.json();
724
+ if (result.exitCode !== 0) {
725
+ console.error('FFmpeg command failed:', result);
726
+ return {
727
+ content: [
728
+ {
729
+ type: 'text',
730
+ text: JSON.stringify({
731
+ success: false,
732
+ error: 'Ken Burns motion generation failed',
733
+ exitCode: result.exitCode,
734
+ stderr: result.stderr,
735
+ command: command.substring(0, 200),
736
+ timestamp: new Date().toISOString(),
737
+ }),
738
+ },
739
+ ],
740
+ };
741
+ }
742
+ console.log('Ken Burns motion generated successfully, downloading file...');
743
+ try {
744
+ await files.download(saveToPath, saveLocalPath);
745
+ }
746
+ catch (downloadError) {
747
+ console.warn('Failed to download file to local:', downloadError);
748
+ // 继续执行,因为远程文件已生成成功
749
+ }
750
+ const resultData = {
751
+ success: true,
752
+ uri: saveToPath,
753
+ durationMs: Math.floor(duration * 1000),
754
+ frameIndex: frame_index,
755
+ imagePath: image_path,
756
+ size,
757
+ dimensions: { width, height },
758
+ timestamp: new Date().toISOString(),
759
+ };
760
+ console.log(`Ken Burns motion completed: ${saveToPath}`);
410
761
  return {
411
- content: [{ type: 'text', text: JSON.stringify(result) }],
762
+ content: [
763
+ {
764
+ type: 'text',
765
+ text: JSON.stringify(resultData),
766
+ },
767
+ ],
412
768
  };
413
769
  }
414
- await files.download(saveToPath, saveLocalPath);
415
- return {
416
- content: [
417
- {
418
- type: 'text',
419
- text: JSON.stringify({
420
- uri: saveToPath,
421
- durationMs: Math.floor(duration * 1000),
422
- }),
423
- },
424
- ],
425
- };
770
+ catch (error) {
771
+ return createErrorResponse(error, 'generate-video-fallback');
772
+ }
426
773
  });
427
774
  server.registerTool('generate-bgm', {
428
775
  title: 'Generate BGM',
@@ -437,35 +784,71 @@ server.registerTool('generate-bgm', {
437
784
  saveToFileName: zod_1.z.string().describe('The filename to save.'),
438
785
  },
439
786
  }, async ({ prompt, duration, saveToFileName }, context) => {
440
- const ai = session.ai;
441
- let progress = 0;
442
- const res = await ai.generateBGM({
443
- prompt,
444
- duration,
445
- onProgress: async (metaData) => {
446
- await sendProgress(context, metaData.Result?.Progress ?? ++progress, metaData.Result?.Progress ? 100 : undefined, JSON.stringify(metaData));
447
- },
448
- });
449
- if (res.url) {
450
- const uri = await saveMertial(session, res.url, saveToFileName);
451
- const { url, duration, ...opts } = res;
452
- return {
453
- content: [
454
- {
455
- type: 'text',
456
- text: JSON.stringify({
457
- source: url,
458
- uri,
459
- durationMs: Math.floor(duration * 1000),
460
- ...opts,
461
- }),
462
- },
463
- ],
464
- };
787
+ try {
788
+ // 验证session状态
789
+ const currentSession = validateSession('generate-bgm');
790
+ const validatedFileName = validateFileName(saveToFileName);
791
+ console.log(`Generating BGM with prompt: ${prompt.substring(0, 100)}... (${duration}s)`);
792
+ const ai = currentSession.ai;
793
+ let progress = 0;
794
+ const res = await ai.generateBGM({
795
+ prompt: prompt.trim(),
796
+ duration,
797
+ onProgress: async (metaData) => {
798
+ try {
799
+ await sendProgress(context, metaData.Result?.Progress ?? ++progress, metaData.Result?.Progress ? 100 : undefined, JSON.stringify(metaData));
800
+ }
801
+ catch (progressError) {
802
+ console.warn('Failed to send progress update:', progressError);
803
+ }
804
+ },
805
+ });
806
+ if (!res) {
807
+ throw new Error('Failed to generate BGM: no response from AI service');
808
+ }
809
+ if (res.url) {
810
+ console.log('BGM generated successfully, saving to materials...');
811
+ const uri = await saveMertial(currentSession, res.url, validatedFileName);
812
+ const { url, duration: bgmDuration, ...opts } = res;
813
+ const result = {
814
+ success: true,
815
+ source: url,
816
+ uri,
817
+ durationMs: Math.floor((bgmDuration || duration) * 1000),
818
+ prompt: prompt.substring(0, 100),
819
+ requestedDuration: duration,
820
+ timestamp: new Date().toISOString(),
821
+ ...opts,
822
+ };
823
+ return {
824
+ content: [
825
+ {
826
+ type: 'text',
827
+ text: JSON.stringify(result),
828
+ },
829
+ ],
830
+ };
831
+ }
832
+ else {
833
+ console.warn('BGM generation completed but no URL returned');
834
+ return {
835
+ content: [
836
+ {
837
+ type: 'text',
838
+ text: JSON.stringify({
839
+ success: false,
840
+ error: 'No BGM URL returned from AI service',
841
+ response: res,
842
+ timestamp: new Date().toISOString(),
843
+ }),
844
+ },
845
+ ],
846
+ };
847
+ }
848
+ }
849
+ catch (error) {
850
+ return createErrorResponse(error, 'generate-bgm');
465
851
  }
466
- return {
467
- content: [{ type: 'text', text: JSON.stringify(res) }],
468
- };
469
852
  });
470
853
  server.registerTool('generate-scene-tts', {
471
854
  title: 'Generate Scene TTS',
@@ -501,32 +884,65 @@ server.registerTool('generate-scene-tts', {
501
884
  .describe('适合作为视频配音的中英文音色(推荐15个)。来源于系统音色清单,偏中性、耐听,覆盖新闻/主持、企业宣传、科普解说、纪录片与生活方式等场景。'),
502
885
  },
503
886
  }, async ({ text, voiceName, saveToFileName, speed }) => {
504
- const ai = session.ai;
505
- const res = await ai.textToSpeech({
506
- text,
507
- voiceName,
508
- speed,
509
- });
510
- if (res.url) {
511
- const uri = await saveMertial(session, res.url, saveToFileName);
512
- const { url, duration, ...opts } = res;
513
- return {
514
- content: [
515
- {
516
- type: 'text',
517
- text: JSON.stringify({
518
- source: url,
519
- uri,
520
- durationMs: Math.floor(duration * 1000),
521
- ...opts,
522
- }),
523
- },
524
- ],
525
- };
887
+ try {
888
+ // 验证session状态
889
+ const currentSession = validateSession('generate-scene-tts');
890
+ const validatedFileName = validateFileName(saveToFileName);
891
+ const finalSpeed = speed ?? 1;
892
+ console.log(`Generating TTS with voice: ${voiceName}, speed: ${finalSpeed}, text: ${text.substring(0, 100)}...`);
893
+ const ai = currentSession.ai;
894
+ const res = await ai.textToSpeech({
895
+ text: text.trim(),
896
+ voiceName,
897
+ speed: finalSpeed,
898
+ });
899
+ if (!res) {
900
+ throw new Error('Failed to generate TTS: no response from AI service');
901
+ }
902
+ if (res.url) {
903
+ console.log('TTS generated successfully, saving to materials...');
904
+ const uri = await saveMertial(currentSession, res.url, validatedFileName);
905
+ const { url, duration, ...opts } = res;
906
+ const result = {
907
+ success: true,
908
+ source: url,
909
+ uri,
910
+ durationMs: Math.floor((duration || 0) * 1000),
911
+ text: text.substring(0, 100),
912
+ voiceName,
913
+ speed: finalSpeed,
914
+ timestamp: new Date().toISOString(),
915
+ ...opts,
916
+ };
917
+ return {
918
+ content: [
919
+ {
920
+ type: 'text',
921
+ text: JSON.stringify(result),
922
+ },
923
+ ],
924
+ };
925
+ }
926
+ else {
927
+ console.warn('TTS generation completed but no URL returned');
928
+ return {
929
+ content: [
930
+ {
931
+ type: 'text',
932
+ text: JSON.stringify({
933
+ success: false,
934
+ error: 'No TTS URL returned from AI service',
935
+ response: res,
936
+ timestamp: new Date().toISOString(),
937
+ }),
938
+ },
939
+ ],
940
+ };
941
+ }
942
+ }
943
+ catch (error) {
944
+ return createErrorResponse(error, 'generate-scene-tts');
526
945
  }
527
- return {
528
- content: [{ type: 'text', text: JSON.stringify(res) }],
529
- };
530
946
  });
531
947
  server.registerTool('compile-and-run', {
532
948
  title: 'Compile And Run',
@@ -540,71 +956,89 @@ server.registerTool('compile-and-run', {
540
956
  },
541
957
  }, async ({ project, outputFileName }) => {
542
958
  try {
543
- // Project is already validated by zVideoProject schema
544
- const validated = project;
545
- // Get working directory
546
- const terminal = session.terminal;
959
+ // 验证session状态
960
+ const currentSession = validateSession('compile-and-run');
961
+ console.log('Starting video compilation and rendering...');
962
+ // 验证terminal可用性
963
+ const terminal = currentSession.terminal;
964
+ if (!terminal) {
965
+ throw new Error('Terminal not available in current session');
966
+ }
967
+ // 验证输出文件名安全性
968
+ const outFile = outputFileName || 'output.mp4';
969
+ const validatedFileName = validateFileName(outFile);
970
+ console.log(`Output file: ${validatedFileName}`);
971
+ // 构建工作目录路径
547
972
  const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}`;
548
973
  const outputDir = `${workDir}/output`;
549
- // Set output file
550
- const outFile = outputFileName || 'output.mp4';
551
- const outputPath = `${outputDir}/${outFile}`;
552
- // Update export configuration
553
- validated.export.outFile = outputPath;
554
- // Compile to FFmpeg command
555
- const compiled = (0, videokit_1.compileToFfmpeg)(validated, {
556
- workingDir: outputDir,
557
- subtitleStrategy: 'auto',
558
- });
559
- // Log the FFmpeg command for debugging
560
- console.log('FFmpeg Command:', compiled.cmd);
561
- // Run FFmpeg
562
- const result = await runFfmpeg(session, compiled);
974
+ const outputPath = `${outputDir}/${validatedFileName}`;
975
+ // Project已经通过zVideoProject schema验证
976
+ const validated = { ...project };
977
+ // 更新导出配置
978
+ validated.export = {
979
+ ...validated.export,
980
+ outFile: outputPath,
981
+ };
982
+ console.log('Compiling VideoProject to FFmpeg command...');
983
+ // 编译为FFmpeg命令
984
+ let compiled;
985
+ try {
986
+ compiled = (0, videokit_1.compileToFfmpeg)(validated, {
987
+ workingDir: outputDir,
988
+ subtitleStrategy: 'auto',
989
+ });
990
+ }
991
+ catch (compileError) {
992
+ console.error('Failed to compile VideoProject:', compileError);
993
+ throw new Error(`Failed to compile VideoProject: ${compileError}`);
994
+ }
995
+ console.log(`FFmpeg command generated (${compiled.cmd.length} chars)`);
996
+ console.log('FFmpeg Command:', compiled.cmd.substring(0, 200) + '...');
997
+ // 执行FFmpeg命令
998
+ console.log('Executing FFmpeg command...');
999
+ const result = await runFfmpeg(currentSession, compiled);
563
1000
  if (result.exitCode === 0) {
1001
+ console.log('Video compilation completed successfully');
1002
+ const successResult = {
1003
+ success: true,
1004
+ outputPath,
1005
+ outputFileName: validatedFileName,
1006
+ command: compiled.cmd.substring(0, 500), // 限制命令长度
1007
+ message: 'Video compilation completed successfully',
1008
+ timestamp: new Date().toISOString(),
1009
+ };
564
1010
  return {
565
1011
  content: [
566
1012
  {
567
1013
  type: 'text',
568
- text: JSON.stringify({
569
- success: true,
570
- outputPath,
571
- command: compiled.cmd,
572
- message: 'Video compilation completed successfully',
573
- }),
1014
+ text: JSON.stringify(successResult),
574
1015
  },
575
1016
  ],
576
1017
  };
577
1018
  }
578
1019
  else {
1020
+ console.error(`FFmpeg failed with exit code: ${result.exitCode}`);
1021
+ const failureResult = {
1022
+ success: false,
1023
+ exitCode: result.exitCode,
1024
+ outputPath,
1025
+ command: compiled.cmd.substring(0, 500),
1026
+ stderr: result.stderr?.substring(0, 1000), // 限制错误输出长度
1027
+ message: `FFmpeg exited with code ${result.exitCode}`,
1028
+ timestamp: new Date().toISOString(),
1029
+ };
579
1030
  return {
580
1031
  content: [
581
1032
  {
582
1033
  type: 'text',
583
- text: JSON.stringify({
584
- success: false,
585
- exitCode: result.exitCode,
586
- command: compiled.cmd,
587
- message: `FFmpeg exited with code ${result.exitCode}`,
588
- }),
1034
+ text: JSON.stringify(failureResult),
589
1035
  },
590
1036
  ],
591
1037
  };
592
1038
  }
593
1039
  }
594
1040
  catch (error) {
595
- const errorMessage = error instanceof Error ? error.message : String(error);
596
- return {
597
- content: [
598
- {
599
- type: 'text',
600
- text: JSON.stringify({
601
- success: false,
602
- error: errorMessage,
603
- message: 'Failed to compile and run project',
604
- }),
605
- },
606
- ],
607
- };
1041
+ return createErrorResponse(error, 'compile-and-run');
608
1042
  }
609
1043
  });
610
1044
  server.registerTool('get-video-project-schema', {
@@ -620,22 +1054,43 @@ server.registerTool('get-video-project-schema', {
620
1054
  content: [
621
1055
  {
622
1056
  type: 'text',
623
- text: `VideoProject JSON Schema:\n\n${JSON.stringify(schema, null, 2)}`,
1057
+ text: JSON.stringify({
1058
+ success: true,
1059
+ schema,
1060
+ timestamp: new Date().toISOString(),
1061
+ }),
624
1062
  },
625
1063
  ],
626
1064
  };
627
1065
  }
628
1066
  catch (error) {
1067
+ return createErrorResponse(error, 'get-video-project-schema');
1068
+ }
1069
+ });
1070
+ // 注册 ZeroCut 指导规范 Prompt
1071
+ server.registerPrompt('zerocut-guideline', {
1072
+ title: 'ZeroCut 短视频创作指导规范',
1073
+ description: '专业的短视频创作 Agent 指导规范,包含完整的工作流程、工具说明和质量建议',
1074
+ }, async () => {
1075
+ try {
1076
+ const promptPath = (0, node_path_1.resolve)(__dirname, './prompts/zerocut-guideline.md');
1077
+ const promptContent = await (0, promises_1.readFile)(promptPath, 'utf-8');
629
1078
  return {
630
- content: [
1079
+ messages: [
631
1080
  {
632
- type: 'text',
633
- text: `Error reading VideoProject schema: ${error}`,
1081
+ role: 'user',
1082
+ content: {
1083
+ type: 'text',
1084
+ text: promptContent,
1085
+ },
634
1086
  },
635
1087
  ],
636
- isError: true,
637
1088
  };
638
1089
  }
1090
+ catch (error) {
1091
+ console.error('Failed to load zerocut-guideline prompt:', error);
1092
+ throw new Error(`Failed to load zerocut-guideline prompt: ${error}`);
1093
+ }
639
1094
  });
640
1095
  async function run() {
641
1096
  // Start receiving messages on stdin and sending messages on stdout