cerevox 0.3.2 → 0.3.4

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