补
This commit is contained in:
@@ -12,7 +12,7 @@ export const OMNI_RESOLUTIONS = ["1080p", "720p", "480p"];
|
||||
export const OMNI_RATIOS = ["9:16", "16:9", "1:1", "4:3", "3:4"];
|
||||
export const OMNI_VIDEO_DURATIONS = [
|
||||
"智能时长", "4 秒", "5 秒", "6 秒", "7 秒", "8 秒", "9 秒", "10 秒",
|
||||
"11 秒", "12 秒", "13 秒", "14 秒", "15 秒", "30 秒",
|
||||
"11 秒", "12 秒", "13 秒", "14 秒", "15 秒", "30 秒", "45 秒", "60 秒",
|
||||
];
|
||||
export const OMNI_IMAGE_COUNTS = ["1 张", "2 张", "4 张", "8 张"];
|
||||
|
||||
@@ -70,11 +70,21 @@ function catalogModelLabels(
|
||||
return unique.length ? unique : fallback;
|
||||
}
|
||||
|
||||
function durationLabelsForModel(config: ModelConfig | undefined, isVideo: boolean): string[] {
|
||||
function canCreateSegmentedVideo(config: ModelConfig | undefined, model: string): boolean {
|
||||
// 31–60 秒不是单次 API 时长,而是平台会自动拆成 <=30 秒的两段。
|
||||
// 只有 Seedance 2.5(或目录里声明了 30 秒能力的模型)可选这个总时长。
|
||||
return modelDurations(config).some((seconds) => seconds >= 30)
|
||||
|| /seedance\s*2\.5/i.test(model || "");
|
||||
}
|
||||
|
||||
function durationLabelsForModel(config: ModelConfig | undefined, model: string, isVideo: boolean): string[] {
|
||||
if (!isVideo) return [...OMNI_IMAGE_COUNTS];
|
||||
const seconds = modelDurations(config);
|
||||
if (!seconds.length) return [...OMNI_VIDEO_DURATIONS];
|
||||
return ["智能时长", ...seconds.map((n) => `${n} 秒`)];
|
||||
const segmented = canCreateSegmentedVideo(config, model) ? [45, 60] : [];
|
||||
return ["智能时长", ...seconds, ...segmented]
|
||||
.filter((seconds, index, values) => values.indexOf(seconds) === index)
|
||||
.map((seconds) => typeof seconds === "number" ? `${seconds} 秒` : seconds);
|
||||
}
|
||||
|
||||
export function OmniParamBar({
|
||||
@@ -120,7 +130,7 @@ export function OmniParamBar({
|
||||
);
|
||||
const allowedRes = isVideo ? modelResolutions(selected) : [];
|
||||
const resolutions = withCurrent(allowedRes.length ? allowedRes : OMNI_RESOLUTIONS, resolution);
|
||||
const durations = withCurrent(durationLabelsForModel(selected, isVideo), duration);
|
||||
const durations = withCurrent(durationLabelsForModel(selected, model, isVideo), duration);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomOn(isVideo ? duration !== "智能时长" : true);
|
||||
@@ -173,12 +183,15 @@ export function OmniParamBar({
|
||||
if (nextRes.length && resolution && !nextRes.includes(resolution)) {
|
||||
onResolution(nextRes.includes("720p") ? "720p" : nextRes[0]);
|
||||
}
|
||||
const nextDur = durationLabelsForModel(selected, true);
|
||||
if (duration && duration !== "智能时长" && !nextDur.includes(duration)) {
|
||||
const nextDur = durationLabelsForModel(selected, model, true);
|
||||
const seconds = Number(String(duration || "").replace(/\D/g, ""));
|
||||
const isPlannedSegmentedDuration = seconds > 30 && seconds <= 60 && canCreateSegmentedVideo(selected, model);
|
||||
// 60 秒是总时长,生成时会拆段;不要因为单段目录最高 30 秒就把它偷偷改回 8 秒/智能时长。
|
||||
if (duration && duration !== "智能时长" && !nextDur.includes(duration) && !isPlannedSegmentedDuration) {
|
||||
onDuration(nextDur.includes("8 秒") ? "8 秒" : (nextDur.find((x) => x !== "智能时长") || "智能时长"));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selected?.id, isVideo]);
|
||||
}, [selected?.id, isVideo, model]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1237,6 +1237,10 @@
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.omni-result-media.is-grid .omni-process-frame {
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.omni-result-play {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
@@ -1488,6 +1488,8 @@ function ConfirmCard({
|
||||
const cardIsVideo = message.payload.kind !== "image" && isVideo;
|
||||
const durationChanged =
|
||||
cardIsVideo && Boolean(draft.duration) && Boolean(snapshot.duration) && draft.duration !== snapshot.duration;
|
||||
const durationSeconds = Number(String(draft.duration || "").replace(/\D/g, ""));
|
||||
const willGenerateInSegments = cardIsVideo && durationSeconds > 30 && durationSeconds <= 60;
|
||||
const setField = (key: string, value: string) => setDraft((prev) => ({ ...prev, [key]: value }));
|
||||
const summary = paramLine(draft, cardIsVideo) || "当前参数";
|
||||
// 确认卡积分:改模型/分辨率/时长/张数时按后台挂牌实时重算(与后端 quote_* 同口径;标准团队系数=1)
|
||||
@@ -1537,6 +1539,8 @@ function ConfirmCard({
|
||||
)}
|
||||
{durationChanged ? (
|
||||
<p className="omni-confirm-hint">改时长会重新生成脚本。确认后先重写方案,不会直接出片。</p>
|
||||
) : willGenerateInSegments ? (
|
||||
<p className="omni-confirm-hint">{durationSeconds} 秒将拆成 2 段生成,每段最长 30 秒;片段完成后先给你预览,再由你决定是否合并成片。</p>
|
||||
) : null}
|
||||
<div className="omni-confirm-foot">
|
||||
<span>{submitted ? "已确认,正在出片" : "点确认后才会提交生成"}</span>
|
||||
@@ -1917,25 +1921,72 @@ function withoutLegacyGateArtifacts(list: CreationMessage[]): CreationMessage[]
|
||||
return hidden.size ? list.filter((message) => !hidden.has(message.id)) : list;
|
||||
}
|
||||
|
||||
function ProcessCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
function ProcessCard({
|
||||
payload,
|
||||
onPreview,
|
||||
}: {
|
||||
payload: Record<string, unknown>;
|
||||
onPreview?: (src: string, kind: "image" | "video", name: string) => void;
|
||||
}) {
|
||||
const isSegmentedVideo = payload.kind === "video_segments";
|
||||
const isMerge = payload.kind === "video_merge";
|
||||
const isVideo = payload.kind === "video" || isSegmentedVideo || isMerge;
|
||||
const segmentCount = Array.isArray(payload.segments) ? payload.segments.length : 0;
|
||||
const assets = (payload.assets as Array<Record<string, string>> | undefined) || [];
|
||||
const completedSegments = Number(payload.completed_segment_count || 0);
|
||||
const waitingSegments = isSegmentedVideo ? Math.max(1, segmentCount - completedSegments) : 1;
|
||||
return (
|
||||
<section className="omni-result-card omni-process-card">
|
||||
<div className="omni-result-media">
|
||||
<figure className="omni-result-tile">
|
||||
<div className="omni-process-frame" aria-hidden="true">
|
||||
<span className="omni-process-ring" />
|
||||
<strong>{isVideo ? "视频生成中" : "图片生成中"}</strong>
|
||||
</div>
|
||||
</figure>
|
||||
<div className={`omni-result-media${isSegmentedVideo ? " is-grid" : ""}`}>
|
||||
{assets.map((asset, index) => {
|
||||
const cover = asset.cover || asset.url || "";
|
||||
const url = asset.url || cover;
|
||||
const video = asset.type === "video";
|
||||
const label = String(asset.label || `第 ${index + 1} 段`);
|
||||
return (
|
||||
<figure className="omni-result-tile" key={asset.id || `${url}-${index}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="omni-result-preview"
|
||||
onClick={() => url && onPreview?.(url, video ? "video" : "image", label)}
|
||||
>
|
||||
<img src={cover} alt={label} />
|
||||
{video ? (
|
||||
<span className="omni-result-play" aria-hidden="true">
|
||||
<Play />
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
</figure>
|
||||
);
|
||||
})}
|
||||
{Array.from({ length: waitingSegments }).map((_, index) => (
|
||||
<figure className="omni-result-tile" key={`generating-${index}`}>
|
||||
<div className="omni-process-frame" aria-hidden="true">
|
||||
<span className="omni-process-ring" />
|
||||
<strong>{isVideo ? "视频生成中" : "图片生成中"}</strong>
|
||||
</div>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
<div className="omni-result-info">
|
||||
<div>
|
||||
<strong>{isMerge ? "正在合并成片" : isSegmentedVideo ? `正在生成 ${segmentCount || 2} 段视频` : isVideo ? "正在生成视频" : "正在出图"}</strong>
|
||||
<small>{isMerge ? "正在拼接已确认的片段" : isSegmentedVideo ? "片段完成后会先展示给你确认" : isVideo ? "方案编译中,请稍候" : "画面生成中,请稍候"}</small>
|
||||
<strong>{
|
||||
isMerge
|
||||
? "正在合并成片"
|
||||
: isSegmentedVideo && completedSegments
|
||||
? `第 ${completedSegments} 段已生成,正在生成第 ${completedSegments + 1} 段`
|
||||
: isSegmentedVideo
|
||||
? `正在生成 ${segmentCount || 2} 段视频`
|
||||
: isVideo ? "正在生成视频" : "正在出图"
|
||||
}</strong>
|
||||
<small>{
|
||||
isMerge
|
||||
? "正在拼接已确认的片段"
|
||||
: isSegmentedVideo && completedSegments
|
||||
? "已生成的片段可以先点击预览"
|
||||
: isSegmentedVideo ? "片段完成后会先展示给你确认" : isVideo ? "方案编译中,请稍候" : "画面生成中,请稍候"
|
||||
}</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -2772,7 +2823,13 @@ export function OmniSessionPage({
|
||||
);
|
||||
}
|
||||
case "generating":
|
||||
return <ProcessCard key={message.clientKey || message.id} payload={message.payload} />;
|
||||
return (
|
||||
<ProcessCard
|
||||
key={message.clientKey || message.id}
|
||||
payload={message.payload}
|
||||
onPreview={(src, kind, name) => setAssetPreview({ src, kind, name })}
|
||||
/>
|
||||
);
|
||||
case "result":
|
||||
return (
|
||||
<ResultCard
|
||||
|
||||
Reference in New Issue
Block a user