fix: harden scale adjust patch workflow

This commit is contained in:
134 2026-07-04 15:26:31 +08:00
parent f483fab33a
commit fec71038c5
4 changed files with 47 additions and 14 deletions

View File

@ -3710,6 +3710,9 @@ function buildScaleAdjustPatchPrompt(
"Clean local image edit for a cropped workspace.",
`Return one clean realistic crop with the same framing and aspect as input image 1. This crop maps to full-photo pixels left=${patchReference.workspacePixelRect.left}, top=${patchReference.workspacePixelRect.top}, width=${patchReference.workspacePixelRect.width}, height=${patchReference.workspacePixelRect.height}.`,
"Input image 1 is the rough target draft. The selected subject is already placed at the requested final size and position; keep that final footprint.",
structureLockedDraft
? "Structure lock: treat the selected subject pixels in input image 1 as the locked product/person layer. AI work is limited to background completion, boundary blending, occlusion, lighting, contact shadows, and removing rough draft artifacts. Do not repaint the product/person interior."
: "",
includeSourceReference
? "Input image 2 is the original clean crop for background texture, lighting, perspective, and non-target objects only. Do not restore the selected subject to its old size."
: "",
@ -3732,6 +3735,9 @@ function buildScaleAdjustPatchPrompt(
firstInputRole,
secondInputRole,
cropLines,
structureLockedDraft
? "Structure lock: treat the selected subject pixels in input image 1 as the locked product/person layer. AI work is limited to background completion, boundary blending, occlusion, lighting, contact shadows, and removing rough draft artifacts. Do not repaint or redesign the target subject interior."
: "",
"Only edit the selected target subject inside the search area. The target subject can be a person/model/child/body part, furniture, product, or another movable object. Keep the crop borders, wall, window frame, floor, rug, sofa, lamps, decor, lighting, camera perspective, and all non-target subjects stable.",
"If the selected target is a person, scale the whole visible person/body coherently to the final footprint while preserving identity, face, pose, anatomy, hand shape, hair, clothing, and natural human proportions. If a person appears outside the selected target, preserve that non-target person without resizing or redrawing them.",
"When the final footprint is smaller, the selected subject must clearly shrink; erase the old larger subject footprint outside the final footprint and replace it with natural background/floor/rug/wall/shadow that matches the original crop.",
@ -4170,9 +4176,7 @@ export async function POST(request: NextRequest) {
: [];
const scaleAdjustProtectedRegions =
isScaleAdjustOperation && scaleAdjustPatchReference
? scaleAdjustPatchReference.structureLockedDraft
? [scaleAdjustPatchReference.workspaceRect]
: scaleAdjustProtectedChangeRegions
? scaleAdjustProtectedChangeRegions
: scaleAdjustCompositeRegions;
const scaleAdjustVerificationConfig = isScaleAdjustOperation
? {
@ -4386,12 +4390,8 @@ export async function POST(request: NextRequest) {
featherMaxPx: 42,
featherMinPx: 10,
featherRatio: 0.018,
maskMode: scaleAdjustPatchReference.structureLockedDraft
? "workspace"
: "diff",
regions: scaleAdjustPatchReference.structureLockedDraft
? undefined
: scaleAdjustPatchCompositeRegions,
maskMode: "diff",
regions: scaleAdjustPatchCompositeRegions,
sourceImageUrl,
workspaceRegion: scaleAdjustPatchReference.workspaceRect,
}

View File

@ -764,7 +764,7 @@ type DirectDetailReviewResult = {
};
const AGENT_IMAGE_TASK_POLL_INTERVAL_MS = 1600;
const AGENT_IMAGE_TASK_TIMEOUT_MS = 10 * 60 * 1000;
const AGENT_IMAGE_TASK_TIMEOUT_MS = 15 * 60 * 1000;
const AGENT_DETAIL_IMAGE_PARALLEL_WORKER_COUNT = 4;
function sleep(ms: number) {

View File

@ -796,7 +796,7 @@ const CANVAS_AGENT_SECTION_TITLES = new Set([
"输出设置",
]);
const CANVAS_IMAGE_TASK_POLL_INTERVAL_MS = 1600;
const CANVAS_IMAGE_TASK_TIMEOUT_MS = 10 * 60 * 1000;
const CANVAS_IMAGE_TASK_TIMEOUT_MS = 15 * 60 * 1000;
const CANVAS_GENERATION_NODE_GAP = 56;
const CANVAS_GENERATION_STACK_GAP = 28;
const CANVAS_GENERATION_COLLISION_GAP = 32;

View File

@ -58,7 +58,9 @@ interface QueueState {
draining: boolean;
}
const STALE_RUNNING_MS = 5 * 60 * 1000;
const DEFAULT_RUNNING_TIMEOUT_MS = 10 * 60 * 1000;
const SCALE_ADJUST_RUNNING_TIMEOUT_MS = 15 * 60 * 1000;
const LOCAL_ACTIVE_RUNNING_HARD_TIMEOUT_MS = 30 * 60 * 1000;
const PENDING_LOOKAHEAD_LIMIT = 100;
const QUEUE_STATUS_PENDING_LOOKAHEAD_LIMIT = 500;
const QUEUE_STATUS_RECENT_SAMPLE_LIMIT = 120;
@ -98,6 +100,29 @@ function normalizeTaskModelKey(request: unknown) {
return normalizeCanvasImageModelKey(rawModel);
}
function getRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function getCanvasImageRunningTimeoutMs(request: unknown) {
const record = getRecord(request);
const debugMeta = getRecord(record?.debugMeta);
const debugLabel = String(record?.debugLabel || "");
const operation = String(debugMeta?.operation || "");
const isScaleAdjust =
debugLabel === "canvas-scale-adjust" ||
operation === "scale-adjust" ||
Boolean(debugMeta?.scaleAdjustPatchWorkflowApplied) ||
Boolean(debugMeta?.scaleAdjustPatchForcedForShrink) ||
Boolean(record?.scaleAdjustPatchComposite);
return isScaleAdjust
? SCALE_ADJUST_RUNNING_TIMEOUT_MS
: DEFAULT_RUNNING_TIMEOUT_MS;
}
function isStyleProTaskRequest(request: unknown): request is StyleProTaskRequest {
return Boolean(
request &&
@ -599,8 +624,16 @@ export async function getCanvasImageTaskForUser(params: {
if (!task) return null;
if (task.status === "running" && task.startedAt) {
const staleBefore = new Date(Date.now() - STALE_RUNNING_MS);
if (task.startedAt < staleBefore) {
const now = Date.now();
const runningAgeMs = now - task.startedAt.getTime();
const runningTimeoutMs = getCanvasImageRunningTimeoutMs(task.request);
const locallyRunning = getQueueState().running.has(task.id);
const shouldMarkStale =
runningAgeMs >= runningTimeoutMs &&
(!locallyRunning || runningAgeMs >= LOCAL_ACTIVE_RUNNING_HARD_TIMEOUT_MS);
if (shouldMarkStale) {
const staleBefore = new Date(now - runningTimeoutMs);
const timeoutMessage = "图片生成任务已超时,请重新发起生成";
const completedAt = new Date();
const [timedOutTask] = await db