70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
|
|
import path from 'path';
|
|||
|
|
import fs from 'fs';
|
|||
|
|
import { execSync } from 'child_process';
|
|||
|
|
import { runInit, loadJSON, PIPELINE_SCRIPT, PROJECT_ROOT } from './shared';
|
|||
|
|
import type { ToolDefinition } from './types';
|
|||
|
|
|
|||
|
|
export const generateVideos: ToolDefinition = {
|
|||
|
|
name: 'generate_videos',
|
|||
|
|
description: '图生视频:根据已有图片和提示词生成 AI 视频。内部创建临时 manifest,先上传图片再执行 videos 阶段。返回视频文件路径。',
|
|||
|
|
input_schema: {
|
|||
|
|
type: 'object',
|
|||
|
|
properties: {
|
|||
|
|
accountId: { type: 'string', description: '账号ID,用于继承视频模型等配置' },
|
|||
|
|
imagePath: { type: 'string', description: '图片文件路径(本地绝对路径或相对 PROJECTROOT 的路径)' },
|
|||
|
|
prompt: { type: 'string', description: '视频提示词(videoPrompt),描述图片如何动起来' },
|
|||
|
|
videoModel: { type: 'string', description: '视频模型(可选,默认继承账号配置): veo3-fast, veo3-fast-frames, kling, grok' },
|
|||
|
|
imagePrompt: { type: 'string', description: '图片提示词(可选),用于 manifest 记录' },
|
|||
|
|
},
|
|||
|
|
required: ['accountId', 'imagePath', 'prompt'],
|
|||
|
|
},
|
|||
|
|
execute: async (params) => {
|
|||
|
|
const { accountId, imagePath, prompt, videoModel, imagePrompt } = params as Record<string, string>;
|
|||
|
|
const resolvedImagePath = path.isAbsolute(imagePath)
|
|||
|
|
? imagePath
|
|||
|
|
: path.resolve(PROJECT_ROOT, imagePath);
|
|||
|
|
if (!fs.existsSync(resolvedImagePath)) {
|
|||
|
|
return `错误: 图片文件不存在: ${resolvedImagePath}`;
|
|||
|
|
}
|
|||
|
|
const baseName = path.basename(resolvedImagePath);
|
|||
|
|
const item = {
|
|||
|
|
id: 1,
|
|||
|
|
shotDesc: imagePrompt || prompt,
|
|||
|
|
script: '',
|
|||
|
|
imagePrompt: imagePrompt || prompt,
|
|||
|
|
videoPrompt: prompt,
|
|||
|
|
keyword: 'generated',
|
|||
|
|
file: `images/${baseName}`,
|
|||
|
|
};
|
|||
|
|
const manifestPath = runInit({
|
|||
|
|
account: accountId,
|
|||
|
|
mode: 'single',
|
|||
|
|
items: [item],
|
|||
|
|
videoModel: videoModel,
|
|||
|
|
});
|
|||
|
|
// Copy image into manifest's images dir
|
|||
|
|
const manifestDir = path.dirname(manifestPath);
|
|||
|
|
const imagesDir = path.join(manifestDir, 'images');
|
|||
|
|
if (!fs.existsSync(imagesDir)) fs.mkdirSync(imagesDir, { recursive: true });
|
|||
|
|
const targetPath = path.join(imagesDir, baseName);
|
|||
|
|
if (resolvedImagePath !== targetPath) {
|
|||
|
|
fs.copyFileSync(resolvedImagePath, targetPath);
|
|||
|
|
}
|
|||
|
|
// Run upload + videos phases
|
|||
|
|
execSync(`node "${PIPELINE_SCRIPT}" run --manifest "${manifestPath}" --phase upload,videos`, {
|
|||
|
|
cwd: PROJECT_ROOT, encoding: 'utf-8',
|
|||
|
|
});
|
|||
|
|
const manifest = loadJSON(manifestPath) as {
|
|||
|
|
items?: Array<{ id: number; video?: string; videoUrl?: string; videoDuration?: number; status?: string }>;
|
|||
|
|
};
|
|||
|
|
const results = (manifest.items || []).map((it) => ({
|
|||
|
|
id: it.id,
|
|||
|
|
video: it.video || null,
|
|||
|
|
videoUrl: it.videoUrl || null,
|
|||
|
|
videoDuration: it.videoDuration || null,
|
|||
|
|
status: it.status,
|
|||
|
|
}));
|
|||
|
|
return JSON.stringify({ manifestPath, videos: results }, null, 2);
|
|||
|
|
},
|
|||
|
|
};
|