添加seedance2.5

This commit is contained in:
Azmat@qq.com
2026-08-28 16:03:23 +08:00
parent c0a0b9603b
commit a618f02315
16 changed files with 416 additions and 80 deletions
@@ -9,17 +9,44 @@ export type LocalRef = FreeVideoRef & {
uploading?: boolean; // 上传中(url 是 blob: 预览,禁止提交)
};
// 只提供好看的标签/副标题;能出现哪些模型由后端返回的 modelConfigs 决定(见 free-create.tsx videoConfigs)。
// 新模型上线只要后端有数据就会出现在下拉里,不再需要发一次前端。
export const FC_MODELS = [
{ name: "doubao-seedance-2-5-260628", label: "Seedance 2.5", desc: "最长 30 秒 · 支持 1080P / 4K" },
{ name: "doubao-seedance-2-0-260128", label: "Seedance 2.0", desc: "标准档 · 支持 1080P / 4K" },
{ name: "doubao-seedance-2-0-fast-260128", label: "Seedance 2.0 Fast", desc: "更快出片 · 480P / 720P" },
{ name: "doubao-seedance-2-0-mini-260615", label: "Seedance 2.0 Mini", desc: "轻量便宜 · 480P / 720P" }
] as const;
export const FC_STANDARD_MODEL = FC_MODELS[0].name;
// 默认选中的模型。按名字写死,不跟 FC_MODELS 的排序走 —— 调换顺序不该改默认档。
export const FC_STANDARD_MODEL = "doubao-seedance-2-0-260128";
export const FC_RATIOS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"] as const;
export const FC_RESOLUTIONS = ["480p", "720p", "1080p", "4k"] as const;
export const FC_DURATIONS = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as const;
type ModelCapabilities = { resolutions?: string[]; durations?: number[] };
function capabilitiesOf(config: ModelConfig | undefined): ModelCapabilities {
const meta = (config?.metadata || {}) as Record<string, unknown>;
const caps = (meta.capabilities || {}) as ModelCapabilities;
return {
resolutions: caps.resolutions || (meta.resolutions as string[] | undefined),
durations: caps.durations || (meta.durations as number[] | undefined)
};
}
/** 该模型支持的分辨率档。后端没配就回落全集(由后端提交校验兜底)。 */
export function modelResolutions(config: ModelConfig | undefined): string[] {
const list = capabilitiesOf(config).resolutions;
return list?.length ? list : [...FC_RESOLUTIONS];
}
/** 该模型支持的出片时长。Seedance 2.5 能到 30 秒,2.0 只有 15 —— 写死会把新模型卡住。 */
export function modelDurations(config: ModelConfig | undefined): number[] {
const list = capabilitiesOf(config).durations;
return list?.length ? list : [...FC_DURATIONS];
}
export const MODE_LABELS: Record<FreeMode, string> = { universal: "全能参考", keyframe: "首尾帧" };
// 上传素材限制(与后端 FreeVideoUploadView / jimeng inputBar 对齐)
@@ -1,17 +1,19 @@
// 自由创作·输入条工具栏:模型/模式/比例/分辨率/时长/种子 下拉 + 预估消耗 + 清空 + 生成。
// 约束联动(与后端校验一致):1080P/4K 仅标准档;切非标准档时分辨率自动回落 720P
// 模型下拉列后端返回的全部视频模型(FC_MODELS 只提供好看的标签)
// 约束联动(与后端校验一致):分辨率与时长档位取自所选模型的 metadata.capabilities,
// 换档时把超出新模型能力的选择夹回去。
import { useEffect, useRef, useState } from "react";
import { ArrowRight, ChevronDown } from "lucide-react";
import type { ModelConfig } from "../../types";
import {
DEFAULT_BILLING_RATES,
FC_DURATIONS,
FC_MODELS,
FC_RATIOS,
FC_RESOLUTIONS,
FC_STANDARD_MODEL,
MODE_LABELS,
estimateCost,
modelDurations,
modelResolutions,
type BillingRates,
type FreeMode,
type LocalRef
@@ -28,6 +30,7 @@ function FcDropdown({ label, display, items, onSelect, disabled }: {
}) {
const [open, setOpen] = useState(false);
const wrapRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const onDown = (event: MouseEvent) => {
@@ -38,6 +41,15 @@ function FcDropdown({ label, display, items, onSelect, disabled }: {
document.addEventListener("keydown", onKey);
return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); };
}, [open]);
// 菜单封了高会滚动(时长有 27 档),打开时默认停在顶部会看不到当前选中项。
// 直接改菜单的 scrollTop,不用 scrollIntoView —— 后者会连带把整个页面滚一下。
useEffect(() => {
if (!open) return;
const menu = menuRef.current;
const active = menu?.querySelector<HTMLElement>(".fc-dd-item.selected");
if (!menu || !active) return;
menu.scrollTop = active.offsetTop - menu.clientHeight / 2 + active.offsetHeight / 2;
}, [open]);
return (
<div className={`fc-dd${open ? " open" : ""}`} ref={wrapRef}>
<button type="button" className="fc-chip" disabled={disabled} onClick={() => setOpen((v) => !v)} title={label}>
@@ -45,7 +57,7 @@ function FcDropdown({ label, display, items, onSelect, disabled }: {
<ChevronDown />
</button>
{open && (
<div className="fc-dd-menu">
<div className="fc-dd-menu" ref={menuRef}>
{items.map((item) => (
<button
key={item.value}
@@ -86,8 +98,10 @@ export function FreeToolbar({ mode, model, ratio, resolution, duration, seed, re
onClear: () => void;
onSend: () => void;
}) {
const isStandard = model === FC_STANDARD_MODEL;
const config = videoConfigs.find((c) => c.name === model);
// 可选的分辨率/时长按所选模型的能力来,不再写死 —— Seedance 2.5 能出 30 秒,2.0 只有 15。
const allowedResolutions = modelResolutions(config);
const allowedDurations = modelDurations(config);
const { tokens, points } = estimateCost(config, { ratio, resolution, duration, refs }, billingRates || DEFAULT_BILLING_RATES);
const [seedOpen, setSeedOpen] = useState(false);
const seedRef = useRef<HTMLDivElement>(null);
@@ -106,11 +120,23 @@ export function FreeToolbar({ mode, model, ratio, resolution, duration, seed, re
<div className="fc-chips">
<FcDropdown
label="模型"
display={FC_MODELS.find((m) => m.name === model)?.label || model}
items={FC_MODELS.map((m) => ({ value: m.name, label: m.label, desc: m.desc }))}
display={FC_MODELS.find((m) => m.name === model)?.label || config?.display_name || model}
items={videoConfigs.map((c) => {
const known = FC_MODELS.find((m) => m.name === c.name);
return { value: c.name, label: known?.label || c.display_name || c.name, desc: known?.desc };
})}
onSelect={(value) => {
onModelChange(value);
if (value !== FC_STANDARD_MODEL && (resolution === "1080p" || resolution === "4k")) onResolutionChange("720p");
// 换档后把分辨率/时长夹回新模型支持的范围,否则会带着上一档的选择提交然后被后端拒
const next = videoConfigs.find((c) => c.name === value);
const resolutions = modelResolutions(next);
if (!resolutions.includes(resolution)) {
onResolutionChange(resolutions.includes("720p") ? "720p" : resolutions[0]);
}
const durations = modelDurations(next);
if (!durations.includes(duration)) {
onDurationChange(durations.includes(5) ? 5 : durations[0]);
}
}}
/>
<FcDropdown
@@ -129,15 +155,15 @@ export function FreeToolbar({ mode, model, ratio, resolution, duration, seed, re
items={FC_RESOLUTIONS.map((r) => ({
value: r,
label: r.toUpperCase(),
disabled: (r === "1080p" || r === "4k") && !isStandard,
hint: "仅 Seedance 2.0 标准档支持"
disabled: !allowedResolutions.includes(r),
hint: "当前模型不支持这一档"
}))}
onSelect={onResolutionChange}
/>
<FcDropdown
label="时长"
display={`${duration}s`}
items={FC_DURATIONS.map((d) => ({ value: String(d), label: `${d}s` }))}
items={allowedDurations.map((d) => ({ value: String(d), label: `${d}s` }))}
onSelect={(value) => onDurationChange(Number(value))}
/>
<div className={`fc-dd${seedOpen ? " open" : ""}`} ref={seedRef}>