chore: expand canvas log hygiene coverage
This commit is contained in:
parent
91a98083a1
commit
fadcbbc206
@ -1,25 +1,27 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { callQwen3Vl } from "@/lib/ai/qwen3-vl";
|
import { callQwen3Vl } from "@/lib/ai/qwen3-vl";
|
||||||
|
import { createLogger } from "@/lib/observability/logger";
|
||||||
|
|
||||||
|
const logger = createLogger("canvas-auto-title");
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
console.log("[Auto-title] API called");
|
logger.debug("request_received");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
console.log("[Auto-title] Unauthorized");
|
logger.warn("unauthorized");
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { imageUrl } = await request.json();
|
const { imageUrl } = await request.json();
|
||||||
console.log("[Auto-title] Image URL:", imageUrl);
|
|
||||||
|
|
||||||
if (!imageUrl) {
|
if (!imageUrl) {
|
||||||
return NextResponse.json({ error: "Image URL is required" }, { status: 400 });
|
return NextResponse.json({ error: "Image URL is required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Auto-title] 使用硬编码模型: qwen3-vl");
|
logger.debug("model_selected", { model: "qwen3-vl" });
|
||||||
|
|
||||||
// 构造 Prompt
|
// 构造 Prompt
|
||||||
const prompt = "请用2-4个字概括这张图片的主体内容,作为文件标题。直接返回标题,不要包含任何标点符号或其他文字。";
|
const prompt = "请用2-4个字概括这张图片的主体内容,作为文件标题。直接返回标题,不要包含任何标点符号或其他文字。";
|
||||||
@ -35,11 +37,11 @@ export async function POST(request: NextRequest) {
|
|||||||
title = title.replace(/[\s\p{P}\p{S}]+/gu, "");
|
title = title.replace(/[\s\p{P}\p{S}]+/gu, "");
|
||||||
if (title.length > 4) title = title.slice(0, 4);
|
if (title.length > 4) title = title.slice(0, 4);
|
||||||
if (title.length < 2) title = "新画布";
|
if (title.length < 2) title = "新画布";
|
||||||
console.log("[Auto-title] Generated title:", title, "model:", model);
|
logger.info("title_generated", { model, titleLength: title.length });
|
||||||
return NextResponse.json({ title });
|
return NextResponse.json({ title });
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Auto-title] Error:", error);
|
logger.error("auto_title_failed", error);
|
||||||
// 失败时返回默认标题,不报错中断流程
|
// 失败时返回默认标题,不报错中断流程
|
||||||
return NextResponse.json({ title: "新画布" });
|
return NextResponse.json({ title: "新画布" });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
type DetailKimiGenerateOptions,
|
type DetailKimiGenerateOptions,
|
||||||
} from "@/lib/ai/detail-agent-kimi-profile";
|
} from "@/lib/ai/detail-agent-kimi-profile";
|
||||||
import { inlineVisionImages } from "@/lib/ai/vision-inline";
|
import { inlineVisionImages } from "@/lib/ai/vision-inline";
|
||||||
|
import { createLogger } from "@/lib/observability/logger";
|
||||||
import { filterReachableRemoteUrls } from "@/lib/net/url-access";
|
import { filterReachableRemoteUrls } from "@/lib/net/url-access";
|
||||||
import {
|
import {
|
||||||
buildDetailUiProtocol,
|
buildDetailUiProtocol,
|
||||||
@ -47,6 +48,7 @@ const DETAIL_PLAN_INLINE_TIMEOUT_MS = 4000;
|
|||||||
const DETAIL_PLAN_INLINE_RETRIES = 1;
|
const DETAIL_PLAN_INLINE_RETRIES = 1;
|
||||||
const DETAIL_PLAN_MAIN_TIMEOUT_MS = 45000;
|
const DETAIL_PLAN_MAIN_TIMEOUT_MS = 45000;
|
||||||
const DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS = 10000;
|
const DETAIL_PLAN_PREFLIGHT_TIMEOUT_MS = 10000;
|
||||||
|
const logger = createLogger("canvas-detail-plan");
|
||||||
|
|
||||||
function isAbortError(error: unknown): boolean {
|
function isAbortError(error: unknown): boolean {
|
||||||
const target = error as {
|
const target = error as {
|
||||||
@ -1774,7 +1776,7 @@ async function runDualDirectorPlanPreflight(params: {
|
|||||||
candidateScreens,
|
candidateScreens,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Canvas Detail Plan] preflight error:", error);
|
logger.warn("preflight_failed", error);
|
||||||
return defaultResult;
|
return defaultResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1808,19 +1810,17 @@ export async function POST(request: Request) {
|
|||||||
const bundleImageUrls = bundleImageCheck.kept.slice(0, 3);
|
const bundleImageUrls = bundleImageCheck.kept.slice(0, 3);
|
||||||
const modelImageUrl = modelImageCheck.kept[0] || "";
|
const modelImageUrl = modelImageCheck.kept[0] || "";
|
||||||
if (referenceImageCheck.dropped.length > 0) {
|
if (referenceImageCheck.dropped.length > 0) {
|
||||||
console.warn("[Canvas Detail Plan] Dropping unreachable referenceImages:", {
|
logger.warn("reference_images_dropped", {
|
||||||
count: referenceImageCheck.dropped.length,
|
count: referenceImageCheck.dropped.length,
|
||||||
urls: referenceImageCheck.dropped.slice(0, 6),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (bundleImageCheck.dropped.length > 0) {
|
if (bundleImageCheck.dropped.length > 0) {
|
||||||
console.warn("[Canvas Detail Plan] Dropping unreachable bundleImageUrls:", {
|
logger.warn("bundle_images_dropped", {
|
||||||
count: bundleImageCheck.dropped.length,
|
count: bundleImageCheck.dropped.length,
|
||||||
urls: bundleImageCheck.dropped.slice(0, 3),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (modelImageUrlRaw && !modelImageUrl) {
|
if (modelImageUrlRaw && !modelImageUrl) {
|
||||||
console.warn("[Canvas Detail Plan] modelImageUrl unreachable, ignore this run:", modelImageUrlRaw);
|
logger.warn("model_image_dropped");
|
||||||
}
|
}
|
||||||
const effectiveReferenceImageNotes =
|
const effectiveReferenceImageNotes =
|
||||||
referenceImages.length === referenceImagesRaw.length ? referenceImageNotes : [];
|
referenceImages.length === referenceImagesRaw.length ? referenceImageNotes : [];
|
||||||
@ -1935,7 +1935,7 @@ export async function POST(request: Request) {
|
|||||||
isLocalMoonshot: Boolean(localDetailAgentResolved),
|
isLocalMoonshot: Boolean(localDetailAgentResolved),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[Canvas Detail Plan] Planner model selected:", {
|
logger.info("planner_model_selected", {
|
||||||
id: selectedModelRef.id,
|
id: selectedModelRef.id,
|
||||||
displayName: selectedModelRef.displayName,
|
displayName: selectedModelRef.displayName,
|
||||||
modelId: selectedModelRef.modelId,
|
modelId: selectedModelRef.modelId,
|
||||||
@ -2030,10 +2030,9 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isReferenceImageDownloadError(error) || isTransientNetworkError(error)) {
|
if (isReferenceImageDownloadError(error) || isTransientNetworkError(error)) {
|
||||||
console.warn(
|
logger.warn("vision_input_preprocessing_failed", error, {
|
||||||
"[Canvas Detail Plan] Vision input preprocessing failed, retry without reference images:",
|
referenceImageCount: referenceImages.length,
|
||||||
error
|
});
|
||||||
);
|
|
||||||
imageInputs = [];
|
imageInputs = [];
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
@ -2125,10 +2124,9 @@ export async function POST(request: Request) {
|
|||||||
if (!shouldRetryWithoutImages) {
|
if (!shouldRetryWithoutImages) {
|
||||||
throw firstError;
|
throw firstError;
|
||||||
}
|
}
|
||||||
console.warn(
|
logger.warn("planner_retry_without_reference_images", firstError, {
|
||||||
"[Canvas Detail Plan] Primary planning with reference images failed, retrying without reference images:",
|
imageInputCount: imageInputs.length,
|
||||||
firstError
|
});
|
||||||
);
|
|
||||||
const retryResult = await runPlanner([]);
|
const retryResult = await runPlanner([]);
|
||||||
text = String(retryResult?.text || "");
|
text = String(retryResult?.text || "");
|
||||||
}
|
}
|
||||||
@ -2141,7 +2139,7 @@ export async function POST(request: Request) {
|
|||||||
isTransientNetworkError(error) ||
|
isTransientNetworkError(error) ||
|
||||||
/operationtimeouterror|timed out|timeout/i.test(message)
|
/operationtimeouterror|timed out|timeout/i.test(message)
|
||||||
) {
|
) {
|
||||||
console.error("[Canvas Detail Plan] Primary planning aborted, fallback applied:", error);
|
logger.error("primary_planning_fallback_applied", error);
|
||||||
const fallbackReason = /invalid scheme/i.test(message)
|
const fallbackReason = /invalid scheme/i.test(message)
|
||||||
? "规划模型图片输入协议不兼容,已回退到稳定分镜模板。"
|
? "规划模型图片输入协议不兼容,已回退到稳定分镜模板。"
|
||||||
: isReferenceImageDownloadError(error)
|
: isReferenceImageDownloadError(error)
|
||||||
@ -2384,7 +2382,7 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
return NextResponse.json({ success: true, data: response });
|
return NextResponse.json({ success: true, data: response });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Canvas Detail Plan] Error:", error);
|
logger.error("detail_plan_failed", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: error instanceof Error ? error.message : "详情规划失败" },
|
{ success: false, error: error instanceof Error ? error.message : "详情规划失败" },
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { callQwen3Vl } from "@/lib/ai/qwen3-vl";
|
import { callQwen3Vl } from "@/lib/ai/qwen3-vl";
|
||||||
|
import { createLogger } from "@/lib/observability/logger";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
@ -13,6 +14,7 @@ const requestSchema = z.object({
|
|||||||
|
|
||||||
const OCR_IMAGE_FETCH_TIMEOUT_MS = 15000;
|
const OCR_IMAGE_FETCH_TIMEOUT_MS = 15000;
|
||||||
const OCR_IMAGE_MAX_SIDE = 1400;
|
const OCR_IMAGE_MAX_SIDE = 1400;
|
||||||
|
const logger = createLogger("canvas-text-detect");
|
||||||
|
|
||||||
interface OcrItem {
|
interface OcrItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -145,7 +147,7 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ success: false, error: "仅支持 URL 图片,不允许 base64 传输" }, { status: 400 });
|
return NextResponse.json({ success: false, error: "仅支持 URL 图片,不允许 base64 传输" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Canvas Text Detect] 使用硬编码模型: qwen3-vl");
|
logger.debug("model_selected", { model: "qwen3-vl" });
|
||||||
const prompt = [
|
const prompt = [
|
||||||
"你是图片文字识别助手。",
|
"你是图片文字识别助手。",
|
||||||
"请识别图片中可见文字并按阅读顺序输出 JSON。",
|
"请识别图片中可见文字并按阅读顺序输出 JSON。",
|
||||||
@ -172,7 +174,9 @@ export async function POST(request: Request) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn("[Canvas Text Detect] URL 拉图超时,切换 data URL 兜底");
|
logger.warn("vision_url_fallback", {
|
||||||
|
reason: error instanceof Error ? error.message : "unknown",
|
||||||
|
});
|
||||||
const dataUrl = await buildVisionDataUrl(imageUrl);
|
const dataUrl = await buildVisionDataUrl(imageUrl);
|
||||||
const fallbackResult = await callQwen3Vl({
|
const fallbackResult = await callQwen3Vl({
|
||||||
imageUrl: dataUrl,
|
imageUrl: dataUrl,
|
||||||
@ -205,7 +209,7 @@ export async function POST(request: Request) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Canvas Text Detect] Error:", error);
|
logger.error("text_detect_failed", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import {
|
|||||||
type SceneFusionImageGenConfig,
|
type SceneFusionImageGenConfig,
|
||||||
} from "@/lib/scene-fusion/output-capabilities";
|
} from "@/lib/scene-fusion/output-capabilities";
|
||||||
import { acquireCanvasImageModelSlot } from "@/lib/canvas/image-concurrency";
|
import { acquireCanvasImageModelSlot } from "@/lib/canvas/image-concurrency";
|
||||||
|
import { createLogger } from "@/lib/observability/logger";
|
||||||
|
|
||||||
// 请求体 Schema
|
// 请求体 Schema
|
||||||
const sceneFusionRequestSchema = z.object({
|
const sceneFusionRequestSchema = z.object({
|
||||||
@ -70,6 +71,7 @@ const SCENE_FUSION_RANDOM_BG_PROMPT_TAIL =
|
|||||||
"硬性要求:背景必须与原图明显不同(颜色、结构、元素至少两项显著变化),禁止保留原背景纹理或直接回传原图;仅保留产品主体并保证主体结构、轮廓比例、部件关系和边缘完整自然,允许为新背景调整朝向、角度和透视。";
|
"硬性要求:背景必须与原图明显不同(颜色、结构、元素至少两项显著变化),禁止保留原背景纹理或直接回传原图;仅保留产品主体并保证主体结构、轮廓比例、部件关系和边缘完整自然,允许为新背景调整朝向、角度和透视。";
|
||||||
let sceneFusionRoundRobinCursor = 0;
|
let sceneFusionRoundRobinCursor = 0;
|
||||||
type SceneFusionModelCooldown = Map<string, number>;
|
type SceneFusionModelCooldown = Map<string, number>;
|
||||||
|
const logger = createLogger("scene-fusion");
|
||||||
|
|
||||||
interface SceneFusionModelCandidate {
|
interface SceneFusionModelCandidate {
|
||||||
model: {
|
model: {
|
||||||
@ -304,8 +306,7 @@ async function getAverageHashBits(
|
|||||||
hashCache.set(imageUrl, hashBits);
|
hashCache.set(imageUrl, hashBits);
|
||||||
return hashBits;
|
return hashBits;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[SceneFusion] Echo hash compute skipped:", {
|
logger.warn("echo_hash_compute_skipped", {
|
||||||
imageUrl: imageUrl.slice(0, 120),
|
|
||||||
error: error instanceof Error ? error.message : "unknown",
|
error: error instanceof Error ? error.message : "unknown",
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
@ -357,9 +358,12 @@ async function detectEchoedProductResult(
|
|||||||
|
|
||||||
const distance = hammingDistanceBits(productHash, generatedHash);
|
const distance = hammingDistanceBits(productHash, generatedHash);
|
||||||
if (distance <= SCENE_FUSION_ECHO_HASH_THRESHOLD) {
|
if (distance <= SCENE_FUSION_ECHO_HASH_THRESHOLD) {
|
||||||
console.warn(
|
logger.warn("echo_guard_hit", {
|
||||||
`[SceneFusion][${params.requestId}] Echo-guard hit: scene#${params.sceneIndex + 1}, model=${params.modelName}, distance=${distance}`
|
requestId: params.requestId,
|
||||||
);
|
sceneIndex: params.sceneIndex + 1,
|
||||||
|
modelName: params.modelName,
|
||||||
|
distance,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
tooSimilar: true,
|
tooSimilar: true,
|
||||||
reason: "hash_too_close",
|
reason: "hash_too_close",
|
||||||
@ -603,7 +607,8 @@ async function reviewSceneFusionResult(params: {
|
|||||||
suspectedEcho !== true &&
|
suspectedEcho !== true &&
|
||||||
score >= 0.5;
|
score >= 0.5;
|
||||||
|
|
||||||
console.log(`[SceneFusion][${params.requestId}] Review result:`, {
|
logger.info("review_result", {
|
||||||
|
requestId: params.requestId,
|
||||||
sceneIndex: params.sceneIndex + 1,
|
sceneIndex: params.sceneIndex + 1,
|
||||||
candidate: params.modelName,
|
candidate: params.modelName,
|
||||||
passed,
|
passed,
|
||||||
@ -621,7 +626,8 @@ async function reviewSceneFusionResult(params: {
|
|||||||
model,
|
model,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`[SceneFusion][${params.requestId}] Review skipped:`, {
|
logger.warn("review_skipped", {
|
||||||
|
requestId: params.requestId,
|
||||||
sceneIndex: params.sceneIndex + 1,
|
sceneIndex: params.sceneIndex + 1,
|
||||||
candidate: params.modelName,
|
candidate: params.modelName,
|
||||||
error: error instanceof Error ? error.message : "unknown",
|
error: error instanceof Error ? error.message : "unknown",
|
||||||
@ -745,10 +751,10 @@ async function enrichRandomBackgroundScenesWithAgent(params: {
|
|||||||
scene.customPrompt = mergeCustomPrompt(scene.customPrompt, `场景建议:${generatedPrompt}`);
|
scene.customPrompt = mergeCustomPrompt(scene.customPrompt, `场景建议:${generatedPrompt}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
logger.warn("random_prompt_enhance_skipped", error, {
|
||||||
`[SceneFusion][${params.requestId}] Random scene prompt enhance skipped: scene=${scene.id}`,
|
requestId: params.requestId,
|
||||||
error
|
sceneId: scene.id,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -809,7 +815,7 @@ async function enrichSceneLabelsWithAgent(params: {
|
|||||||
meta.productModel = productDetected.model;
|
meta.productModel = productDetected.model;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`[SceneFusion][${params.requestId}] Product detect skipped:`, error);
|
logger.warn("product_detect_skipped", error, { requestId: params.requestId });
|
||||||
}
|
}
|
||||||
|
|
||||||
const unresolvedIndexes = clonedScenes
|
const unresolvedIndexes = clonedScenes
|
||||||
@ -858,10 +864,10 @@ async function enrichSceneLabelsWithAgent(params: {
|
|||||||
metaScene.model = targetDetected.model;
|
metaScene.model = targetDetected.model;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
logger.warn("scene_target_detect_skipped", error, {
|
||||||
`[SceneFusion][${params.requestId}] Scene target detect skipped: scene=${scene.id}`,
|
requestId: params.requestId,
|
||||||
error
|
sceneId: scene.id,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -957,7 +963,7 @@ async function getSceneFusionConfig() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SceneFusion] Failed to load config:", error);
|
logger.error("config_load_failed", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1000,9 +1006,10 @@ async function preprocessProductImageWithRmbg(
|
|||||||
const candidateUrls = buildPreRmbgCandidateUrls(request);
|
const candidateUrls = buildPreRmbgCandidateUrls(request);
|
||||||
let lastReason = "no_candidate_url";
|
let lastReason = "no_candidate_url";
|
||||||
|
|
||||||
console.log(
|
logger.info("pre_rmbg_started", {
|
||||||
`[SceneFusion][${requestId}] Pre-RMBG start: ${productImageUrl.substring(0, 120)}..., candidates=${candidateUrls.join(" | ")}`
|
requestId,
|
||||||
);
|
candidateCount: candidateUrls.length,
|
||||||
|
});
|
||||||
|
|
||||||
for (const rmbgUrl of candidateUrls) {
|
for (const rmbgUrl of candidateUrls) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@ -1026,7 +1033,8 @@ async function preprocessProductImageWithRmbg(
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
lastReason = `request_failed_${response.status}`;
|
lastReason = `request_failed_${response.status}`;
|
||||||
console.warn(`[SceneFusion][${requestId}] Pre-RMBG candidate failed: ${rmbgUrl}`, {
|
logger.warn("pre_rmbg_candidate_failed", {
|
||||||
|
requestId,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
detail: payload?.detail || payload?.error || "unknown",
|
detail: payload?.detail || payload?.error || "unknown",
|
||||||
});
|
});
|
||||||
@ -1036,7 +1044,8 @@ async function preprocessProductImageWithRmbg(
|
|||||||
const cleanedUrl = String(payload?.original_url || payload?.thumbnail_url || "").trim();
|
const cleanedUrl = String(payload?.original_url || payload?.thumbnail_url || "").trim();
|
||||||
if (!cleanedUrl || cleanedUrl === productImageUrl) {
|
if (!cleanedUrl || cleanedUrl === productImageUrl) {
|
||||||
lastReason = cleanedUrl ? "same_as_source" : "empty_result";
|
lastReason = cleanedUrl ? "same_as_source" : "empty_result";
|
||||||
console.warn(`[SceneFusion][${requestId}] Pre-RMBG candidate invalid result: ${rmbgUrl}`, {
|
logger.warn("pre_rmbg_candidate_invalid", {
|
||||||
|
requestId,
|
||||||
reason: lastReason,
|
reason: lastReason,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@ -1049,7 +1058,7 @@ async function preprocessProductImageWithRmbg(
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastReason = error instanceof Error ? error.message : "exception";
|
lastReason = error instanceof Error ? error.message : "exception";
|
||||||
console.warn(`[SceneFusion][${requestId}] Pre-RMBG candidate exception: ${rmbgUrl}`, error);
|
logger.warn("pre_rmbg_candidate_exception", error, { requestId });
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
@ -1073,7 +1082,7 @@ async function createThumbnail(buffer: Buffer): Promise<Buffer> {
|
|||||||
.toBuffer();
|
.toBuffer();
|
||||||
return thumbnailBuffer;
|
return thumbnailBuffer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SceneFusion] Failed to create thumbnail:", error);
|
logger.error("thumbnail_create_failed", error);
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1098,10 +1107,10 @@ async function uploadBase64ToS3(base64Data: string, prefix: string = "scene-fusi
|
|||||||
|
|
||||||
const filename = generateFilename(`${prefix}-ref`, "png");
|
const filename = generateFilename(`${prefix}-ref`, "png");
|
||||||
const result = await uploadFile(filename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
const result = await uploadFile(filename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||||
console.log(`[SceneFusion] Uploaded reference image to: ${result.url}`);
|
logger.info("reference_image_uploaded", { prefix, bytes: buffer.byteLength });
|
||||||
return result.url;
|
return result.url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SceneFusion] Failed to upload reference image:", error);
|
logger.error("reference_image_upload_failed", error, { prefix });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1114,15 +1123,15 @@ async function downloadAndUploadToS3(
|
|||||||
prefix: string = "scene-fusion"
|
prefix: string = "scene-fusion"
|
||||||
): Promise<{ thumbnail: string; original: string }> {
|
): Promise<{ thumbnail: string; original: string }> {
|
||||||
try {
|
try {
|
||||||
console.log("[SceneFusion] Downloading image from:", imageUrl.substring(0, 100) + "...");
|
logger.debug("download_upload_started", { prefix });
|
||||||
|
|
||||||
const response = await fetch(imageUrl);
|
const response = await fetch(imageUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to download image: ${response.status}`);
|
throw new Error(`Failed to download image: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = Buffer.from(await response.arrayBuffer());
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
console.log(`[SceneFusion] Downloaded image, size: ${buffer.length} bytes`);
|
logger.debug("image_downloaded", { prefix, bytes: buffer.length });
|
||||||
|
|
||||||
// 创建缩略图
|
// 创建缩略图
|
||||||
const thumbnailBuffer = await createThumbnail(buffer);
|
const thumbnailBuffer = await createThumbnail(buffer);
|
||||||
@ -1130,20 +1139,20 @@ async function downloadAndUploadToS3(
|
|||||||
// 上传原图
|
// 上传原图
|
||||||
const originalFilename = generateFilename(`${prefix}-orig`, "png");
|
const originalFilename = generateFilename(`${prefix}-orig`, "png");
|
||||||
const originalResult = await uploadFile(originalFilename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
const originalResult = await uploadFile(originalFilename, buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||||
console.log(`[SceneFusion] Uploaded original to: ${originalResult.url}`);
|
logger.debug("original_uploaded", { prefix, bytes: buffer.byteLength });
|
||||||
|
|
||||||
// 上传缩略图
|
// 上传缩略图
|
||||||
const thumbnailFilename = generateFilename(`${prefix}-thumb`, "jpg");
|
const thumbnailFilename = generateFilename(`${prefix}-thumb`, "jpg");
|
||||||
const thumbnailArrayBuffer = new Uint8Array(thumbnailBuffer).buffer;
|
const thumbnailArrayBuffer = new Uint8Array(thumbnailBuffer).buffer;
|
||||||
const thumbnailResult = await uploadFile(thumbnailFilename, thumbnailArrayBuffer);
|
const thumbnailResult = await uploadFile(thumbnailFilename, thumbnailArrayBuffer);
|
||||||
console.log(`[SceneFusion] Uploaded thumbnail to: ${thumbnailResult.url}`);
|
logger.debug("thumbnail_uploaded", { prefix, bytes: thumbnailBuffer.byteLength });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
original: originalResult.url,
|
original: originalResult.url,
|
||||||
thumbnail: thumbnailResult.url,
|
thumbnail: thumbnailResult.url,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SceneFusion] Failed to download/upload image:", error);
|
logger.error("download_upload_failed", error, { prefix });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1229,7 +1238,7 @@ async function saveSceneFusionImageRecord(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (assetError) {
|
} catch (assetError) {
|
||||||
console.error("[SceneFusion] ⚠️ MediaAsset 注册失败:", assetError);
|
logger.error("media_asset_register_failed", assetError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1318,13 +1327,13 @@ async function callAlibabaImageAPI(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[SceneFusion][${modelId}] Request Debug:
|
logger.debug("alibaba_request_prepared", {
|
||||||
- Index: Scene ${sceneImageUrl.substring(sceneImageUrl.length-15)}
|
modelId,
|
||||||
- Product URL: ${productImageUrl}
|
size: size || "auto",
|
||||||
- Scene URL: ${sceneImageUrl}
|
productImageCount: productImageUrl ? 1 : 0,
|
||||||
- Size: ${size || 'auto'}
|
sceneImageCount: sceneImageUrl ? 1 : 0,
|
||||||
- Prompt: ${prompt}
|
promptLength: prompt.length,
|
||||||
`);
|
});
|
||||||
|
|
||||||
const response = await fetch(baseURL, {
|
const response = await fetch(baseURL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -1337,23 +1346,35 @@ async function callAlibabaImageAPI(
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// 打印完整响应用于调试
|
logger.debug("alibaba_response_received", {
|
||||||
console.log(`[SceneFusion][${modelId}] Full API Response:`, JSON.stringify(data, null, 2));
|
modelId,
|
||||||
|
ok: response.ok,
|
||||||
|
code: data?.code ?? null,
|
||||||
|
hasOutput: Boolean(data?.output),
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok || data.code) {
|
if (!response.ok || data.code) {
|
||||||
console.error("[SceneFusion] API Error Response:", JSON.stringify(data, null, 2));
|
logger.error("alibaba_response_error", {
|
||||||
|
modelId,
|
||||||
|
status: response.status,
|
||||||
|
code: data?.code ?? null,
|
||||||
|
message: data?.message ?? null,
|
||||||
|
});
|
||||||
throw new Error(data.message || (data.code ? `Error ${data.code}: ${data.message}` : `API request failed: ${response.status}`));
|
throw new Error(data.message || (data.code ? `Error ${data.code}: ${data.message}` : `API request failed: ${response.status}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取生成的图片 URL
|
// 提取生成的图片 URL
|
||||||
const choices = data.output?.choices;
|
const choices = data.output?.choices;
|
||||||
if (!choices || choices.length === 0) {
|
if (!choices || choices.length === 0) {
|
||||||
console.error("[SceneFusion] No choices in response, output:", data.output);
|
logger.error("alibaba_no_choices", { modelId, hasOutput: Boolean(data?.output) });
|
||||||
throw new Error("No images in response");
|
throw new Error("No images in response");
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageContent = choices[0]?.message?.content;
|
const imageContent = choices[0]?.message?.content;
|
||||||
console.log(`[SceneFusion] Image content array:`, JSON.stringify(imageContent, null, 2));
|
logger.debug("alibaba_image_content_received", {
|
||||||
|
modelId,
|
||||||
|
contentCount: Array.isArray(imageContent) ? imageContent.length : 0,
|
||||||
|
});
|
||||||
|
|
||||||
if (!imageContent || imageContent.length === 0) {
|
if (!imageContent || imageContent.length === 0) {
|
||||||
throw new Error("No image content in response");
|
throw new Error("No image content in response");
|
||||||
@ -1364,25 +1385,25 @@ async function callAlibabaImageAPI(
|
|||||||
for (const item of imageContent) {
|
for (const item of imageContent) {
|
||||||
if (item.image) {
|
if (item.image) {
|
||||||
generatedImageUrl = item.image;
|
generatedImageUrl = item.image;
|
||||||
console.log(`[SceneFusion] Found image URL in content item`);
|
logger.debug("alibaba_image_url_found", { modelId });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!generatedImageUrl) {
|
if (!generatedImageUrl) {
|
||||||
console.error("[SceneFusion] No image URL found in content:", imageContent);
|
logger.error("alibaba_image_url_missing", {
|
||||||
|
modelId,
|
||||||
|
contentCount: Array.isArray(imageContent) ? imageContent.length : 0,
|
||||||
|
});
|
||||||
throw new Error("No image URL in response");
|
throw new Error("No image URL in response");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查返回的图片 URL 是否与输入相同(可能是 API 返回了原图)
|
// 检查返回的图片 URL 是否与输入相同(可能是 API 返回了原图)
|
||||||
if (generatedImageUrl === productImageUrl || generatedImageUrl === sceneImageUrl) {
|
if (generatedImageUrl === productImageUrl || generatedImageUrl === sceneImageUrl) {
|
||||||
console.warn(`[SceneFusion] ⚠️ WARNING: API returned same URL as input!`);
|
logger.warn("alibaba_returned_input_url", { modelId });
|
||||||
console.warn(`[SceneFusion] - Generated URL: ${generatedImageUrl}`);
|
|
||||||
console.warn(`[SceneFusion] - Product URL: ${productImageUrl}`);
|
|
||||||
console.warn(`[SceneFusion] - Scene URL: ${sceneImageUrl}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[SceneFusion] Generated image URL:", generatedImageUrl.substring(0, 100) + "...");
|
logger.debug("alibaba_image_generated", { modelId });
|
||||||
return generatedImageUrl;
|
return generatedImageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1468,8 +1489,11 @@ async function processSceneWithProvider(
|
|||||||
sceneMode
|
sceneMode
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[SceneFusion] Built prompt: ${prompt}`);
|
logger.debug("provider_prompt_built", {
|
||||||
console.log(`[SceneFusion] Provider type: ${provider.type}`);
|
providerType: provider.type,
|
||||||
|
promptLength: prompt.length,
|
||||||
|
sceneMode,
|
||||||
|
});
|
||||||
|
|
||||||
// 根据 provider 类型选择不同的处理逻辑
|
// 根据 provider 类型选择不同的处理逻辑
|
||||||
switch (provider.type) {
|
switch (provider.type) {
|
||||||
@ -1541,7 +1565,7 @@ async function processWithGemini(
|
|||||||
aspectRatio?: string,
|
aspectRatio?: string,
|
||||||
imageSize?: string
|
imageSize?: string
|
||||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||||
console.log(`[SceneFusion][Gemini] Processing with model: ${modelId}`);
|
logger.info("provider_processing", { providerType: "gemini", modelId });
|
||||||
|
|
||||||
// 创建 Gemini provider
|
// 创建 Gemini provider
|
||||||
const geminiProvider = createGeminiImage({
|
const geminiProvider = createGeminiImage({
|
||||||
@ -1589,7 +1613,7 @@ async function processWithRuxaImage(
|
|||||||
aspectRatio?: string,
|
aspectRatio?: string,
|
||||||
imageSize?: string
|
imageSize?: string
|
||||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||||
console.log(`[SceneFusion][RUXA] Processing with model: ${modelId}`);
|
logger.info("provider_processing", { providerType: "ruxa-image", modelId });
|
||||||
|
|
||||||
const ruxaProvider = createRuxaImage({
|
const ruxaProvider = createRuxaImage({
|
||||||
baseURL: provider.baseUrl,
|
baseURL: provider.baseUrl,
|
||||||
@ -1629,7 +1653,7 @@ async function processWithFalImage(
|
|||||||
aspectRatio?: string,
|
aspectRatio?: string,
|
||||||
imageSize?: string
|
imageSize?: string
|
||||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||||
console.log(`[SceneFusion][FAL] Processing with model: ${modelId}`);
|
logger.info("provider_processing", { providerType: "fal-image", modelId });
|
||||||
|
|
||||||
const falProvider = createFalImage({
|
const falProvider = createFalImage({
|
||||||
baseURL: provider.baseUrl,
|
baseURL: provider.baseUrl,
|
||||||
@ -1669,7 +1693,7 @@ async function processWithOpenAIImage(
|
|||||||
aspectRatio?: string,
|
aspectRatio?: string,
|
||||||
imageSize?: string
|
imageSize?: string
|
||||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||||
console.log(`[SceneFusion][OpenAI Image] Processing with model: ${modelId}`);
|
logger.info("provider_processing", { providerType: "openai-image", modelId });
|
||||||
|
|
||||||
const openAIImageProvider = createOpenAIImage({
|
const openAIImageProvider = createOpenAIImage({
|
||||||
baseURL: provider.baseUrl,
|
baseURL: provider.baseUrl,
|
||||||
@ -1712,7 +1736,7 @@ async function processWithVolcanoVision(
|
|||||||
aspectRatio?: string,
|
aspectRatio?: string,
|
||||||
imageSize?: string
|
imageSize?: string
|
||||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||||
console.log(`[SceneFusion][Volcano] Processing with model: ${modelId}`);
|
logger.info("provider_processing", { providerType: "volcano-vision", modelId });
|
||||||
|
|
||||||
// 创建 Volcano Vision provider
|
// 创建 Volcano Vision provider
|
||||||
const volcanoProvider = createVolcanoVision({
|
const volcanoProvider = createVolcanoVision({
|
||||||
@ -1753,7 +1777,7 @@ async function processWithVolcanoVision(
|
|||||||
* 格式: 
|
* 格式: 
|
||||||
*/
|
*/
|
||||||
function parseImageFromMarkdown(markdown: string): { thumbnail: string; original: string } {
|
function parseImageFromMarkdown(markdown: string): { thumbnail: string; original: string } {
|
||||||
console.log(`[SceneFusion] Parsing markdown: ${markdown.substring(0, 200)}...`);
|
logger.debug("markdown_parse_started", { markdownLength: markdown.length });
|
||||||
|
|
||||||
// 匹配  格式
|
// 匹配  格式
|
||||||
const match = markdown.match(/!\[thumb\|\|\|([^|]+)\|\|\|[^\]]*\]\(([^)]+)\)/);
|
const match = markdown.match(/!\[thumb\|\|\|([^|]+)\|\|\|[^\]]*\]\(([^)]+)\)/);
|
||||||
@ -1805,7 +1829,7 @@ async function processScene(
|
|||||||
sceneMode
|
sceneMode
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[SceneFusion] Built prompt: ${prompt}`);
|
logger.debug("legacy_prompt_built", { promptLength: prompt.length, sceneMode });
|
||||||
|
|
||||||
// 调用阿里生图 API
|
// 调用阿里生图 API
|
||||||
const generatedImageUrl = await callAlibabaImageAPI(
|
const generatedImageUrl = await callAlibabaImageAPI(
|
||||||
@ -1890,14 +1914,15 @@ async function processSceneWithDiversity(
|
|||||||
const attemptCandidates =
|
const attemptCandidates =
|
||||||
orderedHealthyCandidates.length > 0 ? orderedHealthyCandidates : orderedFallbackCandidates;
|
orderedHealthyCandidates.length > 0 ? orderedHealthyCandidates : orderedFallbackCandidates;
|
||||||
|
|
||||||
console.log(
|
logger.debug("candidate_order_resolved", {
|
||||||
`[SceneFusion][${params.requestId}] Scene#${params.sceneIndex + 1} candidate order: ${attemptCandidates
|
requestId: params.requestId,
|
||||||
.map(
|
sceneIndex: params.sceneIndex + 1,
|
||||||
(candidate) =>
|
candidates: attemptCandidates.map((candidate) => ({
|
||||||
`${candidate.model.displayName}/${candidate.provider.type}/score=${getSceneFusionCandidatePriority(candidate)}`
|
modelId: candidate.model.modelId,
|
||||||
)
|
providerType: candidate.provider.type,
|
||||||
.join(" -> ")}`
|
score: getSceneFusionCandidatePriority(candidate),
|
||||||
);
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
const failureReasons: string[] = [];
|
const failureReasons: string[] = [];
|
||||||
|
|
||||||
@ -1909,9 +1934,12 @@ async function processSceneWithDiversity(
|
|||||||
imageSize: params.imageSize,
|
imageSize: params.imageSize,
|
||||||
});
|
});
|
||||||
if (normalizedOutputParams.changed) {
|
if (normalizedOutputParams.changed) {
|
||||||
console.log(
|
logger.info("candidate_output_params_normalized", {
|
||||||
`[SceneFusion][${params.requestId}] Candidate ${candidate.model.displayName} output params normalized: ${normalizedOutputParams.changes.join(", ")}`
|
requestId: params.requestId,
|
||||||
);
|
sceneIndex: params.sceneIndex + 1,
|
||||||
|
modelId: candidate.model.modelId,
|
||||||
|
changes: normalizedOutputParams.changes,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelSlot = await acquireCanvasImageModelSlot({
|
const modelSlot = await acquireCanvasImageModelSlot({
|
||||||
@ -2000,17 +2028,19 @@ async function processSceneWithDiversity(
|
|||||||
markSceneFusionCandidateFailed(candidate, params.modelCooldown);
|
markSceneFusionCandidateFailed(candidate, params.modelCooldown);
|
||||||
// Batch processing can fail because of per-image inputs, review gates, or echo checks.
|
// Batch processing can fail because of per-image inputs, review gates, or echo checks.
|
||||||
// Keep these failures local to this request so they do not auto-disable shared models.
|
// Keep these failures local to this request so they do not auto-disable shared models.
|
||||||
const logCandidateFailure = isSceneFusionReviewRejectError(errorMessage)
|
const failureMeta = {
|
||||||
? console.warn
|
|
||||||
: console.error;
|
|
||||||
logCandidateFailure("[SceneFusion] Candidate failed, try next:", {
|
|
||||||
requestId: params.requestId,
|
requestId: params.requestId,
|
||||||
sceneIndex: params.sceneIndex,
|
sceneIndex: params.sceneIndex,
|
||||||
attempt: attemptIndex + 1,
|
attempt: attemptIndex + 1,
|
||||||
model: candidate.model.modelId,
|
model: candidate.model.modelId,
|
||||||
provider: candidate.provider.type,
|
provider: candidate.provider.type,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
});
|
};
|
||||||
|
if (isSceneFusionReviewRejectError(errorMessage)) {
|
||||||
|
logger.warn("candidate_review_rejected", failureMeta);
|
||||||
|
} else {
|
||||||
|
logger.error("candidate_failed", failureMeta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2060,7 +2090,16 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
// 计算输出尺寸
|
// 计算输出尺寸
|
||||||
const size = calculateSize(aspectRatio || "auto", imageSize || "2K");
|
const size = calculateSize(aspectRatio || "auto", imageSize || "2K");
|
||||||
console.log(`[SceneFusion] Output settings: aspectRatio=${aspectRatio || 'auto'}, imageSize=${imageSize || '2K'}, calculated size=${size || 'auto'}`);
|
logger.info("request_received", {
|
||||||
|
requestId,
|
||||||
|
aspectRatio: aspectRatio || "auto",
|
||||||
|
imageSize: imageSize || "2K",
|
||||||
|
size: size || "auto",
|
||||||
|
sceneCount: scenes.length,
|
||||||
|
sceneMode: resolvedSceneMode,
|
||||||
|
hasProductName: Boolean(productName),
|
||||||
|
preferredModelId: config.modelId || null,
|
||||||
|
});
|
||||||
|
|
||||||
// 检查场景数量限制
|
// 检查场景数量限制
|
||||||
const effectiveMaxScenes = Math.min(config.maxScenes, SCENE_FUSION_MAX_ITEMS);
|
const effectiveMaxScenes = Math.min(config.maxScenes, SCENE_FUSION_MAX_ITEMS);
|
||||||
@ -2071,19 +2110,11 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SceneFusion] Processing ${scenes.length} scenes for user ${session.user.id}`);
|
|
||||||
console.log(`[SceneFusion] Product name: ${productName || '(not specified)'}`);
|
|
||||||
console.log(`[SceneFusion] Scene mode: ${resolvedSceneMode}`);
|
|
||||||
console.log(`[SceneFusion] Using default prompt: ${config.defaultPrompt.substring(0, 50)}...`);
|
|
||||||
console.log(`[SceneFusion] Preferred model ID: ${config.modelId || '(none)'}`);
|
|
||||||
|
|
||||||
// 4. 获取生图模型池(固定多样性模式)
|
// 4. 获取生图模型池(固定多样性模式)
|
||||||
const allModels = await getEnabledAiModelsWithProvider();
|
const allModels = await getEnabledAiModelsWithProvider();
|
||||||
const candidateModels = buildSceneFusionCandidatePool(allModels, config.modelId);
|
const candidateModels = buildSceneFusionCandidatePool(allModels, config.modelId);
|
||||||
if (config.modelId && !candidateModels.some((candidate) => candidate.model.id === config.modelId)) {
|
if (config.modelId && !candidateModels.some((candidate) => candidate.model.id === config.modelId)) {
|
||||||
console.warn(
|
logger.warn("preferred_model_unavailable", { requestId, modelId: config.modelId });
|
||||||
`[SceneFusion] Preferred model ${config.modelId} not available, fallback to full diversity pool`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidateModels.length === 0) {
|
if (candidateModels.length === 0) {
|
||||||
@ -2093,37 +2124,39 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
logger.info("candidate_pool_resolved", {
|
||||||
`[SceneFusion] Diversity candidates: ${candidateModels
|
requestId,
|
||||||
.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)
|
candidateCount: candidateModels.length,
|
||||||
.join(", ")}`
|
providers: [...new Set(candidateModels.map((candidate) => candidate.provider.type))],
|
||||||
);
|
});
|
||||||
const unsupportedCandidates = candidateModels.filter(
|
const unsupportedCandidates = candidateModels.filter(
|
||||||
(candidate) => !canProcessSceneFusionProviderType(candidate.provider.type)
|
(candidate) => !canProcessSceneFusionProviderType(candidate.provider.type)
|
||||||
);
|
);
|
||||||
if (unsupportedCandidates.length > 0) {
|
if (unsupportedCandidates.length > 0) {
|
||||||
console.warn(
|
logger.warn("unsupported_candidates_present", {
|
||||||
`[SceneFusion] Skip unsupported providers for scene fusion: ${unsupportedCandidates
|
requestId,
|
||||||
.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)
|
count: unsupportedCandidates.length,
|
||||||
.join(", ")}`
|
providers: [...new Set(unsupportedCandidates.map((candidate) => candidate.provider.type))],
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
const nonVisionCandidates = candidateModels.filter(
|
const nonVisionCandidates = candidateModels.filter(
|
||||||
(candidate) => !candidate.model.supportsVision
|
(candidate) => !candidate.model.supportsVision
|
||||||
);
|
);
|
||||||
if (nonVisionCandidates.length > 0) {
|
if (nonVisionCandidates.length > 0) {
|
||||||
console.warn(
|
logger.warn("non_vision_candidates_present", {
|
||||||
`[SceneFusion] Models without supportsVision are included for failover diversity: ${nonVisionCandidates
|
requestId,
|
||||||
.map((candidate) => `${candidate.model.displayName}(${candidate.provider.type})`)
|
count: nonVisionCandidates.length,
|
||||||
.join(", ")}`
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const requestOffset = getNextSceneFusionRoundRobinOffset(
|
const requestOffset = getNextSceneFusionRoundRobinOffset(
|
||||||
Math.max(1, candidateModels.length)
|
Math.max(1, candidateModels.length)
|
||||||
);
|
);
|
||||||
console.log(
|
logger.debug("round_robin_offset_resolved", {
|
||||||
`[SceneFusion][${requestId}] Round-robin offset=${requestOffset}, scenes=${scenes.length}, candidates=${candidateModels.length}`
|
requestId,
|
||||||
);
|
requestOffset,
|
||||||
|
sceneCount: scenes.length,
|
||||||
|
candidateCount: candidateModels.length,
|
||||||
|
});
|
||||||
|
|
||||||
// 5. 预处理:将 Base64 图片上传到 S3
|
// 5. 预处理:将 Base64 图片上传到 S3
|
||||||
let productImageUrl = productImage;
|
let productImageUrl = productImage;
|
||||||
@ -2138,9 +2171,12 @@ export async function POST(request: Request) {
|
|||||||
shouldForcePreRmbg ? true : config.preRmbgEnabled
|
shouldForcePreRmbg ? true : config.preRmbgEnabled
|
||||||
);
|
);
|
||||||
const fusionProductImageUrl = preprocessedProduct.productImageUrl;
|
const fusionProductImageUrl = preprocessedProduct.productImageUrl;
|
||||||
console.log(
|
logger.info("pre_rmbg_resolved", {
|
||||||
`[SceneFusion][${requestId}] Pre-RMBG result: used=${preprocessedProduct.usedPreRmbg}, provider=${preprocessedProduct.provider || "-"}, reason=${preprocessedProduct.reason || "-"}`
|
requestId,
|
||||||
);
|
used: preprocessedProduct.usedPreRmbg,
|
||||||
|
provider: preprocessedProduct.provider || null,
|
||||||
|
reason: preprocessedProduct.reason || null,
|
||||||
|
});
|
||||||
|
|
||||||
const processedScenes = await Promise.all(
|
const processedScenes = await Promise.all(
|
||||||
scenes.map(async (scene) => {
|
scenes.map(async (scene) => {
|
||||||
@ -2175,17 +2211,18 @@ export async function POST(request: Request) {
|
|||||||
const imageHashCache = new Map<string, string>();
|
const imageHashCache = new Map<string, string>();
|
||||||
const modelCooldown: SceneFusionModelCooldown = new Map();
|
const modelCooldown: SceneFusionModelCooldown = new Map();
|
||||||
|
|
||||||
console.log(
|
logger.info("agent_labels_resolved", {
|
||||||
`[SceneFusion][${requestId}] Agent labels: product=${resolvedProductName || "(none)"}; targets=${resolvedScenes
|
requestId,
|
||||||
.map((scene) => `${scene.id}:${scene.targetName || "-"}`)
|
hasProductName: Boolean(resolvedProductName),
|
||||||
.join(", ")}`
|
targetCount: resolvedScenes.filter((scene) => Boolean(scene.targetName)).length,
|
||||||
);
|
sceneCount: resolvedScenes.length,
|
||||||
|
});
|
||||||
if (resolvedSceneMode === "random-bg") {
|
if (resolvedSceneMode === "random-bg") {
|
||||||
console.log(
|
logger.debug("random_bg_prompts_resolved", {
|
||||||
`[SceneFusion][${requestId}] Random-bg prompts: ${resolvedScenes
|
requestId,
|
||||||
.map((scene) => `${scene.id}:${(scene.customPrompt || "").slice(0, 80) || "-"}`)
|
sceneCount: resolvedScenes.length,
|
||||||
.join(" | ")}`
|
promptCount: resolvedScenes.filter((scene) => Boolean(scene.customPrompt)).length,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 创建流式响应,每完成一张图就立即发送给前端
|
// 6. 创建流式响应,每完成一张图就立即发送给前端
|
||||||
@ -2199,7 +2236,7 @@ export async function POST(request: Request) {
|
|||||||
try {
|
try {
|
||||||
controller.close();
|
controller.close();
|
||||||
} catch (closeError) {
|
} catch (closeError) {
|
||||||
console.warn("[SceneFusion] Stream close skipped:", closeError);
|
logger.warn("stream_close_skipped", closeError, { requestId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const enqueue = (payload: Record<string, unknown>) => {
|
const enqueue = (payload: Record<string, unknown>) => {
|
||||||
@ -2209,7 +2246,7 @@ export async function POST(request: Request) {
|
|||||||
return true;
|
return true;
|
||||||
} catch (enqueueError) {
|
} catch (enqueueError) {
|
||||||
streamClosed = true;
|
streamClosed = true;
|
||||||
console.warn("[SceneFusion] Stream enqueue skipped:", enqueueError);
|
logger.warn("stream_enqueue_skipped", enqueueError, { requestId });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -2258,27 +2295,16 @@ export async function POST(request: Request) {
|
|||||||
const resolvedMetaScene =
|
const resolvedMetaScene =
|
||||||
enriched.meta.sceneTargets.find((item) => item.id === resolvedScene.id) || null;
|
enriched.meta.sceneTargets.find((item) => item.id === resolvedScene.id) || null;
|
||||||
|
|
||||||
console.log(
|
logger.info("scene_processing_started", {
|
||||||
`[SceneFusion] ========== Processing scene ${currentIndex + 1}/${processedScenes.length} ==========`
|
requestId,
|
||||||
);
|
sceneId: scene.id,
|
||||||
console.log(`[SceneFusion] Scene ID: ${scene.id}`);
|
sceneIndex: currentIndex + 1,
|
||||||
console.log(
|
total: processedScenes.length,
|
||||||
`[SceneFusion] Product Image URL: ${fusionProductImageUrl.substring(0, 80)}...`
|
hasTargetName: Boolean(resolvedScene.targetName),
|
||||||
);
|
hasPositionHint: Boolean(resolvedMetaScene?.positionHint),
|
||||||
console.log(
|
hasReplaceCue: Boolean(resolvedMetaScene?.replaceCue),
|
||||||
`[SceneFusion] Scene Image URL: ${scene.imageUrl.substring(0, 80)}...`
|
hasCustomPrompt: Boolean(resolvedScene.customPrompt),
|
||||||
);
|
});
|
||||||
console.log(
|
|
||||||
`[SceneFusion] Target Name: ${resolvedScene.targetName || "(not specified)"}`
|
|
||||||
);
|
|
||||||
if (resolvedMetaScene?.positionHint || resolvedMetaScene?.replaceCue) {
|
|
||||||
console.log(
|
|
||||||
`[SceneFusion] Target Hints: position=${resolvedMetaScene?.positionHint || "-"}, cue=${resolvedMetaScene?.replaceCue || "-"}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`[SceneFusion] Custom Prompt: ${resolvedScene.customPrompt || "(none)"}`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { result, candidate, attempts } = await processSceneWithDiversity({
|
const { result, candidate, attempts } = await processSceneWithDiversity({
|
||||||
@ -2333,9 +2359,13 @@ export async function POST(request: Request) {
|
|||||||
modelId: candidate.model.modelId,
|
modelId: candidate.model.modelId,
|
||||||
sourceImageUrls: [fusionProductImageUrl, resolvedScene.imageUrl],
|
sourceImageUrls: [fusionProductImageUrl, resolvedScene.imageUrl],
|
||||||
});
|
});
|
||||||
console.log(
|
logger.info("scene_saved", {
|
||||||
`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`
|
requestId,
|
||||||
);
|
sceneId: scene.id,
|
||||||
|
modelId: candidate.model.modelId,
|
||||||
|
providerType: candidate.provider.type,
|
||||||
|
attempts,
|
||||||
|
});
|
||||||
|
|
||||||
successCount += 1;
|
successCount += 1;
|
||||||
if (!enqueue({
|
if (!enqueue({
|
||||||
@ -2356,7 +2386,11 @@ export async function POST(request: Request) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[SceneFusion] Failed to process scene ${scene.id}:`, error);
|
logger.error("scene_processing_failed", error, {
|
||||||
|
requestId,
|
||||||
|
sceneId: scene.id,
|
||||||
|
sceneIndex: currentIndex + 1,
|
||||||
|
});
|
||||||
failCount += 1;
|
failCount += 1;
|
||||||
if (!enqueue({
|
if (!enqueue({
|
||||||
type: "result",
|
type: "result",
|
||||||
@ -2375,7 +2409,12 @@ export async function POST(request: Request) {
|
|||||||
await Promise.all(workers);
|
await Promise.all(workers);
|
||||||
|
|
||||||
// 发送完成消息
|
// 发送完成消息
|
||||||
console.log(`[SceneFusion] Completed: ${successCount} success, ${failCount} failed`);
|
logger.info("request_completed", {
|
||||||
|
requestId,
|
||||||
|
total: processedScenes.length,
|
||||||
|
success: successCount,
|
||||||
|
failed: failCount,
|
||||||
|
});
|
||||||
enqueue({
|
enqueue({
|
||||||
type: "done",
|
type: "done",
|
||||||
summary: {
|
summary: {
|
||||||
@ -2386,7 +2425,7 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SceneFusion] Stream error:", error);
|
logger.error("stream_failed", error, { requestId });
|
||||||
// 发送错误消息
|
// 发送错误消息
|
||||||
if (!streamClosed) {
|
if (!streamClosed) {
|
||||||
enqueue({
|
enqueue({
|
||||||
@ -2408,7 +2447,7 @@ export async function POST(request: Request) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SceneFusion] Unexpected error:", error);
|
logger.error("request_failed", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: error instanceof Error ? error.message : "处理失败" },
|
{ error: error instanceof Error ? error.message : "处理失败" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -55,6 +55,7 @@ import {
|
|||||||
import { requestLongTaskResult } from "@/lib/client/long-task";
|
import { requestLongTaskResult } from "@/lib/client/long-task";
|
||||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||||
|
import { createClientLogger } from "@/lib/observability/client-logger";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { AngleEditDialog } from "./angle-edit-dialog";
|
import { AngleEditDialog } from "./angle-edit-dialog";
|
||||||
import { SceneFusionView } from "./scene-fusion-view";
|
import { SceneFusionView } from "./scene-fusion-view";
|
||||||
@ -64,6 +65,8 @@ import { Slider } from "./ui/slider";
|
|||||||
|
|
||||||
type Tool = "pen" | "arrow" | "circle" | "text";
|
type Tool = "pen" | "arrow" | "circle" | "text";
|
||||||
|
|
||||||
|
const logger = createClientLogger("image-annotator");
|
||||||
|
|
||||||
type DrawingAction = {
|
type DrawingAction = {
|
||||||
type: Tool;
|
type: Tool;
|
||||||
color: string;
|
color: string;
|
||||||
@ -315,17 +318,14 @@ export function ImageAnnotator({
|
|||||||
const localImage = await localPreviewPromise;
|
const localImage = await localPreviewPromise;
|
||||||
setImage(localImage);
|
setImage(localImage);
|
||||||
} catch (previewError) {
|
} catch (previewError) {
|
||||||
console.warn(
|
logger.warn("local_preview_failed", previewError);
|
||||||
"[ImageAnnotator] Local preview failed, fallback to uploaded url:",
|
|
||||||
previewError
|
|
||||||
);
|
|
||||||
const uploadedImage = await loadImageFromSource(uploadResult.url);
|
const uploadedImage = await loadImageFromSource(uploadResult.url);
|
||||||
setImage(uploadedImage);
|
setImage(uploadedImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("图片上传成功");
|
toast.success("图片上传成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[ImageAnnotator] 上传图片失败:", error);
|
logger.error("upload_failed", error);
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "上传失败,请重试";
|
error instanceof Error ? error.message : "上传失败,请重试";
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
@ -517,7 +517,7 @@ export function ImageAnnotator({
|
|||||||
if (data.siteLogoDark) setSiteLogoDark(data.siteLogoDark);
|
if (data.siteLogoDark) setSiteLogoDark(data.siteLogoDark);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("[ImageAnnotator] Failed to fetch site settings:", err);
|
logger.warn("site_settings_fetch_failed", err);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -559,7 +559,7 @@ export function ImageAnnotator({
|
|||||||
setSelectedAspectRatio(null);
|
setSelectedAspectRatio(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
console.error("初始化图片加载失败:", error);
|
logger.error("initial_image_load_failed", error);
|
||||||
setImage(null);
|
setImage(null);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
toast.error("当前图片加载失败,请重试");
|
toast.error("当前图片加载失败,请重试");
|
||||||
@ -892,9 +892,8 @@ export function ImageAnnotator({
|
|||||||
setIsRemovingBg(true);
|
setIsRemovingBg(true);
|
||||||
try {
|
try {
|
||||||
// 使用原图URL获取原图数据,而不是canvas的预览图
|
// 使用原图URL获取原图数据,而不是canvas的预览图
|
||||||
console.log("[抠图] 使用原图URL获取数据:", originalImageUrl);
|
|
||||||
const base64Data = await imageUrlToBase64(originalImageUrl);
|
const base64Data = await imageUrlToBase64(originalImageUrl);
|
||||||
console.log("[抠图] 原图Base64长度:", base64Data.length, "字符");
|
logger.debug("rmbg_source_prepared", { base64Length: base64Data.length });
|
||||||
|
|
||||||
// 调用抠图API
|
// 调用抠图API
|
||||||
const response = await fetch("/api/rmbg", {
|
const response = await fetch("/api/rmbg", {
|
||||||
@ -917,23 +916,18 @@ export function ImageAnnotator({
|
|||||||
|
|
||||||
if (result.success && result.image_base64) {
|
if (result.success && result.image_base64) {
|
||||||
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图)
|
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图)
|
||||||
console.log("[抠图] API返回结果:", {
|
logger.info("rmbg_completed", {
|
||||||
hasOriginalUrl: !!result.original_url,
|
hasOriginalUrl: !!result.original_url,
|
||||||
hasThumbnailUrl: !!result.thumbnail_url,
|
hasThumbnailUrl: !!result.thumbnail_url,
|
||||||
originalUrl: result.original_url?.substring(0, 100),
|
base64Length: String(result.image_base64 || "").length,
|
||||||
thumbnailUrl: result.thumbnail_url?.substring(0, 100),
|
|
||||||
});
|
});
|
||||||
if (result.original_url) {
|
if (result.original_url) {
|
||||||
console.log("[抠图] ✅ 使用原图URL:", result.original_url);
|
|
||||||
setProcessedImageUrl(result.original_url);
|
setProcessedImageUrl(result.original_url);
|
||||||
} else if (result.thumbnail_url) {
|
} else if (result.thumbnail_url) {
|
||||||
console.warn(
|
logger.warn("rmbg_original_url_missing", { hasThumbnailUrl: true });
|
||||||
"[抠图] ⚠️ 未找到original_url,使用thumbnail_url作为备选:",
|
|
||||||
result.thumbnail_url
|
|
||||||
);
|
|
||||||
setProcessedImageUrl(result.thumbnail_url);
|
setProcessedImageUrl(result.thumbnail_url);
|
||||||
} else {
|
} else {
|
||||||
console.warn("[抠图] ⚠️ 未找到图片URL");
|
logger.warn("rmbg_image_url_missing");
|
||||||
setProcessedImageUrl(null);
|
setProcessedImageUrl(null);
|
||||||
}
|
}
|
||||||
setProcessedImageBase64(result.image_base64);
|
setProcessedImageBase64(result.image_base64);
|
||||||
@ -946,7 +940,7 @@ export function ImageAnnotator({
|
|||||||
throw new Error(result.error || result.detail || "抠图处理失败");
|
throw new Error(result.error || result.detail || "抠图处理失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[抠图] 错误:", error);
|
logger.error("rmbg_failed", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error ? error.message : "抠图失败,请稍后重试"
|
error instanceof Error ? error.message : "抠图失败,请稍后重试"
|
||||||
);
|
);
|
||||||
@ -961,8 +955,6 @@ export function ImageAnnotator({
|
|||||||
|
|
||||||
setIsRemovingWatermark(true);
|
setIsRemovingWatermark(true);
|
||||||
try {
|
try {
|
||||||
console.log("[去水印] 使用原图URL:", originalImageUrl);
|
|
||||||
|
|
||||||
const result = await requestLongTaskResult({
|
const result = await requestLongTaskResult({
|
||||||
endpoint: "/api/watermark-remove",
|
endpoint: "/api/watermark-remove",
|
||||||
payload: {
|
payload: {
|
||||||
@ -971,9 +963,9 @@ export function ImageAnnotator({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.success && result.original_url) {
|
if (result.success && result.original_url) {
|
||||||
console.log("[去水印] API返回结果:", {
|
logger.info("watermark_remove_completed", {
|
||||||
originalUrl: result.original_url?.substring(0, 100),
|
hasOriginalUrl: !!result.original_url,
|
||||||
thumbnailUrl: result.thumbnail_url?.substring(0, 100),
|
hasThumbnailUrl: !!result.thumbnail_url,
|
||||||
});
|
});
|
||||||
|
|
||||||
setProcessedImageUrl(result.original_url);
|
setProcessedImageUrl(result.original_url);
|
||||||
@ -988,7 +980,7 @@ export function ImageAnnotator({
|
|||||||
};
|
};
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
} catch {
|
} catch {
|
||||||
console.warn("[去水印] 无法获取Base64,仅使用URL");
|
logger.warn("watermark_remove_base64_fetch_failed");
|
||||||
}
|
}
|
||||||
setProcessType("watermark");
|
setProcessType("watermark");
|
||||||
|
|
||||||
@ -999,7 +991,7 @@ export function ImageAnnotator({
|
|||||||
throw new Error(result.error || "去水印处理失败");
|
throw new Error(result.error || "去水印处理失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[去水印] 错误:", error);
|
logger.error("watermark_remove_failed", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error ? error.message : "去水印失败,请稍后重试"
|
error instanceof Error ? error.message : "去水印失败,请稍后重试"
|
||||||
);
|
);
|
||||||
@ -1015,9 +1007,8 @@ export function ImageAnnotator({
|
|||||||
setIsEnhancing(true);
|
setIsEnhancing(true);
|
||||||
try {
|
try {
|
||||||
// 使用原图URL获取原图数据,而不是canvas的预览图
|
// 使用原图URL获取原图数据,而不是canvas的预览图
|
||||||
console.log("[变清晰] 使用原图URL获取数据:", originalImageUrl);
|
|
||||||
const base64Data = await imageUrlToBase64(originalImageUrl);
|
const base64Data = await imageUrlToBase64(originalImageUrl);
|
||||||
console.log("[变清晰] 原图Base64长度:", base64Data.length, "字符");
|
logger.debug("enhance_source_prepared", { base64Length: base64Data.length });
|
||||||
|
|
||||||
const result = await requestLongTaskResult({
|
const result = await requestLongTaskResult({
|
||||||
endpoint: "/api/enhance",
|
endpoint: "/api/enhance",
|
||||||
@ -1032,40 +1023,28 @@ export function ImageAnnotator({
|
|||||||
|
|
||||||
if (result.success && result.image_base64) {
|
if (result.success && result.image_base64) {
|
||||||
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是处理后的原图)
|
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是处理后的原图)
|
||||||
console.log("[变清晰] API返回结果:", {
|
logger.info("enhance_completed", {
|
||||||
hasOriginalUrl: !!result.original_url,
|
hasOriginalUrl: !!result.original_url,
|
||||||
hasThumbnailUrl: !!result.thumbnail_url,
|
hasThumbnailUrl: !!result.thumbnail_url,
|
||||||
originalUrl: result.original_url,
|
|
||||||
thumbnailUrl: result.thumbnail_url,
|
|
||||||
base64Length: result.image_base64?.length || 0,
|
base64Length: result.image_base64?.length || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ 验证:检查返回的URL是否是处理后的图片
|
// ✅ 验证:检查返回的URL是否是处理后的图片
|
||||||
if (result.original_url) {
|
if (result.original_url) {
|
||||||
console.log("[变清晰] ✅ 使用处理后的原图URL:", result.original_url);
|
|
||||||
console.log(
|
|
||||||
"[变清晰] ✅ URL应该包含 'enhance-orig' 标识,确认是处理后的图片"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 验证URL格式(应该包含enhance-orig)
|
// 验证URL格式(应该包含enhance-orig)
|
||||||
if (
|
if (
|
||||||
!result.original_url.includes("enhance-orig") &&
|
!result.original_url.includes("enhance-orig") &&
|
||||||
!result.original_url.includes("enhance-orig")
|
!result.original_url.includes("enhance-orig")
|
||||||
) {
|
) {
|
||||||
console.warn(
|
logger.warn("enhance_result_marker_missing");
|
||||||
"[变清晰] ⚠️ 警告:URL不包含 'enhance-orig' 标识,可能不是处理后的图片!"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setProcessedImageUrl(result.original_url);
|
setProcessedImageUrl(result.original_url);
|
||||||
} else if (result.thumbnail_url) {
|
} else if (result.thumbnail_url) {
|
||||||
console.warn(
|
logger.warn("enhance_original_url_missing", { hasThumbnailUrl: true });
|
||||||
"[变清晰] ⚠️ 未找到original_url,使用thumbnail_url作为备选:",
|
|
||||||
result.thumbnail_url
|
|
||||||
);
|
|
||||||
setProcessedImageUrl(result.thumbnail_url);
|
setProcessedImageUrl(result.thumbnail_url);
|
||||||
} else {
|
} else {
|
||||||
console.warn("[变清晰] ⚠️ 未找到图片URL");
|
logger.warn("enhance_image_url_missing");
|
||||||
setProcessedImageUrl(null);
|
setProcessedImageUrl(null);
|
||||||
}
|
}
|
||||||
setProcessedImageBase64(result.image_base64);
|
setProcessedImageBase64(result.image_base64);
|
||||||
@ -1078,7 +1057,7 @@ export function ImageAnnotator({
|
|||||||
throw new Error(result.error || result.detail || "变清晰处理失败");
|
throw new Error(result.error || result.detail || "变清晰处理失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[变清晰] 错误:", error);
|
logger.error("enhance_failed", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error ? error.message : "变清晰失败,请稍后重试"
|
error instanceof Error ? error.message : "变清晰失败,请稍后重试"
|
||||||
);
|
);
|
||||||
@ -1359,7 +1338,7 @@ export function ImageAnnotator({
|
|||||||
const finalResultUrl = result.original_url || dataUrl;
|
const finalResultUrl = result.original_url || dataUrl;
|
||||||
|
|
||||||
if (result.original_url) {
|
if (result.original_url) {
|
||||||
console.log("[尺寸调整] 原图URL:", result.original_url);
|
logger.info("resize_completed", { hasOriginalUrl: true });
|
||||||
setProcessedImageUrl(result.original_url);
|
setProcessedImageUrl(result.original_url);
|
||||||
} else {
|
} else {
|
||||||
setProcessedImageUrl(null);
|
setProcessedImageUrl(null);
|
||||||
@ -1372,7 +1351,7 @@ export function ImageAnnotator({
|
|||||||
throw new Error(result.error || "尺寸调整处理失败");
|
throw new Error(result.error || "尺寸调整处理失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[尺寸调整] 错误:", error);
|
logger.error("resize_failed", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error ? error.message : "尺寸调整失败,请稍后重试"
|
error instanceof Error ? error.message : "尺寸调整失败,请稍后重试"
|
||||||
);
|
);
|
||||||
@ -1388,8 +1367,6 @@ export function ImageAnnotator({
|
|||||||
setIsExtending(true);
|
setIsExtending(true);
|
||||||
try {
|
try {
|
||||||
// 直接使用原图URL,而不是转换为base64
|
// 直接使用原图URL,而不是转换为base64
|
||||||
console.log("[图像拓展] 使用原图URL:", originalImageUrl);
|
|
||||||
|
|
||||||
// 调用图像拓展API(直接发送URL)
|
// 调用图像拓展API(直接发送URL)
|
||||||
const response = await fetch("/api/extend", {
|
const response = await fetch("/api/extend", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -1413,15 +1390,17 @@ export function ImageAnnotator({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图)
|
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图)
|
||||||
if (result.original_url) {
|
if (result.original_url) {
|
||||||
console.log("[图像拓展] 原图URL:", result.original_url);
|
logger.info("extend_completed", {
|
||||||
|
hasOriginalUrl: true,
|
||||||
|
hasThumbnailUrl: !!result.thumbnail_url,
|
||||||
|
hasBase64: !!result.base64,
|
||||||
|
});
|
||||||
setProcessedImageUrl(result.original_url);
|
setProcessedImageUrl(result.original_url);
|
||||||
} else if (result.thumbnail_url) {
|
} else if (result.thumbnail_url) {
|
||||||
console.warn(
|
logger.warn("extend_original_url_missing", { hasThumbnailUrl: true });
|
||||||
"[图像拓展] ⚠️ 未找到original_url,使用thumbnail_url作为备选"
|
|
||||||
);
|
|
||||||
setProcessedImageUrl(result.thumbnail_url);
|
setProcessedImageUrl(result.thumbnail_url);
|
||||||
} else {
|
} else {
|
||||||
console.warn("[图像拓展] ⚠️ 未找到图片URL");
|
logger.warn("extend_image_url_missing");
|
||||||
setProcessedImageUrl(null);
|
setProcessedImageUrl(null);
|
||||||
}
|
}
|
||||||
// 优先使用返回的base64,如果没有则使用thumbnail_url或original_url
|
// 优先使用返回的base64,如果没有则使用thumbnail_url或original_url
|
||||||
@ -1435,7 +1414,7 @@ export function ImageAnnotator({
|
|||||||
throw new Error(result.error || result.detail || "图像拓展处理失败");
|
throw new Error(result.error || result.detail || "图像拓展处理失败");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[图像拓展] 错误:", error);
|
logger.error("extend_failed", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error ? error.message : "图像拓展失败,请稍后重试"
|
error instanceof Error ? error.message : "图像拓展失败,请稍后重试"
|
||||||
);
|
);
|
||||||
@ -1507,9 +1486,7 @@ export function ImageAnnotator({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 验证:确认下载的是处理后的图片URL
|
// ✅ 验证:确认下载的是处理后的图片URL
|
||||||
console.log("[下载] 📥 开始下载,processType:", processType);
|
logger.debug("download_started", { processType });
|
||||||
console.log("[下载] 📥 processedImageUrl:", processedImageUrl);
|
|
||||||
console.log("[下载] 📥 这个URL应该是处理后的图片,不是原图!");
|
|
||||||
|
|
||||||
// 确保使用的是处理后的原图URL(检查URL中是否包含thumb标识)
|
// 确保使用的是处理后的原图URL(检查URL中是否包含thumb标识)
|
||||||
let downloadUrl = processedImageUrl;
|
let downloadUrl = processedImageUrl;
|
||||||
@ -1526,7 +1503,7 @@ export function ImageAnnotator({
|
|||||||
.replace(/-thumb\./g, "-orig.")
|
.replace(/-thumb\./g, "-orig.")
|
||||||
.replace(/\/thumb\//g, "/orig/")
|
.replace(/\/thumb\//g, "/orig/")
|
||||||
.replace(/thumbnail/g, "original");
|
.replace(/thumbnail/g, "original");
|
||||||
console.warn("[下载] ⚠️ 检测到缩略图URL,已转换为原图URL:", downloadUrl);
|
logger.warn("download_thumbnail_url_rewritten", { processType });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果转换后的URL仍然包含thumb标识,尝试更激进的转换
|
// 如果转换后的URL仍然包含thumb标识,尝试更激进的转换
|
||||||
@ -1538,18 +1515,20 @@ export function ImageAnnotator({
|
|||||||
if (pathname.includes("thumb")) {
|
if (pathname.includes("thumb")) {
|
||||||
const newPathname = pathname.replace(/thumb/g, "orig");
|
const newPathname = pathname.replace(/thumb/g, "orig");
|
||||||
downloadUrl = downloadUrl.replace(pathname, newPathname);
|
downloadUrl = downloadUrl.replace(pathname, newPathname);
|
||||||
console.warn("[下载] ⚠️ 二次转换原图URL:", downloadUrl);
|
logger.warn("download_thumbnail_url_rewritten_again", { processType });
|
||||||
}
|
}
|
||||||
} catch (urlError) {
|
} catch (urlError) {
|
||||||
// 如果URL解析失败,尝试简单的字符串替换
|
// 如果URL解析失败,尝试简单的字符串替换
|
||||||
downloadUrl = downloadUrl.replace(/thumb/g, "orig");
|
downloadUrl = downloadUrl.replace(/thumb/g, "orig");
|
||||||
console.warn("[下载] ⚠️ 使用简单替换转换原图URL:", downloadUrl);
|
logger.warn("download_thumbnail_url_string_rewrite", {
|
||||||
|
processType,
|
||||||
|
reason: urlError instanceof Error ? urlError.message : "unknown",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 验证:确认下载URL是正确的
|
// ✅ 验证:确认下载URL是正确的
|
||||||
console.log("[下载] ✅ 最终下载URL:", downloadUrl);
|
logger.debug("download_url_resolved", { processType });
|
||||||
console.log("[下载] ✅ 这个URL应该指向处理后的图片(不是原图)");
|
|
||||||
|
|
||||||
// 如果是变清晰,验证URL应该包含enhance相关标识
|
// 如果是变清晰,验证URL应该包含enhance相关标识
|
||||||
if (
|
if (
|
||||||
@ -1557,9 +1536,7 @@ export function ImageAnnotator({
|
|||||||
!downloadUrl.includes("enhance") &&
|
!downloadUrl.includes("enhance") &&
|
||||||
!downloadUrl.includes("1767358346590")
|
!downloadUrl.includes("1767358346590")
|
||||||
) {
|
) {
|
||||||
console.warn(
|
logger.warn("download_enhance_marker_missing");
|
||||||
"[下载] ⚠️ 警告:变清晰下载URL不包含 'enhance' 标识,可能下载了错误的图片!"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从原图URL下载(原图已上传到S3)
|
// 从原图URL下载(原图已上传到S3)
|
||||||
@ -1570,7 +1547,7 @@ export function ImageAnnotator({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
console.log("[下载] 下载的图片大小:", blob.size, "bytes");
|
logger.info("download_blob_ready", { bytes: blob.size, processType });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@ -1582,7 +1559,7 @@ export function ImageAnnotator({
|
|||||||
|
|
||||||
toast.success("原图下载成功");
|
toast.success("原图下载成功");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[下载] 错误:", error);
|
logger.error("download_failed", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
`下载失败: ${error instanceof Error ? error.message : "请稍后重试"}`
|
`下载失败: ${error instanceof Error ? error.message : "请稍后重试"}`
|
||||||
);
|
);
|
||||||
@ -1615,7 +1592,7 @@ export function ImageAnnotator({
|
|||||||
}
|
}
|
||||||
return uploadResult.url;
|
return uploadResult.url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("上传图片失败:", error);
|
logger.error("server_upload_failed", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,12 @@ const files = [
|
|||||||
"lib/ai/providers/gemini-image.ts",
|
"lib/ai/providers/gemini-image.ts",
|
||||||
"lib/ai/providers/volcano-vision.ts",
|
"lib/ai/providers/volcano-vision.ts",
|
||||||
"lib/ai/providers/qianwen-video.ts",
|
"lib/ai/providers/qianwen-video.ts",
|
||||||
|
"services/canvas/index.ts",
|
||||||
|
"components/image-annotator.tsx",
|
||||||
|
"app/api/canvas/auto-title/route.ts",
|
||||||
|
"app/api/canvas/text-detect/route.ts",
|
||||||
|
"app/api/canvas/detail-plan/route.ts",
|
||||||
|
"app/api/scene-fusion/route.ts",
|
||||||
];
|
];
|
||||||
|
|
||||||
const consolePattern = /\bconsole\.(log|warn|error|info|debug)\b/g;
|
const consolePattern = /\bconsole\.(log|warn|error|info|debug)\b/g;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import type {
|
|||||||
EnhanceProOutputFormat,
|
EnhanceProOutputFormat,
|
||||||
EnhanceProSubjectDetection,
|
EnhanceProSubjectDetection,
|
||||||
} from '@/lib/enhance-pro/config';
|
} from '@/lib/enhance-pro/config';
|
||||||
import { createLogger } from '@/lib/logger';
|
import { createLogger } from '@/lib/observability/logger';
|
||||||
|
|
||||||
const log = createLogger("Canvas");
|
const log = createLogger("Canvas");
|
||||||
|
|
||||||
@ -267,7 +267,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 图片上传失败:', error);
|
logCatch(error, "image_upload");
|
||||||
return { success: false, error: '图片上传失败' };
|
return { success: false, error: '图片上传失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -374,7 +374,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
return { success: false, error: data.error || '局部编辑提交失败' };
|
return { success: false, error: data.error || '局部编辑提交失败' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 局部编辑提交失败:', error);
|
logCatch(error, "local_edit_submit");
|
||||||
return { success: false, error: '局部编辑提交失败' };
|
return { success: false, error: '局部编辑提交失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -403,7 +403,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
return { success: false, error: data.error || '查询生图任务失败' };
|
return { success: false, error: data.error || '查询生图任务失败' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 查询生图任务失败:', error);
|
logCatch(error, "image_task_fetch", { taskId });
|
||||||
return { success: false, error: '查询生图任务失败' };
|
return { success: false, error: '查询生图任务失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -422,7 +422,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
return { success: false, error: data.error || '抠图失败' };
|
return { success: false, error: data.error || '抠图失败' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 抠图失败:', error);
|
logCatch(error, "remove_background");
|
||||||
return { success: false, error: '抠图失败' };
|
return { success: false, error: '抠图失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -458,7 +458,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
return { success: false, error: data.error || '框选抠图失败' };
|
return { success: false, error: data.error || '框选抠图失败' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 框选抠图失败:', error);
|
logCatch(error, "box_cutout");
|
||||||
return { success: false, error: '框选抠图失败' };
|
return { success: false, error: '框选抠图失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -477,7 +477,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
return { success: false, error: data.error || '去水印失败' };
|
return { success: false, error: data.error || '去水印失败' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 去水印失败:', error);
|
logCatch(error, "remove_watermark");
|
||||||
return { success: false, error: '去水印失败' };
|
return { success: false, error: '去水印失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -504,7 +504,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
return { success: false, error: data.error || '图片增强失败' };
|
return { success: false, error: data.error || '图片增强失败' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 图片增强失败:', error);
|
logCatch(error, "enhance_image");
|
||||||
return { success: false, error: '图片增强失败' };
|
return { success: false, error: '图片增强失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -559,7 +559,7 @@ export const imageGenerationService = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 图像增强 Pro 失败:', error);
|
logCatch(error, "enhance_image_pro");
|
||||||
if (error instanceof Error && /failed to fetch|network/i.test(error.message)) {
|
if (error instanceof Error && /failed to fetch|network/i.test(error.message)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -612,7 +612,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
return { success: false, error: data.error || data.detail || '图片扩展失败' };
|
return { success: false, error: data.error || data.detail || '图片扩展失败' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 图片扩展失败:', error);
|
logCatch(error, "extend_image");
|
||||||
return { success: false, error: '图片扩展失败' };
|
return { success: false, error: '图片扩展失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -658,7 +658,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
return { success: false, error: data.error || '图层拆分失败' };
|
return { success: false, error: data.error || '图层拆分失败' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 图层拆分失败:', error);
|
logCatch(error, "split_image_layers");
|
||||||
return { success: false, error: '图层拆分失败' };
|
return { success: false, error: '图层拆分失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -714,7 +714,7 @@ export const imageGenerationService = {
|
|||||||
}
|
}
|
||||||
return { success: false, error: data.error || '尺寸调整失败' };
|
return { success: false, error: data.error || '尺寸调整失败' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 尺寸调整失败:', error);
|
logCatch(error, "resize_image", { width, height });
|
||||||
return { success: false, error: '尺寸调整失败' };
|
return { success: false, error: '尺寸调整失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -796,7 +796,7 @@ export const imageGenerationService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 文字识别失败:', error);
|
logCatch(error, "detect_canvas_text");
|
||||||
return { success: false, error: '文字识别失败' };
|
return { success: false, error: '文字识别失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -826,7 +826,7 @@ export const imageGenerationService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 文案建议生成失败:', error);
|
logCatch(error, "text_suggestions");
|
||||||
return { success: false, error: '文案建议生成失败' };
|
return { success: false, error: '文案建议生成失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -845,7 +845,7 @@ export const imageGenerationService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Canvas] 文字替换失败:', error);
|
logCatch(error, "edit_canvas_text", { itemCount: params.items.length });
|
||||||
return { success: false, error: '文字替换失败' };
|
return { success: false, error: '文字替换失败' };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user