增加即梦图片5.0和视频2.5的能力

This commit is contained in:
andy committed 2026-08-21 13:44:56 +08:00
1 parent 542246e4ce
commit 88a4268d40
55 files changed
+1412 -200

No files matched your search

+21 -12
View File
@@ -1,9 +1,10 @@
import { VIDEO_DURATION_MAX } from "@/lib/video-settings";
import { SEEDANCE_25_MODEL, VIDEO_DURATION_MAX, isSeedance25Model } from "@/lib/video-settings";
const SEEDANCE_FPS = 24;
const SEEDANCE_TOKEN_SCALE = 1_000_000;
export type SeedanceBillingEstimateInput = {
model?: unknown;
resolution?: unknown;
aspectRatio?: unknown;
outputDurationSeconds: number;
@@ -13,6 +14,7 @@ export type SeedanceBillingEstimateInput = {
};
export type SeedanceActualBillingInput = {
model?: unknown;
resolution?: unknown;
inputVideo?: boolean;
completionTokens: number;
@@ -20,11 +22,17 @@ export type SeedanceActualBillingInput = {
};
/**
* 火山方舟文档的 Seedance 2.0 视频 token 单价,单位为“分/百万 token”。
* 480p/720p、1080p 和 4K 的单价不同;含输入视频时使用对应的输入视频档。
* 火山方舟文档的 Seedance 视频 token 单价,单位为“分/百万 token”。
* 每个模型、分辨率及是否包含输入视频都有独立价格。
*/
export function seedanceTokenPriceFenPerMillion(resolution: unknown, inputVideo = false): number {
export function seedanceTokenPriceFenPerMillion(model: unknown, resolution: unknown, inputVideo = false): number {
const normalized = normalizeSeedanceResolution(resolution);
if (String(model || "").trim() === SEEDANCE_25_MODEL) {
const prices = normalized === "1080p"
? { withoutVideo: 7_700, withVideo: 4_600 }
: { withoutVideo: 7_000, withVideo: 4_200 };
return inputVideo ? prices.withVideo : prices.withoutVideo;
}
const prices: Record<string, { withoutVideo: number; withVideo: number }> = {
"480p": { withoutVideo: 4_600, withVideo: 2_800 },
"720p": { withoutVideo: 4_600, withVideo: 2_800 },
@@ -42,7 +50,7 @@ export function calculateSeedanceActualAmountFen(input: SeedanceActualBillingInp
if (!Number.isFinite(input.markupMultiplier) || input.markupMultiplier < 1) {
throw new Error("Seedance 计费倍率不能低于 1.00。");
}
const tokenPriceFenPerMillion = seedanceTokenPriceFenPerMillion(input.resolution, input.inputVideo);
const tokenPriceFenPerMillion = seedanceTokenPriceFenPerMillion(input.model, input.resolution, input.inputVideo);
return Math.max(1, Math.ceil(
input.completionTokens * tokenPriceFenPerMillion * input.markupMultiplier / SEEDANCE_TOKEN_SCALE
));
@@ -54,7 +62,7 @@ export function estimateSeedanceAmountFen(input: SeedanceBillingEstimateInput):
}
const inputVideo = Boolean(input.inputVideo);
const inputDuration = inputVideo
? normalizeInputVideoDuration(input.inputVideoDurationSeconds)
? normalizeInputVideoDuration(input.inputVideoDurationSeconds, input.model)
: 0;
const completionTokens = estimateSeedanceCompletionTokens({
resolution: input.resolution,
@@ -63,7 +71,7 @@ export function estimateSeedanceAmountFen(input: SeedanceBillingEstimateInput):
inputVideoDurationSeconds: inputDuration
});
return Math.max(1, Math.ceil(
completionTokens * seedanceTokenPriceFenPerMillion(input.resolution, inputVideo) * input.markupMultiplier / SEEDANCE_TOKEN_SCALE
completionTokens * seedanceTokenPriceFenPerMillion(input.model, input.resolution, inputVideo) * input.markupMultiplier / SEEDANCE_TOKEN_SCALE
));
}
@@ -84,10 +92,11 @@ export function estimateSeedanceCompletionTokens(input: {
/**
* 没有媒体元数据时按官方可接收的最大输入视频时长冻结,避免先扣少、成功后补扣造成余额不足。
*/
export function normalizeInputVideoDuration(value: unknown): number {
export function normalizeInputVideoDuration(value: unknown, model?: unknown): number {
const duration = Number(value);
if (!Number.isFinite(duration) || duration <= 0) return VIDEO_DURATION_MAX;
return Math.min(VIDEO_DURATION_MAX, duration);
const maximum = isSeedance25Model(String(model || "")) ? 30 : VIDEO_DURATION_MAX;
if (!Number.isFinite(duration) || duration <= 0) return maximum;
return Math.min(maximum, isSeedance25Model(String(model || "")) ? Math.max(4, duration) : duration);
}
export function normalizeSeedanceResolution(value: unknown): string {
@@ -116,7 +125,7 @@ export function requestHasInputVideo(requestPayload: Record<string, unknown>): b
return requestMaterials(requestPayload).some((material) => String(material.type || "").toLowerCase() === "video");
}
export function requestInputVideoDurationSeconds(requestPayload: Record<string, unknown>): number | undefined {
export function requestInputVideoDurationSeconds(requestPayload: Record<string, unknown>, model?: unknown): number | undefined {
const videoMaterials = requestMaterials(requestPayload)
.filter((material) => String(material.type || "").toLowerCase() === "video");
if (!videoMaterials.length) return undefined;
@@ -129,7 +138,7 @@ export function requestInputVideoDurationSeconds(requestPayload: Record<string,
))
.filter((value): value is number => value !== undefined && value > 0);
if (!knownDurations.length) return undefined;
return Math.min(VIDEO_DURATION_MAX, knownDurations.reduce((sum, value) => sum + value, 0));
return normalizeInputVideoDuration(knownDurations.reduce((sum, value) => sum + value, 0), model);
}
function requestMaterials(requestPayload: Record<string, unknown>): Array<Record<string, unknown>> {