Fix/ecommerce video 400 bug #7

Merged
stringadmin merged 10 commits from fix/ecommerce-video-400-bug into master 2026-06-03 02:47:07 +00:00
Showing only changes of commit ec9204437d - Show all commits
+144 -43
View File
@@ -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 };
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) => (
{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>
) : (