Fix/ecommerce video 400 bug #7
@@ -20,6 +20,8 @@ const ecommerceSlide5 = `${OSS_MUBAN}/slide-5.png`;
|
|||||||
import ImageMentionMenu, { getImageMentionQuery, insertImageMentionValue, type MentionImageOption } from "./ImageMentionMenu";
|
import ImageMentionMenu, { getImageMentionQuery, insertImageMentionValue, type MentionImageOption } from "./ImageMentionMenu";
|
||||||
import EcommerceVideoWorkspace from "./EcommerceVideoWorkspace";
|
import EcommerceVideoWorkspace from "./EcommerceVideoWorkspace";
|
||||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||||
|
import { ServerRequestError } from "../../api/serverConnection";
|
||||||
|
import { waitForTask } from "../../api/taskSubscription";
|
||||||
import {
|
import {
|
||||||
analyzeProductImages,
|
analyzeProductImages,
|
||||||
buildProductSummary,
|
buildProductSummary,
|
||||||
@@ -819,6 +821,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const [ratio, setRatio] = useState(getPlatformDefaultRatio(platformOptions[0]));
|
const [ratio, setRatio] = useState(getPlatformDefaultRatio(platformOptions[0]));
|
||||||
const [status, setStatus] = useState<ProductCloneStatus>("idle");
|
const [status, setStatus] = useState<ProductCloneStatus>("idle");
|
||||||
const [results, setResults] = useState<CloneResult[]>([]);
|
const [results, setResults] = useState<CloneResult[]>([]);
|
||||||
|
const imageAbortRef = useRef({ current: false });
|
||||||
const [garmentImages, setGarmentImages] = useState<CloneImageItem[]>([]);
|
const [garmentImages, setGarmentImages] = useState<CloneImageItem[]>([]);
|
||||||
const [modelSource, setModelSource] = useState<TryOnModelSource>("ai");
|
const [modelSource, setModelSource] = useState<TryOnModelSource>("ai");
|
||||||
const [modelGender, setModelGender] = useState(tryOnModelOptions.gender[0]);
|
const [modelGender, setModelGender] = useState(tryOnModelOptions.gender[0]);
|
||||||
@@ -1339,6 +1342,116 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
return urls;
|
return urls;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uploadCloneImages = async (images: CloneImageItem[]): Promise<string[]> => {
|
||||||
|
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
|
||||||
|
const urls: string[] = [];
|
||||||
|
for (const item of images) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(item.src);
|
||||||
|
const rawBlob = await resp.blob();
|
||||||
|
const mimeType = SUPPORTED_IMAGE_TYPES.has(rawBlob.type) ? rawBlob.type : "image/png";
|
||||||
|
const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType });
|
||||||
|
const { url } = await aiGenerationClient.uploadAssetBinary(blob, { name: item.name, mimeType, scope: "ecommerce-product" });
|
||||||
|
urls.push(url);
|
||||||
|
} catch {
|
||||||
|
// skip images that fail to upload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IMAGE_MODEL = "gpt-image-2";
|
||||||
|
|
||||||
|
const buildEcommerceImagePrompt = (outputKey: CloneOutputKey, userText: string, pPlatform: string, pRatio: string, pLanguage: string, pMarket: string): string => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (outputKey === "set") {
|
||||||
|
parts.push("Generate a complete set of e-commerce product images for online marketplace listing.");
|
||||||
|
parts.push(`Include: main product image with clean white background, lifestyle/scene image showing product in real use, model-wearing image if applicable, detail/close-up image highlighting texture and quality, selling-point infographic image.`);
|
||||||
|
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
|
||||||
|
parts.push("All images must comply with platform image guidelines — proper margins, no watermark, no prohibited content.");
|
||||||
|
} else if (outputKey === "detail") {
|
||||||
|
parts.push("Generate a series of A+ detail page images for an e-commerce product listing.");
|
||||||
|
parts.push(`Create images for each section: hero/first-screen visual, core selling points, usage scenes, multi-angle views, product detail close-ups.`);
|
||||||
|
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
|
||||||
|
parts.push("Design should follow platform A+ page best practices — clear hierarchy, professional typography, high visual impact.");
|
||||||
|
} else if (outputKey === "model") {
|
||||||
|
parts.push("Generate model/try-on lifestyle images for an e-commerce product listing.");
|
||||||
|
parts.push(`Show the product being used or worn by a model in attractive lifestyle settings.`);
|
||||||
|
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
|
||||||
|
parts.push("Model should appear natural and appealing. Background should complement the product. Image must meet platform standards.");
|
||||||
|
} else if (outputKey === "hot") {
|
||||||
|
parts.push("Generate a high-conversion e-commerce product image that closely replicates the style and composition of the reference image while adapting it to the target platform.");
|
||||||
|
parts.push(`Replicate the visual style, color palette, and layout feel of the source product image, then adapt it for ${pPlatform} marketplace standards.`);
|
||||||
|
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
|
||||||
|
parts.push("The result must look professional and optimized for high click-through rate and conversion on the specified platform.");
|
||||||
|
}
|
||||||
|
if (userText.trim()) {
|
||||||
|
parts.push(`Additional user requirements: ${userText.trim()}`);
|
||||||
|
}
|
||||||
|
return parts.join(" ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateEcommerceImage = async (
|
||||||
|
outputKey: CloneOutputKey,
|
||||||
|
images: CloneImageItem[],
|
||||||
|
userText: string,
|
||||||
|
pPlatform: string,
|
||||||
|
pRatio: string,
|
||||||
|
pLanguage: string,
|
||||||
|
pMarket: string,
|
||||||
|
setStatusFn: (status: "generating" | "done" | "idle") => void,
|
||||||
|
setResultFn: (results: CloneResult[]) => void,
|
||||||
|
): Promise<void> => {
|
||||||
|
setStatusFn("generating");
|
||||||
|
try {
|
||||||
|
const referenceUrls = await uploadCloneImages(images);
|
||||||
|
if (!referenceUrls.length) {
|
||||||
|
setStatusFn("idle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = buildEcommerceImagePrompt(outputKey, userText, pPlatform, pRatio, pLanguage, pMarket);
|
||||||
|
const cardLabels = productSetPreviewCards.map((c) => c.label);
|
||||||
|
const stamp = Date.now();
|
||||||
|
|
||||||
|
const { taskId } = await aiGenerationClient.createImageTask({
|
||||||
|
model: IMAGE_MODEL,
|
||||||
|
prompt,
|
||||||
|
ratio: pRatio,
|
||||||
|
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||||
|
gridMode: outputKey === "set" ? "grid" : "single",
|
||||||
|
referenceUrls,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultUrl = await waitForTask(taskId, {
|
||||||
|
abortRef: imageAbortRef.current,
|
||||||
|
onProgress: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resultUrl) {
|
||||||
|
setResultFn(
|
||||||
|
cardLabels.map((label, i) => ({
|
||||||
|
id: `ecommerce-${stamp}-${i}`,
|
||||||
|
src: resultUrl,
|
||||||
|
label,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
setStatusFn("done");
|
||||||
|
} else {
|
||||||
|
setStatusFn("idle");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ServerRequestError && err.status === 402) {
|
||||||
|
setResultFn([{
|
||||||
|
id: `ecommerce-error-402`,
|
||||||
|
src: "",
|
||||||
|
label: "余额不足,请充值后继续",
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
setStatusFn("idle");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const adVideoUploadedUrlsRef = useRef<string[]>([]);
|
const adVideoUploadedUrlsRef = useRef<string[]>([]);
|
||||||
|
|
||||||
const handleAdVideoPlan = async () => {
|
const handleAdVideoPlan = async () => {
|
||||||
@@ -1540,32 +1653,37 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
|
|
||||||
const handleGenerate = () => {
|
const handleGenerate = () => {
|
||||||
if (!canGenerate) return;
|
if (!canGenerate) return;
|
||||||
setStatus("generating");
|
imageAbortRef.current = { current: false };
|
||||||
window.setTimeout(() => {
|
void generateEcommerceImage(
|
||||||
const stamp = Date.now();
|
cloneOutput, productImages, requirement,
|
||||||
setResults(
|
platform, ratio, language, market,
|
||||||
sampleResults.map((src, index) => ({
|
(s) => setStatus(s as ProductCloneStatus), setResults,
|
||||||
id: `clone-result-${stamp}-${index}`,
|
|
||||||
src,
|
|
||||||
label: index === 0 ? "高度复刻" : index === 1 ? "参考风格" : "平台适配",
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
setStatus("done");
|
|
||||||
}, 900);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGenerateModel = () => {
|
const handleGenerateModel = () => {
|
||||||
|
imageAbortRef.current = { current: false };
|
||||||
setTryOnStatus("modeling");
|
setTryOnStatus("modeling");
|
||||||
window.setTimeout(() => setTryOnStatus("ready"), 700);
|
void generateEcommerceImage(
|
||||||
|
"model", garmentImages, requirement,
|
||||||
|
platform, ratio, language, market,
|
||||||
|
(s) => {
|
||||||
|
if (s === "done") setTryOnStatus("ready");
|
||||||
|
else setTryOnStatus(s as TryOnStatus);
|
||||||
|
},
|
||||||
|
() => { setTryOnStatus("ready"); },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTryOnGenerate = () => {
|
const handleTryOnGenerate = () => {
|
||||||
if (!canGenerateTryOn) return;
|
if (!canGenerateTryOn) return;
|
||||||
setTryOnStatus("generating");
|
imageAbortRef.current = { current: false };
|
||||||
window.setTimeout(() => {
|
void generateEcommerceImage(
|
||||||
setTryOnResultImages([tryOnAssets.tryA, tryOnAssets.tryB, tryOnAssets.hatResultA]);
|
"model", garmentImages, requirement,
|
||||||
setTryOnStatus("done");
|
platform, ratio, language, market,
|
||||||
}, 900);
|
(s) => setTryOnStatus(s as TryOnStatus),
|
||||||
|
(res) => setTryOnResultImages(res.map((r) => r.src).filter(Boolean)),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleScene = (scene: string) => {
|
const toggleScene = (scene: string) => {
|
||||||
@@ -1582,8 +1700,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
|
|
||||||
const handleSetGenerate = () => {
|
const handleSetGenerate = () => {
|
||||||
if (!canGenerateSet) return;
|
if (!canGenerateSet) return;
|
||||||
setProductSetStatus("generating");
|
imageAbortRef.current = { current: false };
|
||||||
window.setTimeout(() => setProductSetStatus("done"), 900);
|
void generateEcommerceImage(
|
||||||
|
"set", setImages, productSetRequirement,
|
||||||
|
productSetPlatform, productSetRatio, productSetLanguage, productSetMarket,
|
||||||
|
(s) => setProductSetStatus(s as ProductSetStatus),
|
||||||
|
(res) => { setProductSetStatus("done"); },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openProductSetPreview = (card: { src: string; label: string }) => {
|
const openProductSetPreview = (card: { src: string; label: string }) => {
|
||||||
@@ -1598,8 +1721,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
|
|
||||||
const handleDetailGenerate = () => {
|
const handleDetailGenerate = () => {
|
||||||
if (!canGenerateDetail) return;
|
if (!canGenerateDetail) return;
|
||||||
setDetailStatus("generating");
|
imageAbortRef.current = { current: false };
|
||||||
window.setTimeout(() => setDetailStatus("done"), 900);
|
void generateEcommerceImage(
|
||||||
|
"detail", detailProductImages, detailRequirement,
|
||||||
|
detailPlatform, getPlatformDefaultRatio(detailPlatform), detailLanguage, detailMarket,
|
||||||
|
(s) => setDetailStatus(s as DetailStatus),
|
||||||
|
(res) => { setDetailStatus("done"); },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetTask = () => {
|
const resetTask = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user