cerevox 3.9.3 → 3.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.
- package/dist/core/ai.d.ts +8 -0
- package/dist/core/ai.d.ts.map +1 -1
- package/dist/core/ai.js +19 -0
- package/dist/core/ai.js.map +1 -1
- package/dist/mcp/servers/prompts/outlines-backup.md +2 -2
- package/dist/mcp/servers/zerocut.d.ts.map +1 -1
- package/dist/mcp/servers/zerocut.js +1545 -1632
- package/dist/mcp/servers/zerocut.js.map +1 -1
- package/package.json +1 -1
|
@@ -329,7 +329,7 @@ let session = null;
|
|
|
329
329
|
let projectLocalDir = process.env.ZEROCUT_PROJECT_CWD || process.cwd() || '.';
|
|
330
330
|
let checkStoryboardFlag = false;
|
|
331
331
|
let checkAudioVideoDurationFlag = false;
|
|
332
|
-
let checkStoryboardSubtitlesFlag = false;
|
|
332
|
+
// let checkStoryboardSubtitlesFlag = false;
|
|
333
333
|
let closeSessionTimerId = null;
|
|
334
334
|
// 注册 ZeroCut 指导规范 Prompt
|
|
335
335
|
server.registerPrompt('zerocut-guideline', {
|
|
@@ -585,7 +585,7 @@ server.registerTool('retrieve-rules-context', {
|
|
|
585
585
|
// 当 projectRulesFile 不存在时,设置 checkAudioVideoDurationFlag 为 false
|
|
586
586
|
checkAudioVideoDurationFlag = false;
|
|
587
587
|
// 当 projectRulesFile 不存在时,设置 checkStoryboardSubtitlesFlag 为 false
|
|
588
|
-
checkStoryboardSubtitlesFlag = false;
|
|
588
|
+
// checkStoryboardSubtitlesFlag = false;
|
|
589
589
|
}
|
|
590
590
|
if (type === 'switch') {
|
|
591
591
|
(0, node_fs_1.rmSync)(projectRulesFile, { force: true });
|
|
@@ -667,6 +667,178 @@ ${JSON.stringify(rulesList, null, 2)}
|
|
|
667
667
|
return createErrorResponse(`Failed to load rules context prompt for ${prompt}: ${error}`, 'retrieve-rules-context');
|
|
668
668
|
}
|
|
669
669
|
});
|
|
670
|
+
server.registerTool('upload-custom-material', {
|
|
671
|
+
title: 'Upload Custom Material',
|
|
672
|
+
description: 'Upload material files (images: jpeg/png, videos: mp4, audio: mp3) from the local filesystem to the materials directory. For video and audio files, duration information will be included in the response.',
|
|
673
|
+
inputSchema: {
|
|
674
|
+
localFileName: zod_1.z
|
|
675
|
+
.string()
|
|
676
|
+
.describe('The file name of the file to upload. Supported formats: jpeg, png, mp4, mp3'),
|
|
677
|
+
},
|
|
678
|
+
}, async ({ localFileName }) => {
|
|
679
|
+
try {
|
|
680
|
+
// 验证session状态
|
|
681
|
+
const currentSession = await validateSession('upload-custom-material');
|
|
682
|
+
// 构建本地文件路径,使用 ZEROCUT_PROJECT_CWD 环境变量
|
|
683
|
+
const validatedPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'materials', localFileName.trim());
|
|
684
|
+
// 验证本地文件存在性
|
|
685
|
+
if (!(0, node_fs_1.existsSync)(validatedPath)) {
|
|
686
|
+
throw new Error(`File not found: ${validatedPath}`);
|
|
687
|
+
}
|
|
688
|
+
const fileName = (0, node_path_1.basename)(validatedPath);
|
|
689
|
+
const validatedFileName = validateFileName(fileName);
|
|
690
|
+
// 检查文件格式
|
|
691
|
+
const fileExtension = fileName.toLowerCase().split('.').pop();
|
|
692
|
+
const allowedFormats = ['jpeg', 'jpg', 'png', 'mp4', 'mp3'];
|
|
693
|
+
if (!fileExtension || !allowedFormats.includes(fileExtension)) {
|
|
694
|
+
throw new Error(`Unsupported file format: ${fileExtension}. Allowed formats: ${allowedFormats.join(', ')}`);
|
|
695
|
+
}
|
|
696
|
+
console.log(`Uploading material: ${validatedPath} -> ${validatedFileName}`);
|
|
697
|
+
const files = currentSession.files;
|
|
698
|
+
// 直接上传到 sandbox 的 cerevox-zerocut 项目 materials 目录
|
|
699
|
+
const terminal = currentSession.terminal;
|
|
700
|
+
const materialsDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials`;
|
|
701
|
+
await files.upload(validatedPath, `${materialsDir}/${validatedFileName}`);
|
|
702
|
+
// 通过 getMaterialUri 获取材料 URI
|
|
703
|
+
const materialUri = getMaterialUri(currentSession, validatedFileName);
|
|
704
|
+
// 检测媒体文件时长
|
|
705
|
+
let durationMs;
|
|
706
|
+
const isVideoOrAudio = ['mp4', 'mp3'].includes(fileExtension);
|
|
707
|
+
if (isVideoOrAudio) {
|
|
708
|
+
try {
|
|
709
|
+
// 使用现有的 getMediaDuration 函数获取时长(秒)
|
|
710
|
+
let durationSeconds = null;
|
|
711
|
+
if (fileExtension === 'mp4') {
|
|
712
|
+
durationSeconds = await (0, videokit_1.getMediaDuration)(validatedPath);
|
|
713
|
+
}
|
|
714
|
+
else if (fileExtension === 'mp3') {
|
|
715
|
+
durationSeconds = await (0, mp3_duration_1.default)(validatedPath);
|
|
716
|
+
}
|
|
717
|
+
if (durationSeconds !== null) {
|
|
718
|
+
durationMs = Math.round(durationSeconds * 1000); // 转换为毫秒
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
console.warn(`Failed to get duration for ${validatedFileName}:`, error);
|
|
723
|
+
// 时长检测失败不影响上传,继续处理
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const result = {
|
|
727
|
+
success: true,
|
|
728
|
+
uri: materialUri,
|
|
729
|
+
filename: validatedFileName,
|
|
730
|
+
timestamp: new Date().toISOString(),
|
|
731
|
+
};
|
|
732
|
+
// 如果是视频或音频文件且成功获取时长,添加到结果中
|
|
733
|
+
if (durationMs !== undefined) {
|
|
734
|
+
result.durationMs = durationMs;
|
|
735
|
+
}
|
|
736
|
+
console.log('Material uploaded successfully:', result);
|
|
737
|
+
try {
|
|
738
|
+
await updateMediaLogs(currentSession, validatedFileName, result);
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
console.warn(`Failed to update media_logs.json for ${validatedFileName}:`, error);
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
content: [
|
|
745
|
+
{
|
|
746
|
+
type: 'text',
|
|
747
|
+
text: JSON.stringify(result),
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
return createErrorResponse(error, 'upload-custom-material');
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
server.registerTool('wait-for-task-finish', {
|
|
757
|
+
title: 'Wait Workflow or VideoTask Done;只有正在运行Coze工作流或者有异步生成视频任务时才需要执行这个工具;⚠️ 如果执行这个工具未失败只是超时,你应立即再次重新调用,以继续等待直到任务完成或失败',
|
|
758
|
+
description: 'Wait for a workflow to complete.',
|
|
759
|
+
inputSchema: {
|
|
760
|
+
taskUrl: zod_1.z
|
|
761
|
+
.string()
|
|
762
|
+
.describe('The taskUrl of the task to wait for. 一般是Coze工作流任务或视频、图片生成工具调用时返回的taskUrl'),
|
|
763
|
+
saveToFileName: zod_1.z
|
|
764
|
+
.string()
|
|
765
|
+
.describe('The file name to save the media to. 根据任务是图片或视频,应该是mp4或png文件'),
|
|
766
|
+
},
|
|
767
|
+
}, async ({ taskUrl, saveToFileName }, context) => {
|
|
768
|
+
try {
|
|
769
|
+
const currentSession = await validateSession('wait-for-task-finish');
|
|
770
|
+
const ai = currentSession.ai;
|
|
771
|
+
const validatedFileName = validateFileName(saveToFileName);
|
|
772
|
+
let progress = 0;
|
|
773
|
+
const keySet = new Set();
|
|
774
|
+
const res = await ai.waitForTaskComplete({
|
|
775
|
+
taskUrl,
|
|
776
|
+
onProgress: async (metaData) => {
|
|
777
|
+
try {
|
|
778
|
+
// 保存输出节点的内容
|
|
779
|
+
const data = metaData.data;
|
|
780
|
+
if (data) {
|
|
781
|
+
const ext = (0, node_path_1.extname)(validatedFileName);
|
|
782
|
+
const baseName = validatedFileName.slice(0, -ext.length);
|
|
783
|
+
for (const [key, value] of Object.entries(data)) {
|
|
784
|
+
if (keySet.has(value)) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (key.startsWith('Materials_')) {
|
|
788
|
+
keySet.add(value);
|
|
789
|
+
const materials = JSON.parse(value);
|
|
790
|
+
for (const { url, filename } of materials) {
|
|
791
|
+
await saveMaterial(currentSession, url, `${baseName}-parts-${filename}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
|
|
797
|
+
}
|
|
798
|
+
catch (progressError) {
|
|
799
|
+
console.warn('Failed to send progress update:', progressError);
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
timeout: 300000,
|
|
803
|
+
});
|
|
804
|
+
if (res.url) {
|
|
805
|
+
const uri = await saveMaterial(currentSession, res.url, validatedFileName);
|
|
806
|
+
// Update media_logs.json
|
|
807
|
+
try {
|
|
808
|
+
await updateMediaLogs(currentSession, validatedFileName, res, 'audio');
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
console.warn(`Failed to update media_logs.json for ${validatedFileName}:`, error);
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
content: [
|
|
815
|
+
{
|
|
816
|
+
type: 'text',
|
|
817
|
+
text: JSON.stringify({
|
|
818
|
+
success: true,
|
|
819
|
+
uri,
|
|
820
|
+
source: res.url,
|
|
821
|
+
duration: res.duration,
|
|
822
|
+
resolution: res.resolution,
|
|
823
|
+
ratio: res.ratio,
|
|
824
|
+
}),
|
|
825
|
+
},
|
|
826
|
+
],
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
content: [
|
|
831
|
+
{
|
|
832
|
+
type: 'text',
|
|
833
|
+
text: JSON.stringify(res),
|
|
834
|
+
},
|
|
835
|
+
],
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
return createErrorResponse(error, 'wait-for-task-finish');
|
|
840
|
+
}
|
|
841
|
+
});
|
|
670
842
|
server.registerTool('generate-character-image', {
|
|
671
843
|
title: 'Generate Character Image',
|
|
672
844
|
description: 'Generate a turnaround image or portrait for any character.',
|
|
@@ -873,92 +1045,6 @@ ${roleDescriptionPrompt}
|
|
|
873
1045
|
return createErrorResponse(error, 'generate-character-image');
|
|
874
1046
|
}
|
|
875
1047
|
});
|
|
876
|
-
server.registerTool('upload-custom-material', {
|
|
877
|
-
title: 'Upload Custom Material',
|
|
878
|
-
description: 'Upload material files (images: jpeg/png, videos: mp4, audio: mp3) from the local filesystem to the materials directory. For video and audio files, duration information will be included in the response.',
|
|
879
|
-
inputSchema: {
|
|
880
|
-
localFileName: zod_1.z
|
|
881
|
-
.string()
|
|
882
|
-
.describe('The file name of the file to upload. Supported formats: jpeg, png, mp4, mp3'),
|
|
883
|
-
},
|
|
884
|
-
}, async ({ localFileName }) => {
|
|
885
|
-
try {
|
|
886
|
-
// 验证session状态
|
|
887
|
-
const currentSession = await validateSession('upload-custom-material');
|
|
888
|
-
// 构建本地文件路径,使用 ZEROCUT_PROJECT_CWD 环境变量
|
|
889
|
-
const validatedPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'materials', localFileName.trim());
|
|
890
|
-
// 验证本地文件存在性
|
|
891
|
-
if (!(0, node_fs_1.existsSync)(validatedPath)) {
|
|
892
|
-
throw new Error(`File not found: ${validatedPath}`);
|
|
893
|
-
}
|
|
894
|
-
const fileName = (0, node_path_1.basename)(validatedPath);
|
|
895
|
-
const validatedFileName = validateFileName(fileName);
|
|
896
|
-
// 检查文件格式
|
|
897
|
-
const fileExtension = fileName.toLowerCase().split('.').pop();
|
|
898
|
-
const allowedFormats = ['jpeg', 'jpg', 'png', 'mp4', 'mp3'];
|
|
899
|
-
if (!fileExtension || !allowedFormats.includes(fileExtension)) {
|
|
900
|
-
throw new Error(`Unsupported file format: ${fileExtension}. Allowed formats: ${allowedFormats.join(', ')}`);
|
|
901
|
-
}
|
|
902
|
-
console.log(`Uploading material: ${validatedPath} -> ${validatedFileName}`);
|
|
903
|
-
const files = currentSession.files;
|
|
904
|
-
// 直接上传到 sandbox 的 cerevox-zerocut 项目 materials 目录
|
|
905
|
-
const terminal = currentSession.terminal;
|
|
906
|
-
const materialsDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials`;
|
|
907
|
-
await files.upload(validatedPath, `${materialsDir}/${validatedFileName}`);
|
|
908
|
-
// 通过 getMaterialUri 获取材料 URI
|
|
909
|
-
const materialUri = getMaterialUri(currentSession, validatedFileName);
|
|
910
|
-
// 检测媒体文件时长
|
|
911
|
-
let durationMs;
|
|
912
|
-
const isVideoOrAudio = ['mp4', 'mp3'].includes(fileExtension);
|
|
913
|
-
if (isVideoOrAudio) {
|
|
914
|
-
try {
|
|
915
|
-
// 使用现有的 getMediaDuration 函数获取时长(秒)
|
|
916
|
-
let durationSeconds = null;
|
|
917
|
-
if (fileExtension === 'mp4') {
|
|
918
|
-
durationSeconds = await (0, videokit_1.getMediaDuration)(validatedPath);
|
|
919
|
-
}
|
|
920
|
-
else if (fileExtension === 'mp3') {
|
|
921
|
-
durationSeconds = await (0, mp3_duration_1.default)(validatedPath);
|
|
922
|
-
}
|
|
923
|
-
if (durationSeconds !== null) {
|
|
924
|
-
durationMs = Math.round(durationSeconds * 1000); // 转换为毫秒
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
catch (error) {
|
|
928
|
-
console.warn(`Failed to get duration for ${validatedFileName}:`, error);
|
|
929
|
-
// 时长检测失败不影响上传,继续处理
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
const result = {
|
|
933
|
-
success: true,
|
|
934
|
-
uri: materialUri,
|
|
935
|
-
filename: validatedFileName,
|
|
936
|
-
timestamp: new Date().toISOString(),
|
|
937
|
-
};
|
|
938
|
-
// 如果是视频或音频文件且成功获取时长,添加到结果中
|
|
939
|
-
if (durationMs !== undefined) {
|
|
940
|
-
result.durationMs = durationMs;
|
|
941
|
-
}
|
|
942
|
-
console.log('Material uploaded successfully:', result);
|
|
943
|
-
try {
|
|
944
|
-
await updateMediaLogs(currentSession, validatedFileName, result);
|
|
945
|
-
}
|
|
946
|
-
catch (error) {
|
|
947
|
-
console.warn(`Failed to update media_logs.json for ${validatedFileName}:`, error);
|
|
948
|
-
}
|
|
949
|
-
return {
|
|
950
|
-
content: [
|
|
951
|
-
{
|
|
952
|
-
type: 'text',
|
|
953
|
-
text: JSON.stringify(result),
|
|
954
|
-
},
|
|
955
|
-
],
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
catch (error) {
|
|
959
|
-
return createErrorResponse(error, 'upload-custom-material');
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
1048
|
server.registerTool('generate-image', {
|
|
963
1049
|
title: 'Generate Image',
|
|
964
1050
|
description: `生成图片,支持批量生成1-9张图,若用户要求生成关联图片或组图,请一次生成,不要分几次生成`,
|
|
@@ -1590,6 +1676,7 @@ server.registerTool('generate-short-video-outlines', {
|
|
|
1590
1676
|
language,
|
|
1591
1677
|
images,
|
|
1592
1678
|
videoModel: model,
|
|
1679
|
+
aspectRatio: orientation === 'portrait' ? '9:16' : '16:9',
|
|
1593
1680
|
});
|
|
1594
1681
|
if (!res) {
|
|
1595
1682
|
throw new Error('Failed to generate short video outlines: no response from AI service');
|
|
@@ -1671,508 +1758,137 @@ server.registerTool('generate-short-video-outlines', {
|
|
|
1671
1758
|
return createErrorResponse(error, 'generate-short-video-outlines');
|
|
1672
1759
|
}
|
|
1673
1760
|
});
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
description: `图生视频和首尾帧生视频工具`,
|
|
1761
|
+
server.registerTool('generate-music-or-mv', {
|
|
1762
|
+
title: '创作音乐(Music)或音乐视频(Music Video)',
|
|
1763
|
+
description: '生成音乐,包括MV(music video)、BGM 或 歌曲',
|
|
1678
1764
|
inputSchema: {
|
|
1679
|
-
prompt: zod_1.z
|
|
1765
|
+
prompt: zod_1.z.string().describe('The prompt to generate.'),
|
|
1766
|
+
singerPhoto: zod_1.z
|
|
1680
1767
|
.string()
|
|
1681
|
-
.describe('The prompt to generate. 一般要严格对应 storyboard 中当前场景的 video_prompt 字段描述;传这个参数时,若跳过了一致性检查,记得保留镜头切换语言,如“切镜至第二镜头”这样的指令不应当省略。'),
|
|
1682
|
-
sceneIndex: zod_1.z
|
|
1683
|
-
.number()
|
|
1684
|
-
.min(1)
|
|
1685
1768
|
.optional()
|
|
1686
|
-
.describe('
|
|
1687
|
-
|
|
1769
|
+
.describe('The singer photo to use. 只有type为music_video的时候才生效,也可以不传,模型会自动生成'),
|
|
1770
|
+
mvOrientation: zod_1.z
|
|
1771
|
+
.enum(['portrait', 'landscape'])
|
|
1772
|
+
.optional()
|
|
1773
|
+
.describe('The orientation of the music video. Defaults to portrait.')
|
|
1774
|
+
.default('portrait'),
|
|
1775
|
+
mvOriginalSong: zod_1.z
|
|
1688
1776
|
.string()
|
|
1689
1777
|
.optional()
|
|
1690
|
-
.
|
|
1691
|
-
|
|
1692
|
-
skipConsistencyCheck: zod_1.z
|
|
1778
|
+
.describe('用于生成mv的音乐. 只有type为music_video的时候才生效,也可以不传,模型会自动创作'),
|
|
1779
|
+
mvGenSubtitles: zod_1.z
|
|
1693
1780
|
.boolean()
|
|
1694
1781
|
.optional()
|
|
1695
1782
|
.default(false)
|
|
1696
|
-
.describe('
|
|
1697
|
-
skipCheckWithSceneReason: zod_1.z
|
|
1698
|
-
.string()
|
|
1699
|
-
.optional()
|
|
1700
|
-
.describe('跳过校验的理由,如果skipConsistencyCheck设为true,必须要传这个参数'),
|
|
1783
|
+
.describe('是否生成mv的字幕. 默认为false,只有type为music_video的时候才生效'),
|
|
1701
1784
|
type: zod_1.z
|
|
1702
|
-
.enum([
|
|
1703
|
-
'
|
|
1704
|
-
'
|
|
1705
|
-
|
|
1706
|
-
'
|
|
1707
|
-
'vidu-turbo',
|
|
1708
|
-
'vidu-pro',
|
|
1709
|
-
'vidu-uc',
|
|
1710
|
-
'vidu-uc-pro',
|
|
1711
|
-
'kling',
|
|
1712
|
-
'pixv',
|
|
1713
|
-
'veo3.1',
|
|
1714
|
-
'veo3.1-pro',
|
|
1715
|
-
'kenburns',
|
|
1716
|
-
'zero',
|
|
1717
|
-
'zero-fast',
|
|
1718
|
-
])
|
|
1719
|
-
.default('vidu')
|
|
1720
|
-
.describe('除非用户明确提出使用其他模型,否则默认用vidu模型;zero 系列模型适合创作8-24秒带故事情节的短片'),
|
|
1721
|
-
mute: zod_1.z
|
|
1722
|
-
.boolean()
|
|
1723
|
-
.optional()
|
|
1724
|
-
.default(true)
|
|
1725
|
-
.describe('Whether to mute the video (effective for vidu,sora2 and veo3.1). 除非用户明确要求,否则默认静音;非静音模式下模型会自己配音,不需要再使用tts工具!'),
|
|
1726
|
-
seed: zod_1.z
|
|
1727
|
-
.number()
|
|
1728
|
-
.optional()
|
|
1729
|
-
.describe('The seed to use for random number generation. 用户可以指定一个固定值来获得可重复的结果'),
|
|
1730
|
-
saveToFileName: zod_1.z
|
|
1731
|
-
.string()
|
|
1732
|
-
.describe('The filename to save. 应该是mp4文件'),
|
|
1733
|
-
start_frame: zod_1.z
|
|
1734
|
-
.string()
|
|
1785
|
+
.enum(['bgm', 'song', 'music_video'])
|
|
1786
|
+
.describe('The type of music. Defaults to BGM. ⚠️ 如果 type 是 music_video,会直接生成音频和视频,**不需要**额外专门生成歌曲')
|
|
1787
|
+
.default('bgm'),
|
|
1788
|
+
model: zod_1.z
|
|
1789
|
+
.enum(['doubao', 'minimax'])
|
|
1735
1790
|
.optional()
|
|
1736
|
-
.describe('The
|
|
1791
|
+
.describe('The model to use. Defaults to doubao.')
|
|
1792
|
+
.default('doubao'),
|
|
1737
1793
|
duration: zod_1.z
|
|
1738
1794
|
.number()
|
|
1739
|
-
.min(
|
|
1740
|
-
.max(
|
|
1741
|
-
.describe('The duration of the
|
|
1742
|
-
|
|
1743
|
-
.string()
|
|
1744
|
-
.optional()
|
|
1745
|
-
.describe('The image file name of the end frame (for non-zero models) or highlight frame (for zero models).'),
|
|
1746
|
-
saveLastFrameAs: zod_1.z
|
|
1747
|
-
.string()
|
|
1748
|
-
.optional()
|
|
1749
|
-
.describe('The filename to save the last frame. 只有用户要求使用镜头自然延伸时,才需要这个参数,默认不传'),
|
|
1750
|
-
resolution: zod_1.z
|
|
1751
|
-
.enum(['720p', '1080p'])
|
|
1752
|
-
.optional()
|
|
1753
|
-
.default('720p')
|
|
1754
|
-
.describe('除非用户明确提出使用其他分辨率,否则一律用720p'),
|
|
1755
|
-
optimizePrompt: zod_1.z
|
|
1795
|
+
.min(30)
|
|
1796
|
+
.max(240)
|
|
1797
|
+
.describe('The duration of the bgm or music.'),
|
|
1798
|
+
skipCopyCheck: zod_1.z
|
|
1756
1799
|
.boolean()
|
|
1757
|
-
.optional()
|
|
1758
1800
|
.default(false)
|
|
1759
|
-
.describe('Whether to
|
|
1801
|
+
.describe('Whether to skip copyright check.'),
|
|
1802
|
+
saveToFileName: zod_1.z
|
|
1803
|
+
.string()
|
|
1804
|
+
.describe('The filename to save. 如果type是music video,应该是mp4文件,否则应该是mp3文件'),
|
|
1760
1805
|
},
|
|
1761
|
-
}, async ({ prompt,
|
|
1806
|
+
}, async ({ prompt, singerPhoto, mvOrientation, mvOriginalSong, mvGenSubtitles, type, model, duration, skipCopyCheck, saveToFileName, }, context) => {
|
|
1762
1807
|
try {
|
|
1763
1808
|
// 验证session状态
|
|
1764
|
-
const currentSession = await validateSession('generate-
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
if (
|
|
1770
|
-
|
|
1771
|
-
}
|
|
1772
|
-
else if (isZeroModel && duration < 8) {
|
|
1773
|
-
return createErrorResponse('zero 系列模型的视频仅支持 8 秒以上时长', 'generate-video');
|
|
1774
|
-
}
|
|
1775
|
-
if (type === 'zero-fast' && duration > 24) {
|
|
1776
|
-
return createErrorResponse('zero-fast 模型的视频仅支持 24 秒以下时长', 'generate-video');
|
|
1777
|
-
}
|
|
1778
|
-
if (type === 'zero' && resolution !== '1080p') {
|
|
1779
|
-
console.warn(`zero 模型的视频仅支持 1080p 分辨率,用户指定的分辨率为 %s,已自动将 ${resolution} 转换为 1080p`, resolution);
|
|
1780
|
-
resolution = '1080p';
|
|
1809
|
+
const currentSession = await validateSession('generate-music');
|
|
1810
|
+
const validatedFileName = validateFileName(saveToFileName);
|
|
1811
|
+
console.log(`Generating Music with prompt: ${prompt.substring(0, 100)}... (${duration}s)`);
|
|
1812
|
+
const ai = currentSession.ai;
|
|
1813
|
+
let progress = 0;
|
|
1814
|
+
if (type === 'bgm' && duration > 120) {
|
|
1815
|
+
throw new Error('BGM duration must be at most 120 seconds.');
|
|
1781
1816
|
}
|
|
1782
|
-
|
|
1783
|
-
if (
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1817
|
+
let res;
|
|
1818
|
+
if (type === 'music_video') {
|
|
1819
|
+
const singer_photo = singerPhoto
|
|
1820
|
+
? await getMaterialUri(currentSession, singerPhoto)
|
|
1821
|
+
: undefined;
|
|
1822
|
+
const original_song = mvOriginalSong
|
|
1823
|
+
? await getMaterialUri(currentSession, mvOriginalSong)
|
|
1824
|
+
: undefined;
|
|
1825
|
+
res = await ai.generateZeroCutMusicVideo({
|
|
1826
|
+
// prompt: `${prompt.trim()} 音乐时长${duration}秒`,
|
|
1827
|
+
prompt,
|
|
1828
|
+
singerPhoto: singer_photo,
|
|
1829
|
+
orientation: mvOrientation,
|
|
1830
|
+
genSubtitles: mvGenSubtitles,
|
|
1831
|
+
originalSong: original_song,
|
|
1832
|
+
duration,
|
|
1833
|
+
resolution: '720p',
|
|
1834
|
+
onProgress: async (metaData) => {
|
|
1790
1835
|
try {
|
|
1791
|
-
|
|
1836
|
+
await sendProgress(context, metaData.Result?.Progress ?? ++progress, metaData.Result?.Progress ? 100 : undefined, JSON.stringify(metaData));
|
|
1792
1837
|
}
|
|
1793
|
-
catch (
|
|
1794
|
-
|
|
1838
|
+
catch (progressError) {
|
|
1839
|
+
console.warn('Failed to send progress update:', progressError);
|
|
1795
1840
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
return createErrorResponse(`视频时长必须严格遵照storyboard的设定,用户指定的时长为 ${duration} 秒,而 storyboard 中建议的时长为 ${scene.video_duration} 秒。如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用`, 'generate-video');
|
|
1809
|
-
}
|
|
1810
|
-
if (storyBoard.voice_type &&
|
|
1811
|
-
storyBoard.voice_type !== 'slient') {
|
|
1812
|
-
if (mute) {
|
|
1813
|
-
return createErrorResponse('有对话和旁白的场景不能静音,请将mute设为false再重新使用工具。如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-video');
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
// 检查 use_video_model 与 type 参数的一致性
|
|
1817
|
-
if (scene.use_video_model &&
|
|
1818
|
-
type &&
|
|
1819
|
-
scene.use_video_model !== type) {
|
|
1820
|
-
return createErrorResponse(`场景建议的视频模型(${scene.use_video_model})与传入的type参数(${type})不一致。请确保use_video_model与type参数值相同,或将skipConsistencyCheck设置为true后再次调用`, 'generate-video');
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
else {
|
|
1824
|
-
console.warn(`Scene index ${sceneIndex} not found in storyboard.json`);
|
|
1825
|
-
}
|
|
1841
|
+
},
|
|
1842
|
+
waitForFinish: false,
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
else {
|
|
1846
|
+
const finalPrompt = `${prompt.trim()} ${type === 'bgm' ? `纯音乐无歌词,时长${duration}秒` : `时长${duration}秒,使用${model}模型`}`;
|
|
1847
|
+
res = await ai.generateMusic({
|
|
1848
|
+
prompt: finalPrompt,
|
|
1849
|
+
skipCopyCheck,
|
|
1850
|
+
onProgress: async (metaData) => {
|
|
1851
|
+
try {
|
|
1852
|
+
await sendProgress(context, metaData.Result?.Progress ?? ++progress, metaData.Result?.Progress ? 100 : undefined, JSON.stringify(metaData));
|
|
1826
1853
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
}
|
|
1854
|
+
catch (progressError) {
|
|
1855
|
+
console.warn('Failed to send progress update:', progressError);
|
|
1856
|
+
}
|
|
1857
|
+
},
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
if (!res) {
|
|
1861
|
+
throw new Error('Failed to generate Music: no response from AI service');
|
|
1862
|
+
}
|
|
1863
|
+
if (res.url) {
|
|
1864
|
+
console.log('Music generated successfully, saving to materials...');
|
|
1865
|
+
const uri = await saveMaterial(currentSession, res.url, validatedFileName);
|
|
1866
|
+
const { url, duration: bgmDuration, captions, ...opts } = res;
|
|
1867
|
+
// 保存captions到本地
|
|
1868
|
+
if (captions) {
|
|
1869
|
+
const captionsText = JSON.stringify(captions, null, 2);
|
|
1870
|
+
// 本地路径
|
|
1871
|
+
const localPath = node_path_1.default.resolve(projectLocalDir, 'materials', `${validatedFileName}.captions.json`);
|
|
1872
|
+
// 保存到本地
|
|
1873
|
+
await (0, promises_1.writeFile)(localPath, captionsText);
|
|
1874
|
+
}
|
|
1875
|
+
const result = {
|
|
1876
|
+
success: true,
|
|
1877
|
+
// source: url,
|
|
1878
|
+
uri,
|
|
1879
|
+
durationMs: Math.floor((bgmDuration || duration) * 1000),
|
|
1880
|
+
prompt,
|
|
1881
|
+
captions,
|
|
1882
|
+
requestedDuration: duration,
|
|
1883
|
+
timestamp: new Date().toISOString(),
|
|
1884
|
+
...opts,
|
|
1885
|
+
};
|
|
1886
|
+
// Update media_logs.json
|
|
1887
|
+
try {
|
|
1888
|
+
await updateMediaLogs(currentSession, validatedFileName, result, 'audio');
|
|
1831
1889
|
}
|
|
1832
1890
|
catch (error) {
|
|
1833
|
-
console.
|
|
1834
|
-
// 如果读取或解析 storyboard.json 失败,继续执行但记录警告
|
|
1835
|
-
}
|
|
1836
|
-
// 校验视频时长与 timeline_analysis.json 中 proposed_video_scenes 的匹配
|
|
1837
|
-
try {
|
|
1838
|
-
const timelineAnalysisPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'timeline_analysis.json');
|
|
1839
|
-
if ((0, node_fs_1.existsSync)(timelineAnalysisPath)) {
|
|
1840
|
-
const timelineAnalysisContent = await (0, promises_1.readFile)(timelineAnalysisPath, 'utf8');
|
|
1841
|
-
const timelineAnalysis = JSON.parse(timelineAnalysisContent);
|
|
1842
|
-
if (timelineAnalysis.proposed_video_scenes &&
|
|
1843
|
-
Array.isArray(timelineAnalysis.proposed_video_scenes)) {
|
|
1844
|
-
const videoScene = timelineAnalysis.proposed_video_scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
|
|
1845
|
-
if (videoScene && videoScene.video_duration_s) {
|
|
1846
|
-
const expectedDuration = videoScene.video_duration_s;
|
|
1847
|
-
if (duration !== expectedDuration) {
|
|
1848
|
-
return createErrorResponse(`视频时长必须与timeline_analysis中的设定匹配。当前场景${videoScene.scene_id}要求时长为${expectedDuration}秒,但传入的duration为${duration}秒。请调整duration参数为${expectedDuration}秒后重试。如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用。`, 'generate-video');
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
else {
|
|
1852
|
-
console.warn(`Scene ${sceneIndex} (scene_${sceneIndex.toString().padStart(2, '0')}) not found in timeline_analysis.json proposed_video_scenes`);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
else {
|
|
1857
|
-
// 检查音频时长标志
|
|
1858
|
-
try {
|
|
1859
|
-
const storyBoardPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, storyBoardFile);
|
|
1860
|
-
if ((0, node_fs_1.existsSync)(storyBoardPath)) {
|
|
1861
|
-
const storyBoardContent = await (0, promises_1.readFile)(storyBoardPath, 'utf8');
|
|
1862
|
-
const storyBoard = JSON.parse(storyBoardContent);
|
|
1863
|
-
if (storyBoard.scenes && Array.isArray(storyBoard.scenes)) {
|
|
1864
|
-
const scene = storyBoard.scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
|
|
1865
|
-
if (scene &&
|
|
1866
|
-
((scene.script && scene.script.trim() !== '') ||
|
|
1867
|
-
scene.dialog)) {
|
|
1868
|
-
if (!checkAudioVideoDurationFlag) {
|
|
1869
|
-
checkAudioVideoDurationFlag = true;
|
|
1870
|
-
if (scene.audio_mode === 'vo_sync') {
|
|
1871
|
-
return createErrorResponse('请先自我检查 media_logs 中的音频时长,确保 storyboard 中视频时长为音频时长向上取整 即 ceil(音频时长),然后再按照正确的视频时长创建视频', 'generate-video');
|
|
1872
|
-
}
|
|
1873
|
-
else if (scene.audio_mode === 'dialogue') {
|
|
1874
|
-
return createErrorResponse('请先自我检查 media_logs 中的音频时长,确保 storyboard 中视频时长**不小于**音频时长向上取整 即 ceil(音频时长),然后再按照正确的视频时长创建视频', 'generate-video');
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
catch (error) {
|
|
1882
|
-
console.error('Failed to check audio duration flag:', error);
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
catch (error) {
|
|
1887
|
-
console.error('Failed to validate duration with timeline analysis:', error);
|
|
1888
|
-
// 如果读取或解析 timeline_analysis.json 失败,继续执行但记录警告
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
const validatedFileName = validateFileName(saveToFileName);
|
|
1892
|
-
console.log(`Generating video with prompt: ${prompt.substring(0, 100)}...`);
|
|
1893
|
-
const ai = currentSession.ai;
|
|
1894
|
-
let progress = 0;
|
|
1895
|
-
const startFrameUri = start_frame
|
|
1896
|
-
? getMaterialUri(currentSession, start_frame)
|
|
1897
|
-
: undefined;
|
|
1898
|
-
const endFrameUri = end_frame
|
|
1899
|
-
? getMaterialUri(currentSession, end_frame)
|
|
1900
|
-
: undefined;
|
|
1901
|
-
if (optimizePrompt) {
|
|
1902
|
-
try {
|
|
1903
|
-
const promptOptimizer = await (0, promises_1.readFile)((0, node_path_1.resolve)(__dirname, './prompts/video-prompt-optimizer.md'), 'utf8');
|
|
1904
|
-
const completion = await ai.getCompletions({
|
|
1905
|
-
messages: [
|
|
1906
|
-
{
|
|
1907
|
-
role: 'system',
|
|
1908
|
-
content: promptOptimizer,
|
|
1909
|
-
},
|
|
1910
|
-
{
|
|
1911
|
-
role: 'user',
|
|
1912
|
-
content: prompt.trim(),
|
|
1913
|
-
},
|
|
1914
|
-
],
|
|
1915
|
-
});
|
|
1916
|
-
const optimizedPrompt = completion.choices[0]?.message?.content;
|
|
1917
|
-
if (optimizedPrompt) {
|
|
1918
|
-
prompt = optimizedPrompt;
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
catch (error) {
|
|
1922
|
-
console.error('Failed to optimize prompt:', error);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
if (type === 'kenburns') {
|
|
1926
|
-
// 实现 getImageSize 函数
|
|
1927
|
-
async function getImageSize(imageUri) {
|
|
1928
|
-
// imageUri 是一个可以 http 访问的图片
|
|
1929
|
-
// eslint-disable-next-line custom/no-fetch-in-src
|
|
1930
|
-
const response = await fetch(imageUri);
|
|
1931
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
1932
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
1933
|
-
const image = (0, image_size_1.default)(buffer);
|
|
1934
|
-
return [image.width, image.height];
|
|
1935
|
-
}
|
|
1936
|
-
const [imageWidth, imageHeight] = await getImageSize(startFrameUri);
|
|
1937
|
-
const kenburnsPrompt = `根据用户描述和配置,生成适合的 kenburns 动画参数。
|
|
1938
|
-
|
|
1939
|
-
## 配置信息
|
|
1940
|
-
- 视频时长:${duration}秒
|
|
1941
|
-
- 分辨率:${resolution}
|
|
1942
|
-
- 保存最后一帧:${saveLastFrameAs ? '是' : '否'}
|
|
1943
|
-
- 图片宽高:${imageWidth}x${imageHeight}
|
|
1944
|
-
- 上一次选择的特效:${lastEffect || '无'}
|
|
1945
|
-
|
|
1946
|
-
## camera_motion 可选特效
|
|
1947
|
-
- zoom_in
|
|
1948
|
-
- zoom_out
|
|
1949
|
-
- zoom_in_left_top
|
|
1950
|
-
- zoom_out_left_top
|
|
1951
|
-
- zoom_in_right_top
|
|
1952
|
-
- zoom_out_right_top
|
|
1953
|
-
- zoom_in_left_bottom
|
|
1954
|
-
- zoom_out_left_bottom
|
|
1955
|
-
- zoom_in_right_bottom
|
|
1956
|
-
- zoom_out_right_bottom
|
|
1957
|
-
- pan_up
|
|
1958
|
-
- pan_down
|
|
1959
|
-
- pan_left
|
|
1960
|
-
- pan_right
|
|
1961
|
-
- static
|
|
1962
|
-
|
|
1963
|
-
## 特效选用规则
|
|
1964
|
-
- 视频时长6秒内:优先选择zoom类型(zoom_in, zoom_out)
|
|
1965
|
-
- 视频时长6秒以上:优先选择pan类型(pan_left, pan_right, pan_up, pan_down)
|
|
1966
|
-
- 除非用户指定,否则不要主动使用 static 效果
|
|
1967
|
-
- 除非用户指定,否则不要和上一次选择的效果重复
|
|
1968
|
-
`;
|
|
1969
|
-
const schema = {
|
|
1970
|
-
name: 'kenburns_effect',
|
|
1971
|
-
schema: {
|
|
1972
|
-
type: 'object',
|
|
1973
|
-
properties: {
|
|
1974
|
-
camera_motion: {
|
|
1975
|
-
type: 'string',
|
|
1976
|
-
description: 'kenburns 特效类型',
|
|
1977
|
-
enum: [
|
|
1978
|
-
'zoom_in',
|
|
1979
|
-
'zoom_out',
|
|
1980
|
-
'zoom_in_left_top',
|
|
1981
|
-
'zoom_out_left_top',
|
|
1982
|
-
'zoom_in_right_top',
|
|
1983
|
-
'zoom_out_right_top',
|
|
1984
|
-
'zoom_in_left_bottom',
|
|
1985
|
-
'zoom_out_left_bottom',
|
|
1986
|
-
'zoom_in_right_bottom',
|
|
1987
|
-
'zoom_out_right_bottom',
|
|
1988
|
-
'pan_up',
|
|
1989
|
-
'pan_down',
|
|
1990
|
-
'pan_left',
|
|
1991
|
-
'pan_right',
|
|
1992
|
-
'static',
|
|
1993
|
-
],
|
|
1994
|
-
},
|
|
1995
|
-
size: {
|
|
1996
|
-
type: 'string',
|
|
1997
|
-
description: 'kenburns 视频 宽x高',
|
|
1998
|
-
},
|
|
1999
|
-
duration: {
|
|
2000
|
-
type: 'number',
|
|
2001
|
-
description: 'kenburns 视频 时长',
|
|
2002
|
-
},
|
|
2003
|
-
},
|
|
2004
|
-
required: ['camera_motion', 'size', 'duration'],
|
|
2005
|
-
},
|
|
2006
|
-
};
|
|
2007
|
-
const analysisPayload = {
|
|
2008
|
-
model: 'Doubao-Seed-1.6-flash',
|
|
2009
|
-
messages: [
|
|
2010
|
-
{
|
|
2011
|
-
role: 'system',
|
|
2012
|
-
content: kenburnsPrompt,
|
|
2013
|
-
},
|
|
2014
|
-
{
|
|
2015
|
-
role: 'user',
|
|
2016
|
-
content: prompt.trim(),
|
|
2017
|
-
},
|
|
2018
|
-
],
|
|
2019
|
-
response_format: {
|
|
2020
|
-
type: 'json_schema',
|
|
2021
|
-
json_schema: schema,
|
|
2022
|
-
},
|
|
2023
|
-
};
|
|
2024
|
-
console.log(JSON.stringify(analysisPayload, null, 2));
|
|
2025
|
-
const completion = await ai.getCompletions(analysisPayload);
|
|
2026
|
-
const analysisResult = completion.choices[0]?.message?.content;
|
|
2027
|
-
if (!analysisResult) {
|
|
2028
|
-
throw new Error('Failed to generate kenburns effect');
|
|
2029
|
-
}
|
|
2030
|
-
try {
|
|
2031
|
-
const kenburnsEffect = JSON.parse(analysisResult);
|
|
2032
|
-
const { camera_motion, size, duration } = kenburnsEffect;
|
|
2033
|
-
if (!camera_motion || !size || !duration) {
|
|
2034
|
-
throw new Error('Invalid kenburns effect parameters');
|
|
2035
|
-
}
|
|
2036
|
-
lastEffect = camera_motion;
|
|
2037
|
-
const files = currentSession.files;
|
|
2038
|
-
const terminal = currentSession.terminal;
|
|
2039
|
-
start_frame = (0, node_path_1.resolve)('/home/user/cerevox-zerocut/projects', terminal.id, 'materials', (0, node_path_1.basename)(start_frame));
|
|
2040
|
-
const saveToPath = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials/${validatedFileName}`;
|
|
2041
|
-
const saveLocalPath = (0, node_path_1.resolve)(projectLocalDir, 'materials', validatedFileName);
|
|
2042
|
-
// 解析尺寸参数
|
|
2043
|
-
const sizeArray = size.split('x');
|
|
2044
|
-
if (sizeArray.length !== 2) {
|
|
2045
|
-
throw new Error(`Invalid size format: ${size}. Expected format: WIDTHxHEIGHT`);
|
|
2046
|
-
}
|
|
2047
|
-
const [widthStr, heightStr] = sizeArray;
|
|
2048
|
-
const width = Number(widthStr);
|
|
2049
|
-
const height = Number(heightStr);
|
|
2050
|
-
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
|
|
2051
|
-
throw new Error(`Invalid dimensions: ${width}x${height}. Both width and height must be positive numbers`);
|
|
2052
|
-
}
|
|
2053
|
-
console.log(`Compiling Ken Burns motion command...`);
|
|
2054
|
-
let command;
|
|
2055
|
-
try {
|
|
2056
|
-
command = await (0, videokit_1.compileKenBurnsMotion)(start_frame, duration, camera_motion, {
|
|
2057
|
-
output: saveToPath,
|
|
2058
|
-
width,
|
|
2059
|
-
height,
|
|
2060
|
-
});
|
|
2061
|
-
}
|
|
2062
|
-
catch (compileError) {
|
|
2063
|
-
console.error('Failed to compile Ken Burns motion command:', compileError);
|
|
2064
|
-
throw new Error(`Failed to compile Ken Burns motion: ${compileError}`);
|
|
2065
|
-
}
|
|
2066
|
-
console.log(`Executing FFmpeg command: ${command.substring(0, 100)}...`);
|
|
2067
|
-
const res = await terminal.run(command);
|
|
2068
|
-
const result = await res.json();
|
|
2069
|
-
if (result.exitCode !== 0) {
|
|
2070
|
-
console.error('FFmpeg command failed:', result);
|
|
2071
|
-
return {
|
|
2072
|
-
content: [
|
|
2073
|
-
{
|
|
2074
|
-
type: 'text',
|
|
2075
|
-
text: JSON.stringify({
|
|
2076
|
-
success: false,
|
|
2077
|
-
error: 'Ken Burns motion generation failed',
|
|
2078
|
-
exitCode: result.exitCode,
|
|
2079
|
-
stderr: result.stderr,
|
|
2080
|
-
command,
|
|
2081
|
-
timestamp: new Date().toISOString(),
|
|
2082
|
-
}),
|
|
2083
|
-
},
|
|
2084
|
-
],
|
|
2085
|
-
};
|
|
2086
|
-
}
|
|
2087
|
-
console.log('Ken Burns motion generated successfully, downloading file...');
|
|
2088
|
-
try {
|
|
2089
|
-
await files.download(saveToPath, saveLocalPath);
|
|
2090
|
-
}
|
|
2091
|
-
catch (downloadError) {
|
|
2092
|
-
console.warn('Failed to download file to local:', downloadError);
|
|
2093
|
-
// 继续执行,因为远程文件已生成成功
|
|
2094
|
-
}
|
|
2095
|
-
const resultData = {
|
|
2096
|
-
success: true,
|
|
2097
|
-
uri: saveToPath,
|
|
2098
|
-
durationMs: Math.floor(duration * 1000),
|
|
2099
|
-
cameraMotion: camera_motion,
|
|
2100
|
-
imagePath: start_frame,
|
|
2101
|
-
size,
|
|
2102
|
-
dimensions: { width, height },
|
|
2103
|
-
timestamp: new Date().toISOString(),
|
|
2104
|
-
systemPrompt: kenburnsPrompt,
|
|
2105
|
-
};
|
|
2106
|
-
console.log(`Ken Burns motion completed: ${saveToPath}`);
|
|
2107
|
-
currentSession.track('KenBurns Video Generated', {
|
|
2108
|
-
durationMs: Math.floor(duration * 1000).toString(),
|
|
2109
|
-
cameraMotion: camera_motion,
|
|
2110
|
-
imagePath: start_frame,
|
|
2111
|
-
size,
|
|
2112
|
-
dimensions: size,
|
|
2113
|
-
});
|
|
2114
|
-
return {
|
|
2115
|
-
content: [
|
|
2116
|
-
{
|
|
2117
|
-
type: 'text',
|
|
2118
|
-
text: JSON.stringify(resultData),
|
|
2119
|
-
},
|
|
2120
|
-
],
|
|
2121
|
-
};
|
|
2122
|
-
}
|
|
2123
|
-
catch (error) {
|
|
2124
|
-
throw new Error('Failed to parse kenburns effect parameters');
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
const res = await ai.framesToVideo({
|
|
2128
|
-
prompt: prompt.trim(),
|
|
2129
|
-
start_frame: startFrameUri,
|
|
2130
|
-
end_frame: endFrameUri,
|
|
2131
|
-
duration,
|
|
2132
|
-
resolution,
|
|
2133
|
-
type,
|
|
2134
|
-
return_last_frame: Boolean(saveLastFrameAs),
|
|
2135
|
-
onProgress: async (metaData) => {
|
|
2136
|
-
try {
|
|
2137
|
-
await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
|
|
2138
|
-
}
|
|
2139
|
-
catch (progressError) {
|
|
2140
|
-
console.warn('Failed to send progress update:', progressError);
|
|
2141
|
-
}
|
|
2142
|
-
},
|
|
2143
|
-
waitForFinish: type !== 'zero',
|
|
2144
|
-
mute,
|
|
2145
|
-
seed,
|
|
2146
|
-
});
|
|
2147
|
-
if (!res) {
|
|
2148
|
-
throw new Error('Failed to generate video: no response from AI service');
|
|
2149
|
-
}
|
|
2150
|
-
if (res.url) {
|
|
2151
|
-
console.log('Video generated successfully, saving to materials...');
|
|
2152
|
-
const uri = await saveMaterial(currentSession, res.url, validatedFileName);
|
|
2153
|
-
let lastFrameUri;
|
|
2154
|
-
if (saveLastFrameAs && res.last_frame_url) {
|
|
2155
|
-
lastFrameUri = await saveMaterial(currentSession, res.last_frame_url, validateFileName(saveLastFrameAs));
|
|
2156
|
-
}
|
|
2157
|
-
const { url, duration: videoDuration, ...opts } = res;
|
|
2158
|
-
const result = {
|
|
2159
|
-
success: true,
|
|
2160
|
-
// source: url,
|
|
2161
|
-
uri,
|
|
2162
|
-
lastFrameUri,
|
|
2163
|
-
durationMs: Math.floor((videoDuration || duration) * 1000),
|
|
2164
|
-
prompt,
|
|
2165
|
-
timestamp: new Date().toISOString(),
|
|
2166
|
-
startFrame: start_frame,
|
|
2167
|
-
endFrame: end_frame,
|
|
2168
|
-
...opts,
|
|
2169
|
-
};
|
|
2170
|
-
// Update media_logs.json
|
|
2171
|
-
try {
|
|
2172
|
-
await updateMediaLogs(currentSession, validatedFileName, result);
|
|
2173
|
-
}
|
|
2174
|
-
catch (error) {
|
|
2175
|
-
console.warn(`Failed to update media_logs.json for ${validatedFileName}:`, error);
|
|
1891
|
+
console.warn('Failed to update media_logs.json:', error);
|
|
2176
1892
|
}
|
|
2177
1893
|
return {
|
|
2178
1894
|
content: [
|
|
@@ -2190,7 +1906,7 @@ server.registerTool('generate-video', {
|
|
|
2190
1906
|
type: 'text',
|
|
2191
1907
|
text: JSON.stringify({
|
|
2192
1908
|
success: true,
|
|
2193
|
-
message: '该视频生成任务正在运行中,它是异步任务,且执行时间较长,你应立即调用工具 wait-for-task-finish
|
|
1909
|
+
message: '该视频生成任务正在运行中,它是异步任务,且执行时间较长,你应立即调用工具 wait-for-task-finish 来等待任务结束,如如 wait-for-task-finish 工具调用超时,你应立即再次重新调用直到任务结束。',
|
|
2194
1910
|
taskUrl: res.taskUrl,
|
|
2195
1911
|
}),
|
|
2196
1912
|
},
|
|
@@ -2198,16 +1914,15 @@ server.registerTool('generate-video', {
|
|
|
2198
1914
|
};
|
|
2199
1915
|
}
|
|
2200
1916
|
else {
|
|
2201
|
-
console.warn('
|
|
1917
|
+
console.warn('Music generation completed but no URL returned');
|
|
2202
1918
|
return {
|
|
2203
1919
|
content: [
|
|
2204
1920
|
{
|
|
2205
1921
|
type: 'text',
|
|
2206
1922
|
text: JSON.stringify({
|
|
2207
1923
|
success: false,
|
|
2208
|
-
error: 'No
|
|
1924
|
+
error: 'No Music URL returned from AI service',
|
|
2209
1925
|
response: res,
|
|
2210
|
-
startFrameUri,
|
|
2211
1926
|
timestamp: new Date().toISOString(),
|
|
2212
1927
|
}),
|
|
2213
1928
|
},
|
|
@@ -2216,607 +1931,1197 @@ server.registerTool('generate-video', {
|
|
|
2216
1931
|
}
|
|
2217
1932
|
}
|
|
2218
1933
|
catch (error) {
|
|
2219
|
-
return createErrorResponse(error, 'generate-
|
|
2220
|
-
}
|
|
2221
|
-
});
|
|
2222
|
-
server.registerTool('wait-for-task-finish', {
|
|
2223
|
-
title: 'Wait Workflow or VideoTask Done;只有正在运行Coze工作流或者有异步生成视频任务时才需要执行这个工具;⚠️ 如果执行这个工具未失败只是超时,你应立即再次重新调用,以继续等待直到任务完成或失败',
|
|
2224
|
-
description: 'Wait for a workflow to complete.',
|
|
2225
|
-
inputSchema: {
|
|
2226
|
-
taskUrl: zod_1.z
|
|
2227
|
-
.string()
|
|
2228
|
-
.describe('The taskUrl of the video task to wait for.'),
|
|
2229
|
-
saveToFileName: zod_1.z
|
|
2230
|
-
.string()
|
|
2231
|
-
.describe('The file name to save the video to. 应该是mp4文件'),
|
|
2232
|
-
},
|
|
2233
|
-
}, async ({ taskUrl, saveToFileName }, context) => {
|
|
2234
|
-
try {
|
|
2235
|
-
const currentSession = await validateSession('wait-for-task-finish');
|
|
2236
|
-
const ai = currentSession.ai;
|
|
2237
|
-
const validatedFileName = validateFileName(saveToFileName);
|
|
2238
|
-
let progress = 0;
|
|
2239
|
-
const keySet = new Set();
|
|
2240
|
-
const res = await ai.waitForTaskComplete({
|
|
2241
|
-
taskUrl,
|
|
2242
|
-
onProgress: async (metaData) => {
|
|
2243
|
-
try {
|
|
2244
|
-
// 保存输出节点的内容
|
|
2245
|
-
const data = metaData.data;
|
|
2246
|
-
if (data) {
|
|
2247
|
-
const ext = (0, node_path_1.extname)(validatedFileName);
|
|
2248
|
-
const baseName = validatedFileName.slice(0, -ext.length);
|
|
2249
|
-
for (const [key, value] of Object.entries(data)) {
|
|
2250
|
-
if (keySet.has(value)) {
|
|
2251
|
-
continue;
|
|
2252
|
-
}
|
|
2253
|
-
if (key.startsWith('Materials_')) {
|
|
2254
|
-
keySet.add(value);
|
|
2255
|
-
const materials = JSON.parse(value);
|
|
2256
|
-
for (const { url, filename } of materials) {
|
|
2257
|
-
await saveMaterial(currentSession, url, `${baseName}-parts-${filename}`);
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
|
|
2263
|
-
}
|
|
2264
|
-
catch (progressError) {
|
|
2265
|
-
console.warn('Failed to send progress update:', progressError);
|
|
2266
|
-
}
|
|
2267
|
-
},
|
|
2268
|
-
timeout: 300000,
|
|
2269
|
-
});
|
|
2270
|
-
if (res.url) {
|
|
2271
|
-
const uri = await saveMaterial(currentSession, res.url, validatedFileName);
|
|
2272
|
-
// Update media_logs.json
|
|
2273
|
-
try {
|
|
2274
|
-
await updateMediaLogs(currentSession, validatedFileName, res, 'audio');
|
|
2275
|
-
}
|
|
2276
|
-
catch (error) {
|
|
2277
|
-
console.warn(`Failed to update media_logs.json for ${validatedFileName}:`, error);
|
|
2278
|
-
}
|
|
2279
|
-
return {
|
|
2280
|
-
content: [
|
|
2281
|
-
{
|
|
2282
|
-
type: 'text',
|
|
2283
|
-
text: JSON.stringify({
|
|
2284
|
-
success: true,
|
|
2285
|
-
uri,
|
|
2286
|
-
source: res.url,
|
|
2287
|
-
duration: res.duration,
|
|
2288
|
-
resolution: res.resolution,
|
|
2289
|
-
ratio: res.ratio,
|
|
2290
|
-
}),
|
|
2291
|
-
},
|
|
2292
|
-
],
|
|
2293
|
-
};
|
|
2294
|
-
}
|
|
2295
|
-
return {
|
|
2296
|
-
content: [
|
|
2297
|
-
{
|
|
2298
|
-
type: 'text',
|
|
2299
|
-
text: JSON.stringify(res),
|
|
2300
|
-
},
|
|
2301
|
-
],
|
|
2302
|
-
};
|
|
2303
|
-
}
|
|
2304
|
-
catch (error) {
|
|
2305
|
-
return createErrorResponse(error, 'wait-for-task-finish');
|
|
1934
|
+
return createErrorResponse(error, 'generate-music');
|
|
2306
1935
|
}
|
|
2307
1936
|
});
|
|
2308
|
-
server.registerTool(
|
|
2309
|
-
|
|
2310
|
-
|
|
1937
|
+
// server.registerTool(
|
|
1938
|
+
// 'generate-scene-tts',
|
|
1939
|
+
// {
|
|
1940
|
+
// title: 'Generate Scene TTS',
|
|
1941
|
+
// description: `生成场景配音`,
|
|
1942
|
+
// inputSchema: {
|
|
1943
|
+
// text: z.string().describe('The text to generate.'),
|
|
1944
|
+
// sceneIndex: z
|
|
1945
|
+
// .number()
|
|
1946
|
+
// .min(1)
|
|
1947
|
+
// .optional()
|
|
1948
|
+
// .describe(
|
|
1949
|
+
// '场景索引,从1开始的下标,如果非场景对应素材,则可不传,场景素材必传'
|
|
1950
|
+
// ),
|
|
1951
|
+
// storyBoardFile: z
|
|
1952
|
+
// .string()
|
|
1953
|
+
// .optional()
|
|
1954
|
+
// .default('storyboard.json')
|
|
1955
|
+
// .describe('故事板文件路径'),
|
|
1956
|
+
// skipConsistencyCheck: z
|
|
1957
|
+
// .boolean()
|
|
1958
|
+
// .optional()
|
|
1959
|
+
// .default(false)
|
|
1960
|
+
// .describe('是否跳过一致性检查,默认为false(即默认进行一致性检查)'),
|
|
1961
|
+
// skipCheckWithSceneReason: z
|
|
1962
|
+
// .string()
|
|
1963
|
+
// .optional()
|
|
1964
|
+
// .describe(
|
|
1965
|
+
// '跳过校验的理由,如果skipConsistencyCheck设为true,必须要传这个参数'
|
|
1966
|
+
// ),
|
|
1967
|
+
// saveToFileName: z
|
|
1968
|
+
// .string()
|
|
1969
|
+
// .describe('The filename to save. 应该是mp3文件'),
|
|
1970
|
+
// speed: z
|
|
1971
|
+
// .number()
|
|
1972
|
+
// .min(0.5)
|
|
1973
|
+
// .max(2)
|
|
1974
|
+
// .optional()
|
|
1975
|
+
// .default(1)
|
|
1976
|
+
// .describe('The speed of the tts.'),
|
|
1977
|
+
// pitch: z
|
|
1978
|
+
// .number()
|
|
1979
|
+
// .min(-12)
|
|
1980
|
+
// .max(12)
|
|
1981
|
+
// .optional()
|
|
1982
|
+
// .default(0)
|
|
1983
|
+
// .describe('The pitch of the tts.'),
|
|
1984
|
+
// volume: z
|
|
1985
|
+
// .number()
|
|
1986
|
+
// .min(0)
|
|
1987
|
+
// .max(10)
|
|
1988
|
+
// .optional()
|
|
1989
|
+
// .default(1.0)
|
|
1990
|
+
// .describe('The volume of the tts.'),
|
|
1991
|
+
// voiceID: z
|
|
1992
|
+
// .string()
|
|
1993
|
+
// .describe(
|
|
1994
|
+
// `适合作为视频配音的音色ID,除非用户指定,否则你必须确保已通过 pick-voice 工具挑选出真实存在的音色。`
|
|
1995
|
+
// ),
|
|
1996
|
+
// context_texts: z
|
|
1997
|
+
// .array(z.string())
|
|
1998
|
+
// .default([])
|
|
1999
|
+
// .describe(
|
|
2000
|
+
// `语音合成的辅助信息,用于模型对话式合成,能更好的体现语音情感
|
|
2001
|
+
// 可以探索,比如常见示例有以下几种:
|
|
2002
|
+
// 1. 语速调整
|
|
2003
|
+
// - context_texts: ["你可以说慢一点吗?"]
|
|
2004
|
+
// 2. 情绪/语气调整
|
|
2005
|
+
// - context_texts=["你可以用特别特别痛心的语气说话吗?"]
|
|
2006
|
+
// - context_texts=["嗯,你的语气再欢乐一点"]
|
|
2007
|
+
// 3. 音量调整
|
|
2008
|
+
// - context_texts=["你嗓门再小点。"]
|
|
2009
|
+
// 4. 音感调整
|
|
2010
|
+
// - context_texts=["你能用骄傲的语气来说话吗?"]
|
|
2011
|
+
// `
|
|
2012
|
+
// ),
|
|
2013
|
+
// explicit_language: z.enum(['zh', 'en', 'ja']).optional().default('zh'),
|
|
2014
|
+
// },
|
|
2015
|
+
// },
|
|
2016
|
+
// async ({
|
|
2017
|
+
// text,
|
|
2018
|
+
// sceneIndex,
|
|
2019
|
+
// storyBoardFile,
|
|
2020
|
+
// skipConsistencyCheck,
|
|
2021
|
+
// voiceID,
|
|
2022
|
+
// saveToFileName,
|
|
2023
|
+
// speed,
|
|
2024
|
+
// pitch,
|
|
2025
|
+
// volume,
|
|
2026
|
+
// context_texts,
|
|
2027
|
+
// explicit_language,
|
|
2028
|
+
// }) => {
|
|
2029
|
+
// try {
|
|
2030
|
+
// // 验证session状态
|
|
2031
|
+
// const currentSession = await validateSession('generate-scene-tts');
|
|
2032
|
+
// const validatedFileName = validateFileName(saveToFileName);
|
|
2033
|
+
// const finalSpeed = speed ?? 1;
|
|
2034
|
+
// volume = volume ?? 1;
|
|
2035
|
+
// const ai = currentSession.ai;
|
|
2036
|
+
// let scene = null;
|
|
2037
|
+
// // 校验 text 与 storyboard.json 中场景设定的一致性
|
|
2038
|
+
// if (sceneIndex && !skipConsistencyCheck) {
|
|
2039
|
+
// try {
|
|
2040
|
+
// const voice = (await ai.listVoices()).find(v => v.id === voiceID);
|
|
2041
|
+
// if (!voice) {
|
|
2042
|
+
// return createErrorResponse(
|
|
2043
|
+
// `Voice ${voiceID} not found in voice-list. Use pick-voice tool to pick an available voice. 若用户坚持要使用该音色,需跳过一致性检查。`,
|
|
2044
|
+
// 'generate-scene-tts'
|
|
2045
|
+
// );
|
|
2046
|
+
// }
|
|
2047
|
+
// const storyBoardPath = resolve(
|
|
2048
|
+
// process.env.ZEROCUT_PROJECT_CWD || process.cwd(),
|
|
2049
|
+
// projectLocalDir,
|
|
2050
|
+
// storyBoardFile
|
|
2051
|
+
// );
|
|
2052
|
+
// if (existsSync(storyBoardPath)) {
|
|
2053
|
+
// const storyBoardContent = await readFile(storyBoardPath, 'utf8');
|
|
2054
|
+
// // 检查 storyBoard JSON 语法合法性
|
|
2055
|
+
// let storyBoard;
|
|
2056
|
+
// try {
|
|
2057
|
+
// storyBoard = JSON.parse(storyBoardContent);
|
|
2058
|
+
// } catch (jsonError) {
|
|
2059
|
+
// return createErrorResponse(
|
|
2060
|
+
// `storyBoard 文件 ${storyBoardFile} 存在 JSON 语法错误,请修复后重试。错误详情: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`,
|
|
2061
|
+
// 'generate-scene-tts'
|
|
2062
|
+
// );
|
|
2063
|
+
// }
|
|
2064
|
+
// if (storyBoard.scenes && Array.isArray(storyBoard.scenes)) {
|
|
2065
|
+
// scene = storyBoard.scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
|
|
2066
|
+
// if (scene) {
|
|
2067
|
+
// const script = scene.script;
|
|
2068
|
+
// let isValidText = false;
|
|
2069
|
+
// // 检查 text 是否严格等于 script
|
|
2070
|
+
// if (script && text === script) {
|
|
2071
|
+
// isValidText = true;
|
|
2072
|
+
// }
|
|
2073
|
+
// // 检查 text 是否严格等于 dialog 数组中某个元素的 script
|
|
2074
|
+
// if (
|
|
2075
|
+
// !isValidText &&
|
|
2076
|
+
// scene.dialog &&
|
|
2077
|
+
// Array.isArray(scene.dialog)
|
|
2078
|
+
// ) {
|
|
2079
|
+
// for (const dialogItem of scene.dialog) {
|
|
2080
|
+
// if (dialogItem.script && text === dialogItem.script) {
|
|
2081
|
+
// isValidText = true;
|
|
2082
|
+
// break;
|
|
2083
|
+
// }
|
|
2084
|
+
// }
|
|
2085
|
+
// }
|
|
2086
|
+
// if (!isValidText) {
|
|
2087
|
+
// return createErrorResponse(
|
|
2088
|
+
// '配音文本必须严格遵照storyboard的设定,如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用',
|
|
2089
|
+
// 'generate-scene-tts'
|
|
2090
|
+
// );
|
|
2091
|
+
// }
|
|
2092
|
+
// } else {
|
|
2093
|
+
// console.warn(
|
|
2094
|
+
// `Scene index ${sceneIndex} not found in storyboard.json`
|
|
2095
|
+
// );
|
|
2096
|
+
// }
|
|
2097
|
+
// }
|
|
2098
|
+
// } else {
|
|
2099
|
+
// console.warn(`Story board file not found: ${storyBoardPath}`);
|
|
2100
|
+
// }
|
|
2101
|
+
// } catch (error) {
|
|
2102
|
+
// console.error('Failed to validate text with story board:', error);
|
|
2103
|
+
// // 如果读取或解析 storyboard.json 失败,继续执行但记录警告
|
|
2104
|
+
// }
|
|
2105
|
+
// }
|
|
2106
|
+
// console.log(
|
|
2107
|
+
// `Generating TTS with voice: ${voiceID}, speed: ${finalSpeed}, text: ${text.substring(0, 100)}...`
|
|
2108
|
+
// );
|
|
2109
|
+
// if (voiceID.startsWith('BV0')) {
|
|
2110
|
+
// throw new Error(
|
|
2111
|
+
// `BV0* 系列音色已弃用,你必须通过 pick-voice 工具挑选一个真实存在的音色。`
|
|
2112
|
+
// );
|
|
2113
|
+
// }
|
|
2114
|
+
// const type =
|
|
2115
|
+
// voiceID.startsWith('zh_') ||
|
|
2116
|
+
// voiceID.startsWith('en_') ||
|
|
2117
|
+
// voiceID.startsWith('multi_') ||
|
|
2118
|
+
// voiceID.startsWith('saturn_') ||
|
|
2119
|
+
// voiceID.startsWith('ICL_')
|
|
2120
|
+
// ? 'volcano'
|
|
2121
|
+
// : 'minimax';
|
|
2122
|
+
// let res;
|
|
2123
|
+
// let emotion = 'auto';
|
|
2124
|
+
// if (type === 'volcano') {
|
|
2125
|
+
// volume = Math.max(Math.min(volume, 2.0), 0.5);
|
|
2126
|
+
// res = await ai.textToSpeechVolc({
|
|
2127
|
+
// text: text.trim(),
|
|
2128
|
+
// speaker: voiceID,
|
|
2129
|
+
// speed: Math.floor(100 * (finalSpeed - 1)),
|
|
2130
|
+
// volume: Math.floor(100 * (volume - 1)),
|
|
2131
|
+
// context_texts,
|
|
2132
|
+
// explicit_language,
|
|
2133
|
+
// voice_to_caption:
|
|
2134
|
+
// explicit_language === 'zh' || explicit_language === 'en',
|
|
2135
|
+
// });
|
|
2136
|
+
// } else {
|
|
2137
|
+
// emotion = 'neutral';
|
|
2138
|
+
// if (context_texts.length > 0) {
|
|
2139
|
+
// const prompt = `根据用户输入语音内容和上下文内容,从文字判断语音合理的情感,然后选择以下情感**之一**返回结果:
|
|
2140
|
+
// "happy", "sad", "angry", "fearful", "disgusted", "surprised", "calm", "fluent", "whisper", "neutral"
|
|
2141
|
+
// ## 要求
|
|
2142
|
+
// 输出 JSON 格式,包含一个 emotion 字段,值为以上情感之一。
|
|
2143
|
+
// `;
|
|
2144
|
+
// const schema = {
|
|
2145
|
+
// name: 'emotion_schema',
|
|
2146
|
+
// schema: {
|
|
2147
|
+
// type: 'object',
|
|
2148
|
+
// properties: {
|
|
2149
|
+
// emotion: {
|
|
2150
|
+
// type: 'string',
|
|
2151
|
+
// enum: [
|
|
2152
|
+
// 'neutral',
|
|
2153
|
+
// 'happy',
|
|
2154
|
+
// 'sad',
|
|
2155
|
+
// 'angry',
|
|
2156
|
+
// 'fearful',
|
|
2157
|
+
// 'disgusted',
|
|
2158
|
+
// 'surprised',
|
|
2159
|
+
// 'calm',
|
|
2160
|
+
// 'fluent',
|
|
2161
|
+
// 'whisper',
|
|
2162
|
+
// ],
|
|
2163
|
+
// description: '用户输入语音的情感',
|
|
2164
|
+
// },
|
|
2165
|
+
// },
|
|
2166
|
+
// required: ['emotion'],
|
|
2167
|
+
// },
|
|
2168
|
+
// };
|
|
2169
|
+
// const payload: any = {
|
|
2170
|
+
// model: 'Doubao-Seed-1.6',
|
|
2171
|
+
// messages: [
|
|
2172
|
+
// {
|
|
2173
|
+
// role: 'system',
|
|
2174
|
+
// content: prompt,
|
|
2175
|
+
// },
|
|
2176
|
+
// {
|
|
2177
|
+
// role: 'user',
|
|
2178
|
+
// content: `## 语音内容:
|
|
2179
|
+
// ${text.trim()}
|
|
2180
|
+
// ## 语音上下文
|
|
2181
|
+
// ${context_texts.join('\n')}
|
|
2182
|
+
// `,
|
|
2183
|
+
// },
|
|
2184
|
+
// ],
|
|
2185
|
+
// response_format: {
|
|
2186
|
+
// type: 'json_schema',
|
|
2187
|
+
// json_schema: schema,
|
|
2188
|
+
// },
|
|
2189
|
+
// };
|
|
2190
|
+
// const completion = await ai.getCompletions(payload);
|
|
2191
|
+
// const emotionObj = JSON.parse(
|
|
2192
|
+
// completion.choices[0]?.message?.content ?? '{}'
|
|
2193
|
+
// );
|
|
2194
|
+
// emotion = emotionObj.emotion ?? 'neutral';
|
|
2195
|
+
// }
|
|
2196
|
+
// res = await ai.textToSpeech({
|
|
2197
|
+
// text: text.trim(),
|
|
2198
|
+
// voiceName: voiceID,
|
|
2199
|
+
// speed: finalSpeed,
|
|
2200
|
+
// pitch,
|
|
2201
|
+
// volume,
|
|
2202
|
+
// emotion,
|
|
2203
|
+
// voice_to_caption:
|
|
2204
|
+
// explicit_language === 'zh' || explicit_language === 'en',
|
|
2205
|
+
// });
|
|
2206
|
+
// }
|
|
2207
|
+
// if (!res) {
|
|
2208
|
+
// throw new Error('Failed to generate TTS: no response from AI service');
|
|
2209
|
+
// }
|
|
2210
|
+
// if (res.url) {
|
|
2211
|
+
// console.log('TTS generated successfully, saving to materials...');
|
|
2212
|
+
// const { url, duration, ...opts } = res;
|
|
2213
|
+
// if (!skipConsistencyCheck && duration > 16) {
|
|
2214
|
+
// return createErrorResponse(
|
|
2215
|
+
// 'TTS duration exceeds 16 seconds, 建议调整文本长度、提升语速或拆分场景...,⚠️如简化文本内容或拆分文本,需要立即更新 storyboard 以保持内容同步!如仍要生成,可设置 skipConsistencyCheck 为 true,跳过一致性检查。',
|
|
2216
|
+
// 'generate-scene-tts'
|
|
2217
|
+
// );
|
|
2218
|
+
// }
|
|
2219
|
+
// if (!duration) {
|
|
2220
|
+
// return createErrorResponse(
|
|
2221
|
+
// 'TTS duration not returned from AI service',
|
|
2222
|
+
// 'generate-scene-tts'
|
|
2223
|
+
// );
|
|
2224
|
+
// }
|
|
2225
|
+
// const uri = await saveMaterial(currentSession, url, validatedFileName);
|
|
2226
|
+
// let warn = '';
|
|
2227
|
+
// if (scene) {
|
|
2228
|
+
// const minDur = Math.ceil(duration);
|
|
2229
|
+
// if (scene.audio_mode === 'vo_sync' && scene.duration !== minDur) {
|
|
2230
|
+
// warn = `场景${sceneIndex}设定的时长${scene.duration}秒与实际生成的语音时长${minDur}秒不一致,音画同步将有问题,建议修改场景时长为${minDur}秒`;
|
|
2231
|
+
// } else if (scene.duration < minDur) {
|
|
2232
|
+
// warn = `场景${sceneIndex}设定的时长${scene.duration}秒小于实际生成的语音时长${duration}秒,可能会导致场景结束时音频未播放完成,建议修改场景时长为${minDur}秒`;
|
|
2233
|
+
// }
|
|
2234
|
+
// }
|
|
2235
|
+
// const result = {
|
|
2236
|
+
// success: true,
|
|
2237
|
+
// warn,
|
|
2238
|
+
// source: url, // 方便调试
|
|
2239
|
+
// uri,
|
|
2240
|
+
// durationMs: Math.floor((duration || 0) * 1000),
|
|
2241
|
+
// text,
|
|
2242
|
+
// emotion,
|
|
2243
|
+
// context_texts,
|
|
2244
|
+
// voiceName: voiceID,
|
|
2245
|
+
// speed: finalSpeed,
|
|
2246
|
+
// timestamp: new Date().toISOString(),
|
|
2247
|
+
// ...opts,
|
|
2248
|
+
// };
|
|
2249
|
+
// // Update media_logs.json
|
|
2250
|
+
// try {
|
|
2251
|
+
// await updateMediaLogs(
|
|
2252
|
+
// currentSession,
|
|
2253
|
+
// validatedFileName,
|
|
2254
|
+
// result,
|
|
2255
|
+
// 'audio'
|
|
2256
|
+
// );
|
|
2257
|
+
// } catch (error) {
|
|
2258
|
+
// console.warn(
|
|
2259
|
+
// `Failed to update media_logs.json for ${validatedFileName}:`,
|
|
2260
|
+
// error
|
|
2261
|
+
// );
|
|
2262
|
+
// }
|
|
2263
|
+
// return {
|
|
2264
|
+
// content: [
|
|
2265
|
+
// {
|
|
2266
|
+
// type: 'text' as const,
|
|
2267
|
+
// text: JSON.stringify(result),
|
|
2268
|
+
// },
|
|
2269
|
+
// ],
|
|
2270
|
+
// };
|
|
2271
|
+
// } else {
|
|
2272
|
+
// console.warn('TTS generation completed but no URL returned');
|
|
2273
|
+
// return {
|
|
2274
|
+
// content: [
|
|
2275
|
+
// {
|
|
2276
|
+
// type: 'text' as const,
|
|
2277
|
+
// text: JSON.stringify({
|
|
2278
|
+
// success: false,
|
|
2279
|
+
// error:
|
|
2280
|
+
// 'No TTS URL returned from AI service. You should use pick-voice tool to pick an available voice.',
|
|
2281
|
+
// response: res,
|
|
2282
|
+
// timestamp: new Date().toISOString(),
|
|
2283
|
+
// }),
|
|
2284
|
+
// },
|
|
2285
|
+
// ],
|
|
2286
|
+
// };
|
|
2287
|
+
// }
|
|
2288
|
+
// } catch (error) {
|
|
2289
|
+
// return createErrorResponse(error, 'generate-scene-tts');
|
|
2290
|
+
// }
|
|
2291
|
+
// }
|
|
2292
|
+
// );
|
|
2293
|
+
// server.registerTool(
|
|
2294
|
+
// 'compile-and-run',
|
|
2295
|
+
// {
|
|
2296
|
+
// title: 'Compile And Run',
|
|
2297
|
+
// description: 'Compile project to ffmpeg command and run it.',
|
|
2298
|
+
// inputSchema: {
|
|
2299
|
+
// projectFileName: z
|
|
2300
|
+
// .string()
|
|
2301
|
+
// .describe('The VideoProject configuration object.'),
|
|
2302
|
+
// outputFileName: z
|
|
2303
|
+
// .string()
|
|
2304
|
+
// .optional()
|
|
2305
|
+
// .describe('Output video filename (optional, defaults to output.mp4).'),
|
|
2306
|
+
// },
|
|
2307
|
+
// },
|
|
2308
|
+
// async ({ projectFileName, outputFileName }) => {
|
|
2309
|
+
// try {
|
|
2310
|
+
// // 验证session状态
|
|
2311
|
+
// const currentSession = await validateSession('compile-and-run');
|
|
2312
|
+
// // 检查字幕内容匹配标记
|
|
2313
|
+
// if (!checkStoryboardSubtitlesFlag) {
|
|
2314
|
+
// checkStoryboardSubtitlesFlag = true;
|
|
2315
|
+
// return createErrorResponse(
|
|
2316
|
+
// `请先对 draft_content 进行以下一致性检查:
|
|
2317
|
+
// 1. 检查字幕文字内容是否与 storyboard 中各个场景的 script 或 dialog 内容完全一致(⚠️ 允许字幕分段展示,只要最终文本保持一致就行)
|
|
2318
|
+
// 2. 检查视频 resolution 设定是否与 storyboard 的 orientation 设置一致,默认 720p 情况下视频尺寸应为横屏 1280x720,竖屏 720x1280,若视频为 1080p 则尺寸应分别为横屏 1920x1080 和竖屏 1080x1920,切勿设反
|
|
2319
|
+
// 3. 除非用户明确表示不要背景音乐,否则应检查是否有生成并配置了 BGM,若无,则生成 BGM 并将其加入素材和轨道配置
|
|
2320
|
+
// 以上检查任何一项有问题,先修复 draft_content 使其符合要求后再进行合成`,
|
|
2321
|
+
// 'compile-and-run'
|
|
2322
|
+
// );
|
|
2323
|
+
// }
|
|
2324
|
+
// console.log('Starting video compilation and rendering...');
|
|
2325
|
+
// // 验证terminal可用性
|
|
2326
|
+
// const terminal = currentSession.terminal;
|
|
2327
|
+
// if (!terminal) {
|
|
2328
|
+
// throw new Error('Terminal not available in current session');
|
|
2329
|
+
// }
|
|
2330
|
+
// const localProjectFile = resolve(
|
|
2331
|
+
// projectLocalDir,
|
|
2332
|
+
// basename(projectFileName)
|
|
2333
|
+
// );
|
|
2334
|
+
// const project = JSON.parse(await readFile(localProjectFile, 'utf-8'));
|
|
2335
|
+
// // 验证输出文件名安全性
|
|
2336
|
+
// const outFile = outputFileName || project.export.outFile || 'output.mp4';
|
|
2337
|
+
// const validatedFileName = validateFileName(outFile);
|
|
2338
|
+
// console.log(`Output file: ${validatedFileName}`);
|
|
2339
|
+
// // 构建工作目录路径
|
|
2340
|
+
// const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}`;
|
|
2341
|
+
// const outputDir = `${workDir}/output`;
|
|
2342
|
+
// const outputPath = `${outputDir}/${validatedFileName}`;
|
|
2343
|
+
// // Project已经通过zVideoProject schema验证
|
|
2344
|
+
// const validated = { ...project };
|
|
2345
|
+
// // 更新导出配置
|
|
2346
|
+
// validated.export = {
|
|
2347
|
+
// ...validated.export,
|
|
2348
|
+
// outFile: outputPath,
|
|
2349
|
+
// };
|
|
2350
|
+
// console.log('Compiling VideoProject to FFmpeg command...');
|
|
2351
|
+
// // 编译为FFmpeg命令
|
|
2352
|
+
// let compiled: CompileResult;
|
|
2353
|
+
// try {
|
|
2354
|
+
// compiled = await compileToFfmpeg(validated, {
|
|
2355
|
+
// workingDir: outputDir,
|
|
2356
|
+
// subtitleStrategy: 'ass',
|
|
2357
|
+
// subtitlesFileName: `${outFile.replace(/\.mp4$/, '')}.subtitles.ass`,
|
|
2358
|
+
// });
|
|
2359
|
+
// } catch (compileError) {
|
|
2360
|
+
// console.error('Failed to compile VideoProject:', compileError);
|
|
2361
|
+
// throw new Error(`Failed to compile VideoProject: ${compileError}`);
|
|
2362
|
+
// }
|
|
2363
|
+
// console.log(`FFmpeg command generated (${compiled.cmd.length} chars)`);
|
|
2364
|
+
// console.log('FFmpeg Command:', compiled.cmd.substring(0, 200) + '...');
|
|
2365
|
+
// // 执行FFmpeg命令
|
|
2366
|
+
// console.log('Executing FFmpeg command...');
|
|
2367
|
+
// const result = await runFfmpeg(currentSession, compiled);
|
|
2368
|
+
// if (result.exitCode === 0) {
|
|
2369
|
+
// console.log('Video compilation completed successfully');
|
|
2370
|
+
// // 自动下载输出文件
|
|
2371
|
+
// console.log('Starting automatic download of output files...');
|
|
2372
|
+
// let downloadResult = null;
|
|
2373
|
+
// try {
|
|
2374
|
+
// const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/output`;
|
|
2375
|
+
// let outputs: string[] = [];
|
|
2376
|
+
// try {
|
|
2377
|
+
// outputs = (await currentSession.files.listFiles(workDir)) || [];
|
|
2378
|
+
// } catch (listError) {
|
|
2379
|
+
// console.warn('Failed to list output files:', listError);
|
|
2380
|
+
// outputs = [];
|
|
2381
|
+
// }
|
|
2382
|
+
// if (outputs.length > 0) {
|
|
2383
|
+
// const outputDir = resolve(
|
|
2384
|
+
// process.env.ZEROCUT_PROJECT_CWD || process.cwd(),
|
|
2385
|
+
// projectLocalDir,
|
|
2386
|
+
// 'output'
|
|
2387
|
+
// );
|
|
2388
|
+
// await mkdir(outputDir, { recursive: true });
|
|
2389
|
+
// const downloadErrors: string[] = [];
|
|
2390
|
+
// const successfulDownloads: string[] = [];
|
|
2391
|
+
// const promises = outputs.map(async output => {
|
|
2392
|
+
// try {
|
|
2393
|
+
// await currentSession.files.download(
|
|
2394
|
+
// `${workDir}/${output}`,
|
|
2395
|
+
// `${outputDir}/${output}`
|
|
2396
|
+
// );
|
|
2397
|
+
// successfulDownloads.push(output);
|
|
2398
|
+
// return output;
|
|
2399
|
+
// } catch (downloadError) {
|
|
2400
|
+
// const errorMsg = `Failed to download ${output}: ${downloadError}`;
|
|
2401
|
+
// console.error(errorMsg);
|
|
2402
|
+
// downloadErrors.push(errorMsg);
|
|
2403
|
+
// return null;
|
|
2404
|
+
// }
|
|
2405
|
+
// });
|
|
2406
|
+
// const results = await Promise.all(promises);
|
|
2407
|
+
// const sources = results.filter(
|
|
2408
|
+
// output => output !== null
|
|
2409
|
+
// ) as string[];
|
|
2410
|
+
// downloadResult = {
|
|
2411
|
+
// totalFiles: outputs.length,
|
|
2412
|
+
// successfulDownloads: sources.length,
|
|
2413
|
+
// failedDownloads: downloadErrors.length,
|
|
2414
|
+
// downloadErrors:
|
|
2415
|
+
// downloadErrors.length > 0 ? downloadErrors : undefined,
|
|
2416
|
+
// downloadPath: outputDir,
|
|
2417
|
+
// sources,
|
|
2418
|
+
// };
|
|
2419
|
+
// console.log(
|
|
2420
|
+
// `Download completed: ${sources.length}/${outputs.length} files successful`
|
|
2421
|
+
// );
|
|
2422
|
+
// } else {
|
|
2423
|
+
// console.log('No output files found to download');
|
|
2424
|
+
// downloadResult = {
|
|
2425
|
+
// totalFiles: 0,
|
|
2426
|
+
// successfulDownloads: 0,
|
|
2427
|
+
// failedDownloads: 0,
|
|
2428
|
+
// message: 'No output files found to download',
|
|
2429
|
+
// };
|
|
2430
|
+
// }
|
|
2431
|
+
// } catch (downloadError) {
|
|
2432
|
+
// console.error('Download process failed:', downloadError);
|
|
2433
|
+
// downloadResult = {
|
|
2434
|
+
// error: `Download failed: ${downloadError}`,
|
|
2435
|
+
// totalFiles: 0,
|
|
2436
|
+
// successfulDownloads: 0,
|
|
2437
|
+
// failedDownloads: 1,
|
|
2438
|
+
// };
|
|
2439
|
+
// }
|
|
2440
|
+
// const successResult = {
|
|
2441
|
+
// success: true,
|
|
2442
|
+
// outputPath,
|
|
2443
|
+
// outputFileName: validatedFileName,
|
|
2444
|
+
// command: compiled.cmd,
|
|
2445
|
+
// message: 'Video compilation and download completed successfully',
|
|
2446
|
+
// download: downloadResult,
|
|
2447
|
+
// timestamp: new Date().toISOString(),
|
|
2448
|
+
// };
|
|
2449
|
+
// return {
|
|
2450
|
+
// content: [
|
|
2451
|
+
// {
|
|
2452
|
+
// type: 'text' as const,
|
|
2453
|
+
// text: JSON.stringify(successResult),
|
|
2454
|
+
// },
|
|
2455
|
+
// ],
|
|
2456
|
+
// };
|
|
2457
|
+
// } else {
|
|
2458
|
+
// console.error(`FFmpeg failed with exit code: ${result.exitCode}`);
|
|
2459
|
+
// const failureResult = {
|
|
2460
|
+
// success: false,
|
|
2461
|
+
// exitCode: result.exitCode,
|
|
2462
|
+
// outputPath,
|
|
2463
|
+
// // command: compiled.cmd,
|
|
2464
|
+
// stderr: result.stderr,
|
|
2465
|
+
// message: `FFmpeg exited with code ${result.exitCode}`,
|
|
2466
|
+
// timestamp: new Date().toISOString(),
|
|
2467
|
+
// };
|
|
2468
|
+
// if (result.exitCode === 254) {
|
|
2469
|
+
// failureResult.message =
|
|
2470
|
+
// 'FFmpeg failed with code 254. Close current session immediately (inMinutes = 0) and re-open a new session to try again.';
|
|
2471
|
+
// }
|
|
2472
|
+
// return {
|
|
2473
|
+
// content: [
|
|
2474
|
+
// {
|
|
2475
|
+
// type: 'text' as const,
|
|
2476
|
+
// text: JSON.stringify(failureResult),
|
|
2477
|
+
// },
|
|
2478
|
+
// ],
|
|
2479
|
+
// };
|
|
2480
|
+
// }
|
|
2481
|
+
// } catch (error) {
|
|
2482
|
+
// return createErrorResponse(error, 'compile-and-run');
|
|
2483
|
+
// }
|
|
2484
|
+
// }
|
|
2485
|
+
// );
|
|
2486
|
+
// server.registerTool(
|
|
2487
|
+
// 'get-schema',
|
|
2488
|
+
// {
|
|
2489
|
+
// title: 'Get Storyboard Schema or Draft Content Schema',
|
|
2490
|
+
// description:
|
|
2491
|
+
// 'Get the complete Storyboard or Draft Content JSON Schema definition. Use this schema to validate storyboard.json or draft_content.json files.',
|
|
2492
|
+
// inputSchema: {
|
|
2493
|
+
// type: z
|
|
2494
|
+
// .enum(['storyboard', 'draft_content'])
|
|
2495
|
+
// .describe(
|
|
2496
|
+
// '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'
|
|
2497
|
+
// ),
|
|
2498
|
+
// },
|
|
2499
|
+
// },
|
|
2500
|
+
// async ({ type }) => {
|
|
2501
|
+
// try {
|
|
2502
|
+
// const schemaPath = resolve(__dirname, `./prompts/${type}-schema.json`);
|
|
2503
|
+
// const schemaContent = await readFile(schemaPath, 'utf-8');
|
|
2504
|
+
// const schema = JSON.parse(schemaContent);
|
|
2505
|
+
// let important_guidelines = '';
|
|
2506
|
+
// if (type === 'draft_content') {
|
|
2507
|
+
// important_guidelines = `⚠️ 生成文件时请严格遵守输出规范,字幕文本内容必须与 storyboard.json 中的 script(或dialog) 字段的文本内容完全一致。
|
|
2508
|
+
// ** 字幕优化 **
|
|
2509
|
+
// * 在保证字幕文本内容与 storyboard.json 中的 script(或dialog) 字段的文本内容完全一致的前提下,可根据 tts 返回的 \`captions.utterances\` 字段对字幕的显示进行优化,将过长的字幕分段显示,在 draft_content.json 中使用分段字幕,captions 的内容在 media_logs.json 中可查询到。
|
|
2510
|
+
// * 如用户未特殊指定,字幕样式(字体及大小)务必遵守输出规范
|
|
2511
|
+
// `;
|
|
2512
|
+
// }
|
|
2513
|
+
// return {
|
|
2514
|
+
// content: [
|
|
2515
|
+
// {
|
|
2516
|
+
// type: 'text' as const,
|
|
2517
|
+
// text: JSON.stringify({
|
|
2518
|
+
// success: true,
|
|
2519
|
+
// schema,
|
|
2520
|
+
// important_guidelines,
|
|
2521
|
+
// timestamp: new Date().toISOString(),
|
|
2522
|
+
// }),
|
|
2523
|
+
// },
|
|
2524
|
+
// ],
|
|
2525
|
+
// };
|
|
2526
|
+
// } catch (error) {
|
|
2527
|
+
// return createErrorResponse(error, 'get-schema');
|
|
2528
|
+
// }
|
|
2529
|
+
// }
|
|
2530
|
+
// );
|
|
2531
|
+
// server.registerTool(
|
|
2532
|
+
// 'pick-voice',
|
|
2533
|
+
// {
|
|
2534
|
+
// title: 'Pick Voice',
|
|
2535
|
+
// description:
|
|
2536
|
+
// '根据用户需求,选择尽可能符合要求的语音,在合适的情况下,优先采用 volcano_tts_2 类型的语音',
|
|
2537
|
+
// inputSchema: {
|
|
2538
|
+
// prompt: z
|
|
2539
|
+
// .string()
|
|
2540
|
+
// .describe('用户需求描述,例如:一个有亲和力的,适合给孩子讲故事的语音'),
|
|
2541
|
+
// custom_design: z
|
|
2542
|
+
// .boolean()
|
|
2543
|
+
// .optional()
|
|
2544
|
+
// .describe(
|
|
2545
|
+
// '是否自定义语音,由于要消耗较多积分,因此**只有用户明确要求自己设计语音**,才将该参数设为true'
|
|
2546
|
+
// ),
|
|
2547
|
+
// custom_design_preview: z
|
|
2548
|
+
// .string()
|
|
2549
|
+
// .optional()
|
|
2550
|
+
// .describe(
|
|
2551
|
+
// '用户自定义语音的预览文本,用于展示自定义语音的效果,只有 custom_design 为 true 时才需要'
|
|
2552
|
+
// ),
|
|
2553
|
+
// custom_design_save_to: z
|
|
2554
|
+
// .string()
|
|
2555
|
+
// .optional()
|
|
2556
|
+
// .describe(
|
|
2557
|
+
// '自定义语音的保存路径,例如:custom_voice.mp3 custom_voice_{id}.mp3'
|
|
2558
|
+
// ),
|
|
2559
|
+
// },
|
|
2560
|
+
// },
|
|
2561
|
+
// async ({
|
|
2562
|
+
// prompt,
|
|
2563
|
+
// custom_design,
|
|
2564
|
+
// custom_design_preview,
|
|
2565
|
+
// custom_design_save_to,
|
|
2566
|
+
// }) => {
|
|
2567
|
+
// try {
|
|
2568
|
+
// // 验证session状态
|
|
2569
|
+
// const currentSession = await validateSession('pick-voice');
|
|
2570
|
+
// const ai = currentSession.ai;
|
|
2571
|
+
// if (custom_design) {
|
|
2572
|
+
// if (!custom_design_preview) {
|
|
2573
|
+
// throw new Error(
|
|
2574
|
+
// 'custom_design_preview is required when custom_design is true'
|
|
2575
|
+
// );
|
|
2576
|
+
// }
|
|
2577
|
+
// const data = await currentSession.ai.voiceDesign({
|
|
2578
|
+
// prompt,
|
|
2579
|
+
// previewText: custom_design_preview,
|
|
2580
|
+
// });
|
|
2581
|
+
// if (data.voice_id) {
|
|
2582
|
+
// const trial_audio = data.trial_audio;
|
|
2583
|
+
// let uri = '';
|
|
2584
|
+
// if (trial_audio) {
|
|
2585
|
+
// uri = await saveMaterial(
|
|
2586
|
+
// currentSession,
|
|
2587
|
+
// trial_audio,
|
|
2588
|
+
// custom_design_save_to || `custom_voice_${data.voice_id}.mp3`
|
|
2589
|
+
// );
|
|
2590
|
+
// }
|
|
2591
|
+
// return {
|
|
2592
|
+
// content: [
|
|
2593
|
+
// {
|
|
2594
|
+
// type: 'text' as const,
|
|
2595
|
+
// text: JSON.stringify({
|
|
2596
|
+
// success: true,
|
|
2597
|
+
// ...data,
|
|
2598
|
+
// uri,
|
|
2599
|
+
// timestamp: new Date().toISOString(),
|
|
2600
|
+
// }),
|
|
2601
|
+
// },
|
|
2602
|
+
// ],
|
|
2603
|
+
// };
|
|
2604
|
+
// } else {
|
|
2605
|
+
// throw new Error(`Voice design failed, ${JSON.stringify(data)}`);
|
|
2606
|
+
// }
|
|
2607
|
+
// }
|
|
2608
|
+
// const data = await ai.pickVoice({ prompt });
|
|
2609
|
+
// return {
|
|
2610
|
+
// content: [
|
|
2611
|
+
// {
|
|
2612
|
+
// type: 'text' as const,
|
|
2613
|
+
// text: JSON.stringify({
|
|
2614
|
+
// success: true,
|
|
2615
|
+
// ...data,
|
|
2616
|
+
// timestamp: new Date().toISOString(),
|
|
2617
|
+
// }),
|
|
2618
|
+
// },
|
|
2619
|
+
// ],
|
|
2620
|
+
// };
|
|
2621
|
+
// } catch (error) {
|
|
2622
|
+
// return createErrorResponse(error, 'pick-voice');
|
|
2623
|
+
// }
|
|
2624
|
+
// }
|
|
2625
|
+
// );
|
|
2626
|
+
let lastEffect = '';
|
|
2627
|
+
server.registerTool('generate-video', {
|
|
2628
|
+
title: 'Generate Video',
|
|
2629
|
+
description: `图生视频和首尾帧生视频工具`,
|
|
2311
2630
|
inputSchema: {
|
|
2312
|
-
|
|
2631
|
+
prompt: zod_1.z
|
|
2313
2632
|
.string()
|
|
2314
|
-
.describe('The
|
|
2315
|
-
|
|
2316
|
-
.boolean()
|
|
2317
|
-
.describe('Whether to create a sound effect that loops smoothly. '),
|
|
2318
|
-
duration_seconds: zod_1.z
|
|
2633
|
+
.describe('The prompt to generate. 一般要严格对应 storyboard 中当前场景的 video_prompt 字段描述;传这个参数时,若跳过了一致性检查,记得保留镜头切换语言,如“切镜至第二镜头”这样的指令不应当省略。'),
|
|
2634
|
+
sceneIndex: zod_1.z
|
|
2319
2635
|
.number()
|
|
2320
|
-
.min(
|
|
2321
|
-
.max(30)
|
|
2636
|
+
.min(1)
|
|
2322
2637
|
.optional()
|
|
2323
|
-
.describe('
|
|
2324
|
-
|
|
2325
|
-
.string()
|
|
2326
|
-
.describe('The filename to save. 应该是mp3文件'),
|
|
2327
|
-
},
|
|
2328
|
-
}, async ({ prompt_in_english, loop, saveToFileName, duration_seconds }) => {
|
|
2329
|
-
try {
|
|
2330
|
-
const currentSession = await validateSession('generate-sound-effect');
|
|
2331
|
-
const ai = currentSession.ai;
|
|
2332
|
-
const res = await ai.generateSoundEffect({
|
|
2333
|
-
prompt: prompt_in_english,
|
|
2334
|
-
loop,
|
|
2335
|
-
duration_seconds,
|
|
2336
|
-
});
|
|
2337
|
-
if (res.url) {
|
|
2338
|
-
const uri = await saveMaterial(currentSession, res.url, saveToFileName);
|
|
2339
|
-
// Update media_logs.json
|
|
2340
|
-
try {
|
|
2341
|
-
await updateMediaLogs(currentSession, saveToFileName, res, 'audio');
|
|
2342
|
-
}
|
|
2343
|
-
catch (error) {
|
|
2344
|
-
console.warn(`Failed to update media_logs.json for ${saveToFileName}:`, error);
|
|
2345
|
-
}
|
|
2346
|
-
return {
|
|
2347
|
-
content: [
|
|
2348
|
-
{
|
|
2349
|
-
type: 'text',
|
|
2350
|
-
text: JSON.stringify({
|
|
2351
|
-
success: true,
|
|
2352
|
-
uri,
|
|
2353
|
-
source: res.url,
|
|
2354
|
-
duration: res.duration,
|
|
2355
|
-
}),
|
|
2356
|
-
},
|
|
2357
|
-
],
|
|
2358
|
-
};
|
|
2359
|
-
}
|
|
2360
|
-
return {
|
|
2361
|
-
content: [
|
|
2362
|
-
{
|
|
2363
|
-
type: 'text',
|
|
2364
|
-
text: JSON.stringify(res),
|
|
2365
|
-
},
|
|
2366
|
-
],
|
|
2367
|
-
};
|
|
2368
|
-
}
|
|
2369
|
-
catch (error) {
|
|
2370
|
-
return createErrorResponse(error, 'generate-sound-effect');
|
|
2371
|
-
}
|
|
2372
|
-
});
|
|
2373
|
-
server.registerTool('generate-music-or-mv', {
|
|
2374
|
-
title: '创作音乐(Music)或音乐视频(Music Video)',
|
|
2375
|
-
description: '生成音乐,包括MV(music video)、BGM 或 歌曲',
|
|
2376
|
-
inputSchema: {
|
|
2377
|
-
prompt: zod_1.z.string().describe('The prompt to generate.'),
|
|
2378
|
-
singerPhoto: zod_1.z
|
|
2638
|
+
.describe('场景索引,从1开始的下标,如果非场景对应素材,则可不传,场景素材必传'),
|
|
2639
|
+
storyBoardFile: zod_1.z
|
|
2379
2640
|
.string()
|
|
2380
2641
|
.optional()
|
|
2381
|
-
.
|
|
2382
|
-
|
|
2383
|
-
|
|
2642
|
+
.default('storyboard.json')
|
|
2643
|
+
.describe('故事板文件路径'),
|
|
2644
|
+
skipConsistencyCheck: zod_1.z
|
|
2645
|
+
.boolean()
|
|
2384
2646
|
.optional()
|
|
2385
|
-
.
|
|
2386
|
-
.
|
|
2387
|
-
|
|
2647
|
+
.default(false)
|
|
2648
|
+
.describe('是否跳过一致性检查,默认为false(即默认进行一致性检查)'),
|
|
2649
|
+
skipCheckWithSceneReason: zod_1.z
|
|
2388
2650
|
.string()
|
|
2389
2651
|
.optional()
|
|
2390
|
-
.describe('
|
|
2391
|
-
|
|
2652
|
+
.describe('跳过校验的理由,如果skipConsistencyCheck设为true,必须要传这个参数'),
|
|
2653
|
+
type: zod_1.z
|
|
2654
|
+
.enum([
|
|
2655
|
+
'pro',
|
|
2656
|
+
'hailuo',
|
|
2657
|
+
'hailuo-fast',
|
|
2658
|
+
'vidu',
|
|
2659
|
+
'vidu-turbo',
|
|
2660
|
+
'vidu-pro',
|
|
2661
|
+
'vidu-uc',
|
|
2662
|
+
'vidu-uc-pro',
|
|
2663
|
+
'kling',
|
|
2664
|
+
'pixv',
|
|
2665
|
+
'veo3.1',
|
|
2666
|
+
'veo3.1-pro',
|
|
2667
|
+
'kenburns',
|
|
2668
|
+
'zero',
|
|
2669
|
+
'zero-fast',
|
|
2670
|
+
])
|
|
2671
|
+
.default('vidu')
|
|
2672
|
+
.describe('除非用户明确提出使用其他模型,否则默认用vidu模型;zero 系列模型适合创作8-24秒带故事情节的短片'),
|
|
2673
|
+
mute: zod_1.z
|
|
2392
2674
|
.boolean()
|
|
2393
2675
|
.optional()
|
|
2394
|
-
.default(
|
|
2395
|
-
.describe('
|
|
2396
|
-
|
|
2397
|
-
.
|
|
2398
|
-
.describe('The type of music. Defaults to BGM. ⚠️ 如果 type 是 music_video,会直接生成音频和视频,**不需要**额外专门生成歌曲')
|
|
2399
|
-
.default('bgm'),
|
|
2400
|
-
model: zod_1.z
|
|
2401
|
-
.enum(['doubao', 'minimax'])
|
|
2676
|
+
.default(true)
|
|
2677
|
+
.describe('Whether to mute the video (effective for vidu,sora2 and veo3.1). 除非用户明确要求,否则默认静音;非静音模式下模型会自己配音,不需要再使用tts工具!'),
|
|
2678
|
+
seed: zod_1.z
|
|
2679
|
+
.number()
|
|
2402
2680
|
.optional()
|
|
2403
|
-
.describe('The
|
|
2404
|
-
|
|
2681
|
+
.describe('The seed to use for random number generation. 用户可以指定一个固定值来获得可重复的结果'),
|
|
2682
|
+
saveToFileName: zod_1.z
|
|
2683
|
+
.string()
|
|
2684
|
+
.describe('The filename to save. 应该是mp4文件'),
|
|
2685
|
+
start_frame: zod_1.z
|
|
2686
|
+
.string()
|
|
2687
|
+
.optional()
|
|
2688
|
+
.describe('The image file name of the start frame.'),
|
|
2405
2689
|
duration: zod_1.z
|
|
2406
2690
|
.number()
|
|
2407
|
-
.min(
|
|
2408
|
-
.max(
|
|
2409
|
-
.describe('The duration of the
|
|
2410
|
-
|
|
2691
|
+
.min(0)
|
|
2692
|
+
.max(30)
|
|
2693
|
+
.describe('The duration of the video. 可以传0,此时会根据视频提示词内容自动确定时长'),
|
|
2694
|
+
end_frame: zod_1.z
|
|
2695
|
+
.string()
|
|
2696
|
+
.optional()
|
|
2697
|
+
.describe('The image file name of the end frame (for non-zero models) or highlight frame (for zero models).'),
|
|
2698
|
+
saveLastFrameAs: zod_1.z
|
|
2699
|
+
.string()
|
|
2700
|
+
.optional()
|
|
2701
|
+
.describe('The filename to save the last frame. 只有用户要求使用镜头自然延伸时,才需要这个参数,默认不传'),
|
|
2702
|
+
resolution: zod_1.z
|
|
2703
|
+
.enum(['720p', '1080p'])
|
|
2704
|
+
.optional()
|
|
2705
|
+
.default('720p')
|
|
2706
|
+
.describe('除非用户明确提出使用其他分辨率,否则一律用720p'),
|
|
2707
|
+
optimizePrompt: zod_1.z
|
|
2411
2708
|
.boolean()
|
|
2709
|
+
.optional()
|
|
2412
2710
|
.default(false)
|
|
2413
|
-
.describe('Whether to
|
|
2414
|
-
saveToFileName: zod_1.z
|
|
2415
|
-
.string()
|
|
2416
|
-
.describe('The filename to save. 如果type是music video,应该是mp4文件,否则应该是mp3文件'),
|
|
2711
|
+
.describe('Whether to optimize the prompt.'),
|
|
2417
2712
|
},
|
|
2418
|
-
}, async ({ prompt,
|
|
2713
|
+
}, async ({ prompt, sceneIndex, storyBoardFile = 'storyboard.json', skipConsistencyCheck = false, saveToFileName, start_frame, end_frame, duration, resolution, type = 'vidu', optimizePrompt, saveLastFrameAs, mute = true, seed, }, context) => {
|
|
2419
2714
|
try {
|
|
2420
2715
|
// 验证session状态
|
|
2421
|
-
const currentSession = await validateSession('generate-
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
let progress = 0;
|
|
2426
|
-
if (type === 'bgm' && duration > 120) {
|
|
2427
|
-
throw new Error('BGM duration must be at most 120 seconds.');
|
|
2716
|
+
const currentSession = await validateSession('generate-video');
|
|
2717
|
+
const isZeroModel = type.startsWith('zero');
|
|
2718
|
+
if (!start_frame && !isZeroModel) {
|
|
2719
|
+
return createErrorResponse('start_frame 不能为空', 'generate-video');
|
|
2428
2720
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2721
|
+
if (!isZeroModel && duration > 16) {
|
|
2722
|
+
return createErrorResponse('非 zero 系列模型的视频仅支持 16 秒以下时长', 'generate-video');
|
|
2723
|
+
}
|
|
2724
|
+
else if (isZeroModel && duration < 8) {
|
|
2725
|
+
return createErrorResponse('zero 系列模型的视频仅支持 8 秒以上时长', 'generate-video');
|
|
2726
|
+
}
|
|
2727
|
+
if (type === 'zero-fast' && duration > 24) {
|
|
2728
|
+
return createErrorResponse('zero-fast 模型的视频仅支持 24 秒以下时长', 'generate-video');
|
|
2729
|
+
}
|
|
2730
|
+
if (type === 'zero' && resolution !== '1080p') {
|
|
2731
|
+
console.warn(`zero 模型的视频仅支持 1080p 分辨率,用户指定的分辨率为 %s,已自动将 ${resolution} 转换为 1080p`, resolution);
|
|
2732
|
+
resolution = '1080p';
|
|
2733
|
+
}
|
|
2734
|
+
// 校验 prompt 与 storyboard.json 中场景设定的一致性以及视频时长与 timeline_analysis.json 中 proposed_video_scenes 的匹配
|
|
2735
|
+
if (sceneIndex && !skipConsistencyCheck) {
|
|
2736
|
+
try {
|
|
2737
|
+
const storyBoardPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, storyBoardFile);
|
|
2738
|
+
if ((0, node_fs_1.existsSync)(storyBoardPath)) {
|
|
2739
|
+
const storyBoardContent = await (0, promises_1.readFile)(storyBoardPath, 'utf8');
|
|
2740
|
+
// 检查 storyBoard JSON 语法合法性
|
|
2741
|
+
let storyBoard;
|
|
2447
2742
|
try {
|
|
2448
|
-
|
|
2743
|
+
storyBoard = JSON.parse(storyBoardContent);
|
|
2744
|
+
}
|
|
2745
|
+
catch (jsonError) {
|
|
2746
|
+
return createErrorResponse(`storyBoard 文件 ${storyBoardFile} 存在 JSON 语法错误,请修复后重试。错误详情: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`, 'generate-video');
|
|
2747
|
+
}
|
|
2748
|
+
if (storyBoard.scenes && Array.isArray(storyBoard.scenes)) {
|
|
2749
|
+
const scene = storyBoard.scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
|
|
2750
|
+
if (scene) {
|
|
2751
|
+
const videoPrompt = scene.video_prompt;
|
|
2752
|
+
if (videoPrompt && prompt !== videoPrompt) {
|
|
2753
|
+
return createErrorResponse('视频提示词必须严格遵照storyboard的设定,如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-video');
|
|
2754
|
+
}
|
|
2755
|
+
if (scene.is_continuous && !end_frame) {
|
|
2756
|
+
return createErrorResponse('连续场景必须指定end_frame参数,如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-video');
|
|
2757
|
+
}
|
|
2758
|
+
if (scene.video_duration != null &&
|
|
2759
|
+
duration !== scene.video_duration) {
|
|
2760
|
+
return createErrorResponse(`视频时长必须严格遵照storyboard的设定,用户指定的时长为 ${duration} 秒,而 storyboard 中建议的时长为 ${scene.video_duration} 秒。如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用`, 'generate-video');
|
|
2761
|
+
}
|
|
2762
|
+
if (storyBoard.voice_type &&
|
|
2763
|
+
storyBoard.voice_type !== 'slient') {
|
|
2764
|
+
if (mute) {
|
|
2765
|
+
return createErrorResponse('有对话和旁白的场景不能静音,请将mute设为false再重新使用工具。如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-video');
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
// 检查 use_video_model 与 type 参数的一致性
|
|
2769
|
+
if (scene.use_video_model &&
|
|
2770
|
+
type &&
|
|
2771
|
+
scene.use_video_model !== type) {
|
|
2772
|
+
return createErrorResponse(`场景建议的视频模型(${scene.use_video_model})与传入的type参数(${type})不一致。请确保use_video_model与type参数值相同,或将skipConsistencyCheck设置为true后再次调用`, 'generate-video');
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
else {
|
|
2776
|
+
console.warn(`Scene index ${sceneIndex} not found in storyboard.json`);
|
|
2777
|
+
}
|
|
2449
2778
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2779
|
+
}
|
|
2780
|
+
else {
|
|
2781
|
+
console.warn(`Story board file not found: ${storyBoardPath}`);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
catch (error) {
|
|
2785
|
+
console.error('Failed to validate prompt with story board:', error);
|
|
2786
|
+
// 如果读取或解析 storyboard.json 失败,继续执行但记录警告
|
|
2787
|
+
}
|
|
2788
|
+
// 校验视频时长与 timeline_analysis.json 中 proposed_video_scenes 的匹配
|
|
2789
|
+
try {
|
|
2790
|
+
const timelineAnalysisPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'timeline_analysis.json');
|
|
2791
|
+
if ((0, node_fs_1.existsSync)(timelineAnalysisPath)) {
|
|
2792
|
+
const timelineAnalysisContent = await (0, promises_1.readFile)(timelineAnalysisPath, 'utf8');
|
|
2793
|
+
const timelineAnalysis = JSON.parse(timelineAnalysisContent);
|
|
2794
|
+
if (timelineAnalysis.proposed_video_scenes &&
|
|
2795
|
+
Array.isArray(timelineAnalysis.proposed_video_scenes)) {
|
|
2796
|
+
const videoScene = timelineAnalysis.proposed_video_scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
|
|
2797
|
+
if (videoScene && videoScene.video_duration_s) {
|
|
2798
|
+
const expectedDuration = videoScene.video_duration_s;
|
|
2799
|
+
if (duration !== expectedDuration) {
|
|
2800
|
+
return createErrorResponse(`视频时长必须与timeline_analysis中的设定匹配。当前场景${videoScene.scene_id}要求时长为${expectedDuration}秒,但传入的duration为${duration}秒。请调整duration参数为${expectedDuration}秒后重试。如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用。`, 'generate-video');
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
else {
|
|
2804
|
+
console.warn(`Scene ${sceneIndex} (scene_${sceneIndex.toString().padStart(2, '0')}) not found in timeline_analysis.json proposed_video_scenes`);
|
|
2805
|
+
}
|
|
2452
2806
|
}
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
}
|
|
2457
|
-
else {
|
|
2458
|
-
const finalPrompt = `${prompt.trim()} ${type === 'bgm' ? `纯音乐无歌词,时长${duration}秒` : `时长${duration}秒,使用${model}模型`}`;
|
|
2459
|
-
res = await ai.generateMusic({
|
|
2460
|
-
prompt: finalPrompt,
|
|
2461
|
-
skipCopyCheck,
|
|
2462
|
-
onProgress: async (metaData) => {
|
|
2807
|
+
}
|
|
2808
|
+
else {
|
|
2809
|
+
// 检查音频时长标志
|
|
2463
2810
|
try {
|
|
2464
|
-
|
|
2811
|
+
const storyBoardPath = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, storyBoardFile);
|
|
2812
|
+
if ((0, node_fs_1.existsSync)(storyBoardPath)) {
|
|
2813
|
+
const storyBoardContent = await (0, promises_1.readFile)(storyBoardPath, 'utf8');
|
|
2814
|
+
const storyBoard = JSON.parse(storyBoardContent);
|
|
2815
|
+
if (storyBoard.scenes && Array.isArray(storyBoard.scenes)) {
|
|
2816
|
+
const scene = storyBoard.scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
|
|
2817
|
+
if (scene &&
|
|
2818
|
+
((scene.script && scene.script.trim() !== '') ||
|
|
2819
|
+
scene.dialog)) {
|
|
2820
|
+
if (!checkAudioVideoDurationFlag) {
|
|
2821
|
+
checkAudioVideoDurationFlag = true;
|
|
2822
|
+
if (scene.audio_mode === 'vo_sync') {
|
|
2823
|
+
return createErrorResponse('请先自我检查 media_logs 中的音频时长,确保 storyboard 中视频时长为音频时长向上取整 即 ceil(音频时长),然后再按照正确的视频时长创建视频', 'generate-video');
|
|
2824
|
+
}
|
|
2825
|
+
else if (scene.audio_mode === 'dialogue') {
|
|
2826
|
+
return createErrorResponse('请先自我检查 media_logs 中的音频时长,确保 storyboard 中视频时长**不小于**音频时长向上取整 即 ceil(音频时长),然后再按照正确的视频时长创建视频', 'generate-video');
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2465
2832
|
}
|
|
2466
|
-
catch (
|
|
2467
|
-
console.
|
|
2833
|
+
catch (error) {
|
|
2834
|
+
console.error('Failed to check audio duration flag:', error);
|
|
2468
2835
|
}
|
|
2469
|
-
}
|
|
2470
|
-
});
|
|
2471
|
-
}
|
|
2472
|
-
if (!res) {
|
|
2473
|
-
throw new Error('Failed to generate Music: no response from AI service');
|
|
2474
|
-
}
|
|
2475
|
-
if (res.url) {
|
|
2476
|
-
console.log('Music generated successfully, saving to materials...');
|
|
2477
|
-
const uri = await saveMaterial(currentSession, res.url, validatedFileName);
|
|
2478
|
-
const { url, duration: bgmDuration, captions, ...opts } = res;
|
|
2479
|
-
// 保存captions到本地
|
|
2480
|
-
if (captions) {
|
|
2481
|
-
const captionsText = JSON.stringify(captions, null, 2);
|
|
2482
|
-
// 本地路径
|
|
2483
|
-
const localPath = node_path_1.default.resolve(projectLocalDir, 'materials', `${validatedFileName}.captions.json`);
|
|
2484
|
-
// 保存到本地
|
|
2485
|
-
await (0, promises_1.writeFile)(localPath, captionsText);
|
|
2836
|
+
}
|
|
2486
2837
|
}
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
//
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2838
|
+
catch (error) {
|
|
2839
|
+
console.error('Failed to validate duration with timeline analysis:', error);
|
|
2840
|
+
// 如果读取或解析 timeline_analysis.json 失败,继续执行但记录警告
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
const validatedFileName = validateFileName(saveToFileName);
|
|
2844
|
+
console.log(`Generating video with prompt: ${prompt.substring(0, 100)}...`);
|
|
2845
|
+
const ai = currentSession.ai;
|
|
2846
|
+
let progress = 0;
|
|
2847
|
+
const startFrameUri = start_frame
|
|
2848
|
+
? getMaterialUri(currentSession, start_frame)
|
|
2849
|
+
: undefined;
|
|
2850
|
+
const endFrameUri = end_frame
|
|
2851
|
+
? getMaterialUri(currentSession, end_frame)
|
|
2852
|
+
: undefined;
|
|
2853
|
+
if (optimizePrompt) {
|
|
2499
2854
|
try {
|
|
2500
|
-
await
|
|
2855
|
+
const promptOptimizer = await (0, promises_1.readFile)((0, node_path_1.resolve)(__dirname, './prompts/video-prompt-optimizer.md'), 'utf8');
|
|
2856
|
+
const completion = await ai.getCompletions({
|
|
2857
|
+
messages: [
|
|
2858
|
+
{
|
|
2859
|
+
role: 'system',
|
|
2860
|
+
content: promptOptimizer,
|
|
2861
|
+
},
|
|
2862
|
+
{
|
|
2863
|
+
role: 'user',
|
|
2864
|
+
content: prompt.trim(),
|
|
2865
|
+
},
|
|
2866
|
+
],
|
|
2867
|
+
});
|
|
2868
|
+
const optimizedPrompt = completion.choices[0]?.message?.content;
|
|
2869
|
+
if (optimizedPrompt) {
|
|
2870
|
+
prompt = optimizedPrompt;
|
|
2871
|
+
}
|
|
2501
2872
|
}
|
|
2502
2873
|
catch (error) {
|
|
2503
|
-
console.
|
|
2874
|
+
console.error('Failed to optimize prompt:', error);
|
|
2504
2875
|
}
|
|
2505
|
-
return {
|
|
2506
|
-
content: [
|
|
2507
|
-
{
|
|
2508
|
-
type: 'text',
|
|
2509
|
-
text: JSON.stringify(result),
|
|
2510
|
-
},
|
|
2511
|
-
],
|
|
2512
|
-
};
|
|
2513
2876
|
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2877
|
+
if (type === 'kenburns') {
|
|
2878
|
+
// 实现 getImageSize 函数
|
|
2879
|
+
async function getImageSize(imageUri) {
|
|
2880
|
+
// imageUri 是一个可以 http 访问的图片
|
|
2881
|
+
// eslint-disable-next-line custom/no-fetch-in-src
|
|
2882
|
+
const response = await fetch(imageUri);
|
|
2883
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
2884
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
2885
|
+
const image = (0, image_size_1.default)(buffer);
|
|
2886
|
+
return [image.width, image.height];
|
|
2887
|
+
}
|
|
2888
|
+
const [imageWidth, imageHeight] = await getImageSize(startFrameUri);
|
|
2889
|
+
const kenburnsPrompt = `根据用户描述和配置,生成适合的 kenburns 动画参数。
|
|
2890
|
+
|
|
2891
|
+
## 配置信息
|
|
2892
|
+
- 视频时长:${duration}秒
|
|
2893
|
+
- 分辨率:${resolution}
|
|
2894
|
+
- 保存最后一帧:${saveLastFrameAs ? '是' : '否'}
|
|
2895
|
+
- 图片宽高:${imageWidth}x${imageHeight}
|
|
2896
|
+
- 上一次选择的特效:${lastEffect || '无'}
|
|
2897
|
+
|
|
2898
|
+
## camera_motion 可选特效
|
|
2899
|
+
- zoom_in
|
|
2900
|
+
- zoom_out
|
|
2901
|
+
- zoom_in_left_top
|
|
2902
|
+
- zoom_out_left_top
|
|
2903
|
+
- zoom_in_right_top
|
|
2904
|
+
- zoom_out_right_top
|
|
2905
|
+
- zoom_in_left_bottom
|
|
2906
|
+
- zoom_out_left_bottom
|
|
2907
|
+
- zoom_in_right_bottom
|
|
2908
|
+
- zoom_out_right_bottom
|
|
2909
|
+
- pan_up
|
|
2910
|
+
- pan_down
|
|
2911
|
+
- pan_left
|
|
2912
|
+
- pan_right
|
|
2913
|
+
- static
|
|
2914
|
+
|
|
2915
|
+
## 特效选用规则
|
|
2916
|
+
- 视频时长6秒内:优先选择zoom类型(zoom_in, zoom_out)
|
|
2917
|
+
- 视频时长6秒以上:优先选择pan类型(pan_left, pan_right, pan_up, pan_down)
|
|
2918
|
+
- 除非用户指定,否则不要主动使用 static 效果
|
|
2919
|
+
- 除非用户指定,否则不要和上一次选择的效果重复
|
|
2920
|
+
`;
|
|
2921
|
+
const schema = {
|
|
2922
|
+
name: 'kenburns_effect',
|
|
2923
|
+
schema: {
|
|
2924
|
+
type: 'object',
|
|
2925
|
+
properties: {
|
|
2926
|
+
camera_motion: {
|
|
2927
|
+
type: 'string',
|
|
2928
|
+
description: 'kenburns 特效类型',
|
|
2929
|
+
enum: [
|
|
2930
|
+
'zoom_in',
|
|
2931
|
+
'zoom_out',
|
|
2932
|
+
'zoom_in_left_top',
|
|
2933
|
+
'zoom_out_left_top',
|
|
2934
|
+
'zoom_in_right_top',
|
|
2935
|
+
'zoom_out_right_top',
|
|
2936
|
+
'zoom_in_left_bottom',
|
|
2937
|
+
'zoom_out_left_bottom',
|
|
2938
|
+
'zoom_in_right_bottom',
|
|
2939
|
+
'zoom_out_right_bottom',
|
|
2940
|
+
'pan_up',
|
|
2941
|
+
'pan_down',
|
|
2942
|
+
'pan_left',
|
|
2943
|
+
'pan_right',
|
|
2944
|
+
'static',
|
|
2945
|
+
],
|
|
2946
|
+
},
|
|
2947
|
+
size: {
|
|
2948
|
+
type: 'string',
|
|
2949
|
+
description: 'kenburns 视频 宽x高',
|
|
2950
|
+
},
|
|
2951
|
+
duration: {
|
|
2952
|
+
type: 'number',
|
|
2953
|
+
description: 'kenburns 视频 时长',
|
|
2954
|
+
},
|
|
2524
2955
|
},
|
|
2525
|
-
|
|
2956
|
+
required: ['camera_motion', 'size', 'duration'],
|
|
2957
|
+
},
|
|
2526
2958
|
};
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
return {
|
|
2531
|
-
content: [
|
|
2959
|
+
const analysisPayload = {
|
|
2960
|
+
model: 'Doubao-Seed-1.6-flash',
|
|
2961
|
+
messages: [
|
|
2532
2962
|
{
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
}),
|
|
2963
|
+
role: 'system',
|
|
2964
|
+
content: kenburnsPrompt,
|
|
2965
|
+
},
|
|
2966
|
+
{
|
|
2967
|
+
role: 'user',
|
|
2968
|
+
content: prompt.trim(),
|
|
2540
2969
|
},
|
|
2541
2970
|
],
|
|
2971
|
+
response_format: {
|
|
2972
|
+
type: 'json_schema',
|
|
2973
|
+
json_schema: schema,
|
|
2974
|
+
},
|
|
2542
2975
|
};
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
}
|
|
2549
|
-
server.registerTool('generate-scene-tts', {
|
|
2550
|
-
title: 'Generate Scene TTS',
|
|
2551
|
-
description: `生成场景配音`,
|
|
2552
|
-
inputSchema: {
|
|
2553
|
-
text: zod_1.z.string().describe('The text to generate.'),
|
|
2554
|
-
sceneIndex: zod_1.z
|
|
2555
|
-
.number()
|
|
2556
|
-
.min(1)
|
|
2557
|
-
.optional()
|
|
2558
|
-
.describe('场景索引,从1开始的下标,如果非场景对应素材,则可不传,场景素材必传'),
|
|
2559
|
-
storyBoardFile: zod_1.z
|
|
2560
|
-
.string()
|
|
2561
|
-
.optional()
|
|
2562
|
-
.default('storyboard.json')
|
|
2563
|
-
.describe('故事板文件路径'),
|
|
2564
|
-
skipConsistencyCheck: zod_1.z
|
|
2565
|
-
.boolean()
|
|
2566
|
-
.optional()
|
|
2567
|
-
.default(false)
|
|
2568
|
-
.describe('是否跳过一致性检查,默认为false(即默认进行一致性检查)'),
|
|
2569
|
-
skipCheckWithSceneReason: zod_1.z
|
|
2570
|
-
.string()
|
|
2571
|
-
.optional()
|
|
2572
|
-
.describe('跳过校验的理由,如果skipConsistencyCheck设为true,必须要传这个参数'),
|
|
2573
|
-
saveToFileName: zod_1.z
|
|
2574
|
-
.string()
|
|
2575
|
-
.describe('The filename to save. 应该是mp3文件'),
|
|
2576
|
-
speed: zod_1.z
|
|
2577
|
-
.number()
|
|
2578
|
-
.min(0.5)
|
|
2579
|
-
.max(2)
|
|
2580
|
-
.optional()
|
|
2581
|
-
.default(1)
|
|
2582
|
-
.describe('The speed of the tts.'),
|
|
2583
|
-
pitch: zod_1.z
|
|
2584
|
-
.number()
|
|
2585
|
-
.min(-12)
|
|
2586
|
-
.max(12)
|
|
2587
|
-
.optional()
|
|
2588
|
-
.default(0)
|
|
2589
|
-
.describe('The pitch of the tts.'),
|
|
2590
|
-
volume: zod_1.z
|
|
2591
|
-
.number()
|
|
2592
|
-
.min(0)
|
|
2593
|
-
.max(10)
|
|
2594
|
-
.optional()
|
|
2595
|
-
.default(1.0)
|
|
2596
|
-
.describe('The volume of the tts.'),
|
|
2597
|
-
voiceID: zod_1.z
|
|
2598
|
-
.string()
|
|
2599
|
-
.describe(`适合作为视频配音的音色ID,除非用户指定,否则你必须确保已通过 pick-voice 工具挑选出真实存在的音色。`),
|
|
2600
|
-
context_texts: zod_1.z
|
|
2601
|
-
.array(zod_1.z.string())
|
|
2602
|
-
.default([])
|
|
2603
|
-
.describe(`语音合成的辅助信息,用于模型对话式合成,能更好的体现语音情感
|
|
2604
|
-
|
|
2605
|
-
可以探索,比如常见示例有以下几种:
|
|
2606
|
-
|
|
2607
|
-
1. 语速调整
|
|
2608
|
-
- context_texts: ["你可以说慢一点吗?"]
|
|
2609
|
-
2. 情绪/语气调整
|
|
2610
|
-
- context_texts=["你可以用特别特别痛心的语气说话吗?"]
|
|
2611
|
-
- context_texts=["嗯,你的语气再欢乐一点"]
|
|
2612
|
-
3. 音量调整
|
|
2613
|
-
- context_texts=["你嗓门再小点。"]
|
|
2614
|
-
4. 音感调整
|
|
2615
|
-
- context_texts=["你能用骄傲的语气来说话吗?"]
|
|
2616
|
-
`),
|
|
2617
|
-
explicit_language: zod_1.z.enum(['zh', 'en', 'ja']).optional().default('zh'),
|
|
2618
|
-
},
|
|
2619
|
-
}, async ({ text, sceneIndex, storyBoardFile, skipConsistencyCheck, voiceID, saveToFileName, speed, pitch, volume, context_texts, explicit_language, }) => {
|
|
2620
|
-
try {
|
|
2621
|
-
// 验证session状态
|
|
2622
|
-
const currentSession = await validateSession('generate-scene-tts');
|
|
2623
|
-
const validatedFileName = validateFileName(saveToFileName);
|
|
2624
|
-
const finalSpeed = speed ?? 1;
|
|
2625
|
-
volume = volume ?? 1;
|
|
2626
|
-
const ai = currentSession.ai;
|
|
2627
|
-
let scene = null;
|
|
2628
|
-
// 校验 text 与 storyboard.json 中场景设定的一致性
|
|
2629
|
-
if (sceneIndex && !skipConsistencyCheck) {
|
|
2976
|
+
console.log(JSON.stringify(analysisPayload, null, 2));
|
|
2977
|
+
const completion = await ai.getCompletions(analysisPayload);
|
|
2978
|
+
const analysisResult = completion.choices[0]?.message?.content;
|
|
2979
|
+
if (!analysisResult) {
|
|
2980
|
+
throw new Error('Failed to generate kenburns effect');
|
|
2981
|
+
}
|
|
2630
2982
|
try {
|
|
2631
|
-
const
|
|
2632
|
-
|
|
2633
|
-
|
|
2983
|
+
const kenburnsEffect = JSON.parse(analysisResult);
|
|
2984
|
+
const { camera_motion, size, duration } = kenburnsEffect;
|
|
2985
|
+
if (!camera_motion || !size || !duration) {
|
|
2986
|
+
throw new Error('Invalid kenburns effect parameters');
|
|
2634
2987
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
}
|
|
2646
|
-
if (storyBoard.scenes && Array.isArray(storyBoard.scenes)) {
|
|
2647
|
-
scene = storyBoard.scenes[sceneIndex - 1]; // sceneIndex 从1开始,数组从0开始
|
|
2648
|
-
if (scene) {
|
|
2649
|
-
const script = scene.script;
|
|
2650
|
-
let isValidText = false;
|
|
2651
|
-
// 检查 text 是否严格等于 script
|
|
2652
|
-
if (script && text === script) {
|
|
2653
|
-
isValidText = true;
|
|
2654
|
-
}
|
|
2655
|
-
// 检查 text 是否严格等于 dialog 数组中某个元素的 script
|
|
2656
|
-
if (!isValidText &&
|
|
2657
|
-
scene.dialog &&
|
|
2658
|
-
Array.isArray(scene.dialog)) {
|
|
2659
|
-
for (const dialogItem of scene.dialog) {
|
|
2660
|
-
if (dialogItem.script && text === dialogItem.script) {
|
|
2661
|
-
isValidText = true;
|
|
2662
|
-
break;
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
if (!isValidText) {
|
|
2667
|
-
return createErrorResponse('配音文本必须严格遵照storyboard的设定,如果用户明确指出不需要遵守,请将skipConsistencyCheck设置为true后再次调用', 'generate-scene-tts');
|
|
2668
|
-
}
|
|
2669
|
-
}
|
|
2670
|
-
else {
|
|
2671
|
-
console.warn(`Scene index ${sceneIndex} not found in storyboard.json`);
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2988
|
+
lastEffect = camera_motion;
|
|
2989
|
+
const files = currentSession.files;
|
|
2990
|
+
const terminal = currentSession.terminal;
|
|
2991
|
+
start_frame = (0, node_path_1.resolve)('/home/user/cerevox-zerocut/projects', terminal.id, 'materials', (0, node_path_1.basename)(start_frame));
|
|
2992
|
+
const saveToPath = `/home/user/cerevox-zerocut/projects/${terminal.id}/materials/${validatedFileName}`;
|
|
2993
|
+
const saveLocalPath = (0, node_path_1.resolve)(projectLocalDir, 'materials', validatedFileName);
|
|
2994
|
+
// 解析尺寸参数
|
|
2995
|
+
const sizeArray = size.split('x');
|
|
2996
|
+
if (sizeArray.length !== 2) {
|
|
2997
|
+
throw new Error(`Invalid size format: ${size}. Expected format: WIDTHxHEIGHT`);
|
|
2674
2998
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2999
|
+
const [widthStr, heightStr] = sizeArray;
|
|
3000
|
+
const width = Number(widthStr);
|
|
3001
|
+
const height = Number(heightStr);
|
|
3002
|
+
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
|
|
3003
|
+
throw new Error(`Invalid dimensions: ${width}x${height}. Both width and height must be positive numbers`);
|
|
2677
3004
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
}
|
|
2709
|
-
else {
|
|
2710
|
-
emotion = 'neutral';
|
|
2711
|
-
if (context_texts.length > 0) {
|
|
2712
|
-
const prompt = `根据用户输入语音内容和上下文内容,从文字判断语音合理的情感,然后选择以下情感**之一**返回结果:
|
|
2713
|
-
|
|
2714
|
-
"happy", "sad", "angry", "fearful", "disgusted", "surprised", "calm", "fluent", "whisper", "neutral"
|
|
2715
|
-
|
|
2716
|
-
## 要求
|
|
2717
|
-
输出 JSON 格式,包含一个 emotion 字段,值为以上情感之一。
|
|
2718
|
-
`;
|
|
2719
|
-
const schema = {
|
|
2720
|
-
name: 'emotion_schema',
|
|
2721
|
-
schema: {
|
|
2722
|
-
type: 'object',
|
|
2723
|
-
properties: {
|
|
2724
|
-
emotion: {
|
|
2725
|
-
type: 'string',
|
|
2726
|
-
enum: [
|
|
2727
|
-
'neutral',
|
|
2728
|
-
'happy',
|
|
2729
|
-
'sad',
|
|
2730
|
-
'angry',
|
|
2731
|
-
'fearful',
|
|
2732
|
-
'disgusted',
|
|
2733
|
-
'surprised',
|
|
2734
|
-
'calm',
|
|
2735
|
-
'fluent',
|
|
2736
|
-
'whisper',
|
|
2737
|
-
],
|
|
2738
|
-
description: '用户输入语音的情感',
|
|
3005
|
+
console.log(`Compiling Ken Burns motion command...`);
|
|
3006
|
+
let command;
|
|
3007
|
+
try {
|
|
3008
|
+
command = await (0, videokit_1.compileKenBurnsMotion)(start_frame, duration, camera_motion, {
|
|
3009
|
+
output: saveToPath,
|
|
3010
|
+
width,
|
|
3011
|
+
height,
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
catch (compileError) {
|
|
3015
|
+
console.error('Failed to compile Ken Burns motion command:', compileError);
|
|
3016
|
+
throw new Error(`Failed to compile Ken Burns motion: ${compileError}`);
|
|
3017
|
+
}
|
|
3018
|
+
console.log(`Executing FFmpeg command: ${command.substring(0, 100)}...`);
|
|
3019
|
+
const res = await terminal.run(command);
|
|
3020
|
+
const result = await res.json();
|
|
3021
|
+
if (result.exitCode !== 0) {
|
|
3022
|
+
console.error('FFmpeg command failed:', result);
|
|
3023
|
+
return {
|
|
3024
|
+
content: [
|
|
3025
|
+
{
|
|
3026
|
+
type: 'text',
|
|
3027
|
+
text: JSON.stringify({
|
|
3028
|
+
success: false,
|
|
3029
|
+
error: 'Ken Burns motion generation failed',
|
|
3030
|
+
exitCode: result.exitCode,
|
|
3031
|
+
stderr: result.stderr,
|
|
3032
|
+
command,
|
|
3033
|
+
timestamp: new Date().toISOString(),
|
|
3034
|
+
}),
|
|
2739
3035
|
},
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
3036
|
+
],
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
console.log('Ken Burns motion generated successfully, downloading file...');
|
|
3040
|
+
try {
|
|
3041
|
+
await files.download(saveToPath, saveLocalPath);
|
|
3042
|
+
}
|
|
3043
|
+
catch (downloadError) {
|
|
3044
|
+
console.warn('Failed to download file to local:', downloadError);
|
|
3045
|
+
// 继续执行,因为远程文件已生成成功
|
|
3046
|
+
}
|
|
3047
|
+
const resultData = {
|
|
3048
|
+
success: true,
|
|
3049
|
+
uri: saveToPath,
|
|
3050
|
+
durationMs: Math.floor(duration * 1000),
|
|
3051
|
+
cameraMotion: camera_motion,
|
|
3052
|
+
imagePath: start_frame,
|
|
3053
|
+
size,
|
|
3054
|
+
dimensions: { width, height },
|
|
3055
|
+
timestamp: new Date().toISOString(),
|
|
3056
|
+
systemPrompt: kenburnsPrompt,
|
|
2743
3057
|
};
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
3058
|
+
console.log(`Ken Burns motion completed: ${saveToPath}`);
|
|
3059
|
+
currentSession.track('KenBurns Video Generated', {
|
|
3060
|
+
durationMs: Math.floor(duration * 1000).toString(),
|
|
3061
|
+
cameraMotion: camera_motion,
|
|
3062
|
+
imagePath: start_frame,
|
|
3063
|
+
size,
|
|
3064
|
+
dimensions: size,
|
|
3065
|
+
});
|
|
3066
|
+
return {
|
|
3067
|
+
content: [
|
|
2751
3068
|
{
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
${text.trim()}
|
|
2755
|
-
|
|
2756
|
-
## 语音上下文
|
|
2757
|
-
${context_texts.join('\n')}
|
|
2758
|
-
`,
|
|
3069
|
+
type: 'text',
|
|
3070
|
+
text: JSON.stringify(resultData),
|
|
2759
3071
|
},
|
|
2760
3072
|
],
|
|
2761
|
-
response_format: {
|
|
2762
|
-
type: 'json_schema',
|
|
2763
|
-
json_schema: schema,
|
|
2764
|
-
},
|
|
2765
3073
|
};
|
|
2766
|
-
const completion = await ai.getCompletions(payload);
|
|
2767
|
-
const emotionObj = JSON.parse(completion.choices[0]?.message?.content ?? '{}');
|
|
2768
|
-
emotion = emotionObj.emotion ?? 'neutral';
|
|
2769
3074
|
}
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
speed: finalSpeed,
|
|
2774
|
-
pitch,
|
|
2775
|
-
volume,
|
|
2776
|
-
emotion,
|
|
2777
|
-
voice_to_caption: explicit_language === 'zh' || explicit_language === 'en',
|
|
2778
|
-
});
|
|
3075
|
+
catch (error) {
|
|
3076
|
+
throw new Error('Failed to parse kenburns effect parameters');
|
|
3077
|
+
}
|
|
2779
3078
|
}
|
|
3079
|
+
const res = await ai.framesToVideo({
|
|
3080
|
+
prompt: prompt.trim(),
|
|
3081
|
+
start_frame: startFrameUri,
|
|
3082
|
+
end_frame: endFrameUri,
|
|
3083
|
+
duration,
|
|
3084
|
+
resolution,
|
|
3085
|
+
type,
|
|
3086
|
+
return_last_frame: Boolean(saveLastFrameAs),
|
|
3087
|
+
onProgress: async (metaData) => {
|
|
3088
|
+
try {
|
|
3089
|
+
await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
|
|
3090
|
+
}
|
|
3091
|
+
catch (progressError) {
|
|
3092
|
+
console.warn('Failed to send progress update:', progressError);
|
|
3093
|
+
}
|
|
3094
|
+
},
|
|
3095
|
+
waitForFinish: type !== 'zero',
|
|
3096
|
+
mute,
|
|
3097
|
+
seed,
|
|
3098
|
+
});
|
|
2780
3099
|
if (!res) {
|
|
2781
|
-
throw new Error('Failed to generate
|
|
3100
|
+
throw new Error('Failed to generate video: no response from AI service');
|
|
2782
3101
|
}
|
|
2783
3102
|
if (res.url) {
|
|
2784
|
-
console.log('
|
|
2785
|
-
const
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
if (!duration) {
|
|
2790
|
-
return createErrorResponse('TTS duration not returned from AI service', 'generate-scene-tts');
|
|
2791
|
-
}
|
|
2792
|
-
const uri = await saveMaterial(currentSession, url, validatedFileName);
|
|
2793
|
-
let warn = '';
|
|
2794
|
-
if (scene) {
|
|
2795
|
-
const minDur = Math.ceil(duration);
|
|
2796
|
-
if (scene.audio_mode === 'vo_sync' && scene.duration !== minDur) {
|
|
2797
|
-
warn = `场景${sceneIndex}设定的时长${scene.duration}秒与实际生成的语音时长${minDur}秒不一致,音画同步将有问题,建议修改场景时长为${minDur}秒`;
|
|
2798
|
-
}
|
|
2799
|
-
else if (scene.duration < minDur) {
|
|
2800
|
-
warn = `场景${sceneIndex}设定的时长${scene.duration}秒小于实际生成的语音时长${duration}秒,可能会导致场景结束时音频未播放完成,建议修改场景时长为${minDur}秒`;
|
|
2801
|
-
}
|
|
3103
|
+
console.log('Video generated successfully, saving to materials...');
|
|
3104
|
+
const uri = await saveMaterial(currentSession, res.url, validatedFileName);
|
|
3105
|
+
let lastFrameUri;
|
|
3106
|
+
if (saveLastFrameAs && res.last_frame_url) {
|
|
3107
|
+
lastFrameUri = await saveMaterial(currentSession, res.last_frame_url, validateFileName(saveLastFrameAs));
|
|
2802
3108
|
}
|
|
3109
|
+
const { url, duration: videoDuration, ...opts } = res;
|
|
2803
3110
|
const result = {
|
|
2804
3111
|
success: true,
|
|
2805
|
-
|
|
2806
|
-
source: url, // 方便调试
|
|
3112
|
+
// source: url,
|
|
2807
3113
|
uri,
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
context_texts,
|
|
2812
|
-
voiceName: voiceID,
|
|
2813
|
-
speed: finalSpeed,
|
|
3114
|
+
lastFrameUri,
|
|
3115
|
+
durationMs: Math.floor((videoDuration || duration) * 1000),
|
|
3116
|
+
prompt,
|
|
2814
3117
|
timestamp: new Date().toISOString(),
|
|
3118
|
+
startFrame: start_frame,
|
|
3119
|
+
endFrame: end_frame,
|
|
2815
3120
|
...opts,
|
|
2816
3121
|
};
|
|
2817
3122
|
// Update media_logs.json
|
|
2818
3123
|
try {
|
|
2819
|
-
await updateMediaLogs(currentSession, validatedFileName, result
|
|
3124
|
+
await updateMediaLogs(currentSession, validatedFileName, result);
|
|
2820
3125
|
}
|
|
2821
3126
|
catch (error) {
|
|
2822
3127
|
console.warn(`Failed to update media_logs.json for ${validatedFileName}:`, error);
|
|
@@ -2825,21 +3130,36 @@ ${context_texts.join('\n')}
|
|
|
2825
3130
|
content: [
|
|
2826
3131
|
{
|
|
2827
3132
|
type: 'text',
|
|
2828
|
-
text: JSON.stringify(result),
|
|
3133
|
+
text: JSON.stringify(result),
|
|
3134
|
+
},
|
|
3135
|
+
],
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
else if (res.taskUrl) {
|
|
3139
|
+
return {
|
|
3140
|
+
content: [
|
|
3141
|
+
{
|
|
3142
|
+
type: 'text',
|
|
3143
|
+
text: JSON.stringify({
|
|
3144
|
+
success: true,
|
|
3145
|
+
message: '该视频生成任务正在运行中,它是异步任务,且执行时间较长,你应立即调用工具 wait-for-task-finish 来等待任务结束,如 wait-for-task-finish 工具调用超时,你应立即再次重新调用直到任务结束。',
|
|
3146
|
+
taskUrl: res.taskUrl,
|
|
3147
|
+
}),
|
|
2829
3148
|
},
|
|
2830
3149
|
],
|
|
2831
3150
|
};
|
|
2832
3151
|
}
|
|
2833
3152
|
else {
|
|
2834
|
-
console.warn('
|
|
3153
|
+
console.warn('Video generation completed but no URL returned');
|
|
2835
3154
|
return {
|
|
2836
3155
|
content: [
|
|
2837
3156
|
{
|
|
2838
3157
|
type: 'text',
|
|
2839
3158
|
text: JSON.stringify({
|
|
2840
3159
|
success: false,
|
|
2841
|
-
error: 'No
|
|
3160
|
+
error: 'No video URL returned from AI service',
|
|
2842
3161
|
response: res,
|
|
3162
|
+
startFrameUri,
|
|
2843
3163
|
timestamp: new Date().toISOString(),
|
|
2844
3164
|
}),
|
|
2845
3165
|
},
|
|
@@ -2848,294 +3168,139 @@ ${context_texts.join('\n')}
|
|
|
2848
3168
|
}
|
|
2849
3169
|
}
|
|
2850
3170
|
catch (error) {
|
|
2851
|
-
return createErrorResponse(error, 'generate-
|
|
3171
|
+
return createErrorResponse(error, 'generate-video');
|
|
2852
3172
|
}
|
|
2853
3173
|
});
|
|
2854
|
-
server.registerTool('
|
|
2855
|
-
title: '
|
|
2856
|
-
description:
|
|
3174
|
+
server.registerTool('edit-video', {
|
|
3175
|
+
title: 'Edit Video',
|
|
3176
|
+
description: `Edit video using Coze workflow,可以做以下事情:
|
|
3177
|
+
|
|
3178
|
+
- 替换视频内容,type 为 replace
|
|
3179
|
+
- 视频对口型,type 为 lipsync
|
|
3180
|
+
- 视频动作模仿,type 为 imitate
|
|
3181
|
+
`,
|
|
2857
3182
|
inputSchema: {
|
|
2858
|
-
|
|
2859
|
-
.
|
|
2860
|
-
.describe('The
|
|
2861
|
-
|
|
3183
|
+
type: zod_1.z
|
|
3184
|
+
.enum(['replace', 'lipsync', 'imitate'])
|
|
3185
|
+
.describe('The editing type'),
|
|
3186
|
+
video: zod_1.z.string().describe(`The video to edit
|
|
3187
|
+
|
|
3188
|
+
- type 为 replace 时,video 为要替换内容的视频
|
|
3189
|
+
- type 为 lipsync 时,video 为要对口型的视频
|
|
3190
|
+
- type 为 imitate 时,video 为模仿动作参考视频
|
|
3191
|
+
`),
|
|
3192
|
+
prompt: zod_1.z.string().optional().describe(`The editing prompt
|
|
3193
|
+
|
|
3194
|
+
- type 为 replace 时,prompt 为要替换的内容
|
|
3195
|
+
- type 为 lipsync 时,prompt 为空
|
|
3196
|
+
- type 为 imitate 时,prompt 为要模仿动作的内容,和 referenceImageUrl 二选一
|
|
3197
|
+
`),
|
|
3198
|
+
referenceImage: zod_1.z.string().optional()
|
|
3199
|
+
.describe(`The reference image File for editing
|
|
3200
|
+
- type 为 replace 时,referenceImage 为参考图片
|
|
3201
|
+
- type 为 lipsync 时,referenceImage 为空
|
|
3202
|
+
- type 为 imitate 时,referenceImage 为要模仿动作的画面,和 prompt 二选一
|
|
3203
|
+
`),
|
|
3204
|
+
saveToFileName: zod_1.z
|
|
2862
3205
|
.string()
|
|
2863
|
-
.
|
|
2864
|
-
.describe('Output video filename (optional, defaults to output.mp4).'),
|
|
3206
|
+
.describe(`The file name to save the edited video to. 应该是mp4文件`),
|
|
2865
3207
|
},
|
|
2866
|
-
}, async ({
|
|
3208
|
+
}, async ({ type, video, prompt, referenceImage, saveToFileName }, context) => {
|
|
2867
3209
|
try {
|
|
2868
|
-
|
|
2869
|
-
const
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
console.log('Starting video compilation and rendering...');
|
|
2882
|
-
// 验证terminal可用性
|
|
2883
|
-
const terminal = currentSession.terminal;
|
|
2884
|
-
if (!terminal) {
|
|
2885
|
-
throw new Error('Terminal not available in current session');
|
|
2886
|
-
}
|
|
2887
|
-
const localProjectFile = (0, node_path_1.resolve)(projectLocalDir, (0, node_path_1.basename)(projectFileName));
|
|
2888
|
-
const project = JSON.parse(await (0, promises_1.readFile)(localProjectFile, 'utf-8'));
|
|
2889
|
-
// 验证输出文件名安全性
|
|
2890
|
-
const outFile = outputFileName || project.export.outFile || 'output.mp4';
|
|
2891
|
-
const validatedFileName = validateFileName(outFile);
|
|
2892
|
-
console.log(`Output file: ${validatedFileName}`);
|
|
2893
|
-
// 构建工作目录路径
|
|
2894
|
-
const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}`;
|
|
2895
|
-
const outputDir = `${workDir}/output`;
|
|
2896
|
-
const outputPath = `${outputDir}/${validatedFileName}`;
|
|
2897
|
-
// Project已经通过zVideoProject schema验证
|
|
2898
|
-
const validated = { ...project };
|
|
2899
|
-
// 更新导出配置
|
|
2900
|
-
validated.export = {
|
|
2901
|
-
...validated.export,
|
|
2902
|
-
outFile: outputPath,
|
|
3210
|
+
const currentSession = await validateSession('edit-video');
|
|
3211
|
+
const ai = currentSession.ai;
|
|
3212
|
+
const validatedFileName = validateFileName(saveToFileName);
|
|
3213
|
+
let res = {};
|
|
3214
|
+
let progress = 0;
|
|
3215
|
+
const onProgress = async (metaData) => {
|
|
3216
|
+
console.log('Replace video progress:', metaData);
|
|
3217
|
+
try {
|
|
3218
|
+
await sendProgress(context, ++progress, undefined, JSON.stringify(metaData));
|
|
3219
|
+
}
|
|
3220
|
+
catch (progressError) {
|
|
3221
|
+
console.warn('Failed to send progress update:', progressError);
|
|
3222
|
+
}
|
|
2903
3223
|
};
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
3224
|
+
let referenceImageUrl = undefined;
|
|
3225
|
+
if (referenceImage) {
|
|
3226
|
+
referenceImageUrl = getMaterialUri(currentSession, referenceImage);
|
|
3227
|
+
}
|
|
3228
|
+
const videoUrl = getMaterialUri(currentSession, video);
|
|
3229
|
+
if (type === 'replace') {
|
|
3230
|
+
if (!prompt) {
|
|
3231
|
+
throw new Error('prompt is required for replace type');
|
|
3232
|
+
}
|
|
3233
|
+
res = await ai.editVideo({
|
|
3234
|
+
videoUrl,
|
|
3235
|
+
prompt,
|
|
3236
|
+
referenceImageUrl,
|
|
3237
|
+
onProgress,
|
|
2912
3238
|
});
|
|
2913
3239
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
3240
|
+
else if (type === 'lipsync') {
|
|
3241
|
+
// 调用AI的lipSync方法,使用处理后的音频
|
|
3242
|
+
const { audio: audioUrl } = await ai.splitVideoAndAudio({
|
|
3243
|
+
videoUrl,
|
|
3244
|
+
});
|
|
3245
|
+
res = await ai.lipSync({
|
|
3246
|
+
type: 'pixv',
|
|
3247
|
+
videoUrl,
|
|
3248
|
+
audioUrl,
|
|
3249
|
+
audioInMs: 0,
|
|
3250
|
+
pad_audio: false,
|
|
3251
|
+
onProgress,
|
|
3252
|
+
});
|
|
2917
3253
|
}
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
console.log('Executing FFmpeg command...');
|
|
2922
|
-
const result = await runFfmpeg(currentSession, compiled);
|
|
2923
|
-
if (result.exitCode === 0) {
|
|
2924
|
-
console.log('Video compilation completed successfully');
|
|
2925
|
-
// 自动下载输出文件
|
|
2926
|
-
console.log('Starting automatic download of output files...');
|
|
2927
|
-
let downloadResult = null;
|
|
2928
|
-
try {
|
|
2929
|
-
const workDir = `/home/user/cerevox-zerocut/projects/${terminal.id}/output`;
|
|
2930
|
-
let outputs = [];
|
|
2931
|
-
try {
|
|
2932
|
-
outputs = (await currentSession.files.listFiles(workDir)) || [];
|
|
2933
|
-
}
|
|
2934
|
-
catch (listError) {
|
|
2935
|
-
console.warn('Failed to list output files:', listError);
|
|
2936
|
-
outputs = [];
|
|
2937
|
-
}
|
|
2938
|
-
if (outputs.length > 0) {
|
|
2939
|
-
const outputDir = (0, node_path_1.resolve)(process.env.ZEROCUT_PROJECT_CWD || process.cwd(), projectLocalDir, 'output');
|
|
2940
|
-
await (0, promises_1.mkdir)(outputDir, { recursive: true });
|
|
2941
|
-
const downloadErrors = [];
|
|
2942
|
-
const successfulDownloads = [];
|
|
2943
|
-
const promises = outputs.map(async (output) => {
|
|
2944
|
-
try {
|
|
2945
|
-
await currentSession.files.download(`${workDir}/${output}`, `${outputDir}/${output}`);
|
|
2946
|
-
successfulDownloads.push(output);
|
|
2947
|
-
return output;
|
|
2948
|
-
}
|
|
2949
|
-
catch (downloadError) {
|
|
2950
|
-
const errorMsg = `Failed to download ${output}: ${downloadError}`;
|
|
2951
|
-
console.error(errorMsg);
|
|
2952
|
-
downloadErrors.push(errorMsg);
|
|
2953
|
-
return null;
|
|
2954
|
-
}
|
|
2955
|
-
});
|
|
2956
|
-
const results = await Promise.all(promises);
|
|
2957
|
-
const sources = results.filter(output => output !== null);
|
|
2958
|
-
downloadResult = {
|
|
2959
|
-
totalFiles: outputs.length,
|
|
2960
|
-
successfulDownloads: sources.length,
|
|
2961
|
-
failedDownloads: downloadErrors.length,
|
|
2962
|
-
downloadErrors: downloadErrors.length > 0 ? downloadErrors : undefined,
|
|
2963
|
-
downloadPath: outputDir,
|
|
2964
|
-
sources,
|
|
2965
|
-
};
|
|
2966
|
-
console.log(`Download completed: ${sources.length}/${outputs.length} files successful`);
|
|
2967
|
-
}
|
|
2968
|
-
else {
|
|
2969
|
-
console.log('No output files found to download');
|
|
2970
|
-
downloadResult = {
|
|
2971
|
-
totalFiles: 0,
|
|
2972
|
-
successfulDownloads: 0,
|
|
2973
|
-
failedDownloads: 0,
|
|
2974
|
-
message: 'No output files found to download',
|
|
2975
|
-
};
|
|
2976
|
-
}
|
|
3254
|
+
else if (type === 'imitate') {
|
|
3255
|
+
if (!prompt && !referenceImageUrl) {
|
|
3256
|
+
throw new Error('prompt or referenceImageUrl is required for imitate type');
|
|
2977
3257
|
}
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
failedDownloads: 1,
|
|
2985
|
-
};
|
|
3258
|
+
if (prompt) {
|
|
3259
|
+
referenceImageUrl = (await ai.generateImage({
|
|
3260
|
+
prompt,
|
|
3261
|
+
type: 'banana',
|
|
3262
|
+
image: referenceImageUrl ? [referenceImageUrl] : undefined,
|
|
3263
|
+
})).url;
|
|
2986
3264
|
}
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
message: 'Video compilation and download completed successfully',
|
|
2993
|
-
download: downloadResult,
|
|
2994
|
-
timestamp: new Date().toISOString(),
|
|
2995
|
-
};
|
|
2996
|
-
return {
|
|
2997
|
-
content: [
|
|
2998
|
-
{
|
|
2999
|
-
type: 'text',
|
|
3000
|
-
text: JSON.stringify(successResult),
|
|
3001
|
-
},
|
|
3002
|
-
],
|
|
3003
|
-
};
|
|
3265
|
+
res = await ai.actionImitation({
|
|
3266
|
+
videoUrl,
|
|
3267
|
+
imageUrl: referenceImageUrl,
|
|
3268
|
+
onProgress,
|
|
3269
|
+
});
|
|
3004
3270
|
}
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
const
|
|
3008
|
-
success:
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
stderr: result.stderr,
|
|
3013
|
-
message: `FFmpeg exited with code ${result.exitCode}`,
|
|
3271
|
+
if (res.url) {
|
|
3272
|
+
const uri = await saveMaterial(currentSession, res.url, validatedFileName);
|
|
3273
|
+
const result = {
|
|
3274
|
+
success: true,
|
|
3275
|
+
// source: url,
|
|
3276
|
+
uri,
|
|
3277
|
+
prompt,
|
|
3014
3278
|
timestamp: new Date().toISOString(),
|
|
3015
3279
|
};
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3280
|
+
// Update media_logs.json
|
|
3281
|
+
try {
|
|
3282
|
+
await updateMediaLogs(currentSession, validatedFileName, result);
|
|
3283
|
+
}
|
|
3284
|
+
catch (error) {
|
|
3285
|
+
console.warn(`Failed to update media_logs.json for ${validatedFileName}:`, error);
|
|
3019
3286
|
}
|
|
3020
3287
|
return {
|
|
3021
3288
|
content: [
|
|
3022
3289
|
{
|
|
3023
3290
|
type: 'text',
|
|
3024
|
-
text: JSON.stringify(
|
|
3291
|
+
text: JSON.stringify(result),
|
|
3025
3292
|
},
|
|
3026
3293
|
],
|
|
3027
3294
|
};
|
|
3028
3295
|
}
|
|
3029
|
-
}
|
|
3030
|
-
catch (error) {
|
|
3031
|
-
return createErrorResponse(error, 'compile-and-run');
|
|
3032
|
-
}
|
|
3033
|
-
});
|
|
3034
|
-
server.registerTool('get-schema', {
|
|
3035
|
-
title: 'Get Storyboard Schema or Draft Content Schema',
|
|
3036
|
-
description: 'Get the complete Storyboard or Draft Content JSON Schema definition. Use this schema to validate storyboard.json or draft_content.json files.',
|
|
3037
|
-
inputSchema: {
|
|
3038
|
-
type: zod_1.z
|
|
3039
|
-
.enum(['storyboard', 'draft_content'])
|
|
3040
|
-
.describe('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'),
|
|
3041
|
-
},
|
|
3042
|
-
}, async ({ type }) => {
|
|
3043
|
-
try {
|
|
3044
|
-
const schemaPath = (0, node_path_1.resolve)(__dirname, `./prompts/${type}-schema.json`);
|
|
3045
|
-
const schemaContent = await (0, promises_1.readFile)(schemaPath, 'utf-8');
|
|
3046
|
-
const schema = JSON.parse(schemaContent);
|
|
3047
|
-
let important_guidelines = '';
|
|
3048
|
-
if (type === 'draft_content') {
|
|
3049
|
-
important_guidelines = `⚠️ 生成文件时请严格遵守输出规范,字幕文本内容必须与 storyboard.json 中的 script(或dialog) 字段的文本内容完全一致。
|
|
3050
|
-
|
|
3051
|
-
** 字幕优化 **
|
|
3052
|
-
* 在保证字幕文本内容与 storyboard.json 中的 script(或dialog) 字段的文本内容完全一致的前提下,可根据 tts 返回的 \`captions.utterances\` 字段对字幕的显示进行优化,将过长的字幕分段显示,在 draft_content.json 中使用分段字幕,captions 的内容在 media_logs.json 中可查询到。
|
|
3053
|
-
* 如用户未特殊指定,字幕样式(字体及大小)务必遵守输出规范
|
|
3054
|
-
`;
|
|
3055
|
-
}
|
|
3056
|
-
return {
|
|
3057
|
-
content: [
|
|
3058
|
-
{
|
|
3059
|
-
type: 'text',
|
|
3060
|
-
text: JSON.stringify({
|
|
3061
|
-
success: true,
|
|
3062
|
-
schema,
|
|
3063
|
-
important_guidelines,
|
|
3064
|
-
timestamp: new Date().toISOString(),
|
|
3065
|
-
}),
|
|
3066
|
-
},
|
|
3067
|
-
],
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
3070
|
-
catch (error) {
|
|
3071
|
-
return createErrorResponse(error, 'get-schema');
|
|
3072
|
-
}
|
|
3073
|
-
});
|
|
3074
|
-
server.registerTool('pick-voice', {
|
|
3075
|
-
title: 'Pick Voice',
|
|
3076
|
-
description: '根据用户需求,选择尽可能符合要求的语音,在合适的情况下,优先采用 volcano_tts_2 类型的语音',
|
|
3077
|
-
inputSchema: {
|
|
3078
|
-
prompt: zod_1.z
|
|
3079
|
-
.string()
|
|
3080
|
-
.describe('用户需求描述,例如:一个有亲和力的,适合给孩子讲故事的语音'),
|
|
3081
|
-
custom_design: zod_1.z
|
|
3082
|
-
.boolean()
|
|
3083
|
-
.optional()
|
|
3084
|
-
.describe('是否自定义语音,由于要消耗较多积分,因此**只有用户明确要求自己设计语音**,才将该参数设为true'),
|
|
3085
|
-
custom_design_preview: zod_1.z
|
|
3086
|
-
.string()
|
|
3087
|
-
.optional()
|
|
3088
|
-
.describe('用户自定义语音的预览文本,用于展示自定义语音的效果,只有 custom_design 为 true 时才需要'),
|
|
3089
|
-
custom_design_save_to: zod_1.z
|
|
3090
|
-
.string()
|
|
3091
|
-
.optional()
|
|
3092
|
-
.describe('自定义语音的保存路径,例如:custom_voice.mp3 custom_voice_{id}.mp3'),
|
|
3093
|
-
},
|
|
3094
|
-
}, async ({ prompt, custom_design, custom_design_preview, custom_design_save_to, }) => {
|
|
3095
|
-
try {
|
|
3096
|
-
// 验证session状态
|
|
3097
|
-
const currentSession = await validateSession('pick-voice');
|
|
3098
|
-
const ai = currentSession.ai;
|
|
3099
|
-
if (custom_design) {
|
|
3100
|
-
if (!custom_design_preview) {
|
|
3101
|
-
throw new Error('custom_design_preview is required when custom_design is true');
|
|
3102
|
-
}
|
|
3103
|
-
const data = await currentSession.ai.voiceDesign({
|
|
3104
|
-
prompt,
|
|
3105
|
-
previewText: custom_design_preview,
|
|
3106
|
-
});
|
|
3107
|
-
if (data.voice_id) {
|
|
3108
|
-
const trial_audio = data.trial_audio;
|
|
3109
|
-
let uri = '';
|
|
3110
|
-
if (trial_audio) {
|
|
3111
|
-
uri = await saveMaterial(currentSession, trial_audio, custom_design_save_to || `custom_voice_${data.voice_id}.mp3`);
|
|
3112
|
-
}
|
|
3113
|
-
return {
|
|
3114
|
-
content: [
|
|
3115
|
-
{
|
|
3116
|
-
type: 'text',
|
|
3117
|
-
text: JSON.stringify({
|
|
3118
|
-
success: true,
|
|
3119
|
-
...data,
|
|
3120
|
-
uri,
|
|
3121
|
-
timestamp: new Date().toISOString(),
|
|
3122
|
-
}),
|
|
3123
|
-
},
|
|
3124
|
-
],
|
|
3125
|
-
};
|
|
3126
|
-
}
|
|
3127
|
-
else {
|
|
3128
|
-
throw new Error(`Voice design failed, ${JSON.stringify(data)}`);
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
|
-
const data = await ai.pickVoice({ prompt });
|
|
3132
3296
|
return {
|
|
3133
3297
|
content: [
|
|
3134
3298
|
{
|
|
3135
3299
|
type: 'text',
|
|
3136
3300
|
text: JSON.stringify({
|
|
3137
|
-
success:
|
|
3138
|
-
|
|
3301
|
+
success: false,
|
|
3302
|
+
error: 'No video URL returned from AI service',
|
|
3303
|
+
response: res,
|
|
3139
3304
|
timestamp: new Date().toISOString(),
|
|
3140
3305
|
}),
|
|
3141
3306
|
},
|
|
@@ -3143,7 +3308,7 @@ server.registerTool('pick-voice', {
|
|
|
3143
3308
|
};
|
|
3144
3309
|
}
|
|
3145
3310
|
catch (error) {
|
|
3146
|
-
return createErrorResponse(error, '
|
|
3311
|
+
return createErrorResponse(error, 'edit-video');
|
|
3147
3312
|
}
|
|
3148
3313
|
});
|
|
3149
3314
|
server.registerTool('media-analyzer', {
|
|
@@ -3363,16 +3528,6 @@ server.registerTool('audio-video-sync', {
|
|
|
3363
3528
|
title: 'Audio Video Sync',
|
|
3364
3529
|
description: 'Generate audio-video-synced video by matching video with audio or lip sync.',
|
|
3365
3530
|
inputSchema: {
|
|
3366
|
-
lipSync: zod_1.z
|
|
3367
|
-
.object({
|
|
3368
|
-
type: zod_1.z
|
|
3369
|
-
.enum(['pixv', 'vidu', 'basic', 'lite'])
|
|
3370
|
-
.optional()
|
|
3371
|
-
.default('pixv'),
|
|
3372
|
-
padAudio: zod_1.z.boolean().optional().default(true),
|
|
3373
|
-
})
|
|
3374
|
-
.optional()
|
|
3375
|
-
.describe('默认不用对口型,除非用户明确指定需要对口型。'),
|
|
3376
3531
|
videos: zod_1.z
|
|
3377
3532
|
.array(zod_1.z.string())
|
|
3378
3533
|
.describe('The video file names in materials directory. 如果多个视频,将会按顺序拼接'),
|
|
@@ -3399,7 +3554,7 @@ server.registerTool('audio-video-sync', {
|
|
|
3399
3554
|
.string()
|
|
3400
3555
|
.describe('The filename to save the audio-video-synced video. 应该是mp4文件'),
|
|
3401
3556
|
},
|
|
3402
|
-
}, async ({
|
|
3557
|
+
}, async ({ videos, audio, audioInMs, audioFadeOutMs, audioVolume, videoAudioVolume, saveToFileName, loopAudio, addSubtitles, }, context) => {
|
|
3403
3558
|
try {
|
|
3404
3559
|
// 验证session状态
|
|
3405
3560
|
const currentSession = await validateSession('audio-video-sync');
|
|
@@ -3423,81 +3578,19 @@ server.registerTool('audio-video-sync', {
|
|
|
3423
3578
|
mediaUrls: videoUrls,
|
|
3424
3579
|
})).url;
|
|
3425
3580
|
}
|
|
3426
|
-
|
|
3427
|
-
// 不需要对口型
|
|
3428
|
-
const result = await ai.voSync({
|
|
3429
|
-
videoUrl,
|
|
3430
|
-
audioUrl,
|
|
3431
|
-
audioInMs,
|
|
3432
|
-
audioFadeOutMs,
|
|
3433
|
-
keepVideoAudio: true,
|
|
3434
|
-
audioVolume,
|
|
3435
|
-
videoAudioVolume,
|
|
3436
|
-
loopAudio,
|
|
3437
|
-
subtitles: addSubtitles,
|
|
3438
|
-
});
|
|
3439
|
-
if (result.url) {
|
|
3440
|
-
console.log('Audio sync completed successfully');
|
|
3441
|
-
// 保存到项目材料目录
|
|
3442
|
-
const uri = await saveMaterial(currentSession, result.url, validatedFileName);
|
|
3443
|
-
return {
|
|
3444
|
-
content: [
|
|
3445
|
-
{
|
|
3446
|
-
type: 'text',
|
|
3447
|
-
text: JSON.stringify({
|
|
3448
|
-
success: true,
|
|
3449
|
-
uri,
|
|
3450
|
-
durationMs: Math.floor(result.duration * 1000),
|
|
3451
|
-
filename: validatedFileName,
|
|
3452
|
-
timestamp: new Date().toISOString(),
|
|
3453
|
-
}),
|
|
3454
|
-
},
|
|
3455
|
-
],
|
|
3456
|
-
};
|
|
3457
|
-
}
|
|
3458
|
-
else {
|
|
3459
|
-
console.warn('Audio sync completed but no URL returned');
|
|
3460
|
-
return {
|
|
3461
|
-
content: [
|
|
3462
|
-
{
|
|
3463
|
-
type: 'text',
|
|
3464
|
-
text: JSON.stringify({
|
|
3465
|
-
success: false,
|
|
3466
|
-
error: 'No video URL returned from lip sync service',
|
|
3467
|
-
response: result,
|
|
3468
|
-
timestamp: new Date().toISOString(),
|
|
3469
|
-
}),
|
|
3470
|
-
},
|
|
3471
|
-
],
|
|
3472
|
-
};
|
|
3473
|
-
}
|
|
3474
|
-
}
|
|
3475
|
-
// 调用AI的lipSync方法,使用处理后的音频
|
|
3476
|
-
let progress = 0;
|
|
3477
|
-
const result = await ai.lipSync({
|
|
3478
|
-
type: lipSync.type,
|
|
3581
|
+
const result = await ai.voSync({
|
|
3479
3582
|
videoUrl,
|
|
3480
3583
|
audioUrl,
|
|
3481
3584
|
audioInMs,
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
catch (progressError) {
|
|
3489
|
-
console.warn('Failed to send progress update:', progressError);
|
|
3490
|
-
}
|
|
3491
|
-
},
|
|
3585
|
+
audioFadeOutMs,
|
|
3586
|
+
keepVideoAudio: true,
|
|
3587
|
+
audioVolume,
|
|
3588
|
+
videoAudioVolume,
|
|
3589
|
+
loopAudio,
|
|
3590
|
+
subtitles: addSubtitles,
|
|
3492
3591
|
});
|
|
3493
3592
|
if (result.url) {
|
|
3494
|
-
console.log('
|
|
3495
|
-
if (addSubtitles) {
|
|
3496
|
-
const addSubtitlesRes = await ai.addVideoSubtitles({
|
|
3497
|
-
videoUrl: result.url,
|
|
3498
|
-
});
|
|
3499
|
-
result.url = addSubtitlesRes.url || result.url;
|
|
3500
|
-
}
|
|
3593
|
+
console.log('Audio sync completed successfully');
|
|
3501
3594
|
// 保存到项目材料目录
|
|
3502
3595
|
const uri = await saveMaterial(currentSession, result.url, validatedFileName);
|
|
3503
3596
|
return {
|
|
@@ -3508,8 +3601,6 @@ server.registerTool('audio-video-sync', {
|
|
|
3508
3601
|
success: true,
|
|
3509
3602
|
uri,
|
|
3510
3603
|
durationMs: Math.floor(result.duration * 1000),
|
|
3511
|
-
resolution: result.resolution,
|
|
3512
|
-
ratio: result.ratio,
|
|
3513
3604
|
filename: validatedFileName,
|
|
3514
3605
|
timestamp: new Date().toISOString(),
|
|
3515
3606
|
}),
|
|
@@ -3518,7 +3609,7 @@ server.registerTool('audio-video-sync', {
|
|
|
3518
3609
|
};
|
|
3519
3610
|
}
|
|
3520
3611
|
else {
|
|
3521
|
-
console.warn('
|
|
3612
|
+
console.warn('Audio sync completed but no URL returned');
|
|
3522
3613
|
return {
|
|
3523
3614
|
content: [
|
|
3524
3615
|
{
|
|
@@ -4268,44 +4359,6 @@ server.registerTool('run-ffmpeg-command', {
|
|
|
4268
4359
|
return createErrorResponse(error, 'run-ffmpeg-command');
|
|
4269
4360
|
}
|
|
4270
4361
|
});
|
|
4271
|
-
// server.registerTool(
|
|
4272
|
-
// 'do-storyboard-optimization',
|
|
4273
|
-
// {
|
|
4274
|
-
// title: 'Do Storyboard Optimization',
|
|
4275
|
-
// description:
|
|
4276
|
-
// 'Get storyboard optimization guidelines and action instructions.',
|
|
4277
|
-
// inputSchema: {},
|
|
4278
|
-
// },
|
|
4279
|
-
// async () => {
|
|
4280
|
-
// try {
|
|
4281
|
-
// // 调用 do-storyboard-optimization 工具时,设置 checkStoryboardFlag 为 true
|
|
4282
|
-
// checkStoryboardFlag = true;
|
|
4283
|
-
// const guidelinePath = resolve(
|
|
4284
|
-
// __dirname,
|
|
4285
|
-
// './prompts/actions/storyboard_optimization.md'
|
|
4286
|
-
// );
|
|
4287
|
-
// const storyboardOptimizationGuidelines = await readFile(
|
|
4288
|
-
// guidelinePath,
|
|
4289
|
-
// 'utf-8'
|
|
4290
|
-
// );
|
|
4291
|
-
// return {
|
|
4292
|
-
// content: [
|
|
4293
|
-
// {
|
|
4294
|
-
// type: 'text' as const,
|
|
4295
|
-
// text: JSON.stringify({
|
|
4296
|
-
// content: {
|
|
4297
|
-
// guideline: storyboardOptimizationGuidelines,
|
|
4298
|
-
// action: '你应当根据guideline优化storyboard.json',
|
|
4299
|
-
// },
|
|
4300
|
-
// }),
|
|
4301
|
-
// },
|
|
4302
|
-
// ],
|
|
4303
|
-
// };
|
|
4304
|
-
// } catch (error) {
|
|
4305
|
-
// return createErrorResponse(error, 'do-storyboard-optimization');
|
|
4306
|
-
// }
|
|
4307
|
-
// }
|
|
4308
|
-
// );
|
|
4309
4362
|
server.registerTool('search-context', {
|
|
4310
4363
|
title: 'Search Context',
|
|
4311
4364
|
description: 'Search the context.',
|
|
@@ -4397,146 +4450,6 @@ server.registerTool('list-project-files', {
|
|
|
4397
4450
|
return createErrorResponse(error, 'list-project-files');
|
|
4398
4451
|
}
|
|
4399
4452
|
});
|
|
4400
|
-
// server.registerTool(
|
|
4401
|
-
// 'build-capcat-draft',
|
|
4402
|
-
// {
|
|
4403
|
-
// title: 'Build CapCut Draft',
|
|
4404
|
-
// description:
|
|
4405
|
-
// 'Read draft_content.json file, parse JSON and generate URIs for all assets in timeline tracks, then output the processed JSON string.',
|
|
4406
|
-
// inputSchema: {
|
|
4407
|
-
// draftContentFile: z
|
|
4408
|
-
// .string()
|
|
4409
|
-
// .optional()
|
|
4410
|
-
// .default('draft_content.json')
|
|
4411
|
-
// .describe(
|
|
4412
|
-
// 'The draft content file name to read (defaults to draft_content.json).'
|
|
4413
|
-
// ),
|
|
4414
|
-
// },
|
|
4415
|
-
// },
|
|
4416
|
-
// async ({ draftContentFile }) => {
|
|
4417
|
-
// try {
|
|
4418
|
-
// await validateSession('build-capcat-draft');
|
|
4419
|
-
// if (!session) {
|
|
4420
|
-
// throw new Error('No active session');
|
|
4421
|
-
// }
|
|
4422
|
-
// // 读取 draft_content.json 文件
|
|
4423
|
-
// const draftContentPath = join(projectLocalDir, draftContentFile);
|
|
4424
|
-
// if (!existsSync(draftContentPath)) {
|
|
4425
|
-
// throw new Error(
|
|
4426
|
-
// `${draftContentFile} file not found in project directory`
|
|
4427
|
-
// );
|
|
4428
|
-
// }
|
|
4429
|
-
// const draftContentRaw = await readFile(draftContentPath, 'utf-8');
|
|
4430
|
-
// const draftContent = JSON.parse(draftContentRaw);
|
|
4431
|
-
// const videoInfos: any[] = [];
|
|
4432
|
-
// const audioInfos: any[] = [];
|
|
4433
|
-
// // 为 timeline 中的所有视频资源生成 URI 和视频信息
|
|
4434
|
-
// if (draftContent.timeline && draftContent.timeline.tracks) {
|
|
4435
|
-
// for (const track of draftContent.timeline.tracks) {
|
|
4436
|
-
// if (track.type === 'video' && track.clips) {
|
|
4437
|
-
// for (const clip of track.clips) {
|
|
4438
|
-
// if (clip.assetId) {
|
|
4439
|
-
// // 在 assets 中找到对应的资源
|
|
4440
|
-
// const asset = draftContent.assets?.find(
|
|
4441
|
-
// (a: any) => a.id === clip.assetId
|
|
4442
|
-
// );
|
|
4443
|
-
// if (asset && asset.uri && asset.type === 'video') {
|
|
4444
|
-
// // 获取本地文件路径并上传到 coze
|
|
4445
|
-
// const localPath = join(
|
|
4446
|
-
// projectLocalDir,
|
|
4447
|
-
// 'materials',
|
|
4448
|
-
// basename(asset.uri)
|
|
4449
|
-
// );
|
|
4450
|
-
// const uploadResult = await uploadFile(localPath);
|
|
4451
|
-
// const videoUrl = uploadResult.url;
|
|
4452
|
-
// // 构建视频信息对象
|
|
4453
|
-
// const videoInfo = {
|
|
4454
|
-
// video_url: videoUrl,
|
|
4455
|
-
// duration: clip.durationMs
|
|
4456
|
-
// ? clip.durationMs * 1000
|
|
4457
|
-
// : asset.durationMs
|
|
4458
|
-
// ? asset.durationMs * 1000
|
|
4459
|
-
// : 0, // 转换为微秒
|
|
4460
|
-
// width: draftContent.settings?.resolution?.width || 1920,
|
|
4461
|
-
// height: draftContent.settings?.resolution?.height || 1080,
|
|
4462
|
-
// start: clip.startMs ? clip.startMs * 1000 : 0, // 转换为微秒
|
|
4463
|
-
// end: ((clip.startMs ?? 0) + (clip.durationMs ?? 0)) * 1000,
|
|
4464
|
-
// };
|
|
4465
|
-
// videoInfos.push(videoInfo);
|
|
4466
|
-
// }
|
|
4467
|
-
// }
|
|
4468
|
-
// }
|
|
4469
|
-
// } else if (track.type === 'audio' && track.clips) {
|
|
4470
|
-
// for (const clip of track.clips) {
|
|
4471
|
-
// if (clip.assetId) {
|
|
4472
|
-
// // 在 assets 中找到对应的资源
|
|
4473
|
-
// const asset = draftContent.assets?.find(
|
|
4474
|
-
// (a: any) => a.id === clip.assetId
|
|
4475
|
-
// );
|
|
4476
|
-
// if (asset && asset.uri && asset.type === 'audio') {
|
|
4477
|
-
// // 获取本地文件路径并上传到 coze
|
|
4478
|
-
// const localPath = join(
|
|
4479
|
-
// projectLocalDir,
|
|
4480
|
-
// 'materials',
|
|
4481
|
-
// basename(asset.uri)
|
|
4482
|
-
// );
|
|
4483
|
-
// const uploadResult = await uploadFile(localPath);
|
|
4484
|
-
// const audioUrl = uploadResult.url;
|
|
4485
|
-
// // 构建音频信息对象
|
|
4486
|
-
// const audioInfo = {
|
|
4487
|
-
// audio_url: audioUrl,
|
|
4488
|
-
// duration: clip.durationMs
|
|
4489
|
-
// ? Math.round(clip.durationMs / 1000)
|
|
4490
|
-
// : asset.durationMs
|
|
4491
|
-
// ? Math.round(asset.durationMs / 1000)
|
|
4492
|
-
// : 0, // 转换为秒
|
|
4493
|
-
// start: clip.startMs ? clip.startMs * 1000 : 0, // 转换为微秒
|
|
4494
|
-
// end: ((clip.startMs ?? 0) + (clip.durationMs ?? 0)) * 1000,
|
|
4495
|
-
// audio_effect: '',
|
|
4496
|
-
// };
|
|
4497
|
-
// audioInfos.push(audioInfo);
|
|
4498
|
-
// }
|
|
4499
|
-
// }
|
|
4500
|
-
// }
|
|
4501
|
-
// }
|
|
4502
|
-
// }
|
|
4503
|
-
// }
|
|
4504
|
-
// // 处理字幕信息
|
|
4505
|
-
// const captionInfos: any[] = [];
|
|
4506
|
-
// if (draftContent.subtitles && Array.isArray(draftContent.subtitles)) {
|
|
4507
|
-
// for (const subtitle of draftContent.subtitles) {
|
|
4508
|
-
// captionInfos.push({
|
|
4509
|
-
// text: subtitle.text || '',
|
|
4510
|
-
// start: (subtitle.startMs || 0) * 1000, // 转换为微秒
|
|
4511
|
-
// end: (subtitle.endMs || 0) * 1000, // 转换为微秒
|
|
4512
|
-
// keyword: '',
|
|
4513
|
-
// });
|
|
4514
|
-
// }
|
|
4515
|
-
// }
|
|
4516
|
-
// const parameters = {
|
|
4517
|
-
// video_infos: JSON.stringify(videoInfos),
|
|
4518
|
-
// audio_infos: JSON.stringify(audioInfos),
|
|
4519
|
-
// caption_infos: JSON.stringify(captionInfos),
|
|
4520
|
-
// width: draftContent.settings?.resolution?.width || 720,
|
|
4521
|
-
// height: draftContent.settings?.resolution?.height || 1280,
|
|
4522
|
-
// };
|
|
4523
|
-
// const workflow_id = '7559885633272758313';
|
|
4524
|
-
// const result = await runWorkflow(workflow_id, parameters);
|
|
4525
|
-
// // 返回 video_infos、audio_infos、captions、width 和 height 对象
|
|
4526
|
-
// return {
|
|
4527
|
-
// content: [
|
|
4528
|
-
// {
|
|
4529
|
-
// type: 'text',
|
|
4530
|
-
// text: JSON.stringify(result, null, 2),
|
|
4531
|
-
// },
|
|
4532
|
-
// ],
|
|
4533
|
-
// };
|
|
4534
|
-
// } catch (error) {
|
|
4535
|
-
// console.error('Error building CapCut draft:', error);
|
|
4536
|
-
// return createErrorResponse(error, 'build-capcat-draft');
|
|
4537
|
-
// }
|
|
4538
|
-
// }
|
|
4539
|
-
// );
|
|
4540
4453
|
async function run() {
|
|
4541
4454
|
// Start receiving messages on stdin and sending messages on stdout
|
|
4542
4455
|
const transport = new stdio_js_1.StdioServerTransport();
|