diff --git a/lib/ai/krea-models.ts b/lib/ai/krea-models.ts index 72b234f98..f272048d7 100644 --- a/lib/ai/krea-models.ts +++ b/lib/ai/krea-models.ts @@ -78,6 +78,9 @@ function createImageConfig(fields: string[]) { } function createVideoDurationOptions(modelId: string) { + if (modelId.includes("/bytedance/seedance-2")) { + return Array.from({ length: 12 }, (_, index) => index + 4); + } if (modelId.includes("/minimax/hailuo-02") || modelId.includes("/minimax/hailuo-2.3")) { return [6, 10]; } @@ -114,13 +117,18 @@ function createVideoCapabilities(modelId: string, fields: string[]) { const durationOptions = fields.includes("duration") ? createVideoDurationOptions(modelId) : [5]; const resolutionOptions = fields.includes("resolution") - ? (modelId.includes("/minimax/") ? ["720p", "1080p"] : ["480p", "720p", "1080p"]) + ? modelId.includes("/bytedance/seedance-2") + ? ["480p", "720p"] + : (modelId.includes("/minimax/") ? ["720p", "1080p"] : ["480p", "720p", "1080p"]) : ["720p"]; + const ratioOptions = modelId.includes("/bytedance/seedance-2") + ? ["16:9", "9:16"] + : ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"]; return { supportedModes: Array.from(supportedModes), supportedResolutions: resolutionOptions, - supportedRatios: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"], + supportedRatios: ratioOptions, durationRange: { min: Math.min(...durationOptions), max: Math.max(...durationOptions), diff --git a/lib/ai/providers/krea.ts b/lib/ai/providers/krea.ts index 361b93590..807b4942f 100644 --- a/lib/ai/providers/krea.ts +++ b/lib/ai/providers/krea.ts @@ -325,6 +325,9 @@ function normalizeBatchSize(value?: number) { function normalizeVideoDuration(duration: unknown, endpoint: string) { const value = Number.isFinite(Number(duration)) ? Math.round(Number(duration)) : 5; + if (endpoint.includes("/bytedance/seedance-2")) { + return clamp(value || 5, 4, 15); + } if (endpoint.includes("/minimax/hailuo-02") || endpoint.includes("/minimax/hailuo-2.3")) { return clamp(value || 6, 6, 10); } @@ -345,6 +348,9 @@ function normalizeVideoDuration(duration: unknown, endpoint: string) { function normalizeVideoResolution(resolution: unknown, endpoint: string) { const normalized = normalizeText(String(resolution || "")).toLowerCase(); + if (endpoint.includes("/bytedance/seedance-2")) { + return normalized === "480p" ? "480p" : "720p"; + } if (endpoint.includes("/minimax/")) { if (normalized === "1080p") return "1080p"; if (normalized === "512p" || normalized === "480p") return "512p"; @@ -356,6 +362,9 @@ function normalizeVideoResolution(resolution: unknown, endpoint: string) { function normalizeVideoAspectRatio(ratio: unknown, endpoint: string) { const normalized = normalizeAspectRatio(String(ratio || ""), "16:9"); + if (endpoint.includes("/bytedance/seedance-2")) { + return normalized === "9:16" ? "9:16" : "16:9"; + } if (endpoint.includes("/openai/sora-2")) { return normalized === "9:16" ? "9:16" : "16:9"; } @@ -728,6 +737,11 @@ async function requestKreaJob( if (!jobId) { throw new Error("Krea 未返回任务 ID"); } + console.log("[Krea] Job accepted:", { + endpoint, + jobId, + status: normalizeText(payload?.status) || null, + }); return { ...payload, job_id: jobId }; } @@ -771,6 +785,12 @@ async function waitForKreaJob( return payload; } if (status === "failed" || status === "cancelled") { + console.error("[Krea] Job failed:", { + jobId, + kind, + status, + response: stringifyForLog(payload), + }); throw new Error(String(payload?.error || payload?.message || "Krea 任务失败")); } @@ -821,6 +841,15 @@ function buildKreaBody(params: { const required = new Set(requiredFields); const body: Record = {}; const isVideo = kind === "video"; + const videoMode = + isVideo + ? videoParams?.mode || + (imageUrls.length > 1 + ? "first-last-frame" + : imageUrls.length > 0 + ? "first-frame" + : "text-to-video") + : undefined; const hasImageReference = kind !== "video" && imageUrls.length > 0 && hasKreaSourceImageField(fields); const requestedImageAspectRatio = normalizeText(imageParams?.aspectRatio).toLowerCase(); @@ -914,14 +943,26 @@ function buildKreaBody(params: { body.characterReferenceImages = imageUrls.slice(0, 4); } - if (fields.has("referenceImages") && imageUrls.length > 0) { + if ( + fields.has("referenceImages") && + imageUrls.length > 0 && + (!isVideo || videoMode === "reference-images") + ) { body.referenceImages = endpoint.includes("/image/runway/gen-4-image") ? buildTaggedReferences(imageUrls, "ref", 3) : imageUrls.slice(0, endpoint.includes("/seedance-2") ? 9 : 4); } - if (fields.has("startImage") && imageUrls[0]) body.startImage = imageUrls[0]; - if (fields.has("endImage") && imageUrls[1]) body.endImage = imageUrls[1]; + if ( + fields.has("startImage") && + imageUrls[0] && + (!isVideo || videoMode === "first-frame" || videoMode === "first-last-frame") + ) { + body.startImage = imageUrls[0]; + } + if (fields.has("endImage") && imageUrls[1] && (!isVideo || videoMode === "first-last-frame")) { + body.endImage = imageUrls[1]; + } if (fields.has("init_image_url") && imageUrls[0]) body.init_image_url = imageUrls[0]; if (fields.has("references") && imageUrls[0]) { body.references = [{ type: "image", uri: imageUrls[0] }];