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 EcommerceVideoWorkspace from "./EcommerceVideoWorkspace";
|
||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||
import { ServerRequestError } from "../../api/serverConnection";
|
||||
import { waitForTask } from "../../api/taskSubscription";
|
||||
import {
|
||||
analyzeProductImages,
|
||||
buildProductSummary,
|
||||
@@ -819,6 +821,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const [ratio, setRatio] = useState(getPlatformDefaultRatio(platformOptions[0]));
|
||||
const [status, setStatus] = useState<ProductCloneStatus>("idle");
|
||||
const [results, setResults] = useState<CloneResult[]>([]);
|
||||
const imageAbortRef = useRef({ current: false });
|
||||
const [garmentImages, setGarmentImages] = useState<CloneImageItem[]>([]);
|
||||
const [modelSource, setModelSource] = useState<TryOnModelSource>("ai");
|
||||
const [modelGender, setModelGender] = useState(tryOnModelOptions.gender[0]);
|
||||
@@ -1339,6 +1342,116 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
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 handleAdVideoPlan = async () => {
|
||||
@@ -1540,32 +1653,37 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
const handleGenerate = () => {
|
||||
if (!canGenerate) return;
|
||||
setStatus("generating");
|
||||
window.setTimeout(() => {
|
||||
const stamp = Date.now();
|
||||
setResults(
|
||||
sampleResults.map((src, index) => ({
|
||||
id: `clone-result-${stamp}-${index}`,
|
||||
src,
|
||||
label: index === 0 ? "高度复刻" : index === 1 ? "参考风格" : "平台适配",
|
||||
})),
|
||||
);
|
||||
setStatus("done");
|
||||
}, 900);
|
||||
imageAbortRef.current = { current: false };
|
||||
void generateEcommerceImage(
|
||||
cloneOutput, productImages, requirement,
|
||||
platform, ratio, language, market,
|
||||
(s) => setStatus(s as ProductCloneStatus), setResults,
|
||||
);
|
||||
};
|
||||
|
||||
const handleGenerateModel = () => {
|
||||
imageAbortRef.current = { current: false };
|
||||
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 = () => {
|
||||
if (!canGenerateTryOn) return;
|
||||
setTryOnStatus("generating");
|
||||
window.setTimeout(() => {
|
||||
setTryOnResultImages([tryOnAssets.tryA, tryOnAssets.tryB, tryOnAssets.hatResultA]);
|
||||
setTryOnStatus("done");
|
||||
}, 900);
|
||||
imageAbortRef.current = { current: false };
|
||||
void generateEcommerceImage(
|
||||
"model", garmentImages, requirement,
|
||||
platform, ratio, language, market,
|
||||
(s) => setTryOnStatus(s as TryOnStatus),
|
||||
(res) => setTryOnResultImages(res.map((r) => r.src).filter(Boolean)),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleScene = (scene: string) => {
|
||||
@@ -1582,8 +1700,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
const handleSetGenerate = () => {
|
||||
if (!canGenerateSet) return;
|
||||
setProductSetStatus("generating");
|
||||
window.setTimeout(() => setProductSetStatus("done"), 900);
|
||||
imageAbortRef.current = { current: false };
|
||||
void generateEcommerceImage(
|
||||
"set", setImages, productSetRequirement,
|
||||
productSetPlatform, productSetRatio, productSetLanguage, productSetMarket,
|
||||
(s) => setProductSetStatus(s as ProductSetStatus),
|
||||
(res) => { setProductSetStatus("done"); },
|
||||
);
|
||||
};
|
||||
|
||||
const openProductSetPreview = (card: { src: string; label: string }) => {
|
||||
@@ -1598,8 +1721,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
const handleDetailGenerate = () => {
|
||||
if (!canGenerateDetail) return;
|
||||
setDetailStatus("generating");
|
||||
window.setTimeout(() => setDetailStatus("done"), 900);
|
||||
imageAbortRef.current = { current: false };
|
||||
void generateEcommerceImage(
|
||||
"detail", detailProductImages, detailRequirement,
|
||||
detailPlatform, getPlatformDefaultRatio(detailPlatform), detailLanguage, detailMarket,
|
||||
(s) => setDetailStatus(s as DetailStatus),
|
||||
(res) => { setDetailStatus("done"); },
|
||||
);
|
||||
};
|
||||
|
||||
const resetTask = () => {
|
||||
|
||||
Reference in New Issue
Block a user