视频复刻积分规则优化
This commit is contained in:
@@ -78,6 +78,17 @@ export function pointsPerImageFromCatalog(config: ModelConfig | undefined): numb
|
||||
return Number.isFinite(unit) && unit > 0 ? unit : null;
|
||||
}
|
||||
|
||||
/** 文本/视觉次价:优先挂牌 points_per_call,否则 unit_price。 */
|
||||
export function pointsPerCallFromCatalog(config: ModelConfig | undefined): number | null {
|
||||
const pricing = (config?.metadata?.points_pricing || {}) as Record<string, unknown>;
|
||||
if (pricing.points_per_call != null) {
|
||||
const n = Number(pricing.points_per_call);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
const unit = Number(config?.unit_price);
|
||||
return Number.isFinite(unit) && unit > 0 ? unit : null;
|
||||
}
|
||||
|
||||
export const MODE_LABELS: Record<FreeMode, string> = { universal: "全能参考", keyframe: "首尾帧" };
|
||||
|
||||
// 上传素材限制(与后端 FreeVideoUploadView / jimeng inputBar 对齐)
|
||||
|
||||
@@ -29,7 +29,8 @@ import type { NavigateFn } from "./route-config";
|
||||
export const REMIX_PROMPT_KEY = "fc-remix-prompt";
|
||||
const REMIX_DRAFT_KEY = "vr-digest-draft";
|
||||
const JOB_KEY = "airshelf:video-remix-job";
|
||||
const VIDEO_DIGEST_POINTS = 30;
|
||||
/** 未挂牌时的回落价(与后端 VIDEO_DIGEST_POINTS 一致)。 */
|
||||
const VIDEO_DIGEST_POINTS_FALLBACK = 30;
|
||||
|
||||
type ProgressStage = "upload" | "analyze" | "prompt";
|
||||
|
||||
@@ -190,9 +191,17 @@ export function VideoRemixPage({ textModels = [], onNotify, onBack, navigate }:
|
||||
useEffect(() => {
|
||||
void api.billingConfig().then((cfg) => setPriceMultiplier(Number(cfg.team_price_multiplier) || 1)).catch(() => undefined);
|
||||
}, []);
|
||||
// 与后端一致:只认挂牌 points_per_call,不吃 unit_price;未挂牌回落 30
|
||||
const hangpaiCall = (() => {
|
||||
const raw = (activeModel?.metadata?.points_pricing as { points_per_call?: unknown } | undefined)?.points_per_call;
|
||||
if (raw == null) return null;
|
||||
const n = Number(raw);
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
})();
|
||||
const listedPoints = hangpaiCall ?? VIDEO_DIGEST_POINTS_FALLBACK;
|
||||
const estimatedPoints = priceMultiplier === 1
|
||||
? VIDEO_DIGEST_POINTS
|
||||
: Math.max(1, Math.round(Number((VIDEO_DIGEST_POINTS * priceMultiplier).toFixed(6))));
|
||||
? listedPoints
|
||||
: Math.max(1, Math.round(Number((listedPoints * priceMultiplier).toFixed(6))));
|
||||
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
|
||||
@@ -39,6 +39,9 @@ const CHARACTER_MARK = "[视频复刻·角色]";
|
||||
const VIDEO_ACCEPT = "video/mp4,video/quicktime";
|
||||
// 复刻固定 Seedance 2.5:参考视频只用来提炼分镜稿,成片最长 30 秒。
|
||||
const REPLACE_SEEDANCE_MODEL = "doubao-seedance-2-5-260628";
|
||||
/** 与后端 DIGEST_VISION_MODEL_NAME 一致:商品复刻拆镜用 Gemini。 */
|
||||
const DIGEST_GEMINI_MODEL = "gemini-3.1-pro-preview";
|
||||
const DIGEST_POINTS_FALLBACK = 30;
|
||||
const PRODUCT_SOURCE_PURPOSE = "video_replace_product";
|
||||
const REF_SECONDS_MAX = { product: 30, character: 30 } as const;
|
||||
const REF_BYTES_MAX = { product: 200 * 1024 * 1024, character: 200 * 1024 * 1024 } as const;
|
||||
@@ -399,6 +402,14 @@ export function VideoReplacePage({
|
||||
() => videoConfigs.find((config) => config.name === REPLACE_SEEDANCE_MODEL) || videoConfigs[0],
|
||||
[videoConfigs],
|
||||
);
|
||||
const digestModel = useMemo(() => {
|
||||
const texts = modelConfigs.filter((c) => c.capability === "text" && c.status === "active");
|
||||
return (
|
||||
texts.find((c) => c.name === DIGEST_GEMINI_MODEL)
|
||||
|| texts.find((c) => c.name.includes("gemini-3.1") || (c.display_name || "").includes("Gemini 3.1"))
|
||||
|| null
|
||||
);
|
||||
}, [modelConfigs]);
|
||||
|
||||
const copy = REPLACE_MODE_COPY[replaceMode];
|
||||
const videoReady = Boolean(videoRef?.asset_id);
|
||||
@@ -430,18 +441,33 @@ export function VideoReplacePage({
|
||||
}));
|
||||
const aspectRatio = ratioFromSize(videoMeta.width, videoMeta.height);
|
||||
const outputDuration = clampDuration(videoMeta.duration || SEEDANCE_MAX_OUTPUT_SECONDS);
|
||||
// 上传前不显示积分;上传后 = Gemini 提炼挂牌(仅商品复刻) + Seedance 2.5 720P × 时长
|
||||
// 商品复刻:原片只提炼、不进 Seedance → 普通秒价;角色复刻:原片作引用视频 → 含引用秒价
|
||||
const digestHangpai = (() => {
|
||||
if (replaceMode !== "product") return 0;
|
||||
const pricing = (digestModel?.metadata?.points_pricing as { points_per_call?: unknown } | undefined)?.points_per_call;
|
||||
if (pricing != null) {
|
||||
const n = Number(pricing);
|
||||
if (Number.isFinite(n) && n > 0) return n;
|
||||
}
|
||||
return DIGEST_POINTS_FALLBACK;
|
||||
})();
|
||||
const imageRefCount = source === "library"
|
||||
? Math.max(1, libraryImageCount)
|
||||
: Math.max(0, (tempFiles.length ? tempFiles : tempAssetRefs).length);
|
||||
const seedanceRefs: { type: string }[] = [
|
||||
...(replaceMode === "character" && videoReady ? [{ type: "video" as const }] : []),
|
||||
...Array.from({ length: imageRefCount }, () => ({ type: "image" as const })),
|
||||
];
|
||||
const estimated = estimateCost(preferredModel, {
|
||||
ratio: aspectRatio,
|
||||
resolution: "720p",
|
||||
duration: outputDuration,
|
||||
refs: [
|
||||
...((source === "library"
|
||||
? Array.from({ length: Math.max(1, libraryImageCount) }, () => ({ type: "image" as const }))
|
||||
: (tempFiles.length ? tempFiles : tempAssetRefs)
|
||||
).map(() => ({ type: "image" }))),
|
||||
],
|
||||
refs: seedanceRefs,
|
||||
}, billingRates);
|
||||
const points = estimated.points;
|
||||
const points = videoReady
|
||||
? (estimated.points + (replaceMode === "product" ? digestHangpai : 0))
|
||||
: 0;
|
||||
// 上传参考视频不算「正在复刻」:右侧面板要保持待命,只在上传区自己显示进度。
|
||||
// 生成按钮不会因此被误点 —— videoReady 要等 asset_id 回来才为真。
|
||||
const generating = Boolean(job && isInFlight(job.status)) || submitting;
|
||||
|
||||
Reference in New Issue
Block a user