图片生成、视频生成
This commit is contained in:
parent
1da0ef5c18
commit
6e8e89b624
@ -1,14 +1,14 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
jimeng-free-api:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile
|
||||
image: jimeng-free-api:latest
|
||||
container_name: jimeng-free-api
|
||||
ports:
|
||||
- "8443:3302"
|
||||
# jimeng-free-api:
|
||||
# build:
|
||||
# context: ./
|
||||
# dockerfile: Dockerfile
|
||||
# image: jimeng-free-api:latest
|
||||
# container_name: jimeng-free-api
|
||||
# ports:
|
||||
# - "8443:3302"
|
||||
jimeng-free-api-pro:
|
||||
build:
|
||||
context: ./
|
||||
|
||||
92
src/api/ImagesTaskCache.ts
Normal file
92
src/api/ImagesTaskCache.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
// import { format as dateFormat } from 'date-fns';
|
||||
const timeZone = 'Asia/Shanghai'; // Beijing Time
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
|
||||
const LOG_PATH = path.resolve("./logs/images_task_cache.log");
|
||||
|
||||
function cacheLog(value: string, color?: string) {
|
||||
try {
|
||||
const head = `[ImagesTaskCache][${formatInTimeZone(new Date(),timeZone, "yyyy-MM-dd HH:mm:ss.SSS")}] `;
|
||||
value = head + value;
|
||||
// console.log(color ? value[color] : value);
|
||||
fs.ensureDirSync(path.dirname(LOG_PATH));
|
||||
fs.appendFileSync(LOG_PATH, value + "\n");
|
||||
}
|
||||
catch(err) {
|
||||
console.error("ImagesTaskCache log write error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
export class ImagesTaskCache {
|
||||
private static instance: ImagesTaskCache;
|
||||
private taskCache: Map<string, number|string>;
|
||||
|
||||
private constructor() {
|
||||
this.taskCache = new Map<string, number|string>();
|
||||
cacheLog("ImagesTaskCache initialized");
|
||||
}
|
||||
|
||||
public static getInstance(): ImagesTaskCache {
|
||||
if (!ImagesTaskCache.instance) {
|
||||
ImagesTaskCache.instance = new ImagesTaskCache();
|
||||
}
|
||||
return ImagesTaskCache.instance;
|
||||
}
|
||||
|
||||
public startTask(taskId: string): void {
|
||||
const startTime = Math.floor(Date.now() / 1000); // Current time in seconds
|
||||
this.taskCache.set(taskId, startTime);
|
||||
cacheLog(`Task started: ${taskId} at ${startTime}`);
|
||||
}
|
||||
|
||||
public finishTask(taskId: string, status: -1 | -2 | -3, url:string = ''): void {
|
||||
if (this.taskCache.has(taskId)) {
|
||||
this.taskCache.set(taskId, status);
|
||||
let statusMessage = '';
|
||||
switch (status) {
|
||||
case -1:
|
||||
{
|
||||
statusMessage = 'successfully';
|
||||
if (url) {
|
||||
this.taskCache.set(taskId, url);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case -2: statusMessage = 'failed'; break;
|
||||
case -3: statusMessage = 'timed out'; break;
|
||||
}
|
||||
cacheLog(`Task ${taskId} finished ${statusMessage} (status: ${status})`);
|
||||
} else {
|
||||
cacheLog(`Attempted to finish non-existent task: ${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
public getTaskStatus(taskId: string): number | string | undefined {
|
||||
return this.taskCache.get(taskId);
|
||||
}
|
||||
|
||||
public getPendingTasks(): string[] {
|
||||
const pendingTasks: string[] = [];
|
||||
for (const [taskId, status] of this.taskCache.entries()) {
|
||||
if (typeof status == 'number' && status > 0) {
|
||||
pendingTasks.push(taskId);
|
||||
}
|
||||
}
|
||||
return pendingTasks;
|
||||
}
|
||||
|
||||
public logPendingTasksOnShutdown(): void {
|
||||
const pendingTasks = this.getPendingTasks();
|
||||
if (pendingTasks.length > 0) {
|
||||
cacheLog(`Pending tasks at shutdown: ${pendingTasks.join(', ')}`, 'yellow');
|
||||
} else {
|
||||
cacheLog("No pending tasks at shutdown.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the singleton instance when the module is loaded.
|
||||
// This ensures it's ready when the service starts.
|
||||
ImagesTaskCache.getInstance();
|
||||
@ -5,11 +5,13 @@ import EX from "@/api/consts/exceptions.ts";
|
||||
import util from "@/lib/util.ts";
|
||||
import { getCredit, receiveCredit, request } from "./core.ts";
|
||||
import logger from "@/lib/logger.ts";
|
||||
import { ImagesTaskCache } from '@/api/ImagesTaskCache.ts';
|
||||
|
||||
const DEFAULT_ASSISTANT_ID = "513695";
|
||||
export const DEFAULT_MODEL = "jimeng-3.0";
|
||||
const DRAFT_VERSION = "3.0.2";
|
||||
const MODEL_MAP = {
|
||||
"jimeng-3.1": "high_aes_general_v30l_art_fangzhou:general_v3.0_18b",
|
||||
"jimeng-3.0": "high_aes_general_v30l:general_v3.0_18b",
|
||||
"jimeng-2.1": "high_aes_general_v21_L:general_v2.1_L",
|
||||
"jimeng-2.0-pro": "high_aes_general_v20_L:general_v2.0_L",
|
||||
@ -24,6 +26,7 @@ export function getModel(model: string) {
|
||||
|
||||
export async function generateImages(
|
||||
_model: string,
|
||||
task_id: string,
|
||||
prompt: string,
|
||||
{
|
||||
width = 1024,
|
||||
@ -38,6 +41,10 @@ export async function generateImages(
|
||||
},
|
||||
refreshToken: string
|
||||
) {
|
||||
const imagesTaskCache = ImagesTaskCache.getInstance();
|
||||
imagesTaskCache.startTask(task_id);
|
||||
|
||||
try {
|
||||
const model = getModel(_model);
|
||||
logger.info(`使用模型: ${_model} 映射模型: ${model} ${width}x${height} 精细度: ${sampleStrength}`);
|
||||
|
||||
@ -109,6 +116,7 @@ export async function generateImages(
|
||||
id: util.uuid(),
|
||||
height,
|
||||
width,
|
||||
resolution_type: "1k",
|
||||
},
|
||||
},
|
||||
history_option: {
|
||||
@ -131,7 +139,7 @@ export async function generateImages(
|
||||
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
|
||||
let status = 20, failCode, item_list = [];
|
||||
while (status === 20) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
|
||||
data: {
|
||||
history_ids: [historyId],
|
||||
@ -243,11 +251,25 @@ export async function generateImages(
|
||||
else
|
||||
throw new APIException(EX.API_IMAGE_GENERATION_FAILED);
|
||||
}
|
||||
return item_list.map((item) => {
|
||||
const imageUrls = item_list.map((item) => {
|
||||
if(!item?.image?.large_images?.[0]?.image_url)
|
||||
return item?.common_attr?.cover_url || null;
|
||||
return item.image.large_images[0].image_url;
|
||||
});
|
||||
const validImageUrls = imageUrls.filter(url => url !== null);
|
||||
if (validImageUrls.length > 0) {
|
||||
imagesTaskCache.finishTask(task_id, -1, validImageUrls.join(",")); // Success
|
||||
} else {
|
||||
// If no valid URLs but no explicit error thrown earlier, consider it a failure.
|
||||
// This could happen if item_list is empty or items don't have video_url.
|
||||
imagesTaskCache.finishTask(task_id, -2); // Failure
|
||||
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "图片生成未返回有效链接");
|
||||
}
|
||||
return validImageUrls;
|
||||
}catch (error) {
|
||||
imagesTaskCache.finishTask(task_id, -2); // Failure due to exception
|
||||
throw error; // Re-throw the error to be handled by the caller
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadImages(
|
||||
|
||||
@ -13,6 +13,7 @@ const DRAFT_VERSION = "3.0.5";
|
||||
const DRAFT_V_VERSION = "1.0.0";
|
||||
const MODEL_MAP = {
|
||||
"jimeng-v-3.0": "dreamina_ic_generate_video_model_vgfm_3.0",
|
||||
"jimeng-v-3.0-pro": "dreamina_ic_generate_video_model_vgfm_3.0_pro",
|
||||
};
|
||||
|
||||
export function getModel(model: string) {
|
||||
@ -20,19 +21,18 @@ export function getModel(model: string) {
|
||||
}
|
||||
//图片生成视频 首帧
|
||||
export async function generateVideo(
|
||||
_model: string,
|
||||
task_id: string,
|
||||
prompt: string,
|
||||
{
|
||||
width = 512,
|
||||
height = 512,
|
||||
imgURL = "",
|
||||
images = [],
|
||||
isPro = false,
|
||||
duration = 5000,
|
||||
ratio = '9:16',
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
imgURL: string;
|
||||
images: any[];
|
||||
isPro: boolean;
|
||||
duration: number;
|
||||
ratio: string;
|
||||
},
|
||||
refreshToken: string
|
||||
) {
|
||||
@ -40,13 +40,64 @@ export async function generateVideo(
|
||||
videoTaskCache.startTask(task_id);
|
||||
|
||||
try {
|
||||
if(!imgURL){
|
||||
throw new APIException(EX.API_REQUEST_PARAMS_INVALID);
|
||||
return;
|
||||
let _model = DEFAULT_MODEL;
|
||||
if(isPro){
|
||||
_model = "jimeng-v-3.0-pro";
|
||||
}
|
||||
let video_gen_inputs:any = {
|
||||
type: "",
|
||||
id: util.uuid(),
|
||||
min_version: DRAFT_V_VERSION,
|
||||
prompt: prompt,
|
||||
video_mode:2,
|
||||
fps:24,
|
||||
duration_ms:duration,
|
||||
};
|
||||
if(!images || !images.length){
|
||||
//无图片
|
||||
}else if(images.length === 1){
|
||||
//首帧
|
||||
video_gen_inputs.first_frame_image = {
|
||||
type: "image",
|
||||
id: util.uuid(),
|
||||
source_from: "upload",
|
||||
platform_type: 1,
|
||||
name: "",
|
||||
image_uri: images[0].url,
|
||||
width: images[0].width,
|
||||
height: images[0].height,
|
||||
format: "",
|
||||
uri: images[0].url,
|
||||
}
|
||||
}else if(images.length >= 2){
|
||||
//首尾帧
|
||||
video_gen_inputs.first_frame_image = {
|
||||
type: "image",
|
||||
id: util.uuid(),
|
||||
source_from: "upload",
|
||||
platform_type: 1,
|
||||
name: "",
|
||||
image_uri: images[0].url,
|
||||
width: images[0].width,
|
||||
height: images[0].height,
|
||||
format: "",
|
||||
uri: images[0].url,
|
||||
}
|
||||
video_gen_inputs.end_frame_image = {
|
||||
type: "image",
|
||||
id: util.uuid(),
|
||||
source_from: "upload",
|
||||
platform_type: 1,
|
||||
name: "",
|
||||
image_uri: images[1].url,
|
||||
width: images[1].width,
|
||||
height: images[1].height,
|
||||
format: "",
|
||||
uri: images[1].url,
|
||||
}
|
||||
}
|
||||
const model = getModel(_model);
|
||||
logger.info(`使用模型: ${_model} : ${model} 参考图片尺寸: ${width}x${height} 图片地址 ${imgURL} 持续时间: ${duration} 提示词: ${prompt}`);
|
||||
|
||||
|
||||
const { totalCredit } = await getCredit(refreshToken);
|
||||
if (totalCredit <= 0)
|
||||
await receiveCredit(refreshToken);
|
||||
@ -120,30 +171,8 @@ export async function generateVideo(
|
||||
text_to_video_params:{
|
||||
type: "",
|
||||
id: util.uuid(),
|
||||
video_gen_inputs:[
|
||||
{
|
||||
type: "",
|
||||
id: util.uuid(),
|
||||
min_version: DRAFT_V_VERSION,
|
||||
prompt: prompt,
|
||||
first_frame_image:{
|
||||
type: "image",
|
||||
id: util.uuid(),
|
||||
source_from: "upload",
|
||||
platform_type: 1,
|
||||
name: "",
|
||||
image_uri: imgURL,
|
||||
width: width,
|
||||
height: height,
|
||||
format: "",
|
||||
uri: imgURL,
|
||||
},
|
||||
video_mode:2,
|
||||
fps:24,
|
||||
duration_ms:duration,
|
||||
}
|
||||
],
|
||||
video_aspect_ratio:"9:16",
|
||||
video_gen_inputs:[video_gen_inputs],
|
||||
video_aspect_ratio:ratio,
|
||||
seed: Math.floor(Math.random() * 100000000) + 2500000000,
|
||||
model_req_key: model,
|
||||
},
|
||||
@ -174,7 +203,7 @@ export async function generateVideo(
|
||||
//
|
||||
let emptyCount = 30;
|
||||
while (status === 20) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
|
||||
data: {
|
||||
history_ids: [historyId],
|
||||
|
||||
@ -4,19 +4,44 @@ import Request from "@/lib/request/Request.ts";
|
||||
import { generateImages } from "@/api/controllers/images.ts";
|
||||
import { tokenSplit } from "@/api/controllers/core.ts";
|
||||
import util from "@/lib/util.ts";
|
||||
import { ImagesTaskCache } from '@/api/ImagesTaskCache.ts';
|
||||
|
||||
export default {
|
||||
prefix: "/v1/images",
|
||||
get: {
|
||||
"/query": async (request: Request) => {
|
||||
const imagesTaskCache = ImagesTaskCache.getInstance();
|
||||
request
|
||||
.validate("query.task_id", _.isString) // 从 query 中校验
|
||||
const {
|
||||
task_id,
|
||||
} = request.query; // 从 query 中获取
|
||||
let res = imagesTaskCache.getTaskStatus(task_id);
|
||||
// console.log("查询任务状态", task_id, 'res:',res);
|
||||
if(typeof res === 'string'){
|
||||
return {
|
||||
created: util.unixTimestamp(),
|
||||
data:{task_id, url:res, status:-1},
|
||||
};
|
||||
}else{
|
||||
return {
|
||||
created: util.unixTimestamp(),
|
||||
data:{task_id, url:"", status:res||0},
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
post: {
|
||||
"/generations": async (request: Request) => {
|
||||
request
|
||||
.validate("body.model", v => _.isUndefined(v) || _.isString(v))
|
||||
// .validate("body.model", v => _.isUndefined(v) || _.isString(v))
|
||||
.validate("body.task_id", _.isString)
|
||||
.validate("body.prompt", _.isString)
|
||||
.validate("body.negative_prompt", v => _.isUndefined(v) || _.isString(v))
|
||||
// .validate("body.negative_prompt", v => _.isUndefined(v) || _.isString(v))
|
||||
.validate("body.width", v => _.isUndefined(v) || _.isFinite(v))
|
||||
.validate("body.height", v => _.isUndefined(v) || _.isFinite(v))
|
||||
.validate("body.sample_strength", v => _.isUndefined(v) || _.isFinite(v))
|
||||
// .validate("body.sample_strength", v => _.isUndefined(v) || _.isFinite(v))
|
||||
.validate("body.response_format", v => _.isUndefined(v) || _.isString(v))
|
||||
.validate("headers.authorization", _.isString);
|
||||
// refresh_token切分
|
||||
@ -24,20 +49,21 @@ export default {
|
||||
// 随机挑选一个refresh_token
|
||||
const token = _.sample(tokens);
|
||||
const {
|
||||
model,
|
||||
// model,
|
||||
task_id,
|
||||
prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
// negative_prompt: negativePrompt,
|
||||
width,
|
||||
height,
|
||||
sample_strength: sampleStrength,
|
||||
// sample_strength: sampleStrength,
|
||||
response_format,
|
||||
} = request.body;
|
||||
const responseFormat = _.defaultTo(response_format, "url");
|
||||
const imageUrls = await generateImages(model, prompt, {
|
||||
const imageUrls = await generateImages('jimeng-3.1', task_id, prompt, {
|
||||
width,
|
||||
height,
|
||||
sampleStrength,
|
||||
negativePrompt,
|
||||
sampleStrength:0.5,
|
||||
negativePrompt:"",
|
||||
}, token);
|
||||
let data = [];
|
||||
if (responseFormat == "b64_json") {
|
||||
|
||||
@ -35,13 +35,12 @@ export default {
|
||||
post: {
|
||||
"/generations": async (request: Request) => {
|
||||
request
|
||||
.validate("body.model", v => _.isUndefined(v) || _.isString(v))
|
||||
.validate("body.task_id", _.isString)
|
||||
.validate("body.prompt", _.isString)
|
||||
.validate("body.image", v => _.isUndefined(v) || _.isString(v))
|
||||
.validate("body.width", v => _.isUndefined(v) || _.isFinite(v))
|
||||
.validate("body.height", v => _.isUndefined(v) || _.isFinite(v))
|
||||
.validate("body.images", v => _.isUndefined(v) || _.isArray(v))
|
||||
.validate("body.is_pro", v => _.isUndefined(v) || _.isBoolean(v))
|
||||
.validate("body.duration", v => _.isUndefined(v) || _.isFinite(v))
|
||||
.validate("body.ratio", v => _.isUndefined(v) || _.isString(v))
|
||||
.validate("headers.authorization", _.isString);
|
||||
// refresh_token切分
|
||||
const tokens = tokenSplit(request.headers.authorization);
|
||||
@ -49,20 +48,19 @@ export default {
|
||||
const token = _.sample(tokens);
|
||||
const {
|
||||
task_id,
|
||||
model,
|
||||
prompt,
|
||||
width,
|
||||
height,
|
||||
image,
|
||||
images,
|
||||
is_pro,
|
||||
duration,
|
||||
ratio,
|
||||
} = request.body;
|
||||
// const imageUrls = await generateVideo(model, task_id, prompt, {
|
||||
//不等结果 直接返回
|
||||
generateVideo(model, task_id, prompt, {
|
||||
width,
|
||||
height,
|
||||
imgURL:image,
|
||||
generateVideo(task_id, prompt, {
|
||||
images:images,
|
||||
isPro:is_pro,
|
||||
duration:duration*1000,
|
||||
ratio,
|
||||
}, token);
|
||||
// let data = [];
|
||||
// data = imageUrls.map((url) => ({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user