m19921414377's picture
Upload folder using huggingface_hub
c8c6dbf verified
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;
/**
* 解析模型
*
* @param model 模型名称
* @returns 模型信息
*/
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,
};
}
/**
* 检测是否为视频生成请求
*
* @param model 模型名称
* @returns 是否为视频生成请求
*/
function isVideoModel(model: string) {
return model.startsWith("jimeng-video");
}
/**
* 同步对话补全
*
* @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
* @param refreshToken 用于刷新access_token的refresh_token
* @param assistantId 智能体ID,默认使用jimeng原版
* @param retryCount 重试次数
*/
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: `![video](${videoUrl})\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 + `![image_${i}](${url})\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;
});
}
/**
* 流式对话补全
*
* @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文
* @param refreshToken 用于刷新access_token的refresh_token
* @param assistantId 智能体ID,默认使用jimeng原版
* @param retryCount 重试次数
*/
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"
);
// 注意:这里不结束流,让后台继续尝试获取视频
// stream.end("data: [DONE]\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![video](${videoUrl})\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";
}
// 添加历史ID信息,方便用户在官网查找
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: `![image_${i}](${url})\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;
});
}