Fix/ecommerce video 400 bug #7
@@ -1364,21 +1364,35 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
|
|
||||||
const IMAGE_MODEL = "gpt-image-2";
|
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 buildEcommerceImagePrompt = (outputKey: CloneOutputKey, userText: string, pPlatform: string, pRatio: string, pLanguage: string, pMarket: string): string => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (outputKey === "set") {
|
if (outputKey === "detail") {
|
||||||
parts.push("Generate a complete set of e-commerce product images for online marketplace listing.");
|
parts.push("Generate a professional A+ detail page hero image for an e-commerce product 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("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(`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.");
|
parts.push("Follow platform A+ page best practices — clear hierarchy, professional typography, high visual impact.");
|
||||||
} 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") {
|
} else if (outputKey === "model") {
|
||||||
parts.push("Generate model/try-on lifestyle images for an e-commerce product listing.");
|
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(`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.");
|
parts.push("Model should appear natural and appealing. Background should complement the product. Image must meet platform standards.");
|
||||||
} else if (outputKey === "hot") {
|
} else if (outputKey === "hot") {
|
||||||
@@ -1393,6 +1407,67 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
return parts.join(" ");
|
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 (
|
const generateEcommerceImage = async (
|
||||||
outputKey: CloneOutputKey,
|
outputKey: CloneOutputKey,
|
||||||
images: CloneImageItem[],
|
images: CloneImageItem[],
|
||||||
@@ -1413,7 +1488,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prompt = buildEcommerceImagePrompt(outputKey, userText, pPlatform, pRatio, pLanguage, pMarket);
|
const prompt = buildEcommerceImagePrompt(outputKey, userText, pPlatform, pRatio, pLanguage, pMarket);
|
||||||
const cardLabels = productSetPreviewCards.map((c) => c.label);
|
|
||||||
const stamp = Date.now();
|
const stamp = Date.now();
|
||||||
|
|
||||||
const { taskId } = await aiGenerationClient.createImageTask({
|
const { taskId } = await aiGenerationClient.createImageTask({
|
||||||
@@ -1421,7 +1495,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
prompt,
|
prompt,
|
||||||
ratio: pRatio,
|
ratio: pRatio,
|
||||||
quality: pRatio.includes("720") ? "720P" : "1080P",
|
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||||
gridMode: outputKey === "set" ? "grid" : "single",
|
gridMode: "single",
|
||||||
referenceUrls,
|
referenceUrls,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1431,24 +1505,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (resultUrl) {
|
if (resultUrl) {
|
||||||
setResultFn(
|
setResultFn([{ id: `ecommerce-${stamp}`, src: resultUrl, label: selectedCloneOutput.label }]);
|
||||||
cardLabels.map((label, i) => ({
|
|
||||||
id: `ecommerce-${stamp}-${i}`,
|
|
||||||
src: resultUrl,
|
|
||||||
label,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
setStatusFn("done");
|
setStatusFn("done");
|
||||||
} else {
|
} else {
|
||||||
setStatusFn("idle");
|
setStatusFn("idle");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ServerRequestError && err.status === 402) {
|
if (err instanceof ServerRequestError && err.status === 402) {
|
||||||
setResultFn([{
|
setResultFn([{ id: `ecommerce-error-402`, src: "", label: "余额不足,请充值后继续" }]);
|
||||||
id: `ecommerce-error-402`,
|
|
||||||
src: "",
|
|
||||||
label: "余额不足,请充值后继续",
|
|
||||||
}]);
|
|
||||||
}
|
}
|
||||||
setStatusFn("idle");
|
setStatusFn("idle");
|
||||||
}
|
}
|
||||||
@@ -1656,11 +1720,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const handleGenerate = () => {
|
const handleGenerate = () => {
|
||||||
if (!canGenerate) return;
|
if (!canGenerate) return;
|
||||||
imageAbortRef.current = { current: false };
|
imageAbortRef.current = { current: false };
|
||||||
void generateEcommerceImage(
|
if (cloneOutput === "set") {
|
||||||
cloneOutput, productImages, requirement,
|
void generateSetImages(
|
||||||
platform, ratio, language, market,
|
productImages, cloneSetCounts, requirement,
|
||||||
(s) => setStatus(s as ProductCloneStatus), setResults,
|
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 = () => {
|
const handleGenerateModel = () => {
|
||||||
@@ -1703,11 +1776,11 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const handleSetGenerate = () => {
|
const handleSetGenerate = () => {
|
||||||
if (!canGenerateSet) return;
|
if (!canGenerateSet) return;
|
||||||
imageAbortRef.current = { current: false };
|
imageAbortRef.current = { current: false };
|
||||||
void generateEcommerceImage(
|
void generateSetImages(
|
||||||
"set", setImages, productSetRequirement,
|
setImages, cloneSetCounts, productSetRequirement,
|
||||||
productSetPlatform, productSetRatio, productSetLanguage, productSetMarket,
|
productSetPlatform, productSetRatio, productSetLanguage, productSetMarket,
|
||||||
(s) => setProductSetStatus(s as ProductSetStatus),
|
(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+详情页";
|
detailProductImages.length === 0 ? "请上传产品图" : detailStatus === "generating" ? "生成中..." : "生成A+详情页";
|
||||||
const clonePrimaryLabel =
|
const clonePrimaryLabel =
|
||||||
productImages.length === 0 ? "请先上传商品原图" : status === "generating" ? "生成中..." : `生成${selectedCloneOutput.label}`;
|
productImages.length === 0 ? "请先上传商品原图" : status === "generating" ? "生成中..." : `生成${selectedCloneOutput.label}`;
|
||||||
const setPreviewCards = productSetPreviewCards.map((card, index) => ({
|
const setPreviewCards: CloneResult[] = [];
|
||||||
...card,
|
let setIndex = 0;
|
||||||
src: productSetResultImages[index] ?? card.src,
|
for (const countKey of cloneSetCountOptions.map((o) => o.key)) {
|
||||||
}));
|
const count = cloneSetCounts[countKey];
|
||||||
const clonePreviewCards = productSetPreviewCards.map((card, index) => ({
|
const info = setCountLabels[countKey];
|
||||||
...card,
|
for (let i = 0; i < count; i++) {
|
||||||
src: results[index]?.src ?? card.src,
|
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<{
|
const cloneBasicSelects: Array<{
|
||||||
key: CloneBasicSelectKey;
|
key: CloneBasicSelectKey;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -2723,14 +2817,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="product-set-main-card"
|
className="product-set-main-card"
|
||||||
onClick={() => openProductSetPreview(setPreviewCards[0])}
|
onClick={() => openProductSetPreview(setPreviewCards[0] ?? productSetPreviewCards[0])}
|
||||||
>
|
>
|
||||||
<img src={setPreviewCards[0].src} alt="01 主图" />
|
<img src={setImages[0]?.src ?? (setPreviewCards[0]?.src ?? productSetPreviewCards[0].src)} alt="商品原图" />
|
||||||
<span>{setPreviewCards[0].label}</span>
|
<span>原图素材</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="product-set-flow-arrow" aria-hidden="true" />
|
<div className="product-set-flow-arrow" aria-hidden="true" />
|
||||||
<div className="product-set-card-grid result-reveal">
|
<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)}>
|
<button key={card.id} type="button" onClick={() => openProductSetPreview(card)}>
|
||||||
<img src={card.src} alt={card.label} />
|
<img src={card.src} alt={card.label} />
|
||||||
<span>{card.label}</span>
|
<span>{card.label}</span>
|
||||||
@@ -2783,18 +2877,25 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
|
|
||||||
{status === "done" ? (
|
{status === "done" ? (
|
||||||
<section className="clone-ai-preview-showcase" aria-label="生成结果">
|
<section className="clone-ai-preview-showcase" aria-label="生成结果">
|
||||||
<button type="button" className="clone-ai-main-result" onClick={() => openProductSetPreview(clonePreviewCards[0])}>
|
<button type="button" className="clone-ai-main-result" onClick={() => openProductSetPreview(cloneOutput === "set" ? clonePreviewCards[0] : results[0])}>
|
||||||
<img src={productImages[0]?.src ?? clonePreviewCards[0].src} alt="上传商品原图" />
|
<img src={productImages[0]?.src ?? (cloneOutput === "set" ? clonePreviewCards[0].src : results[0]?.src ?? "")} alt="上传商品原图" />
|
||||||
<span>原图素材</span>
|
<span>原图素材</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="clone-ai-flow-arrow" aria-hidden="true" />
|
<div className="clone-ai-flow-arrow" aria-hidden="true" />
|
||||||
<div className="clone-ai-result-grid result-reveal">
|
<div className="clone-ai-result-grid result-reveal">
|
||||||
{clonePreviewCards.map((card) => (
|
{cloneOutput === "set" ? (
|
||||||
<button key={card.id} type="button" onClick={() => openProductSetPreview(card)}>
|
clonePreviewCards.map((card) => (
|
||||||
<img src={card.src} alt={card.label} />
|
<button key={card.id} type="button" onClick={() => openProductSetPreview(card)}>
|
||||||
<span>{card.label}</span>
|
<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>
|
</button>
|
||||||
))}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user