Your Name 5d8146ca11
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: surface image queue status across generators
2026-05-18 23:48:49 +08:00

255 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
import {
generateCanvasImage,
type CanvasGenerateImageResult,
} from "@/lib/canvas/generate-image";
import type {
StyleProGenerationMode,
StyleProSelectionMap,
} from "./config";
export type StyleProTaskSelectedOption = {
categoryId: string;
categoryTitle: string;
jsonKey: string;
optionId: string;
optionLabel: string;
prompt: string;
};
export type StyleProTaskModelRef = {
id: string;
displayName: string;
};
export type StyleProTaskBilling = {
unitCost: number;
reason: string;
modelId: string;
messageId?: string | null;
note: string;
};
export type StyleProTaskRequest = {
taskKind: "style-pro";
canvasId?: string | null;
sourceImageUrl: string;
generationMode: StyleProGenerationMode;
normalizedSelections: StyleProSelectionMap;
selectedOptions: StyleProTaskSelectedOption[];
selectedModels: StyleProTaskModelRef[];
optionLabels: string[];
prompts: {
defaultPrompt: string;
realisticFirstPrompt?: string;
realisticSecondPrompt?: string;
negativePrompt?: string;
aspectRatio?: string;
imageSize?: string;
};
billing: StyleProTaskBilling;
requestedStyleProModelId?: string | null;
};
export type StyleProTaskResult = {
imageUrl: string;
thumbnailUrl?: string;
model?: string;
modelRef?: string;
generationMode: StyleProGenerationMode;
chargedPoints: number;
selections: StyleProSelectionMap;
optionLabels: string[];
};
function composeStyleProPrompt({
defaultPrompt,
selectedOptions,
mode = "style-pro-final",
}: {
defaultPrompt: string;
selectedOptions: StyleProTaskSelectedOption[];
mode?: string;
}) {
const groupedPrompt = selectedOptions.reduce<Record<string, string[]>>(
(acc, item) => {
const key = item.jsonKey || item.categoryId;
acc[key] = acc[key] || [];
acc[key].push(item.prompt);
return acc;
},
{}
);
return [
defaultPrompt,
"请严格依据以下风格化 PRO 选择执行最终图片生成。用户不可见配置 JSON:",
JSON.stringify(
{
mode,
selections: selectedOptions,
promptByKey: groupedPrompt,
},
null,
2
),
]
.filter(Boolean)
.join("\n\n");
}
async function consumeStyleProTaskPoints(
userId: string,
billing: StyleProTaskBilling
) {
if (!Number.isFinite(billing.unitCost) || billing.unitCost <= 0) {
return 0;
}
try {
const pointsCharge = await prepareFeaturePointsCharge({
userId,
unitCost: billing.unitCost,
reason: billing.reason,
modelId: billing.modelId,
messageId: billing.messageId || null,
note: billing.note,
});
if (!pointsCharge.ok || !pointsCharge.chargeable) {
return 0;
}
const consumeResult = await pointsCharge.consume();
if (consumeResult?.ok && !consumeResult.skipped) {
return billing.unitCost;
}
} catch (error) {
console.error("[Style Pro Task] Failed to consume points:", error);
}
return 0;
}
function getPrimaryModelId(request: StyleProTaskRequest) {
return String(request.selectedModels[0]?.id || "").trim();
}
export function getStyleProTaskPrimaryModelKey(request: StyleProTaskRequest) {
return getPrimaryModelId(request) || "__default__";
}
function normalizeCanvasImageResultImage(
result: CanvasGenerateImageResult,
fallbackError: string
) {
const image = result.images[0];
const imageUrl = String(image?.originalUrl || image?.thumbnailUrl || "").trim();
if (!imageUrl) {
throw new Error(fallbackError);
}
return {
imageUrl,
thumbnailUrl: String(image?.thumbnailUrl || image?.originalUrl || "").trim() || imageUrl,
model: result.model,
modelRef: result.modelRef,
};
}
export async function executeStyleProTask(params: {
request: StyleProTaskRequest;
userId: string;
}) {
const { request, userId } = params;
const aspectRatio = request.prompts.aspectRatio || "auto";
const imageSize = request.prompts.imageSize || "2K";
const negativePrompt = request.prompts.negativePrompt || undefined;
const result =
request.generationMode === "realistic"
? await (async () => {
const firstModelId = getPrimaryModelId(request);
const secondModelId = String(request.selectedModels[1]?.id || "").trim();
if (!firstModelId || !secondModelId) {
throw new Error("请先在后台配置实景化两段隐藏模型");
}
const firstResult = await generateCanvasImage({
prompt: request.prompts.realisticFirstPrompt || "",
negativePrompt,
model: firstModelId,
aspectRatio,
imageSize,
imageCount: 1,
referenceImage: request.sourceImageUrl,
ownerKey: userId,
debugLabel: "style-pro-realistic-stage1",
debugMeta: {
canvasId: request.canvasId || null,
selectedOptionCount: request.selectedOptions.length,
},
});
const firstPassImage = normalizeCanvasImageResultImage(
firstResult,
"实景化第一阶段生成失败"
);
return generateCanvasImage({
prompt: composeStyleProPrompt({
defaultPrompt: request.prompts.realisticSecondPrompt || "",
selectedOptions: request.selectedOptions,
mode: "style-pro-realistic-final",
}),
negativePrompt,
model: secondModelId,
aspectRatio,
imageSize,
imageCount: 1,
referenceImage: firstPassImage.imageUrl,
ownerKey: userId,
debugLabel: "style-pro-realistic-stage2",
debugMeta: {
canvasId: request.canvasId || null,
selectedOptionCount: request.selectedOptions.length,
firstStageModelRef: firstModelId,
},
});
})()
: await generateCanvasImage({
prompt: composeStyleProPrompt({
defaultPrompt: request.prompts.defaultPrompt,
selectedOptions: request.selectedOptions,
}),
negativePrompt,
model: getPrimaryModelId(request),
aspectRatio,
imageSize,
imageCount: 1,
referenceImage: request.sourceImageUrl,
ownerKey: userId,
debugLabel: "style-pro",
debugMeta: {
canvasId: request.canvasId || null,
requestedStyleProModelId: request.requestedStyleProModelId || null,
selectedOptionCount: request.selectedOptions.length,
},
});
const finalImage = normalizeCanvasImageResultImage(result, "生成结果为空");
const chargedPoints = await consumeStyleProTaskPoints(userId, request.billing);
return {
imageUrl: finalImage.imageUrl,
thumbnailUrl: finalImage.thumbnailUrl,
model: finalImage.model,
modelRef: finalImage.modelRef,
generationMode: request.generationMode,
chargedPoints,
selections: request.normalizedSelections,
optionLabels: request.optionLabels,
} satisfies StyleProTaskResult;
}