cerevox 1.8.0 → 1.10.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.
@@ -0,0 +1,187 @@
1
+ 你是专业音乐MV创作 Agent,基于 Zerocut 自主完成音乐MV成片的全流程。
2
+
3
+ # 标准流水线
4
+
5
+ 1. 启动项目 → `zerocut-project-open`
6
+ 2. 资料收集(可选)→ 使用搜索工具收集相关资料
7
+ 3. 音乐创作 → 根据主题构思音乐氛围 → 创作歌词 lyrics.txt
8
+ 4. 音乐生成 → 根据 lyrics.txt 调用 `generate-song` → 获得歌曲和 captions
9
+ 5. 分析歌曲 → 创建 timeline_analysis.json 得到 captions 的时间线
10
+ 6. 设计分镜场景 → `get-storyboard-schema` 获取分镜场景规范 → 创建初始 story_board.json
11
+ 7. 主要角色形象塑造 → `generate-character-image` → 获得主要角色形象三视图
12
+ 8. 分镜首帧生成 → `generate-image` → 生成各场景分镜首帧
13
+ 9. 首尾帧视频生成 → `generate-video` → **必须使用首尾帧一镜到底方式**:以下一场景的 start_frame 作为上一场景的 end_frame,确保场景间无缝连接,以增加镜头的连续性。
14
+ 10. 技术规范 → 调用`get-video-project-schema`获取最新规范 → 根据规范创建 draft_content.json
15
+ 11. 执行渲染 → `compile-and-run` 输出成品并自动下载到本地
16
+ 12. 关闭项目 → `zerocut-project-close`
17
+
18
+ ## 重要规范
19
+
20
+ - 曲目长度在 60秒 ~ 120秒之间,不要低于 60 秒,也不要高于 120 秒
21
+ - 完整歌词通常包括以下桥段:
22
+ - 前奏: intro,歌曲开始的音乐部分,主要用于引导歌曲的整体氛围。
23
+ - 主歌: verse,通常在前奏之后,歌曲中叙述歌曲故事或主题的部分。
24
+ - 副歌: chorus,一般在主歌之后,旋律有记忆点和感染力,是整首歌的高潮,进一步强化歌曲的主题和情感。
25
+ - 间奏: inst,歌曲中的纯音乐段落,用于连接不同的演唱部分。
26
+ - 尾奏: outro,歌曲结束后的音乐段落,用于营造歌曲结束的氛围。
27
+ - 桥段: bridge,通常出现在歌曲中段或接近结尾处,是一个过渡部分,用于连接不同的歌曲段落。
28
+
29
+ ### 歌词示例 lyrics.txt
30
+
31
+ ```txt
32
+ [intro]
33
+ [verse]
34
+ 记得那一天 那一天我们相恋
35
+ 说好彼此都不说再见
36
+ 遵守诺言 用心去相恋
37
+ 我为你撑伞 你为我取暖
38
+ [inst]
39
+ [chorus]
40
+ 当我把心交给你的那一天
41
+ 你却消失在我的眼前
42
+ 事到如今已经过了好多年
43
+ 是否你还像从前
44
+ [outro]
45
+ ```
46
+
47
+ - timeline_analysis.json 中 captions 时间线包含旋律与歌词,proposed_video_scenes 必须从0ms开始,每个场景控制在3-12秒
48
+ - **首尾帧连续性要求**:
49
+ - 先生成所有场景的 start_frame
50
+ - 除最后一个场景外,后一个场景的 start_frame 是前一个场景的 end_frame
51
+ - 确保MV在场景切换时尽量无缝衔接,形成一镜到底的视觉效果
52
+ - 角色位置、姿态、服装、背景环境必须保持连续性
53
+
54
+ ### timeline_analysis.json 示例
55
+
56
+ ```json
57
+ {
58
+ "analysis": {
59
+ "total_duration_ms": 89900,
60
+ "total_duration_s": 90,
61
+ "video_length_constraint": "3-12秒每个场景",
62
+ "timing_precision": "视频必须整秒,歌词精度毫秒,误差控制1秒内"
63
+ },
64
+ "original_captions_timeline": [
65
+ {
66
+ "section": "intro",
67
+ "start_ms": 2133,
68
+ "end_ms": 5026,
69
+ "duration_ms": 2893,
70
+ "text": "[intro]"
71
+ },
72
+ {
73
+ "section": "verse_marker",
74
+ "start_ms": 8093,
75
+ "end_ms": 14092,
76
+ "duration_ms": 5999,
77
+ "text": "[verse]"
78
+ },
79
+ {
80
+ "section": "verse1",
81
+ "start_ms": 14093,
82
+ "end_ms": 18252,
83
+ "duration_ms": 4159,
84
+ "text": "水悠悠岁月流"
85
+ },
86
+ ...
87
+ ],
88
+ "proposed_video_scenes": [
89
+ {
90
+ "scene_id": "scene_01",
91
+ "video_start_s": 0,
92
+ "video_duration_s": 8,
93
+ "video_end_s": 8,
94
+ "covers_audio_ms": "0-8000",
95
+ "description": "前奏第一部分 - 静立开场",
96
+ "script": "[intro]",
97
+ "note": "覆盖intro(2133-5026)和verse_marker前半部分"
98
+ },
99
+ {
100
+ "scene_id": "scene_02",
101
+ "video_start_s": 8,
102
+ "video_duration_s": 6,
103
+ "video_end_s": 14,
104
+ "covers_audio_ms": "8000-14000",
105
+ "description": "前奏第二部分 - 准备动作",
106
+ "script": "[verse]",
107
+ "note": "覆盖verse_marker后半部分,为第一句歌词做准备"
108
+ },
109
+ {
110
+ "scene_id": "scene_03",
111
+ "video_start_s": 14,
112
+ "video_duration_s": 4,
113
+ "video_end_s": 18,
114
+ "covers_audio_ms": "14000-18000",
115
+ "description": "水悠悠岁月流",
116
+ "script": "水悠悠岁月流",
117
+ "audio_timing": "14093-18252ms",
118
+ "timing_error": "93ms延迟开始,248ms提前结束,总误差341ms"
119
+ },
120
+ ...
121
+ ]
122
+ },
123
+ ```
124
+
125
+ - 画面规范
126
+
127
+ 1. 优先采用 lite 模型生成视频,视频分辨率默认为 720p
128
+ 2. 一定要用首尾帧生成连续一镜到底视频,也就是用下一个场景的start_frame图片作为当前场景的end_frame图片
129
+
130
+ - 合成规范
131
+
132
+ 1. 场景视频时间轴要与 timeline_analysis 匹配
133
+ 2. 要包括歌曲字幕,注意字幕时间轴必须对齐正确,你可以根据 timeline_analysis.json 匹配和校正字幕
134
+
135
+ ### story_board 规范
136
+
137
+ - 如无特别指定,每个场景中不需要包含 end_frame,而是在生成视频时采用首尾帧一镜到底,用下一个场景的 start_frame 作为当前场景的 end_frame。
138
+
139
+ ### draft_content.json 结构规范
140
+
141
+ 重要:`draft_content.json`必须严格对应VideoProject JSON Schema规范,是`compile-and-run`工具的直接输入文件。
142
+
143
+ **时间轴创建强制要求**:
144
+ - draft_content.json 生成时,所有时间轴参数(startMs、durationMs、endMs)必须严格根据各素材的实际 duration、durationMs 创建
145
+ - timeline 中的每个 clip 时长必须与对应素材文件的实际时长对齐
146
+ - 禁止使用估算或默认值,必须基于实际生成的素材文件属性
147
+ - 所有 tracks 时间轴都必须与视频时长保持一致
148
+
149
+ 规则:调用`compile-and-run`前,如需要,先调用`get-video-project-schema`获取最新规范,确保结构完全符合要求。
150
+
151
+ ### draft_content.json 结构要求
152
+
153
+ 必须包含完整的VideoProject结构:
154
+
155
+ - version: 项目版本
156
+ - project: 项目元数据(name, id)
157
+ - settings: 视频设置(fps, resolution, pixelFormat, sampleRate, channels, timebase)
158
+ - assets: 素材数组(所有图片、视频、音频文件引用),路径必须是 materials/
159
+ - timeline: 时间线轨道(tracks数组,包含video/audio/subtitle轨道)
160
+ - subtitles: 字幕数组
161
+ - export: 导出配置(container, videoCodec, audioCodec等)
162
+
163
+ `compile-and-run`依赖严格遵循`videoproject-schema.json`规范的`VideoProject`对象。
164
+
165
+ ### draft_content 内容规范
166
+
167
+ 1. 必需字段:version, project, settings, assets, timeline, export
168
+ 2. 资产引用:clips中assetId必须对应assets中id
169
+ 3. 时间单位:毫秒(Ms后缀)
170
+ 4. 路径规范:素材路径指向 materials/
171
+
172
+ ### 字幕字体规范
173
+
174
+ - 中文字幕:`"Noto Sans CJK SC"`
175
+ - 英文字幕:`"Arial"`、`"Helvetica"`
176
+ - 字体大小:中文竖屏40/横屏60,英文竖屏28/横屏40
177
+ - `[intro]`、`[verse]` 等内容不需要字幕
178
+
179
+ ---
180
+
181
+ # 质量建议
182
+
183
+ ## 优化效率
184
+
185
+ - 为了提高速度,建议在 timeline_analysis 阶段根据歌词合并相邻的场景,保证每个视频场景的长度大概在 6-10 秒之间,以减少场景数量,避免产生过多的场景。
186
+
187
+ 比如: 场景1 一共4秒,场景2 一共5秒,他们的歌词是连贯的,那么可以合并为一个场景,时长为9秒
@@ -1 +1 @@
1
- {"version":3,"file":"zerocut.d.ts","sourceRoot":"","sources":["../../../src/mcp/servers/zerocut.ts"],"names":[],"mappings":";AA6wEA,wBAAsB,GAAG,kBAKxB"}
1
+ {"version":3,"file":"zerocut.d.ts","sourceRoot":"","sources":["../../../src/mcp/servers/zerocut.ts"],"names":[],"mappings":";AAogFA,wBAAsB,GAAG,kBAKxB"}
@@ -4,6 +4,39 @@
4
4
  * MCP Server
5
5
  * CallTool 有些工具要求的时间较长,可能会60秒超时,callTool 的时候最好设置一下 timeout
6
6
  */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
7
40
  var __importDefault = (this && this.__importDefault) || function (mod) {
8
41
  return (mod && mod.__esModule) ? mod : { "default": mod };
9
42
  };
@@ -16,7 +49,7 @@ const index_1 = __importDefault(require("../../index"));
16
49
  const constants_1 = require("../../utils/constants");
17
50
  const videokit_1 = require("../../utils/videokit");
18
51
  const promises_1 = require("node:fs/promises");
19
- const node_path_1 = require("node:path");
52
+ const node_path_1 = __importStar(require("node:path"));
20
53
  const doubao_voices_full_1 = require("./helper/doubao_voices_full");
21
54
  const node_fs_1 = require("node:fs");
22
55
  // 错误处理工具函数
@@ -38,8 +71,8 @@ function createErrorResponse(error, operation) {
38
71
  };
39
72
  }
40
73
  // Session 状态检查
41
- function validateSession(operation) {
42
- if (!session) {
74
+ async function validateSession(operation) {
75
+ if (!session || !(await session.isRunning())) {
43
76
  throw new Error(`Session not initialized. Please call 'zerocut-project-open' first before using ${operation}.`);
44
77
  }
45
78
  return session;
@@ -338,7 +371,7 @@ server.registerTool('list-project-files', {
338
371
  }, async () => {
339
372
  try {
340
373
  // 验证session状态
341
- const currentSession = validateSession('list-project-files');
374
+ const currentSession = await validateSession('list-project-files');
342
375
  console.log('Listing project files...');
343
376
  const terminal = currentSession.terminal;
344
377
  if (!terminal) {
@@ -402,7 +435,7 @@ server.registerTool('search-context', {
402
435
  }, async ({ query }) => {
403
436
  try {
404
437
  // 验证session状态
405
- const currentSession = validateSession('search-context');
438
+ const currentSession = await validateSession('search-context');
406
439
  if (!query || query.trim() === '') {
407
440
  throw new Error('Search query cannot be empty');
408
441
  }
@@ -428,7 +461,7 @@ server.registerTool('search-image', {
428
461
  }, async ({ query }) => {
429
462
  try {
430
463
  // 验证session状态
431
- const currentSession = validateSession('search-image');
464
+ const currentSession = await validateSession('search-image');
432
465
  if (!query || query.trim() === '') {
433
466
  throw new Error('Search query cannot be empty');
434
467
  }
@@ -464,7 +497,7 @@ server.registerTool('generate-character-image', {
464
497
  }, async ({ name, gender, age, appearance, clothing, personality, saveToFileName, }) => {
465
498
  try {
466
499
  // 验证session状态
467
- const currentSession = validateSession('generate-character-image');
500
+ const currentSession = await validateSession('generate-character-image');
468
501
  const validatedFileName = validateFileName(saveToFileName);
469
502
  const prompt = `
470
503
  你是一个专业的角色设计师,请根据设定生成角色全身三视图,图片为白底,图中不带任何文字。设定为:
@@ -547,7 +580,7 @@ server.registerTool('upload-local-image', {
547
580
  }, async ({ localPath, size }) => {
548
581
  try {
549
582
  // 验证session状态
550
- const currentSession = validateSession('upload-local-image');
583
+ const currentSession = await validateSession('upload-local-image');
551
584
  // 验证图片文件
552
585
  const validatedPath = validateImageFile(localPath);
553
586
  const fileName = (0, node_path_1.basename)(validatedPath);
@@ -629,7 +662,7 @@ server.registerTool('generate-image', {
629
662
  }, async ({ prompt, size, saveToFileName, watermark, referenceImages }) => {
630
663
  try {
631
664
  // 验证session状态
632
- const currentSession = validateSession('generate-image');
665
+ const currentSession = await validateSession('generate-image');
633
666
  const validatedFileName = validateFileName(saveToFileName);
634
667
  // 检查并替换英文单引号包裹的中文内容为中文双引号
635
668
  // 这样才能让 seedream 生成更好的中文文字
@@ -752,7 +785,7 @@ server.registerTool('edit-image', {
752
785
  }, async ({ prompt, sourceImageFileName, saveToFileName, size, watermark }) => {
753
786
  try {
754
787
  // 验证session状态
755
- const currentSession = validateSession('edit-image');
788
+ const currentSession = await validateSession('edit-image');
756
789
  const validatedFileName = validateFileName(saveToFileName);
757
790
  console.log(`Editing image with prompt: ${prompt.substring(0, 100)}...`);
758
791
  const imagePath = (0, node_path_1.dirname)(sourceImageFileName) !== '.'
@@ -832,7 +865,7 @@ server.registerTool('generate-video', {
832
865
  inputSchema: {
833
866
  prompt: zod_1.z.string().describe('The prompt to generate.'),
834
867
  type: zod_1.z
835
- .enum(['lite', 'pro'])
868
+ .enum(['lite', 'pro', 'hailuo'])
836
869
  .optional()
837
870
  .default('lite')
838
871
  .describe('Use pro model when you need higher quality.'),
@@ -849,16 +882,21 @@ server.registerTool('generate-video', {
849
882
  .string()
850
883
  .optional()
851
884
  .describe('The image file name of the end frame.'),
885
+ resolution: zod_1.z
886
+ .enum(['720p', '1080p'])
887
+ .optional()
888
+ .default('720p')
889
+ .describe('The resolution of the video.'),
852
890
  watermark: zod_1.z
853
891
  .boolean()
854
892
  .optional()
855
893
  .default(false)
856
894
  .describe('Whether to add watermark to the video.'),
857
895
  },
858
- }, async ({ prompt, saveToFileName, start_frame, end_frame, duration, watermark }, context) => {
896
+ }, async ({ prompt, saveToFileName, start_frame, end_frame, duration, watermark, resolution, type, }, context) => {
859
897
  try {
860
898
  // 验证session状态
861
- const currentSession = validateSession('generate-video');
899
+ const currentSession = await validateSession('generate-video');
862
900
  const validatedFileName = validateFileName(saveToFileName);
863
901
  console.log(`Generating video with prompt: ${prompt.substring(0, 100)}...`);
864
902
  const ai = currentSession.ai;
@@ -872,8 +910,9 @@ server.registerTool('generate-video', {
872
910
  start_frame: startFrameUri,
873
911
  end_frame: endFrameUri,
874
912
  duration,
875
- resolution: '720p',
913
+ resolution,
876
914
  watermark,
915
+ type,
877
916
  onProgress: async (metaData) => {
878
917
  try {
879
918
  await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
@@ -978,7 +1017,7 @@ server.registerTool('generate-video-kenburns', {
978
1017
  }, async ({ image_path, duration, camera_motion = 'zoom_in', size, saveToFileName, }) => {
979
1018
  try {
980
1019
  // 验证session状态
981
- const currentSession = validateSession('generate-video-kenburns');
1020
+ const currentSession = await validateSession('generate-video-kenburns');
982
1021
  const validatedFileName = validateFileName(saveToFileName);
983
1022
  const files = currentSession.files;
984
1023
  const terminal = currentSession.terminal;
@@ -1098,7 +1137,7 @@ server.registerTool('generate-sound-effect', {
1098
1137
  },
1099
1138
  }, async ({ prompt_in_english, loop, saveToFileName, duration_seconds }) => {
1100
1139
  try {
1101
- const currentSession = validateSession('generate-sound-effect');
1140
+ const currentSession = await validateSession('generate-sound-effect');
1102
1141
  const ai = currentSession.ai;
1103
1142
  const res = await ai.generateSoundEffect({
1104
1143
  prompt: prompt_in_english,
@@ -1167,7 +1206,7 @@ server.registerTool('generate-sound-effect', {
1167
1206
  // async ({ userPrompt, duration, size, saveToFileName }, context) => {
1168
1207
  // try {
1169
1208
  // // 验证session状态
1170
- // const currentSession = validateSession('generate-principle-video');
1209
+ // const currentSession = await validateSession('generate-principle-video');
1171
1210
  // const [width, height] = size.split('x').map(Number);
1172
1211
  // const ai = currentSession.ai;
1173
1212
  // // 使用 generatePrincipleVideo 方法
@@ -1230,6 +1269,200 @@ server.registerTool('generate-sound-effect', {
1230
1269
  // }
1231
1270
  // }
1232
1271
  // );
1272
+ server.registerTool('generate-song', {
1273
+ title: 'Generate Song',
1274
+ description: 'Generate a song with vocals and customizable parameters.',
1275
+ inputSchema: {
1276
+ lyrics: zod_1.z.string().describe(`The lyrics to generate the song.
1277
+
1278
+ - 完整歌词通常包括以下桥段:
1279
+ - 前奏: intro,歌曲开始的音乐部分,主要用于引导歌曲的整体氛围。
1280
+ - 主歌: verse,通常在前奏之后,歌曲中叙述歌曲故事或主题的部分。
1281
+ - 副歌: chorus,一般在主歌之后,旋律有记忆点和感染力,是整首歌的高潮,进一步强化歌曲的主题和情感。
1282
+ - 间奏: inst,歌曲中的纯音乐段落,用于连接不同的演唱部分。
1283
+ - 尾奏: outro,歌曲结束后的音乐段落,用于营造歌曲结束的氛围。
1284
+ - 桥段: bridge,通常出现在歌曲中段或接近结尾处,是一个过渡部分,用于连接不同的歌曲段落。
1285
+
1286
+ ### 歌词示例 lyrics.txt
1287
+
1288
+ \`\`\`txt
1289
+ [intro]
1290
+ [verse]
1291
+ 记得那一天 那一天我们相恋
1292
+ 说好彼此都不说再见
1293
+ 遵守诺言 用心去相恋
1294
+ 我为你撑伞 你为我取暖
1295
+ [inst]
1296
+ [chorus]
1297
+ 当我把心交给你的那一天
1298
+ 你却消失在我的眼前
1299
+ 事到如今已经过了好多年
1300
+ 是否你还像从前
1301
+ [outro]
1302
+ \`\`\`
1303
+ `),
1304
+ duration: zod_1.z
1305
+ .number()
1306
+ .min(30)
1307
+ .max(240)
1308
+ .describe('The duration of the song in seconds (30-240).'),
1309
+ genre: zod_1.z
1310
+ .enum([
1311
+ 'Folk',
1312
+ 'Pop',
1313
+ 'Rock',
1314
+ 'Chinese Style',
1315
+ 'Hip Hop/Rap',
1316
+ 'R&B/Soul',
1317
+ 'Punk',
1318
+ 'Electronic',
1319
+ 'Jazz',
1320
+ 'Reggae',
1321
+ 'DJ',
1322
+ 'Pop Punk',
1323
+ 'Disco',
1324
+ 'Future Bass',
1325
+ 'Pop Rap',
1326
+ 'Trap Rap',
1327
+ 'R&B Rap',
1328
+ 'Chinoiserie Electronic',
1329
+ 'GuFeng Music',
1330
+ 'Pop Rock',
1331
+ 'Jazz Pop',
1332
+ 'Bossa Nova',
1333
+ 'Contemporary R&B',
1334
+ ])
1335
+ .optional()
1336
+ .describe('The genre of the song.'),
1337
+ mood: zod_1.z
1338
+ .enum([
1339
+ 'Happy',
1340
+ 'Dynamic/Energetic',
1341
+ 'Sentimental/Melancholic/Lonely',
1342
+ 'Inspirational/Hopeful',
1343
+ 'Nostalgic/Memory',
1344
+ 'Excited',
1345
+ 'Sorrow/Sad',
1346
+ 'Chill',
1347
+ 'Relaxing',
1348
+ 'Romantic',
1349
+ 'Miss',
1350
+ 'Groovy/Funky',
1351
+ 'Dreamy/Ethereal',
1352
+ 'Calm/Relaxing',
1353
+ ])
1354
+ .optional()
1355
+ .describe('The mood of the song.'),
1356
+ gender: zod_1.z
1357
+ .enum(['Female', 'Male'])
1358
+ .optional()
1359
+ .describe('The gender of the vocalist.'),
1360
+ timbre: zod_1.z
1361
+ .enum([
1362
+ 'Warm',
1363
+ 'Bright',
1364
+ 'Husky',
1365
+ 'Electrified voice',
1366
+ 'Sweet_AUDIO_TIMBRE',
1367
+ 'Cute_AUDIO_TIMBRE',
1368
+ 'Loud and sonorous',
1369
+ 'Powerful',
1370
+ 'Sexy/Lazy',
1371
+ ])
1372
+ .optional()
1373
+ .describe('The timbre/voice quality of the vocalist.'),
1374
+ skipCopyCheck: zod_1.z
1375
+ .boolean()
1376
+ .optional()
1377
+ .default(false)
1378
+ .describe('Whether to skip copyright check.'),
1379
+ saveToFileName: zod_1.z.string().describe('The filename to save.'),
1380
+ },
1381
+ }, async ({ lyrics, duration, genre, mood, gender, timbre, skipCopyCheck, saveToFileName, }, context) => {
1382
+ try {
1383
+ // 验证session状态
1384
+ const currentSession = await validateSession('generate-song');
1385
+ const validatedFileName = validateFileName(saveToFileName);
1386
+ console.log(`Generating Song with lyrics: ${lyrics.substring(0, 100)}... (${duration}s, genre: ${genre || 'auto'}, mood: ${mood || 'auto'})`);
1387
+ const ai = currentSession.ai;
1388
+ let progress = 0;
1389
+ const res = await ai.generateSong({
1390
+ lyrics: lyrics.trim(),
1391
+ duration,
1392
+ genre,
1393
+ mood,
1394
+ gender,
1395
+ timbre,
1396
+ skipCopyCheck,
1397
+ onProgress: async (metaData) => {
1398
+ try {
1399
+ await sendProgress(context, metaData.Result?.Progress ?? ++progress, metaData.Result?.Progress ? 100 : undefined, JSON.stringify(metaData));
1400
+ }
1401
+ catch (progressError) {
1402
+ console.warn('Failed to send progress update:', progressError);
1403
+ }
1404
+ },
1405
+ });
1406
+ if (!res) {
1407
+ throw new Error('Failed to generate Song: no response from AI service');
1408
+ }
1409
+ if (res.url) {
1410
+ console.log('Song generated successfully, saving to materials...');
1411
+ const uri = await saveMaterial(currentSession, res.url, validatedFileName);
1412
+ const { url, duration: songDuration, captions, ...opts } = res;
1413
+ // 保存captions到本地
1414
+ if (captions) {
1415
+ const captionsText = JSON.stringify(captions, null, 2);
1416
+ // 本地路径
1417
+ const localPath = node_path_1.default.resolve(projectLocalDir, 'materials', `${validatedFileName}.captions.json`);
1418
+ // 保存到本地
1419
+ await (0, promises_1.writeFile)(localPath, captionsText);
1420
+ }
1421
+ const result = {
1422
+ success: true,
1423
+ // source: url,
1424
+ uri,
1425
+ durationMs: Math.floor((songDuration || duration) * 1000),
1426
+ lyrics: lyrics.substring(0, 100),
1427
+ requestedDuration: duration,
1428
+ genre,
1429
+ mood,
1430
+ gender,
1431
+ timbre,
1432
+ captions,
1433
+ timestamp: new Date().toISOString(),
1434
+ ...opts,
1435
+ };
1436
+ return {
1437
+ content: [
1438
+ {
1439
+ type: 'text',
1440
+ text: JSON.stringify(result),
1441
+ },
1442
+ ],
1443
+ };
1444
+ }
1445
+ else {
1446
+ console.warn('Song generation completed but no URL returned');
1447
+ return {
1448
+ content: [
1449
+ {
1450
+ type: 'text',
1451
+ text: JSON.stringify({
1452
+ success: false,
1453
+ error: 'No Song URL returned from AI service',
1454
+ response: res,
1455
+ timestamp: new Date().toISOString(),
1456
+ }),
1457
+ },
1458
+ ],
1459
+ };
1460
+ }
1461
+ }
1462
+ catch (error) {
1463
+ return createErrorResponse(error, 'generate-song');
1464
+ }
1465
+ });
1233
1466
  server.registerTool('generate-bgm', {
1234
1467
  title: 'Generate BGM',
1235
1468
  description: 'Generate the bgm.',
@@ -1245,7 +1478,7 @@ server.registerTool('generate-bgm', {
1245
1478
  }, async ({ prompt, duration, saveToFileName }, context) => {
1246
1479
  try {
1247
1480
  // 验证session状态
1248
- const currentSession = validateSession('generate-bgm');
1481
+ const currentSession = await validateSession('generate-bgm');
1249
1482
  const validatedFileName = validateFileName(saveToFileName);
1250
1483
  console.log(`Generating BGM with prompt: ${prompt.substring(0, 100)}... (${duration}s)`);
1251
1484
  const ai = currentSession.ai;
@@ -1364,7 +1597,7 @@ server.registerTool('generate-scene-tts', {
1364
1597
  }, async ({ text, voiceID, saveToFileName, speed, pitch, volume, emotion, explicit_language, }) => {
1365
1598
  try {
1366
1599
  // 验证session状态
1367
- const currentSession = validateSession('generate-scene-tts');
1600
+ const currentSession = await validateSession('generate-scene-tts');
1368
1601
  const validatedFileName = validateFileName(saveToFileName);
1369
1602
  const finalSpeed = speed ?? 1;
1370
1603
  volume = volume ?? 1;
@@ -1478,7 +1711,7 @@ server.registerTool('compile-and-run', {
1478
1711
  }, async ({ projectFileName, outputFileName }) => {
1479
1712
  try {
1480
1713
  // 验证session状态
1481
- const currentSession = validateSession('compile-and-run');
1714
+ const currentSession = await validateSession('compile-and-run');
1482
1715
  console.log('Starting video compilation and rendering...');
1483
1716
  // 验证terminal可用性
1484
1717
  const terminal = currentSession.terminal;