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
+152 -51
View File
@@ -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>
) : ( ) : (