fix: align canvas billing and 3d task routing
This commit is contained in:
parent
1870f87beb
commit
432d315bc6
@ -5,6 +5,7 @@ import {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
type ChangeEvent,
|
||||
type DragEvent,
|
||||
} from "react";
|
||||
@ -1044,6 +1045,43 @@ const defaultLovartPromptTagVisibility: LovartPromptTagVisibility = {
|
||||
video: true,
|
||||
};
|
||||
|
||||
function stableStringifyComparableValue(value: unknown): string {
|
||||
if (value === null || typeof value !== "object") {
|
||||
return JSON.stringify(value) ?? "null";
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((item) => stableStringifyComparableValue(item)).join(",")}]`;
|
||||
}
|
||||
|
||||
const entries = Object.entries(value as Record<string, unknown>)
|
||||
.filter(([, item]) => item !== undefined)
|
||||
.sort(([left], [right]) => left.localeCompare(right));
|
||||
|
||||
return `{${entries
|
||||
.map(([key, item]) => `${JSON.stringify(key)}:${stableStringifyComparableValue(item)}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
|
||||
function areComparableValuesEqual(left: unknown, right: unknown) {
|
||||
return stableStringifyComparableValue(left) === stableStringifyComparableValue(right);
|
||||
}
|
||||
|
||||
function pickChangedTopLevelFields<T extends Record<string, unknown>>(
|
||||
current: T,
|
||||
baseline: Partial<T> | null | undefined
|
||||
) {
|
||||
if (!baseline) return { ...current };
|
||||
|
||||
const changed: Partial<T> = {};
|
||||
for (const [key, value] of Object.entries(current) as Array<[keyof T, T[keyof T]]>) {
|
||||
if (!areComparableValuesEqual(value, baseline[key])) {
|
||||
changed[key] = value;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
const defaultContactConfig: ContactConfig = {
|
||||
phone: "",
|
||||
wechatQrUrl: "",
|
||||
@ -1588,6 +1626,10 @@ export function AdminSettingsClient() {
|
||||
htmlContent: "",
|
||||
updatedAt: null,
|
||||
});
|
||||
const lastLoadedSiteSettingsRef = useRef<SiteSettings | null>(null);
|
||||
const lastLoadedHomeAnimationConfigRef = useRef<HomeAnimationConfig | null>(
|
||||
null
|
||||
);
|
||||
const [newOverrideUserId, setNewOverrideUserId] = useState("");
|
||||
const [newOverrideEmail, setNewOverrideEmail] = useState("");
|
||||
const [newOverridePlanId, setNewOverridePlanId] = useState("");
|
||||
@ -1614,7 +1656,7 @@ export function AdminSettingsClient() {
|
||||
(siteSettingsData as any).dulupayConfig
|
||||
);
|
||||
|
||||
setSiteSettings({
|
||||
const nextSiteSettings: SiteSettings = {
|
||||
siteName: siteSettingsData.siteName || "AI Chatbot",
|
||||
siteLogo: siteSettingsData.siteLogo || "",
|
||||
siteLogoDark: siteSettingsData.siteLogoDark || "",
|
||||
@ -1828,7 +1870,9 @@ export function AdminSettingsClient() {
|
||||
: ["全部", "惊喜"],
|
||||
membershipConfig: normalizedMembershipConfig,
|
||||
membershipUserOverrides: normalizedMembershipOverrides,
|
||||
});
|
||||
};
|
||||
setSiteSettings(nextSiteSettings);
|
||||
lastLoadedSiteSettingsRef.current = nextSiteSettings;
|
||||
}
|
||||
}, [siteSettingsData]);
|
||||
|
||||
@ -1840,7 +1884,7 @@ export function AdminSettingsClient() {
|
||||
? (homeAnimationSettingData.setting.value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
setHomeAnimationConfig({
|
||||
const nextHomeAnimationConfig = {
|
||||
enabled: raw.enabled === true,
|
||||
fileName: String(raw.fileName || "").trim(),
|
||||
htmlContent:
|
||||
@ -1848,7 +1892,9 @@ export function AdminSettingsClient() {
|
||||
? raw.htmlContent.replace(/^\uFEFF/, "").replace(/\u0000/g, "")
|
||||
: "",
|
||||
updatedAt: String(raw.updatedAt || "").trim() || null,
|
||||
});
|
||||
};
|
||||
setHomeAnimationConfig(nextHomeAnimationConfig);
|
||||
lastLoadedHomeAnimationConfigRef.current = nextHomeAnimationConfig;
|
||||
}, [homeAnimationSettingData]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -3482,25 +3528,42 @@ export function AdminSettingsClient() {
|
||||
const parsedPointsSystemConfig = normalizePointsSystemConfig(
|
||||
siteSettings.pointsSystemConfig
|
||||
);
|
||||
const normalizedHomeAnimationConfig: HomeAnimationConfig = {
|
||||
enabled: homeAnimationConfig.enabled,
|
||||
fileName: homeAnimationConfig.fileName.trim(),
|
||||
htmlContent: normalizedHomeAnimationHtml,
|
||||
updatedAt: homeAnimationConfig.updatedAt,
|
||||
};
|
||||
|
||||
const siteSettingsPayload: Partial<SiteSettings> = {
|
||||
...siteSettings,
|
||||
};
|
||||
delete siteSettingsPayload.agentWorkflowUiConfig;
|
||||
|
||||
const payload = {
|
||||
...siteSettingsPayload,
|
||||
homeAnimationConfig: {
|
||||
enabled: homeAnimationConfig.enabled,
|
||||
fileName: homeAnimationConfig.fileName.trim(),
|
||||
htmlContent: normalizedHomeAnimationHtml,
|
||||
updatedAt: homeAnimationConfig.updatedAt,
|
||||
},
|
||||
membershipConfig: parsedMembershipConfig,
|
||||
membershipUserOverrides: parsedMembershipOverrides,
|
||||
pointsSystemConfig: parsedPointsSystemConfig,
|
||||
pointsPackages: parsedPointsPackages,
|
||||
};
|
||||
delete siteSettingsPayload.agentWorkflowUiConfig;
|
||||
|
||||
const changedSiteSettingsPayload = pickChangedTopLevelFields(
|
||||
siteSettingsPayload as Record<string, unknown>,
|
||||
lastLoadedSiteSettingsRef.current as Partial<Record<string, unknown>> | null
|
||||
) as Partial<SiteSettings>;
|
||||
const shouldSaveHomeAnimation = !areComparableValuesEqual(
|
||||
normalizedHomeAnimationConfig,
|
||||
lastLoadedHomeAnimationConfigRef.current
|
||||
);
|
||||
|
||||
const payload = {
|
||||
...changedSiteSettingsPayload,
|
||||
...(shouldSaveHomeAnimation
|
||||
? { homeAnimationConfig: normalizedHomeAnimationConfig }
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (Object.keys(payload).length === 0) {
|
||||
toast.info("没有需要保存的改动");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/site-settings", {
|
||||
method: "PUT",
|
||||
@ -3517,14 +3580,23 @@ export function AdminSettingsClient() {
|
||||
...prev,
|
||||
...nextSiteSettings,
|
||||
}));
|
||||
setHomeAnimationConfig((prev) => ({
|
||||
...prev,
|
||||
htmlContent: normalizedHomeAnimationHtml,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}));
|
||||
lastLoadedSiteSettingsRef.current = {
|
||||
...(lastLoadedSiteSettingsRef.current || siteSettings),
|
||||
...siteSettingsPayload,
|
||||
};
|
||||
if (shouldSaveHomeAnimation) {
|
||||
const committedHomeAnimationConfig: HomeAnimationConfig = {
|
||||
...normalizedHomeAnimationConfig,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
setHomeAnimationConfig(committedHomeAnimationConfig);
|
||||
lastLoadedHomeAnimationConfigRef.current = committedHomeAnimationConfig;
|
||||
}
|
||||
toast.success("网站设置保存成功");
|
||||
mutateSiteSettings();
|
||||
mutateHomeAnimationSetting();
|
||||
if (shouldSaveHomeAnimation) {
|
||||
mutateHomeAnimationSetting();
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "保存网站设置失败");
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
@ -28,6 +27,7 @@ import {
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import {
|
||||
getVideoCanvasFunctionBillingCopy,
|
||||
type VideoCanvasFunctionConfig,
|
||||
type VideoCanvasFunctionDefinition,
|
||||
VIDEO_CANVAS_FUNCTION_CONFIG_KEY,
|
||||
@ -260,7 +260,7 @@ export function VideoCanvasSettingsClient() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">视频画布功能设置</h1>
|
||||
<p className="mt-2 max-w-3xl text-sm text-muted-foreground">
|
||||
这里集中配置复刻画布里的工具能力、预设提示词、默认模型和扣点。前端优先按复刻目标触发,后端缺口可以先留空或后续替换。
|
||||
这里集中配置复刻画布里的工具能力、预设提示词和默认模型。积分与扣费统一跟随全站模型/功能配置,不再在视频画布里单独维护一套价格。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@ -327,6 +327,9 @@ export function VideoCanvasSettingsClient() {
|
||||
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
|
||||
视频画布运行时会通过 /api/video-canvas/config 读取这里保存的提示词。比如“多机位九宫格”的分镜规则、黑色分割线要求和镜头说明,都由管理员在这里维护,前端不再写死覆盖。
|
||||
</p>
|
||||
<p className="mt-2 max-w-3xl text-sm text-muted-foreground">
|
||||
计费说明只展示来源,不在这里改价;需要调整积分请回到全站积分设置、模型设置或对应功能设置统一修改。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
@ -355,7 +358,7 @@ export function VideoCanvasSettingsClient() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_120px]">
|
||||
<div className="grid gap-4 md:grid-cols-1">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`model-${item.key}`}>模型覆盖</Label>
|
||||
<ModelSelect
|
||||
@ -366,17 +369,13 @@ export function VideoCanvasSettingsClient() {
|
||||
value={item.model || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`credit-${item.key}`}>扣点</Label>
|
||||
<Input
|
||||
id={`credit-${item.key}`}
|
||||
min={0}
|
||||
onChange={(event) =>
|
||||
updateFunction(item.key, { creditCost: Number(event.target.value || 0) })
|
||||
}
|
||||
type="number"
|
||||
value={item.creditCost ?? 0}
|
||||
/>
|
||||
<div className="rounded-lg border border-dashed border-border/70 bg-muted/20 px-3 py-2">
|
||||
<div className="text-xs font-medium text-foreground">
|
||||
积分来源:{getVideoCanvasFunctionBillingCopy(item).shortLabel}
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
{getVideoCanvasFunctionBillingCopy(item).description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@ -6,7 +6,10 @@ import {
|
||||
pickTaskPreviewImageUrl,
|
||||
tripoRequest,
|
||||
} from "@/lib/tripo/client";
|
||||
import { getTripoProviderConfig } from "@/lib/tripo/server";
|
||||
import {
|
||||
getTripoProviderConfig,
|
||||
resolveTripoProviderConfigForTaskId,
|
||||
} from "@/lib/tripo/server";
|
||||
import type { TripoTaskData } from "@/lib/tripo/types";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
|
||||
@ -227,7 +230,10 @@ export async function GET(request: Request, { params }: RouteParams) {
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const config = await getTripoProviderConfig(searchParams.get("providerId"));
|
||||
const preferredProviderId = searchParams.get("providerId");
|
||||
const config = preferredProviderId
|
||||
? await getTripoProviderConfig(preferredProviderId)
|
||||
: await resolveTripoProviderConfigForTaskId(normalizedTaskId);
|
||||
const result = await tripoRequest<TripoTaskData>(config, {
|
||||
method: "GET",
|
||||
path: `/task/${encodeURIComponent(normalizedTaskId)}`,
|
||||
|
||||
@ -5,7 +5,10 @@ import { resolveThreeDModelRealId } from "@/lib/model3d/model-alias-config";
|
||||
import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server";
|
||||
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
|
||||
import { get3DTaskCreditCost } from "@/lib/points/config";
|
||||
import { getTripoProviderConfig } from "@/lib/tripo/server";
|
||||
import {
|
||||
getTripoProviderConfig,
|
||||
resolveTripoProviderConfigForTaskId,
|
||||
} from "@/lib/tripo/server";
|
||||
import { TripoApiError, tripoRequest } from "@/lib/tripo/client";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
@ -366,9 +369,27 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
|
||||
}
|
||||
|
||||
const config = await getTripoProviderConfig(
|
||||
readOptionalTrimmedText(body.providerId)
|
||||
);
|
||||
const preferredProviderId = readOptionalTrimmedText(body.providerId);
|
||||
const dependencyTaskId =
|
||||
taskType === "refine_model"
|
||||
? String(body.draftModelTaskId || "").trim()
|
||||
: taskType === "convert_model" ||
|
||||
taskType === "stylize_model" ||
|
||||
taskType === "texture_model" ||
|
||||
taskType === "animate_prerigcheck" ||
|
||||
taskType === "animate_rig" ||
|
||||
taskType === "animate_retarget" ||
|
||||
taskType === "mesh_segmentation" ||
|
||||
taskType === "mesh_completion" ||
|
||||
taskType === "highpoly_to_lowpoly"
|
||||
? String(body.originalModelTaskId || "").trim()
|
||||
: "";
|
||||
const config = dependencyTaskId
|
||||
? await resolveTripoProviderConfigForTaskId(
|
||||
dependencyTaskId,
|
||||
preferredProviderId,
|
||||
)
|
||||
: await getTripoProviderConfig(preferredProviderId);
|
||||
const result = await tripoRequest<Record<string, unknown>>(config, {
|
||||
method: "POST",
|
||||
path: "/task",
|
||||
|
||||
@ -1554,10 +1554,19 @@ export default function CanvasModel3DPage() {
|
||||
exportFormat?: string | null;
|
||||
assetPayload?: Model3DTaskAssetPayload | null;
|
||||
}) => {
|
||||
const normalizedPayload =
|
||||
params.provider === "tripo-3d" &&
|
||||
!String(params.payload.providerId || "").trim() &&
|
||||
params.assetPayload?.providerId
|
||||
? {
|
||||
...params.payload,
|
||||
providerId: params.assetPayload.providerId,
|
||||
}
|
||||
: params.payload;
|
||||
const createResult =
|
||||
params.provider === "worldlabs-3d"
|
||||
? await worldLabsGenerationService.createTask(params.payload)
|
||||
: await model3DGenerationService.createTask(params.payload);
|
||||
? await worldLabsGenerationService.createTask(normalizedPayload)
|
||||
: await model3DGenerationService.createTask(normalizedPayload);
|
||||
if (!createResult.success || !createResult.data?.taskId) {
|
||||
throw new Error(createResult.error || "创建任务失败");
|
||||
}
|
||||
@ -1622,6 +1631,8 @@ export default function CanvasModel3DPage() {
|
||||
promptText: string;
|
||||
sourceTaskId: string;
|
||||
successMessage: string;
|
||||
assetPayload?: Model3DTaskAssetPayload | null;
|
||||
modelLabel?: string;
|
||||
}) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
@ -1630,8 +1641,9 @@ export default function CanvasModel3DPage() {
|
||||
payload: params.payload,
|
||||
mode: params.mode,
|
||||
promptText: params.promptText,
|
||||
modelLabel: modelVersion,
|
||||
modelLabel: params.modelLabel || modelVersion,
|
||||
sourceTaskId: params.sourceTaskId,
|
||||
assetPayload: params.assetPayload ?? null,
|
||||
});
|
||||
toast.success(params.successMessage);
|
||||
} catch (error) {
|
||||
@ -2292,6 +2304,7 @@ export default function CanvasModel3DPage() {
|
||||
modelLabel: readyTask.modelVersion,
|
||||
sourceTaskId: baseTaskId,
|
||||
exportFormat,
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
});
|
||||
setAutoDownloadTaskId(task.taskId);
|
||||
toast.success(`已提交${exportFormat}转换任务,完成后自动下载`);
|
||||
@ -2319,6 +2332,7 @@ export default function CanvasModel3DPage() {
|
||||
promptText: `精修任务(来源 ${readyTask.taskId.slice(0, 8)})`,
|
||||
modelLabel: readyTask.modelVersion,
|
||||
sourceTaskId: readyTask.taskId,
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
});
|
||||
toast.success("已提交模型精修任务");
|
||||
} catch (error) {
|
||||
@ -2348,6 +2362,8 @@ export default function CanvasModel3DPage() {
|
||||
promptText: `纹理重绘(来源 ${readyTask.taskId.slice(0, 8)})`,
|
||||
sourceTaskId: readyTask.taskId,
|
||||
successMessage: "已提交纹理重绘任务",
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
modelLabel: readyTask.modelVersion,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2368,6 +2384,8 @@ export default function CanvasModel3DPage() {
|
||||
promptText: `风格化 ${stylizeStyle}(来源 ${readyTask.taskId.slice(0, 8)})`,
|
||||
sourceTaskId: readyTask.taskId,
|
||||
successMessage: "已提交风格化任务",
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
modelLabel: readyTask.modelVersion,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2384,6 +2402,8 @@ export default function CanvasModel3DPage() {
|
||||
promptText: `可绑定检测(来源 ${readyTask.taskId.slice(0, 8)})`,
|
||||
sourceTaskId: readyTask.taskId,
|
||||
successMessage: "已提交可绑定检测任务",
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
modelLabel: readyTask.modelVersion,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2403,6 +2423,8 @@ export default function CanvasModel3DPage() {
|
||||
promptText: `自动绑定 ${rigType}(来源 ${readyTask.taskId.slice(0, 8)})`,
|
||||
sourceTaskId: readyTask.taskId,
|
||||
successMessage: "已提交自动绑定任务",
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
modelLabel: readyTask.modelVersion,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2424,6 +2446,8 @@ export default function CanvasModel3DPage() {
|
||||
promptText: `动作 ${retargetAnimation}(来源 ${readyTask.taskId.slice(0, 8)})`,
|
||||
sourceTaskId: readyTask.taskId,
|
||||
successMessage: "已提交动作重定向任务",
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
modelLabel: readyTask.modelVersion,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2440,6 +2464,8 @@ export default function CanvasModel3DPage() {
|
||||
promptText: `网格分割(来源 ${readyTask.taskId.slice(0, 8)})`,
|
||||
sourceTaskId: readyTask.taskId,
|
||||
successMessage: "已提交网格分割任务",
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
modelLabel: readyTask.modelVersion,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2462,6 +2488,8 @@ export default function CanvasModel3DPage() {
|
||||
promptText: `网格补全(来源 ${readyTask.taskId.slice(0, 8)})`,
|
||||
sourceTaskId: readyTask.taskId,
|
||||
successMessage: "已提交网格补全任务",
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
modelLabel: readyTask.modelVersion,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2483,6 +2511,8 @@ export default function CanvasModel3DPage() {
|
||||
promptText: `低模优化(来源 ${readyTask.taskId.slice(0, 8)})`,
|
||||
sourceTaskId: readyTask.taskId,
|
||||
successMessage: "已提交低模优化任务",
|
||||
assetPayload: readyTask.assetPayload ?? null,
|
||||
modelLabel: readyTask.modelVersion,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -46,10 +46,18 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
import capturedCanvasModules from "@/artifacts/liblib-frontend-capture-ed9a2c12570a49b9a1186017b2c38b52/canvas-modules.json";
|
||||
import { getVideoCreditCostFromConfig } from "@/lib/ai/video-model-config";
|
||||
import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import {
|
||||
defaultPointsSystemConfig,
|
||||
getImageToolCreditCost,
|
||||
normalizePointsSystemConfig,
|
||||
type ImageToolCreditFeature,
|
||||
type PointsSystemConfig,
|
||||
} from "@/lib/points/config";
|
||||
import { cn, fetcher } from "@/lib/utils";
|
||||
import {
|
||||
canvasService,
|
||||
@ -58,6 +66,7 @@ import {
|
||||
workflowAnalysisService,
|
||||
} from "@/services/canvas";
|
||||
import {
|
||||
getVideoCanvasFunctionBillingCopy,
|
||||
type VideoCanvasFunctionConfig,
|
||||
type VideoCanvasFunctionDefinition,
|
||||
defaultVideoCanvasFunctionConfig,
|
||||
@ -251,6 +260,13 @@ type CanvasModelOption = {
|
||||
isDefault?: boolean;
|
||||
isSubModel?: boolean;
|
||||
capabilities?: unknown;
|
||||
imageGenConfig?: {
|
||||
creditCostPerImage?: number | null;
|
||||
creditCostPerVideo?: number | null;
|
||||
videoDurationOptions?: number[] | null;
|
||||
creditCostPerVideoByDuration?: Record<string, number> | null;
|
||||
creditCostPerVideoByResolution?: Record<string, number> | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type CanvasModelKind = "image" | "video" | "multimodal" | "any";
|
||||
@ -312,6 +328,7 @@ function getSiteShowcaseName(siteName?: string | null) {
|
||||
}
|
||||
|
||||
type MembershipSummaryResponse = {
|
||||
isHighVip?: boolean;
|
||||
currentPlanId?: string;
|
||||
currentPlan?: {
|
||||
id?: string;
|
||||
@ -332,6 +349,7 @@ type MembershipSummaryResponse = {
|
||||
remainingThisMonth?: number | null;
|
||||
totalImages?: number | null;
|
||||
};
|
||||
pointsSystemConfig?: PointsSystemConfig | null;
|
||||
};
|
||||
|
||||
type MembershipPlan = {
|
||||
@ -1729,6 +1747,130 @@ function getCanvasModelDisplayName(
|
||||
return match?.displayName || normalized;
|
||||
}
|
||||
|
||||
type VideoCanvasBillingDisplay = {
|
||||
label: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
function getCanvasModelOptionByRef(
|
||||
modelRef: string | undefined,
|
||||
models: CanvasModelOption[],
|
||||
) {
|
||||
const normalized = normalizeModelRef(modelRef);
|
||||
if (!normalized) return null;
|
||||
return (
|
||||
models.find(
|
||||
(model) =>
|
||||
model.id === normalized ||
|
||||
model.modelId === normalized ||
|
||||
getCanvasModelRef(model) === normalized,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
function getVideoCanvasFallbackBillingDisplay(
|
||||
definition?: Pick<VideoCanvasFunctionDefinition, "key" | "route"> | null,
|
||||
): VideoCanvasBillingDisplay {
|
||||
const copy = definition
|
||||
? getVideoCanvasFunctionBillingCopy(definition)
|
||||
: {
|
||||
shortLabel: "按站点",
|
||||
description: "沿用站点统一模型与功能积分规则,不在视频画布单独改价。",
|
||||
};
|
||||
return {
|
||||
label: copy.shortLabel,
|
||||
message: copy.description,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveVideoCanvasBillingDisplay(params: {
|
||||
membershipSummary?: MembershipSummaryResponse;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
modelRef?: string;
|
||||
modelKind?: CanvasModelKind;
|
||||
imageCount?: number;
|
||||
duration?: number | null;
|
||||
resolution?: string | null;
|
||||
billingFeature?: ImageToolCreditFeature | null;
|
||||
functionDefinition?: Pick<VideoCanvasFunctionDefinition, "key" | "route"> | null;
|
||||
}): VideoCanvasBillingDisplay {
|
||||
const fallback = getVideoCanvasFallbackBillingDisplay(params.functionDefinition);
|
||||
const pointsConfig = normalizePointsSystemConfig(
|
||||
params.membershipSummary?.pointsSystemConfig || defaultPointsSystemConfig,
|
||||
);
|
||||
|
||||
if (params.membershipSummary?.isHighVip) {
|
||||
return {
|
||||
label: "VIP",
|
||||
message: "当前高阶会员按站点规则免扣积分。",
|
||||
};
|
||||
}
|
||||
|
||||
if (pointsConfig.enabled !== true) {
|
||||
return {
|
||||
label: "免费",
|
||||
message: "当前站点未启用积分扣费。",
|
||||
};
|
||||
}
|
||||
|
||||
if (params.billingFeature) {
|
||||
const featureCost = getImageToolCreditCost(pointsConfig, params.billingFeature);
|
||||
if (featureCost > 0) {
|
||||
return {
|
||||
label: String(featureCost),
|
||||
message: `当前功能按全站积分设置消耗 ${featureCost} 积分。`,
|
||||
};
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const matchedModel = getCanvasModelOptionByRef(
|
||||
params.modelRef,
|
||||
params.modelOptions || [],
|
||||
);
|
||||
if (!matchedModel?.imageGenConfig) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (params.modelKind === "video") {
|
||||
const videoCost = getVideoCreditCostFromConfig(
|
||||
matchedModel.imageGenConfig,
|
||||
params.duration,
|
||||
params.resolution,
|
||||
);
|
||||
if (videoCost > 0) {
|
||||
return {
|
||||
label: String(videoCost),
|
||||
message: `当前视频模型按站点配置预计消耗 ${videoCost} 积分。`,
|
||||
};
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (params.modelKind === "image") {
|
||||
const unitCost = Math.max(
|
||||
0,
|
||||
Math.round(Number(matchedModel.imageGenConfig.creditCostPerImage ?? 0)),
|
||||
);
|
||||
if (unitCost > 0) {
|
||||
const safeImageCount = Math.max(
|
||||
1,
|
||||
Math.round(Number(params.imageCount || 1)),
|
||||
);
|
||||
const totalCost = unitCost * safeImageCount;
|
||||
return {
|
||||
label: String(unitCost),
|
||||
message:
|
||||
safeImageCount > 1
|
||||
? `当前图片模型按站点配置 ${unitCost} 积分/张,本次共 ${totalCost} 积分。`
|
||||
: `当前图片模型按站点配置消耗 ${unitCost} 积分/张。`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function composeFunctionPrompt(
|
||||
definition: VideoCanvasFunctionDefinition,
|
||||
userPrompt?: string,
|
||||
@ -2552,6 +2694,10 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
"/api/models",
|
||||
fetcher
|
||||
);
|
||||
const { data: membershipSummary } = useSWR<MembershipSummaryResponse>(
|
||||
"/api/user/membership-summary",
|
||||
fetcher
|
||||
);
|
||||
const videoCanvasFunctionConfig = useMemo(
|
||||
() => normalizeVideoCanvasFunctionConfig(videoCanvasFunctionSettings?.config),
|
||||
[videoCanvasFunctionSettings?.config]
|
||||
@ -5398,6 +5544,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
{selectedPresetAnchor && selectedPresetControlNode ? (
|
||||
<PresetWorkflowControls
|
||||
anchor={selectedPresetAnchor}
|
||||
membershipSummary={membershipSummary}
|
||||
mode={selectedPresetMode ?? "character-three-view"}
|
||||
functionConfig={videoCanvasFunctionConfig}
|
||||
modelOptions={canvasModelOptions}
|
||||
@ -5473,10 +5620,15 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
{selectedNode && selectedTargetModuleKind === "text" && selectedTargetAnchor ? (
|
||||
<TextTargetModuleComposer
|
||||
anchor={selectedTargetAnchor}
|
||||
billingDefinition={getConfiguredFunction(
|
||||
videoCanvasFunctionConfig,
|
||||
"story-script",
|
||||
)}
|
||||
defaultModel={getFunctionModelValue(
|
||||
videoCanvasFunctionConfig,
|
||||
getConfiguredFunction(videoCanvasFunctionConfig, "story-script"),
|
||||
)}
|
||||
membershipSummary={membershipSummary}
|
||||
modelOptions={canvasModelOptions}
|
||||
node={selectedNode}
|
||||
onSubmit={(prompt, model) => {
|
||||
@ -5509,6 +5661,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
<VideoTargetModuleComposer
|
||||
anchor={selectedTargetAnchor}
|
||||
functionConfig={videoCanvasFunctionConfig}
|
||||
membershipSummary={membershipSummary}
|
||||
modelOptions={canvasModelOptions}
|
||||
node={selectedNode}
|
||||
sourceNodes={selectedTargetSourceNodes}
|
||||
@ -6221,14 +6374,14 @@ function MagnetGlyph() {
|
||||
|
||||
function GridSplitActionToolbar({
|
||||
anchor,
|
||||
creditCost,
|
||||
billingLabel,
|
||||
onClearSelection,
|
||||
onCreateNodes,
|
||||
onUpscale,
|
||||
selectedCount,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
creditCost: number;
|
||||
billingLabel: string;
|
||||
onClearSelection: () => void;
|
||||
onCreateNodes: () => void;
|
||||
onUpscale: (scaleFactor: number) => void;
|
||||
@ -6287,7 +6440,7 @@ function GridSplitActionToolbar({
|
||||
<SceneToolIcon kind="hd" />
|
||||
<span className="whitespace-nowrap">生成高清图片</span>
|
||||
<span className="rounded-[7px] bg-white/10 px-2 py-0.5 text-[13px] text-white/72">
|
||||
↑{Math.max(0, creditCost)}
|
||||
{billingLabel}
|
||||
</span>
|
||||
</button>
|
||||
<div className="relative">
|
||||
@ -6700,6 +6853,7 @@ function VideoAnalysisFloatingPanel({
|
||||
function PresetWorkflowControls({
|
||||
anchor,
|
||||
functionConfig,
|
||||
membershipSummary,
|
||||
mode,
|
||||
modelOptions,
|
||||
nodes,
|
||||
@ -6713,6 +6867,7 @@ function PresetWorkflowControls({
|
||||
}: {
|
||||
anchor: PresetControlAnchor;
|
||||
functionConfig: VideoCanvasFunctionConfig;
|
||||
membershipSummary?: MembershipSummaryResponse;
|
||||
mode: Exclude<ActivePresetControlMode, null>;
|
||||
modelOptions: CanvasModelOption[];
|
||||
nodes: CloneNode[];
|
||||
@ -6732,7 +6887,9 @@ function PresetWorkflowControls({
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<GridSplitActionToolbar
|
||||
anchor={anchor}
|
||||
creditCost={(getConfiguredFunction(functionConfig, "hd-upscale").creditCost || 0) * gridSplit.selectedIndices.length}
|
||||
billingLabel={getVideoCanvasFunctionBillingCopy(
|
||||
getConfiguredFunction(functionConfig, "hd-upscale")
|
||||
).shortLabel}
|
||||
onClearSelection={onClearGridSplitSelection}
|
||||
onCreateNodes={onGridSplitCreateNodes}
|
||||
onUpscale={onGridSplitUpscale}
|
||||
@ -6746,7 +6903,10 @@ function PresetWorkflowControls({
|
||||
<CharacterPresetToolbar anchor={anchor} onToolAction={onImageToolAction} />
|
||||
<PromptComposer
|
||||
anchor={anchor}
|
||||
billingFeature={getImageGenerationBillingFeature(mode)}
|
||||
defaultModel={getFunctionModelValue(functionConfig, definition)}
|
||||
functionDefinition={definition}
|
||||
membershipSummary={membershipSummary}
|
||||
modelKind="image"
|
||||
modelOptions={modelOptions}
|
||||
nodes={nodes}
|
||||
@ -6772,8 +6932,9 @@ function PresetWorkflowControls({
|
||||
/>
|
||||
<PresetComposerPanel
|
||||
anchor={anchor}
|
||||
credit="8"
|
||||
billingDefinition={definition}
|
||||
defaultModel={getFunctionModelValue(functionConfig, definition)}
|
||||
membershipSummary={membershipSummary}
|
||||
modelKind="multimodal"
|
||||
modelOptions={modelOptions}
|
||||
onSubmit={onPresetSubmit}
|
||||
@ -6800,8 +6961,9 @@ function PresetWorkflowControls({
|
||||
/>
|
||||
<PresetComposerPanel
|
||||
anchor={anchor}
|
||||
credit="28"
|
||||
billingDefinition={definition}
|
||||
defaultModel={getFunctionModelValue(functionConfig, definition)}
|
||||
membershipSummary={membershipSummary}
|
||||
modelKind="video"
|
||||
modelOptions={modelOptions}
|
||||
onSubmit={onPresetSubmit}
|
||||
@ -6819,7 +6981,9 @@ function PresetWorkflowControls({
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<AudioToVideoComposerPanel
|
||||
anchor={anchor}
|
||||
billingDefinition={definition}
|
||||
defaultModel={getFunctionModelValue(functionConfig, definition)}
|
||||
membershipSummary={membershipSummary}
|
||||
modelOptions={modelOptions}
|
||||
onSubmit={onPresetSubmit}
|
||||
/>
|
||||
@ -6829,13 +6993,17 @@ function PresetWorkflowControls({
|
||||
|
||||
function TextTargetModuleComposer({
|
||||
anchor,
|
||||
billingDefinition,
|
||||
defaultModel = "auto",
|
||||
membershipSummary,
|
||||
modelOptions = [],
|
||||
node,
|
||||
onSubmit,
|
||||
}: {
|
||||
anchor: PresetControlAnchor;
|
||||
billingDefinition?: VideoCanvasFunctionDefinition;
|
||||
defaultModel?: string;
|
||||
membershipSummary?: MembershipSummaryResponse;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
node: CloneNode;
|
||||
onSubmit: (prompt: string, model: string) => void;
|
||||
@ -6859,6 +7027,18 @@ function TextTargetModuleComposer({
|
||||
setSelectedModel(initialModel);
|
||||
}, [initialModel, node.id]);
|
||||
|
||||
const billingDisplay = useMemo(
|
||||
() =>
|
||||
resolveVideoCanvasBillingDisplay({
|
||||
functionDefinition: billingDefinition,
|
||||
membershipSummary,
|
||||
modelKind: "multimodal",
|
||||
modelOptions,
|
||||
modelRef: selectedModel,
|
||||
}),
|
||||
[billingDefinition, membershipSummary, modelOptions, selectedModel],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-auto absolute z-30 overflow-visible"
|
||||
@ -6903,7 +7083,12 @@ function TextTargetModuleComposer({
|
||||
/>
|
||||
<div className="min-w-0 flex-1" />
|
||||
<ComposerControl label="文" onClick={() => toast.info("翻译接口待接入。")} />
|
||||
<ComposerControl label="6" icon="⚡" muted onClick={() => toast.info("积分估算接入本地积分系统。")} />
|
||||
<ComposerControl
|
||||
label={billingDisplay.label}
|
||||
icon="⚡"
|
||||
muted
|
||||
onClick={() => toast.info(billingDisplay.message)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="absolute bottom-5 right-5 flex h-11 w-11 items-center justify-center rounded-[12px] bg-white/76 text-[24px] leading-none text-black transition hover:bg-white"
|
||||
@ -6920,6 +7105,7 @@ function TextTargetModuleComposer({
|
||||
function VideoTargetModuleComposer({
|
||||
anchor,
|
||||
functionConfig,
|
||||
membershipSummary,
|
||||
modelOptions = [],
|
||||
node,
|
||||
onSubmit,
|
||||
@ -6927,6 +7113,7 @@ function VideoTargetModuleComposer({
|
||||
}: {
|
||||
anchor: PresetControlAnchor;
|
||||
functionConfig: VideoCanvasFunctionConfig;
|
||||
membershipSummary?: MembershipSummaryResponse;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
node: CloneNode;
|
||||
onSubmit: (payload: VideoTargetComposerSubmitPayload) => void;
|
||||
@ -7010,6 +7197,27 @@ function VideoTargetModuleComposer({
|
||||
});
|
||||
}, [activeModelFallback]);
|
||||
|
||||
const billingDisplay = useMemo(
|
||||
() =>
|
||||
resolveVideoCanvasBillingDisplay({
|
||||
duration: normalizeVideoDurationValue(settings.duration),
|
||||
functionDefinition: activeDefinition,
|
||||
membershipSummary,
|
||||
modelKind: "video",
|
||||
modelOptions,
|
||||
modelRef: settings.model,
|
||||
resolution: normalizeVideoResolutionValue(settings.resolution),
|
||||
}),
|
||||
[
|
||||
activeDefinition,
|
||||
membershipSummary,
|
||||
modelOptions,
|
||||
settings.duration,
|
||||
settings.model,
|
||||
settings.resolution,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-auto absolute z-30 overflow-visible"
|
||||
@ -7162,10 +7370,10 @@ function VideoTargetModuleComposer({
|
||||
<ComposerControl label="" icon="⌘" onClick={() => toast.info("高级视频参数后端待接入。")} />
|
||||
<ComposerControl label="1个" dropdown onClick={() => toast.info("当前仅支持单次生成 1 个视频结果。")} />
|
||||
<ComposerControl
|
||||
label={String(activeDefinition.creditCost || 14)}
|
||||
label={billingDisplay.label}
|
||||
icon="⚡"
|
||||
muted
|
||||
onClick={() => toast.info("积分估算接入本地积分系统。")}
|
||||
onClick={() => toast.info(billingDisplay.message)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@ -7267,12 +7475,16 @@ function CharacterPresetToolbar({
|
||||
|
||||
function AudioToVideoComposerPanel({
|
||||
anchor,
|
||||
billingDefinition,
|
||||
defaultModel = "auto",
|
||||
membershipSummary,
|
||||
modelOptions = [],
|
||||
onSubmit,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
billingDefinition?: VideoCanvasFunctionDefinition;
|
||||
defaultModel?: string;
|
||||
membershipSummary?: MembershipSummaryResponse;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
onSubmit?: (prompt: string, settings?: ComposerSettings) => void;
|
||||
}) {
|
||||
@ -7291,6 +7503,20 @@ function AudioToVideoComposerPanel({
|
||||
setSettings((current) => ({ ...current, model: normalizeModelRef(defaultModel) || "auto" }));
|
||||
}, [defaultModel]);
|
||||
|
||||
const billingDisplay = useMemo(
|
||||
() =>
|
||||
resolveVideoCanvasBillingDisplay({
|
||||
duration: 5,
|
||||
functionDefinition: billingDefinition,
|
||||
membershipSummary,
|
||||
modelKind: "video",
|
||||
modelOptions,
|
||||
modelRef: settings.model,
|
||||
resolution: normalizeVideoResolutionValue(settings.resolution),
|
||||
}),
|
||||
[billingDefinition, membershipSummary, modelOptions, settings.model, settings.resolution],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-auto absolute overflow-visible"
|
||||
@ -7381,7 +7607,12 @@ function AudioToVideoComposerPanel({
|
||||
<ComposerControl label="文" onClick={() => toast.info("翻译接口待接入。")} />
|
||||
<ComposerControl label="" icon="⌘" onClick={() => toast.info("高级设置后端待接入。")} />
|
||||
<ComposerControl label="1个" dropdown onClick={() => toast.info("生成数量后端待接入。")} />
|
||||
<ComposerControl label="108 / 135" icon="⚡" muted onClick={() => toast.info("积分估算接入本地积分系统。")} />
|
||||
<ComposerControl
|
||||
label={billingDisplay.label}
|
||||
icon="⚡"
|
||||
muted
|
||||
onClick={() => toast.info(billingDisplay.message)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="absolute bottom-5 right-5 flex h-14 w-14 items-center justify-center rounded-[14px] bg-white text-[34px] leading-none text-black hover:bg-white/90"
|
||||
@ -7612,7 +7843,6 @@ function StoryScriptScene() {
|
||||
<ScriptGeneratorPanel />
|
||||
</div>
|
||||
<PresetComposerPanel
|
||||
credit="8"
|
||||
prompt={STORY_SCRIPT_PROMPT}
|
||||
submitMessage="故事脚本生成接口待接入。"
|
||||
title="脚本生成器"
|
||||
@ -7638,7 +7868,7 @@ function FirstFrameVideoScene() {
|
||||
<VideoResultPanel label="视频" />
|
||||
</div>
|
||||
<PresetComposerPanel
|
||||
credit="28"
|
||||
modelKind="video"
|
||||
prompt="以当前图为首帧生成视频。"
|
||||
submitMessage="首帧图生视频接口待接入。"
|
||||
thumbnailUrl={FIRST_FRAME_IMAGE_URL}
|
||||
@ -7668,7 +7898,7 @@ function AudioToVideoScene() {
|
||||
<VideoResultPanel label="视频" />
|
||||
</div>
|
||||
<PresetComposerPanel
|
||||
credit="32"
|
||||
modelKind="video"
|
||||
prompt={AUDIO_TO_VIDEO_PROMPT}
|
||||
submitMessage="音频生视频接口待接入。"
|
||||
thumbnailUrl={AUDIO_REFERENCE_IMAGE_URL}
|
||||
@ -7869,8 +8099,9 @@ function AudioResourcePanel() {
|
||||
|
||||
function PresetComposerPanel({
|
||||
anchor,
|
||||
credit,
|
||||
billingDefinition,
|
||||
defaultModel = "auto",
|
||||
membershipSummary,
|
||||
modelKind = "image",
|
||||
modelOptions = [],
|
||||
onSubmit,
|
||||
@ -7880,8 +8111,9 @@ function PresetComposerPanel({
|
||||
title,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
credit: string;
|
||||
billingDefinition?: VideoCanvasFunctionDefinition;
|
||||
defaultModel?: string;
|
||||
membershipSummary?: MembershipSummaryResponse;
|
||||
modelKind?: CanvasModelKind;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
onSubmit?: (prompt: string, settings?: ComposerSettings) => void;
|
||||
@ -7909,6 +8141,21 @@ function PresetComposerPanel({
|
||||
setSettings((current) => ({ ...current, model: normalizeModelRef(defaultModel) || "auto" }));
|
||||
}, [defaultModel]);
|
||||
|
||||
const billingDisplay = useMemo(
|
||||
() =>
|
||||
resolveVideoCanvasBillingDisplay({
|
||||
duration: modelKind === "video" ? 5 : null,
|
||||
functionDefinition: billingDefinition,
|
||||
imageCount: 1,
|
||||
membershipSummary,
|
||||
modelKind,
|
||||
modelOptions,
|
||||
modelRef: settings.model,
|
||||
resolution: modelKind === "video" ? "720p" : null,
|
||||
}),
|
||||
[billingDefinition, membershipSummary, modelKind, modelOptions, settings.model],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-auto absolute overflow-visible"
|
||||
@ -7966,7 +8213,12 @@ function PresetComposerPanel({
|
||||
<ComposerControl label="2K" dropdown onClick={() => toast.info("清晰度选择接口待接入。")} />
|
||||
<ComposerControl label="摄像机" icon="▣" onClick={() => toast.info("镜头配置接口待接入。")} />
|
||||
<ComposerControl label="1张" dropdown onClick={() => toast.info("生成数量接口待接入。")} />
|
||||
<ComposerControl label={credit} icon="⚡" muted onClick={() => toast.info("积分估算接口待接入。")} />
|
||||
<ComposerControl
|
||||
label={billingDisplay.label}
|
||||
icon="⚡"
|
||||
muted
|
||||
onClick={() => toast.info(billingDisplay.message)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="absolute bottom-5 right-5 flex h-11 w-11 items-center justify-center rounded-[12px] bg-white/76 text-[24px] text-black hover:bg-white"
|
||||
@ -8455,7 +8707,10 @@ function CharacterImagePanel({ imageUrl, label, wide = false }: { imageUrl: stri
|
||||
|
||||
function PromptComposer({
|
||||
anchor,
|
||||
billingFeature = null,
|
||||
defaultModel = "auto",
|
||||
functionDefinition,
|
||||
membershipSummary,
|
||||
modelKind = "image",
|
||||
modelOptions = [],
|
||||
nodes = [],
|
||||
@ -8463,7 +8718,10 @@ function PromptComposer({
|
||||
referenceThumbnailUrl = CHARACTER_IMAGE_URL,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
billingFeature?: ImageToolCreditFeature | null;
|
||||
defaultModel?: string;
|
||||
functionDefinition?: VideoCanvasFunctionDefinition;
|
||||
membershipSummary?: MembershipSummaryResponse;
|
||||
modelKind?: CanvasModelKind;
|
||||
modelOptions?: CanvasModelOption[];
|
||||
nodes?: CloneNode[];
|
||||
@ -8490,6 +8748,27 @@ function PromptComposer({
|
||||
() => nodes.filter((node) => node.kind !== "script" && node.inputType !== "audio"),
|
||||
[nodes]
|
||||
);
|
||||
const billingDisplay = useMemo(
|
||||
() =>
|
||||
resolveVideoCanvasBillingDisplay({
|
||||
billingFeature,
|
||||
functionDefinition,
|
||||
imageCount: Math.max(1, Number.parseInt(settings.count, 10) || 1),
|
||||
membershipSummary,
|
||||
modelKind,
|
||||
modelOptions,
|
||||
modelRef: settings.model,
|
||||
}),
|
||||
[
|
||||
billingFeature,
|
||||
functionDefinition,
|
||||
membershipSummary,
|
||||
modelKind,
|
||||
modelOptions,
|
||||
settings.count,
|
||||
settings.model,
|
||||
],
|
||||
);
|
||||
|
||||
const insertReference = useCallback((node: CloneNode) => {
|
||||
const label = node.title || "节点";
|
||||
@ -8637,7 +8916,12 @@ function PromptComposer({
|
||||
<ComposerControl active={menu === "translate"} label="文" onClick={() => setMenu((current) => current === "translate" ? null : "translate")} />
|
||||
<ComposerControl active={menu === "settings"} label="" icon="⌘" onClick={() => setMenu((current) => current === "settings" ? null : "settings")} />
|
||||
<ComposerControl active={menu === "count"} label={settings.count} dropdown onClick={() => setMenu((current) => current === "count" ? null : "count")} />
|
||||
<ComposerControl label="14" icon="⚡" muted onClick={() => toast.info("积分估算接口待接入。")} />
|
||||
<ComposerControl
|
||||
label={billingDisplay.label}
|
||||
icon="⚡"
|
||||
muted
|
||||
onClick={() => toast.info(billingDisplay.message)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="absolute bottom-5 right-5 flex h-11 w-11 items-center justify-center rounded-[12px] bg-white/76 text-[24px] text-black hover:bg-white"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import "server-only";
|
||||
|
||||
import { getAiProvidersByType } from "@/lib/db/queries";
|
||||
import { TripoApiError, tripoRequest } from "./client";
|
||||
import type { TripoProviderConfig } from "./types";
|
||||
|
||||
const DEFAULT_TRIPO_BASE_URL = "https://api.tripo3d.ai/v2/openapi";
|
||||
@ -21,6 +22,16 @@ function sanitizeTripoBaseUrl(value: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isTripoTaskNotFoundError(error: unknown): error is TripoApiError {
|
||||
if (!(error instanceof TripoApiError)) return false;
|
||||
if (error.httpStatus === 404) return true;
|
||||
const normalizedMessage = String(error.message || "").toLowerCase();
|
||||
return (
|
||||
normalizedMessage.includes("task is not found") ||
|
||||
normalizedMessage.includes("task not found")
|
||||
);
|
||||
}
|
||||
|
||||
export async function getTripoProviderConfigs(): Promise<TripoProviderConfig[]> {
|
||||
const providers = await getAiProvidersByType({
|
||||
type: "tripo-3d",
|
||||
@ -86,3 +97,58 @@ export async function getTripoProviderConfig(
|
||||
|
||||
throw new Error("未配置 Tripo3D API,请在系统设置-本地AI新增 Tripo3D 提供商");
|
||||
}
|
||||
|
||||
export async function resolveTripoProviderConfigForTaskId(
|
||||
taskId: string,
|
||||
preferredProviderId?: string | null,
|
||||
): Promise<TripoProviderConfig> {
|
||||
const normalizedTaskId = String(taskId || "").trim();
|
||||
if (!normalizedTaskId) {
|
||||
return getTripoProviderConfig(preferredProviderId);
|
||||
}
|
||||
|
||||
const providers = await getTripoProviderConfigs();
|
||||
if (providers.length <= 1) {
|
||||
return getTripoProviderConfig(preferredProviderId);
|
||||
}
|
||||
|
||||
const normalizedPreferredProviderId = String(preferredProviderId || "").trim();
|
||||
const preferredProvider = normalizedPreferredProviderId
|
||||
? providers.find((provider) => provider.providerId === normalizedPreferredProviderId)
|
||||
: null;
|
||||
const orderedProviders = preferredProvider
|
||||
? [
|
||||
preferredProvider,
|
||||
...providers.filter(
|
||||
(provider) => provider.providerId !== preferredProvider.providerId,
|
||||
),
|
||||
]
|
||||
: providers;
|
||||
|
||||
let lastTaskNotFoundError: TripoApiError | null = null;
|
||||
let lastUnexpectedError: unknown = null;
|
||||
|
||||
for (const provider of orderedProviders) {
|
||||
try {
|
||||
await tripoRequest<Record<string, unknown>>(provider, {
|
||||
method: "GET",
|
||||
path: `/task/${encodeURIComponent(normalizedTaskId)}`,
|
||||
});
|
||||
return provider;
|
||||
} catch (error) {
|
||||
if (isTripoTaskNotFoundError(error)) {
|
||||
lastTaskNotFoundError = error;
|
||||
continue;
|
||||
}
|
||||
lastUnexpectedError = error;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastUnexpectedError) {
|
||||
throw lastUnexpectedError;
|
||||
}
|
||||
if (lastTaskNotFoundError) {
|
||||
throw lastTaskNotFoundError;
|
||||
}
|
||||
return getTripoProviderConfig(preferredProviderId);
|
||||
}
|
||||
|
||||
@ -28,12 +28,16 @@ export type VideoCanvasFunctionDefinition = {
|
||||
route: VideoCanvasBackendRoute;
|
||||
enabled: boolean;
|
||||
model?: string;
|
||||
creditCost?: number;
|
||||
prompt: string;
|
||||
description: string;
|
||||
requiresBackend?: boolean;
|
||||
};
|
||||
|
||||
export type VideoCanvasFunctionBillingCopy = {
|
||||
shortLabel: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type VideoCanvasFunctionConfig = {
|
||||
enabled: boolean;
|
||||
defaultImageModel: string;
|
||||
@ -49,7 +53,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"以选中图片作为角色身份、服装、发型、场景光线、镜头距离和画面质感的唯一参考,生成一张 2x2 剧情推演四宫格合成图。画面中只能有 4 个画面区域,只允许一条竖向分割线和一条横向分割线;严禁出现 3 行 3 列、9 个画面区域或九宫格布局。四格都必须保持参考图的近景/半身近中景人物构图,主体占画面主要面积,不要远景、全景、森林空镜或主体很小的画面。四格是同一角色在同一自然光场景中的连续轻微剧情变化:1 正面安静凝视,2 保留参考图抬手/袖摆动作的延续,3 视线或表情轻微变化,4 情绪收束的近中景反应。保持五官、发型、绿色服装、柔和森林逆光、色调和景深一致;每格角度和动作有轻微差异,但不能像 9 个不同机位分镜。输出单张 16:9 图片,2x2 等分排版,分割线清晰干净,不能出现文字、水印、编号、边框标签或 3x3/5x5 宫格。",
|
||||
description: "官方剧情推演四宫格:基于参考图生成 2x2 连续剧情分镜合成图。",
|
||||
@ -60,7 +63,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"以选中图片作为唯一角色参考,生成一张角色三视图设定图。只输出正面、侧面、背面 3 个完整站姿人物,三人横向并排在同一张 16:9 画面中,白色或浅灰干净背景。必须保持同一角色身份、五官、发型、绿色服装、材质、体型比例和配饰一致;正面、侧面、背面视角要清晰可辨,人物全身完整入画,站姿自然。禁止生成 3x3 九宫格、多机位分镜、黑色分割线、重复头像、文字、水印、编号、环境场景或额外人物。",
|
||||
description: "官方角色三视图预设,使用参考图生图/编辑模型。",
|
||||
@ -71,7 +73,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"以选中图片作为角色、服装、发型、场景光线的唯一参考,生成一张 3x3 多机位分镜合成图。必须是 9 个明显不同的镜头画面,不是同一张图裁切或重复人脸。九格分别包含:1 森林环境远景/主体很小,2 全身远景,3 中景正面,4 侧面半身,5 低机位或俯拍,6 面部近景,7 手部/衣袖/动作细节,8 背面或侧背,9 仰视或情绪特写。保持同一角色身份、绿色服装、自然森林光线一致,每格构图、景别、机位和主体大小必须不同。输出为单张 16:9 图片,九格之间使用清晰黑色分割线,不能出现文字、水印、编号,不能白底棚拍。",
|
||||
description: "官方九宫格工具:先生成带黑色分割线的 3x3 分镜图,再由宫格切分拆成单张镜头。",
|
||||
@ -82,7 +83,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"以选中图片作为角色、服装、发型、场景光线和电影质感的唯一参考,生成一张 5x5 连贯分镜合成图。必须是 25 个连续镜头,体现从远景建立场景、人物动作推进、情绪变化、细节特写到结尾反应的完整微型叙事,不是同一张图重复裁切。保持同一角色身份和绿色服装一致,森林自然光线统一;每格景别、机位、主体大小或动作都要有明显差异。输出为单张 16:9 图片,必须严格 5x5 等分排版,25 个宫格的宽高一致、间距一致、边界完整,所有宫格之间使用清晰纯黑分割线,分割线等宽且横线竖线都要贯穿整张图;禁止使用白线、浅色线、柔边、渐变线、不规则拼贴、错位排版、跨格大图、留白边距、文字、水印、编号或额外边框说明。",
|
||||
description: "官方 25 宫格连贯分镜:生成 5x5 连续镜头合成图。",
|
||||
@ -93,7 +93,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"以选中图片为唯一参考,在保持角色身份、发型、服装、姿态和构图一致的前提下,进行电影级光影校正。增强自然逆光、轮廓光、皮肤通透感、背景层次和柔和景深,保留原始角色与场景,不改变人物数量,不生成宫格或分镜。输出单张 16:9 图片,不能出现文字、水印、编号或额外边框。",
|
||||
description: "官方电影级光影校正:对参考图做单张电影感光影重塑。",
|
||||
@ -104,7 +103,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"以选中图片作为当前画面,推演同一镜头时间线 3 秒后的画面。保持角色身份、服装、发型、场景和自然光线一致,只让动作、表情、衣袖/头发摆动、视线或镜头位置发生合理连续变化。输出单张 16:9 图片,不要生成宫格、分镜、文字、水印或编号。",
|
||||
description: "官方画面推演:根据当前帧生成 3 秒后的单帧结果。",
|
||||
@ -115,7 +113,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"以选中图片作为当前画面,反向推演同一镜头时间线 5 秒前的画面。保持角色身份、服装、发型、场景和自然光线一致,只让动作起势、姿态、表情、衣袖/头发位置或镜头位置回到合理的前一状态。输出单张 16:9 图片,不要生成宫格、分镜、文字、水印或编号。",
|
||||
description: "官方画面推演:根据当前帧生成 5 秒前的单帧结果。",
|
||||
@ -126,7 +123,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "utility",
|
||||
route: "local",
|
||||
enabled: true,
|
||||
creditCost: 0,
|
||||
prompt: "按当前图的标准宫格布局进行固定等分切分,将合成图切分为可单独调整的镜头图片。",
|
||||
description: "对齐官方画布:4/9/16/25 和自定义宫格都按当前图的行列固定等分切分,并支持在当前图上多选宫格后继续创建生图或高清节点。",
|
||||
},
|
||||
@ -136,7 +132,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"在保持主体、构图和身份一致的前提下,对图片进行电影级光影重塑,增强轮廓光、层次、对比和空间感,保留自然材质细节。",
|
||||
description: "图片编辑生图模型,复刻官方打光工具。",
|
||||
@ -147,7 +142,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "utility",
|
||||
route: "enhance",
|
||||
enabled: true,
|
||||
creditCost: 8,
|
||||
prompt: "提升图片清晰度、细节和分辨率,保持原图内容不变。",
|
||||
description: "接入现有图片增强接口;高清重绘可在模型字段指定增强 Pro。",
|
||||
},
|
||||
@ -157,7 +151,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "extend",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt:
|
||||
"基于当前图片向四周扩展为 720 全景视野,保持主体与场景一致,补全自然环境和空间透视,避免变形和重复纹理。",
|
||||
description: "优先使用拓图接口,必要时可切到图片编辑生图模型。",
|
||||
@ -168,7 +161,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "utility",
|
||||
route: "extend",
|
||||
enabled: true,
|
||||
creditCost: 10,
|
||||
prompt: "在保持主体不变的前提下扩展画面边界,补全自然背景和构图。",
|
||||
description: "对应官方高清菜单里的扩图。",
|
||||
},
|
||||
@ -178,7 +170,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt: "保持主体结构和身份一致,按照用户补充提示重绘画面局部或整体风格。",
|
||||
description: "对应官方高清菜单里的重绘。",
|
||||
},
|
||||
@ -188,7 +179,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "canvas-generate-image",
|
||||
enabled: true,
|
||||
creditCost: 10,
|
||||
prompt: "根据用户标记或文字说明移除指定元素,并自然补全背景纹理。",
|
||||
description: "当前以前端提示词占位接图片编辑模型,后续可替换为蒙版编辑接口。",
|
||||
},
|
||||
@ -198,7 +188,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "utility",
|
||||
route: "remove-bg",
|
||||
enabled: true,
|
||||
creditCost: 4,
|
||||
prompt: "移除图片背景,保留主体透明或干净背景结果。",
|
||||
description: "接入现有抠图接口。",
|
||||
},
|
||||
@ -208,7 +197,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "utility",
|
||||
route: "local",
|
||||
enabled: true,
|
||||
creditCost: 0,
|
||||
prompt: "按用户选择的裁剪比例或宫格切片范围处理图片。",
|
||||
description: "本地画布裁剪/宫格切分能力。",
|
||||
},
|
||||
@ -218,7 +206,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "image-edit",
|
||||
route: "angle-edit",
|
||||
enabled: true,
|
||||
creditCost: 1,
|
||||
prompt: "根据水平环绕、垂直俯仰和景别缩放参数生成新的角色/主体视角。",
|
||||
description: "接现有 /api/angle-edit,多角度编辑器提交时触发。",
|
||||
},
|
||||
@ -228,7 +215,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "multimodal",
|
||||
route: "canvas-workflow-analyze",
|
||||
enabled: true,
|
||||
creditCost: 6,
|
||||
prompt: "根据输入文本、图片或视频参考,生成结构化分镜脚本,包含镜号、场景、画面、声音和镜头节奏。",
|
||||
description: "多模态大模型/文本模型生成脚本表格。",
|
||||
},
|
||||
@ -238,7 +224,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "multimodal",
|
||||
route: "canvas-workflow-analyze",
|
||||
enabled: true,
|
||||
creditCost: 6,
|
||||
prompt:
|
||||
"解析当前视频的镜头节奏、景别变化、主体动作、背景音乐、人声/音效、关键帧要点,并输出可直接衔接后续图像生成与视频生成节点的结构化故事表。",
|
||||
description: "多模态视频解析:抽取关键帧并输出视频故事表。",
|
||||
@ -249,7 +234,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "multimodal",
|
||||
route: "canvas-workflow-analyze",
|
||||
enabled: true,
|
||||
creditCost: 3,
|
||||
prompt: "分析参考图片,输出结构化中文提示词,包括主体、环境、构图、光影、镜头语言和风格关键词。",
|
||||
description: "多模态识图生成提示词。",
|
||||
},
|
||||
@ -259,7 +243,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "video",
|
||||
route: "canvas-generate-video",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt: "根据文本描述生成短视频,保持镜头运动、节奏和电影级质感。",
|
||||
description: "视频生成模型,走现有视频生成接口。",
|
||||
},
|
||||
@ -269,7 +252,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "video",
|
||||
route: "canvas-generate-video",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt: "以参考图片作为视频首帧,生成自然流畅的短视频。",
|
||||
description: "图片到视频/首帧生视频。",
|
||||
},
|
||||
@ -279,7 +261,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "video",
|
||||
route: "canvas-generate-video",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt: "以两张参考图作为首帧和尾帧,生成首尾连贯的短视频。",
|
||||
description: "首尾帧视频生成。",
|
||||
},
|
||||
@ -289,7 +270,6 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||
kind: "video",
|
||||
route: "canvas-generate-video",
|
||||
enabled: true,
|
||||
creditCost: 14,
|
||||
prompt: "根据上传音频生成对应场景画面,镜头语言、节奏、音乐匹配情绪变化,电影级质感。",
|
||||
description: "官方为音频+参考图到视频;当前先接视频生成提示词和参考图,音频节拍解析接口后续补充。",
|
||||
requiresBackend: true,
|
||||
@ -377,10 +357,6 @@ function normalizeFunctionDefinition(
|
||||
route: (incoming.route as VideoCanvasBackendRoute) || fallback.route,
|
||||
enabled: typeof incoming.enabled === "boolean" ? incoming.enabled : fallback.enabled,
|
||||
model: typeof incoming.model === "string" ? incoming.model : fallback.model,
|
||||
creditCost:
|
||||
typeof incoming.creditCost === "number" && Number.isFinite(incoming.creditCost)
|
||||
? Math.max(0, incoming.creditCost)
|
||||
: fallback.creditCost,
|
||||
prompt: normalizeFunctionPrompt(key, incoming.prompt, fallback.prompt),
|
||||
description: String(incoming.description || fallback.description),
|
||||
requiresBackend:
|
||||
@ -408,7 +384,6 @@ export function normalizeVideoCanvasFunctionConfig(value: unknown): VideoCanvasF
|
||||
kind: (customValue.kind as VideoCanvasFunctionKind) || "workflow",
|
||||
route: (customValue.route as VideoCanvasBackendRoute) || "local",
|
||||
enabled: customValue.enabled !== false,
|
||||
creditCost: typeof customValue.creditCost === "number" ? Math.max(0, customValue.creditCost) : 0,
|
||||
prompt: String(customValue.prompt || ""),
|
||||
description: String(customValue.description || "自定义视频画布功能"),
|
||||
};
|
||||
@ -432,3 +407,75 @@ export function normalizeVideoCanvasFunctionConfig(value: unknown): VideoCanvasF
|
||||
functions,
|
||||
};
|
||||
}
|
||||
|
||||
export function getVideoCanvasFunctionBillingCopy(
|
||||
definition: Pick<VideoCanvasFunctionDefinition, "key" | "route">
|
||||
): VideoCanvasFunctionBillingCopy {
|
||||
if (definition.route === "local") {
|
||||
return {
|
||||
shortLabel: "本地",
|
||||
description: "本地画布工具,不单独扣积分。",
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.key === "character-three-view" || definition.key === "multi-view-nine-grid") {
|
||||
return {
|
||||
shortLabel: "多视角",
|
||||
description: "跟随全站“图片多视角”积分设置,不在视频画布单独改价。",
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.key === "image-repaint") {
|
||||
return {
|
||||
shortLabel: "重绘",
|
||||
description: "跟随全站“图片重绘”积分设置,不在视频画布单独改价。",
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.route === "remove-bg") {
|
||||
return {
|
||||
shortLabel: "抠图",
|
||||
description: "跟随全站“抠图”积分设置,不在视频画布单独改价。",
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.route === "extend") {
|
||||
return {
|
||||
shortLabel: "扩图",
|
||||
description: "跟随全站“图像拓展”积分设置,不在视频画布单独改价。",
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.route === "enhance-pro") {
|
||||
return {
|
||||
shortLabel: "增强Pro",
|
||||
description: "跟随全站“图像增强 Pro”积分设置,不在视频画布单独改价。",
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.route === "enhance") {
|
||||
return {
|
||||
shortLabel: "按站点",
|
||||
description: "沿用站点统一高清/增强能力,不在视频画布单独配置扣点。",
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.route === "canvas-generate-video") {
|
||||
return {
|
||||
shortLabel: "按站点",
|
||||
description: "沿用站点统一视频模型与积分规则,不在视频画布单独改价。",
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.route === "canvas-workflow-analyze" || definition.route === "angle-edit") {
|
||||
return {
|
||||
shortLabel: "按站点",
|
||||
description: "沿用站点统一模型与功能规则,不在视频画布单独改价。",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
shortLabel: "按站点",
|
||||
description: "沿用站点统一图片生成与功能积分规则,不在视频画布单独改价。",
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user