fix: address project review bugs

This commit is contained in:
2026-06-12 17:25:30 +08:00
parent f9e55578b3
commit ad4bca31b1
11 changed files with 209 additions and 124 deletions
@@ -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("916") ? "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;