图片生成、视频生成

This commit is contained in:
jonathang4 2025-08-05 19:14:14 +08:00
parent 1da0ef5c18
commit 6e8e89b624
6 changed files with 235 additions and 68 deletions

View File

@ -1,14 +1,14 @@
version: '3.8' version: '3.8'
services: services:
jimeng-free-api: # jimeng-free-api:
build: # build:
context: ./ # context: ./
dockerfile: Dockerfile # dockerfile: Dockerfile
image: jimeng-free-api:latest # image: jimeng-free-api:latest
container_name: jimeng-free-api # container_name: jimeng-free-api
ports: # ports:
- "8443:3302" # - "8443:3302"
jimeng-free-api-pro: jimeng-free-api-pro:
build: build:
context: ./ context: ./

View 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();

View File

@ -5,11 +5,13 @@ import EX from "@/api/consts/exceptions.ts";
import util from "@/lib/util.ts"; import util from "@/lib/util.ts";
import { getCredit, receiveCredit, request } from "./core.ts"; import { getCredit, receiveCredit, request } from "./core.ts";
import logger from "@/lib/logger.ts"; import logger from "@/lib/logger.ts";
import { ImagesTaskCache } from '@/api/ImagesTaskCache.ts';
const DEFAULT_ASSISTANT_ID = "513695"; const DEFAULT_ASSISTANT_ID = "513695";
export const DEFAULT_MODEL = "jimeng-3.0"; export const DEFAULT_MODEL = "jimeng-3.0";
const DRAFT_VERSION = "3.0.2"; const DRAFT_VERSION = "3.0.2";
const MODEL_MAP = { 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-3.0": "high_aes_general_v30l:general_v3.0_18b",
"jimeng-2.1": "high_aes_general_v21_L:general_v2.1_L", "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", "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( export async function generateImages(
_model: string, _model: string,
task_id: string,
prompt: string, prompt: string,
{ {
width = 1024, width = 1024,
@ -38,6 +41,10 @@ export async function generateImages(
}, },
refreshToken: string refreshToken: string
) { ) {
const imagesTaskCache = ImagesTaskCache.getInstance();
imagesTaskCache.startTask(task_id);
try {
const model = getModel(_model); const model = getModel(_model);
logger.info(`使用模型: ${_model} 映射模型: ${model} ${width}x${height} 精细度: ${sampleStrength}`); logger.info(`使用模型: ${_model} 映射模型: ${model} ${width}x${height} 精细度: ${sampleStrength}`);
@ -109,6 +116,7 @@ export async function generateImages(
id: util.uuid(), id: util.uuid(),
height, height,
width, width,
resolution_type: "1k",
}, },
}, },
history_option: { history_option: {
@ -131,7 +139,7 @@ export async function generateImages(
throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在"); throw new APIException(EX.API_IMAGE_GENERATION_FAILED, "记录ID不存在");
let status = 20, failCode, item_list = []; let status = 20, failCode, item_list = [];
while (status === 20) { 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, { const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
data: { data: {
history_ids: [historyId], history_ids: [historyId],
@ -243,11 +251,25 @@ export async function generateImages(
else else
throw new APIException(EX.API_IMAGE_GENERATION_FAILED); 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) if(!item?.image?.large_images?.[0]?.image_url)
return item?.common_attr?.cover_url || null; return item?.common_attr?.cover_url || null;
return item.image.large_images[0].image_url; 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( export async function uploadImages(

View File

@ -13,6 +13,7 @@ const DRAFT_VERSION = "3.0.5";
const DRAFT_V_VERSION = "1.0.0"; const DRAFT_V_VERSION = "1.0.0";
const MODEL_MAP = { const MODEL_MAP = {
"jimeng-v-3.0": "dreamina_ic_generate_video_model_vgfm_3.0", "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) { export function getModel(model: string) {
@ -20,19 +21,18 @@ export function getModel(model: string) {
} }
//图片生成视频 首帧 //图片生成视频 首帧
export async function generateVideo( export async function generateVideo(
_model: string,
task_id: string, task_id: string,
prompt: string, prompt: string,
{ {
width = 512, images = [],
height = 512, isPro = false,
imgURL = "",
duration = 5000, duration = 5000,
ratio = '9:16',
}: { }: {
width: number; images: any[];
height: number; isPro: boolean;
imgURL: string;
duration: number; duration: number;
ratio: string;
}, },
refreshToken: string refreshToken: string
) { ) {
@ -40,12 +40,63 @@ export async function generateVideo(
videoTaskCache.startTask(task_id); videoTaskCache.startTask(task_id);
try { try {
if(!imgURL){ let _model = DEFAULT_MODEL;
throw new APIException(EX.API_REQUEST_PARAMS_INVALID); if(isPro){
return; _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); const model = getModel(_model);
logger.info(`使用模型: ${_model} ${model} 参考图片尺寸: ${width}x${height} 图片地址 ${imgURL} 持续时间: ${duration} 提示词: ${prompt}`);
const { totalCredit } = await getCredit(refreshToken); const { totalCredit } = await getCredit(refreshToken);
if (totalCredit <= 0) if (totalCredit <= 0)
@ -120,30 +171,8 @@ export async function generateVideo(
text_to_video_params:{ text_to_video_params:{
type: "", type: "",
id: util.uuid(), id: util.uuid(),
video_gen_inputs:[ video_gen_inputs:[video_gen_inputs],
{ video_aspect_ratio:ratio,
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",
seed: Math.floor(Math.random() * 100000000) + 2500000000, seed: Math.floor(Math.random() * 100000000) + 2500000000,
model_req_key: model, model_req_key: model,
}, },
@ -174,7 +203,7 @@ export async function generateVideo(
// //
let emptyCount = 30; let emptyCount = 30;
while (status === 20) { 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, { const result = await request("post", "/mweb/v1/get_history_by_ids", refreshToken, {
data: { data: {
history_ids: [historyId], history_ids: [historyId],

View File

@ -4,19 +4,44 @@ import Request from "@/lib/request/Request.ts";
import { generateImages } from "@/api/controllers/images.ts"; import { generateImages } from "@/api/controllers/images.ts";
import { tokenSplit } from "@/api/controllers/core.ts"; import { tokenSplit } from "@/api/controllers/core.ts";
import util from "@/lib/util.ts"; import util from "@/lib/util.ts";
import { ImagesTaskCache } from '@/api/ImagesTaskCache.ts';
export default { export default {
prefix: "/v1/images", 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: { post: {
"/generations": async (request: Request) => { "/generations": async (request: 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.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.width", v => _.isUndefined(v) || _.isFinite(v))
.validate("body.height", 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("body.response_format", v => _.isUndefined(v) || _.isString(v))
.validate("headers.authorization", _.isString); .validate("headers.authorization", _.isString);
// refresh_token切分 // refresh_token切分
@ -24,20 +49,21 @@ export default {
// 随机挑选一个refresh_token // 随机挑选一个refresh_token
const token = _.sample(tokens); const token = _.sample(tokens);
const { const {
model, // model,
task_id,
prompt, prompt,
negative_prompt: negativePrompt, // negative_prompt: negativePrompt,
width, width,
height, height,
sample_strength: sampleStrength, // sample_strength: sampleStrength,
response_format, response_format,
} = request.body; } = request.body;
const responseFormat = _.defaultTo(response_format, "url"); const responseFormat = _.defaultTo(response_format, "url");
const imageUrls = await generateImages(model, prompt, { const imageUrls = await generateImages('jimeng-3.1', task_id, prompt, {
width, width,
height, height,
sampleStrength, sampleStrength:0.5,
negativePrompt, negativePrompt:"",
}, token); }, token);
let data = []; let data = [];
if (responseFormat == "b64_json") { if (responseFormat == "b64_json") {

View File

@ -35,13 +35,12 @@ export default {
post: { post: {
"/generations": async (request: Request) => { "/generations": async (request: Request) => {
request request
.validate("body.model", v => _.isUndefined(v) || _.isString(v))
.validate("body.task_id", _.isString) .validate("body.task_id", _.isString)
.validate("body.prompt", _.isString) .validate("body.prompt", _.isString)
.validate("body.image", v => _.isUndefined(v) || _.isString(v)) .validate("body.images", v => _.isUndefined(v) || _.isArray(v))
.validate("body.width", v => _.isUndefined(v) || _.isFinite(v)) .validate("body.is_pro", v => _.isUndefined(v) || _.isBoolean(v))
.validate("body.height", v => _.isUndefined(v) || _.isFinite(v))
.validate("body.duration", v => _.isUndefined(v) || _.isFinite(v)) .validate("body.duration", v => _.isUndefined(v) || _.isFinite(v))
.validate("body.ratio", v => _.isUndefined(v) || _.isString(v))
.validate("headers.authorization", _.isString); .validate("headers.authorization", _.isString);
// refresh_token切分 // refresh_token切分
const tokens = tokenSplit(request.headers.authorization); const tokens = tokenSplit(request.headers.authorization);
@ -49,20 +48,19 @@ export default {
const token = _.sample(tokens); const token = _.sample(tokens);
const { const {
task_id, task_id,
model,
prompt, prompt,
width, images,
height, is_pro,
image,
duration, duration,
ratio,
} = request.body; } = request.body;
// const imageUrls = await generateVideo(model, task_id, prompt, { // const imageUrls = await generateVideo(model, task_id, prompt, {
//不等结果 直接返回 //不等结果 直接返回
generateVideo(model, task_id, prompt, { generateVideo(task_id, prompt, {
width, images:images,
height, isPro:is_pro,
imgURL:image,
duration:duration*1000, duration:duration*1000,
ratio,
}, token); }, token);
// let data = []; // let data = [];
// data = imageUrls.map((url) => ({ // data = imageUrls.map((url) => ({