279 lines
11 KiB
TypeScript
279 lines
11 KiB
TypeScript
|
|
import fs from "fs";
|
|||
|
|
import _ from "lodash";
|
|||
|
|
|
|||
|
|
import Request from "@/lib/request/Request.ts";
|
|||
|
|
import { generateImages, generateImageComposition } from "@/api/controllers/images.ts";
|
|||
|
|
import { tokenSplit } from "@/api/controllers/core.ts";
|
|||
|
|
import util from "@/lib/util.ts";
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
prefix: "/v1/images",
|
|||
|
|
|
|||
|
|
post: {
|
|||
|
|
"/generations": async (request: Request) => {
|
|||
|
|
// 检查是否使用了不支持的参数
|
|||
|
|
const unsupportedParams = ['size', 'width', 'height'];
|
|||
|
|
const bodyKeys = Object.keys(request.body);
|
|||
|
|
const foundUnsupported = unsupportedParams.filter(param => bodyKeys.includes(param));
|
|||
|
|
|
|||
|
|
if (foundUnsupported.length > 0) {
|
|||
|
|
throw new Error(`不支持的参数: ${foundUnsupported.join(', ')}。请使用 ratio 和 resolution 参数控制图像尺寸。`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const contentType = request.headers['content-type'] || '';
|
|||
|
|
const isMultiPart = contentType.startsWith('multipart/form-data');
|
|||
|
|
|
|||
|
|
// 根据请求类型进行不同的参数验证
|
|||
|
|
if (isMultiPart) {
|
|||
|
|
request
|
|||
|
|
.validate("body.model", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.prompt", _.isString)
|
|||
|
|
.validate("body.negative_prompt", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.ratio", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.resolution", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.intelligent_ratio", v => _.isUndefined(v) || (typeof v === 'string' && (v === 'true' || v === 'false')) || _.isBoolean(v))
|
|||
|
|
.validate("body.sample_strength", v => _.isUndefined(v) || (typeof v === 'string' && !isNaN(parseFloat(v))) || _.isFinite(v))
|
|||
|
|
.validate("body.response_format", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("headers.authorization", _.isString);
|
|||
|
|
} else {
|
|||
|
|
request
|
|||
|
|
.validate("body.model", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.prompt", _.isString)
|
|||
|
|
.validate("body.images", v => _.isUndefined(v) || _.isArray(v))
|
|||
|
|
.validate("body.negative_prompt", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.ratio", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.resolution", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.intelligent_ratio", v => _.isUndefined(v) || _.isBoolean(v))
|
|||
|
|
.validate("body.sample_strength", v => _.isUndefined(v) || _.isFinite(v))
|
|||
|
|
.validate("body.response_format", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("headers.authorization", _.isString);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理图片数据(如果提供)
|
|||
|
|
let images: (string | Buffer)[] | null = null;
|
|||
|
|
if (isMultiPart) {
|
|||
|
|
const files = (request.files as any)?.images;
|
|||
|
|
if (files) {
|
|||
|
|
const imageFiles = Array.isArray(files) ? files : [files];
|
|||
|
|
if (imageFiles.length > 0) {
|
|||
|
|
if (imageFiles.length > 10) {
|
|||
|
|
throw new Error("最多支持10张输入图片");
|
|||
|
|
}
|
|||
|
|
images = imageFiles.map((file: any) => fs.readFileSync(file.filepath));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
const bodyImages = request.body.images;
|
|||
|
|
if (bodyImages && Array.isArray(bodyImages) && bodyImages.length > 0) {
|
|||
|
|
if (bodyImages.length > 10) {
|
|||
|
|
throw new Error("最多支持10张输入图片");
|
|||
|
|
}
|
|||
|
|
bodyImages.forEach((image: any, index: number) => {
|
|||
|
|
if (!_.isString(image) && !_.isObject(image)) {
|
|||
|
|
throw new Error(`图片 ${index + 1} 格式不正确:应为URL字符串或包含url字段的对象`);
|
|||
|
|
}
|
|||
|
|
if (_.isObject(image) && !(image as any).url) {
|
|||
|
|
throw new Error(`图片 ${index + 1} 缺少url字段`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
images = bodyImages.map((image: any) => _.isString(image) ? image : (image as any).url);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// refresh_token切分
|
|||
|
|
const tokens = tokenSplit(request.headers.authorization);
|
|||
|
|
// 随机挑选一个refresh_token
|
|||
|
|
const token = _.sample(tokens);
|
|||
|
|
|
|||
|
|
const {
|
|||
|
|
model,
|
|||
|
|
prompt,
|
|||
|
|
negative_prompt: negativePrompt,
|
|||
|
|
ratio,
|
|||
|
|
resolution,
|
|||
|
|
intelligent_ratio: intelligentRatio,
|
|||
|
|
sample_strength: sampleStrength,
|
|||
|
|
response_format,
|
|||
|
|
} = request.body;
|
|||
|
|
|
|||
|
|
// 如果是 multipart/form-data,需要将字符串转换为数字和布尔值
|
|||
|
|
const finalSampleStrength = isMultiPart && typeof sampleStrength === 'string'
|
|||
|
|
? parseFloat(sampleStrength)
|
|||
|
|
: sampleStrength;
|
|||
|
|
|
|||
|
|
const finalIntelligentRatio = isMultiPart && typeof intelligentRatio === 'string'
|
|||
|
|
? intelligentRatio === 'true'
|
|||
|
|
: intelligentRatio;
|
|||
|
|
|
|||
|
|
const responseFormat = _.defaultTo(response_format, "url");
|
|||
|
|
|
|||
|
|
// 根据是否有图片数据决定调用文生图还是图生图
|
|||
|
|
let imageUrls: string[];
|
|||
|
|
let resultData: any = {
|
|||
|
|
created: util.unixTimestamp(),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (images && images.length > 0) {
|
|||
|
|
// 图生图模式
|
|||
|
|
imageUrls = await generateImageComposition(model, prompt, images, {
|
|||
|
|
ratio,
|
|||
|
|
resolution,
|
|||
|
|
sampleStrength: finalSampleStrength,
|
|||
|
|
negativePrompt,
|
|||
|
|
intelligentRatio: finalIntelligentRatio,
|
|||
|
|
}, token);
|
|||
|
|
resultData.input_images = images.length;
|
|||
|
|
resultData.composition_type = "multi_image_synthesis";
|
|||
|
|
} else {
|
|||
|
|
// 文生图模式
|
|||
|
|
imageUrls = await generateImages(model, prompt, {
|
|||
|
|
ratio,
|
|||
|
|
resolution,
|
|||
|
|
sampleStrength: finalSampleStrength,
|
|||
|
|
negativePrompt,
|
|||
|
|
intelligentRatio: finalIntelligentRatio,
|
|||
|
|
}, token);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let data = [];
|
|||
|
|
if (responseFormat == "b64_json") {
|
|||
|
|
data = (
|
|||
|
|
await Promise.all(imageUrls.map((url) => util.fetchFileBASE64(url)))
|
|||
|
|
).map((b64) => ({ b64_json: b64 }));
|
|||
|
|
} else {
|
|||
|
|
data = imageUrls.map((url) => ({
|
|||
|
|
url,
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resultData.data = data;
|
|||
|
|
return resultData;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 图片合成路由(图生图)
|
|||
|
|
"/compositions": async (request: Request) => {
|
|||
|
|
// 检查是否使用了不支持的参数
|
|||
|
|
const unsupportedParams = ['size', 'width', 'height'];
|
|||
|
|
const bodyKeys = Object.keys(request.body);
|
|||
|
|
const foundUnsupported = unsupportedParams.filter(param => bodyKeys.includes(param));
|
|||
|
|
|
|||
|
|
if (foundUnsupported.length > 0) {
|
|||
|
|
throw new Error(`不支持的参数: ${foundUnsupported.join(', ')}。请使用 ratio 和 resolution 参数控制图像尺寸。`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const contentType = request.headers['content-type'] || '';
|
|||
|
|
const isMultiPart = contentType.startsWith('multipart/form-data');
|
|||
|
|
|
|||
|
|
if (isMultiPart) {
|
|||
|
|
request
|
|||
|
|
.validate("body.model", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.prompt", _.isString)
|
|||
|
|
.validate("body.negative_prompt", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.ratio", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.resolution", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.intelligent_ratio", v => _.isUndefined(v) || (typeof v === 'string' && (v === 'true' || v === 'false')) || _.isBoolean(v))
|
|||
|
|
.validate("body.sample_strength", v => _.isUndefined(v) || (typeof v === 'string' && !isNaN(parseFloat(v))) || _.isFinite(v))
|
|||
|
|
.validate("body.response_format", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("headers.authorization", _.isString);
|
|||
|
|
} else {
|
|||
|
|
request
|
|||
|
|
.validate("body.model", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.prompt", _.isString)
|
|||
|
|
.validate("body.images", _.isArray)
|
|||
|
|
.validate("body.negative_prompt", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.ratio", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.resolution", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("body.intelligent_ratio", v => _.isUndefined(v) || _.isBoolean(v))
|
|||
|
|
.validate("body.sample_strength", v => _.isUndefined(v) || _.isFinite(v))
|
|||
|
|
.validate("body.response_format", v => _.isUndefined(v) || _.isString(v))
|
|||
|
|
.validate("headers.authorization", _.isString);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let images: (string | Buffer)[] = [];
|
|||
|
|
if (isMultiPart) {
|
|||
|
|
const files = (request.files as any)?.images;
|
|||
|
|
if (!files) {
|
|||
|
|
throw new Error("在form-data中缺少 'images' 字段");
|
|||
|
|
}
|
|||
|
|
const imageFiles = Array.isArray(files) ? files : [files];
|
|||
|
|
if (imageFiles.length === 0) {
|
|||
|
|
throw new Error("至少需要提供1张输入图片");
|
|||
|
|
}
|
|||
|
|
if (imageFiles.length > 10) {
|
|||
|
|
throw new Error("最多支持10张输入图片");
|
|||
|
|
}
|
|||
|
|
images = imageFiles.map((file: any) => fs.readFileSync(file.filepath));
|
|||
|
|
} else {
|
|||
|
|
const bodyImages = request.body.images;
|
|||
|
|
if (!bodyImages || bodyImages.length === 0) {
|
|||
|
|
throw new Error("至少需要提供1张输入图片");
|
|||
|
|
}
|
|||
|
|
if (bodyImages.length > 10) {
|
|||
|
|
throw new Error("最多支持10张输入图片");
|
|||
|
|
}
|
|||
|
|
bodyImages.forEach((image: any, index: number) => {
|
|||
|
|
if (!_.isString(image) && !_.isObject(image)) {
|
|||
|
|
throw new Error(`图片 ${index + 1} 格式不正确:应为URL字符串或包含url字段的对象`);
|
|||
|
|
}
|
|||
|
|
if (_.isObject(image) && !(image as any).url) {
|
|||
|
|
throw new Error(`图片 ${index + 1} 缺少url字段`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
images = bodyImages.map((image: any) => _.isString(image) ? image : (image as any).url);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// refresh_token切分
|
|||
|
|
const tokens = tokenSplit(request.headers.authorization);
|
|||
|
|
// 随机挑选一个refresh_token
|
|||
|
|
const token = _.sample(tokens);
|
|||
|
|
|
|||
|
|
const {
|
|||
|
|
model,
|
|||
|
|
prompt,
|
|||
|
|
negative_prompt: negativePrompt,
|
|||
|
|
ratio,
|
|||
|
|
resolution,
|
|||
|
|
intelligent_ratio: intelligentRatio,
|
|||
|
|
sample_strength: sampleStrength,
|
|||
|
|
response_format,
|
|||
|
|
} = request.body;
|
|||
|
|
|
|||
|
|
// 如果是 multipart/form-data,需要将字符串转换为数字和布尔值
|
|||
|
|
const finalSampleStrength = isMultiPart && typeof sampleStrength === 'string'
|
|||
|
|
? parseFloat(sampleStrength)
|
|||
|
|
: sampleStrength;
|
|||
|
|
|
|||
|
|
const finalIntelligentRatio = isMultiPart && typeof intelligentRatio === 'string'
|
|||
|
|
? intelligentRatio === 'true'
|
|||
|
|
: intelligentRatio;
|
|||
|
|
|
|||
|
|
const responseFormat = _.defaultTo(response_format, "url");
|
|||
|
|
const resultUrls = await generateImageComposition(model, prompt, images, {
|
|||
|
|
ratio,
|
|||
|
|
resolution,
|
|||
|
|
sampleStrength: finalSampleStrength,
|
|||
|
|
negativePrompt,
|
|||
|
|
intelligentRatio: finalIntelligentRatio,
|
|||
|
|
}, token);
|
|||
|
|
|
|||
|
|
let data = [];
|
|||
|
|
if (responseFormat == "b64_json") {
|
|||
|
|
data = (
|
|||
|
|
await Promise.all(resultUrls.map((url) => util.fetchFileBASE64(url)))
|
|||
|
|
).map((b64) => ({ b64_json: b64 }));
|
|||
|
|
} else {
|
|||
|
|
data = resultUrls.map((url) => ({
|
|||
|
|
url,
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
created: util.unixTimestamp(),
|
|||
|
|
data,
|
|||
|
|
input_images: images.length,
|
|||
|
|
composition_type: "multi_image_synthesis",
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
};
|