cerevox 4.16.0 → 4.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,11 +47,9 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
47
47
  const zod_1 = require("zod");
48
48
  const index_1 = __importDefault(require("../../index"));
49
49
  const constants_1 = require("../../utils/constants");
50
- const videokit_1 = require("../../utils/videokit");
51
50
  const promises_1 = require("node:fs/promises");
52
51
  const node_path_1 = __importStar(require("node:path"));
53
52
  const node_fs_1 = require("node:fs");
54
- const image_size_1 = __importDefault(require("image-size"));
55
53
  const seed_1 = require("../../utils/seed");
56
54
  const uuid_1 = require("uuid");
57
55
  const node_crypto_1 = require("node:crypto");
@@ -302,8 +300,8 @@ let projectLocalDir = process.env.ZEROCUT_PROJECT_CWD ||
302
300
  `${(0, node_path_1.resolve)(process.env.ZEROCUT_WORKSPACE_DIR, (0, uuid_1.v4)())}` ||
303
301
  '.';
304
302
  let closeSessionTimerId = null;
305
- const CLIENT_TYPE = process.env.CLIENT_TYPE || 'trae';
306
303
  const MAIN_PROVIDER = process.env.MAIN_PROVIDER || 'zerocut';
304
+ const MCP_TIMEOUT_MS = Number(process.env.RUN_MCP_TIMEOUT_MS || '300000') - 60000;
307
305
  // 注册 ZeroCut 指导规范 Prompt
308
306
  server.registerPrompt('zerocut-guideline', {
309
307
  title: 'ZeroCut 短视频创作指导规范',
@@ -698,165 +696,11 @@ server.registerTool('wait-for-task-finish', {
698
696
  return createErrorResponse(error, 'wait-for-task-finish');
699
697
  }
700
698
  });
701
- server.registerTool('generate-subject-image', {
702
- title: 'Generate Subject Image',
703
- description: '生成主体四视图(人物或物品形象)',
704
- inputSchema: {
705
- type: zod_1.z.enum(['object', 'character']).optional().default('character'),
706
- model: zod_1.z
707
- .enum([
708
- 'seedream',
709
- 'seedream-pro',
710
- 'seedream-5l',
711
- 'banana',
712
- 'banana-pro',
713
- 'wan',
714
- ])
715
- .default('banana-pro')
716
- .describe('除非用户特别指定,否则默认使用 banana-pro 模型'),
717
- prompt: zod_1.z
718
- .string()
719
- .describe('忠于用户原始需求,严格按用户需求用简洁提示词描述主体外观,不用再提这是四视图、无须解释四视图方位,也无须描述与主体无关的其他内容比如一致性之类,工具会自行处理'),
720
- referenceImages: zod_1.z
721
- .array(zod_1.z.object({
722
- name: zod_1.z.string().describe('图片名称,人物名或物品名'),
723
- fileName: zod_1.z.string().describe('Reference image file name'),
724
- }))
725
- .optional()
726
- .default([])
727
- .describe('Array of reference image objects with name, url and type. Can be empty for text-only generation.如果传了参考图,不要文字描述主体详细外观,以免和参考主体冲突'),
728
- saveToFileName: zod_1.z.string().describe('应该是png文件'),
729
- },
730
- }, async ({ type, model, prompt, referenceImages = [], saveToFileName }, context) => {
731
- try {
732
- // 验证session状态
733
- const currentSession = await validateSession('generate-character-image');
734
- checkModelEnabled(type);
735
- const validatedFileName = validateFileName(saveToFileName);
736
- const ai = currentSession.ai;
737
- const imageUrls = await Promise.all(referenceImages.map(async (item) => ({
738
- url: await getMaterialUri(currentSession, item.fileName, {
739
- fileSizeLimit: 10,
740
- }),
741
- name: item.name,
742
- })));
743
- try {
744
- if (type === 'character' && imageUrls.length <= 0) {
745
- // 补充人物细节
746
- const promptOptimizer = await (0, promises_1.readFile)((0, node_path_1.resolve)(__dirname, './prompts/character-prompt-optimizer.md'), 'utf8');
747
- const userContent = [
748
- {
749
- type: 'text',
750
- text: prompt,
751
- },
752
- ];
753
- if (imageUrls.length > 0) {
754
- userContent.push(...imageUrls.map(item => ({
755
- type: 'image_url',
756
- image_url: {
757
- url: item.url,
758
- },
759
- })));
760
- }
761
- const completion = await ai.getCompletions({
762
- model: 'Doubao-Seed-1.8',
763
- messages: [
764
- {
765
- role: 'system',
766
- content: promptOptimizer,
767
- },
768
- {
769
- role: 'user',
770
- content: userContent,
771
- },
772
- ],
773
- });
774
- const optimizedPrompt = completion.choices[0]?.message?.content.trim();
775
- if (optimizedPrompt) {
776
- prompt = `${optimizedPrompt} 8K,超高细节,逼真质感。`;
777
- }
778
- }
779
- }
780
- catch (error) {
781
- console.warn('Failed to optimize character prompt');
782
- }
783
- let progress = 0;
784
- const res = await ai.generateTurnaroundImage({
785
- type: model,
786
- prompt,
787
- reference_images: imageUrls,
788
- onProgress: async (metaData) => {
789
- try {
790
- await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
791
- }
792
- catch (progressError) {
793
- console.warn('Failed to send progress update:', progressError);
794
- }
795
- },
796
- });
797
- if (!res) {
798
- throw new Error('Failed to generate image: no response from AI service');
799
- }
800
- res.url = res.url || res.urls[0];
801
- if (res.taskUrl) {
802
- return {
803
- content: [
804
- {
805
- type: 'text',
806
- text: JSON.stringify({
807
- success: true,
808
- message: '该任务正在运行中,它是异步任务,且执行时间较长,你应立即调用工具 wait-for-task-finish (saveFileName=outline_sheet.png) 来等待任务结束,如 wait-for-task-finish 工具调用超时,你应立即再次重新调用直到任务结束。',
809
- taskUrl: res.taskUrl,
810
- }),
811
- },
812
- ],
813
- };
814
- }
815
- if (res.url) {
816
- console.log('Image generated successfully, saving to materials...');
817
- const image = await saveMaterial(currentSession, res.url, validatedFileName);
818
- const result = {
819
- success: true,
820
- // source: res.url,
821
- image,
822
- prompt,
823
- timestamp: new Date().toISOString(),
824
- };
825
- return {
826
- content: [
827
- {
828
- type: 'text',
829
- text: JSON.stringify(result),
830
- },
831
- ],
832
- };
833
- }
834
- else {
835
- console.warn('Image generation completed but no URL returned');
836
- return {
837
- content: [
838
- {
839
- type: 'text',
840
- text: JSON.stringify({
841
- success: false,
842
- error: 'No image URL returned from AI service',
843
- response: res,
844
- timestamp: new Date().toISOString(),
845
- }),
846
- },
847
- ],
848
- };
849
- }
850
- }
851
- catch (error) {
852
- return createErrorResponse(error, 'generate-character-image');
853
- }
854
- });
855
699
  server.registerTool('generate-image', {
856
700
  title: 'Generate Image',
857
701
  description: `生成图片`,
858
702
  inputSchema: {
859
- type: zod_1.z
703
+ model: zod_1.z
860
704
  .enum([
861
705
  'banana',
862
706
  'banana-pro',
@@ -866,11 +710,23 @@ server.registerTool('generate-image', {
866
710
  'seedream-5l',
867
711
  'zero',
868
712
  'wan',
713
+ ])
714
+ .optional()
715
+ .default(MAIN_PROVIDER === 'aliyun' ? 'wan' : 'seedream-pro'),
716
+ type: zod_1.z
717
+ .enum([
718
+ 'default',
869
719
  'line-sketch',
870
720
  'storyboard-grid',
721
+ 'subject-turnaround',
871
722
  ])
872
723
  .optional()
873
- .default(MAIN_PROVIDER === 'aliyun' ? 'wan' : 'seedream-pro'),
724
+ .default('default')
725
+ .describe(`生成图片类型
726
+ - 默认 default,生成普通图片
727
+ - 生成分镜宫格(3x3): type 使用 storyboard-grid,注意只有九宫格(3x3)才使用这个,其他形式的宫格一律用default!
728
+ - 生成线稿: type 使用 line-sketch
729
+ - 生成主体四视图:type 使用 subject-turnaround`),
874
730
  panelCount: zod_1.z
875
731
  .union([zod_1.z.literal(4), zod_1.z.literal(6), zod_1.z.literal(9)])
876
732
  .optional()
@@ -878,7 +734,7 @@ server.registerTool('generate-image', {
878
734
  .describe('仅 type=storyboard-grid 时生效,默认9宫格'),
879
735
  prompt: zod_1.z
880
736
  .string()
881
- .describe('生图提示词,需严格忠于用户的具体需求; 如有sceneIndex,一般要严格对应 storyboard 中当前分镜的 start_frame 或 end_frame 中的字段描述,如果是生成线稿,则 type 使用 line-sketch'),
737
+ .describe('生图提示词,需严格忠于用户的具体需求; 如有sceneIndex,一般要严格对应 storyboard 中当前分镜的 start_frame 或 end_frame 中的字段描述'),
882
738
  sceneIndex: zod_1.z
883
739
  .number()
884
740
  .min(1)
@@ -964,14 +820,12 @@ server.registerTool('generate-image', {
964
820
  \`\`\`
965
821
  `),
966
822
  },
967
- }, async ({ type = 'seedream', prompt, sceneIndex, size, imageCount = 1, saveToFileNames, watermark, referenceImages, panelCount, }, context) => {
823
+ }, async ({ model = 'seedream-5l', type = 'default', prompt, sceneIndex, size, imageCount = 1, saveToFileNames, watermark, referenceImages, panelCount, }, context) => {
968
824
  try {
969
825
  const storyBoardFile = 'storyboard.json';
970
826
  // 验证session状态
971
827
  const currentSession = await validateSession('generate-image');
972
- if (type !== 'line-sketch' && type !== 'storyboard-grid') {
973
- checkModelEnabled(type);
974
- }
828
+ checkModelEnabled(model);
975
829
  // storyboard-grid 默认为2K分辨率 2560x1440,其余默认 1440x2560.
976
830
  if (!size) {
977
831
  if (type === 'storyboard-grid') {
@@ -1093,6 +947,7 @@ server.registerTool('generate-image', {
1093
947
  const ai = currentSession.ai;
1094
948
  let progress = 0;
1095
949
  const res = await ai.generateImage({
950
+ model,
1096
951
  type,
1097
952
  prompt: processedPrompt,
1098
953
  panelCount: type === 'storyboard-grid' ? panelCount : undefined,
@@ -1253,7 +1108,6 @@ server.registerTool('generate-video-outlines', {
1253
1108
  console.warn('Failed to send progress update:', progressError);
1254
1109
  }
1255
1110
  },
1256
- waitForFinish: CLIENT_TYPE !== '5ire',
1257
1111
  });
1258
1112
  if (!res) {
1259
1113
  throw new Error('Failed to generate short video outlines: no response from AI service');
@@ -1581,7 +1435,6 @@ server.registerTool('text-to-speech', {
1581
1435
  return createErrorResponse(error, 'text-to-speech');
1582
1436
  }
1583
1437
  });
1584
- let lastEffect = '';
1585
1438
  server.registerTool('generate-video', {
1586
1439
  title: 'Generate Video',
1587
1440
  description: `首帧图和首尾帧生视频工具`,
@@ -1597,49 +1450,89 @@ server.registerTool('generate-video', {
1597
1450
  .describe('语音音色详细描述,如性别、声线、情感等,每一次都需要详细信息'),
1598
1451
  voice_id: zod_1.z.string().optional().describe('用户指定优先使用的音色ID'),
1599
1452
  })
1453
+ .describe('只有用户明确使用旁白时,才使用旁白')
1600
1454
  .optional(),
1601
1455
  sceneIndex: zod_1.z
1602
1456
  .number()
1603
1457
  .min(1)
1604
1458
  .optional()
1605
1459
  .describe('分镜索引,从1开始的下标,如果非分镜对应素材,则可不传,分镜素材必传'),
1606
- type: zod_1.z
1460
+ model: zod_1.z
1607
1461
  .enum([
1462
+ 'zerocut3.0',
1608
1463
  'pro',
1609
- 'hailuo',
1610
- 'hailuo-fast',
1464
+ 'seedance-1.5-pro',
1611
1465
  'vidu',
1612
- 'viduq2',
1613
- 'vidu-turbo',
1614
1466
  'vidu-pro',
1615
1467
  'viduq3',
1616
1468
  'viduq3-turbo',
1617
1469
  'kling',
1618
1470
  'kling-v3',
1471
+ 'wan',
1619
1472
  'wan-flash',
1620
- 'pixv',
1621
- 'veo3.1',
1622
- 'veo3.1-pro',
1623
1473
  'sora2',
1624
1474
  'sora2-pro',
1625
- 'kenburns',
1475
+ 'veo3.1',
1476
+ 'veo3.1-pro',
1626
1477
  ])
1627
1478
  .default(MAIN_PROVIDER === 'aliyun' ? 'wan-flash' : 'vidu'),
1628
- audio: zod_1.z
1479
+ silent: zod_1.z
1629
1480
  .boolean()
1630
1481
  .optional()
1631
- .default(true)
1632
- .describe('除非用户明确要求静音,否则默认audio=true,此时视频模型会自己配音,不需要再使用tts工具!'),
1482
+ .default(false)
1483
+ .describe('除非用户明确要求静音,否则默认silent=false,此时视频模型会自己配音,不需要再使用tts工具!'),
1633
1484
  bgm: zod_1.z.boolean().optional().describe('是否启用背景音乐,缺省的话是自动'),
1634
1485
  seed: zod_1.z
1635
1486
  .number()
1636
1487
  .optional()
1637
1488
  .describe('用户可以指定一个固定值来获得可重复的结果'),
1638
1489
  saveToFileName: zod_1.z.string().describe('应该是mp4文件'),
1639
- start_frame: zod_1.z
1640
- .string()
1490
+ images: zod_1.z
1491
+ .array(zod_1.z.object({
1492
+ name: zod_1.z
1493
+ .string()
1494
+ .optional()
1495
+ .describe('图片名称,人物名、图片背景名或物品名'),
1496
+ fileName: zod_1.z.string().describe('Reference image file name'),
1497
+ type: zod_1.z
1498
+ .enum(['reference', 'storyboard', 'first_frame', 'last_frame'])
1499
+ .default('reference')
1500
+ .describe(`Type of reference:
1501
+ - reference 图片是主体参考图,当用户说参考xx图时,默认用这个类型
1502
+ - storyboard 图片是镜头宫格参考图,当用户说参考xx分镜宫格时,用这个类型
1503
+ - first_frame 图片是首帧图,当用户强调图生视频,或者以xxx为首帧时,用这个类型
1504
+ - last_frame 图片是尾帧图,当用户说以xxx为尾帧时,用这个类型
1505
+ `),
1506
+ }))
1641
1507
  .optional()
1642
- .describe('The image file name of the first frame of the video.'),
1508
+ .default([])
1509
+ .describe('Array of reference image objects with name, url and type. Can be empty for text-only generation.'),
1510
+ videos: zod_1.z
1511
+ .array(zod_1.z.object({
1512
+ name: zod_1.z.string().optional().describe('参考视频名称'),
1513
+ fileName: zod_1.z.string().describe('Reference video file name'),
1514
+ type: zod_1.z.enum(['base', 'reference']).describe(`Type of reference:
1515
+ - base 是用于编辑的原视频
1516
+ - ref 是用作参考镜头或主体的视频
1517
+ `),
1518
+ }))
1519
+ .optional()
1520
+ .default([])
1521
+ .describe('Array of reference video objects with name, url and type. Can be empty for text-only generation.'),
1522
+ audios: zod_1.z
1523
+ .array(zod_1.z.object({
1524
+ name: zod_1.z.string().optional().describe('参考音频名称'),
1525
+ fileName: zod_1.z.string().describe('Reference audio file name'),
1526
+ type: zod_1.z.enum(['speaker', 'lipsync', 'sound', 'bgm']).describe(`Type of reference:
1527
+ - speaker 用于角色参考音色
1528
+ - lipsync 将音频与视频对口型
1529
+ - sound 合成背景音效、旁白等
1530
+ - bgm 合成背景音乐
1531
+ `),
1532
+ }))
1533
+ .optional()
1534
+ .default([])
1535
+ .describe('Array of reference audio objects with name, url and type. Can be empty for text-only generation.'),
1643
1536
  duration: zod_1.z
1644
1537
  .number()
1645
1538
  .min(0)
@@ -1647,16 +1540,12 @@ server.registerTool('generate-video', {
1647
1540
  .optional()
1648
1541
  .default(0)
1649
1542
  .describe('若传0,表示根据视频提示词内容自动确定时长,当用户未给出明确时长指令时,优先传0'),
1650
- end_frame: zod_1.z
1651
- .string()
1652
- .optional()
1653
- .describe('The image file name of the last frame of the video.'),
1654
- saveLastFrameAs: zod_1.z
1655
- .string()
1543
+ aspectRatio: zod_1.z
1544
+ .enum(['16:9', '9:16', '1:1'])
1656
1545
  .optional()
1657
- .describe('The filename to save the last frame. 只有用户不传end_frame且需要该帧延续下一个分镜时,才需要这个参数,默认不传'),
1546
+ .describe('若使用首、尾帧,则忽略该参数(自动根据图片适配)'),
1658
1547
  resolution: zod_1.z
1659
- .enum(['720p', '1080p'])
1548
+ .enum(['720p', '1080p', '2K', '4K'])
1660
1549
  .optional()
1661
1550
  .default('720p')
1662
1551
  .describe('除非用户明确提出使用其他分辨率,否则一律用720p'),
@@ -1666,14 +1555,13 @@ server.registerTool('generate-video', {
1666
1555
  .default(true)
1667
1556
  .describe('默认true,自动规范化镜头语言'),
1668
1557
  },
1669
- }, async ({ prompt, narration, sceneIndex, saveToFileName, start_frame, end_frame, duration, resolution, type = 'vidu', optimizeCameraMotion, saveLastFrameAs, audio = true, bgm, seed, }, context) => {
1558
+ }, async ({ prompt, narration, sceneIndex, saveToFileName, images = [], videos = [], audios = [], duration, resolution, aspectRatio, model = 'vidu', optimizeCameraMotion, bgm, silent = false, seed, }, context) => {
1670
1559
  try {
1671
1560
  // 验证session状态
1672
1561
  const currentSession = await validateSession('generate-video');
1673
- checkModelEnabled(type);
1674
- if (!start_frame) {
1675
- return createErrorResponse('start_frame 不能为空', 'generate-video');
1676
- }
1562
+ checkModelEnabled(model);
1563
+ if (model === 'pro')
1564
+ model = 'seedance-1.5-pro';
1677
1565
  const storyBoardFile = 'storyboard.json';
1678
1566
  // 校验 prompt 与 storyboard.json 中分镜设定的一致性
1679
1567
  if (sceneIndex) {
@@ -1696,24 +1584,37 @@ server.registerTool('generate-video', {
1696
1584
  if (videoPrompt && prompt.trim() !== videoPrompt?.trim()) {
1697
1585
  return createErrorResponse('视频提示词必须严格遵照storyboard的设定,内容完全一致,包括空格、回车、标点和特殊符号,检查传入的prompt参数与storyboard中的video_prompt是否完全一致', 'generate-video');
1698
1586
  }
1699
- if (scene.is_continuous && !end_frame) {
1700
- return createErrorResponse('连续分镜必须指定end_frame参数', 'generate-video');
1701
- }
1702
1587
  if (scene.video_duration != null &&
1703
1588
  duration !== scene.video_duration) {
1704
1589
  return createErrorResponse(`视频时长必须严格遵照storyboard的设定,storyboard 中设定的时长为 ${scene.video_duration} 秒${scene.video_duration === 0 ? '。0秒表示根据视频提示词内容自动确定时长' : ''}`, 'generate-video');
1705
1590
  }
1706
1591
  if (storyBoard.voice_type &&
1707
1592
  storyBoard.voice_type !== 'slient') {
1708
- if (!audio) {
1709
- return createErrorResponse('有对话和旁白的分镜不能静音,请将audio设为true再重新使用工具。', 'generate-video');
1593
+ if (silent) {
1594
+ return createErrorResponse('有对话和旁白的分镜不能静音,请将silent设为false再重新使用工具。', 'generate-video');
1595
+ }
1596
+ }
1597
+ if (storyBoard.narration && !narration) {
1598
+ return createErrorResponse('有对话和旁白的分镜必须传入旁白参数 narration。', 'generate-video');
1599
+ }
1600
+ // 检查 storyBoard.orientation 与 size 参数的一致性
1601
+ if (storyBoard.orientation && aspectRatio) {
1602
+ const isLandscapeSize = aspectRatio === '16:9';
1603
+ const isPortraitSize = aspectRatio === '9:16';
1604
+ if (storyBoard.orientation === 'landscape' &&
1605
+ !isLandscapeSize) {
1606
+ return createErrorResponse(`故事板设定为横屏模式(orientation: landscape),但视频为竖屏格式,请使用横屏尺寸 1280x720`, 'generate-video');
1607
+ }
1608
+ if (storyBoard.orientation === 'portrait' &&
1609
+ !isPortraitSize) {
1610
+ return createErrorResponse(`故事板设定为竖屏模式(orientation: portrait),但视频为横屏格式,请使用竖屏尺寸 720x1280`, 'generate-video');
1710
1611
  }
1711
1612
  }
1712
1613
  // 检查 use_video_model 与 type 参数的一致性
1713
1614
  if (scene.use_video_model &&
1714
- type &&
1715
- scene.use_video_model !== type) {
1716
- return createErrorResponse(`分镜建议的视频模型(${scene.use_video_model})与传入的type参数(${type})不一致。请确保use_video_model与type参数值相同。`, 'generate-video');
1615
+ model &&
1616
+ scene.use_video_model !== model) {
1617
+ return createErrorResponse(`分镜建议的视频模型(${scene.use_video_model})与传入的模型参数(${model})不一致。请确保use_video_model与type参数值相同。`, 'generate-video');
1717
1618
  }
1718
1619
  }
1719
1620
  else {
@@ -1733,232 +1634,71 @@ server.registerTool('generate-video', {
1733
1634
  const validatedFileName = validateFileName(saveToFileName);
1734
1635
  console.log(`Generating video with prompt: ${prompt.substring(0, 100)}...`);
1735
1636
  const ai = currentSession.ai;
1637
+ let narrationVoice;
1638
+ if (narration) {
1639
+ if (model === 'seedance-1.5-pro' ||
1640
+ model === 'wan-flash' ||
1641
+ (model?.startsWith('vidu') && !model?.startsWith('viduq3'))) {
1642
+ narrationVoice = await ai.textToSpeech({
1643
+ text: narration.text,
1644
+ voiceId: narration.voice_id,
1645
+ prompt: narration.tone,
1646
+ });
1647
+ const voiceDuration = narrationVoice.duration;
1648
+ if (duration === 0) {
1649
+ duration = Math.ceil(voiceDuration + 0.05);
1650
+ }
1651
+ }
1652
+ else {
1653
+ prompt = `画外音(${narration.tone}):“${narration.text}”\n\n${prompt}`;
1654
+ }
1655
+ }
1736
1656
  let progress = 0;
1737
- const startFrameUri = start_frame
1738
- ? await getMaterialUri(currentSession, start_frame, {
1739
- fileSizeLimit: 10,
1740
- })
1741
- : undefined;
1742
- const endFrameUri = end_frame
1743
- ? await getMaterialUri(currentSession, end_frame, {
1657
+ const referenceImages = await Promise.all(images.map(async (item) => {
1658
+ const url = await getMaterialUri(currentSession, item.fileName, {
1744
1659
  fileSizeLimit: 10,
1745
- })
1746
- : undefined;
1747
- if (type === 'kenburns') {
1748
- // 实现 getImageSize 函数
1749
- async function getImageSize(imageUri) {
1750
- // imageUri 是一个可以 http 访问的图片
1751
- // eslint-disable-next-line custom/no-fetch-in-src
1752
- const response = await fetch(imageUri);
1753
- const arrayBuffer = await response.arrayBuffer();
1754
- const buffer = Buffer.from(arrayBuffer);
1755
- const image = (0, image_size_1.default)(buffer);
1756
- return [image.width, image.height];
1757
- }
1758
- const [imageWidth, imageHeight] = await getImageSize(startFrameUri);
1759
- const kenburnsPrompt = `根据用户描述和配置,生成适合的 kenburns 动画参数。
1760
-
1761
- ## 配置信息
1762
- - 视频时长:${duration}秒
1763
- - 分辨率:${resolution}
1764
- - 保存最后一帧:${saveLastFrameAs ? '是' : '否'}
1765
- - 图片宽高:${imageWidth}x${imageHeight}
1766
- - 上一次选择的特效:${lastEffect || '无'}
1767
-
1768
- ## camera_motion 可选特效
1769
- - zoom_in
1770
- - zoom_out
1771
- - zoom_in_left_top
1772
- - zoom_out_left_top
1773
- - zoom_in_right_top
1774
- - zoom_out_right_top
1775
- - zoom_in_left_bottom
1776
- - zoom_out_left_bottom
1777
- - zoom_in_right_bottom
1778
- - zoom_out_right_bottom
1779
- - pan_up
1780
- - pan_down
1781
- - pan_left
1782
- - pan_right
1783
- - static
1784
-
1785
- ## 特效选用规则
1786
- - 视频时长6秒内:优先选择zoom类型(zoom_in, zoom_out)
1787
- - 视频时长6秒以上:优先选择pan类型(pan_left, pan_right, pan_up, pan_down)
1788
- - 除非用户指定,否则不要主动使用 static 效果
1789
- - 除非用户指定,否则不要和上一次选择的效果重复
1790
- `;
1791
- const schema = {
1792
- name: 'kenburns_effect',
1793
- schema: {
1794
- type: 'object',
1795
- properties: {
1796
- camera_motion: {
1797
- type: 'string',
1798
- description: 'kenburns 特效类型',
1799
- enum: [
1800
- 'zoom_in',
1801
- 'zoom_out',
1802
- 'zoom_in_left_top',
1803
- 'zoom_out_left_top',
1804
- 'zoom_in_right_top',
1805
- 'zoom_out_right_top',
1806
- 'zoom_in_left_bottom',
1807
- 'zoom_out_left_bottom',
1808
- 'zoom_in_right_bottom',
1809
- 'zoom_out_right_bottom',
1810
- 'pan_up',
1811
- 'pan_down',
1812
- 'pan_left',
1813
- 'pan_right',
1814
- 'static',
1815
- ],
1816
- },
1817
- size: {
1818
- type: 'string',
1819
- description: 'kenburns 视频 宽x高',
1820
- },
1821
- duration: {
1822
- type: 'number',
1823
- description: 'kenburns 视频 时长',
1824
- },
1825
- },
1826
- required: ['camera_motion', 'size', 'duration'],
1827
- },
1660
+ });
1661
+ return {
1662
+ name: item.name,
1663
+ url,
1664
+ type: item.type,
1828
1665
  };
1829
- const analysisPayload = {
1830
- model: 'Doubao-Seed-1.8',
1831
- messages: [
1832
- {
1833
- role: 'system',
1834
- content: kenburnsPrompt,
1835
- },
1836
- {
1837
- role: 'user',
1838
- content: prompt.trim(),
1839
- },
1840
- ],
1841
- response_format: {
1842
- type: 'json_schema',
1843
- json_schema: schema,
1844
- },
1666
+ }));
1667
+ const referenceVideos = await Promise.all(videos.map(async (item) => {
1668
+ const url = await getMaterialUri(currentSession, item.fileName, {
1669
+ fileSizeLimit: 50,
1670
+ });
1671
+ return {
1672
+ name: item.name,
1673
+ url,
1674
+ type: item.type,
1845
1675
  };
1846
- console.log(JSON.stringify(analysisPayload, null, 2));
1847
- const completion = await ai.getCompletions(analysisPayload);
1848
- const analysisResult = completion.choices[0]?.message?.content;
1849
- if (!analysisResult) {
1850
- throw new Error('Failed to generate kenburns effect');
1851
- }
1852
- try {
1853
- const kenburnsEffect = JSON.parse(analysisResult);
1854
- const { camera_motion, size, duration } = kenburnsEffect;
1855
- if (!camera_motion || !size || !duration) {
1856
- throw new Error('Invalid kenburns effect parameters');
1857
- }
1858
- lastEffect = camera_motion;
1859
- const files = currentSession.files;
1860
- const terminal = currentSession.terminal;
1861
- await getMaterialUri(currentSession, start_frame, {
1862
- fileSizeLimit: 10,
1863
- }); // 确保素材上传
1864
- start_frame = (0, node_path_1.resolve)('/home/user/cerevox-zerocut/projects', terminal.id, 'materials', (0, node_path_1.basename)(start_frame));
1865
- const saveToPath = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials/${validatedFileName}`;
1866
- const saveLocalPath = (0, node_path_1.resolve)(projectLocalDir, 'materials', validatedFileName);
1867
- // 解析尺寸参数
1868
- const sizeArray = size.split('x');
1869
- if (sizeArray.length !== 2) {
1870
- throw new Error(`Invalid size format: ${size}. Expected format: WIDTHxHEIGHT`);
1871
- }
1872
- const [widthStr, heightStr] = sizeArray;
1873
- const width = Number(widthStr);
1874
- const height = Number(heightStr);
1875
- if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
1876
- throw new Error(`Invalid dimensions: ${width}x${height}. Both width and height must be positive numbers`);
1877
- }
1878
- console.log(`Compiling Ken Burns motion command...`);
1879
- let command;
1880
- try {
1881
- command = await (0, videokit_1.compileKenBurnsMotion)(start_frame, duration, camera_motion, {
1882
- output: saveToPath,
1883
- width,
1884
- height,
1885
- });
1886
- }
1887
- catch (compileError) {
1888
- console.error('Failed to compile Ken Burns motion command:', compileError);
1889
- throw new Error(`Failed to compile Ken Burns motion: ${compileError}`);
1890
- }
1891
- console.log(`Executing FFmpeg command: ${command.substring(0, 100)}...`);
1892
- const res = await terminal.run(command);
1893
- const result = await res.json();
1894
- if (result.exitCode !== 0) {
1895
- console.error('FFmpeg command failed:', result);
1896
- return {
1897
- content: [
1898
- {
1899
- type: 'text',
1900
- text: JSON.stringify({
1901
- success: false,
1902
- error: 'Ken Burns motion generation failed',
1903
- exitCode: result.exitCode,
1904
- stderr: result.stderr,
1905
- command,
1906
- timestamp: new Date().toISOString(),
1907
- }),
1908
- },
1909
- ],
1910
- };
1911
- }
1912
- console.log('Ken Burns motion generated successfully, downloading file...');
1913
- try {
1914
- await files.download(saveToPath, saveLocalPath);
1915
- }
1916
- catch (downloadError) {
1917
- console.warn('Failed to download file to local:', downloadError);
1918
- // 继续执行,因为远程文件已生成成功
1919
- }
1920
- const resultData = {
1921
- success: true,
1922
- uri: saveToPath,
1923
- durationMs: Math.floor(duration * 1000),
1924
- cameraMotion: camera_motion,
1925
- imagePath: start_frame,
1926
- size,
1927
- dimensions: { width, height },
1928
- timestamp: new Date().toISOString(),
1929
- systemPrompt: kenburnsPrompt,
1930
- };
1931
- console.log(`Ken Burns motion completed: ${saveToPath}`);
1932
- currentSession.track('KenBurns Video Generated', {
1933
- durationMs: Math.floor(duration * 1000).toString(),
1934
- cameraMotion: camera_motion,
1935
- imagePath: start_frame,
1936
- size,
1937
- dimensions: size,
1938
- });
1939
- return {
1940
- content: [
1941
- {
1942
- type: 'text',
1943
- text: JSON.stringify(resultData),
1944
- },
1945
- ],
1946
- };
1947
- }
1948
- catch (error) {
1949
- throw new Error('Failed to parse kenburns effect parameters');
1950
- }
1676
+ }));
1677
+ const referenceAudios = await Promise.all(audios.map(async (item) => {
1678
+ const url = await getMaterialUri(currentSession, item.fileName);
1679
+ return {
1680
+ name: item.name,
1681
+ url,
1682
+ type: item.type,
1683
+ };
1684
+ }));
1685
+ if (narration) {
1686
+ referenceAudios.push({
1687
+ name: narration?.voice_id,
1688
+ url: narrationVoice.url,
1689
+ type: 'sound',
1690
+ });
1951
1691
  }
1952
- const res = await ai.framesToVideo({
1692
+ const res = await ai.generateVideo({
1953
1693
  prompt: prompt.trim(),
1954
- narration,
1955
- start_frame: startFrameUri,
1956
- end_frame: endFrameUri,
1694
+ model,
1695
+ images: referenceImages,
1696
+ videos: referenceVideos,
1697
+ audios: referenceAudios,
1957
1698
  duration,
1958
1699
  resolution,
1959
- type,
1960
- return_last_frame: Boolean(saveLastFrameAs),
1961
- optimizeCameraMotion,
1700
+ aspect_ratio: aspectRatio,
1701
+ optimize_camera: optimizeCameraMotion,
1962
1702
  onProgress: async (metaData) => {
1963
1703
  try {
1964
1704
  await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
@@ -1967,10 +1707,10 @@ server.registerTool('generate-video', {
1967
1707
  console.warn('Failed to send progress update:', progressError);
1968
1708
  }
1969
1709
  },
1970
- waitForFinish: CLIENT_TYPE !== '5ire',
1971
- mute: !audio,
1972
- enableBGM: bgm,
1710
+ mute: silent,
1711
+ bgm,
1973
1712
  seed,
1713
+ timeout: MCP_TIMEOUT_MS,
1974
1714
  });
1975
1715
  if (!res) {
1976
1716
  throw new Error('Failed to generate video: no response from AI service');
@@ -1978,21 +1718,14 @@ server.registerTool('generate-video', {
1978
1718
  if (res.url) {
1979
1719
  console.log('Video generated successfully, saving to materials...');
1980
1720
  const video = await saveMaterial(currentSession, res.url, validatedFileName);
1981
- let lastFrameImage;
1982
- if (saveLastFrameAs && res.last_frame_url) {
1983
- lastFrameImage = await saveMaterial(currentSession, res.last_frame_url, validateFileName(saveLastFrameAs));
1984
- }
1985
1721
  const { url, duration: videoDuration, ...opts } = res;
1986
1722
  const result = {
1987
1723
  success: true,
1988
1724
  // source: url,
1989
1725
  video,
1990
- lastFrameImage,
1991
1726
  durationMs: Math.floor((videoDuration || duration) * 1000),
1992
1727
  prompt,
1993
1728
  timestamp: new Date().toISOString(),
1994
- startFrame: start_frame,
1995
- endFrame: end_frame,
1996
1729
  ...opts,
1997
1730
  };
1998
1731
  // Update media_logs.json
@@ -2035,7 +1768,6 @@ server.registerTool('generate-video', {
2035
1768
  success: false,
2036
1769
  error: 'No video URL returned from AI service',
2037
1770
  response: res,
2038
- startFrameUri,
2039
1771
  timestamp: new Date().toISOString(),
2040
1772
  }),
2041
1773
  },
@@ -2047,515 +1779,6 @@ server.registerTool('generate-video', {
2047
1779
  return createErrorResponse(error, 'generate-video');
2048
1780
  }
2049
1781
  });
2050
- server.registerTool('generate-video-by-ref', {
2051
- title: 'Generate Video By Text or References',
2052
- description: `参考生视频(包含文生视频)工具`,
2053
- inputSchema: {
2054
- prompt: zod_1.z
2055
- .string()
2056
- .describe('The prompt to generate video with or without reference images. '),
2057
- referenceImages: zod_1.z
2058
- .array(zod_1.z.object({
2059
- name: zod_1.z.string().describe('图片名称,人物名、图片背景名或物品名'),
2060
- fileName: zod_1.z.string().describe('Reference image file name'),
2061
- type: zod_1.z
2062
- .enum(['subject', 'background', 'storyboard'])
2063
- .describe('Type of reference: subject是主体,background是背景,storyboard是镜头宫格'),
2064
- }))
2065
- .optional()
2066
- .default([])
2067
- .describe('Array of reference image objects with name, url and type. Can be empty for text-only generation.'),
2068
- referenceVideo: zod_1.z
2069
- .object({
2070
- name: zod_1.z
2071
- .string()
2072
- .describe('Reference video file name in materials directory'),
2073
- fileName: zod_1.z.string().describe('Reference video file name'),
2074
- type: zod_1.z.enum(['video']).describe('Type of reference: video'),
2075
- })
2076
- .optional()
2077
- .describe('Reference video file name in materials directory, 用于参考视频动作和特效,只有vidu-pro和kling-v3模型支持'),
2078
- duration: zod_1.z
2079
- .number()
2080
- .min(0)
2081
- .max(16)
2082
- .optional()
2083
- .default(0)
2084
- .describe('若传0,表示根据视频提示词内容自动确定时长,当用户未给出明确时长指令时,优先传0'),
2085
- aspectRatio: zod_1.z.enum(['16:9', '9:16', '1:1']).default('16:9'),
2086
- resolution: zod_1.z.enum(['720p', '1080p']).default('720p'),
2087
- type: zod_1.z
2088
- .enum([
2089
- 'pro',
2090
- 'sora2',
2091
- 'sora2-pro',
2092
- 'veo3.1',
2093
- 'veo3.1-pro',
2094
- 'vidu',
2095
- 'viduq2',
2096
- 'vidu-pro',
2097
- 'viduq2-pro',
2098
- 'viduq3',
2099
- 'viduq3-turbo',
2100
- 'wan',
2101
- 'kling',
2102
- 'kling-v3',
2103
- 'zerocut3.0',
2104
- 'pixv',
2105
- ])
2106
- .default(MAIN_PROVIDER === 'aliyun' ? 'wan' : 'vidu'),
2107
- audio: zod_1.z
2108
- .boolean()
2109
- .optional()
2110
- .default(true)
2111
- .describe('除非用户明确要求静音,否则默认audio=true,此时视频模型会自己配音,不需要再使用tts工具!'),
2112
- bgm: zod_1.z.boolean().optional().describe('是否启用背景音乐,缺省的话是自动'),
2113
- style: zod_1.z.string().optional().describe('画面及美学风格'),
2114
- seed: zod_1.z
2115
- .number()
2116
- .optional()
2117
- .describe('用户可以指定一个固定值来获得可重复的结果'),
2118
- saveToFileName: zod_1.z.string().describe('应该是mp4文件'),
2119
- sceneIndex: zod_1.z
2120
- .number()
2121
- .min(1)
2122
- .optional()
2123
- .describe('分镜索引,从1开始的下标,如果非分镜对应素材,则可不传,分镜素材必传'),
2124
- optimizeCameraMotion: zod_1.z
2125
- .boolean()
2126
- .optional()
2127
- .default(true)
2128
- .describe('默认true,自动规范化镜头语言'),
2129
- autoStoryboard: zod_1.z
2130
- .boolean()
2131
- .optional()
2132
- .describe('自动生成分镜宫格(智能分镜),除非用户指定,否则不传该参数,缺省为根据用户需要自动判断'),
2133
- storyboardPanelCount: zod_1.z
2134
- .enum(['4', '6', '9'])
2135
- .optional()
2136
- .describe('智能分镜时的分镜宫格数量,除非用户明确指定,否则缺省,缺省将根据视频时长智能判断'),
2137
- },
2138
- }, async ({ prompt, referenceImages, duration, aspectRatio, resolution, type = 'vidu', audio = true, bgm, saveToFileName, sceneIndex, optimizeCameraMotion, seed, referenceVideo, autoStoryboard, storyboardPanelCount, }, context) => {
2139
- try {
2140
- const storyBoardFile = 'storyboard.json';
2141
- // 验证session状态
2142
- const currentSession = await validateSession('generate-video-by-ref');
2143
- checkModelEnabled(type);
2144
- const storyBoardPath = (0, node_path_1.resolve)(projectLocalDir, storyBoardFile);
2145
- if (type !== 'vidu-pro' &&
2146
- type !== 'viduq2-pro' &&
2147
- type !== 'kling-v3' &&
2148
- referenceVideo) {
2149
- return createErrorResponse('只有vidu-pro和kling-v3模型支持参考视频', 'generate-video-by-ref');
2150
- }
2151
- const outlineSheetImagePath = (0, node_path_1.resolve)(projectLocalDir, 'materials', 'outline_sheet.png');
2152
- const hasOutlineSheet = (0, node_fs_1.existsSync)(outlineSheetImagePath);
2153
- // 校验 prompt 与 storyboard.json 中分镜设定的一致性(如果提供了 sceneIndex)
2154
- if (sceneIndex) {
2155
- try {
2156
- if (hasOutlineSheet) {
2157
- return createErrorResponse('监测到素材中存在outline_sheet.png这张图(由outline工具生成的),应采用 generate-video 图生视频。', 'generate-video-by-ref');
2158
- }
2159
- if ((0, node_fs_1.existsSync)(storyBoardPath)) {
2160
- const storyBoardContent = await (0, promises_1.readFile)(storyBoardPath, 'utf8');
2161
- // 检查 storyBoard JSON 语法合法性
2162
- let storyBoard;
2163
- try {
2164
- storyBoard = JSON.parse(storyBoardContent);
2165
- }
2166
- catch (jsonError) {
2167
- return createErrorResponse(`storyBoard 文件 ${storyBoardFile} 存在 JSON 语法错误,请修复后重试。错误详情: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`, 'generate-video-by-ref');
2168
- }
2169
- if (storyBoard.scenes && Array.isArray(storyBoard.scenes)) {
2170
- const scene = storyBoard.scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
2171
- if (scene) {
2172
- const videoPrompt = scene.video_prompt;
2173
- if (videoPrompt && prompt.trim() !== videoPrompt?.trim()) {
2174
- return createErrorResponse('视频提示词必须严格遵照storyboard的设定,内容完全一致,包括空格、回车、标点和特殊符号,检查传入的prompt参数与storyboard中的video_prompt是否完全一致', 'generate-video-by-ref');
2175
- }
2176
- // 检查 scene.is_continuous 是否为 true
2177
- if (scene.is_continuous === true) {
2178
- return createErrorResponse('连续镜头应使用首尾帧,请修改连续镜头设置,或将本分镜改为首尾帧方式实现', 'generate-video-by-ref');
2179
- }
2180
- if (scene.video_type !== 'references') {
2181
- return createErrorResponse(`分镜 ${sceneIndex} 中的 video_type (${scene.video_type}) 未设置为 'references',不应当使用参考生视频,请使用图生视频 generate-video 方式生成`, 'generate-video-by-ref');
2182
- }
2183
- // 检查 use_video_model 与 type 参数的一致性
2184
- if (scene.use_video_model &&
2185
- type &&
2186
- scene.use_video_model !== type) {
2187
- return createErrorResponse(`分镜 ${sceneIndex} 中的 use_video_model (${scene.use_video_model}) 必须与 type 参数 (${type}) 保持一致`, 'generate-video-by-ref');
2188
- }
2189
- // 检查 scene.references 的一致性
2190
- if (scene.references &&
2191
- Array.isArray(scene.references) &&
2192
- scene.references.length > 0) {
2193
- // 获取 main_characters 和 reference_objects
2194
- const mainCharacters = storyBoard.main_characters || [];
2195
- const referenceObjects = storyBoard.reference_objects || [];
2196
- for (const referenceName of scene.references) {
2197
- let found = false;
2198
- let requiredImage = '';
2199
- // 检查是否在 main_characters 中
2200
- const character = mainCharacters.find((char) => char.name === referenceName);
2201
- if (character) {
2202
- if (!character.reference_image) {
2203
- return createErrorResponse(`分镜 ${sceneIndex} 中引用的角色 "${referenceName}" 在 main_characters 中没有 reference_image 属性`, 'generate-video-by-ref');
2204
- }
2205
- requiredImage = character.reference_image;
2206
- found = true;
2207
- }
2208
- // 检查是否在 reference_objects 中
2209
- if (!found) {
2210
- const refObject = referenceObjects.find((obj) => obj.name === referenceName);
2211
- if (refObject) {
2212
- if (!refObject.image) {
2213
- return createErrorResponse(`分镜 ${sceneIndex} 中引用的物品/背景 "${referenceName}" 在 reference_objects 中没有 image 属性`, 'generate-video-by-ref');
2214
- }
2215
- requiredImage = refObject.image;
2216
- found = true;
2217
- }
2218
- }
2219
- // 如果既不在 main_characters 也不在 reference_objects 中
2220
- if (!found) {
2221
- return createErrorResponse(`分镜 ${sceneIndex} 中引用的 "${referenceName}" 在 main_characters 或 reference_objects 中都找不到对应的定义`, 'generate-video-by-ref');
2222
- }
2223
- // 检查对应的图片是否在 referenceImages 参数中
2224
- if (!referenceImages.some(ref => ref.fileName === requiredImage)) {
2225
- return createErrorResponse(`分镜 ${sceneIndex} 中引用的 "${referenceName}" 对应的图片 "${requiredImage}" 不在 referenceImages 参数中,请确保传入正确的参考图片`, 'generate-video-by-ref');
2226
- }
2227
- }
2228
- }
2229
- // 检查 storyBoard.orientation 与 size 参数的一致性
2230
- if (storyBoard.orientation) {
2231
- const isLandscapeSize = aspectRatio === '16:9';
2232
- const isPortraitSize = aspectRatio === '9:16';
2233
- if (storyBoard.orientation === 'landscape' &&
2234
- !isLandscapeSize) {
2235
- return createErrorResponse(`故事板设定为横屏模式(orientation: landscape),但视频为竖屏格式,请使用横屏尺寸 1280x720`, 'generate-video-by-ref');
2236
- }
2237
- if (storyBoard.orientation === 'portrait' &&
2238
- !isPortraitSize) {
2239
- return createErrorResponse(`故事板设定为竖屏模式(orientation: portrait),但视频为横屏格式,请使用竖屏尺寸 720x1280`, 'generate-video-by-ref');
2240
- }
2241
- }
2242
- }
2243
- else {
2244
- console.warn(`Scene index ${sceneIndex} not found in storyboard.json`);
2245
- }
2246
- }
2247
- }
2248
- else {
2249
- console.warn(`Story board file not found: ${storyBoardPath}`);
2250
- }
2251
- }
2252
- catch (error) {
2253
- console.error('Failed to validate prompt with story board:', error);
2254
- // 如果读取或解析 storyboard.json 失败,继续执行但记录警告
2255
- }
2256
- }
2257
- const validatedFileName = validateFileName(saveToFileName);
2258
- // 验证参考图数量限制
2259
- if ((type === 'sora2' ||
2260
- type === 'sora2-pro' ||
2261
- type === 'veo3.1' ||
2262
- type === 'veo3.1-pro') &&
2263
- referenceImages.length > 4) {
2264
- return createErrorResponse(`${type} model only supports maximum 4 reference image`, 'generate-video-by-ref');
2265
- }
2266
- if (referenceImages.length > 7) {
2267
- return createErrorResponse('Only support maximum 7 reference images', 'generate-video-by-ref');
2268
- }
2269
- console.log(`Generating video ${referenceImages.length > 0 ? `with ${referenceImages.length} reference image(s)` : 'without reference images'} using ${type} model...`);
2270
- const task = referenceImages.map(async (imageRef) => {
2271
- const imageUrl = await getMaterialUri(currentSession, imageRef.fileName, {
2272
- fileSizeLimit: 10,
2273
- });
2274
- return {
2275
- type: imageRef.type,
2276
- name: imageRef.name,
2277
- url: imageUrl,
2278
- };
2279
- });
2280
- // 处理参考图:转换为URL而不是base64
2281
- const referenceImageUrls = await Promise.all(task);
2282
- // for (const imageRef of referenceImages) {
2283
- // // 使用 getMaterialUri 获取图片URL
2284
- // const imageUrl = await getMaterialUri(
2285
- // currentSession,
2286
- // imageRef.fileName,
2287
- // {
2288
- // fileSizeLimit: 10,
2289
- // }
2290
- // );
2291
- // referenceImageUrls.push({
2292
- // type: imageRef.type,
2293
- // name: imageRef.name,
2294
- // url: imageUrl,
2295
- // });
2296
- // console.log(
2297
- // `Added reference image URL: ${imageUrl} (name: ${imageRef.name}, type: ${imageRef.type})`
2298
- // );
2299
- // }
2300
- const finalPrompt = `${prompt}`;
2301
- const videos = referenceVideo
2302
- ? [
2303
- {
2304
- type: 'video',
2305
- name: referenceVideo.name,
2306
- url: await getMaterialUri(currentSession, referenceVideo.fileName, {
2307
- fileSizeLimit: 50,
2308
- }),
2309
- },
2310
- ]
2311
- : undefined;
2312
- // 调用 referencesToVideo 函数
2313
- let progress = 0;
2314
- const result = await currentSession.ai.referencesToVideo({
2315
- prompt: finalPrompt,
2316
- reference_images: referenceImageUrls, // 使用URL数组而不是base64数组
2317
- duration,
2318
- aspect_ratio: aspectRatio,
2319
- resolution,
2320
- type,
2321
- mute: !audio,
2322
- enableBGM: bgm,
2323
- seed,
2324
- videos,
2325
- optimizeCameraMotion,
2326
- autoStoryboard,
2327
- storyboardPanelCount: storyboardPanelCount
2328
- ? Number(storyboardPanelCount)
2329
- : undefined,
2330
- onProgress: metaData => {
2331
- console.log('Video generation progress:', metaData);
2332
- if (metaData.storyboard) {
2333
- const url = metaData.storyboard.url;
2334
- // 将它用 saveToFileName 的 basename + .storyboard.png 保存到本地
2335
- const storyboardFileName = `${saveToFileName.split('.')[0]}-storyboard.png`;
2336
- saveMaterial(currentSession, url, storyboardFileName);
2337
- }
2338
- sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
2339
- },
2340
- waitForFinish: CLIENT_TYPE !== '5ire',
2341
- });
2342
- if (result.error) {
2343
- return createErrorResponse(result.error, 'generate-video-by-ref');
2344
- }
2345
- if (!result.url && !result.taskUrl) {
2346
- return createErrorResponse('Video generation failed: no video URL returned', 'generate-video-by-ref');
2347
- }
2348
- else if (result.taskUrl) {
2349
- return {
2350
- content: [
2351
- {
2352
- type: 'text',
2353
- text: JSON.stringify({
2354
- success: true,
2355
- message: '该视频生成任务正在运行中,它是异步任务,且执行时间较长,你应立即调用工具 wait-for-task-finish 来等待任务结束,如如 wait-for-task-finish 工具调用超时,你应立即再次重新调用直到任务结束。',
2356
- taskUrl: result.taskUrl,
2357
- }),
2358
- },
2359
- ],
2360
- };
2361
- }
2362
- // 下载生成的视频
2363
- const video = await saveMaterial(currentSession, result.url, validatedFileName);
2364
- // 更新媒体日志
2365
- await updateMediaLogs(currentSession, validatedFileName, {
2366
- model: type,
2367
- duration: result.duration,
2368
- resolution: result.resolution,
2369
- ratio: result.ratio,
2370
- reference_images_count: referenceImages.length,
2371
- });
2372
- return {
2373
- content: [
2374
- {
2375
- type: 'text',
2376
- text: JSON.stringify({
2377
- success: true,
2378
- message: referenceImages.length > 0
2379
- ? `Video generated successfully using ${referenceImages.length} reference image(s) with ${type} model`
2380
- : `Video generated successfully without reference images using ${type} model`,
2381
- video,
2382
- duration: result.duration,
2383
- resolution: result.resolution,
2384
- ratio: result.ratio,
2385
- url: result.url,
2386
- last_frame_url: result.last_frame_url,
2387
- referenceImageUrls,
2388
- prompt: finalPrompt,
2389
- }, null, 2),
2390
- },
2391
- ],
2392
- };
2393
- }
2394
- catch (error) {
2395
- console.error('Error generating video by reference:', error);
2396
- return createErrorResponse(error.message, 'generate-video-by-ref');
2397
- }
2398
- });
2399
- server.registerTool('edit-video', {
2400
- title: 'Edit Video',
2401
- description: `根据不同type修改视频,包括编辑、对口型、延长、超清`,
2402
- inputSchema: {
2403
- type: zod_1.z
2404
- .enum(['edit', 'lipsync', 'extend', 'upscale'])
2405
- .default('edit')
2406
- .describe('The editing type'),
2407
- video: zod_1.z.string().describe(`The video to edit`),
2408
- duration: zod_1.z
2409
- .number()
2410
- .min(0)
2411
- .max(7)
2412
- .optional()
2413
- .describe(`0 表示与原视频相同。编辑视频时如用户未指定,默认为 0。`),
2414
- resolution: zod_1.z
2415
- .enum(['720p', '1080p', '2K', '4K'])
2416
- .optional()
2417
- .describe(`延长视频和超清视频时的分辨率(可选)`),
2418
- prompt: zod_1.z
2419
- .string()
2420
- .optional()
2421
- .describe(`编辑或延长视频的提示词,对口型或超清可忽略`),
2422
- referenceImages: zod_1.z
2423
- .array(zod_1.z.object({
2424
- name: zod_1.z.string().describe('图片名称,人物名、图片背景名或物品名'),
2425
- fileName: zod_1.z.string().describe('Reference image file name'),
2426
- type: zod_1.z.enum(['subject', 'background', 'last_frame']),
2427
- }))
2428
- .optional()
2429
- .describe(`编辑视频时传参考图,延长视频时传尾帧图,其他情况忽略`),
2430
- saveToFileName: zod_1.z.string().describe(`应该是mp4文件`),
2431
- },
2432
- }, async ({ type, video, duration = 0, resolution, prompt, referenceImages, saveToFileName, }, context) => {
2433
- try {
2434
- const currentSession = await validateSession('edit-video');
2435
- const ai = currentSession.ai;
2436
- const validatedFileName = validateFileName(saveToFileName);
2437
- let res = {};
2438
- let progress = 0;
2439
- if (type === 'extend' && (resolution === '2K' || resolution === '4K')) {
2440
- throw new Error('Extend type only supports 720p resolution or 1080p resolution');
2441
- }
2442
- if (type === 'upscale' && resolution === '720p') {
2443
- throw new Error('720p resolution is not supported for upscale type');
2444
- }
2445
- const onProgress = async (metaData) => {
2446
- console.log('Replace video progress:', metaData);
2447
- try {
2448
- await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
2449
- }
2450
- catch (progressError) {
2451
- console.warn('Failed to send progress update:', progressError);
2452
- }
2453
- };
2454
- let referenceImageUrls = undefined;
2455
- if (referenceImages) {
2456
- const tasks = referenceImages.map(async (file) => {
2457
- const url = await getMaterialUri(currentSession, file.fileName, {
2458
- fileSizeLimit: 10,
2459
- });
2460
- return {
2461
- name: file.name,
2462
- url,
2463
- type: file.type,
2464
- };
2465
- });
2466
- referenceImageUrls = await Promise.all(tasks);
2467
- }
2468
- const videoUrl = await getMaterialUri(currentSession, video, {
2469
- fileSizeLimit: 50,
2470
- });
2471
- if (type === 'edit') {
2472
- if (!prompt) {
2473
- throw new Error('prompt is required for edit type');
2474
- }
2475
- res = await ai.editVideo({
2476
- videoUrl,
2477
- prompt,
2478
- referenceImages: referenceImageUrls?.map(item => item.url),
2479
- onProgress,
2480
- waitForFinish: CLIENT_TYPE !== '5ire',
2481
- });
2482
- }
2483
- else if (type === 'lipsync') {
2484
- // 调用AI的lipSync方法,使用处理后的音频
2485
- const { audio: audioUrl } = await ai.splitVideoAndAudio({
2486
- videoUrl,
2487
- });
2488
- res = await ai.lipSync({
2489
- videoUrl,
2490
- audioUrl,
2491
- audioInMs: 0,
2492
- pad_audio: false,
2493
- onProgress,
2494
- waitForFinish: CLIENT_TYPE !== '5ire',
2495
- });
2496
- }
2497
- else if (type === 'extend') {
2498
- res = await ai.extendVideo({
2499
- video_url: videoUrl,
2500
- resolution: resolution || '720p',
2501
- prompt,
2502
- duration: duration || 5,
2503
- end_frame: referenceImageUrls?.[0].url,
2504
- onProgress,
2505
- waitForFinish: CLIENT_TYPE !== '5ire',
2506
- });
2507
- }
2508
- else if (type === 'upscale') {
2509
- res = await ai.upscaleVideo({
2510
- video_url: videoUrl,
2511
- resolution,
2512
- onProgress,
2513
- waitForFinish: CLIENT_TYPE !== '5ire',
2514
- });
2515
- }
2516
- if (res.url) {
2517
- const video = await saveMaterial(currentSession, res.url, validatedFileName);
2518
- const result = {
2519
- success: true,
2520
- // source: url,
2521
- video,
2522
- prompt,
2523
- timestamp: new Date().toISOString(),
2524
- };
2525
- // Update media_logs.json
2526
- try {
2527
- await updateMediaLogs(currentSession, validatedFileName, result);
2528
- }
2529
- catch (error) {
2530
- console.warn(`Failed to update media_logs.json for ${validatedFileName}:`, error);
2531
- }
2532
- return {
2533
- content: [
2534
- {
2535
- type: 'text',
2536
- text: JSON.stringify(result),
2537
- },
2538
- ],
2539
- };
2540
- }
2541
- return {
2542
- content: [
2543
- {
2544
- type: 'text',
2545
- text: JSON.stringify({
2546
- success: false,
2547
- error: 'No video URL returned from AI service',
2548
- response: res,
2549
- timestamp: new Date().toISOString(),
2550
- }),
2551
- },
2552
- ],
2553
- };
2554
- }
2555
- catch (error) {
2556
- return createErrorResponse(error, 'edit-video');
2557
- }
2558
- });
2559
1782
  server.registerTool('media-analyzer', {
2560
1783
  title: 'Media Analyzer',
2561
1784
  description: 'Analyze media content (images, videos, audio) using Doubao-Seed-1.8 model to understand specific details for creative work.',