fix: address project review bugs
This commit is contained in:
@@ -3453,8 +3453,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
return urls;
|
||||
};
|
||||
|
||||
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" },
|
||||
@@ -3569,7 +3567,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const fullPrompt = userText.trim() ? `${subPrompt} Additional user requirements: ${userText.trim()}` : subPrompt;
|
||||
|
||||
const { taskId } = await aiGenerationClient.createImageTask({
|
||||
model: IMAGE_MODEL,
|
||||
prompt: fullPrompt,
|
||||
ratio: normalizeRatioForApi(pRatio),
|
||||
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||
@@ -3653,7 +3650,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const stamp = Date.now();
|
||||
|
||||
const { taskId } = await aiGenerationClient.createImageTask({
|
||||
model: IMAGE_MODEL,
|
||||
prompt,
|
||||
ratio: normalizeRatioForApi(pRatio),
|
||||
quality: pRatio.includes("720") ? "720P" : "1080P",
|
||||
@@ -6564,6 +6560,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
</div>
|
||||
|
||||
{isCloneTool && !isCommandHistoryCollapsed ? (
|
||||
<div
|
||||
className="ecom-command-history__backdrop"
|
||||
role="presentation"
|
||||
onClick={() => setIsCommandHistoryCollapsed(true)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<aside className="ecom-command-history" aria-label="生成历史">
|
||||
<div className="ecom-command-history__tools">
|
||||
<button
|
||||
|
||||
@@ -11,7 +11,14 @@ import {
|
||||
SendOutlined,
|
||||
StopOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { runVideoPlan, renderSceneImage, renderScene, buildSceneTasks, saveVideoHistory } from "./ecommerceVideoService";
|
||||
import {
|
||||
runVideoPlan,
|
||||
renderSceneImage,
|
||||
renderScene,
|
||||
buildSceneTasks,
|
||||
saveVideoHistory,
|
||||
buildComplianceFailureMessage,
|
||||
} from "./ecommerceVideoService";
|
||||
import {
|
||||
PLAN_STEP_LABELS,
|
||||
PLAN_STEPS_DISPLAY,
|
||||
@@ -70,9 +77,11 @@ function buildInputFingerprint(input: {
|
||||
durationSeconds: number;
|
||||
resolution: string;
|
||||
}): string {
|
||||
const imageCount = input.productImageDataUrls.length;
|
||||
const imageSignature = input.productImageDataUrls
|
||||
.map((source) => `${source.length}:${hashString(source)}`)
|
||||
.join("|");
|
||||
return hashString([
|
||||
String(imageCount),
|
||||
imageSignature,
|
||||
input.requirement.trim(),
|
||||
input.platform,
|
||||
input.aspectRatio,
|
||||
@@ -81,6 +90,10 @@ function buildInputFingerprint(input: {
|
||||
].join("::"));
|
||||
}
|
||||
|
||||
function planAllowsVideoGeneration(plan: EcommerceVideoPlanResult | null): boolean {
|
||||
return plan?.compliance.allow_video_generation !== false;
|
||||
}
|
||||
|
||||
function mapResolutionToQuality(res: string): "720P" | "1080P" {
|
||||
return res.includes("720") ? "720P" : "1080P";
|
||||
}
|
||||
@@ -163,6 +176,10 @@ export default function EcommerceVideoWorkspace({
|
||||
useEffect(() => {
|
||||
const delay = 600;
|
||||
if (stage === "planned" && planResult && scenes.length > 0) {
|
||||
if (!planAllowsVideoGeneration(planResult)) {
|
||||
setError(buildComplianceFailureMessage(planResult.compliance));
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => { void handleGenerateImages(); }, delay);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
@@ -468,6 +485,7 @@ export default function EcommerceVideoWorkspace({
|
||||
let liveCompletedSteps: PlanStep[] = resume
|
||||
? ALL_STEPS.filter((s) => stepCompletedFromProgress(s, resume))
|
||||
: [];
|
||||
let liveCurrentStep: PlanStep | null = null;
|
||||
const persist = (stageNow: EcommerceVideoStage) => {
|
||||
saveEcommerceVideoState({
|
||||
inputFingerprint,
|
||||
@@ -484,7 +502,10 @@ export default function EcommerceVideoWorkspace({
|
||||
const result = await runVideoPlan(
|
||||
productImageSources, requirement, buildConfig(),
|
||||
{
|
||||
onStepStart: (step) => setCurrentStep(step),
|
||||
onStepStart: (step) => {
|
||||
liveCurrentStep = step;
|
||||
setCurrentStep(step);
|
||||
},
|
||||
onStepDone: (step) => {
|
||||
liveCompletedSteps = [...liveCompletedSteps, step];
|
||||
setCompletedSteps((prev) => [...prev, step]);
|
||||
@@ -517,7 +538,7 @@ export default function EcommerceVideoWorkspace({
|
||||
const message = err instanceof Error ? err.message : "策划失败";
|
||||
setError(message);
|
||||
// Mark the step that was in-progress as failed so user can resume
|
||||
setFailedStep((prev) => prev || currentStep);
|
||||
setFailedStep((prev) => prev || liveCurrentStep);
|
||||
setStage("idle");
|
||||
// Persist partial progress so the user can resume after a page switch
|
||||
persist("idle");
|
||||
@@ -526,8 +547,8 @@ export default function EcommerceVideoWorkspace({
|
||||
|
||||
const handlePlan = async () => {
|
||||
if (!isAuthenticated) { onRequestLogin?.(); return; }
|
||||
if (!productImageDataUrls.length && !requirement.trim()) {
|
||||
setError("请先上传产品图片或填写商品说明"); return;
|
||||
if (!productImageDataUrls.length) {
|
||||
setError("请先上传商品图片"); return;
|
||||
}
|
||||
await runPlanFlow(null);
|
||||
};
|
||||
@@ -542,6 +563,10 @@ export default function EcommerceVideoWorkspace({
|
||||
|
||||
const handleGenerateImages = async () => {
|
||||
if (!planResult || !scenes.length) return;
|
||||
if (!planAllowsVideoGeneration(planResult)) {
|
||||
setError(buildComplianceFailureMessage(planResult.compliance));
|
||||
return;
|
||||
}
|
||||
setStage("imaging"); setError(null);
|
||||
renderAbortRef.current = { current: false };
|
||||
const ratio = aspectRatio.includes("9:16") || aspectRatio.includes("9:16") ? "9:16"
|
||||
@@ -555,7 +580,11 @@ export default function EcommerceVideoWorkspace({
|
||||
};
|
||||
// Only redo scenes missing imageUrl — preserves successfully generated images on partial retry
|
||||
const scenesToProcess = currentScenes.filter((s) => !s.imageUrl);
|
||||
if (!scenesToProcess.length) { setStage("imaged"); return; }
|
||||
if (!scenesToProcess.length) {
|
||||
setStage("imaged");
|
||||
saveEcommerceVideoState({ inputFingerprint, stage: "imaged", completedSteps, planResult, scenes: currentScenes, sourceImageUrls });
|
||||
return;
|
||||
}
|
||||
for (const scene of scenesToProcess) {
|
||||
if (renderAbortRef.current.current) break;
|
||||
persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending", error: undefined } : s));
|
||||
@@ -597,6 +626,10 @@ export default function EcommerceVideoWorkspace({
|
||||
|
||||
const handleRenderVideos = async () => {
|
||||
if (!scenes.length) return;
|
||||
if (!planAllowsVideoGeneration(planResult)) {
|
||||
setError(planResult ? buildComplianceFailureMessage(planResult.compliance) : "合规检查未通过,已停止生成。");
|
||||
return;
|
||||
}
|
||||
if (!scenes.some((s) => s.imageUrl)) { setError("请先生成分镜图片"); return; }
|
||||
setStage("rendering"); setError(null);
|
||||
renderAbortRef.current = { current: false };
|
||||
@@ -609,7 +642,12 @@ export default function EcommerceVideoWorkspace({
|
||||
};
|
||||
// Only render scenes that haven't completed yet — preserves successful videos on partial retry
|
||||
const scenesToProcess = currentScenes.filter((s) => s.imageUrl && s.status !== "completed");
|
||||
if (!scenesToProcess.length) { setStage(currentScenes.every((s) => s.status === "completed") ? "completed" : "partial_failed"); return; }
|
||||
if (!scenesToProcess.length) {
|
||||
const finalStage = currentScenes.every((s) => s.status === "completed") ? "completed" as const : "partial_failed" as const;
|
||||
setStage(finalStage);
|
||||
saveEcommerceVideoState({ inputFingerprint, stage: finalStage, completedSteps, planResult, scenes: currentScenes, sourceImageUrls });
|
||||
return;
|
||||
}
|
||||
for (const scene of scenesToProcess) {
|
||||
if (renderAbortRef.current.current) break;
|
||||
if (!scene.imageUrl) continue;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||
import { serverRequest } from "../../api/serverConnection";
|
||||
import { waitForTask } from "../../api/taskSubscription";
|
||||
import { ENTERPRISE_DEFAULT_VIDEO_MODEL } from "../../utils/enterpriseVideoPolicy";
|
||||
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
||||
import { ecommerceOssScopes } from "./ecommerceGenerationPersistence";
|
||||
import { normalizeEcommerceImageMime } from "./ecommerceImageValidation";
|
||||
@@ -130,6 +131,18 @@ export interface PlanCallbacks {
|
||||
|
||||
const LOCAL_PREVIEW_MISSING_FILE_MESSAGE = "Please re-upload the product image before generating the short video.";
|
||||
|
||||
export function buildComplianceFailureMessage(compliance: NonNullable<EcommerceVideoPlanProgress["compliance"]>): string {
|
||||
const issues = compliance.issues
|
||||
.slice(0, 3)
|
||||
.map((issue) => [issue.field, issue.problem, issue.suggestion].filter(Boolean).join(":"))
|
||||
.filter(Boolean)
|
||||
.join(";");
|
||||
|
||||
return issues
|
||||
? `合规检查未通过,已停止生成。${issues}`
|
||||
: "合规检查未通过,已停止生成。请修改商品说明或广告文案后重试。";
|
||||
}
|
||||
|
||||
function readBlobAsDataUrl(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -271,6 +284,10 @@ export async function runVideoPlan(
|
||||
emit();
|
||||
}
|
||||
|
||||
if (progress.compliance.allow_video_generation === false) {
|
||||
throw new Error(buildComplianceFailureMessage(progress.compliance));
|
||||
}
|
||||
|
||||
return {
|
||||
imageUrls: progress.imageUrls!,
|
||||
imageDescription: progress.imageDescription,
|
||||
@@ -303,7 +320,6 @@ export async function renderSceneImage(
|
||||
abortRef: { current: boolean },
|
||||
): Promise<void> {
|
||||
const { taskId } = await aiGenerationClient.createImageTask({
|
||||
model: "gpt-image-2",
|
||||
prompt: input.prompt,
|
||||
ratio: input.aspectRatio,
|
||||
quality: "2K",
|
||||
@@ -315,7 +331,6 @@ export async function renderSceneImage(
|
||||
const resultUrl = await waitForTask(taskId, {
|
||||
abortRef,
|
||||
kind: "image",
|
||||
model: "gpt-image-2",
|
||||
onProgress: (e) => callbacks.onSceneImageProgress(input.sceneId, e.progress),
|
||||
});
|
||||
|
||||
@@ -351,7 +366,7 @@ export async function renderScene(
|
||||
): Promise<void> {
|
||||
const allReferenceUrls = [...input.productImageUrls, input.imageUrl];
|
||||
const model = resolveVideoRequestModel({
|
||||
model: input.model || "happyhorse-1.0",
|
||||
model: input.model || ENTERPRISE_DEFAULT_VIDEO_MODEL,
|
||||
referenceUrls: allReferenceUrls,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user