diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index f207906..fd0bea9 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -263,96 +263,95 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => { name: "剧本资产提取", type: "scriptAssetExtraction", - data: ` - --- - name: universal_agent - description: 专注于从剧本内容中提取所使用的资产(角色、场景、道具)并生成结构化资产列表的助手。 - --- - - # Script Assets Extract - - 你是一个专业的剧本内容分析助手,专注于从剧本文本中识别和提取所有涉及的资产(角色、场景、道具),并为每项资产生成可供下游制作流程使用的结构化描述和提示词。 - - ## 何时使用 - - 用户提供剧本内容,你需要逐段阅读并提取其中涉及的所有资产(人物角色、场景地点、道具物件),输出为结构化的资产列表。产出的资产描述将用于后续 AI 图片生成和制作流程。 - - ## 与系统的对应关系 - - - 资产类型: - - \`role\` — 角色(对应 \`o_assets.type = "role"\`) - - \`scene\` — 场景(对应 \`o_assets.type = "scene"\`) - - \`tool\` — 道具(对应 \`o_assets.type = "tool"\`) - - 下游用途:资产提示词生成 → AI 资产图生成 → 分镜制作 - - ## 输出要求 - - **必须通过调用 \`resultTool\` 工具返回结果**,禁止以纯文本、Markdown 表格或 JSON 代码块等形式直接输出资产列表。 - \`resultTool\` 的 schema 会对字段类型和枚举值做强校验,调用时请严格按照下方字段定义填写,确保数据结构正确、字段完整、类型匹配。 - - 每个资产对象包含以下字段: - - | 字段 | 类型 | 必填 | 说明 | - | ---- | ---- | ---- | ---- | - | \`name\` | string | 是 | 资产名称,使用剧本中的原始称呼,不做其他多余描述 | - | \`desc\` | string | 是 | 资产描述,30-80 字的视觉化描述 | - | \`prompt\` | string | 是 | 生成提示词,英文,用于 AI 图片生成 | - | \`type\` | enum | 是 | 资产类型:\`role\` / \`scene\` / \`tool\` | - - ## 提取规则 - - ### 角色(role) - - - 提取剧本中出现的所有有名字的角色 - - \`desc\`:包含外貌特征、服饰风格、体态气质等视觉要素 - - \`prompt\`:英文提示词,描述角色的外观特征,适用于 AI 角色图生成 - - 同一角色有多个称呼时,取最常用的作为 \`name\` - - 无名龙套(如"路人甲"、"士兵")可跳过,除非其造型对剧情有重要视觉意义 - - ### 场景(scene) - - - 提取剧本中出现的所有场景/地点 - - \`desc\`:包含空间结构、光照氛围、关键陈设、色调基调等视觉要素 - - \`prompt\`:英文提示词,描述场景的整体视觉风格,适用于 AI 场景图生成 - - 同一场景的不同状态(如白天/夜晚)不重复提取,在 \`desc\` 中注明即可 - - ### 道具(tool) - - - 提取剧本中出现的重要道具/物品 - - \`desc\`:包含外观形状、颜色材质、尺寸参考、特殊效果等视觉要素 - - \`prompt\`:英文提示词,描述道具的外观细节,适用于 AI 道具图生成 - - 仅提取有独立视觉意义或剧情功能的道具,通用物品可跳过 - - - ## 提示词(prompt)生成规范 - - - 采用逗号分隔的关键词/短语格式 - - 优先描述**视觉特征**,避免抽象概念 - - 包含风格关键词(如 anime style, manga style 等,根据项目风格决定) - - 角色 prompt 示例:\`a young man, sharp eyebrows, black hair, pale skin, wearing a gray Taoist robe, slender build, cold expression\` - - 场景 prompt 示例:\`dark cave interior, glowing crystals on walls, misty atmosphere, dim blue lighting, stone altar in center\` - - 道具 prompt 示例:\`ancient jade pendant, oval shape, translucent green, carved dragon pattern, glowing faintly\` - - ## 提取流程 - - 1. 通读剧本全文,识别所有出现的角色、场景、道具 - 2. 对每个资产生成结构化的 \`name\`、\`desc\`、\`prompt\`、\`type\` - 3. 去重:同一资产不重复提取 - 4. **必须通过调用 \`resultTool\` 工具输出完整资产列表**,不要分多次调用,一次性将所有资产放入 \`assetsList\` 数组中提交 - - ## 提取原则 - - 1. **忠于剧本**:所有提取基于剧本中的实际内容,不臆造未出现的资产 - 2. **视觉优先**:描述和提示词聚焦视觉特征,便于 AI 图片生成 - 3. **精简实用**:只提取对制作有实际意义的资产,避免过度提取 - 4. **分类准确**:严格按照 role/scene/tool 分类,不混淆 - 5. **提示词质量**:英文提示词应具体、可执行,能直接用于 AI 图片生成 - - ## 注意事项 - - - 资产列表中**不要包含剧本内容本身**,仅提取所使用到的资产 - - 角色的随身物品如果有独立剧情功能,应单独作为道具提取 - - 场景中的固定陈设不需要单独提取为道具,除非该物件有独立剧情作用 + data: `--- +name: universal_agent +description: 专注于从剧本内容中提取所使用的资产(角色、场景、道具)并生成结构化资产列表的助手。 +--- + +# Script Assets Extract + +你是一个专业的剧本内容分析助手,专注于从剧本文本中识别和提取所有涉及的资产(角色、场景、道具),并为每项资产生成可供下游制作流程使用的结构化描述和提示词。 + +## 何时使用 + +用户提供剧本内容,你需要逐段阅读并提取其中涉及的所有资产(人物角色、场景地点、道具物件),输出为结构化的资产列表。产出的资产描述将用于后续 AI 图片生成和制作流程。 + +## 与系统的对应关系 + +- 资产类型: + - \`role\` — 角色(对应 \`o_assets.type = "role"\`) + - \`scene\` — 场景(对应 \`o_assets.type = "scene"\`) + - \`tool\` — 道具(对应 \`o_assets.type = "tool"\`) +- 下游用途:资产提示词生成 → AI 资产图生成 → 分镜制作 + +## 输出要求 + +**必须通过调用 \`resultTool\` 工具返回结果**,禁止以纯文本、Markdown 表格或 JSON 代码块等形式直接输出资产列表。 +\`resultTool\` 的 schema 会对字段类型和枚举值做强校验,调用时请严格按照下方字段定义填写,确保数据结构正确、字段完整、类型匹配。 + +每个资产对象包含以下字段: + +| 字段 | 类型 | 必填 | 说明 | +| ---- | ---- | ---- | ---- | +| \`name\` | string | 是 | 资产名称,使用剧本中的原始称呼,不做其他多余描述 | +| \`desc\` | string | 是 | 资产描述,30-80 字的视觉化描述 | +| \`prompt\` | string | 是 | 生成提示词,英文,用于 AI 图片生成 | +| \`type\` | enum | 是 | 资产类型:\`role\` / \`scene\` / \`tool\` | + +## 提取规则 + +### 角色(role) + +- 提取剧本中出现的所有有名字的角色 +- \`desc\`:包含外貌特征、服饰风格、体态气质等视觉要素 +- \`prompt\`:英文提示词,描述角色的外观特征,适用于 AI 角色图生成 +- 同一角色有多个称呼时,取最常用的作为 \`name\` +- 无名龙套(如"路人甲"、"士兵")可跳过,除非其造型对剧情有重要视觉意义 + +### 场景(scene) + +- 提取剧本中出现的所有场景/地点 +- \`desc\`:包含空间结构、光照氛围、关键陈设、色调基调等视觉要素 +- \`prompt\`:英文提示词,描述场景的整体视觉风格,适用于 AI 场景图生成 +- 同一场景的不同状态(如白天/夜晚)不重复提取,在 \`desc\` 中注明即可 + +### 道具(tool) + +- 提取剧本中出现的重要道具/物品 +- \`desc\`:包含外观形状、颜色材质、尺寸参考、特殊效果等视觉要素 +- \`prompt\`:英文提示词,描述道具的外观细节,适用于 AI 道具图生成 +- 仅提取有独立视觉意义或剧情功能的道具,通用物品可跳过 + + +## 提示词(prompt)生成规范 + +- 采用逗号分隔的关键词/短语格式 +- 优先描述**视觉特征**,避免抽象概念 +- 包含风格关键词(如 anime style, manga style 等,根据项目风格决定) +- 角色 prompt 示例:\`a young man, sharp eyebrows, black hair, pale skin, wearing a gray Taoist robe, slender build, cold expression\` +- 场景 prompt 示例:\`dark cave interior, glowing crystals on walls, misty atmosphere, dim blue lighting, stone altar in center\` +- 道具 prompt 示例:\`ancient jade pendant, oval shape, translucent green, carved dragon pattern, glowing faintly\` + +## 提取流程 + +1. 通读剧本全文,识别所有出现的角色、场景、道具 +2. 对每个资产生成结构化的 \`name\`、\`desc\`、\`prompt\`、\`type\` +3. 去重:同一资产不重复提取 +4. **必须通过调用 \`resultTool\` 工具输出完整资产列表**,不要分多次调用,一次性将所有资产放入 \`assetsList\` 数组中提交 + +## 提取原则 + +1. **忠于剧本**:所有提取基于剧本中的实际内容,不臆造未出现的资产 +2. **视觉优先**:描述和提示词聚焦视觉特征,便于 AI 图片生成 +3. **精简实用**:只提取对制作有实际意义的资产,避免过度提取 +4. **分类准确**:严格按照 role/scene/tool 分类,不混淆 +5. **提示词质量**:英文提示词应具体、可执行,能直接用于 AI 图片生成 + +## 注意事项 + +- 资产列表中**不要包含剧本内容本身**,仅提取所使用到的资产 +- 角色的随身物品如果有独立剧情功能,应单独作为道具提取 +- 场景中的固定陈设不需要单独提取为道具,除非该物件有独立剧情作用 `, }, ]); diff --git a/src/routes/assetsGenerate/batchGenerateImageAssets.ts b/src/routes/assetsGenerate/batchGenerateImageAssets.ts index 1fce062..55e4e30 100644 --- a/src/routes/assetsGenerate/batchGenerateImageAssets.ts +++ b/src/routes/assetsGenerate/batchGenerateImageAssets.ts @@ -110,16 +110,20 @@ export default router.post("/", validateFields(requestSchema), async (req, res) try { const aiImage = u.Ai.Image(model); - await aiImage.run({ - prompt: userPrompt, - imageBase64: item.base64 ? [item.base64] : [], - size: resolution, - aspectRatio: "16:9", - taskClass: cfg.taskClass, - describe, - projectId, - relatedObjects: JSON.stringify(relatedObjects), - }); + await aiImage.run( + { + prompt: userPrompt, + imageBase64: item.base64 ? [item.base64] : [], + size: resolution, + aspectRatio: "16:9", + }, + { + taskClass: cfg.taskClass, + describe, + projectId, + relatedObjects: JSON.stringify(relatedObjects), + }, + ); aiImage.save(imagePath); const imageData = await u.db("o_image").where("id", imageId).select("*").first(); diff --git a/src/routes/assetsGenerate/generateAssets.ts b/src/routes/assetsGenerate/generateAssets.ts index 4314a4b..830c536 100644 --- a/src/routes/assetsGenerate/generateAssets.ts +++ b/src/routes/assetsGenerate/generateAssets.ts @@ -98,16 +98,20 @@ export default router.post("/", validateFields(requestSchema), async (req, res) try { // 4. 调用 AI 生成图片 const aiImage = u.Ai.Image(model); - await aiImage.run({ - prompt: userPrompt, - imageBase64: base64 ? [base64] : [], - size: resolution, - aspectRatio: "16:9", - taskClass: cfg.taskClass, - describe, - projectId, - relatedObjects: JSON.stringify(relatedObjects), - }); + await aiImage.run( + { + prompt: userPrompt, + imageBase64: base64 ? [base64] : [], + size: resolution, + aspectRatio: "16:9", + }, + { + taskClass: cfg.taskClass, + describe, + projectId, + relatedObjects: JSON.stringify(relatedObjects), + }, + ); aiImage.save(imagePath); // 5. 更新记录 & 返回结果 diff --git a/src/routes/production/workbench/generateVideo.ts b/src/routes/production/workbench/generateVideo.ts index cb38602..eb7582b 100644 --- a/src/routes/production/workbench/generateVideo.ts +++ b/src/routes/production/workbench/generateVideo.ts @@ -105,19 +105,23 @@ export default router.post( console.log("%c Line:110 🍑 prompt", "background:#b03734", prompt); const aiVideo = u.Ai.Video(model); - await aiVideo.run({ - projectId, - prompt, - imageBase64: base64.filter((item) => item !== null) as string[], - mode, - duration, - aspectRatio: (ratio?.videoRatio as `${number}:${number}`) || "16:9", - resolution, - audio, - taskClass: "视频生成", - describe: "根据提示词生成视频", - relatedObjects: JSON.stringify(relatedObjects), - }); + await aiVideo.run( + { + prompt, + imageBase64: base64.filter((item) => item !== null) as string[], + mode, + duration, + aspectRatio: (ratio?.videoRatio as `${number}:${number}`) || "16:9", + resolution, + audio, + }, + { + projectId, + taskClass: "视频生成", + describe: "根据提示词生成视频", + relatedObjects: JSON.stringify(relatedObjects), + }, + ); await aiVideo.save(videoPath); await u.db("o_video").where("id", videoId).update({ state: "生成成功" }); // await u.db("o_videoConfig").where("storyboardId", storyboardId).update({ videoId, updateTime: Date.now() }); diff --git a/src/types/database.d.ts b/src/types/database.d.ts index 9b42b68..ab3e7f9 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,8 +1,4 @@ -<<<<<<< HEAD -// @db-hash 93b2462070c45c2b449e9a18c4e88763 -======= // @db-hash 24748d4ef971381a79c720c846f83847 ->>>>>>> 796947cef173e7fe2f96e21fa8aeae23ff0fdf4a //该文件由脚本自动生成,请勿手动修改 export interface memories { @@ -182,6 +178,7 @@ export interface o_storyboard { 'sound'?: string | null; 'state'?: string | null; 'title'?: string | null; + 'videoPrompt'?: string | null; } export interface o_tasks { 'describe'?: string | null; diff --git a/src/utils/ai.ts b/src/utils/ai.ts index 9c6df1e..4f3548d 100644 --- a/src/utils/ai.ts +++ b/src/utils/ai.ts @@ -25,23 +25,10 @@ async function getVendorTemplateFn(fnName: FnName, modelName: `${string}:${strin const selectedModel = modelList.find((i: any) => i.modelName == name); if (!selectedModel) throw new Error(`未找到模型 ${name} id=${id}`); const jsCode = transform(vendorConfigData.code!, { transforms: ["typescript"] }).code; - const running = u.vm(jsCode); - Object.assign(running.vendor.inputValues, JSON.parse(vendorConfigData.inputValues ?? "{}")); - running.vendor.models = modelList; - const fn = running[fnName]; + const fn = u.vm(jsCode)[fnName]; if (!fn) throw new Error(`未找到供应商配置中的函数 ${fnName} id=${id}`); - if (fnName == "textRequest") { - const model = fn(selectedModel); - if (!model) throw new Error(`供应商 textRequest 返回无效模型 id=${id}`); - return model; - } - return async (input: T) => { - const result = await fn(input, selectedModel); - if (result === undefined || result === null) { - throw new Error(`供应商函数 ${fnName} 未返回有效结果 id=${id}`); - } - return result; - }; + if (fnName == "textRequest") return fn(selectedModel); + else return (input: T) => fn(input, selectedModel); } async function withTaskRecord( @@ -113,6 +100,9 @@ interface ImageConfig { imageBase64: string[]; //输入的图片提示词 size: "1K" | "2K" | "4K"; // 图片尺寸 aspectRatio: `${number}:${number}`; // 长宽比 +} + +interface TaskRecord { taskClass: string; // 任务分类 describe: string; // 任务描述 relatedObjects: string; // 相关对象信息,便于后续分析和追踪 @@ -125,8 +115,8 @@ class AiImage { constructor(key: `${string}:${string}`) { this.key = key; } - async run(input: ImageConfig) { - return withTaskRecord(this.key, input.taskClass, input.describe, input.relatedObjects, input.projectId, async (modelName) => { + async run(input: ImageConfig, taskRecord: TaskRecord) { + return withTaskRecord(this.key, taskRecord.taskClass, taskRecord.describe, taskRecord.relatedObjects, taskRecord.projectId, async (modelName) => { const fn = await getVendorTemplateFn("imageRequest", modelName); this.result = await fn(input); if (this.result.startsWith("http")) this.result = await urlToBase64(this.result); @@ -139,7 +129,6 @@ class AiImage { } } interface VideoConfig { - projectId: number; // 项目ID prompt: string; //视频提示词 imageBase64: string[]; //输入的图片提示词 aspectRatio: `${number}:${number}`; // 长宽比 @@ -147,9 +136,6 @@ interface VideoConfig { duration: number; // 视频时长,单位秒 resolution: string; // 视频分辨率 audio: boolean; // 是否需要配音 - taskClass: string; // 任务分类 - describe: string; // 任务描述 - relatedObjects: string; // 相关对象信息,便于后续分析和追踪 } class AiVideo { @@ -158,8 +144,8 @@ class AiVideo { constructor(key: `${string}:${string}`) { this.key = key; } - async run(input: VideoConfig) { - return withTaskRecord(this.key, input.taskClass, input.describe, input.relatedObjects, input.projectId, async (modelName) => { + async run(input: VideoConfig, taskRecord: TaskRecord) { + return withTaskRecord(this.key, taskRecord.taskClass, taskRecord.describe, taskRecord.relatedObjects, taskRecord.projectId, async (modelName) => { const fn = await getVendorTemplateFn("videoRequest", modelName); this.result = await fn(input); if (this.result.startsWith("http")) this.result = await urlToBase64(this.result); @@ -177,8 +163,8 @@ class AiAudio { constructor(key: `${string}:${string}`) { this.key = key; } - async run(input: VideoConfig) { - return withTaskRecord(this.key, input.taskClass, input.describe, input.relatedObjects, input.projectId, async (modelName) => { + async run(input: VideoConfig, taskRecord: TaskRecord) { + return withTaskRecord(this.key, taskRecord.taskClass, taskRecord.describe, taskRecord.relatedObjects, taskRecord.projectId, async (modelName) => { const fn = await getVendorTemplateFn("ttsRequest", modelName); this.result = await fn(input); if (this.result.startsWith("http")) this.result = await urlToBase64(this.result);