一键成片优化
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
estimateCost,
|
||||
FC_MODELS,
|
||||
modelLabel,
|
||||
pointsPerCallFromCatalog,
|
||||
pointsPerImageFromCatalog,
|
||||
type BillingRates,
|
||||
} from "../components/free-create/constants";
|
||||
@@ -60,6 +61,10 @@ const QUICK_RESOLUTIONS = [
|
||||
{ value: "4k", label: "4K 超清" },
|
||||
];
|
||||
const QUICK_DURATIONS = [15, 30, 45, 60];
|
||||
/** 与后端 QUICK_SCRIPT_MODEL_NAME 一致 */
|
||||
const QUICK_SCRIPT_MODEL = "doubao-seed-2-1-pro-260628";
|
||||
const QUICK_SCRIPT_POINTS_FALLBACK = 100;
|
||||
const QUICK_IMAGE_POINTS_FALLBACK = 20;
|
||||
|
||||
function modelResolutions(config: ModelConfig | undefined) {
|
||||
const capabilities = (config?.metadata?.capabilities || {}) as Record<string, unknown>;
|
||||
@@ -413,6 +418,24 @@ export function QuickCreatePage({
|
||||
onNotify?.("info", "请选择商品品类,脚本会按品类来写");
|
||||
return;
|
||||
}
|
||||
// 与后端门禁同口径:可用积分不够预估总额就不提交。
|
||||
if (estimatedPoints > 0) {
|
||||
try {
|
||||
const summary = await api.billingSummary();
|
||||
const balance = Number(summary.account?.balance || 0);
|
||||
const reserved = Number(summary.account?.reserved_balance || 0);
|
||||
const available = balance - reserved;
|
||||
if (available < estimatedPoints) {
|
||||
onNotify?.(
|
||||
"info",
|
||||
`积分不足:预计需要 ${estimatedPoints} 积分,当前可用 ${Math.max(0, Math.floor(available))} 积分,请充值后再开始`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* 余额接口失败时仍交给后端硬拦 */
|
||||
}
|
||||
}
|
||||
setSubmitting(true);
|
||||
setJob(null);
|
||||
setServiceUnavailable(false);
|
||||
@@ -465,6 +488,8 @@ export function QuickCreatePage({
|
||||
setServiceUnavailable(true);
|
||||
setUnavailableMessage(error.message || "一键成片服务暂不可用,请稍后再试");
|
||||
onNotify?.("info", error.message || "一键成片服务暂不可用,请稍后再试");
|
||||
} else if (error instanceof ApiError && error.status === 400 && /余额不足|积分不足/.test(error.message || "")) {
|
||||
onNotify?.("info", error.message);
|
||||
} else {
|
||||
onNotify?.("error", error instanceof Error ? error.message : "一键成片启动失败");
|
||||
}
|
||||
@@ -572,14 +597,31 @@ export function QuickCreatePage({
|
||||
{ ratio: aspectRatio, resolution, duration: totalDuration, refs: [] },
|
||||
billingRates,
|
||||
);
|
||||
// 基础资产按图像挂牌预估;视频按挂牌秒价。最终按成功任务实扣。
|
||||
// 与实扣对齐:脚本(Seed 2.1 Pro 挂牌) + 默认出图挂牌×场次粗估 + 视频挂牌秒价×时长
|
||||
const applyTeam = (raw: number) => {
|
||||
const multiplier = billingRates.multiplier || 1;
|
||||
if (!(raw > 0)) return 0;
|
||||
return multiplier === 1 ? raw : Math.max(1, Math.round(Number((raw * multiplier).toFixed(6))));
|
||||
};
|
||||
const scriptUnit = (() => {
|
||||
const texts = modelConfigs.filter((m) => m.capability === "text" && m.status === "active");
|
||||
const script = texts.find((m) => m.name === QUICK_SCRIPT_MODEL) || texts.find((m) => m.is_default) || texts[0];
|
||||
const listed = pointsPerCallFromCatalog(script);
|
||||
return listed != null && listed > 0 ? listed : QUICK_SCRIPT_POINTS_FALLBACK;
|
||||
})();
|
||||
const imageUnit = (() => {
|
||||
const img = modelConfigs.find((m) => m.capability === "image" && m.status === "active")
|
||||
const images = modelConfigs.filter((m) => m.capability === "image" && m.status === "active");
|
||||
// 与后端 get_default_model(IMAGE) 一致:优先 is_default,再回落影擎-Image2 名字
|
||||
const img = images.find((m) => m.is_default)
|
||||
|| images.find((m) => m.name === "gpt-image-2")
|
||||
|| images[0]
|
||||
|| modelConfigs.find((m) => m.capability === "image");
|
||||
const listed = pointsPerImageFromCatalog(img);
|
||||
return listed != null && listed > 0 ? listed : 20;
|
||||
return listed != null && listed > 0 ? listed : QUICK_IMAGE_POINTS_FALLBACK;
|
||||
})();
|
||||
const estimatedPoints = videoEstimate.listed ? videoEstimate.points + sceneCount * imageUnit : 0;
|
||||
const estimatedPoints = videoEstimate.listed
|
||||
? videoEstimate.points + applyTeam(scriptUnit) + applyTeam(sceneCount * imageUnit)
|
||||
: 0;
|
||||
const shellClass = [
|
||||
"quick-create-shell",
|
||||
restoring ? "is-restoring" : "",
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
UserRound,
|
||||
Users,
|
||||
Wallet,
|
||||
WandSparkles
|
||||
WandSparkles,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export type Page =
|
||||
@@ -79,7 +80,7 @@ export type NavigateOptions = {
|
||||
};
|
||||
export type NavigateFn = (page: Page, options?: NavigateOptions) => void;
|
||||
export type Notice = { type: "success" | "error" | "info"; text: string } | null;
|
||||
export type NavItem = { page: Page; label: string; icon: typeof Home; badge?: string };
|
||||
export type NavItem = { page: Page; label: string; icon: LucideIcon; badge?: string };
|
||||
|
||||
// 仅团队主账号可访问的团队级页面。入口与路由守卫共用此规则,避免权限逻辑分散。
|
||||
const OWNER_ONLY_PAGES = new Set<Page>(["team", "account"]);
|
||||
|
||||
@@ -588,6 +588,7 @@ export type ModelConfig = {
|
||||
display_name: string;
|
||||
capability: string;
|
||||
status: string;
|
||||
is_default?: boolean;
|
||||
unit_price?: string; // 单位价(每张图/每次调用),前端据此算「预估扣费」与后端实扣一致(PMC#20)
|
||||
// 模型元数据:自由创作视频模型带 pricing(元/百万tokens 分档价表)/resolutions/durations,前端预估消耗读它
|
||||
metadata?: Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user