cerevox 4.0.0-alpha.2 → 4.0.0-alpha.20

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.
@@ -54,6 +54,7 @@ const node_fs_1 = require("node:fs");
54
54
  const mp3_duration_1 = __importDefault(require("mp3-duration"));
55
55
  const image_size_1 = __importDefault(require("image-size"));
56
56
  const seed_1 = require("../../utils/seed");
57
+ const uuid_1 = require("uuid");
57
58
  function createErrorResponse(error, operation, details) {
58
59
  const errorMessage = error instanceof Error ? error.message : String(error);
59
60
  console.error(`[${operation}] Error:`, error);
@@ -140,6 +141,8 @@ async function saveMaterial(session, url, saveToFileName) {
140
141
  const terminal = session.terminal;
141
142
  const saveToPath = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials/${saveToFileName}`;
142
143
  const saveLocalPath = (0, node_path_1.resolve)(projectLocalDir, 'materials', saveToFileName);
144
+ // 确保目录存在
145
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(saveLocalPath), { recursive: true });
143
146
  // 先下载到本地,再上传 sandbox,比直接 sandbox 更好,也以免下载超时
144
147
  // 通过 fetch 下载到本地
145
148
  const res = await fetch(url);
@@ -261,7 +264,7 @@ async function filterMaterialsForUpload(materials, projectLocalDir) {
261
264
  regularFiles.push(material);
262
265
  }
263
266
  }
264
- // 处理场景文件(scXX_ 前缀)
267
+ // 处理分镜文件(scXX_ 前缀)
265
268
  if (sceneFiles.length > 0 && mediaLogs.length > 0) {
266
269
  // 按前缀和扩展名分组
267
270
  const groupedFiles = new Map();
@@ -309,7 +312,7 @@ async function filterMaterialsForUpload(materials, projectLocalDir) {
309
312
  }
310
313
  }
311
314
  else {
312
- // 如果没有 media_logs.json 或没有场景文件,全部上传
315
+ // 如果没有 media_logs.json 或没有分镜文件,全部上传
313
316
  filesToUpload.push(...sceneFiles);
314
317
  }
315
318
  // 处理普通文件(全量上传)
@@ -330,11 +333,11 @@ const cerevox = new index_1.default({
330
333
  logLevel: 'error',
331
334
  });
332
335
  let session = null;
333
- let projectLocalDir = process.env.ZEROCUT_PROJECT_CWD || process.cwd() || '.';
334
- let checkStoryboardFlag = false;
335
- let checkAudioVideoDurationFlag = false;
336
- // let checkStoryboardSubtitlesFlag = false;
336
+ let projectLocalDir = process.env.ZEROCUT_PROJECT_CWD ||
337
+ `${(0, node_path_1.resolve)(process.env.ZEROCUT_WORKSPACE_DIR, (0, uuid_1.v4)())}` ||
338
+ '.';
337
339
  let closeSessionTimerId = null;
340
+ const CLIENT_TYPE = process.env.CLIENT_TYPE || 'trae';
338
341
  // 注册 ZeroCut 指导规范 Prompt
339
342
  server.registerPrompt('zerocut-guideline', {
340
343
  title: 'ZeroCut 短视频创作指导规范',
@@ -356,19 +359,17 @@ server.registerPrompt('zerocut-guideline', {
356
359
  };
357
360
  }
358
361
  catch (error) {
359
- console.error('Failed to load zerocut-guideline prompt:', error);
360
- throw new Error(`Failed to load zerocut-guideline prompt: ${error}`);
362
+ console.error('Failed to load zerocut-guideline-web prompt:', error);
363
+ throw new Error(`Failed to load zerocut-guideline-web prompt: ${error}`);
361
364
  }
362
365
  });
363
366
  server.registerTool('project-open', {
364
367
  title: 'Open Project',
365
368
  description: 'Launch a new Cerevox session with a Chromium browser instance and open a new project context. Supports smart file filtering to optimize upload performance.',
366
369
  inputSchema: {
367
- localDir: zod_1.z
370
+ projectName: zod_1.z
368
371
  .string()
369
- .optional()
370
- .default('.')
371
- .describe('The path of the file to upload.'),
372
+ .describe('项目名,命名规则如同命名文件,可以用空格,但不得使用特殊字符(如/、:等),如果你知晓当前目录,必须用当前目录的最后一级目录名作为项目名,否则根据用户需求自定义项目名'),
372
373
  tosFiles: zod_1.z
373
374
  .array(zod_1.z.string())
374
375
  .optional()
@@ -380,7 +381,7 @@ server.registerTool('project-open', {
380
381
  .default(false)
381
382
  .describe('Whether to upload all files without filtering. If true, skips the smart filtering logic.'),
382
383
  },
383
- }, async ({ localDir, uploadAllFiles, tosFiles }, context) => {
384
+ }, async ({ projectName, uploadAllFiles, tosFiles }, context) => {
384
385
  try {
385
386
  if (closeSessionTimerId) {
386
387
  clearTimeout(closeSessionTimerId);
@@ -388,12 +389,9 @@ server.registerTool('project-open', {
388
389
  }
389
390
  // 检查是否已有活跃session
390
391
  if (session) {
391
- console.warn('Session already exists, closing previous session');
392
- // try {
393
- // await session.close();
394
- // } catch (closeError) {
395
- // console.warn('Failed to close previous session:', closeError);
396
- // }
392
+ if (session.projectName !== projectName) {
393
+ return createErrorResponse('project-open', `Another project is already open: ${session.projectName},Ask user to close it first.`);
394
+ }
397
395
  const result = {
398
396
  success: true,
399
397
  sessionId: session.id,
@@ -424,10 +422,20 @@ server.registerTool('project-open', {
424
422
  if (!session) {
425
423
  throw new Error('Failed to create Cerevox session');
426
424
  }
425
+ // 保存项目名到session
426
+ session.projectName = projectName;
427
427
  console.log('Initializing project...');
428
428
  const workDir = await initProject(session);
429
- projectLocalDir = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), localDir || '.');
429
+ if (!process.env.ZEROCUT_PROJECT_CWD &&
430
+ !process.env.ZEROCUT_WORKSPACE_DIR) {
431
+ throw new Error('ZEROCUT_WORKSPACE_DIR environment variable is required');
432
+ }
433
+ projectLocalDir = process.env.ZEROCUT_PROJECT_CWD
434
+ ? (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD, '.')
435
+ : (0, node_path_1.resolve)(process.env.ZEROCUT_WORKSPACE_DIR, projectName);
430
436
  const syncDir = (0, node_path_1.resolve)(projectLocalDir, 'materials');
437
+ // 保证项目目录存在
438
+ await (0, promises_1.mkdir)(projectLocalDir, { recursive: true });
431
439
  try {
432
440
  await (0, promises_1.mkdir)(syncDir, { recursive: true });
433
441
  }
@@ -439,25 +447,23 @@ server.registerTool('project-open', {
439
447
  // 文件过滤逻辑
440
448
  let filesToUpload = [];
441
449
  let skippedFiles = [];
442
- if (localDir) {
443
- try {
444
- materials = await listFiles(syncDir);
445
- }
446
- catch (listError) {
447
- console.warn('Failed to list materials:', listError);
448
- materials = [];
449
- }
450
- if (uploadAllFiles) {
451
- // 如果 uploadAllFiles 为 true,跳过智能过滤,上传所有文件
452
- filesToUpload = materials;
453
- skippedFiles = [];
454
- }
455
- else {
456
- // 智能文件过滤逻辑
457
- const filterResult = await filterMaterialsForUpload(materials, projectLocalDir);
458
- filesToUpload = filterResult.filesToUpload;
459
- skippedFiles = filterResult.skippedFiles;
460
- }
450
+ try {
451
+ materials = await listFiles(syncDir);
452
+ }
453
+ catch (listError) {
454
+ console.warn('Failed to list materials:', listError);
455
+ materials = [];
456
+ }
457
+ if (uploadAllFiles) {
458
+ // 如果 uploadAllFiles 为 true,跳过智能过滤,上传所有文件
459
+ filesToUpload = materials;
460
+ skippedFiles = [];
461
+ }
462
+ else {
463
+ // 智能文件过滤逻辑
464
+ const filterResult = await filterMaterialsForUpload(materials, projectLocalDir);
465
+ filesToUpload = filterResult.filesToUpload;
466
+ skippedFiles = filterResult.skippedFiles;
461
467
  }
462
468
  const files = session.files;
463
469
  let progress = 0;
@@ -491,6 +497,7 @@ server.registerTool('project-open', {
491
497
  nextActionSuggest: '检查规则上下文是否已召回,若未召回,调用 retrieve_rules 工具召回规则上下文',
492
498
  sessionId: session.id,
493
499
  workDir,
500
+ projectName,
494
501
  projectLocalDir,
495
502
  materials,
496
503
  uploadedFiles: filesToUpload.map(file => (0, node_path_1.basename)(file)),
@@ -521,7 +528,7 @@ server.registerTool('project-close', {
521
528
  .min(0)
522
529
  .max(20)
523
530
  .default(5)
524
- .describe('Close the session after the specified number of minutes. Default is 5 minutes. 除非用户要求立即关闭会话,将该参数设置为0,否则应默认设为5'),
531
+ .describe('Close the session after the specified number of minutes. Default is 5 minutes. 当用户主动要求关闭会话时,将该参数设置为0,否则应默认设为5'),
525
532
  },
526
533
  }, async ({ inMinutes }) => {
527
534
  try {
@@ -578,9 +585,9 @@ server.registerTool('retrieve-rules-context', {
578
585
  }
579
586
  else {
580
587
  // 当 projectRulesFile 不存在时,设置 checkStoryboardFlag 为 false
581
- checkStoryboardFlag = false;
588
+ // checkStoryboardFlag = false;
582
589
  // 当 projectRulesFile 不存在时,设置 checkAudioVideoDurationFlag 为 false
583
- checkAudioVideoDurationFlag = false;
590
+ // checkAudioVideoDurationFlag = false;
584
591
  }
585
592
  try {
586
593
  const ai = currentSession.ai;
@@ -636,8 +643,8 @@ server.registerTool('upload-custom-material', {
636
643
  try {
637
644
  // 验证session状态
638
645
  const currentSession = await validateSession('upload-custom-material');
639
- // 构建本地文件路径,使用 ZEROCUT_PROJECT_CWD 环境变量
640
- const validatedPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'materials', localFileName.trim());
646
+ // 构建本地文件路径,使用 projectLocalDir
647
+ const validatedPath = (0, node_path_1.resolve)(projectLocalDir, 'materials', localFileName.trim());
641
648
  // 验证本地文件存在性
642
649
  if (!(0, node_fs_1.existsSync)(validatedPath)) {
643
650
  throw new Error(`File not found: ${validatedPath}`);
@@ -711,7 +718,7 @@ server.registerTool('upload-custom-material', {
711
718
  }
712
719
  });
713
720
  server.registerTool('wait-for-task-finish', {
714
- title: 'Wait Workflow or VideoTask Done;只有正在运行Coze工作流或者有异步生成视频任务时才需要执行这个工具;⚠️ 如果执行这个工具未失败只是超时,你应立即再次重新调用,以继续等待直到任务完成或失败',
721
+ title: 'Wait Workflow or VideoTask Done;只有正在运行Coze工作流或者有异步生成视频任务时才需要执行这个工具;⚠️ 如果执行这个工具未失败只是超时,你应立即再次重新调用,以继续等待直到任务完成或失败;‼️ 有的任务执行时间需要很长,所以如果只是超时不是失败,重试多少次都是正常的,请耐心等待即可。',
715
722
  description: 'Wait for a workflow to complete.',
716
723
  inputSchema: {
717
724
  taskUrl: zod_1.z
@@ -767,6 +774,32 @@ server.registerTool('wait-for-task-finish', {
767
774
  catch (error) {
768
775
  console.warn(`Failed to update media_logs.json for ${validatedFileName}:`, error);
769
776
  }
777
+ if (res.data?.scenes) {
778
+ const { scenes, video_type, voice_type, voiceover_tone, bgm_prompt, video_model, aspect_ratio, } = res.data;
779
+ const seed = (0, seed_1.getRandomSeed)();
780
+ const orientation = aspect_ratio === '16:9' ? 'landscape' : 'portrait';
781
+ const storyboard = {
782
+ orientation,
783
+ video_type,
784
+ outline_sheet: 'outline_sheet.png',
785
+ bgm_prompt,
786
+ voice_type,
787
+ scenes: scenes.map((scene) => {
788
+ let video_prompt = scene.video_prompt;
789
+ if (voiceover_tone && video_prompt.includes('画外音')) {
790
+ video_prompt = video_prompt.replace(/画外音[::]/g, `画外音(${voiceover_tone}):`);
791
+ }
792
+ return {
793
+ ...scene,
794
+ video_prompt,
795
+ use_video_model: video_model,
796
+ seed,
797
+ };
798
+ }),
799
+ };
800
+ const saveLocalPath = (0, node_path_1.resolve)(projectLocalDir, 'storyboard.json');
801
+ await (0, promises_1.writeFile)(saveLocalPath, JSON.stringify(storyboard, null, 2));
802
+ }
770
803
  return {
771
804
  content: [
772
805
  {
@@ -932,7 +965,7 @@ ${roleDescriptionPrompt}
932
965
  ? referenceImage
933
966
  : `./materials/${referenceImage}`;
934
967
  // 需要得到当前项目的绝对路径
935
- const imageFilePath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, imagePath);
968
+ const imageFilePath = (0, node_path_1.resolve)(projectLocalDir, imagePath);
936
969
  // 读取图片文件内容
937
970
  const imageBuffer = await (0, promises_1.readFile)(imageFilePath);
938
971
  const fileName = (0, node_path_1.basename)(imagePath);
@@ -1020,26 +1053,12 @@ server.registerTool('generate-image', {
1020
1053
  .default('seedream-pro'),
1021
1054
  prompt: zod_1.z
1022
1055
  .string()
1023
- .describe('The prompt to generate. 一般要严格对应 storyboard 中当前场景的 start_frame 或 end_frame 中的字段描述,如果是生成线稿,则 type 使用 line-sketch,如果是生成故事板印样或人物镜头宫格,则 type 使用 shot-grid'),
1056
+ .describe('The prompt to generate. 一般要严格对应 storyboard 中当前分镜的 start_frame 或 end_frame 中的字段描述,如果是生成线稿,则 type 使用 line-sketch,如果是生成故事板印样或人物镜头宫格,则 type 使用 shot-grid'),
1024
1057
  sceneIndex: zod_1.z
1025
1058
  .number()
1026
1059
  .min(1)
1027
1060
  .optional()
1028
- .describe('场景索引,从1开始的下标,如果非场景对应素材,则可不传,场景素材必传'),
1029
- storyBoardFile: zod_1.z
1030
- .string()
1031
- .optional()
1032
- .default('storyboard.json')
1033
- .describe('故事板文件路径'),
1034
- skipConsistencyCheck: zod_1.z
1035
- .boolean()
1036
- .optional()
1037
- .default(false)
1038
- .describe('是否跳过一致性检查,默认为false(即默认进行一致性检查)'),
1039
- skipCheckWithSceneReason: zod_1.z
1040
- .string()
1041
- .optional()
1042
- .describe('跳过校验的理由,如果skipConsistencyCheck设为true,必须要传这个参数'),
1061
+ .describe('分镜索引,从1开始的下标,如果非分镜对应素材,则可不传,分镜素材必传'),
1043
1062
  size: zod_1.z
1044
1063
  .enum([
1045
1064
  '1024x1024',
@@ -1124,22 +1143,16 @@ server.registerTool('generate-image', {
1124
1143
  \`\`\`
1125
1144
  `),
1126
1145
  },
1127
- }, async ({ type = 'seedream', prompt, sceneIndex, storyBoardFile = 'storyboard.json', skipConsistencyCheck = false, size = '720x1280', imageCount = 1, saveToFileNames, watermark, referenceImages, }, context) => {
1146
+ }, async ({ type = 'seedream', prompt, sceneIndex, size = '720x1280', imageCount = 1, saveToFileNames, watermark, referenceImages, }, context) => {
1128
1147
  try {
1148
+ const storyBoardFile = 'storyboard.json';
1129
1149
  // 验证session状态
1130
1150
  const currentSession = await validateSession('generate-image');
1131
- const storyBoardPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, storyBoardFile);
1132
- const outlineSheetImagePath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'materials', 'outline_sheet.png');
1151
+ const storyBoardPath = (0, node_path_1.resolve)(projectLocalDir, storyBoardFile);
1152
+ const outlineSheetImagePath = (0, node_path_1.resolve)(projectLocalDir, 'materials', 'outline_sheet.png');
1133
1153
  const hasOutlineSheet = (0, node_fs_1.existsSync)(outlineSheetImagePath);
1134
- // 检查 storyboard 标志
1135
- if (!hasOutlineSheet &&
1136
- !checkStoryboardFlag &&
1137
- (0, node_fs_1.existsSync)(storyBoardPath)) {
1138
- checkStoryboardFlag = true;
1139
- return createErrorResponse('必须先审查生成的 storyboard.json 内容,确保每个场景中的stage_atmosphere内容按照规则被正确融合到start_frame和video_prompt中,不得遗漏,检查完成后先汇报,如果有问题,应当先修改 storyboard.json 内容,然后再调用 generate-image 生成图片。注意修改 storyboard 内容时,仅修改相应字段的字符串值,不要破坏JSON格式!', 'generate-image');
1140
- }
1141
- // 校验 prompt 与 storyboard.json 中场景设定的一致性
1142
- if (sceneIndex && !skipConsistencyCheck) {
1154
+ // 校验 prompt 与 storyboard.json 中分镜设定的一致性
1155
+ if (sceneIndex) {
1143
1156
  try {
1144
1157
  if ((0, node_fs_1.existsSync)(storyBoardPath)) {
1145
1158
  const storyBoardContent = await (0, promises_1.readFile)(storyBoardPath, 'utf8');
@@ -1158,7 +1171,7 @@ server.registerTool('generate-image', {
1158
1171
  const endFrame = scene.end_frame;
1159
1172
  // 检查 prompt 是否严格等于 start_frame 或 end_frame
1160
1173
  if (prompt !== startFrame && prompt !== endFrame) {
1161
- return createErrorResponse('图片提示词必须严格遵照storyboard的设定,如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-image');
1174
+ return createErrorResponse('图片提示词必须严格遵照storyboard的设定', 'generate-image');
1162
1175
  }
1163
1176
  if (hasOutlineSheet &&
1164
1177
  (!referenceImages ||
@@ -1226,73 +1239,6 @@ server.registerTool('generate-image', {
1226
1239
  // 检查并替换英文单引号包裹的中文内容为中文双引号
1227
1240
  // 这样才能让 seedream 生成更好的中文文字
1228
1241
  let processedPrompt = prompt.replace(/'([^']*[\u4e00-\u9fff][^']*)'/g, '“$1”');
1229
- try {
1230
- const ai = currentSession.ai;
1231
- const promptOptimizer = await (0, promises_1.readFile)((0, node_path_1.resolve)(__dirname, './prompts/image-prompt-optimizer.md'), 'utf8');
1232
- if (!hasOutlineSheet) {
1233
- const schema = {
1234
- name: 'optimize_image_prompt',
1235
- schema: {
1236
- type: 'object',
1237
- properties: {
1238
- prompt_optimized: {
1239
- type: 'string',
1240
- description: '优化后的提示词',
1241
- },
1242
- metaphor_modifiers: {
1243
- type: 'array',
1244
- description: '从 prompt_optimized 中抽取的所有比喻修饰词(字符串数组)',
1245
- items: {
1246
- type: 'string',
1247
- description: '比喻性修饰词,例如 “如羽毛般轻盈”、“像晨雾一样柔和”',
1248
- },
1249
- },
1250
- },
1251
- required: ['prompt_optimized', 'metaphor_modifiers'],
1252
- },
1253
- };
1254
- const completion = await ai.getCompletions({
1255
- model: 'Doubao-Seed-1.6',
1256
- messages: [
1257
- {
1258
- role: 'system',
1259
- content: promptOptimizer,
1260
- },
1261
- {
1262
- role: 'user',
1263
- content: `## 用户指令
1264
-
1265
- ${processedPrompt.trim()}
1266
-
1267
- ## 参考图
1268
-
1269
- ${referenceImages?.map((ref, index) => `图${index + 1}:${ref.image}`).join('\n') || '无'}`,
1270
- },
1271
- ],
1272
- response_format: {
1273
- type: 'json_schema',
1274
- json_schema: schema,
1275
- },
1276
- });
1277
- const optimizedPrompt = completion.choices[0]?.message?.content.trim();
1278
- if (optimizedPrompt) {
1279
- try {
1280
- const { prompt_optimized, metaphor_modifiers } = JSON.parse(optimizedPrompt);
1281
- processedPrompt = `${prompt_optimized}`;
1282
- if (metaphor_modifiers?.length) {
1283
- processedPrompt += `\n\n注意:下面这些是形象比喻,并不是输出内容。\n${metaphor_modifiers}`;
1284
- }
1285
- }
1286
- catch (ex) {
1287
- console.error('Failed to parse optimized prompt:', ex);
1288
- processedPrompt = optimizedPrompt;
1289
- }
1290
- }
1291
- }
1292
- }
1293
- catch (error) {
1294
- console.error('Failed to optimize prompt:', error);
1295
- }
1296
1242
  if (imageCount > 1) {
1297
1243
  processedPrompt = `请生成${imageCount}张${size}大小的关联图片,每张图分别生成,不要拼接为大图 ${processedPrompt}`;
1298
1244
  }
@@ -1309,7 +1255,7 @@ server.registerTool('generate-image', {
1309
1255
  ? refImage.image
1310
1256
  : `./materials/${refImage.image}`;
1311
1257
  // 需要得到当前项目的绝对路径
1312
- const imageFilePath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, imagePath);
1258
+ const imageFilePath = (0, node_path_1.resolve)(projectLocalDir, imagePath);
1313
1259
  try {
1314
1260
  // 直接读取本地文件
1315
1261
  if (!(0, node_fs_1.existsSync)(imageFilePath)) {
@@ -1365,12 +1311,12 @@ server.registerTool('generate-image', {
1365
1311
  if (objectPrefix.length > 0) {
1366
1312
  processedPrompt = `${objectPrefix.join('\n')}
1367
1313
 
1368
- ${turnaroundMessage}参考以上图片,执行以下场景绘图:
1314
+ ${turnaroundMessage}参考以上图片,执行以下绘图指令:
1369
1315
  ${processedPrompt}`.trim();
1370
1316
  }
1371
1317
  }
1372
1318
  const ai = currentSession.ai;
1373
- const { taskUrl } = await ai.generateImage({
1319
+ const taskRes = await ai.generateImage({
1374
1320
  type,
1375
1321
  prompt: processedPrompt,
1376
1322
  size,
@@ -1378,6 +1324,10 @@ ${processedPrompt}`.trim();
1378
1324
  image: imageBase64Array,
1379
1325
  async: true,
1380
1326
  });
1327
+ const taskUrl = taskRes.taskUrl;
1328
+ if (!taskUrl) {
1329
+ return createErrorResponse(`Failed to generate image: ${taskRes.error || JSON.stringify(taskRes)}`, 'generate-image');
1330
+ }
1381
1331
  let progress = 0;
1382
1332
  const res = await ai.waitForTaskComplete({
1383
1333
  taskUrl,
@@ -1402,7 +1352,7 @@ ${processedPrompt}`.trim();
1402
1352
  ];
1403
1353
  }
1404
1354
  else {
1405
- // 多图场景
1355
+ // 多图
1406
1356
  uris = await Promise.all(res.urls.map((url, i) => {
1407
1357
  if (!url)
1408
1358
  return '';
@@ -1518,7 +1468,7 @@ server.registerTool('edit-image', {
1518
1468
  ? sourceImageFileName
1519
1469
  : `./materials/${sourceImageFileName}`;
1520
1470
  // 需要得到当前项目的绝对路径
1521
- const imageFilePath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, imagePath);
1471
+ const imageFilePath = (0, node_path_1.resolve)(projectLocalDir, imagePath);
1522
1472
  if (!(0, node_fs_1.existsSync)(imageFilePath)) {
1523
1473
  return createErrorResponse(`Reference image not found: ${imageFilePath}`, 'edit-image');
1524
1474
  }
@@ -1586,9 +1536,9 @@ server.registerTool('edit-image', {
1586
1536
  return createErrorResponse(error, 'edit-image');
1587
1537
  }
1588
1538
  });
1589
- server.registerTool('generate-short-video-outlines', {
1590
- title: 'Generate Short Video Outlines',
1591
- description: `根据用户描述生成短视频的大纲;执行本工具会自动创建两个文件,一个是storyboard.json,一个是outline_sheet.png,前者是分镜设置,后者是分镜宫格图`,
1539
+ server.registerTool('generate-video-outlines', {
1540
+ title: 'Generate Video Outlines',
1541
+ description: `根据用户描述生成短视频的大纲;执行本工具会自动创建两个文件,一个是storyboard.json,一个是outline_sheet.png,前者是分镜设置,后者是分镜宫格图,其中outline_sheet.png生成于materials目录下,storyboard.json生成于materials的上级目录。`,
1592
1542
  inputSchema: {
1593
1543
  prompt: zod_1.z
1594
1544
  .string()
@@ -1624,10 +1574,11 @@ server.registerTool('generate-short-video-outlines', {
1624
1574
  .default('pro')
1625
1575
  .describe('除非用户主动提出使用其他模型,否则一律用pro模型'),
1626
1576
  },
1627
- }, async ({ prompt, voiceType, language, images, orientation, model }) => {
1577
+ }, async ({ prompt, voiceType, language, images, orientation, model }, context) => {
1628
1578
  try {
1629
1579
  // 验证session状态
1630
- const currentSession = await validateSession('generate-short-video-outlines');
1580
+ const currentSession = await validateSession('generate-video-outlines');
1581
+ let progress = 0;
1631
1582
  const ai = currentSession.ai;
1632
1583
  const res = await ai.generateShortVideoOutlines({
1633
1584
  prompt,
@@ -1636,6 +1587,15 @@ server.registerTool('generate-short-video-outlines', {
1636
1587
  images,
1637
1588
  videoModel: model,
1638
1589
  aspectRatio: orientation === 'portrait' ? '9:16' : '16:9',
1590
+ onProgress: async (metaData) => {
1591
+ try {
1592
+ await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
1593
+ }
1594
+ catch (progressError) {
1595
+ console.warn('Failed to send progress update:', progressError);
1596
+ }
1597
+ },
1598
+ waitForFinish: CLIENT_TYPE !== '5ire',
1639
1599
  });
1640
1600
  if (!res) {
1641
1601
  throw new Error('Failed to generate short video outlines: no response from AI service');
@@ -1647,7 +1607,7 @@ server.registerTool('generate-short-video-outlines', {
1647
1607
  type: 'text',
1648
1608
  text: JSON.stringify({
1649
1609
  success: true,
1650
- message: '该任务正在运行中,它是异步任务,且执行时间较长,你应立即调用工具 wait-for-task-finish 来等待任务结束,如 wait-for-task-finish 工具调用超时,你应立即再次重新调用直到任务结束。',
1610
+ message: '该任务正在运行中,它是异步任务,且执行时间较长,你应立即调用工具 wait-for-task-finish (saveFileName=outline_sheet.png) 来等待任务结束,如 wait-for-task-finish 工具调用超时,你应立即再次重新调用直到任务结束。',
1651
1611
  taskUrl: res.taskUrl,
1652
1612
  }),
1653
1613
  },
@@ -1657,7 +1617,7 @@ server.registerTool('generate-short-video-outlines', {
1657
1617
  else if (res.url) {
1658
1618
  const url = res.url;
1659
1619
  await saveMaterial(currentSession, url, 'outline_sheet.png');
1660
- const { scenes, video_type, voice_type, voiceover_tone, bgm_prompt } = res.data || {};
1620
+ const { scenes, video_type, voice_type, voiceover_tone, bgm_prompt, video_model, } = res.data || {};
1661
1621
  const seed = (0, seed_1.getRandomSeed)();
1662
1622
  const storyboard = {
1663
1623
  orientation,
@@ -1667,19 +1627,13 @@ server.registerTool('generate-short-video-outlines', {
1667
1627
  voice_type,
1668
1628
  scenes: scenes.map((scene) => {
1669
1629
  let video_prompt = scene.video_prompt;
1670
- if (video_prompt.includes('画外音') ||
1671
- video_prompt.toLowerCase().includes('voiceover')) {
1672
- if (voiceover_tone) {
1673
- video_prompt = video_prompt.replace(/画外音[::]\s*“([^”]*)”/g, `画外音(${voiceover_tone})镜头内所有角色都不言语,从远处传来广播声<广播开始>$1——</广播结束>`);
1674
- }
1675
- else {
1676
- video_prompt = video_prompt.replace(/画外音[::]\s*“([^”]*)”/g, `镜头内所有角色都不言语,从远处传来广播声<广播开始>$1——</广播结束>`);
1677
- }
1630
+ if (voiceover_tone && video_prompt.includes('画外音')) {
1631
+ video_prompt = video_prompt.replace(/画外音[::]/g, `画外音(${voiceover_tone}):`);
1678
1632
  }
1679
1633
  return {
1680
1634
  ...scene,
1681
1635
  video_prompt,
1682
- use_video_model: model,
1636
+ use_video_model: video_model,
1683
1637
  seed,
1684
1638
  };
1685
1639
  }),
@@ -1714,7 +1668,7 @@ server.registerTool('generate-short-video-outlines', {
1714
1668
  };
1715
1669
  }
1716
1670
  catch (error) {
1717
- return createErrorResponse(error, 'generate-short-video-outlines');
1671
+ return createErrorResponse(error, 'generate-video-outlines');
1718
1672
  }
1719
1673
  });
1720
1674
  server.registerTool('generate-music-or-mv', {
@@ -2026,722 +1980,17 @@ server.registerTool('text-to-speech', {
2026
1980
  return createErrorResponse(error, 'text-to-speech');
2027
1981
  }
2028
1982
  });
2029
- // server.registerTool(
2030
- // 'generate-scene-tts',
2031
- // {
2032
- // title: 'Generate Scene TTS',
2033
- // description: `生成场景配音`,
2034
- // inputSchema: {
2035
- // text: z.string().describe('The text to generate.'),
2036
- // sceneIndex: z
2037
- // .number()
2038
- // .min(1)
2039
- // .optional()
2040
- // .describe(
2041
- // '场景索引,从1开始的下标,如果非场景对应素材,则可不传,场景素材必传'
2042
- // ),
2043
- // storyBoardFile: z
2044
- // .string()
2045
- // .optional()
2046
- // .default('storyboard.json')
2047
- // .describe('故事板文件路径'),
2048
- // skipConsistencyCheck: z
2049
- // .boolean()
2050
- // .optional()
2051
- // .default(false)
2052
- // .describe('是否跳过一致性检查,默认为false(即默认进行一致性检查)'),
2053
- // skipCheckWithSceneReason: z
2054
- // .string()
2055
- // .optional()
2056
- // .describe(
2057
- // '跳过校验的理由,如果skipConsistencyCheck设为true,必须要传这个参数'
2058
- // ),
2059
- // saveToFileName: z
2060
- // .string()
2061
- // .describe('The filename to save. 应该是mp3文件'),
2062
- // speed: z
2063
- // .number()
2064
- // .min(0.5)
2065
- // .max(2)
2066
- // .optional()
2067
- // .default(1)
2068
- // .describe('The speed of the tts.'),
2069
- // pitch: z
2070
- // .number()
2071
- // .min(-12)
2072
- // .max(12)
2073
- // .optional()
2074
- // .default(0)
2075
- // .describe('The pitch of the tts.'),
2076
- // volume: z
2077
- // .number()
2078
- // .min(0)
2079
- // .max(10)
2080
- // .optional()
2081
- // .default(1.0)
2082
- // .describe('The volume of the tts.'),
2083
- // voiceID: z
2084
- // .string()
2085
- // .describe(
2086
- // `适合作为视频配音的音色ID,除非用户指定,否则你必须确保已通过 pick-voice 工具挑选出真实存在的音色。`
2087
- // ),
2088
- // context_texts: z
2089
- // .array(z.string())
2090
- // .default([])
2091
- // .describe(
2092
- // `语音合成的辅助信息,用于模型对话式合成,能更好的体现语音情感
2093
- // 可以探索,比如常见示例有以下几种:
2094
- // 1. 语速调整
2095
- // - context_texts: ["你可以说慢一点吗?"]
2096
- // 2. 情绪/语气调整
2097
- // - context_texts=["你可以用特别特别痛心的语气说话吗?"]
2098
- // - context_texts=["嗯,你的语气再欢乐一点"]
2099
- // 3. 音量调整
2100
- // - context_texts=["你嗓门再小点。"]
2101
- // 4. 音感调整
2102
- // - context_texts=["你能用骄傲的语气来说话吗?"]
2103
- // `
2104
- // ),
2105
- // explicit_language: z.enum(['zh', 'en', 'ja']).optional().default('zh'),
2106
- // },
2107
- // },
2108
- // async ({
2109
- // text,
2110
- // sceneIndex,
2111
- // storyBoardFile,
2112
- // skipConsistencyCheck,
2113
- // voiceID,
2114
- // saveToFileName,
2115
- // speed,
2116
- // pitch,
2117
- // volume,
2118
- // context_texts,
2119
- // explicit_language,
2120
- // }) => {
2121
- // try {
2122
- // // 验证session状态
2123
- // const currentSession = await validateSession('generate-scene-tts');
2124
- // const validatedFileName = validateFileName(saveToFileName);
2125
- // const finalSpeed = speed ?? 1;
2126
- // volume = volume ?? 1;
2127
- // const ai = currentSession.ai;
2128
- // let scene = null;
2129
- // // 校验 text 与 storyboard.json 中场景设定的一致性
2130
- // if (sceneIndex && !skipConsistencyCheck) {
2131
- // try {
2132
- // const voice = (await ai.listVoices()).find(v => v.id === voiceID);
2133
- // if (!voice) {
2134
- // return createErrorResponse(
2135
- // `Voice ${voiceID} not found in voice-list. Use pick-voice tool to pick an available voice. 若用户坚持要使用该音色,需跳过一致性检查。`,
2136
- // 'generate-scene-tts'
2137
- // );
2138
- // }
2139
- // const storyBoardPath = resolve(
2140
- // process.env.ZEROCUT_PROJECT_CWD || process.cwd(),
2141
- // projectLocalDir,
2142
- // storyBoardFile
2143
- // );
2144
- // if (existsSync(storyBoardPath)) {
2145
- // const storyBoardContent = await readFile(storyBoardPath, 'utf8');
2146
- // // 检查 storyBoard JSON 语法合法性
2147
- // let storyBoard;
2148
- // try {
2149
- // storyBoard = JSON.parse(storyBoardContent);
2150
- // } catch (jsonError) {
2151
- // return createErrorResponse(
2152
- // `storyBoard 文件 ${storyBoardFile} 存在 JSON 语法错误,请修复后重试。错误详情: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`,
2153
- // 'generate-scene-tts'
2154
- // );
2155
- // }
2156
- // if (storyBoard.scenes && Array.isArray(storyBoard.scenes)) {
2157
- // scene = storyBoard.scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
2158
- // if (scene) {
2159
- // const script = scene.script;
2160
- // let isValidText = false;
2161
- // // 检查 text 是否严格等于 script
2162
- // if (script && text === script) {
2163
- // isValidText = true;
2164
- // }
2165
- // // 检查 text 是否严格等于 dialog 数组中某个元素的 script
2166
- // if (
2167
- // !isValidText &&
2168
- // scene.dialog &&
2169
- // Array.isArray(scene.dialog)
2170
- // ) {
2171
- // for (const dialogItem of scene.dialog) {
2172
- // if (dialogItem.script && text === dialogItem.script) {
2173
- // isValidText = true;
2174
- // break;
2175
- // }
2176
- // }
2177
- // }
2178
- // if (!isValidText) {
2179
- // return createErrorResponse(
2180
- // '配音文本必须严格遵照storyboard的设定,如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用',
2181
- // 'generate-scene-tts'
2182
- // );
2183
- // }
2184
- // } else {
2185
- // console.warn(
2186
- // `Scene index ${sceneIndex} not found in storyboard.json`
2187
- // );
2188
- // }
2189
- // }
2190
- // } else {
2191
- // console.warn(`Story board file not found: ${storyBoardPath}`);
2192
- // }
2193
- // } catch (error) {
2194
- // console.error('Failed to validate text with story board:', error);
2195
- // // 如果读取或解析 storyboard.json 失败,继续执行但记录警告
2196
- // }
2197
- // }
2198
- // console.log(
2199
- // `Generating TTS with voice: ${voiceID}, speed: ${finalSpeed}, text: ${text.substring(0, 100)}...`
2200
- // );
2201
- // if (voiceID.startsWith('BV0')) {
2202
- // throw new Error(
2203
- // `BV0* 系列音色已弃用,你必须通过 pick-voice 工具挑选一个真实存在的音色。`
2204
- // );
2205
- // }
2206
- // const type =
2207
- // voiceID.startsWith('zh_') ||
2208
- // voiceID.startsWith('en_') ||
2209
- // voiceID.startsWith('multi_') ||
2210
- // voiceID.startsWith('saturn_') ||
2211
- // voiceID.startsWith('ICL_')
2212
- // ? 'volcano'
2213
- // : 'minimax';
2214
- // let res;
2215
- // let emotion = 'auto';
2216
- // if (type === 'volcano') {
2217
- // volume = Math.max(Math.min(volume, 2.0), 0.5);
2218
- // res = await ai.textToSpeechVolc({
2219
- // text: text.trim(),
2220
- // speaker: voiceID,
2221
- // speed: Math.floor(100 * (finalSpeed - 1)),
2222
- // volume: Math.floor(100 * (volume - 1)),
2223
- // context_texts,
2224
- // explicit_language,
2225
- // voice_to_caption:
2226
- // explicit_language === 'zh' || explicit_language === 'en',
2227
- // });
2228
- // } else {
2229
- // emotion = 'neutral';
2230
- // if (context_texts.length > 0) {
2231
- // const prompt = `根据用户输入语音内容和上下文内容,从文字判断语音合理的情感,然后选择以下情感**之一**返回结果:
2232
- // "happy", "sad", "angry", "fearful", "disgusted", "surprised", "calm", "fluent", "whisper", "neutral"
2233
- // ## 要求
2234
- // 输出 JSON 格式,包含一个 emotion 字段,值为以上情感之一。
2235
- // `;
2236
- // const schema = {
2237
- // name: 'emotion_schema',
2238
- // schema: {
2239
- // type: 'object',
2240
- // properties: {
2241
- // emotion: {
2242
- // type: 'string',
2243
- // enum: [
2244
- // 'neutral',
2245
- // 'happy',
2246
- // 'sad',
2247
- // 'angry',
2248
- // 'fearful',
2249
- // 'disgusted',
2250
- // 'surprised',
2251
- // 'calm',
2252
- // 'fluent',
2253
- // 'whisper',
2254
- // ],
2255
- // description: '用户输入语音的情感',
2256
- // },
2257
- // },
2258
- // required: ['emotion'],
2259
- // },
2260
- // };
2261
- // const payload: any = {
2262
- // model: 'Doubao-Seed-1.6',
2263
- // messages: [
2264
- // {
2265
- // role: 'system',
2266
- // content: prompt,
2267
- // },
2268
- // {
2269
- // role: 'user',
2270
- // content: `## 语音内容:
2271
- // ${text.trim()}
2272
- // ## 语音上下文
2273
- // ${context_texts.join('\n')}
2274
- // `,
2275
- // },
2276
- // ],
2277
- // response_format: {
2278
- // type: 'json_schema',
2279
- // json_schema: schema,
2280
- // },
2281
- // };
2282
- // const completion = await ai.getCompletions(payload);
2283
- // const emotionObj = JSON.parse(
2284
- // completion.choices[0]?.message?.content ?? '{}'
2285
- // );
2286
- // emotion = emotionObj.emotion ?? 'neutral';
2287
- // }
2288
- // res = await ai.textToSpeech({
2289
- // text: text.trim(),
2290
- // voiceName: voiceID,
2291
- // speed: finalSpeed,
2292
- // pitch,
2293
- // volume,
2294
- // emotion,
2295
- // voice_to_caption:
2296
- // explicit_language === 'zh' || explicit_language === 'en',
2297
- // });
2298
- // }
2299
- // if (!res) {
2300
- // throw new Error('Failed to generate TTS: no response from AI service');
2301
- // }
2302
- // if (res.url) {
2303
- // console.log('TTS generated successfully, saving to materials...');
2304
- // const { url, duration, ...opts } = res;
2305
- // if (!skipConsistencyCheck && duration > 16) {
2306
- // return createErrorResponse(
2307
- // 'TTS duration exceeds 16 seconds, 建议调整文本长度、提升语速或拆分场景...,⚠️如简化文本内容或拆分文本,需要立即更新 storyboard 以保持内容同步!如仍要生成,可设置 skipConsistencyCheck 为 true,跳过一致性检查。',
2308
- // 'generate-scene-tts'
2309
- // );
2310
- // }
2311
- // if (!duration) {
2312
- // return createErrorResponse(
2313
- // 'TTS duration not returned from AI service',
2314
- // 'generate-scene-tts'
2315
- // );
2316
- // }
2317
- // const uri = await saveMaterial(currentSession, url, validatedFileName);
2318
- // let warn = '';
2319
- // if (scene) {
2320
- // const minDur = Math.ceil(duration);
2321
- // if (scene.audio_mode === 'vo_sync' && scene.duration !== minDur) {
2322
- // warn = `场景${sceneIndex}设定的时长${scene.duration}秒与实际生成的语音时长${minDur}秒不一致,音画同步将有问题,建议修改场景时长为${minDur}秒`;
2323
- // } else if (scene.duration < minDur) {
2324
- // warn = `场景${sceneIndex}设定的时长${scene.duration}秒小于实际生成的语音时长${duration}秒,可能会导致场景结束时音频未播放完成,建议修改场景时长为${minDur}秒`;
2325
- // }
2326
- // }
2327
- // const result = {
2328
- // success: true,
2329
- // warn,
2330
- // source: url, // 方便调试
2331
- // uri,
2332
- // durationMs: Math.floor((duration || 0) * 1000),
2333
- // text,
2334
- // emotion,
2335
- // context_texts,
2336
- // voiceName: voiceID,
2337
- // speed: finalSpeed,
2338
- // timestamp: new Date().toISOString(),
2339
- // ...opts,
2340
- // };
2341
- // // Update media_logs.json
2342
- // try {
2343
- // await updateMediaLogs(
2344
- // currentSession,
2345
- // validatedFileName,
2346
- // result,
2347
- // 'audio'
2348
- // );
2349
- // } catch (error) {
2350
- // console.warn(
2351
- // `Failed to update media_logs.json for ${validatedFileName}:`,
2352
- // error
2353
- // );
2354
- // }
2355
- // return {
2356
- // content: [
2357
- // {
2358
- // type: 'text' as const,
2359
- // text: JSON.stringify(result),
2360
- // },
2361
- // ],
2362
- // };
2363
- // } else {
2364
- // console.warn('TTS generation completed but no URL returned');
2365
- // return {
2366
- // content: [
2367
- // {
2368
- // type: 'text' as const,
2369
- // text: JSON.stringify({
2370
- // success: false,
2371
- // error:
2372
- // 'No TTS URL returned from AI service. You should use pick-voice tool to pick an available voice.',
2373
- // response: res,
2374
- // timestamp: new Date().toISOString(),
2375
- // }),
2376
- // },
2377
- // ],
2378
- // };
2379
- // }
2380
- // } catch (error) {
2381
- // return createErrorResponse(error, 'generate-scene-tts');
2382
- // }
2383
- // }
2384
- // );
2385
- // server.registerTool(
2386
- // 'compile-and-run',
2387
- // {
2388
- // title: 'Compile And Run',
2389
- // description: 'Compile project to ffmpeg command and run it.',
2390
- // inputSchema: {
2391
- // projectFileName: z
2392
- // .string()
2393
- // .describe('The VideoProject configuration object.'),
2394
- // outputFileName: z
2395
- // .string()
2396
- // .optional()
2397
- // .describe('Output video filename (optional, defaults to output.mp4).'),
2398
- // },
2399
- // },
2400
- // async ({ projectFileName, outputFileName }) => {
2401
- // try {
2402
- // // 验证session状态
2403
- // const currentSession = await validateSession('compile-and-run');
2404
- // // 检查字幕内容匹配标记
2405
- // if (!checkStoryboardSubtitlesFlag) {
2406
- // checkStoryboardSubtitlesFlag = true;
2407
- // return createErrorResponse(
2408
- // `请先对 draft_content 进行以下一致性检查:
2409
- // 1. 检查字幕文字内容是否与 storyboard 中各个场景的 script 或 dialog 内容完全一致(⚠️ 允许字幕分段展示,只要最终文本保持一致就行)
2410
- // 2. 检查视频 resolution 设定是否与 storyboard 的 orientation 设置一致,默认 720p 情况下视频尺寸应为横屏 1280x720,竖屏 720x1280,若视频为 1080p 则尺寸应分别为横屏 1920x1080 和竖屏 1080x1920,切勿设反
2411
- // 3. 除非用户明确表示不要背景音乐,否则应检查是否有生成并配置了 BGM,若无,则生成 BGM 并将其加入素材和轨道配置
2412
- // 以上检查任何一项有问题,先修复 draft_content 使其符合要求后再进行合成`,
2413
- // 'compile-and-run'
2414
- // );
2415
- // }
2416
- // console.log('Starting video compilation and rendering...');
2417
- // // 验证terminal可用性
2418
- // const terminal = currentSession.terminal;
2419
- // if (!terminal) {
2420
- // throw new Error('Terminal not available in current session');
2421
- // }
2422
- // const localProjectFile = resolve(
2423
- // projectLocalDir,
2424
- // basename(projectFileName)
2425
- // );
2426
- // const project = JSON.parse(await readFile(localProjectFile, 'utf-8'));
2427
- // // 验证输出文件名安全性
2428
- // const outFile = outputFileName || project.export.outFile || 'output.mp4';
2429
- // const validatedFileName = validateFileName(outFile);
2430
- // console.log(`Output file: ${validatedFileName}`);
2431
- // // 构建工作目录路径
2432
- // const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}`;
2433
- // const outputDir = `${workDir}/output`;
2434
- // const outputPath = `${outputDir}/${validatedFileName}`;
2435
- // // Project已经通过zVideoProject schema验证
2436
- // const validated = { ...project };
2437
- // // 更新导出配置
2438
- // validated.export = {
2439
- // ...validated.export,
2440
- // outFile: outputPath,
2441
- // };
2442
- // console.log('Compiling VideoProject to FFmpeg command...');
2443
- // // 编译为FFmpeg命令
2444
- // let compiled: CompileResult;
2445
- // try {
2446
- // compiled = await compileToFfmpeg(validated, {
2447
- // workingDir: outputDir,
2448
- // subtitleStrategy: 'ass',
2449
- // subtitlesFileName: `${outFile.replace(/\.mp4$/, '')}.subtitles.ass`,
2450
- // });
2451
- // } catch (compileError) {
2452
- // console.error('Failed to compile VideoProject:', compileError);
2453
- // throw new Error(`Failed to compile VideoProject: ${compileError}`);
2454
- // }
2455
- // console.log(`FFmpeg command generated (${compiled.cmd.length} chars)`);
2456
- // console.log('FFmpeg Command:', compiled.cmd.substring(0, 200) + '...');
2457
- // // 执行FFmpeg命令
2458
- // console.log('Executing FFmpeg command...');
2459
- // const result = await runFfmpeg(currentSession, compiled);
2460
- // if (result.exitCode === 0) {
2461
- // console.log('Video compilation completed successfully');
2462
- // // 自动下载输出文件
2463
- // console.log('Starting automatic download of output files...');
2464
- // let downloadResult = null;
2465
- // try {
2466
- // const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/output`;
2467
- // let outputs: string[] = [];
2468
- // try {
2469
- // outputs = (await currentSession.files.listFiles(workDir)) || [];
2470
- // } catch (listError) {
2471
- // console.warn('Failed to list output files:', listError);
2472
- // outputs = [];
2473
- // }
2474
- // if (outputs.length > 0) {
2475
- // const outputDir = resolve(
2476
- // process.env.ZEROCUT_PROJECT_CWD || process.cwd(),
2477
- // projectLocalDir,
2478
- // 'output'
2479
- // );
2480
- // await mkdir(outputDir, { recursive: true });
2481
- // const downloadErrors: string[] = [];
2482
- // const successfulDownloads: string[] = [];
2483
- // const promises = outputs.map(async output => {
2484
- // try {
2485
- // await currentSession.files.download(
2486
- // `${workDir}/${output}`,
2487
- // `${outputDir}/${output}`
2488
- // );
2489
- // successfulDownloads.push(output);
2490
- // return output;
2491
- // } catch (downloadError) {
2492
- // const errorMsg = `Failed to download ${output}: ${downloadError}`;
2493
- // console.error(errorMsg);
2494
- // downloadErrors.push(errorMsg);
2495
- // return null;
2496
- // }
2497
- // });
2498
- // const results = await Promise.all(promises);
2499
- // const sources = results.filter(
2500
- // output => output !== null
2501
- // ) as string[];
2502
- // downloadResult = {
2503
- // totalFiles: outputs.length,
2504
- // successfulDownloads: sources.length,
2505
- // failedDownloads: downloadErrors.length,
2506
- // downloadErrors:
2507
- // downloadErrors.length > 0 ? downloadErrors : undefined,
2508
- // downloadPath: outputDir,
2509
- // sources,
2510
- // };
2511
- // console.log(
2512
- // `Download completed: ${sources.length}/${outputs.length} files successful`
2513
- // );
2514
- // } else {
2515
- // console.log('No output files found to download');
2516
- // downloadResult = {
2517
- // totalFiles: 0,
2518
- // successfulDownloads: 0,
2519
- // failedDownloads: 0,
2520
- // message: 'No output files found to download',
2521
- // };
2522
- // }
2523
- // } catch (downloadError) {
2524
- // console.error('Download process failed:', downloadError);
2525
- // downloadResult = {
2526
- // error: `Download failed: ${downloadError}`,
2527
- // totalFiles: 0,
2528
- // successfulDownloads: 0,
2529
- // failedDownloads: 1,
2530
- // };
2531
- // }
2532
- // const successResult = {
2533
- // success: true,
2534
- // outputPath,
2535
- // outputFileName: validatedFileName,
2536
- // command: compiled.cmd,
2537
- // message: 'Video compilation and download completed successfully',
2538
- // download: downloadResult,
2539
- // timestamp: new Date().toISOString(),
2540
- // };
2541
- // return {
2542
- // content: [
2543
- // {
2544
- // type: 'text' as const,
2545
- // text: JSON.stringify(successResult),
2546
- // },
2547
- // ],
2548
- // };
2549
- // } else {
2550
- // console.error(`FFmpeg failed with exit code: ${result.exitCode}`);
2551
- // const failureResult = {
2552
- // success: false,
2553
- // exitCode: result.exitCode,
2554
- // outputPath,
2555
- // // command: compiled.cmd,
2556
- // stderr: result.stderr,
2557
- // message: `FFmpeg exited with code ${result.exitCode}`,
2558
- // timestamp: new Date().toISOString(),
2559
- // };
2560
- // if (result.exitCode === 254) {
2561
- // failureResult.message =
2562
- // 'FFmpeg failed with code 254. Close current session immediately (inMinutes = 0) and re-open a new session to try again.';
2563
- // }
2564
- // return {
2565
- // content: [
2566
- // {
2567
- // type: 'text' as const,
2568
- // text: JSON.stringify(failureResult),
2569
- // },
2570
- // ],
2571
- // };
2572
- // }
2573
- // } catch (error) {
2574
- // return createErrorResponse(error, 'compile-and-run');
2575
- // }
2576
- // }
2577
- // );
2578
- // server.registerTool(
2579
- // 'get-schema',
2580
- // {
2581
- // title: 'Get Storyboard Schema or Draft Content Schema',
2582
- // description:
2583
- // 'Get the complete Storyboard or Draft Content JSON Schema definition. Use this schema to validate storyboard.json or draft_content.json files.',
2584
- // inputSchema: {
2585
- // type: z
2586
- // .enum(['storyboard', 'draft_content'])
2587
- // .describe(
2588
- // 'The type of schema to retrieve. Must be either "storyboard" or "draft_content". 用 type: storyboard 的 schema 生成 storyboard.json;用 type: draft_content 的 schema 生成 draft_content.json'
2589
- // ),
2590
- // },
2591
- // },
2592
- // async ({ type }) => {
2593
- // try {
2594
- // const schemaPath = resolve(__dirname, `./prompts/${type}-schema.json`);
2595
- // const schemaContent = await readFile(schemaPath, 'utf-8');
2596
- // const schema = JSON.parse(schemaContent);
2597
- // let important_guidelines = '';
2598
- // if (type === 'draft_content') {
2599
- // important_guidelines = `⚠️ 生成文件时请严格遵守输出规范,字幕文本内容必须与 storyboard.json 中的 script(或dialog) 字段的文本内容完全一致。
2600
- // ** 字幕优化 **
2601
- // * 在保证字幕文本内容与 storyboard.json 中的 script(或dialog) 字段的文本内容完全一致的前提下,可根据 tts 返回的 \`captions.utterances\` 字段对字幕的显示进行优化,将过长的字幕分段显示,在 draft_content.json 中使用分段字幕,captions 的内容在 media_logs.json 中可查询到。
2602
- // * 如用户未特殊指定,字幕样式(字体及大小)务必遵守输出规范
2603
- // `;
2604
- // }
2605
- // return {
2606
- // content: [
2607
- // {
2608
- // type: 'text' as const,
2609
- // text: JSON.stringify({
2610
- // success: true,
2611
- // schema,
2612
- // important_guidelines,
2613
- // timestamp: new Date().toISOString(),
2614
- // }),
2615
- // },
2616
- // ],
2617
- // };
2618
- // } catch (error) {
2619
- // return createErrorResponse(error, 'get-schema');
2620
- // }
2621
- // }
2622
- // );
2623
- // server.registerTool(
2624
- // 'pick-voice',
2625
- // {
2626
- // title: 'Pick Voice',
2627
- // description:
2628
- // '根据用户需求,选择尽可能符合要求的语音,在合适的情况下,优先采用 volcano_tts_2 类型的语音',
2629
- // inputSchema: {
2630
- // prompt: z
2631
- // .string()
2632
- // .describe('用户需求描述,例如:一个有亲和力的,适合给孩子讲故事的语音'),
2633
- // custom_design: z
2634
- // .boolean()
2635
- // .optional()
2636
- // .describe(
2637
- // '是否自定义语音,由于要消耗较多积分,因此**只有用户明确要求自己设计语音**,才将该参数设为true'
2638
- // ),
2639
- // custom_design_preview: z
2640
- // .string()
2641
- // .optional()
2642
- // .describe(
2643
- // '用户自定义语音的预览文本,用于展示自定义语音的效果,只有 custom_design 为 true 时才需要'
2644
- // ),
2645
- // custom_design_save_to: z
2646
- // .string()
2647
- // .optional()
2648
- // .describe(
2649
- // '自定义语音的保存路径,例如:custom_voice.mp3 custom_voice_{id}.mp3'
2650
- // ),
2651
- // },
2652
- // },
2653
- // async ({
2654
- // prompt,
2655
- // custom_design,
2656
- // custom_design_preview,
2657
- // custom_design_save_to,
2658
- // }) => {
2659
- // try {
2660
- // // 验证session状态
2661
- // const currentSession = await validateSession('pick-voice');
2662
- // const ai = currentSession.ai;
2663
- // if (custom_design) {
2664
- // if (!custom_design_preview) {
2665
- // throw new Error(
2666
- // 'custom_design_preview is required when custom_design is true'
2667
- // );
2668
- // }
2669
- // const data = await currentSession.ai.voiceDesign({
2670
- // prompt,
2671
- // previewText: custom_design_preview,
2672
- // });
2673
- // if (data.voice_id) {
2674
- // const trial_audio = data.trial_audio;
2675
- // let uri = '';
2676
- // if (trial_audio) {
2677
- // uri = await saveMaterial(
2678
- // currentSession,
2679
- // trial_audio,
2680
- // custom_design_save_to || `custom_voice_${data.voice_id}.mp3`
2681
- // );
2682
- // }
2683
- // return {
2684
- // content: [
2685
- // {
2686
- // type: 'text' as const,
2687
- // text: JSON.stringify({
2688
- // success: true,
2689
- // ...data,
2690
- // uri,
2691
- // timestamp: new Date().toISOString(),
2692
- // }),
2693
- // },
2694
- // ],
2695
- // };
2696
- // } else {
2697
- // throw new Error(`Voice design failed, ${JSON.stringify(data)}`);
2698
- // }
2699
- // }
2700
- // const data = await ai.pickVoice({ prompt });
2701
- // return {
2702
- // content: [
2703
- // {
2704
- // type: 'text' as const,
2705
- // text: JSON.stringify({
2706
- // success: true,
2707
- // ...data,
2708
- // timestamp: new Date().toISOString(),
2709
- // }),
2710
- // },
2711
- // ],
2712
- // };
2713
- // } catch (error) {
2714
- // return createErrorResponse(error, 'pick-voice');
2715
- // }
2716
- // }
2717
- // );
2718
1983
  let lastEffect = '';
2719
1984
  server.registerTool('generate-video', {
2720
1985
  title: 'Generate Video',
2721
1986
  description: `图生视频和首尾帧生视频工具`,
2722
1987
  inputSchema: {
2723
- prompt: zod_1.z
2724
- .string()
2725
- .describe('The prompt to generate. 一般要严格对应 storyboard 中当前场景的 video_prompt 字段描述;传这个参数时,若跳过了一致性检查,记得保留镜头切换语言,如“切镜至第二镜头”这样的指令不应当省略。'),
1988
+ prompt: zod_1.z.string().describe('The prompt to generate. '),
2726
1989
  sceneIndex: zod_1.z
2727
1990
  .number()
2728
1991
  .min(1)
2729
1992
  .optional()
2730
- .describe('场景索引,从1开始的下标,如果非场景对应素材,则可不传,场景素材必传'),
2731
- storyBoardFile: zod_1.z
2732
- .string()
2733
- .optional()
2734
- .default('storyboard.json')
2735
- .describe('故事板文件路径'),
2736
- skipConsistencyCheck: zod_1.z
2737
- .boolean()
2738
- .optional()
2739
- .default(false)
2740
- .describe('是否跳过一致性检查,默认为false(即默认进行一致性检查)'),
2741
- skipCheckWithSceneReason: zod_1.z
2742
- .string()
2743
- .optional()
2744
- .describe('跳过校验的理由,如果skipConsistencyCheck设为true,必须要传这个参数'),
1993
+ .describe('分镜索引,从1开始的下标,如果非分镜对应素材,则可不传,分镜素材必传'),
2745
1994
  type: zod_1.z
2746
1995
  .enum([
2747
1996
  'pro',
@@ -2802,7 +2051,7 @@ server.registerTool('generate-video', {
2802
2051
  .default(false)
2803
2052
  .describe('Whether to optimize the prompt.'),
2804
2053
  },
2805
- }, async ({ prompt, sceneIndex, storyBoardFile = 'storyboard.json', skipConsistencyCheck = false, saveToFileName, start_frame, end_frame, duration, resolution, type = 'vidu', optimizePrompt, saveLastFrameAs, mute = true, seed, }, context) => {
2054
+ }, async ({ prompt, sceneIndex, saveToFileName, start_frame, end_frame, duration, resolution, type = 'vidu', optimizePrompt, saveLastFrameAs, mute = true, seed, }, context) => {
2806
2055
  try {
2807
2056
  // 验证session状态
2808
2057
  const currentSession = await validateSession('generate-video');
@@ -2823,10 +2072,11 @@ server.registerTool('generate-video', {
2823
2072
  console.warn(`zero 模型的视频仅支持 1080p 分辨率,用户指定的分辨率为 %s,已自动将 ${resolution} 转换为 1080p`, resolution);
2824
2073
  resolution = '1080p';
2825
2074
  }
2826
- // 校验 prompt storyboard.json 中场景设定的一致性以及视频时长与 timeline_analysis.json 中 proposed_video_scenes 的匹配
2827
- if (sceneIndex && !skipConsistencyCheck) {
2075
+ const storyBoardFile = 'storyboard.json';
2076
+ // 校验 prompt storyboard.json 中分镜设定的一致性
2077
+ if (sceneIndex) {
2828
2078
  try {
2829
- const storyBoardPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, storyBoardFile);
2079
+ const storyBoardPath = (0, node_path_1.resolve)(projectLocalDir, storyBoardFile);
2830
2080
  if ((0, node_fs_1.existsSync)(storyBoardPath)) {
2831
2081
  const storyBoardContent = await (0, promises_1.readFile)(storyBoardPath, 'utf8');
2832
2082
  // 检查 storyBoard JSON 语法合法性
@@ -2842,26 +2092,26 @@ server.registerTool('generate-video', {
2842
2092
  if (scene) {
2843
2093
  const videoPrompt = scene.video_prompt;
2844
2094
  if (videoPrompt && prompt !== videoPrompt) {
2845
- return createErrorResponse('视频提示词必须严格遵照storyboard的设定,如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-video');
2095
+ return createErrorResponse('视频提示词必须严格遵照storyboard的设定', 'generate-video');
2846
2096
  }
2847
2097
  if (scene.is_continuous && !end_frame) {
2848
- return createErrorResponse('连续场景必须指定end_frame参数,如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-video');
2098
+ return createErrorResponse('连续分镜必须指定end_frame参数', 'generate-video');
2849
2099
  }
2850
2100
  if (scene.video_duration != null &&
2851
2101
  duration !== scene.video_duration) {
2852
- return createErrorResponse(`视频时长必须严格遵照storyboard的设定,用户指定的时长为 ${duration} 秒,而 storyboard 中建议的时长为 ${scene.video_duration} 秒。如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用`, 'generate-video');
2102
+ return createErrorResponse(`视频时长必须严格遵照storyboard的设定,用户指定的时长为 ${duration} 秒,而 storyboard 中建议的时长为 ${scene.video_duration} 秒。`, 'generate-video');
2853
2103
  }
2854
2104
  if (storyBoard.voice_type &&
2855
2105
  storyBoard.voice_type !== 'slient') {
2856
2106
  if (mute) {
2857
- return createErrorResponse('有对话和旁白的场景不能静音,请将mute设为false再重新使用工具。如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-video');
2107
+ return createErrorResponse('有对话和旁白的分镜不能静音,请将mute设为false再重新使用工具。', 'generate-video');
2858
2108
  }
2859
2109
  }
2860
2110
  // 检查 use_video_model 与 type 参数的一致性
2861
2111
  if (scene.use_video_model &&
2862
2112
  type &&
2863
2113
  scene.use_video_model !== type) {
2864
- return createErrorResponse(`场景建议的视频模型(${scene.use_video_model})与传入的type参数(${type})不一致。请确保use_video_model与type参数值相同,或将skipConsistencyCheck设置为true后再次调用`, 'generate-video');
2114
+ return createErrorResponse(`分镜建议的视频模型(${scene.use_video_model})与传入的type参数(${type})不一致。请确保use_video_model与type参数值相同。`, 'generate-video');
2865
2115
  }
2866
2116
  }
2867
2117
  else {
@@ -2877,60 +2127,6 @@ server.registerTool('generate-video', {
2877
2127
  console.error('Failed to validate prompt with story board:', error);
2878
2128
  // 如果读取或解析 storyboard.json 失败,继续执行但记录警告
2879
2129
  }
2880
- // 校验视频时长与 timeline_analysis.json 中 proposed_video_scenes 的匹配
2881
- try {
2882
- const timelineAnalysisPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'timeline_analysis.json');
2883
- if ((0, node_fs_1.existsSync)(timelineAnalysisPath)) {
2884
- const timelineAnalysisContent = await (0, promises_1.readFile)(timelineAnalysisPath, 'utf8');
2885
- const timelineAnalysis = JSON.parse(timelineAnalysisContent);
2886
- if (timelineAnalysis.proposed_video_scenes &&
2887
- Array.isArray(timelineAnalysis.proposed_video_scenes)) {
2888
- const videoScene = timelineAnalysis.proposed_video_scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
2889
- if (videoScene && videoScene.video_duration_s) {
2890
- const expectedDuration = videoScene.video_duration_s;
2891
- if (duration !== expectedDuration) {
2892
- return createErrorResponse(`视频时长必须与timeline_analysis中的设定匹配。当前场景${videoScene.scene_id}要求时长为${expectedDuration}秒,但传入的duration为${duration}秒。请调整duration参数为${expectedDuration}秒后重试。如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用。`, 'generate-video');
2893
- }
2894
- }
2895
- else {
2896
- console.warn(`Scene ${sceneIndex} (scene_${sceneIndex.toString().padStart(2, '0')}) not found in timeline_analysis.json proposed_video_scenes`);
2897
- }
2898
- }
2899
- }
2900
- else {
2901
- // 检查音频时长标志
2902
- try {
2903
- const storyBoardPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, storyBoardFile);
2904
- if ((0, node_fs_1.existsSync)(storyBoardPath)) {
2905
- const storyBoardContent = await (0, promises_1.readFile)(storyBoardPath, 'utf8');
2906
- const storyBoard = JSON.parse(storyBoardContent);
2907
- if (storyBoard.scenes && Array.isArray(storyBoard.scenes)) {
2908
- const scene = storyBoard.scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
2909
- if (scene &&
2910
- ((scene.script && scene.script.trim() !== '') ||
2911
- scene.dialog)) {
2912
- if (!checkAudioVideoDurationFlag) {
2913
- checkAudioVideoDurationFlag = true;
2914
- if (scene.audio_mode === 'vo_sync') {
2915
- return createErrorResponse('请先自我检查 media_logs 中的音频时长,确保 storyboard 中视频时长为音频时长向上取整 即 ceil(音频时长),然后再按照正确的视频时长创建视频', 'generate-video');
2916
- }
2917
- else if (scene.audio_mode === 'dialogue') {
2918
- return createErrorResponse('请先自我检查 media_logs 中的音频时长,确保 storyboard 中视频时长**不小于**音频时长向上取整 即 ceil(音频时长),然后再按照正确的视频时长创建视频', 'generate-video');
2919
- }
2920
- }
2921
- }
2922
- }
2923
- }
2924
- }
2925
- catch (error) {
2926
- console.error('Failed to check audio duration flag:', error);
2927
- }
2928
- }
2929
- }
2930
- catch (error) {
2931
- console.error('Failed to validate duration with timeline analysis:', error);
2932
- // 如果读取或解析 timeline_analysis.json 失败,继续执行但记录警告
2933
- }
2934
2130
  }
2935
2131
  const validatedFileName = validateFileName(saveToFileName);
2936
2132
  console.log(`Generating video with prompt: ${prompt.substring(0, 100)}...`);
@@ -3049,7 +2245,7 @@ server.registerTool('generate-video', {
3049
2245
  },
3050
2246
  };
3051
2247
  const analysisPayload = {
3052
- model: 'Doubao-Seed-1.6-flash',
2248
+ model: 'Doubao-Seed-1.8',
3053
2249
  messages: [
3054
2250
  {
3055
2251
  role: 'system',
@@ -3184,7 +2380,7 @@ server.registerTool('generate-video', {
3184
2380
  console.warn('Failed to send progress update:', progressError);
3185
2381
  }
3186
2382
  },
3187
- waitForFinish: type !== 'zero',
2383
+ waitForFinish: type !== 'zero' && CLIENT_TYPE !== '5ire',
3188
2384
  mute,
3189
2385
  seed,
3190
2386
  });
@@ -3328,6 +2524,7 @@ server.registerTool('edit-video', {
3328
2524
  prompt,
3329
2525
  referenceImageUrl,
3330
2526
  onProgress,
2527
+ waitForFinish: CLIENT_TYPE !== '5ire',
3331
2528
  });
3332
2529
  }
3333
2530
  else if (type === 'lipsync') {
@@ -3342,6 +2539,7 @@ server.registerTool('edit-video', {
3342
2539
  audioInMs: 0,
3343
2540
  pad_audio: false,
3344
2541
  onProgress,
2542
+ waitForFinish: CLIENT_TYPE !== '5ire',
3345
2543
  });
3346
2544
  }
3347
2545
  else if (type === 'imitate') {
@@ -3359,6 +2557,7 @@ server.registerTool('edit-video', {
3359
2557
  videoUrl,
3360
2558
  imageUrl: referenceImageUrl,
3361
2559
  onProgress,
2560
+ waitForFinish: CLIENT_TYPE !== '5ire',
3362
2561
  });
3363
2562
  }
3364
2563
  if (res.url) {
@@ -3567,11 +2766,15 @@ server.registerTool('audio-video-sync', {
3567
2766
  .describe('The volume of video audio. 0.0 to 2.0.'),
3568
2767
  loopAudio: zod_1.z.boolean().optional().default(true),
3569
2768
  addSubtitles: zod_1.z.boolean().optional().default(false),
2769
+ subtitlesContext: zod_1.z
2770
+ .string()
2771
+ .optional()
2772
+ .describe('字幕的参考上下文(非必需),用于提升字幕准确性'),
3570
2773
  saveToFileName: zod_1.z
3571
2774
  .string()
3572
2775
  .describe('The filename to save the audio-video-synced video. 应该是mp4文件'),
3573
2776
  },
3574
- }, async ({ videos, audio, audioInMs, audioFadeOutMs, audioVolume, videoAudioVolume, saveToFileName, loopAudio, addSubtitles, }, context) => {
2777
+ }, async ({ videos, audio, audioInMs, audioFadeOutMs, audioVolume, videoAudioVolume, saveToFileName, loopAudio, addSubtitles, subtitlesContext, }, context) => {
3575
2778
  try {
3576
2779
  // 验证session状态
3577
2780
  const currentSession = await validateSession('audio-video-sync');
@@ -3605,6 +2808,7 @@ server.registerTool('audio-video-sync', {
3605
2808
  videoAudioVolume,
3606
2809
  loopAudio,
3607
2810
  subtitles: addSubtitles,
2811
+ subtitlesContext,
3608
2812
  });
3609
2813
  if (result.url) {
3610
2814
  console.log('Audio sync completed successfully');
@@ -3652,7 +2856,7 @@ server.registerTool('generate-video-by-ref', {
3652
2856
  inputSchema: {
3653
2857
  prompt: zod_1.z
3654
2858
  .string()
3655
- .describe('The prompt to generate video with or without reference images. 一般要严格对应 storyboard 中当前场景的 video_prompt 字段描述;传这个参数时,若跳过了一致性检查,记得保留镜头切换语言,如“切镜至第二镜头”这样的指令不应当省略。'),
2859
+ .describe('The prompt to generate video with or without reference images. '),
3656
2860
  rewritePrompt: zod_1.z
3657
2861
  .boolean()
3658
2862
  .optional()
@@ -3677,12 +2881,14 @@ server.registerTool('generate-video-by-ref', {
3677
2881
  .max(16)
3678
2882
  .optional()
3679
2883
  .default(5)
3680
- .describe('The duration of the video in seconds.'),
2884
+ .describe('The duration of the video in seconds.可以传0,此时会根据视频提示词内容自动确定时长'),
3681
2885
  aspectRatio: zod_1.z
3682
2886
  .enum(['16:9', '9:16'])
2887
+ .default('16:9')
3683
2888
  .describe('The aspect ratio of the video.'),
3684
2889
  resolution: zod_1.z
3685
2890
  .enum(['720p', '1080p'])
2891
+ .default('720p')
3686
2892
  .describe('The resolution of the video.'),
3687
2893
  type: zod_1.z
3688
2894
  .enum([
@@ -3713,58 +2919,27 @@ server.registerTool('generate-video-by-ref', {
3713
2919
  .number()
3714
2920
  .min(1)
3715
2921
  .optional()
3716
- .describe('场景索引,从1开始的下标,如果非场景对应素材,则可不传,场景素材必传'),
3717
- storyBoardFile: zod_1.z
3718
- .string()
3719
- .optional()
3720
- .default('storyboard.json')
3721
- .describe('故事板文件路径'),
3722
- skipConsistencyCheck: zod_1.z
3723
- .boolean()
3724
- .optional()
3725
- .default(false)
3726
- .describe('是否跳过一致性检查,默认为false(即默认进行一致性检查)'),
3727
- skipCheckWithSceneReason: zod_1.z
3728
- .string()
3729
- .optional()
3730
- .describe('跳过校验的理由,如果skipConsistencyCheck设为true,必须要传这个参数'),
2922
+ .describe('分镜索引,从1开始的下标,如果非分镜对应素材,则可不传,分镜素材必传'),
3731
2923
  optimizePrompt: zod_1.z
3732
2924
  .boolean()
3733
2925
  .optional()
3734
2926
  .default(false)
3735
2927
  .describe('Whether to optimize the prompt.'),
3736
2928
  },
3737
- }, async ({ prompt, rewritePrompt, referenceImages, duration, aspectRatio, resolution, type = 'vidu', mute, saveToFileName, sceneIndex, storyBoardFile, skipConsistencyCheck, optimizePrompt, seed, }, context) => {
2929
+ }, async ({ prompt, rewritePrompt, referenceImages, duration, aspectRatio, resolution, type = 'vidu', mute, saveToFileName, sceneIndex, optimizePrompt, seed, }, context) => {
3738
2930
  try {
2931
+ const storyBoardFile = 'storyboard.json';
3739
2932
  // 验证session状态
3740
2933
  const currentSession = await validateSession('generate-video-by-ref');
3741
- const storyBoardPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, storyBoardFile);
3742
- if (type !== 'pro' && duration === 0) {
3743
- return createErrorResponse('非 pro 模型的视频时长不能为 0', 'generate-video');
3744
- }
3745
- const outlineSheetImagePath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'materials', 'outline_sheet.png');
2934
+ const storyBoardPath = (0, node_path_1.resolve)(projectLocalDir, storyBoardFile);
2935
+ const outlineSheetImagePath = (0, node_path_1.resolve)(projectLocalDir, 'materials', 'outline_sheet.png');
3746
2936
  const hasOutlineSheet = (0, node_fs_1.existsSync)(outlineSheetImagePath);
3747
- if (hasOutlineSheet && !skipConsistencyCheck) {
3748
- return createErrorResponse('如果提供了 outline_sheet.png,应采用 generate-video 图生视频。若用户明确要用参考生视频,则跳过一致性检查。', 'generate-video');
3749
- }
3750
- // 检查 storyboard 标志
3751
- if (!hasOutlineSheet &&
3752
- !checkStoryboardFlag &&
3753
- (0, node_fs_1.existsSync)(storyBoardPath)) {
3754
- checkStoryboardFlag = true;
3755
- return createErrorResponse(`必须先审查生成的 storyboard.json 内容,按照如下步骤:
3756
-
3757
- 1. 确保每个场景中的stage_atmosphere内容按照规则被正确融合到video_prompt中,不得遗漏
3758
- 2. 如有main_characters设定且包含了reference_image,或有reference_objects,需确保video_prompt描述已包含该场景相关main_characters和所有reference_objects中的物品或背景,并确保参考图具体内容已经在video_prompt中有明确描述,如果没有,可忽略。
3759
- 3. 如有配音,先自我检查 media_logs 中的查音频时长,确保以匹配音频时长来生成视频
3760
-
3761
- 检查完上述问题后先汇报,如果有需要,应当先修改 storyboard.json 内容,然后再调用 generate-video-by-ref 生成视频。注意修改 storyboard 内容时,仅修改相应字段的字符串值,不要破坏JSON格式!
3762
-
3763
- 再次调用 generate-video-by-ref 时,如需要参考图,要确保referenceImages使用正确(main_characters中的reference_image作为参考人物,reference_objects中的image作为参考物品或参考背景)`, 'generate-image');
3764
- }
3765
- // 校验 prompt 与 storyboard.json 中场景设定的一致性(如果提供了 sceneIndex)
3766
- if (!skipConsistencyCheck && sceneIndex) {
2937
+ // 校验 prompt storyboard.json 中分镜设定的一致性(如果提供了 sceneIndex)
2938
+ if (sceneIndex) {
3767
2939
  try {
2940
+ if (hasOutlineSheet) {
2941
+ return createErrorResponse('监测到素材中存在outline_sheet.png这张图(由outline工具生成的),应采用 generate-video 图生视频。', 'generate-video-by-ref');
2942
+ }
3768
2943
  if ((0, node_fs_1.existsSync)(storyBoardPath)) {
3769
2944
  const storyBoardContent = await (0, promises_1.readFile)(storyBoardPath, 'utf8');
3770
2945
  // 检查 storyBoard JSON 语法合法性
@@ -3780,20 +2955,20 @@ server.registerTool('generate-video-by-ref', {
3780
2955
  if (scene) {
3781
2956
  const videoPrompt = scene.video_prompt;
3782
2957
  if (videoPrompt && prompt !== videoPrompt) {
3783
- return createErrorResponse('视频提示词必须严格遵照storyboard的设定,如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-video-by-ref');
2958
+ return createErrorResponse('视频提示词必须严格遵照storyboard的设定', 'generate-video-by-ref');
3784
2959
  }
3785
2960
  // 检查 scene.is_continuous 是否为 true
3786
2961
  if (scene.is_continuous === true) {
3787
- return createErrorResponse('连续镜头应使用首尾帧,请修改连续镜头设置,或将本场景改为首尾帧方式实现', 'generate-video-by-ref');
2962
+ return createErrorResponse('连续镜头应使用首尾帧,请修改连续镜头设置,或将本分镜改为首尾帧方式实现', 'generate-video-by-ref');
3788
2963
  }
3789
2964
  if (scene.video_type !== 'references') {
3790
- return createErrorResponse(`场景 ${sceneIndex} 中的 video_type (${scene.video_type}) 未设置为 'references',不应当使用参考生视频,请使用图生视频 generate-video 方式生成`, 'generate-video-by-ref');
2965
+ return createErrorResponse(`分镜 ${sceneIndex} 中的 video_type (${scene.video_type}) 未设置为 'references',不应当使用参考生视频,请使用图生视频 generate-video 方式生成`, 'generate-video-by-ref');
3791
2966
  }
3792
2967
  // 检查 use_video_model 与 type 参数的一致性
3793
2968
  if (scene.use_video_model &&
3794
2969
  type &&
3795
2970
  scene.use_video_model !== type) {
3796
- return createErrorResponse(`场景 ${sceneIndex} 中的 use_video_model (${scene.use_video_model}) 必须与 type 参数 (${type}) 保持一致`, 'generate-video-by-ref');
2971
+ return createErrorResponse(`分镜 ${sceneIndex} 中的 use_video_model (${scene.use_video_model}) 必须与 type 参数 (${type}) 保持一致`, 'generate-video-by-ref');
3797
2972
  }
3798
2973
  // 检查 scene.references 的一致性
3799
2974
  if (scene.references &&
@@ -3809,7 +2984,7 @@ server.registerTool('generate-video-by-ref', {
3809
2984
  const character = mainCharacters.find((char) => char.name === referenceName);
3810
2985
  if (character) {
3811
2986
  if (!character.reference_image) {
3812
- return createErrorResponse(`场景 ${sceneIndex} 中引用的角色 "${referenceName}" 在 main_characters 中没有 reference_image 属性`, 'generate-video-by-ref');
2987
+ return createErrorResponse(`分镜 ${sceneIndex} 中引用的角色 "${referenceName}" 在 main_characters 中没有 reference_image 属性`, 'generate-video-by-ref');
3813
2988
  }
3814
2989
  requiredImage = character.reference_image;
3815
2990
  found = true;
@@ -3819,7 +2994,7 @@ server.registerTool('generate-video-by-ref', {
3819
2994
  const refObject = referenceObjects.find((obj) => obj.name === referenceName);
3820
2995
  if (refObject) {
3821
2996
  if (!refObject.image) {
3822
- return createErrorResponse(`场景 ${sceneIndex} 中引用的物品/背景 "${referenceName}" 在 reference_objects 中没有 image 属性`, 'generate-video-by-ref');
2997
+ return createErrorResponse(`分镜 ${sceneIndex} 中引用的物品/背景 "${referenceName}" 在 reference_objects 中没有 image 属性`, 'generate-video-by-ref');
3823
2998
  }
3824
2999
  requiredImage = refObject.image;
3825
3000
  found = true;
@@ -3827,11 +3002,11 @@ server.registerTool('generate-video-by-ref', {
3827
3002
  }
3828
3003
  // 如果既不在 main_characters 也不在 reference_objects 中
3829
3004
  if (!found) {
3830
- return createErrorResponse(`场景 ${sceneIndex} 中引用的 "${referenceName}" 在 main_characters 或 reference_objects 中都找不到对应的定义`, 'generate-video-by-ref');
3005
+ return createErrorResponse(`分镜 ${sceneIndex} 中引用的 "${referenceName}" 在 main_characters 或 reference_objects 中都找不到对应的定义`, 'generate-video-by-ref');
3831
3006
  }
3832
3007
  // 检查对应的图片是否在 referenceImages 参数中
3833
3008
  if (!referenceImages.some(ref => ref.fileName === requiredImage)) {
3834
- return createErrorResponse(`场景 ${sceneIndex} 中引用的 "${referenceName}" 对应的图片 "${requiredImage}" 不在 referenceImages 参数中,请确保传入正确的参考图片`, 'generate-video-by-ref');
3009
+ return createErrorResponse(`分镜 ${sceneIndex} 中引用的 "${referenceName}" 对应的图片 "${requiredImage}" 不在 referenceImages 参数中,请确保传入正确的参考图片`, 'generate-video-by-ref');
3835
3010
  }
3836
3011
  }
3837
3012
  }
@@ -3936,6 +3111,7 @@ server.registerTool('generate-video-by-ref', {
3936
3111
  console.log('Video generation progress:', metaData);
3937
3112
  sendProgress(context, metaData.progress || 0, 100, 'Generating video...');
3938
3113
  },
3114
+ waitForFinish: CLIENT_TYPE !== '5ire',
3939
3115
  });
3940
3116
  if (result.error) {
3941
3117
  return createErrorResponse(result.error, 'generate-video-by-ref');
@@ -4061,6 +3237,7 @@ server.registerTool('extend-video-duration', {
4061
3237
  onProgress: async (metaData) => {
4062
3238
  sendProgress(context, ++progress, undefined, `Extension progress: ${Math.round(progress * 100)}%`);
4063
3239
  },
3240
+ waitForFinish: CLIENT_TYPE !== '5ire',
4064
3241
  });
4065
3242
  // 检查结果
4066
3243
  if (!result || result.error) {
@@ -4137,7 +3314,7 @@ server.registerTool('use-template', {
4137
3314
  }));
4138
3315
  const validatedFileName = validateFileName(saveToFileName);
4139
3316
  let completion = await ai.getCompletions({
4140
- model: 'Doubao-Seed-1.6',
3317
+ model: 'Doubao-Seed-1.8',
4141
3318
  messages: [
4142
3319
  {
4143
3320
  role: 'system',
@@ -4183,7 +3360,7 @@ ${user_request}
4183
3360
  ${JSON.stringify(materialUrls)}`;
4184
3361
  // console.log(prompt);
4185
3362
  completion = await ai.getCompletions({
4186
- model: 'Doubao-Seed-1.6',
3363
+ model: 'Doubao-Seed-1.8',
4187
3364
  messages: [
4188
3365
  {
4189
3366
  role: 'system',