fix: improve seedance reference handling
This commit is contained in:
@@ -9,6 +9,7 @@ import { ModelPicker } from "@/components/model-picker";
|
||||
import { defaultConfig, useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { CreditSymbol, requestCreditCost } from "@/constant/credits";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { seedanceReferenceLabel } from "@/lib/seedance-video";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover";
|
||||
import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover";
|
||||
@@ -303,7 +304,7 @@ function ImageSortCard({
|
||||
<div className="w-24 shrink-0 overflow-hidden rounded-lg border" style={{ background: theme.node.fill, borderColor: theme.node.stroke }}>
|
||||
<div className="relative">
|
||||
<img src={input.image.dataUrl} alt={input.title} className="aspect-square w-full object-cover" />
|
||||
<span className="absolute left-1 top-1 rounded bg-black/50 px-1 py-0.5 text-[9px] font-medium text-white">{imageIndex + 1}</span>
|
||||
<span className="absolute left-1 top-1 rounded bg-black/50 px-1 py-0.5 text-[9px] font-medium text-white">{seedanceReferenceLabel("image", imageIndex)}</span>
|
||||
<HorizontalOrderButtons index={imageIndex} total={imageTotal} onMove={(offset) => onMove(input, offset)} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,7 +329,7 @@ function VideoSortCard({
|
||||
<div className="w-32 shrink-0 overflow-hidden rounded-lg border" style={{ background: theme.node.fill, borderColor: theme.node.stroke }}>
|
||||
<div className="relative">
|
||||
<video src={input.video.url} className="aspect-video w-full bg-black object-cover" muted preload="metadata" />
|
||||
<span className="absolute left-1 top-1 rounded bg-black/50 px-1 py-0.5 text-[9px] font-medium text-white">{videoIndex + 1}</span>
|
||||
<span className="absolute left-1 top-1 rounded bg-black/50 px-1 py-0.5 text-[9px] font-medium text-white">{seedanceReferenceLabel("video", videoIndex)}</span>
|
||||
<HorizontalOrderButtons index={videoIndex} total={videoTotal} onMove={(offset) => onMove(input, offset)} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,7 +359,7 @@ function AudioSortCard({
|
||||
<audio src={input.audio.url} controls className="h-8 w-full" preload="metadata" />
|
||||
<div className="mt-1 flex justify-between">
|
||||
<Button size="small" className="!h-6 !w-6 !min-w-6 !rounded-full !p-0" icon={<ArrowLeft className="size-3" />} disabled={audioIndex <= 0} onClick={() => onMove(input, -1)} />
|
||||
<span className="text-[10px] opacity-45">{audioIndex + 1}</span>
|
||||
<span className="text-[10px] opacity-45">{seedanceReferenceLabel("audio", audioIndex)}</span>
|
||||
<Button size="small" className="!h-6 !w-6 !min-w-6 !rounded-full !p-0" icon={<ArrowRight className="size-3" />} disabled={audioIndex >= audioTotal - 1} onClick={() => onMove(input, 1)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,10 @@ function readReferenceVideo(node: CanvasNodeData): ReferenceVideo | null {
|
||||
type: node.metadata.mimeType || "video/mp4",
|
||||
url: node.metadata.content,
|
||||
storageKey: node.metadata.storageKey,
|
||||
bytes: node.metadata.bytes,
|
||||
width: node.metadata.naturalWidth,
|
||||
height: node.metadata.naturalHeight,
|
||||
durationMs: node.metadata.durationMs,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { BookOpen, CheckSquare, ClipboardPaste, Download, FolderPlus, History, LoaderCircle, Music2, Plus, SlidersHorizontal, Sparkles, Trash2, Upload, VideoIcon } from "lucide-react";
|
||||
import { ArrowLeft, ArrowRight, BookOpen, CheckSquare, ClipboardPaste, Download, FolderPlus, History, LoaderCircle, Music2, Plus, SlidersHorizontal, Sparkles, Trash2, Upload, VideoIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { App, Button, Checkbox, Drawer, Empty, Input, Modal, Tag, Typography } from "antd";
|
||||
import localforage from "localforage";
|
||||
@@ -13,7 +13,7 @@ import { PromptSelectDialog } from "@/components/prompts/prompt-select-dialog";
|
||||
import { VideoSettingsPanel, normalizeVideoResolutionValue, normalizeVideoSizeValue, videoSizeLabel } from "@/components/video-settings-panel";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { formatBytes, formatDuration } from "@/lib/image-utils";
|
||||
import { boolConfig, isSeedanceVideoConfig, normalizeSeedanceRatio, SEEDANCE_REFERENCE_LIMITS } from "@/lib/seedance-video";
|
||||
import { boolConfig, isSeedanceVideoConfig, normalizeSeedanceRatio, seedanceReferenceLabel, seedanceVideoReferenceError, seedanceVideoReferenceHint, SEEDANCE_REFERENCE_LIMITS } from "@/lib/seedance-video";
|
||||
import { deleteStoredMedia, resolveMediaUrl, uploadMediaFile } from "@/services/file-storage";
|
||||
import { resolveImageUrl, uploadImage } from "@/services/image-storage";
|
||||
import { requestVideoGeneration, storeGeneratedVideo } from "@/services/api/video";
|
||||
@@ -126,7 +126,7 @@ export default function VideoPage() {
|
||||
const nextVideoReferences = await Promise.all(
|
||||
videoFiles.map(async (file) => {
|
||||
const video = await uploadMediaFile(file, "video-reference");
|
||||
return { id: nanoid(), name: file.name, type: video.mimeType, url: video.url, storageKey: video.storageKey, durationMs: video.durationMs };
|
||||
return { id: nanoid(), name: file.name, type: video.mimeType, url: video.url, storageKey: video.storageKey, bytes: video.bytes, width: video.width, height: video.height, durationMs: video.durationMs };
|
||||
}),
|
||||
);
|
||||
const nextAudioReferences = filterAudioReferencesByDuration(
|
||||
@@ -209,6 +209,11 @@ export default function VideoPage() {
|
||||
openConfigDialog(true);
|
||||
return null;
|
||||
}
|
||||
const videoReferenceError = seedanceVideoReferenceError(videoReferences);
|
||||
if (videoReferenceError) {
|
||||
message.error(`${videoReferenceError}。${seedanceVideoReferenceHint}`);
|
||||
return null;
|
||||
}
|
||||
return { text, config: buildVideoConfig(effectiveConfig, model), references: [...references], videoReferences: [...videoReferences], audioReferences: [...audioReferences] };
|
||||
};
|
||||
|
||||
@@ -240,7 +245,7 @@ export default function VideoPage() {
|
||||
const stored = await uploadImage(payload.dataUrl);
|
||||
setReferences((value) => [...value, { id: nanoid(), name: payload.title, type: stored.mimeType, dataUrl: stored.url, storageKey: stored.storageKey }].slice(0, SEEDANCE_REFERENCE_LIMITS.images));
|
||||
} else if (payload.kind === "video") {
|
||||
setVideoReferences((value) => [...value, { id: nanoid(), name: payload.title, type: "video/mp4", url: payload.url, storageKey: payload.storageKey }].slice(0, SEEDANCE_REFERENCE_LIMITS.videos));
|
||||
setVideoReferences((value) => [...value, { id: nanoid(), name: payload.title, type: "video/mp4", url: payload.url, storageKey: payload.storageKey, width: payload.width, height: payload.height }].slice(0, SEEDANCE_REFERENCE_LIMITS.videos));
|
||||
}
|
||||
setAssetPickerOpen(false);
|
||||
};
|
||||
@@ -343,9 +348,11 @@ export default function VideoPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed border-stone-300 p-2 pb-3 overscroll-x-contain dark:border-stone-700">
|
||||
{references.map((item) => (
|
||||
{references.map((item, index) => (
|
||||
<div key={item.id} className="group relative size-20 shrink-0 overflow-hidden rounded-md border border-stone-200 dark:border-stone-800">
|
||||
<img src={item.dataUrl} alt={item.name} className="size-full object-cover" />
|
||||
<span className="absolute left-1 top-1 rounded bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-white">{seedanceReferenceLabel("image", index)}</span>
|
||||
<ReferenceOrderButtons index={index} total={references.length} onMove={(offset) => setReferences((value) => moveListItem(value, index, offset))} />
|
||||
<button type="button" className="absolute right-1 top-1 hidden size-6 items-center justify-center rounded bg-black/60 text-white group-hover:flex" onClick={() => setReferences((value) => value.filter((ref) => ref.id !== item.id))} aria-label="移除参考图">
|
||||
<Trash2 className="size-3.5" />
|
||||
</button>
|
||||
@@ -363,9 +370,11 @@ export default function VideoPage() {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed border-stone-300 p-2 pb-3 overscroll-x-contain dark:border-stone-700">
|
||||
{videoReferences.map((item) => (
|
||||
{videoReferences.map((item, index) => (
|
||||
<div key={item.id} className="group relative h-20 w-32 shrink-0 overflow-hidden rounded-md border border-stone-200 bg-black dark:border-stone-800">
|
||||
<video src={item.url} className="size-full object-cover" muted preload="metadata" />
|
||||
<span className="absolute left-1 top-1 rounded bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-white">{seedanceReferenceLabel("video", index)}</span>
|
||||
<ReferenceOrderButtons index={index} total={videoReferences.length} onMove={(offset) => setVideoReferences((value) => moveListItem(value, index, offset))} />
|
||||
<button type="button" className="absolute right-1 top-1 hidden size-6 items-center justify-center rounded bg-black/60 text-white group-hover:flex" onClick={() => setVideoReferences((value) => value.filter((ref) => ref.id !== item.id))} aria-label="移除参考视频">
|
||||
<Trash2 className="size-3.5" />
|
||||
</button>
|
||||
@@ -383,13 +392,15 @@ export default function VideoPage() {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed border-stone-300 p-2 pb-3 overscroll-x-contain dark:border-stone-700">
|
||||
{audioReferences.map((item) => (
|
||||
{audioReferences.map((item, index) => (
|
||||
<div key={item.id} className="group relative flex h-20 w-48 shrink-0 flex-col justify-center gap-2 rounded-md border border-stone-200 bg-stone-50 px-2 dark:border-stone-800 dark:bg-stone-900">
|
||||
<div className="flex min-w-0 items-center gap-2 text-xs text-stone-500 dark:text-stone-400">
|
||||
<Music2 className="size-4 shrink-0" />
|
||||
<span className="shrink-0 rounded bg-stone-200 px-1 text-[10px] text-stone-700 dark:bg-stone-800 dark:text-stone-200">{seedanceReferenceLabel("audio", index)}</span>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</div>
|
||||
<audio src={item.url} controls className="h-8 w-full" preload="metadata" />
|
||||
<ReferenceOrderButtons index={index} total={audioReferences.length} onMove={(offset) => setAudioReferences((value) => moveListItem(value, index, offset))} />
|
||||
<button type="button" className="absolute right-1 top-1 hidden size-6 items-center justify-center rounded bg-black/60 text-white group-hover:flex" onClick={() => setAudioReferences((value) => value.filter((ref) => ref.id !== item.id))} aria-label="移除参考音频">
|
||||
<Trash2 className="size-3.5" />
|
||||
</button>
|
||||
@@ -698,6 +709,24 @@ function filterAudioReferencesByDuration(existing: ReferenceAudio[], next: Refer
|
||||
return accepted;
|
||||
}
|
||||
|
||||
function moveListItem<T>(items: T[], index: number, offset: number) {
|
||||
const targetIndex = index + offset;
|
||||
if (targetIndex < 0 || targetIndex >= items.length) return items;
|
||||
const next = [...items];
|
||||
[next[index], next[targetIndex]] = [next[targetIndex], next[index]];
|
||||
return next;
|
||||
}
|
||||
|
||||
function ReferenceOrderButtons({ index, total, onMove }: { index: number; total: number; onMove: (offset: number) => void }) {
|
||||
if (total <= 1) return null;
|
||||
return (
|
||||
<div className="absolute inset-x-1 bottom-1 flex justify-between">
|
||||
<Button size="small" className="!h-6 !w-6 !min-w-6 !rounded-full !bg-white/85 !p-0 !shadow-sm" icon={<ArrowLeft className="size-3" />} disabled={index <= 0} onClick={() => onMove(-1)} />
|
||||
<Button size="small" className="!h-6 !w-6 !min-w-6 !rounded-full !bg-white/85 !p-0 !shadow-sm" icon={<ArrowRight className="size-3" />} disabled={index >= total - 1} onClick={() => onMove(1)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeLogConfig(log: Partial<GenerationLog>): GenerationLogConfig {
|
||||
return {
|
||||
model: log.config?.model || log.model || "",
|
||||
|
||||
Reference in New Issue
Block a user