Files
infinite-canvas/web/src/lib/seedance-video.ts
T

170 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { modelOptionName, resolveModelRequestConfig, type AiConfig } from "@/stores/use-config-store";
import type { ReferenceImage } from "@/types/image";
import type { ReferenceAudio, ReferenceVideo } from "@/types/media";
export const SEEDANCE_REFERENCE_LIMITS = {
images: 9,
videos: 3,
audios: 3,
imageMaxBytes: 30 * 1024 * 1024,
videoMaxBytes: 50 * 1024 * 1024,
audioMaxBytes: 15 * 1024 * 1024,
};
export const seedanceResolutionOptions = [
{ value: "480p", label: "480p" },
{ value: "720p", label: "720p" },
{ value: "1080p", label: "1080p" },
] as const;
export const seedanceRatioOptions = [
{ value: "16:9", label: "横屏" },
{ value: "9:16", label: "竖屏" },
{ value: "1:1", label: "方形" },
{ value: "4:3", label: "标准横屏" },
{ value: "3:4", label: "标准竖屏" },
{ value: "21:9", label: "宽银幕" },
{ value: "adaptive", label: "自适应" },
] as const;
export const seedanceDurationOptions = [-1, 4, 5, 6, 8, 10, 12, 15] as const;
const seedancePixels = {
"480p": {
"16:9": "864x496",
"4:3": "752x560",
"1:1": "640x640",
"3:4": "560x752",
"9:16": "496x864",
"21:9": "992x432",
},
"720p": {
"16:9": "1280x720",
"4:3": "1112x834",
"1:1": "960x960",
"3:4": "834x1112",
"9:16": "720x1280",
"21:9": "1470x630",
},
"1080p": {
"16:9": "1920x1080",
"4:3": "1664x1248",
"1:1": "1440x1440",
"3:4": "1248x1664",
"9:16": "1080x1920",
"21:9": "2206x946",
},
} as const;
export function isSeedanceVideoConfig(config: AiConfig | Pick<AiConfig, "model" | "videoModel" | "baseUrl">) {
const requestConfig = "channels" in config ? resolveModelRequestConfig(config, config.model || config.videoModel) : config;
return isSeedanceVideoModel(modelOptionName(requestConfig.model || requestConfig.videoModel)) || isArkPlanBaseUrl(requestConfig.baseUrl);
}
export function isSeedanceVideoModel(model: string) {
const value = model.toLowerCase();
return value.includes("seedance") || value.includes("doubao-seedance");
}
export function isSeedanceFastModel(model: string) {
const value = model.toLowerCase();
return isSeedanceVideoModel(value) && value.includes("fast");
}
export function isArkPlanBaseUrl(baseUrl: string) {
return baseUrl.toLowerCase().includes("ark.cn-beijing.volces.com/api/plan/v3") || baseUrl.toLowerCase().includes("/api/plan/v3");
}
export function normalizeSeedanceResolution(value: string, model = "") {
const normalized = normalizeResolutionToken(value);
if (isSeedanceFastModel(model) && normalized === "1080p") return "720p";
return seedanceResolutionOptions.some((item) => item.value === normalized) ? normalized : "720p";
}
export function normalizeResolutionToken(value: string) {
if (value === "low") return "480p";
if (value === "auto" || value === "high" || value === "medium") return "720p";
const resolution = String(value || "").replace(/p$/i, "") || "720";
return `${resolution}p`;
}
export function normalizeSeedanceDuration(value: string) {
if (String(value).trim() === "-1") return -1;
const seconds = Math.floor(Number(value) || 5);
return Math.max(4, Math.min(15, seconds));
}
export function normalizeSeedanceRatio(value: string) {
if (!value || value === "auto" || value === "adaptive") return "adaptive";
if (seedanceRatioOptions.some((item) => item.value === value)) return value;
const match = value.match(/^(\d+)x(\d+)$/);
if (!match) return "adaptive";
const width = Number(match[1]);
const height = Number(match[2]);
if (!width || !height) return "adaptive";
const ratio = width / height;
const options = [
["16:9", 16 / 9],
["4:3", 4 / 3],
["1:1", 1],
["3:4", 3 / 4],
["9:16", 9 / 16],
["21:9", 21 / 9],
] as const;
return options.reduce((best, item) => (Math.abs(item[1] - ratio) < Math.abs(best[1] - ratio) ? item : best), options[0])[0];
}
export function seedancePixelLabel(resolution: string, ratio: string) {
const normalizedResolution = normalizeSeedanceResolution(resolution) as keyof typeof seedancePixels;
const normalizedRatio = normalizeSeedanceRatio(ratio) as keyof (typeof seedancePixels)[typeof normalizedResolution] | "adaptive";
if (normalizedRatio === "adaptive") return "自动匹配";
return seedancePixels[normalizedResolution][normalizedRatio] || "";
}
export function boolConfig(value: string | undefined, fallback: boolean) {
if (value === "true") return true;
if (value === "false") return false;
return fallback;
}
export function seedanceReferenceLabel(kind: "image" | "video" | "audio", index: number) {
if (kind === "image") return `图片${index + 1}`;
if (kind === "video") return `视频${index + 1}`;
return `音频${index + 1}`;
}
export function buildSeedancePromptText(prompt: string, images: ReferenceImage[], videos: ReferenceVideo[], audios: ReferenceAudio[]) {
const labels = [
...images.map((_, index) => seedanceReferenceLabel("image", index)),
...videos.map((_, index) => seedanceReferenceLabel("video", index)),
...audios.map((_, index) => seedanceReferenceLabel("audio", index)),
];
const text = prompt.trim();
if (!labels.length) return text;
return `参考资产编号:${labels.join("、")}。请按这些编号理解提示词中的图片、视频和音频引用。\n\n${text}`;
}
export function seedanceVideoReferenceError(videos: ReferenceVideo[]) {
let totalDurationMs = 0;
for (let index = 0; index < videos.length; index += 1) {
const video = videos[index];
const label = seedanceReferenceLabel("video", index);
if (video.bytes && video.bytes > SEEDANCE_REFERENCE_LIMITS.videoMaxBytes) return `${label} 超过 50MB,请压缩后再上传`;
if (video.durationMs) {
if (video.durationMs < 2000 || video.durationMs > 15000) return `${label} 时长需要在 2-15 秒之间`;
totalDurationMs += video.durationMs;
}
if (video.width && video.height) {
if (video.width < 300 || video.width > 6000 || video.height < 300 || video.height > 6000) return `${label} 宽高需要在 300-6000px 之间`;
const ratio = video.width / video.height;
if (ratio < 0.4 || ratio > 2.5) return `${label} 宽高比需要在 0.4-2.5 之间`;
const pixels = video.width * video.height;
if (pixels < 640 * 640 || pixels > 2206 * 946) return `${label} 像素总量不符合 Seedance 要求,请转成 480p/720p/1080p 后再上传`;
}
}
if (totalDurationMs > 15000) return "Seedance 参考视频总时长不能超过 15 秒";
return "";
}
export const seedanceVideoReferenceHint = "参考视频需为 mp4/movH.264/H.265FPS 24-60;含真人人脸资产请使用火山授权 asset:// 资产。";