Fix/ecommerce video 400 bug #7
@@ -1364,21 +1364,35 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
const IMAGE_MODEL = "gpt-image-2";
|
||||
|
||||
const setCountLabels: Record<CloneSetCountKey, { label: string; promptDesc: string }> = {
|
||||
selling: { label: "卖点图", promptDesc: "selling-point infographic image highlighting core product advantages and detail close-ups" },
|
||||
white: { label: "白底图", promptDesc: "clean white-background product photo showing the item from its best angle, studio lighting, no props" },
|
||||
scene: { label: "场景图", promptDesc: "lifestyle scene image showing the product in a realistic usage environment with natural surroundings" },
|
||||
};
|
||||
|
||||
const buildSetSubPrompt = (countKey: CloneSetCountKey, index: number, totalCount: number, pPlatform: string, pRatio: string, pLanguage: string, pMarket: string): string => {
|
||||
const info = setCountLabels[countKey];
|
||||
const parts: string[] = [];
|
||||
parts.push(`Generate an e-commerce ${info.label.toLowerCase()} for a product listing.`);
|
||||
parts.push(info.promptDesc);
|
||||
if (totalCount > 1) {
|
||||
parts.push(`This is variant ${index + 1} of ${totalCount} — vary the angle, composition, or emphasis to make each distinct.`);
|
||||
}
|
||||
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
|
||||
parts.push("Must comply with platform image guidelines — proper margins, no watermark, professional quality.");
|
||||
return parts.join(" ");
|
||||
};
|
||||
|
||||
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.`);
|
||||
if (outputKey === "detail") {
|
||||
parts.push("Generate a professional A+ detail page hero image for an e-commerce product listing.");
|
||||
parts.push("Create a high-impact first-screen visual that combines the product photo with key selling points, usage scenes, and detailed specifications in a cohesive layout.");
|
||||
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.");
|
||||
parts.push("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("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") {
|
||||
@@ -1393,6 +1407,67 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
return parts.join(" ");
|
||||
};
|
||||
|
||||
const generateSetImages = async (
|
||||
images: CloneImageItem[],
|
||||
counts: Record<CloneSetCountKey, number>,
|
||||
userText: string,
|
||||
pPlatform: string,
|
||||
pRatio: string,
|
||||
pLanguage: string,
|
||||
pMarket: string,
|
||||
setStatusFn: (status: "generating" | "done" | "idle") => void,
|
||||
setResultFn: (urls: string[]) => void,
|
||||
): Promise<void> => {
|
||||
setStatusFn("generating");
|
||||
try {
|
||||
const referenceUrls = await uploadCloneImages(images);
|
||||
if (!referenceUrls.length) {
|
||||
setStatusFn("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedUrls: string[] = [];
|
||||
const stamp = Date.now();
|
||||
|
||||
for (const countKey of cloneSetCountOptions.map((o) => o.key)) {
|
||||
const count = counts[countKey];
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (imageAbortRef.current.current) break;
|
||||
const subPrompt = buildSetSubPrompt(countKey, i, count, pPlatform, pRatio, pLanguage, pMarket);
|
||||
const fullPrompt = userText.trim() ? `${subPrompt} Additional user requirements: ${userText.trim()}` : subPrompt;
|
||||
|
||||
const { taskId } = await aiGenerationClient.createImageTask({
|
||||
model: IMAGE_MODEL,
|
||||
prompt: fullPrompt,
|
||||
ratio: pRatio,
|
||||
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||
gridMode: "single",
|
||||
referenceUrls,
|
||||
});
|
||||
|
||||
const resultUrl = await waitForTask(taskId, {
|
||||
abortRef: imageAbortRef.current,
|
||||
onProgress: () => {},
|
||||
});
|
||||
|
||||
if (resultUrl) {
|
||||
generatedUrls.push(resultUrl);
|
||||
} else {
|
||||
generatedUrls.push("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setResultFn(generatedUrls);
|
||||
setStatusFn(generatedUrls.some(Boolean) ? "done" : "idle");
|
||||
} catch (err) {
|
||||
if (err instanceof ServerRequestError && err.status === 402) {
|
||||
setResultFn([]);
|
||||
}
|
||||
setStatusFn("idle");
|
||||
}
|
||||
};
|
||||
|
||||
const generateEcommerceImage = async (
|
||||
outputKey: CloneOutputKey,
|
||||
images: CloneImageItem[],
|
||||
@@ -1413,7 +1488,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -1421,7 +1495,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
prompt,
|
||||
ratio: pRatio,
|
||||
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||
gridMode: outputKey === "set" ? "grid" : "single",
|
||||
gridMode: "single",
|
||||
referenceUrls,
|
||||
});
|
||||
|
||||
@@ -1431,24 +1505,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
});
|
||||
|
||||
if (resultUrl) {
|
||||
setResultFn(
|
||||
cardLabels.map((label, i) => ({
|
||||
id: `ecommerce-${stamp}-${i}`,
|
||||
src: resultUrl,
|
||||
label,
|
||||
})),
|
||||
);
|
||||
setResultFn([{ id: `ecommerce-${stamp}`, src: resultUrl, label: selectedCloneOutput.label }]);
|
||||
setStatusFn("done");
|
||||
} else {
|
||||
setStatusFn("idle");
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ServerRequestError && err.status === 402) {
|
||||
setResultFn([{
|
||||
id: `ecommerce-error-402`,
|
||||
src: "",
|
||||
label: "余额不足,请充值后继续",
|
||||
}]);
|
||||
setResultFn([{ id: `ecommerce-error-402`, src: "", label: "余额不足,请充值后继续" }]);
|
||||
}
|
||||
setStatusFn("idle");
|
||||
}
|
||||
@@ -1656,11 +1720,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const handleGenerate = () => {
|
||||
if (!canGenerate) return;
|
||||
imageAbortRef.current = { current: false };
|
||||
void generateEcommerceImage(
|
||||
cloneOutput, productImages, requirement,
|
||||
platform, ratio, language, market,
|
||||
(s) => setStatus(s as ProductCloneStatus), setResults,
|
||||
);
|
||||
if (cloneOutput === "set") {
|
||||
void generateSetImages(
|
||||
productImages, cloneSetCounts, requirement,
|
||||
platform, ratio, language, market,
|
||||
(s) => setStatus(s as ProductCloneStatus),
|
||||
(urls) => setProductSetResultImages(urls),
|
||||
);
|
||||
} else {
|
||||
void generateEcommerceImage(
|
||||
cloneOutput, productImages, requirement,
|
||||
platform, ratio, language, market,
|
||||
(s) => setStatus(s as ProductCloneStatus), setResults,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateModel = () => {
|
||||
@@ -1703,11 +1776,11 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const handleSetGenerate = () => {
|
||||
if (!canGenerateSet) return;
|
||||
imageAbortRef.current = { current: false };
|
||||
void generateEcommerceImage(
|
||||
"set", setImages, productSetRequirement,
|
||||
void generateSetImages(
|
||||
setImages, cloneSetCounts, productSetRequirement,
|
||||
productSetPlatform, productSetRatio, productSetLanguage, productSetMarket,
|
||||
(s) => setProductSetStatus(s as ProductSetStatus),
|
||||
(res) => setProductSetResultImages(res.map((r) => r.src).filter(Boolean)),
|
||||
(urls) => setProductSetResultImages(urls),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1797,14 +1870,35 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
detailProductImages.length === 0 ? "请上传产品图" : detailStatus === "generating" ? "生成中..." : "生成A+详情页";
|
||||
const clonePrimaryLabel =
|
||||
productImages.length === 0 ? "请先上传商品原图" : status === "generating" ? "生成中..." : `生成${selectedCloneOutput.label}`;
|
||||
const setPreviewCards = productSetPreviewCards.map((card, index) => ({
|
||||
...card,
|
||||
src: productSetResultImages[index] ?? card.src,
|
||||
}));
|
||||
const clonePreviewCards = productSetPreviewCards.map((card, index) => ({
|
||||
...card,
|
||||
src: results[index]?.src ?? card.src,
|
||||
}));
|
||||
const setPreviewCards: CloneResult[] = [];
|
||||
let setIndex = 0;
|
||||
for (const countKey of cloneSetCountOptions.map((o) => o.key)) {
|
||||
const count = cloneSetCounts[countKey];
|
||||
const info = setCountLabels[countKey];
|
||||
for (let i = 0; i < count; i++) {
|
||||
setPreviewCards.push({
|
||||
id: `${countKey}-${i}`,
|
||||
src: productSetResultImages[setIndex] ?? productSetPreviewCards[setIndex % productSetPreviewCards.length]?.src ?? "",
|
||||
label: `${info.label}${count > 1 ? ` ${i + 1}` : ""}`,
|
||||
});
|
||||
setIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
const clonePreviewCards: CloneResult[] = [];
|
||||
let cloneIndex = 0;
|
||||
for (const countKey of cloneSetCountOptions.map((o) => o.key)) {
|
||||
const count = cloneSetCounts[countKey];
|
||||
const info = setCountLabels[countKey];
|
||||
for (let i = 0; i < count; i++) {
|
||||
clonePreviewCards.push({
|
||||
id: `${countKey}-${i}`,
|
||||
src: results[cloneIndex]?.src ?? productSetPreviewCards[cloneIndex % productSetPreviewCards.length]?.src ?? "",
|
||||
label: `${info.label}${count > 1 ? ` ${i + 1}` : ""}`,
|
||||
});
|
||||
cloneIndex++;
|
||||
}
|
||||
}
|
||||
const cloneBasicSelects: Array<{
|
||||
key: CloneBasicSelectKey;
|
||||
label: string;
|
||||
@@ -2723,14 +2817,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
<button
|
||||
type="button"
|
||||
className="product-set-main-card"
|
||||
onClick={() => openProductSetPreview(setPreviewCards[0])}
|
||||
onClick={() => openProductSetPreview(setPreviewCards[0] ?? productSetPreviewCards[0])}
|
||||
>
|
||||
<img src={setPreviewCards[0].src} alt="01 主图" />
|
||||
<span>{setPreviewCards[0].label}</span>
|
||||
<img src={setImages[0]?.src ?? (setPreviewCards[0]?.src ?? productSetPreviewCards[0].src)} alt="商品原图" />
|
||||
<span>原图素材</span>
|
||||
</button>
|
||||
<div className="product-set-flow-arrow" aria-hidden="true" />
|
||||
<div className="product-set-card-grid result-reveal">
|
||||
{setPreviewCards.slice(1).map((card) => (
|
||||
{setPreviewCards.map((card) => (
|
||||
<button key={card.id} type="button" onClick={() => openProductSetPreview(card)}>
|
||||
<img src={card.src} alt={card.label} />
|
||||
<span>{card.label}</span>
|
||||
@@ -2783,18 +2877,25 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
{status === "done" ? (
|
||||
<section className="clone-ai-preview-showcase" aria-label="生成结果">
|
||||
<button type="button" className="clone-ai-main-result" onClick={() => openProductSetPreview(clonePreviewCards[0])}>
|
||||
<img src={productImages[0]?.src ?? clonePreviewCards[0].src} alt="上传商品原图" />
|
||||
<button type="button" className="clone-ai-main-result" onClick={() => openProductSetPreview(cloneOutput === "set" ? clonePreviewCards[0] : results[0])}>
|
||||
<img src={productImages[0]?.src ?? (cloneOutput === "set" ? clonePreviewCards[0].src : results[0]?.src ?? "")} alt="上传商品原图" />
|
||||
<span>原图素材</span>
|
||||
</button>
|
||||
<div className="clone-ai-flow-arrow" aria-hidden="true" />
|
||||
<div className="clone-ai-result-grid result-reveal">
|
||||
{clonePreviewCards.map((card) => (
|
||||
<button key={card.id} type="button" onClick={() => openProductSetPreview(card)}>
|
||||
<img src={card.src} alt={card.label} />
|
||||
<span>{card.label}</span>
|
||||
{cloneOutput === "set" ? (
|
||||
clonePreviewCards.map((card) => (
|
||||
<button key={card.id} type="button" onClick={() => openProductSetPreview(card)}>
|
||||
<img src={card.src} alt={card.label} />
|
||||
<span>{card.label}</span>
|
||||
</button>
|
||||
))
|
||||
) : results[0]?.src ? (
|
||||
<button type="button" onClick={() => openProductSetPreview(results[0])}>
|
||||
<img src={results[0].src} alt={selectedCloneOutput.label} />
|
||||
<span>{selectedCloneOutput.label}</span>
|
||||
</button>
|
||||
))}
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user