fix: upload scene fusion inputs as urls
This commit is contained in:
parent
3a4aece308
commit
510e3dc323
@ -40,6 +40,7 @@ import {
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { createCanvasPsdTemplateLaunchToken, saveCanvasPsdTemplateLaunchPayload } from "@/lib/canvas/psd-template-launch";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import { requestLongTaskResult } from "@/lib/client/long-task";
|
||||
import {
|
||||
SCENE_FUSION_ASPECT_RATIO_OPTIONS,
|
||||
@ -171,6 +172,23 @@ function createPersistableSnapshot(snapshot: SceneFusionPersistedState): SceneFu
|
||||
};
|
||||
}
|
||||
|
||||
function getUploadedImageUrl(payload: unknown) {
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||||
return "";
|
||||
}
|
||||
const raw = payload as Record<string, unknown>;
|
||||
return String(
|
||||
raw.url ||
|
||||
raw.originalUrl ||
|
||||
raw.original_url ||
|
||||
raw.previewUrl ||
|
||||
raw.preview_url ||
|
||||
raw.thumbnailUrl ||
|
||||
raw.thumbnail_url ||
|
||||
""
|
||||
).trim();
|
||||
}
|
||||
|
||||
const randomBackgroundStyles = [
|
||||
"现代简约客厅",
|
||||
"北欧卧室",
|
||||
@ -447,6 +465,52 @@ export function SceneFusionView({
|
||||
updateProductPreview(null);
|
||||
};
|
||||
|
||||
const uploadImageFile = async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file, file.name || `scene-fusion-${Date.now()}.png`);
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
const imageUrl = getUploadedImageUrl(payload);
|
||||
if (!response.ok || !imageUrl) {
|
||||
throw new Error(
|
||||
String(
|
||||
payload?.error ||
|
||||
payload?.details ||
|
||||
payload?.message ||
|
||||
`${file.name || "图片"}上传失败`
|
||||
)
|
||||
);
|
||||
}
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
const validateImageFiles = (files: File[]) => {
|
||||
const validFiles: File[] = [];
|
||||
for (const file of files) {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toast.error(`${file.name || "文件"}不是图片文件`);
|
||||
continue;
|
||||
}
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast.error(`图片 ${file.name} 大小不能超过 50MB`);
|
||||
continue;
|
||||
}
|
||||
validFiles.push(file);
|
||||
}
|
||||
return validFiles;
|
||||
};
|
||||
|
||||
const uploadImageFiles = async (files: File[]) => {
|
||||
const uploadedItems = await Promise.all(
|
||||
files.map(async (file, index) => ({
|
||||
file,
|
||||
index,
|
||||
imageUrl: await uploadImageFile(file),
|
||||
}))
|
||||
);
|
||||
return uploadedItems.sort((left, right) => left.index - right.index);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialProductImage) {
|
||||
setProductImage(initialProductImage);
|
||||
@ -479,21 +543,25 @@ export function SceneFusionView({
|
||||
if (typeof parsed.imageSize === "string") {
|
||||
setImageSize(parsed.imageSize);
|
||||
}
|
||||
if (typeof parsed.productImage === "string" && parsed.productImage.trim()) {
|
||||
setProductImage(parsed.productImage);
|
||||
updateProductPreview(parsed.productImage);
|
||||
const restoredProductImage = toPersistableImageValue(parsed.productImage);
|
||||
if (restoredProductImage) {
|
||||
setProductImage(restoredProductImage);
|
||||
updateProductPreview(restoredProductImage);
|
||||
}
|
||||
if (typeof parsed.productName === "string") {
|
||||
setProductName(parsed.productName);
|
||||
}
|
||||
if (Array.isArray(parsed.sceneItems)) {
|
||||
setSceneItems(parsed.sceneItems);
|
||||
setSceneItems(toPersistableItems(parsed.sceneItems));
|
||||
}
|
||||
if (Array.isArray(parsed.multiProductItems)) {
|
||||
setMultiProductItems(parsed.multiProductItems);
|
||||
setMultiProductItems(toPersistableItems(parsed.multiProductItems));
|
||||
}
|
||||
if (typeof parsed.referenceSceneImage === "string") {
|
||||
setReferenceSceneImage(parsed.referenceSceneImage || null);
|
||||
const restoredReferenceSceneImage = toPersistableImageValue(
|
||||
parsed.referenceSceneImage
|
||||
);
|
||||
if (restoredReferenceSceneImage) {
|
||||
setReferenceSceneImage(restoredReferenceSceneImage);
|
||||
}
|
||||
if (typeof parsed.referenceTargetName === "string") {
|
||||
setReferenceTargetName(parsed.referenceTargetName);
|
||||
@ -502,7 +570,7 @@ export function SceneFusionView({
|
||||
setReferenceCustomPrompt(parsed.referenceCustomPrompt);
|
||||
}
|
||||
if (Array.isArray(parsed.randomItems)) {
|
||||
setRandomItems(parsed.randomItems);
|
||||
setRandomItems(toPersistableItems(parsed.randomItems));
|
||||
}
|
||||
if (typeof parsed.randomCount === "number" && Number.isFinite(parsed.randomCount)) {
|
||||
setRandomCount(Math.max(1, Math.min(MAX_BATCH_ITEMS, parsed.randomCount)));
|
||||
@ -511,7 +579,7 @@ export function SceneFusionView({
|
||||
setRandomPrompt(parsed.randomPrompt);
|
||||
}
|
||||
if (Array.isArray(parsed.batchItems)) {
|
||||
setBatchItems(parsed.batchItems);
|
||||
setBatchItems(toPersistableItems(parsed.batchItems));
|
||||
}
|
||||
if (Array.isArray(parsed.selectedResultIds)) {
|
||||
setSelectedResultIds(parsed.selectedResultIds.map((value) => String(value || "").trim()).filter(Boolean));
|
||||
@ -638,144 +706,149 @@ export function SceneFusionView({
|
||||
selectedResultIds,
|
||||
]);
|
||||
|
||||
const handleProductUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
const handleProductUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.currentTarget;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast.error("图片大小不能超过 50MB");
|
||||
const [validFile] = validateImageFiles([file]);
|
||||
if (!validFile) {
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
setProductImage(null);
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
const previewUrl = URL.createObjectURL(validFile);
|
||||
updateProductPreview(previewUrl, true);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const result = event.target?.result;
|
||||
if (typeof result === "string") {
|
||||
setProductImage(result);
|
||||
} else {
|
||||
toast.error("图片读取失败,请重试");
|
||||
}
|
||||
};
|
||||
reader.onerror = () => toast.error("图片读取失败,请重试");
|
||||
reader.readAsDataURL(file);
|
||||
e.target.value = "";
|
||||
try {
|
||||
const imageUrl = await uploadImageFile(validFile);
|
||||
setProductImage(imageUrl);
|
||||
updateProductPreview(imageUrl);
|
||||
toast.success("产品图上传成功");
|
||||
} catch (error) {
|
||||
updateProductPreview(null);
|
||||
toast.error(error instanceof Error ? error.message : "产品图上传失败");
|
||||
} finally {
|
||||
input.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleSceneUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (files.length === 0) return;
|
||||
const handleSceneUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.currentTarget;
|
||||
const files = validateImageFiles(Array.from(input.files || []));
|
||||
if (files.length === 0) {
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
if (sceneItems.length + files.length > maxScenes) {
|
||||
toast.error(`单次最多上传 ${maxScenes} 张参考图`);
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast.error(`图片 ${file.name} 大小不能超过 50MB`);
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const image = event.target?.result as string;
|
||||
setSceneItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Math.random().toString(36).slice(2, 10),
|
||||
image,
|
||||
targetName: "",
|
||||
customPrompt: "",
|
||||
status: "pending",
|
||||
},
|
||||
]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
e.target.value = "";
|
||||
try {
|
||||
const uploadedItems = await uploadImageFiles(files);
|
||||
setSceneItems((prev) => [
|
||||
...prev,
|
||||
...uploadedItems.map((item) => ({
|
||||
id: Math.random().toString(36).slice(2, 10),
|
||||
image: item.imageUrl,
|
||||
targetName: "",
|
||||
customPrompt: "",
|
||||
status: "pending" as ItemStatus,
|
||||
})),
|
||||
]);
|
||||
toast.success(`已上传 ${uploadedItems.length} 张参考图`);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "参考图上传失败");
|
||||
} finally {
|
||||
input.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleMultiProductUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (files.length === 0) return;
|
||||
const handleMultiProductUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.currentTarget;
|
||||
const files = validateImageFiles(Array.from(input.files || []));
|
||||
if (files.length === 0) {
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
if (multiProductItems.length + files.length > MAX_BATCH_ITEMS) {
|
||||
toast.error(`单次最多上传 ${MAX_BATCH_ITEMS} 张产品图`);
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast.error(`图片 ${file.name} 大小不能超过 50MB`);
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const image = event.target?.result as string;
|
||||
setMultiProductItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Math.random().toString(36).slice(2, 10),
|
||||
image,
|
||||
status: "pending",
|
||||
},
|
||||
]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
e.target.value = "";
|
||||
try {
|
||||
const uploadedItems = await uploadImageFiles(files);
|
||||
setMultiProductItems((prev) => [
|
||||
...prev,
|
||||
...uploadedItems.map((item) => ({
|
||||
id: Math.random().toString(36).slice(2, 10),
|
||||
image: item.imageUrl,
|
||||
status: "pending" as ItemStatus,
|
||||
})),
|
||||
]);
|
||||
toast.success(`已上传 ${uploadedItems.length} 张产品图`);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "产品图上传失败");
|
||||
} finally {
|
||||
input.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleReferenceSceneUpload = (
|
||||
const handleReferenceSceneUpload = async (
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const file = e.target.files?.[0];
|
||||
const input = e.currentTarget;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast.error("图片大小不能超过 50MB");
|
||||
const [validFile] = validateImageFiles([file]);
|
||||
if (!validFile) {
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const image = event.target?.result as string;
|
||||
setReferenceSceneImage(image);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
e.target.value = "";
|
||||
try {
|
||||
const imageUrl = await uploadImageFile(validFile);
|
||||
setReferenceSceneImage(imageUrl);
|
||||
toast.success("参考图上传成功");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "参考图上传失败");
|
||||
} finally {
|
||||
input.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (files.length === 0) return;
|
||||
const handleBatchUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.currentTarget;
|
||||
const files = validateImageFiles(Array.from(input.files || []));
|
||||
if (files.length === 0) {
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
if (batchItems.length + files.length > MAX_BATCH_ITEMS) {
|
||||
toast.error(`单次最多上传 ${MAX_BATCH_ITEMS} 张图片`);
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
toast.error(`图片 ${file.name} 大小不能超过 50MB`);
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const image = event.target?.result as string;
|
||||
setBatchItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Math.random().toString(36).slice(2, 10),
|
||||
image,
|
||||
status: "pending",
|
||||
},
|
||||
]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
e.target.value = "";
|
||||
try {
|
||||
const uploadedItems = await uploadImageFiles(files);
|
||||
setBatchItems((prev) => [
|
||||
...prev,
|
||||
...uploadedItems.map((item) => ({
|
||||
id: Math.random().toString(36).slice(2, 10),
|
||||
image: item.imageUrl,
|
||||
status: "pending" as ItemStatus,
|
||||
})),
|
||||
]);
|
||||
toast.success(`已上传 ${uploadedItems.length} 张图片`);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "图片上传失败");
|
||||
} finally {
|
||||
input.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const updateSceneItem = (id: string, patch: Partial<ProcessItem>) => {
|
||||
@ -1468,6 +1541,52 @@ export function SceneFusionView({
|
||||
}
|
||||
};
|
||||
|
||||
const retryBatchImageItem = async (item: ProcessItem) => {
|
||||
if (item.image.startsWith("data:")) {
|
||||
toast.error("这张图仍是旧版本地数据,请重新上传后再处理");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setBatchItems((prev) =>
|
||||
prev.map((batchItem) =>
|
||||
batchItem.id === item.id
|
||||
? { ...batchItem, status: "processing", error: undefined }
|
||||
: batchItem
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await processBatchImageItem(item);
|
||||
setBatchItems((prev) =>
|
||||
prev.map((batchItem) =>
|
||||
batchItem.id === item.id
|
||||
? {
|
||||
...batchItem,
|
||||
status: "completed",
|
||||
result: result.result,
|
||||
originalUrl: result.originalUrl,
|
||||
error: undefined,
|
||||
}
|
||||
: batchItem
|
||||
)
|
||||
);
|
||||
toast.success("已重新处理 1 张图片");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "处理失败";
|
||||
setBatchItems((prev) =>
|
||||
prev.map((batchItem) =>
|
||||
batchItem.id === item.id
|
||||
? { ...batchItem, status: "failed", error: message }
|
||||
: batchItem
|
||||
)
|
||||
);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startProcessing = async () => {
|
||||
if (mode === "scene-fission") {
|
||||
if (!productImage) {
|
||||
@ -1570,14 +1689,7 @@ export function SceneFusionView({
|
||||
return;
|
||||
}
|
||||
|
||||
setBatchItems((prev) =>
|
||||
prev.map((batchItem) =>
|
||||
batchItem.id === item.id
|
||||
? { ...batchItem, status: "pending", error: undefined }
|
||||
: batchItem
|
||||
)
|
||||
);
|
||||
await runBatchImageMode();
|
||||
await retryBatchImageItem(item);
|
||||
};
|
||||
|
||||
const downloadImage = async (url: string, filename: string) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user