| import _ from "lodash"; |
| import { PassThrough } from "stream"; |
|
|
| import APIException from "@/lib/exceptions/APIException.ts"; |
| import EX from "@/api/consts/exceptions.ts"; |
| import logger from "@/lib/logger.ts"; |
| import util from "@/lib/util.ts"; |
| import { generateImages, DEFAULT_MODEL } from "./images.ts"; |
| import { generateVideo, DEFAULT_MODEL as DEFAULT_VIDEO_MODEL } from "./videos.ts"; |
|
|
| |
| const MAX_RETRY_COUNT = 3; |
| |
| const RETRY_DELAY = 5000; |
|
|
| |
| |
| |
| |
| |
| |
| function parseModel(model: string) { |
| const [_model, size] = model.split(":"); |
| const [_, width, height] = /(\d+)[\W\w](\d+)/.exec(size) ?? []; |
| return { |
| model: _model, |
| width: size ? Math.ceil(parseInt(width) / 2) * 2 : 1024, |
| height: size ? Math.ceil(parseInt(height) / 2) * 2 : 1024, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function isVideoModel(model: string) { |
| return model.startsWith("jimeng-video"); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export async function createCompletion( |
| messages: any[], |
| refreshToken: string, |
| _model = DEFAULT_MODEL, |
| retryCount = 0 |
| ) { |
| return (async () => { |
| if (messages.length === 0) |
| throw new APIException(EX.API_REQUEST_PARAMS_INVALID, "消息不能为空"); |
|
|
| const { model, width, height } = parseModel(_model); |
| logger.info(messages); |
|
|
| |
| if (isVideoModel(_model)) { |
| try { |
| |
| logger.info(`开始生成视频,模型: ${_model}`); |
| const videoUrl = await generateVideo( |
| _model, |
| messages[messages.length - 1].content, |
| { |
| width, |
| height, |
| resolution: "720p", |
| }, |
| refreshToken |
| ); |
| |
| logger.info(`视频生成成功,URL: ${videoUrl}`); |
| return { |
| id: util.uuid(), |
| model: _model, |
| object: "chat.completion", |
| choices: [ |
| { |
| index: 0, |
| message: { |
| role: "assistant", |
| content: `\n`, |
| }, |
| finish_reason: "stop", |
| }, |
| ], |
| usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, |
| created: util.unixTimestamp(), |
| }; |
| } catch (error) { |
| logger.error(`视频生成失败: ${error.message}`); |
| |
| if (error instanceof APIException) { |
| throw error; |
| } |
| |
| |
| return { |
| id: util.uuid(), |
| model: _model, |
| object: "chat.completion", |
| choices: [ |
| { |
| index: 0, |
| message: { |
| role: "assistant", |
| content: `生成视频失败: ${error.message}\n\n如果您在即梦官网看到已生成的视频,可能是获取结果时出现了问题,请前往即梦官网查看。`, |
| }, |
| finish_reason: "stop", |
| }, |
| ], |
| usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, |
| created: util.unixTimestamp(), |
| }; |
| } |
| } else { |
| |
| const imageUrls = await generateImages( |
| model, |
| messages[messages.length - 1].content, |
| { |
| width, |
| height, |
| }, |
| refreshToken |
| ); |
|
|
| return { |
| id: util.uuid(), |
| model: _model || model, |
| object: "chat.completion", |
| choices: [ |
| { |
| index: 0, |
| message: { |
| role: "assistant", |
| content: imageUrls.reduce( |
| (acc, url, i) => acc + `\n`, |
| "" |
| ), |
| }, |
| finish_reason: "stop", |
| }, |
| ], |
| usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, |
| created: util.unixTimestamp(), |
| }; |
| } |
| })().catch((err) => { |
| if (retryCount < MAX_RETRY_COUNT) { |
| logger.error(`Response error: ${err.stack}`); |
| logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); |
| return (async () => { |
| await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); |
| return createCompletion(messages, refreshToken, _model, retryCount + 1); |
| })(); |
| } |
| throw err; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export async function createCompletionStream( |
| messages: any[], |
| refreshToken: string, |
| _model = DEFAULT_MODEL, |
| retryCount = 0 |
| ) { |
| return (async () => { |
| const { model, width, height } = parseModel(_model); |
| logger.info(messages); |
|
|
| const stream = new PassThrough(); |
|
|
| if (messages.length === 0) { |
| logger.warn("消息为空,返回空流"); |
| stream.end("data: [DONE]\n\n"); |
| return stream; |
| } |
|
|
| |
| if (isVideoModel(_model)) { |
| |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 0, |
| delta: { role: "assistant", content: "🎬 视频生成中,请稍候...\n这可能需要1-2分钟,请耐心等待" }, |
| finish_reason: null, |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
|
|
| |
| logger.info(`开始生成视频,提示词: ${messages[messages.length - 1].content}`); |
| |
| |
| const progressInterval = setInterval(() => { |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 0, |
| delta: { role: "assistant", content: "." }, |
| finish_reason: null, |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
| }, 5000); |
| |
| |
| const timeoutId = setTimeout(() => { |
| clearInterval(progressInterval); |
| logger.warn(`视频生成超时(2分钟),提示用户前往即梦官网查看`); |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 1, |
| delta: { |
| role: "assistant", |
| content: "\n\n视频生成时间较长(已等待2分钟),但视频可能仍在生成中。\n\n请前往即梦官网查看您的视频:\n1. 访问 https://jimeng.jianying.com/ai-tool/video/generate\n2. 登录后查看您的创作历史\n3. 如果视频已生成,您可以直接在官网下载或分享\n\n您也可以继续等待,系统将在后台继续尝试获取视频(最长约20分钟)。", |
| }, |
| finish_reason: "stop", |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
| |
| |
| }, 2 * 60 * 1000); |
|
|
| logger.info(`开始生成视频,模型: ${_model}, 提示词: ${messages[messages.length - 1].content.substring(0, 50)}...`); |
| |
| |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 0, |
| delta: { |
| role: "assistant", |
| content: "\n\n🎬 视频生成已开始,这可能需要几分钟时间...", |
| }, |
| finish_reason: null, |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
| |
| generateVideo( |
| _model, |
| messages[messages.length - 1].content, |
| { width, height, resolution: "720p" }, |
| refreshToken |
| ) |
| .then((videoUrl) => { |
| clearInterval(progressInterval); |
| clearTimeout(timeoutId); |
| |
| logger.info(`视频生成成功,URL: ${videoUrl}`); |
| |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 1, |
| delta: { |
| role: "assistant", |
| content: `\n\n✅ 视频生成完成!\n\n\n\n您可以:\n1. 直接查看上方视频\n2. 使用以下链接下载或分享:${videoUrl}`, |
| }, |
| finish_reason: null, |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
| |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 2, |
| delta: { |
| role: "assistant", |
| content: "", |
| }, |
| finish_reason: "stop", |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
| stream.end("data: [DONE]\n\n"); |
| }) |
| .catch((err) => { |
| clearInterval(progressInterval); |
| clearTimeout(timeoutId); |
| |
| logger.error(`视频生成失败: ${err.message}`); |
| logger.error(`错误详情: ${JSON.stringify(err)}`); |
| |
| |
| logger.error(`视频生成失败: ${err.message}`); |
| logger.error(`错误详情: ${JSON.stringify(err)}`); |
| |
| |
| let errorMessage = `⚠️ 视频生成过程中遇到问题: ${err.message}`; |
| |
| |
| if (err.message.includes("历史记录不存在")) { |
| errorMessage += "\n\n可能原因:\n1. 视频生成请求已发送,但API无法获取历史记录\n2. 视频生成服务暂时不可用\n3. 历史记录ID无效或已过期\n\n建议操作:\n1. 请前往即梦官网查看您的视频是否已生成:https://jimeng.jianying.com/ai-tool/video/generate\n2. 如果官网已显示视频,但这里无法获取,可能是API连接问题\n3. 如果官网也没有显示,请稍后再试或重新生成视频"; |
| } else if (err.message.includes("获取视频生成结果超时")) { |
| errorMessage += "\n\n视频生成可能仍在进行中,但等待时间已超过系统设定的限制。\n\n请前往即梦官网查看您的视频:https://jimeng.jianying.com/ai-tool/video/generate\n\n如果您在官网上看到视频已生成,但这里无法显示,可能是因为:\n1. 获取结果的过程超时\n2. 网络连接问题\n3. API访问限制"; |
| } else { |
| errorMessage += "\n\n如果您在即梦官网看到已生成的视频,可能是获取结果时出现了问题。\n\n请访问即梦官网查看您的创作历史:https://jimeng.jianying.com/ai-tool/video/generate"; |
| } |
| |
| |
| if (err.historyId) { |
| errorMessage += `\n\n历史记录ID: ${err.historyId}(您可以使用此ID在官网搜索您的视频)`; |
| } |
| |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 1, |
| delta: { |
| role: "assistant", |
| content: `\n\n${errorMessage}`, |
| }, |
| finish_reason: "stop", |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
| stream.end("data: [DONE]\n\n"); |
| }); |
| } else { |
| |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model || model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 0, |
| delta: { role: "assistant", content: "🎨 图像生成中,请稍候..." }, |
| finish_reason: null, |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
|
|
| generateImages( |
| model, |
| messages[messages.length - 1].content, |
| { width, height }, |
| refreshToken |
| ) |
| .then((imageUrls) => { |
| for (let i = 0; i < imageUrls.length; i++) { |
| const url = imageUrls[i]; |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model || model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: i + 1, |
| delta: { |
| role: "assistant", |
| content: `\n`, |
| }, |
| finish_reason: i < imageUrls.length - 1 ? null : "stop", |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
| } |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model || model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: imageUrls.length + 1, |
| delta: { |
| role: "assistant", |
| content: "图像生成完成!", |
| }, |
| finish_reason: "stop", |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
| stream.end("data: [DONE]\n\n"); |
| }) |
| .catch((err) => { |
| stream.write( |
| "data: " + |
| JSON.stringify({ |
| id: util.uuid(), |
| model: _model || model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 1, |
| delta: { |
| role: "assistant", |
| content: `生成图片失败: ${err.message}`, |
| }, |
| finish_reason: "stop", |
| }, |
| ], |
| }) + |
| "\n\n" |
| ); |
| stream.end("data: [DONE]\n\n"); |
| }); |
| } |
| return stream; |
| })().catch((err) => { |
| if (retryCount < MAX_RETRY_COUNT) { |
| logger.error(`Response error: ${err.stack}`); |
| logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); |
| return (async () => { |
| await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); |
| return createCompletionStream( |
| messages, |
| refreshToken, |
| _model, |
| retryCount + 1 |
| ); |
| })(); |
| } |
| throw err; |
| }); |
| } |
|
|