feat: enhance workflow controls and media settings
This commit is contained in:
parent
3775485bdf
commit
6628a6b2d0
@ -193,8 +193,9 @@ const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
|
|||||||
const HOME_CHAT_ANIMATION_MAX_BYTES = 1024 * 1024;
|
const HOME_CHAT_ANIMATION_MAX_BYTES = 1024 * 1024;
|
||||||
const HOME_CHAT_ANIMATION_CONFIG_KEY = "home_chat_animation_config";
|
const HOME_CHAT_ANIMATION_CONFIG_KEY = "home_chat_animation_config";
|
||||||
const VIDEO_PRICING_PREVIEW_RESOLUTIONS = ["480p", "720p", "1080p"] as const;
|
const VIDEO_PRICING_PREVIEW_RESOLUTIONS = ["480p", "720p", "1080p"] as const;
|
||||||
|
type AdminVideoResolution = (typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number];
|
||||||
const DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS: Record<
|
const DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS: Record<
|
||||||
(typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number],
|
AdminVideoResolution,
|
||||||
number
|
number
|
||||||
> = {
|
> = {
|
||||||
"480p": 1,
|
"480p": 1,
|
||||||
@ -202,6 +203,38 @@ const DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS: Record<
|
|||||||
"1080p": 2,
|
"1080p": 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeAdminVideoResolutions(value: unknown): AdminVideoResolution[] {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
const allowed = new Set<string>(VIDEO_PRICING_PREVIEW_RESOLUTIONS);
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
value
|
||||||
|
.map((item) => String(item || "").trim().toLowerCase())
|
||||||
|
.filter((item): item is AdminVideoResolution => allowed.has(item))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdminVideoSupportedResolutions(capabilities: unknown): AdminVideoResolution[] {
|
||||||
|
if (!capabilities || typeof capabilities !== "object") {
|
||||||
|
return [...VIDEO_PRICING_PREVIEW_RESOLUTIONS];
|
||||||
|
}
|
||||||
|
const configured = normalizeAdminVideoResolutions(
|
||||||
|
(capabilities as { supportedResolutions?: unknown }).supportedResolutions
|
||||||
|
);
|
||||||
|
return configured.length ? configured : [...VIDEO_PRICING_PREVIEW_RESOLUTIONS];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAdminVideoSupportedResolutions(
|
||||||
|
capabilities: Record<string, unknown> | null | undefined,
|
||||||
|
nextResolutions: AdminVideoResolution[]
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...(capabilities || {}),
|
||||||
|
supportedResolutions: nextResolutions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function formatCompactNumber(value: number, fractionDigits = 2) {
|
function formatCompactNumber(value: number, fractionDigits = 2) {
|
||||||
if (!Number.isFinite(value)) return "0";
|
if (!Number.isFinite(value)) return "0";
|
||||||
const fixed = value.toFixed(fractionDigits);
|
const fixed = value.toFixed(fractionDigits);
|
||||||
@ -553,6 +586,107 @@ function applyRecommendedEnhanceProPricing(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PointsCostField =
|
||||||
|
| "creditCostPer3DModelPbr"
|
||||||
|
| "creditCostPer3DModelSmartLowPoly"
|
||||||
|
| "creditCostPer3DModelQuad"
|
||||||
|
| "creditCostPer3DModelRefine"
|
||||||
|
| "creditCostPer3DModelTexture"
|
||||||
|
| "creditCostPer3DModelAutoRig"
|
||||||
|
| "creditCostPer3DModelHighpolyToLowpoly"
|
||||||
|
| "creditCostPerImageMultiView"
|
||||||
|
| "creditCostPerImageRemoveBackground"
|
||||||
|
| "creditCostPerImageBoxCutout"
|
||||||
|
| "creditCostPerImageWatermarkRemove"
|
||||||
|
| "creditCostPerImageExtend"
|
||||||
|
| "creditCostPerImageRepaint";
|
||||||
|
|
||||||
|
const THREE_D_GENERATION_POINT_FIELDS: Array<{
|
||||||
|
key: PointsCostField;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
key: "creditCostPer3DModelPbr",
|
||||||
|
label: "PBR 材质",
|
||||||
|
description: "勾选 PBR 材质时,在 3D 模型基础积分上额外扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPer3DModelSmartLowPoly",
|
||||||
|
label: "智能低模",
|
||||||
|
description: "勾选智能低模时额外扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPer3DModelQuad",
|
||||||
|
label: "四边面优化",
|
||||||
|
description: "勾选四边面优化时额外扣除。",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const THREE_D_EDIT_POINT_FIELDS: Array<{
|
||||||
|
key: PointsCostField;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
key: "creditCostPer3DModelRefine",
|
||||||
|
label: "模型精修",
|
||||||
|
description: "提交 refine_model 精修任务时扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPer3DModelTexture",
|
||||||
|
label: "纹理重绘",
|
||||||
|
description: "提交 texture_model 纹理重绘任务时扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPer3DModelAutoRig",
|
||||||
|
label: "自动绑定",
|
||||||
|
description: "提交 animate_rig 自动绑定任务时扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPer3DModelHighpolyToLowpoly",
|
||||||
|
label: "模型减面",
|
||||||
|
description: "提交 highpoly_to_lowpoly 减面任务时扣除。",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const IMAGE_TOOL_POINT_FIELDS: Array<{
|
||||||
|
key: PointsCostField;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
key: "creditCostPerImageMultiView",
|
||||||
|
label: "多视角",
|
||||||
|
description: "角色三视图 / 多机位九宫格等多视角图像生成时扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPerImageRemoveBackground",
|
||||||
|
label: "抠图",
|
||||||
|
description: "调用图片抠图功能时扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPerImageBoxCutout",
|
||||||
|
label: "框选抠图",
|
||||||
|
description: "调用框选抠图功能时扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPerImageWatermarkRemove",
|
||||||
|
label: "去水印",
|
||||||
|
description: "调用去水印功能并成功创建任务后扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPerImageExtend",
|
||||||
|
label: "扩图",
|
||||||
|
description: "调用图片拓展 / 扩图功能时扣除。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "creditCostPerImageRepaint",
|
||||||
|
label: "重绘",
|
||||||
|
description: "调用图片提示词重绘功能时扣除。",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
type AiModel = {
|
type AiModel = {
|
||||||
id: string;
|
id: string;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
@ -572,6 +706,7 @@ type AiModel = {
|
|||||||
isAgentModel?: boolean;
|
isAgentModel?: boolean;
|
||||||
agentCodingModelId?: string;
|
agentCodingModelId?: string;
|
||||||
isMultimodal?: boolean;
|
isMultimodal?: boolean;
|
||||||
|
capabilities?: Record<string, unknown> | null;
|
||||||
imageGenConfig?: ImageGenConfig; // 🆕 图片生成特性配置
|
imageGenConfig?: ImageGenConfig; // 🆕 图片生成特性配置
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1791,6 +1926,7 @@ export function AdminSettingsClient() {
|
|||||||
() =>
|
() =>
|
||||||
enhanceProConfig.apiKeys.map((apiKey, index) => ({
|
enhanceProConfig.apiKeys.map((apiKey, index) => ({
|
||||||
id: `saved-enhance-pro-key-${index}-${apiKey.slice(-6)}`,
|
id: `saved-enhance-pro-key-${index}-${apiKey.slice(-6)}`,
|
||||||
|
index,
|
||||||
name: `Krea Key ${index + 1}`,
|
name: `Krea Key ${index + 1}`,
|
||||||
maskedKey: maskAdminSecret(apiKey),
|
maskedKey: maskAdminSecret(apiKey),
|
||||||
})),
|
})),
|
||||||
@ -2680,6 +2816,7 @@ export function AdminSettingsClient() {
|
|||||||
fallbackModelId1: "", // 备用模型 ID 1
|
fallbackModelId1: "", // 备用模型 ID 1
|
||||||
fallbackModelId2: "", // 备用模型 ID 2
|
fallbackModelId2: "", // 备用模型 ID 2
|
||||||
excludeFromFailover: false,
|
excludeFromFailover: false,
|
||||||
|
capabilities: {} as Record<string, unknown>,
|
||||||
// 🆕 图片生成模型特性配置
|
// 🆕 图片生成模型特性配置
|
||||||
imageGenConfig: createDefaultImageGenConfig(),
|
imageGenConfig: createDefaultImageGenConfig(),
|
||||||
});
|
});
|
||||||
@ -2740,13 +2877,18 @@ export function AdminSettingsClient() {
|
|||||||
const isVideoProviderForModelConfig = isVideoModelDescriptor({
|
const isVideoProviderForModelConfig = isVideoModelDescriptor({
|
||||||
providerType: selectedModelProviderType,
|
providerType: selectedModelProviderType,
|
||||||
modelId: modelForm.modelId,
|
modelId: modelForm.modelId,
|
||||||
|
capabilities: modelForm.capabilities,
|
||||||
supportsImageGen: modelForm.supportsImageGen,
|
supportsImageGen: modelForm.supportsImageGen,
|
||||||
});
|
});
|
||||||
const isImageProviderForModelConfig = isImageGenerationModelDescriptor({
|
const isImageProviderForModelConfig = isImageGenerationModelDescriptor({
|
||||||
providerType: selectedModelProviderType,
|
providerType: selectedModelProviderType,
|
||||||
modelId: modelForm.modelId,
|
modelId: modelForm.modelId,
|
||||||
|
capabilities: modelForm.capabilities,
|
||||||
supportsImageGen: modelForm.supportsImageGen,
|
supportsImageGen: modelForm.supportsImageGen,
|
||||||
});
|
});
|
||||||
|
const modelVideoSupportedResolutions = getAdminVideoSupportedResolutions(
|
||||||
|
modelForm.capabilities
|
||||||
|
);
|
||||||
const configuredVideoDurationOptions = getConfiguredVideoDurationOptions(
|
const configuredVideoDurationOptions = getConfiguredVideoDurationOptions(
|
||||||
modelForm.imageGenConfig
|
modelForm.imageGenConfig
|
||||||
);
|
);
|
||||||
@ -3120,6 +3262,19 @@ export function AdminSettingsClient() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePointsCostField = (key: PointsCostField, value: unknown) => {
|
||||||
|
setSiteSettings((prev) => {
|
||||||
|
const config = normalizePointsSystemConfig(prev.pointsSystemConfig);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
pointsSystemConfig: {
|
||||||
|
...config,
|
||||||
|
[key]: roundCreditCost(value),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const updateEnhanceProPointsBucket = (
|
const updateEnhanceProPointsBucket = (
|
||||||
index: number,
|
index: number,
|
||||||
updater: (item: EnhanceProPointsResolutionBucket) => EnhanceProPointsResolutionBucket
|
updater: (item: EnhanceProPointsResolutionBucket) => EnhanceProPointsResolutionBucket
|
||||||
@ -3570,6 +3725,37 @@ export function AdminSettingsClient() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddEnhanceProPoolKey = () => {
|
||||||
|
const normalizedKey = normalizeEnhanceProKreaConfig({
|
||||||
|
apiKey: enhanceProConfig.apiKey,
|
||||||
|
}).apiKey;
|
||||||
|
|
||||||
|
if (!normalizedKey) {
|
||||||
|
toast.error("请先填写要加入 Key 池的 Krea Bearer Token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnhanceProConfig((prev) => {
|
||||||
|
const nextKeys = normalizeEnhanceProKreaConfig({
|
||||||
|
apiKeys: [...prev.apiKeys, normalizedKey],
|
||||||
|
}).apiKeys;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
apiKey: "",
|
||||||
|
apiKeys: nextKeys,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setEnhanceProApiKeysDraft("");
|
||||||
|
toast.success("Krea Key 已加入池,保存配置后生效");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteEnhanceProPoolKey = (index: number) => {
|
||||||
|
setEnhanceProConfig((prev) => ({
|
||||||
|
...prev,
|
||||||
|
apiKeys: prev.apiKeys.filter((_, itemIndex) => itemIndex !== index),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const handleEnhanceProPresetChange = <K extends keyof EnhanceProPreset>(
|
const handleEnhanceProPresetChange = <K extends keyof EnhanceProPreset>(
|
||||||
index: number,
|
index: number,
|
||||||
key: K,
|
key: K,
|
||||||
@ -5539,6 +5725,7 @@ export function AdminSettingsClient() {
|
|||||||
fallbackModelId1: "",
|
fallbackModelId1: "",
|
||||||
fallbackModelId2: "",
|
fallbackModelId2: "",
|
||||||
excludeFromFailover: false,
|
excludeFromFailover: false,
|
||||||
|
capabilities: {},
|
||||||
imageGenConfig: createDefaultImageGenConfig(),
|
imageGenConfig: createDefaultImageGenConfig(),
|
||||||
});
|
});
|
||||||
setVideoDurationOptionsInput("");
|
setVideoDurationOptionsInput("");
|
||||||
@ -8746,6 +8933,108 @@ export function AdminSettingsClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="rounded-lg border p-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
|
3D 生成附加功能积分
|
||||||
|
</Label>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
这些费用会叠加到“3D模型单次积分消耗”上;填写 0 表示不额外扣分。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
|
{THREE_D_GENERATION_POINT_FIELDS.map((field) => (
|
||||||
|
<div
|
||||||
|
key={field.key}
|
||||||
|
className="rounded-md border bg-muted/20 p-3"
|
||||||
|
>
|
||||||
|
<Label htmlFor={`points-${field.key}`}>
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
<div className="mt-1 min-h-8 text-xs text-muted-foreground">
|
||||||
|
{field.description}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id={`points-${field.key}`}
|
||||||
|
min={0}
|
||||||
|
type="number"
|
||||||
|
value={pointsSystemConfig[field.key]}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePointsCostField(field.key, e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border p-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
|
3D 编辑与优化积分
|
||||||
|
</Label>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
独立 3D 编辑任务按这里配置的单价扣除;填写 0 表示免费。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||||
|
{THREE_D_EDIT_POINT_FIELDS.map((field) => (
|
||||||
|
<div
|
||||||
|
key={field.key}
|
||||||
|
className="rounded-md border bg-muted/20 p-3"
|
||||||
|
>
|
||||||
|
<Label htmlFor={`points-${field.key}`}>
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
<div className="mt-1 min-h-8 text-xs text-muted-foreground">
|
||||||
|
{field.description}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id={`points-${field.key}`}
|
||||||
|
min={0}
|
||||||
|
type="number"
|
||||||
|
value={pointsSystemConfig[field.key]}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePointsCostField(field.key, e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border p-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
|
图像小功能积分
|
||||||
|
</Label>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
多视角、抠图、框选抠图、去水印、扩图、重绘按这里单独扣分;填写 0 表示免费。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
|
{IMAGE_TOOL_POINT_FIELDS.map((field) => (
|
||||||
|
<div
|
||||||
|
key={field.key}
|
||||||
|
className="rounded-md border bg-muted/20 p-3"
|
||||||
|
>
|
||||||
|
<Label htmlFor={`points-${field.key}`}>
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
<div className="mt-1 min-h-8 text-xs text-muted-foreground">
|
||||||
|
{field.description}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id={`points-${field.key}`}
|
||||||
|
min={0}
|
||||||
|
type="number"
|
||||||
|
value={pointsSystemConfig[field.key]}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePointsCostField(field.key, e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="rounded-lg border p-4 space-y-4">
|
<div className="rounded-lg border p-4 space-y-4">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@ -12734,6 +13023,67 @@ export function AdminSettingsClient() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 rounded-lg border bg-muted/20 p-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
前台分辨率显示
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
关闭某个档位后,前台视频参数条不会再显示该分辨率,服务端也会拒绝该档位。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{VIDEO_PRICING_PREVIEW_RESOLUTIONS.map(
|
||||||
|
(resolutionKey) => {
|
||||||
|
const checked = modelVideoSupportedResolutions.includes(
|
||||||
|
resolutionKey
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`video-resolution-${resolutionKey}`}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={(nextChecked) => {
|
||||||
|
const currentSet = new Set(
|
||||||
|
modelVideoSupportedResolutions
|
||||||
|
);
|
||||||
|
if (nextChecked) {
|
||||||
|
currentSet.add(resolutionKey);
|
||||||
|
} else {
|
||||||
|
currentSet.delete(resolutionKey);
|
||||||
|
}
|
||||||
|
const nextResolutions =
|
||||||
|
VIDEO_PRICING_PREVIEW_RESOLUTIONS.filter(
|
||||||
|
(item) => currentSet.has(item)
|
||||||
|
);
|
||||||
|
if (nextResolutions.length === 0) {
|
||||||
|
toast.error(
|
||||||
|
"视频模型至少保留一个可用分辨率"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setModelForm({
|
||||||
|
...modelForm,
|
||||||
|
capabilities:
|
||||||
|
updateAdminVideoSupportedResolutions(
|
||||||
|
modelForm.capabilities,
|
||||||
|
nextResolutions
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label className="text-sm">
|
||||||
|
{resolutionKey}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>支持视频秒数</Label>
|
<Label>支持视频秒数</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -13295,6 +13645,12 @@ export function AdminSettingsClient() {
|
|||||||
(model as any).fallbackModelId2 || "",
|
(model as any).fallbackModelId2 || "",
|
||||||
excludeFromFailover:
|
excludeFromFailover:
|
||||||
(model as any).excludeFromFailover || false,
|
(model as any).excludeFromFailover || false,
|
||||||
|
capabilities:
|
||||||
|
(model as any).capabilities &&
|
||||||
|
typeof (model as any).capabilities === "object" &&
|
||||||
|
!Array.isArray((model as any).capabilities)
|
||||||
|
? (model as any).capabilities
|
||||||
|
: {},
|
||||||
imageGenConfig:
|
imageGenConfig:
|
||||||
(model as any).imageGenConfig || createDefaultImageGenConfig(),
|
(model as any).imageGenConfig || createDefaultImageGenConfig(),
|
||||||
});
|
});
|
||||||
@ -15934,18 +16290,31 @@ export function AdminSettingsClient() {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="enhanceProApiKey">API Key</Label>
|
<Label htmlFor="enhanceProApiKey">API Key</Label>
|
||||||
<Input
|
<div className="flex flex-col gap-2 md:flex-row">
|
||||||
id="enhanceProApiKey"
|
<Input
|
||||||
type="password"
|
id="enhanceProApiKey"
|
||||||
value={enhanceProConfig.apiKey}
|
type="password"
|
||||||
onChange={(e) =>
|
value={enhanceProConfig.apiKey}
|
||||||
setEnhanceProConfig((prev) => ({
|
onChange={(e) =>
|
||||||
...prev,
|
setEnhanceProConfig((prev) => ({
|
||||||
apiKey: e.target.value,
|
...prev,
|
||||||
}))
|
apiKey: e.target.value,
|
||||||
}
|
}))
|
||||||
placeholder="请输入 Krea Bearer Token"
|
}
|
||||||
/>
|
placeholder="请输入 Krea Bearer Token"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleAddEnhanceProPoolKey}
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
添加为新 Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
和 Tripo3D 一样,单个 Key 可以直接加入池;批量导入仍支持一次粘贴多行。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 rounded-xl border p-4">
|
<div className="space-y-4 rounded-xl border p-4">
|
||||||
@ -16066,6 +16435,17 @@ export function AdminSettingsClient() {
|
|||||||
API: {enhanceProConfig.baseUrl || ENHANCE_PRO_KREA_DEFAULT_BASE_URL}
|
API: {enhanceProConfig.baseUrl || ENHANCE_PRO_KREA_DEFAULT_BASE_URL}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteEnhanceProPoolKey(item.index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-1 h-4 w-4" />
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -4663,7 +4663,7 @@ export function AgentWorkflowSettingsClient({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-3 md:grid-cols-3">
|
<CardContent className="grid gap-3 md:grid-cols-4">
|
||||||
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
||||||
<div className="pr-4">
|
<div className="pr-4">
|
||||||
<div className="text-sm font-medium">开启第一步:选择生图模式</div>
|
<div className="text-sm font-medium">开启第一步:选择生图模式</div>
|
||||||
@ -4708,17 +4708,17 @@ export function AgentWorkflowSettingsClient({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
||||||
<div className="pr-4">
|
<div className="pr-4">
|
||||||
<div className="text-sm font-medium">开启套 PSD 模板</div>
|
<div className="text-sm font-medium">开启套 PSD 模板</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
关闭后前台隐藏所有“套PSD”按钮和 PSD 模板编辑弹窗入口。
|
关闭后前台隐藏所有“套PSD”按钮和 PSD 模板编辑弹窗入口。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={workflowPageFeatureSettings.psdTemplateEnabled}
|
checked={workflowPageFeatureSettings.psdTemplateEnabled}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
setConfig((prev) => ({
|
setConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
pageFeatureSettings: {
|
pageFeatureSettings: {
|
||||||
...(prev.pageFeatureSettings ||
|
...(prev.pageFeatureSettings ||
|
||||||
@ -4727,9 +4727,31 @@ export function AgentWorkflowSettingsClient({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
|
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
||||||
|
<div className="pr-4">
|
||||||
|
<div className="text-sm font-medium">开启批量处理</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
关闭后前台顶部导航和无限画布工具栏都隐藏“批量处理”入口。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={workflowPageFeatureSettings.batchProcessingEnabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setConfig((prev) => ({
|
||||||
|
...prev,
|
||||||
|
pageFeatureSettings: {
|
||||||
|
...(prev.pageFeatureSettings ||
|
||||||
|
defaultAgentWorkflowPageFeatureSettings),
|
||||||
|
batchProcessingEnabled: checked,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,8 @@ const imageGenConfigSchema = z.object({
|
|||||||
loadingColor: z.string().optional(),
|
loadingColor: z.string().optional(),
|
||||||
}).optional();
|
}).optional();
|
||||||
|
|
||||||
|
const capabilitiesSchema = z.record(z.string(), z.unknown()).nullable().optional();
|
||||||
|
|
||||||
function normalizeImageGenConfig(config: z.infer<typeof imageGenConfigSchema>) {
|
function normalizeImageGenConfig(config: z.infer<typeof imageGenConfigSchema>) {
|
||||||
if (!config) return config;
|
if (!config) return config;
|
||||||
|
|
||||||
@ -79,6 +81,7 @@ const createModelSchema = z.object({
|
|||||||
isAgentModel: z.boolean().optional(),
|
isAgentModel: z.boolean().optional(),
|
||||||
agentCodingModelId: z.string().max(128).optional(),
|
agentCodingModelId: z.string().max(128).optional(),
|
||||||
isMultimodal: z.boolean().optional(),
|
isMultimodal: z.boolean().optional(),
|
||||||
|
capabilities: capabilitiesSchema,
|
||||||
imageGenConfig: imageGenConfigSchema, // 🆕 图片生成特性配置
|
imageGenConfig: imageGenConfigSchema, // 🆕 图片生成特性配置
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,6 +104,7 @@ const updateModelSchema = z.object({
|
|||||||
isAgentModel: z.boolean().optional(),
|
isAgentModel: z.boolean().optional(),
|
||||||
agentCodingModelId: z.string().max(128).optional(),
|
agentCodingModelId: z.string().max(128).optional(),
|
||||||
isMultimodal: z.boolean().optional(),
|
isMultimodal: z.boolean().optional(),
|
||||||
|
capabilities: capabilitiesSchema,
|
||||||
imageGenConfig: imageGenConfigSchema, // 🆕 图片生成特性配置
|
imageGenConfig: imageGenConfigSchema, // 🆕 图片生成特性配置
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import {
|
import { getPointsSystemConfig } from "@/lib/db/queries";
|
||||||
consumeUserPoints,
|
|
||||||
getPointsSystemConfig,
|
|
||||||
getUserById,
|
|
||||||
} from "@/lib/db/queries";
|
|
||||||
import { resolveThreeDModelRealId } from "@/lib/model3d/model-alias-config";
|
import { resolveThreeDModelRealId } from "@/lib/model3d/model-alias-config";
|
||||||
import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server";
|
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 } from "@/lib/tripo/server";
|
||||||
import { TripoApiError, tripoRequest } from "@/lib/tripo/client";
|
import { TripoApiError, tripoRequest } from "@/lib/tripo/client";
|
||||||
|
|
||||||
@ -109,14 +107,6 @@ function assignMapped(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldCharge3DPoints(taskType: CreateTaskRequest["taskType"]) {
|
|
||||||
return (
|
|
||||||
taskType === "text_to_model" ||
|
|
||||||
taskType === "image_to_model" ||
|
|
||||||
taskType === "multiview_to_model"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@ -359,36 +349,21 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pointsConfig = await getPointsSystemConfig();
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
const shouldChargePoints = shouldCharge3DPoints(taskType);
|
const unitCost = get3DTaskCreditCost(pointsConfig, {
|
||||||
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3DModel))
|
taskType,
|
||||||
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3DModel)))
|
pbr: body.pbr,
|
||||||
: 0;
|
quad: body.quad,
|
||||||
|
smartLowPoly: body.smartLowPoly,
|
||||||
if (
|
});
|
||||||
shouldChargePoints &&
|
const pointsCharge = await prepareFeaturePointsCharge({
|
||||||
pointsConfig.enabled &&
|
userId: session.user.id,
|
||||||
unitCost > 0 &&
|
unitCost,
|
||||||
pointsConfig.blockOnInsufficient !== false
|
reason: "model3d_task",
|
||||||
) {
|
modelId: "tripo-3d",
|
||||||
const userData = await getUserById({ id: session.user.id });
|
note: `3D模型任务消耗积分(${taskType})`,
|
||||||
if (!userData?.isHighVip) {
|
});
|
||||||
const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0));
|
if (!pointsCharge.ok) {
|
||||||
|
return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
|
||||||
if (currentBalance < unitCost) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: "积分不足",
|
|
||||||
code: "insufficient_points",
|
|
||||||
message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${unitCost}。`,
|
|
||||||
balance: currentBalance,
|
|
||||||
expectedCost: unitCost,
|
|
||||||
unitCost,
|
|
||||||
},
|
|
||||||
{ status: 402 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getTripoProviderConfig(
|
const config = await getTripoProviderConfig(
|
||||||
@ -413,17 +388,13 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldChargePoints && pointsConfig.enabled && unitCost > 0) {
|
if (pointsCharge.chargeable) {
|
||||||
try {
|
try {
|
||||||
const consumeResult = await consumeUserPoints({
|
const consumeResult = await pointsCharge.consume({
|
||||||
userId: session.user.id,
|
note: `3D模型任务消耗积分(${taskType} / ${taskId})`,
|
||||||
amount: unitCost,
|
|
||||||
reason: "model3d_generation",
|
|
||||||
modelId: "tripo-3d",
|
|
||||||
note: "3D模型生成消耗积分",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!consumeResult.ok) {
|
if (!consumeResult?.ok) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Points] Failed to consume 3D points. user=${session.user.id} task=${taskId} cost=${unitCost}`
|
`[Points] Failed to consume 3D points. user=${session.user.id} task=${taskId} cost=${unitCost}`
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import {
|
|||||||
executeBoxCutoutJob,
|
executeBoxCutoutJob,
|
||||||
} from "@/lib/ai/box-cutout/service";
|
} from "@/lib/ai/box-cutout/service";
|
||||||
import { RmbgServiceError } from "@/lib/ai/rmbg/service";
|
import { RmbgServiceError } from "@/lib/ai/rmbg/service";
|
||||||
|
import { getPointsSystemConfig } from "@/lib/db/queries";
|
||||||
|
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
|
||||||
|
import { getImageToolCreditCost } from "@/lib/points/config";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
@ -40,6 +43,19 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolvedImageUrl = new URL(rawImageUrl, request.url).toString();
|
const resolvedImageUrl = new URL(rawImageUrl, request.url).toString();
|
||||||
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
|
const pointsCharge = await prepareFeaturePointsCharge({
|
||||||
|
userId: session.user.id,
|
||||||
|
unitCost: getImageToolCreditCost(pointsConfig, "box-cutout"),
|
||||||
|
reason: "image_box_cutout",
|
||||||
|
modelId: "box-cutout",
|
||||||
|
note: "框选抠图消耗积分",
|
||||||
|
pointsConfig,
|
||||||
|
});
|
||||||
|
if (!pointsCharge.ok) {
|
||||||
|
return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
|
||||||
|
}
|
||||||
|
|
||||||
const result = await executeBoxCutoutJob({
|
const result = await executeBoxCutoutJob({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
imageUrl: resolvedImageUrl,
|
imageUrl: resolvedImageUrl,
|
||||||
@ -52,6 +68,14 @@ export async function POST(request: NextRequest) {
|
|||||||
sourceNodeId: body.sourceNodeId,
|
sourceNodeId: body.sourceNodeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (pointsCharge.chargeable) {
|
||||||
|
try {
|
||||||
|
await pointsCharge.consume();
|
||||||
|
} catch (pointsError) {
|
||||||
|
console.error("[Points] Failed to consume box cutout points:", pointsError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(result);
|
return NextResponse.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof BoxCutoutServiceError || error instanceof RmbgServiceError) {
|
if (error instanceof BoxCutoutServiceError || error instanceof RmbgServiceError) {
|
||||||
|
|||||||
@ -9,6 +9,12 @@ import {
|
|||||||
generateCanvasImage,
|
generateCanvasImage,
|
||||||
type CanvasGenerateImageRequest,
|
type CanvasGenerateImageRequest,
|
||||||
} from "@/lib/canvas/generate-image";
|
} from "@/lib/canvas/generate-image";
|
||||||
|
import { getPointsSystemConfig } from "@/lib/db/queries";
|
||||||
|
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
|
||||||
|
import {
|
||||||
|
getImageToolCreditCost,
|
||||||
|
type ImageToolCreditFeature,
|
||||||
|
} from "@/lib/points/config";
|
||||||
|
|
||||||
export const maxDuration = 120;
|
export const maxDuration = 120;
|
||||||
|
|
||||||
@ -16,6 +22,13 @@ interface GenerateImageRequest extends CanvasGenerateImageRequest {
|
|||||||
async?: boolean;
|
async?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeBillingFeature(
|
||||||
|
value: unknown
|
||||||
|
): Extract<ImageToolCreditFeature, "multi-view" | "repaint"> | null {
|
||||||
|
if (value === "multi-view" || value === "repaint") return value;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function toGenerateImagePayload(body: GenerateImageRequest): CanvasGenerateImageRequest {
|
function toGenerateImagePayload(body: GenerateImageRequest): CanvasGenerateImageRequest {
|
||||||
return {
|
return {
|
||||||
prompt: String(body.prompt || "").trim(),
|
prompt: String(body.prompt || "").trim(),
|
||||||
@ -34,6 +47,8 @@ function toGenerateImagePayload(body: GenerateImageRequest): CanvasGenerateImage
|
|||||||
typeof body.imageCount === "number" && Number.isFinite(body.imageCount)
|
typeof body.imageCount === "number" && Number.isFinite(body.imageCount)
|
||||||
? body.imageCount
|
? body.imageCount
|
||||||
: 1,
|
: 1,
|
||||||
|
promptExtend:
|
||||||
|
typeof body.promptExtend === "boolean" ? body.promptExtend : undefined,
|
||||||
referenceImage:
|
referenceImage:
|
||||||
typeof body.referenceImage === "string" ? body.referenceImage : undefined,
|
typeof body.referenceImage === "string" ? body.referenceImage : undefined,
|
||||||
referenceImages: Array.isArray(body.referenceImages)
|
referenceImages: Array.isArray(body.referenceImages)
|
||||||
@ -107,6 +122,30 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const billingFeature = normalizeBillingFeature(body.billingFeature);
|
||||||
|
const pointsCharge = billingFeature
|
||||||
|
? await (async () => {
|
||||||
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
|
return prepareFeaturePointsCharge({
|
||||||
|
userId: session.user.id,
|
||||||
|
unitCost: getImageToolCreditCost(pointsConfig, billingFeature),
|
||||||
|
reason:
|
||||||
|
billingFeature === "multi-view"
|
||||||
|
? "image_multi_view"
|
||||||
|
: "image_repaint",
|
||||||
|
modelId: "canvas-generate-image",
|
||||||
|
note:
|
||||||
|
billingFeature === "multi-view"
|
||||||
|
? "图片多视角生成消耗积分"
|
||||||
|
: "图片重绘消耗积分",
|
||||||
|
pointsConfig,
|
||||||
|
});
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
|
if (pointsCharge && !pointsCharge.ok) {
|
||||||
|
return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
|
||||||
|
}
|
||||||
|
|
||||||
const asyncMode = body.async === true;
|
const asyncMode = body.async === true;
|
||||||
if (asyncMode) {
|
if (asyncMode) {
|
||||||
const task = await createCanvasImageTask({
|
const task = await createCanvasImageTask({
|
||||||
@ -114,6 +153,18 @@ export async function POST(request: Request) {
|
|||||||
request: payload,
|
request: payload,
|
||||||
});
|
});
|
||||||
await enqueueCanvasImageTask(task.id);
|
await enqueueCanvasImageTask(task.id);
|
||||||
|
if (pointsCharge?.chargeable) {
|
||||||
|
try {
|
||||||
|
await pointsCharge.consume({
|
||||||
|
note:
|
||||||
|
billingFeature === "multi-view"
|
||||||
|
? `图片多视角生成消耗积分(${task.id})`
|
||||||
|
: `图片重绘消耗积分(${task.id})`,
|
||||||
|
});
|
||||||
|
} catch (pointsError) {
|
||||||
|
console.error("[Points] Failed to consume canvas image task points:", pointsError);
|
||||||
|
}
|
||||||
|
}
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
@ -122,6 +173,13 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await generateCanvasImage(payload);
|
const result = await generateCanvasImage(payload);
|
||||||
|
if (pointsCharge?.chargeable) {
|
||||||
|
try {
|
||||||
|
await pointsCharge.consume();
|
||||||
|
} catch (pointsError) {
|
||||||
|
console.error("[Points] Failed to consume canvas image points:", pointsError);
|
||||||
|
}
|
||||||
|
}
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: result,
|
data: result,
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { type NextRequest, NextResponse } from "next/server";
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { ExtendServiceError, executeExtendJob } from "@/lib/ai/extend/service";
|
import { ExtendServiceError, executeExtendJob } from "@/lib/ai/extend/service";
|
||||||
|
import { getPointsSystemConfig } from "@/lib/db/queries";
|
||||||
|
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
|
||||||
|
import { getImageToolCreditCost } from "@/lib/points/config";
|
||||||
import {
|
import {
|
||||||
getFriendlyRateLimitMessage,
|
getFriendlyRateLimitMessage,
|
||||||
getUserFacingRequestErrorMessage,
|
getUserFacingRequestErrorMessage,
|
||||||
@ -39,6 +42,19 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
|
const pointsCharge = await prepareFeaturePointsCharge({
|
||||||
|
userId: session.user.id,
|
||||||
|
unitCost: getImageToolCreditCost(pointsConfig, "extend"),
|
||||||
|
reason: "image_extend",
|
||||||
|
modelId: "image-extend",
|
||||||
|
note: "图片扩图消耗积分",
|
||||||
|
pointsConfig,
|
||||||
|
});
|
||||||
|
if (!pointsCharge.ok) {
|
||||||
|
return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
|
||||||
|
}
|
||||||
|
|
||||||
const result = await executeExtendJob({
|
const result = await executeExtendJob({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
@ -50,6 +66,14 @@ export async function POST(request: NextRequest) {
|
|||||||
yScale: body.yScale ? Number(body.yScale) : undefined,
|
yScale: body.yScale ? Number(body.yScale) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (pointsCharge.chargeable) {
|
||||||
|
try {
|
||||||
|
await pointsCharge.consume();
|
||||||
|
} catch (pointsError) {
|
||||||
|
console.error("[Points] Failed to consume extend points:", pointsError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
original_url: result.original_url,
|
original_url: result.original_url,
|
||||||
@ -68,6 +92,19 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
|
const pointsCharge = await prepareFeaturePointsCharge({
|
||||||
|
userId: session.user.id,
|
||||||
|
unitCost: getImageToolCreditCost(pointsConfig, "extend"),
|
||||||
|
reason: "image_extend",
|
||||||
|
modelId: "image-extend",
|
||||||
|
note: "图片扩图消耗积分",
|
||||||
|
pointsConfig,
|
||||||
|
});
|
||||||
|
if (!pointsCharge.ok) {
|
||||||
|
return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
|
||||||
|
}
|
||||||
|
|
||||||
const result = await executeExtendJob({
|
const result = await executeExtendJob({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
imageBase64: buffer.toString("base64"),
|
imageBase64: buffer.toString("base64"),
|
||||||
@ -76,6 +113,14 @@ export async function POST(request: NextRequest) {
|
|||||||
messageId: String(formData.get("messageId") || "") || null,
|
messageId: String(formData.get("messageId") || "") || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (pointsCharge.chargeable) {
|
||||||
|
try {
|
||||||
|
await pointsCharge.consume();
|
||||||
|
} catch (pointsError) {
|
||||||
|
console.error("[Points] Failed to consume extend points:", pointsError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
original_url: result.original_url,
|
original_url: result.original_url,
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import {
|
|||||||
fetchImageBufferFromUrl,
|
fetchImageBufferFromUrl,
|
||||||
RmbgServiceError,
|
RmbgServiceError,
|
||||||
} from "@/lib/ai/rmbg/service";
|
} from "@/lib/ai/rmbg/service";
|
||||||
|
import { getPointsSystemConfig } from "@/lib/db/queries";
|
||||||
|
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
|
||||||
|
import { getImageToolCreditCost } from "@/lib/points/config";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
@ -79,6 +82,19 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
|
const pointsCharge = await prepareFeaturePointsCharge({
|
||||||
|
userId: session.user.id,
|
||||||
|
unitCost: getImageToolCreditCost(pointsConfig, "remove-background"),
|
||||||
|
reason: "image_remove_background",
|
||||||
|
modelId: "rmbg",
|
||||||
|
note: "图片抠图消耗积分",
|
||||||
|
pointsConfig,
|
||||||
|
});
|
||||||
|
if (!pointsCharge.ok) {
|
||||||
|
return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
|
||||||
|
}
|
||||||
|
|
||||||
const result = await executeRmbgJob({
|
const result = await executeRmbgJob({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
imageBuffer,
|
imageBuffer,
|
||||||
@ -90,6 +106,14 @@ export async function POST(request: NextRequest) {
|
|||||||
sourceImageUrl,
|
sourceImageUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (pointsCharge.chargeable) {
|
||||||
|
try {
|
||||||
|
await pointsCharge.consume();
|
||||||
|
} catch (pointsError) {
|
||||||
|
console.error("[Points] Failed to consume RMBG points:", pointsError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
...result,
|
...result,
|
||||||
|
|||||||
@ -1103,6 +1103,7 @@ const updateSchema = z.object({
|
|||||||
presetStepEnabled: z.boolean().optional(),
|
presetStepEnabled: z.boolean().optional(),
|
||||||
materialStepEnabled: z.boolean().optional(),
|
materialStepEnabled: z.boolean().optional(),
|
||||||
psdTemplateEnabled: z.boolean().optional(),
|
psdTemplateEnabled: z.boolean().optional(),
|
||||||
|
batchProcessingEnabled: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
steps: z
|
steps: z
|
||||||
|
|||||||
@ -6,9 +6,12 @@ import { eq } from "drizzle-orm";
|
|||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
import {
|
import {
|
||||||
createMediaAssetRelation,
|
createMediaAssetRelation,
|
||||||
|
getPointsSystemConfig,
|
||||||
getMediaAssetsByUserAndUrls,
|
getMediaAssetsByUserAndUrls,
|
||||||
upsertMediaAsset,
|
upsertMediaAsset,
|
||||||
} from "@/lib/db/queries";
|
} from "@/lib/db/queries";
|
||||||
|
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
|
||||||
|
import { getImageToolCreditCost } from "@/lib/points/config";
|
||||||
|
|
||||||
// PicWish API 需要公网可访问的 HTTP/HTTPS URL
|
// PicWish API 需要公网可访问的 HTTP/HTTPS URL
|
||||||
const WATERMARK_SYNC_WAIT_TIMEOUT_MS = 20_000;
|
const WATERMARK_SYNC_WAIT_TIMEOUT_MS = 20_000;
|
||||||
@ -380,6 +383,19 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
console.log("[WatermarkRemove] 开始处理图片:", publicImageUrl);
|
console.log("[WatermarkRemove] 开始处理图片:", publicImageUrl);
|
||||||
|
|
||||||
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
|
const pointsCharge = await prepareFeaturePointsCharge({
|
||||||
|
userId: session.user.id,
|
||||||
|
unitCost: getImageToolCreditCost(pointsConfig, "watermark-remove"),
|
||||||
|
reason: "image_watermark_remove",
|
||||||
|
modelId: "watermark-remove",
|
||||||
|
note: "去水印消耗积分",
|
||||||
|
pointsConfig,
|
||||||
|
});
|
||||||
|
if (!pointsCharge.ok) {
|
||||||
|
return NextResponse.json(pointsCharge.payload, { status: pointsCharge.status });
|
||||||
|
}
|
||||||
|
|
||||||
// 创建去水印任务
|
// 创建去水印任务
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("sync", "0");
|
formData.append("sync", "0");
|
||||||
@ -416,6 +432,16 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pointsCharge.chargeable) {
|
||||||
|
try {
|
||||||
|
await pointsCharge.consume({
|
||||||
|
note: `去水印任务消耗积分(${taskId})`,
|
||||||
|
});
|
||||||
|
} catch (pointsError) {
|
||||||
|
console.error("[Points] Failed to consume watermark remove points:", pointsError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await finalizeWatermarkTaskResult({
|
const result = await finalizeWatermarkTaskResult({
|
||||||
taskId,
|
taskId,
|
||||||
|
|||||||
@ -1557,6 +1557,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
presetStepEnabled?: boolean;
|
presetStepEnabled?: boolean;
|
||||||
materialStepEnabled?: boolean;
|
materialStepEnabled?: boolean;
|
||||||
psdTemplateEnabled?: boolean;
|
psdTemplateEnabled?: boolean;
|
||||||
|
batchProcessingEnabled?: boolean;
|
||||||
};
|
};
|
||||||
steps?: Array<{
|
steps?: Array<{
|
||||||
key: AgentWorkflowStepKey;
|
key: AgentWorkflowStepKey;
|
||||||
@ -1614,6 +1615,8 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
workflowPageFeatureSettings.presetStepEnabled !== false;
|
workflowPageFeatureSettings.presetStepEnabled !== false;
|
||||||
const isWorkflowMaterialStepEnabled =
|
const isWorkflowMaterialStepEnabled =
|
||||||
workflowPageFeatureSettings.materialStepEnabled !== false;
|
workflowPageFeatureSettings.materialStepEnabled !== false;
|
||||||
|
const isBatchProcessingEnabled =
|
||||||
|
workflowPageFeatureSettings.batchProcessingEnabled !== false;
|
||||||
const sanitizedWorkflowSelections = useMemo(
|
const sanitizedWorkflowSelections = useMemo(
|
||||||
() =>
|
() =>
|
||||||
sanitizeAgentWorkflowSelections({
|
sanitizeAgentWorkflowSelections({
|
||||||
@ -2764,10 +2767,10 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
}, [isAgentMode]);
|
}, [isAgentMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldOpenBatchProcessing) {
|
if (shouldOpenBatchProcessing && isBatchProcessingEnabled) {
|
||||||
setSceneFusionDialogOpen(true);
|
setSceneFusionDialogOpen(true);
|
||||||
}
|
}
|
||||||
}, [shouldOpenBatchProcessing]);
|
}, [isBatchProcessingEnabled, shouldOpenBatchProcessing]);
|
||||||
|
|
||||||
const workflowStorageKey = useMemo(() => `canvas-workflow-state:${id}`, [id]);
|
const workflowStorageKey = useMemo(() => `canvas-workflow-state:${id}`, [id]);
|
||||||
const buildPersistedCanvasData = useCallback(() => {
|
const buildPersistedCanvasData = useCallback(() => {
|
||||||
@ -8458,6 +8461,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
<div className="min-h-screen bg-[#050607] text-white">
|
<div className="min-h-screen bg-[#050607] text-white">
|
||||||
<AgentCanvasTopNavDark
|
<AgentCanvasTopNavDark
|
||||||
activeKey="agent"
|
activeKey="agent"
|
||||||
|
batchProcessingEnabled={isBatchProcessingEnabled}
|
||||||
rightSlot={agentProjectNavSlot}
|
rightSlot={agentProjectNavSlot}
|
||||||
siteLogo={siteSettingsData?.siteLogo}
|
siteLogo={siteSettingsData?.siteLogo}
|
||||||
siteLogoDark={siteSettingsData?.siteLogoDark}
|
siteLogoDark={siteSettingsData?.siteLogoDark}
|
||||||
@ -9614,23 +9618,25 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Tooltip>
|
{isBatchProcessingEnabled ? (
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
className={cn(
|
<Button
|
||||||
isVideoMode
|
className={cn(
|
||||||
? VIDEO_CANVAS_TOOLBAR_BUTTON_CLASS
|
isVideoMode
|
||||||
: "h-9 w-9 rounded-xl"
|
? VIDEO_CANVAS_TOOLBAR_BUTTON_CLASS
|
||||||
)}
|
: "h-9 w-9 rounded-xl"
|
||||||
onClick={() => setSceneFusionDialogOpen(true)}
|
)}
|
||||||
size="icon"
|
onClick={() => setSceneFusionDialogOpen(true)}
|
||||||
variant="ghost"
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
<Layers className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<Layers className="h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipContent side="top">批量处理</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipContent side="top">批量处理</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
Crosshair,
|
||||||
Download,
|
Download,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
@ -615,6 +616,7 @@ export default function CanvasModel3DPage() {
|
|||||||
);
|
);
|
||||||
const [worldViewportMode, setWorldViewportMode] =
|
const [worldViewportMode, setWorldViewportMode] =
|
||||||
useState<WorldViewportMode>("world");
|
useState<WorldViewportMode>("world");
|
||||||
|
const [worldFirstPerson, setWorldFirstPerson] = useState(false);
|
||||||
const [viewerResetToken, setViewerResetToken] = useState(0);
|
const [viewerResetToken, setViewerResetToken] = useState(0);
|
||||||
|
|
||||||
const [taskHistory, setTaskHistory] = useState<TaskRecord[]>([]);
|
const [taskHistory, setTaskHistory] = useState<TaskRecord[]>([]);
|
||||||
@ -2393,6 +2395,12 @@ export default function CanvasModel3DPage() {
|
|||||||
worldViewportMode,
|
worldViewportMode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspaceMode !== "world" || worldViewportMode !== "splat") {
|
||||||
|
setWorldFirstPerson(false);
|
||||||
|
}
|
||||||
|
}, [workspaceMode, worldViewportMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-dvh w-full flex-col overflow-hidden bg-zinc-950 text-zinc-100">
|
<div className="flex h-dvh w-full flex-col overflow-hidden bg-zinc-950 text-zinc-100">
|
||||||
{/* Top Navigation Bar */}
|
{/* Top Navigation Bar */}
|
||||||
@ -3463,16 +3471,34 @@ export default function CanvasModel3DPage() {
|
|||||||
3D 世界控制
|
3D 世界控制
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
className="h-8 rounded-full border-white/10 bg-white/5 px-3 text-zinc-200 hover:bg-white/10 hover:text-white"
|
<Button
|
||||||
onClick={() => setViewerResetToken((prev) => prev + 1)}
|
className={cn(
|
||||||
size="sm"
|
"h-8 rounded-full border-white/10 px-3 text-zinc-200 hover:bg-white/10 hover:text-white",
|
||||||
type="button"
|
worldFirstPerson
|
||||||
variant="outline"
|
? "bg-yellow-500/20 text-yellow-100"
|
||||||
>
|
: "bg-white/5"
|
||||||
<RotateCw className="mr-1.5 h-3.5 w-3.5" />
|
)}
|
||||||
回正
|
disabled={!currentPrimarySpzUrl || worldViewportMode !== "splat"}
|
||||||
</Button>
|
onClick={() => setWorldFirstPerson((prev) => !prev)}
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Crosshair className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
漫游
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="h-8 rounded-full border-white/10 bg-white/5 px-3 text-zinc-200 hover:bg-white/10 hover:text-white"
|
||||||
|
onClick={() => setViewerResetToken((prev) => prev + 1)}
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<RotateCw className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
回正
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||||
@ -3520,10 +3546,12 @@ export default function CanvasModel3DPage() {
|
|||||||
{worldViewportMode === "splat" ? (
|
{worldViewportMode === "splat" ? (
|
||||||
<>
|
<>
|
||||||
<div className="font-medium text-zinc-200">
|
<div className="font-medium text-zinc-200">
|
||||||
鼠标拖拽会围绕场景原点旋转
|
{worldFirstPerson ? "WASD / 方向键可六向移动" : "鼠标拖拽会围绕场景原点旋转"}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
左键拖拽旋转,滚轮缩放;已关闭横向平移。需要切换模型时,直接点击右侧资产卡片。
|
{worldFirstPerson
|
||||||
|
? "点击画面后可锁定鼠标视角,WASD 或方向键移动,Q/R 上升,E/F 下降,Esc 退出鼠标锁定。"
|
||||||
|
: "左键拖拽旋转,滚轮缩放;开启“漫游”后可用 WASD / 方向键移动。需要切换模型时,直接点击右侧资产卡片。"}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : worldViewportMode === "mesh" ? (
|
) : worldViewportMode === "mesh" ? (
|
||||||
@ -3552,6 +3580,7 @@ export default function CanvasModel3DPage() {
|
|||||||
{workspaceMode === "world" ? (
|
{workspaceMode === "world" ? (
|
||||||
<World3DViewer
|
<World3DViewer
|
||||||
emptyStateTitle={viewerEmptyStateTitle}
|
emptyStateTitle={viewerEmptyStateTitle}
|
||||||
|
firstPerson={worldViewportMode === "splat" && worldFirstPerson}
|
||||||
imageUrl={currentWorldImageUrl}
|
imageUrl={currentWorldImageUrl}
|
||||||
meshLayerVisibility={effectiveLayerVisibility}
|
meshLayerVisibility={effectiveLayerVisibility}
|
||||||
meshSettings={viewerSettings}
|
meshSettings={viewerSettings}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ interface AgentCanvasTopNavDarkProps {
|
|||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
rightSlot?: ReactNode;
|
rightSlot?: ReactNode;
|
||||||
activeKey?: "featured" | "agent" | "canvas" | "gallery";
|
activeKey?: "featured" | "agent" | "canvas" | "gallery";
|
||||||
|
batchProcessingEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
@ -48,6 +49,7 @@ export function AgentCanvasTopNavDark({
|
|||||||
onBack,
|
onBack,
|
||||||
rightSlot = null,
|
rightSlot = null,
|
||||||
activeKey = "agent",
|
activeKey = "agent",
|
||||||
|
batchProcessingEnabled = true,
|
||||||
}: AgentCanvasTopNavDarkProps) {
|
}: AgentCanvasTopNavDarkProps) {
|
||||||
const resolvedSiteName = String(siteName || "AI Chatbot").trim() || "AI Chatbot";
|
const resolvedSiteName = String(siteName || "AI Chatbot").trim() || "AI Chatbot";
|
||||||
const displayLogo = siteLogoDark?.trim() || siteLogo?.trim() || "";
|
const displayLogo = siteLogoDark?.trim() || siteLogo?.trim() || "";
|
||||||
@ -94,7 +96,9 @@ export function AgentCanvasTopNavDark({
|
|||||||
|
|
||||||
<div className="ml-auto flex shrink-0 items-center gap-3">
|
<div className="ml-auto flex shrink-0 items-center gap-3">
|
||||||
<nav className="hidden items-center gap-2 md:flex">
|
<nav className="hidden items-center gap-2 md:flex">
|
||||||
{NAV_ITEMS.map((item) => {
|
{NAV_ITEMS.filter(
|
||||||
|
(item) => item.key !== "featured" || batchProcessingEnabled
|
||||||
|
).map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = item.key === activeKey;
|
const isActive = item.key === activeKey;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -4455,6 +4455,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
siteLogoDark={siteLogoDark}
|
siteLogoDark={siteLogoDark}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
activeKey="agent"
|
activeKey="agent"
|
||||||
|
batchProcessingEnabled={resolvedWorkflowFeatureSettings.batchProcessingEnabled}
|
||||||
rightSlot={topNavRightSlot || <div className="w-10" />}
|
rightSlot={topNavRightSlot || <div className="w-10" />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
flattenVisibleAgentWorkflowSteps,
|
flattenVisibleAgentWorkflowSteps,
|
||||||
getAgentWorkflowBlockedOptionKeysForSelections,
|
getAgentWorkflowBlockedOptionKeysForSelections,
|
||||||
getAgentWorkflowBrandStoreEntries,
|
getAgentWorkflowBrandStoreEntries,
|
||||||
|
getAgentWorkflowPageFeatureSettings,
|
||||||
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory,
|
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory,
|
||||||
getAgentWorkflowPriceBandOptions,
|
getAgentWorkflowPriceBandOptions,
|
||||||
getAgentWorkflowThirdCategoryOptionsByBrandStore,
|
getAgentWorkflowThirdCategoryOptionsByBrandStore,
|
||||||
@ -608,6 +609,10 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
steps: getRenderableAgentWorkflowStepTree(source.steps || []),
|
steps: getRenderableAgentWorkflowStepTree(source.steps || []),
|
||||||
};
|
};
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
const pageFeatureSettings = useMemo(
|
||||||
|
() => getAgentWorkflowPageFeatureSettings(config),
|
||||||
|
[config]
|
||||||
|
);
|
||||||
const flattenedResolvedSteps = useMemo(
|
const flattenedResolvedSteps = useMemo(
|
||||||
() =>
|
() =>
|
||||||
flattenVisibleAgentWorkflowSteps(displayConfig.steps, selections).filter(
|
flattenVisibleAgentWorkflowSteps(displayConfig.steps, selections).filter(
|
||||||
@ -1314,6 +1319,7 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
siteLogo={siteLogo}
|
siteLogo={siteLogo}
|
||||||
siteLogoDark={siteLogoDark}
|
siteLogoDark={siteLogoDark}
|
||||||
activeKey="agent"
|
activeKey="agent"
|
||||||
|
batchProcessingEnabled={pageFeatureSettings.batchProcessingEnabled}
|
||||||
rightSlot={topNavRightSlot}
|
rightSlot={topNavRightSlot}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -397,6 +397,7 @@ export function EnhanceProWorkspace({
|
|||||||
const [adjustmentsOpen, setAdjustmentsOpen] = useState(true);
|
const [adjustmentsOpen, setAdjustmentsOpen] = useState(true);
|
||||||
const [faceOpen, setFaceOpen] = useState(true);
|
const [faceOpen, setFaceOpen] = useState(true);
|
||||||
const [previewOffset, setPreviewOffset] = useState({ x: 0, y: 0 });
|
const [previewOffset, setPreviewOffset] = useState({ x: 0, y: 0 });
|
||||||
|
const [comparePosition, setComparePosition] = useState(50);
|
||||||
const [isPreviewDragging, setIsPreviewDragging] = useState(false);
|
const [isPreviewDragging, setIsPreviewDragging] = useState(false);
|
||||||
const previewDragStateRef = useRef<{
|
const previewDragStateRef = useRef<{
|
||||||
pointerId: number;
|
pointerId: number;
|
||||||
@ -434,6 +435,9 @@ export function EnhanceProWorkspace({
|
|||||||
: null;
|
: null;
|
||||||
const activePreviewUrl =
|
const activePreviewUrl =
|
||||||
selectedGeneratedImage?.originalUrl || sourceImageUrl || "";
|
selectedGeneratedImage?.originalUrl || sourceImageUrl || "";
|
||||||
|
const showComparisonPreview = Boolean(
|
||||||
|
selectedGeneratedImage?.originalUrl && sourceImageUrl
|
||||||
|
);
|
||||||
const activePreviewLabel =
|
const activePreviewLabel =
|
||||||
selectedGeneratedImage && selectedResultIndex >= 0
|
selectedGeneratedImage && selectedResultIndex >= 0
|
||||||
? getActiveResultLabel(selectedResultIndex)
|
? getActiveResultLabel(selectedResultIndex)
|
||||||
@ -518,6 +522,10 @@ export function EnhanceProWorkspace({
|
|||||||
previewDragStateRef.current = null;
|
previewDragStateRef.current = null;
|
||||||
}, [activePreviewUrl]);
|
}, [activePreviewUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setComparePosition(50);
|
||||||
|
}, [selectedResultIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formState.face_enhancement) {
|
if (formState.face_enhancement) {
|
||||||
setFaceOpen(true);
|
setFaceOpen(true);
|
||||||
@ -733,16 +741,62 @@ export function EnhanceProWorkspace({
|
|||||||
onPointerMove={handlePreviewPointerMove}
|
onPointerMove={handlePreviewPointerMove}
|
||||||
onPointerUp={finishPreviewDrag}
|
onPointerUp={finishPreviewDrag}
|
||||||
>
|
>
|
||||||
<div className="relative flex max-h-full max-w-full items-center justify-center rounded-[38px] border border-[#e8e8e8] bg-[#fafafa] p-4 shadow-[0_0_0_1px_rgba(0,0,0,0.03),0_24px_56px_rgba(0,0,0,0.08)] backdrop-blur-xl">
|
<div className="relative flex max-h-full max-w-full items-center justify-center rounded-[38px] border border-[#e8e8e8] bg-[#fafafa] p-2 shadow-[0_0_0_1px_rgba(0,0,0,0.03),0_24px_56px_rgba(0,0,0,0.08)] backdrop-blur-xl">
|
||||||
<div className="overflow-hidden rounded-[30px] border border-[#efefef] bg-white shadow-[0_18px_30px_rgba(0,0,0,0.08)]">
|
<div className="relative overflow-hidden rounded-[30px] border border-[#efefef] bg-white shadow-[0_18px_30px_rgba(0,0,0,0.08)]">
|
||||||
<img
|
<div
|
||||||
alt={activePreviewLabel}
|
className="relative"
|
||||||
className="pointer-events-none max-h-[calc(100vh-17rem)] max-w-[min(1000px,calc(100vw-35rem))] object-contain transition-transform duration-75"
|
|
||||||
src={getDisplayImageUrl(activePreviewUrl, 1800)}
|
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(${previewOffset.x}px, ${previewOffset.y}px)`,
|
transform: `translate(${previewOffset.x}px, ${previewOffset.y}px)`,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<img
|
||||||
|
alt={activePreviewLabel}
|
||||||
|
className="pointer-events-none block max-h-[calc(100vh-14rem)] max-w-[min(1200px,calc(100vw-31rem))] object-contain transition-transform duration-75"
|
||||||
|
src={getDisplayImageUrl(activePreviewUrl, 1800)}
|
||||||
|
/>
|
||||||
|
{showComparisonPreview && sourceImageUrl ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
clipPath: `inset(0 ${100 - comparePosition}% 0 0)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="原图对比"
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
src={getDisplayImageUrl(sourceImageUrl, 1800)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-y-0 w-px bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.32),0_0_18px_rgba(0,0,0,0.35)]"
|
||||||
|
style={{ left: `${comparePosition}%` }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{showComparisonPreview ? (
|
||||||
|
<>
|
||||||
|
<div className="pointer-events-none absolute top-3 left-3 rounded-full bg-black/65 px-2.5 py-1 font-medium text-[11px] text-white backdrop-blur-sm">
|
||||||
|
原图
|
||||||
|
</div>
|
||||||
|
<div className="pointer-events-none absolute top-3 right-3 rounded-full bg-black/65 px-2.5 py-1 font-medium text-[11px] text-white backdrop-blur-sm">
|
||||||
|
增强后
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
aria-label="原图和增强结果对比滑块"
|
||||||
|
className="pointer-events-auto absolute right-6 bottom-5 left-6 accent-[#111111]"
|
||||||
|
max={100}
|
||||||
|
min={0}
|
||||||
|
onChange={(event) =>
|
||||||
|
setComparePosition(Number(event.target.value) || 0)
|
||||||
|
}
|
||||||
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
|
type="range"
|
||||||
|
value={comparePosition}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pointer-events-none absolute top-6 left-6 flex items-center gap-2 rounded-full border border-[#e5e5e5] bg-white/96 px-3.5 py-1.5 text-[#666666] text-xs shadow-[0_0_0_1px_rgba(0,0,0,0.03),0_10px_24px_rgba(0,0,0,0.04)] backdrop-blur-sm">
|
<div className="pointer-events-none absolute top-6 left-6 flex items-center gap-2 rounded-full border border-[#e5e5e5] bg-white/96 px-3.5 py-1.5 text-[#666666] text-xs shadow-[0_0_0_1px_rgba(0,0,0,0.03),0_10px_24px_rgba(0,0,0,0.04)] backdrop-blur-sm">
|
||||||
|
|||||||
@ -407,7 +407,13 @@ const OFFICIAL_ZOOM_DOCK_SCALE = 0.65;
|
|||||||
const OFFICIAL_DROPDOWN_SCALE = 0.7;
|
const OFFICIAL_DROPDOWN_SCALE = 0.7;
|
||||||
const OFFICIAL_NINE_GRID_WIDTH = 930;
|
const OFFICIAL_NINE_GRID_WIDTH = 930;
|
||||||
const OFFICIAL_NINE_GRID_HEIGHT = 520;
|
const OFFICIAL_NINE_GRID_HEIGHT = 520;
|
||||||
|
const STORY_BEAT_GRID_FUNCTION_KEY = "story-beat-grid-4";
|
||||||
const MULTI_VIEW_NINE_GRID_FUNCTION_KEY = "multi-view-nine-grid";
|
const MULTI_VIEW_NINE_GRID_FUNCTION_KEY = "multi-view-nine-grid";
|
||||||
|
const CONTINUOUS_STORYBOARD_GRID_FUNCTION_KEY = "continuous-storyboard-grid-25";
|
||||||
|
const CINEMATIC_LIGHTING_CORRECTION_FUNCTION_KEY = "cinematic-lighting-correction";
|
||||||
|
const CHARACTER_THREE_VIEW_FUNCTION_KEY = "character-three-view";
|
||||||
|
const FRAME_PREDICTION_PLUS_3S_FUNCTION_KEY = "frame-prediction-plus-3s";
|
||||||
|
const FRAME_PREDICTION_MINUS_5S_FUNCTION_KEY = "frame-prediction-minus-5s";
|
||||||
const PROMPT_COMPOSER_BASE_WIDTH = 895;
|
const PROMPT_COMPOSER_BASE_WIDTH = 895;
|
||||||
const PROMPT_COMPOSER_BASE_HEIGHT = 286;
|
const PROMPT_COMPOSER_BASE_HEIGHT = 286;
|
||||||
const PRESET_COMPOSER_BASE_HEIGHT = 238;
|
const PRESET_COMPOSER_BASE_HEIGHT = 238;
|
||||||
@ -1033,6 +1039,7 @@ function createMediaResourceNode({
|
|||||||
function createNineGridCompositeNode({
|
function createNineGridCompositeNode({
|
||||||
anchor,
|
anchor,
|
||||||
columns,
|
columns,
|
||||||
|
functionKey = MULTI_VIEW_NINE_GRID_FUNCTION_KEY,
|
||||||
gridPrompt,
|
gridPrompt,
|
||||||
option,
|
option,
|
||||||
rows,
|
rows,
|
||||||
@ -1042,6 +1049,7 @@ function createNineGridCompositeNode({
|
|||||||
}: {
|
}: {
|
||||||
anchor: CloneNode;
|
anchor: CloneNode;
|
||||||
columns: number;
|
columns: number;
|
||||||
|
functionKey?: string;
|
||||||
gridPrompt?: string;
|
gridPrompt?: string;
|
||||||
option?: string;
|
option?: string;
|
||||||
rows: number;
|
rows: number;
|
||||||
@ -1073,6 +1081,7 @@ function createNineGridCompositeNode({
|
|||||||
accent: "#8790ff",
|
accent: "#8790ff",
|
||||||
draftValues: {
|
draftValues: {
|
||||||
action: "IMAGE_GENERATE_NINE_GRID",
|
action: "IMAGE_GENERATE_NINE_GRID",
|
||||||
|
functionKey,
|
||||||
generated: true,
|
generated: true,
|
||||||
gridColumns: columns,
|
gridColumns: columns,
|
||||||
gridPrompt: resolvedGridPrompt,
|
gridPrompt: resolvedGridPrompt,
|
||||||
@ -1084,6 +1093,7 @@ function createNineGridCompositeNode({
|
|||||||
outputSizeLabel: cellCount === 9 ? "2752 × 1536" : "",
|
outputSizeLabel: cellCount === 9 ? "2752 × 1536" : "",
|
||||||
params: {
|
params: {
|
||||||
columns,
|
columns,
|
||||||
|
functionKey,
|
||||||
mode: "grid",
|
mode: "grid",
|
||||||
option: option || "",
|
option: option || "",
|
||||||
prompt: resolvedGridPrompt,
|
prompt: resolvedGridPrompt,
|
||||||
@ -1187,6 +1197,110 @@ function getNodeSourceMediaUrl(node: CloneNode | null | undefined) {
|
|||||||
return getDraftString(node, "sourceUrl") || getNodeMediaUrl(node);
|
return getDraftString(node, "sourceUrl") || getNodeMediaUrl(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOfficialImagePresetFunctionKey(value?: unknown) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
if (!text) return null;
|
||||||
|
if (/角色三视图|三视图生成|正侧背|character[-_\s]?(three|turnaround)/i.test(text)) {
|
||||||
|
return CHARACTER_THREE_VIEW_FUNCTION_KEY;
|
||||||
|
}
|
||||||
|
if (/剧情推演|四宫格|4\s*宫格|2\s*[x×]\s*2/.test(text)) {
|
||||||
|
return STORY_BEAT_GRID_FUNCTION_KEY;
|
||||||
|
}
|
||||||
|
if (/25\s*宫格|连贯分镜|5\s*[x×]\s*5/.test(text)) {
|
||||||
|
return CONTINUOUS_STORYBOARD_GRID_FUNCTION_KEY;
|
||||||
|
}
|
||||||
|
if (/电影级光影|光影校正|cinematic lighting/i.test(text)) {
|
||||||
|
return CINEMATIC_LIGHTING_CORRECTION_FUNCTION_KEY;
|
||||||
|
}
|
||||||
|
if (/3\s*秒后|三秒后|\+\s*3|plus[-_\s]?3/i.test(text)) {
|
||||||
|
return FRAME_PREDICTION_PLUS_3S_FUNCTION_KEY;
|
||||||
|
}
|
||||||
|
if (/5\s*秒前|五秒前|-\s*5|minus[-_\s]?5/i.test(text)) {
|
||||||
|
return FRAME_PREDICTION_MINUS_5S_FUNCTION_KEY;
|
||||||
|
}
|
||||||
|
if (/多机位|九宫格|9\s*宫格|3\s*[x×]\s*3/.test(text)) {
|
||||||
|
return MULTI_VIEW_NINE_GRID_FUNCTION_KEY;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOfficialImagePresetFunctionKey({
|
||||||
|
fallbackKey,
|
||||||
|
option,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
fallbackKey?: string;
|
||||||
|
option?: unknown;
|
||||||
|
title?: unknown;
|
||||||
|
}) {
|
||||||
|
const optionKey = getOfficialImagePresetFunctionKey(option);
|
||||||
|
if (optionKey) return optionKey;
|
||||||
|
const titleKey = getOfficialImagePresetFunctionKey(title);
|
||||||
|
if (titleKey) return titleKey;
|
||||||
|
const normalizedFallbackKey = String(fallbackKey || "").trim();
|
||||||
|
return normalizedFallbackKey || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCharacterThreeViewOption(value?: unknown) {
|
||||||
|
return getOfficialImagePresetFunctionKey(value) === CHARACTER_THREE_VIEW_FUNCTION_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageGenerationBillingFeature(functionKey: string) {
|
||||||
|
if (
|
||||||
|
functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY ||
|
||||||
|
functionKey === CHARACTER_THREE_VIEW_FUNCTION_KEY
|
||||||
|
) {
|
||||||
|
return "multi-view" as const;
|
||||||
|
}
|
||||||
|
if (functionKey === "image-repaint") {
|
||||||
|
return "repaint" as const;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWideImageGenerationFunction(functionKey: string) {
|
||||||
|
return (
|
||||||
|
functionKey === STORY_BEAT_GRID_FUNCTION_KEY ||
|
||||||
|
functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY ||
|
||||||
|
functionKey === CONTINUOUS_STORYBOARD_GRID_FUNCTION_KEY ||
|
||||||
|
functionKey === CINEMATIC_LIGHTING_CORRECTION_FUNCTION_KEY ||
|
||||||
|
functionKey === CHARACTER_THREE_VIEW_FUNCTION_KEY ||
|
||||||
|
functionKey === FRAME_PREDICTION_PLUS_3S_FUNCTION_KEY ||
|
||||||
|
functionKey === FRAME_PREDICTION_MINUS_5S_FUNCTION_KEY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPresetGridLayout(functionKey: string) {
|
||||||
|
if (functionKey === STORY_BEAT_GRID_FUNCTION_KEY) return { columns: 2, rows: 2 };
|
||||||
|
if (functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY) return { columns: 3, rows: 3 };
|
||||||
|
if (functionKey === CONTINUOUS_STORYBOARD_GRID_FUNCTION_KEY) return { columns: 5, rows: 5 };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPresetGridFunction(functionKey: string) {
|
||||||
|
return Boolean(getPresetGridLayout(functionKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeParams(node: CloneNode | null | undefined) {
|
||||||
|
const params = node?.draftValues?.params;
|
||||||
|
return params && typeof params === "object" && !Array.isArray(params)
|
||||||
|
? (params as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeFunctionKey(node: CloneNode | null | undefined) {
|
||||||
|
const params = getNodeParams(node);
|
||||||
|
return String(params.functionKey || node?.draftValues?.functionKey || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCharacterThreeViewNode(node: CloneNode | null | undefined) {
|
||||||
|
if (!node || node.inputType !== "image") return false;
|
||||||
|
return (
|
||||||
|
getNodeFunctionKey(node) === CHARACTER_THREE_VIEW_FUNCTION_KEY ||
|
||||||
|
isCharacterThreeViewOption(node?.title)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getConfiguredFunction(
|
function getConfiguredFunction(
|
||||||
config: VideoCanvasFunctionConfig,
|
config: VideoCanvasFunctionConfig,
|
||||||
key: string,
|
key: string,
|
||||||
@ -1293,19 +1407,23 @@ function composeFunctionPrompt(
|
|||||||
return `${base}\n\n用户补充:${extra}`;
|
return `${base}\n\n用户补充:${extra}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function composeConfiguredImagePresetPrompt(
|
||||||
|
definition: VideoCanvasFunctionDefinition,
|
||||||
|
userPrompt?: string,
|
||||||
|
) {
|
||||||
|
const base = definition.prompt.trim() || getDefaultFunctionPrompt(definition.key);
|
||||||
|
const extra = String(userPrompt || "").trim();
|
||||||
|
const extraFunctionKey = getOfficialImagePresetFunctionKey(extra);
|
||||||
|
if (!extra || extra === base || extra === definition.label || extraFunctionKey === definition.key) return base;
|
||||||
|
if (!base) return extra;
|
||||||
|
return `${base}\n\n用户补充:${extra}`;
|
||||||
|
}
|
||||||
|
|
||||||
function composeMultiViewNineGridPrompt(
|
function composeMultiViewNineGridPrompt(
|
||||||
definition: VideoCanvasFunctionDefinition,
|
definition: VideoCanvasFunctionDefinition,
|
||||||
option?: string,
|
option?: string,
|
||||||
) {
|
) {
|
||||||
const configuredPrompt = definition.prompt.trim() || getDefaultFunctionPrompt(MULTI_VIEW_NINE_GRID_FUNCTION_KEY);
|
return composeConfiguredImagePresetPrompt(definition, option);
|
||||||
const selectedOption = String(option || "").trim();
|
|
||||||
const parts = [configuredPrompt];
|
|
||||||
|
|
||||||
if (selectedOption) {
|
|
||||||
parts.push(`用户选择:${selectedOption}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join("\n\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFirstGeneratedImageUrl(
|
function getFirstGeneratedImageUrl(
|
||||||
@ -1616,15 +1734,50 @@ function normalizeTargetModuleNode(node: CloneNode, kind: AddModuleKind, fallbac
|
|||||||
|
|
||||||
function normalizeRestoredNode(node: CloneNode, fallbackIndex: number): CloneNode {
|
function normalizeRestoredNode(node: CloneNode, fallbackIndex: number): CloneNode {
|
||||||
const action = getDraftString(node, "action");
|
const action = getDraftString(node, "action");
|
||||||
|
const isCharacterThreeViewResultNode = isCharacterThreeViewNode(node);
|
||||||
const isNineGridResultNode =
|
const isNineGridResultNode =
|
||||||
action === "IMAGE_GENERATE_NINE_GRID" ||
|
!isCharacterThreeViewResultNode &&
|
||||||
Boolean(node.draftValues?.nineGridComposite) ||
|
(action === "IMAGE_GENERATE_NINE_GRID" ||
|
||||||
(node.inputType === "image" && /多机位九宫格/.test(node.title || ""));
|
Boolean(node.draftValues?.nineGridComposite) ||
|
||||||
|
(node.inputType === "image" && /多机位九宫格/.test(node.title || "")));
|
||||||
if (action === "TEXT_RESOURCE" && !getPresetMode(node)) {
|
if (action === "TEXT_RESOURCE" && !getPresetMode(node)) {
|
||||||
return normalizeTextResourceNode(node, fallbackIndex);
|
return normalizeTextResourceNode(node, fallbackIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCharacterThreeViewResultNode) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
width: OFFICIAL_NINE_GRID_WIDTH,
|
||||||
|
height: OFFICIAL_NINE_GRID_HEIGHT,
|
||||||
|
draftValues: {
|
||||||
|
...(node.draftValues || {}),
|
||||||
|
action: "IMAGE_GENERATE",
|
||||||
|
functionKey: CHARACTER_THREE_VIEW_FUNCTION_KEY,
|
||||||
|
gridColumns: undefined,
|
||||||
|
gridRows: undefined,
|
||||||
|
nineGridCells: undefined,
|
||||||
|
nineGridComposite: false,
|
||||||
|
outputSizeLabel: getDraftString(node, "outputSizeLabel") || "2752 × 1536",
|
||||||
|
params: {
|
||||||
|
...getNodeParams(node),
|
||||||
|
functionKey: CHARACTER_THREE_VIEW_FUNCTION_KEY,
|
||||||
|
mode: "character-three-view",
|
||||||
|
prompt: getDraftString(node, "prompt") || getDefaultFunctionPrompt(CHARACTER_THREE_VIEW_FUNCTION_KEY),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (isNineGridResultNode) {
|
if (isNineGridResultNode) {
|
||||||
|
const params = getNodeParams(node);
|
||||||
|
const gridFunctionKey =
|
||||||
|
resolveOfficialImagePresetFunctionKey({
|
||||||
|
fallbackKey: getNodeFunctionKey(node),
|
||||||
|
option: params.option || node.draftValues?.toolbarOption,
|
||||||
|
title: node.title,
|
||||||
|
}) ||
|
||||||
|
MULTI_VIEW_NINE_GRID_FUNCTION_KEY;
|
||||||
|
const gridLayout = getPresetGridLayout(gridFunctionKey);
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
width: OFFICIAL_NINE_GRID_WIDTH,
|
width: OFFICIAL_NINE_GRID_WIDTH,
|
||||||
@ -1632,9 +1785,18 @@ function normalizeRestoredNode(node: CloneNode, fallbackIndex: number): CloneNod
|
|||||||
draftValues: {
|
draftValues: {
|
||||||
...(node.draftValues || {}),
|
...(node.draftValues || {}),
|
||||||
action: "IMAGE_GENERATE_NINE_GRID",
|
action: "IMAGE_GENERATE_NINE_GRID",
|
||||||
gridPrompt: getDraftString(node, "gridPrompt") || getDefaultFunctionPrompt(MULTI_VIEW_NINE_GRID_FUNCTION_KEY),
|
functionKey: gridFunctionKey,
|
||||||
|
gridColumns: getDraftNumber(node, "gridColumns", gridLayout?.columns || 3),
|
||||||
|
gridPrompt: getDraftString(node, "gridPrompt") || getDefaultFunctionPrompt(gridFunctionKey),
|
||||||
|
gridRows: getDraftNumber(node, "gridRows", gridLayout?.rows || 3),
|
||||||
nineGridComposite: true,
|
nineGridComposite: true,
|
||||||
outputSizeLabel: getDraftString(node, "outputSizeLabel") || "2752 × 1536",
|
outputSizeLabel: getDraftString(node, "outputSizeLabel") || "2752 × 1536",
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
functionKey: gridFunctionKey,
|
||||||
|
mode: "grid",
|
||||||
|
prompt: getDraftString(node, "gridPrompt") || getDefaultFunctionPrompt(gridFunctionKey),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1764,8 +1926,9 @@ function createStarterWorkflow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (starterTitle === "角色三视图") {
|
if (starterTitle === "角色三视图") {
|
||||||
const preset = "character-three-view";
|
const preset = CHARACTER_THREE_VIEW_FUNCTION_KEY;
|
||||||
const distance = 100;
|
const distance = 100;
|
||||||
|
const prompt = getDefaultFunctionPrompt(CHARACTER_THREE_VIEW_FUNCTION_KEY);
|
||||||
const left = createPresetMediaNode({
|
const left = createPresetMediaNode({
|
||||||
action: "IMAGE_RESOURCE",
|
action: "IMAGE_RESOURCE",
|
||||||
height: 520,
|
height: 520,
|
||||||
@ -1777,18 +1940,32 @@ function createStarterWorkflow(
|
|||||||
x: Math.round(center.x - 874 - distance / 2),
|
x: Math.round(center.x - 874 - distance / 2),
|
||||||
y: Math.round(center.y - 320),
|
y: Math.round(center.y - 320),
|
||||||
});
|
});
|
||||||
const right = createPresetMediaNode({
|
const rightBase = createPresetMediaNode({
|
||||||
action: "IMAGE_GENERATE",
|
action: "IMAGE_GENERATE",
|
||||||
generated: true,
|
generated: true,
|
||||||
height: 520,
|
height: 520,
|
||||||
mediaType: "image",
|
mediaType: "image",
|
||||||
mediaUrl: CHARACTER_THREE_VIEW_URL,
|
mediaUrl: CHARACTER_THREE_VIEW_URL,
|
||||||
preset,
|
preset,
|
||||||
title: "角色三视图",
|
prompt,
|
||||||
width: 660,
|
title: "角色三视图生成",
|
||||||
|
width: OFFICIAL_NINE_GRID_WIDTH,
|
||||||
x: Math.round(center.x + distance / 2),
|
x: Math.round(center.x + distance / 2),
|
||||||
y: Math.round(center.y - 320),
|
y: Math.round(center.y - 320),
|
||||||
});
|
});
|
||||||
|
const right: CloneNode = {
|
||||||
|
...rightBase,
|
||||||
|
draftValues: {
|
||||||
|
...(rightBase.draftValues || {}),
|
||||||
|
functionKey: CHARACTER_THREE_VIEW_FUNCTION_KEY,
|
||||||
|
params: {
|
||||||
|
...getNodeParams(rightBase),
|
||||||
|
functionKey: CHARACTER_THREE_VIEW_FUNCTION_KEY,
|
||||||
|
mode: "character-three-view",
|
||||||
|
prompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
return { nodes: [left, right], edges: makePresetEdges([left], right), selectedId: right.id };
|
return { nodes: [left, right], edges: makePresetEdges([left], right), selectedId: right.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2681,67 +2858,215 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
};
|
};
|
||||||
const title = labels[action];
|
const title = labels[action];
|
||||||
if (action === "grid") {
|
if (action === "grid") {
|
||||||
const { columns, rows } = normalizeGridOption(option || "9宫格");
|
const functionKey =
|
||||||
const gridDefinition = getConfiguredFunction(videoCanvasFunctionConfig, MULTI_VIEW_NINE_GRID_FUNCTION_KEY);
|
getOfficialImagePresetFunctionKey(option) || MULTI_VIEW_NINE_GRID_FUNCTION_KEY;
|
||||||
const prompt = composeMultiViewNineGridPrompt(gridDefinition, option);
|
const definition = getConfiguredFunction(videoCanvasFunctionConfig, functionKey);
|
||||||
const gridNode = createNineGridCompositeNode({
|
const prompt =
|
||||||
anchor,
|
functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
||||||
columns,
|
? composeMultiViewNineGridPrompt(definition, option)
|
||||||
gridPrompt: prompt,
|
: composeConfiguredImagePresetPrompt(definition, option);
|
||||||
option,
|
const shouldGenerate = videoCanvasFunctionConfig.enabled && definition.enabled;
|
||||||
rows,
|
const resultTitle = option?.trim() || definition.label || title;
|
||||||
sourceUrl,
|
const gridLayout = getPresetGridLayout(functionKey);
|
||||||
|
|
||||||
|
if (gridLayout) {
|
||||||
|
const gridNode = createNineGridCompositeNode({
|
||||||
|
anchor,
|
||||||
|
columns: gridLayout.columns,
|
||||||
|
functionKey,
|
||||||
|
gridPrompt: prompt,
|
||||||
|
option: resultTitle,
|
||||||
|
rows: gridLayout.rows,
|
||||||
|
sourceUrl,
|
||||||
|
x: rightX,
|
||||||
|
y: centerY(OFFICIAL_NINE_GRID_HEIGHT),
|
||||||
|
});
|
||||||
|
const pendingGridNode: CloneNode = {
|
||||||
|
...gridNode,
|
||||||
|
draftValues: {
|
||||||
|
...(gridNode.draftValues || {}),
|
||||||
|
functionKey,
|
||||||
|
generated: !shouldGenerate,
|
||||||
|
generationProgress: shouldGenerate ? 2 : 100,
|
||||||
|
generationStatus: shouldGenerate ? "pending" : "complete",
|
||||||
|
gridPrompt: prompt,
|
||||||
|
mediaUrl: shouldGenerate ? "" : getDraftString(gridNode, "mediaUrl"),
|
||||||
|
params: {
|
||||||
|
...getNodeParams(gridNode),
|
||||||
|
functionKey,
|
||||||
|
mode: "grid",
|
||||||
|
option: resultTitle,
|
||||||
|
prompt,
|
||||||
|
sourceNodeId: anchor.id,
|
||||||
|
sourceUrl,
|
||||||
|
},
|
||||||
|
pendingLabel: "生成中",
|
||||||
|
prompt,
|
||||||
|
sourceNodeId: anchor.id,
|
||||||
|
sourceUrl,
|
||||||
|
toolbarOption: resultTitle,
|
||||||
|
url: shouldGenerate ? [] : gridNode.draftValues?.url,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((current) => [...current, pendingGridNode]);
|
||||||
|
setEdges((current) => [...current, { id: makeId("edge"), source: anchor.id, target: pendingGridNode.id }]);
|
||||||
|
setSelectedNodeId(pendingGridNode.id);
|
||||||
|
setCanvasCommand(null);
|
||||||
|
setNodeContextMenu(null);
|
||||||
|
setConnectionMenu(null);
|
||||||
|
setActivePanel(null);
|
||||||
|
|
||||||
|
if (shouldGenerate) {
|
||||||
|
const response = await imageGenerationService.generateImage({
|
||||||
|
aspectRatio: "16:9",
|
||||||
|
billingFeature: getImageGenerationBillingFeature(functionKey),
|
||||||
|
imageCount: 1,
|
||||||
|
model: getFunctionModel(videoCanvasFunctionConfig, definition),
|
||||||
|
prompt,
|
||||||
|
promptExtend: false,
|
||||||
|
referenceImage: sourceUrl,
|
||||||
|
});
|
||||||
|
const generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : "";
|
||||||
|
if (generatedUrl) {
|
||||||
|
setNodes((current) =>
|
||||||
|
current.map((node) =>
|
||||||
|
node.id === pendingGridNode.id && getDraftString(node, "generationStatus") === "pending"
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
draftValues: {
|
||||||
|
...(node.draftValues || {}),
|
||||||
|
functionKey,
|
||||||
|
generated: true,
|
||||||
|
generationProgress: 100,
|
||||||
|
generationStatus: "complete",
|
||||||
|
gridPrompt: prompt,
|
||||||
|
mediaUrl: generatedUrl,
|
||||||
|
params: {
|
||||||
|
...getNodeParams(node),
|
||||||
|
functionKey,
|
||||||
|
mode: "grid",
|
||||||
|
option: resultTitle,
|
||||||
|
prompt,
|
||||||
|
},
|
||||||
|
prompt,
|
||||||
|
sourceNodeId: anchor.id,
|
||||||
|
sourceUrl,
|
||||||
|
toolbarOption: resultTitle,
|
||||||
|
url: [generatedUrl],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const errorMessage = response.error || `${definition.label}生成未返回结果`;
|
||||||
|
setNodes((current) =>
|
||||||
|
current.map((node) =>
|
||||||
|
node.id === pendingGridNode.id && getDraftString(node, "generationStatus") === "pending"
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
draftValues: {
|
||||||
|
...(node.draftValues || {}),
|
||||||
|
generationStatus: "failed",
|
||||||
|
generationError: errorMessage,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
toast.error(`${errorMessage},已保留占位节点`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toast.success(shouldGenerate ? `已创建${definition.label}生成中占位节点` : `已创建${definition.label}节点`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultNode = createMediaResourceNode({
|
||||||
|
action: "IMAGE_GENERATE",
|
||||||
|
height: OFFICIAL_NINE_GRID_HEIGHT,
|
||||||
|
mediaType: "image",
|
||||||
|
mediaUrl: "",
|
||||||
|
prompt,
|
||||||
|
title: resultTitle,
|
||||||
|
width: OFFICIAL_NINE_GRID_WIDTH,
|
||||||
x: rightX,
|
x: rightX,
|
||||||
y: centerY(OFFICIAL_NINE_GRID_HEIGHT),
|
y: centerY(OFFICIAL_NINE_GRID_HEIGHT),
|
||||||
});
|
});
|
||||||
const shouldGenerate = videoCanvasFunctionConfig.enabled && gridDefinition?.enabled;
|
const pendingNode: CloneNode = {
|
||||||
const pendingGridNode: CloneNode = {
|
...resultNode,
|
||||||
...gridNode,
|
subtitle: shouldGenerate ? "生成中" : "生成结果",
|
||||||
draftValues: {
|
draftValues: {
|
||||||
...(gridNode.draftValues || {}),
|
...(resultNode.draftValues || {}),
|
||||||
generated: !shouldGenerate,
|
action: "IMAGE_GENERATE",
|
||||||
generationProgress: shouldGenerate ? 2 : 100,
|
functionKey,
|
||||||
generationStatus: shouldGenerate ? "pending" : "complete",
|
generated: false,
|
||||||
mediaUrl: shouldGenerate ? "" : getDraftString(gridNode, "mediaUrl"),
|
generationProgress: shouldGenerate ? 2 : 0,
|
||||||
|
generationStatus: shouldGenerate ? "pending" : "idle",
|
||||||
|
mediaType: "image",
|
||||||
|
mediaUrl: "",
|
||||||
|
nodeType: "image",
|
||||||
|
outputSizeLabel: "2752 × 1536",
|
||||||
|
params: {
|
||||||
|
functionKey,
|
||||||
|
mode: functionKey === CHARACTER_THREE_VIEW_FUNCTION_KEY ? "character-three-view" : "image",
|
||||||
|
option: resultTitle,
|
||||||
|
prompt,
|
||||||
|
sourceNodeId: anchor.id,
|
||||||
|
sourceUrl,
|
||||||
|
},
|
||||||
pendingLabel: "生成中",
|
pendingLabel: "生成中",
|
||||||
|
prompt,
|
||||||
sourceNodeId: anchor.id,
|
sourceNodeId: anchor.id,
|
||||||
sourceUrl,
|
sourceUrl,
|
||||||
url: shouldGenerate ? [] : gridNode.draftValues?.url,
|
toolbarOption: resultTitle,
|
||||||
|
url: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
setNodes((current) => [...current, pendingGridNode]);
|
setNodes((current) => [...current, pendingNode]);
|
||||||
setEdges((current) => [...current, { id: makeId("edge"), source: anchor.id, target: pendingGridNode.id }]);
|
setEdges((current) => [...current, { id: makeId("edge"), source: anchor.id, target: pendingNode.id }]);
|
||||||
setSelectedNodeId(pendingGridNode.id);
|
setSelectedNodeId(pendingNode.id);
|
||||||
setCanvasCommand(null);
|
setCanvasCommand(null);
|
||||||
setNodeContextMenu(null);
|
setNodeContextMenu(null);
|
||||||
setConnectionMenu(null);
|
setConnectionMenu(null);
|
||||||
setActivePanel(null);
|
setActivePanel(null);
|
||||||
|
|
||||||
if (videoCanvasFunctionConfig.enabled && gridDefinition?.enabled) {
|
if (shouldGenerate) {
|
||||||
const response = await imageGenerationService.generateImage({
|
const response = await imageGenerationService.generateImage({
|
||||||
aspectRatio: "16:9",
|
aspectRatio: "16:9",
|
||||||
|
billingFeature: getImageGenerationBillingFeature(functionKey),
|
||||||
imageCount: 1,
|
imageCount: 1,
|
||||||
model: getFunctionModel(videoCanvasFunctionConfig, gridDefinition),
|
model: getFunctionModel(videoCanvasFunctionConfig, definition),
|
||||||
prompt,
|
prompt,
|
||||||
|
promptExtend: false,
|
||||||
referenceImage: sourceUrl,
|
referenceImage: sourceUrl,
|
||||||
});
|
});
|
||||||
const generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : "";
|
const generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : "";
|
||||||
if (generatedUrl) {
|
if (generatedUrl) {
|
||||||
setNodes((current) =>
|
setNodes((current) =>
|
||||||
current.map((node) =>
|
current.map((node) =>
|
||||||
node.id === pendingGridNode.id && getDraftString(node, "generationStatus") === "pending"
|
node.id === pendingNode.id && getDraftString(node, "generationStatus") === "pending"
|
||||||
? {
|
? {
|
||||||
...node,
|
...node,
|
||||||
|
subtitle: "生成结果",
|
||||||
draftValues: {
|
draftValues: {
|
||||||
...(node.draftValues || {}),
|
...(node.draftValues || {}),
|
||||||
|
functionKey,
|
||||||
generated: true,
|
generated: true,
|
||||||
generationProgress: 100,
|
generationProgress: 100,
|
||||||
generationStatus: "complete",
|
generationStatus: "complete",
|
||||||
mediaUrl: generatedUrl,
|
mediaUrl: generatedUrl,
|
||||||
|
params: {
|
||||||
|
...getNodeParams(node),
|
||||||
|
functionKey,
|
||||||
|
option: resultTitle,
|
||||||
|
prompt,
|
||||||
|
},
|
||||||
prompt,
|
prompt,
|
||||||
sourceNodeId: anchor.id,
|
sourceNodeId: anchor.id,
|
||||||
sourceUrl,
|
sourceUrl,
|
||||||
|
toolbarOption: resultTitle,
|
||||||
url: [generatedUrl],
|
url: [generatedUrl],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -2749,10 +3074,10 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = response.error || "九宫格生成未返回结果";
|
const errorMessage = response.error || `${definition.label}生成未返回结果`;
|
||||||
setNodes((current) =>
|
setNodes((current) =>
|
||||||
current.map((node) =>
|
current.map((node) =>
|
||||||
node.id === pendingGridNode.id && getDraftString(node, "generationStatus") === "pending"
|
node.id === pendingNode.id && getDraftString(node, "generationStatus") === "pending"
|
||||||
? {
|
? {
|
||||||
...node,
|
...node,
|
||||||
draftValues: {
|
draftValues: {
|
||||||
@ -2764,10 +3089,10 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
: node
|
: node
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
toast.error(`${errorMessage},已保留可切分占位节点`);
|
toast.error(`${errorMessage},已保留占位节点`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toast.success(shouldGenerate ? "已创建生成中占位节点" : "已按预设提示词生成多机位九宫格结果图");
|
toast.success(shouldGenerate ? `已创建${definition.label}生成中占位节点` : `已创建${definition.label}节点`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2844,9 +3169,11 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
} else if (definition.route === "canvas-generate-image") {
|
} else if (definition.route === "canvas-generate-image") {
|
||||||
const response = await imageGenerationService.generateImage({
|
const response = await imageGenerationService.generateImage({
|
||||||
aspectRatio: "16:9",
|
aspectRatio: "16:9",
|
||||||
|
billingFeature: getImageGenerationBillingFeature(functionKey),
|
||||||
imageCount: 1,
|
imageCount: 1,
|
||||||
model: getFunctionModel(videoCanvasFunctionConfig, definition),
|
model: getFunctionModel(videoCanvasFunctionConfig, definition),
|
||||||
prompt,
|
prompt,
|
||||||
|
promptExtend: false,
|
||||||
referenceImage: sourceUrl,
|
referenceImage: sourceUrl,
|
||||||
});
|
});
|
||||||
generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : "";
|
generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : "";
|
||||||
@ -2955,30 +3282,40 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
const sourceUrl = getNodeMediaUrl(anchor) || CHARACTER_IMAGE_URL;
|
const sourceUrl = getNodeMediaUrl(anchor) || CHARACTER_IMAGE_URL;
|
||||||
const rightX = Math.round(anchor.x + anchor.width + OFFICIAL_NODE_GAP);
|
const rightX = Math.round(anchor.x + anchor.width + OFFICIAL_NODE_GAP);
|
||||||
const starterMode = getPresetMode(anchor);
|
const starterMode = getPresetMode(anchor);
|
||||||
|
const slashScene = payload.slashScene || "";
|
||||||
|
const promptInput = payload.prompt.trim();
|
||||||
|
const slashFunctionKey = getOfficialImagePresetFunctionKey(slashScene);
|
||||||
const functionKey =
|
const functionKey =
|
||||||
payload.slashScene && /九宫格|宫格|多机位/.test(payload.slashScene)
|
slashFunctionKey ||
|
||||||
? MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
(starterMode === CHARACTER_THREE_VIEW_FUNCTION_KEY
|
||||||
: starterMode === "character-three-view"
|
? CHARACTER_THREE_VIEW_FUNCTION_KEY
|
||||||
? "character-three-view"
|
: "image-repaint");
|
||||||
: "image-repaint";
|
|
||||||
const definition = getConfiguredFunction(videoCanvasFunctionConfig, functionKey);
|
const definition = getConfiguredFunction(videoCanvasFunctionConfig, functionKey);
|
||||||
const prompt = composeFunctionPrompt(
|
const isWideGeneration = isWideImageGenerationFunction(functionKey);
|
||||||
definition,
|
const resultWidth = isWideGeneration ? OFFICIAL_NINE_GRID_WIDTH : 420;
|
||||||
payload.prompt.trim() || payload.slashScene || "生成角色三视图",
|
const resultHeight = isWideGeneration ? OFFICIAL_NINE_GRID_HEIGHT : 280;
|
||||||
);
|
const resultTitle = slashScene || definition.label || "图片生成";
|
||||||
|
const gridLayout = getPresetGridLayout(functionKey);
|
||||||
|
const count = Math.max(1, Math.min(4, Number.parseInt(payload.settings.count, 10) || 1));
|
||||||
const finalPrompt =
|
const finalPrompt =
|
||||||
functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
||||||
? composeMultiViewNineGridPrompt(definition, payload.prompt.trim() || payload.slashScene || "多机位九宫格")
|
? composeMultiViewNineGridPrompt(definition, promptInput || slashScene || "多机位九宫格")
|
||||||
: prompt;
|
: isWideGeneration
|
||||||
const count = Math.max(1, Math.min(4, Number.parseInt(payload.settings.count, 10) || 1));
|
? composeConfiguredImagePresetPrompt(definition, promptInput || slashScene || definition.label)
|
||||||
|
: composeFunctionPrompt(
|
||||||
|
definition,
|
||||||
|
promptInput || slashScene || "生成角色三视图",
|
||||||
|
);
|
||||||
let generatedUrl = "";
|
let generatedUrl = "";
|
||||||
|
|
||||||
if (videoCanvasFunctionConfig.enabled && definition?.enabled) {
|
if (videoCanvasFunctionConfig.enabled && definition?.enabled) {
|
||||||
const response = await imageGenerationService.generateImage({
|
const response = await imageGenerationService.generateImage({
|
||||||
aspectRatio: payload.settings.ratio,
|
aspectRatio: isWideGeneration ? "16:9" : payload.settings.ratio,
|
||||||
imageCount: count,
|
billingFeature: getImageGenerationBillingFeature(functionKey),
|
||||||
|
imageCount: isWideGeneration ? 1 : count,
|
||||||
model: getFunctionModel(videoCanvasFunctionConfig, definition, payload.settings.model),
|
model: getFunctionModel(videoCanvasFunctionConfig, definition, payload.settings.model),
|
||||||
prompt: finalPrompt,
|
prompt: finalPrompt,
|
||||||
|
promptExtend: !isWideGeneration,
|
||||||
referenceImage: sourceUrl,
|
referenceImage: sourceUrl,
|
||||||
});
|
});
|
||||||
generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : "";
|
generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : "";
|
||||||
@ -2990,38 +3327,74 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
const generatedNode = generatedUrl
|
const generatedNode = generatedUrl
|
||||||
? createMediaResourceNode({
|
? createMediaResourceNode({
|
||||||
action: "IMAGE_RESOURCE",
|
action: "IMAGE_RESOURCE",
|
||||||
height: functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY ? OFFICIAL_NINE_GRID_HEIGHT : 280,
|
height: resultHeight,
|
||||||
mediaType: "image",
|
mediaType: "image",
|
||||||
mediaUrl: generatedUrl,
|
mediaUrl: generatedUrl,
|
||||||
prompt: finalPrompt,
|
prompt: finalPrompt,
|
||||||
title: payload.slashScene || definition.label || "图片生成",
|
title: resultTitle,
|
||||||
width: functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY ? OFFICIAL_NINE_GRID_WIDTH : 420,
|
width: resultWidth,
|
||||||
x: rightX,
|
x: rightX,
|
||||||
y: Math.round(anchor.y + anchor.height / 2 - (functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY ? OFFICIAL_NINE_GRID_HEIGHT : 280) / 2),
|
y: Math.round(anchor.y + anchor.height / 2 - resultHeight / 2),
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const created = generatedNode || createTargetModuleNode("image", rightX, 0, nodes.length + 1);
|
const placeholderNode = isWideGeneration
|
||||||
|
? createMediaResourceNode({
|
||||||
|
action:
|
||||||
|
isPresetGridFunction(functionKey)
|
||||||
|
? "IMAGE_GENERATE_NINE_GRID"
|
||||||
|
: "IMAGE_GENERATE",
|
||||||
|
height: resultHeight,
|
||||||
|
mediaType: "image",
|
||||||
|
mediaUrl: "",
|
||||||
|
prompt: finalPrompt,
|
||||||
|
title: resultTitle,
|
||||||
|
width: resultWidth,
|
||||||
|
x: rightX,
|
||||||
|
y: Math.round(anchor.y + anchor.height / 2 - resultHeight / 2),
|
||||||
|
})
|
||||||
|
: createTargetModuleNode("image", rightX, 0, nodes.length + 1);
|
||||||
|
const created = generatedNode || placeholderNode;
|
||||||
const resultNode: CloneNode = {
|
const resultNode: CloneNode = {
|
||||||
...created,
|
...created,
|
||||||
title: payload.slashScene || definition.label || "图片生成",
|
title: resultTitle,
|
||||||
y:
|
y:
|
||||||
generatedNode?.y ??
|
generatedNode?.y ??
|
||||||
Math.round(anchor.y + anchor.height / 2 - created.height / 2),
|
(isWideGeneration
|
||||||
|
? placeholderNode.y
|
||||||
|
: Math.round(anchor.y + anchor.height / 2 - created.height / 2)),
|
||||||
draftValues: {
|
draftValues: {
|
||||||
...(created.draftValues || {}),
|
...(created.draftValues || {}),
|
||||||
|
functionKey,
|
||||||
generated: Boolean(generatedUrl) || created.draftValues?.generated,
|
generated: Boolean(generatedUrl) || created.draftValues?.generated,
|
||||||
|
generationStatus: generatedUrl ? "complete" : created.draftValues?.generationStatus,
|
||||||
|
gridColumns: gridLayout?.columns || created.draftValues?.gridColumns,
|
||||||
|
gridPrompt: gridLayout ? finalPrompt : created.draftValues?.gridPrompt,
|
||||||
|
gridRows: gridLayout?.rows || created.draftValues?.gridRows,
|
||||||
|
nineGridCells: gridLayout
|
||||||
|
? Array.from({ length: gridLayout.rows * gridLayout.columns }).map((_, index) => getGridCellVisual(index))
|
||||||
|
: created.draftValues?.nineGridCells,
|
||||||
|
nineGridComposite: gridLayout ? true : created.draftValues?.nineGridComposite,
|
||||||
|
outputSizeLabel: isWideGeneration ? "2752 × 1536" : created.draftValues?.outputSizeLabel,
|
||||||
params: {
|
params: {
|
||||||
|
functionKey,
|
||||||
|
mode:
|
||||||
|
functionKey === CHARACTER_THREE_VIEW_FUNCTION_KEY
|
||||||
|
? "character-three-view"
|
||||||
|
: isPresetGridFunction(functionKey)
|
||||||
|
? "grid"
|
||||||
|
: "image",
|
||||||
prompt: finalPrompt,
|
prompt: finalPrompt,
|
||||||
references: payload.references,
|
references: payload.references,
|
||||||
settings: payload.settings,
|
settings: payload.settings,
|
||||||
slashScene: payload.slashScene,
|
slashScene,
|
||||||
sourceNodeId: anchor.id,
|
sourceNodeId: anchor.id,
|
||||||
sourceUrl,
|
sourceUrl,
|
||||||
},
|
},
|
||||||
|
prompt: finalPrompt,
|
||||||
sourceNodeId: anchor.id,
|
sourceNodeId: anchor.id,
|
||||||
sourceUrl,
|
sourceUrl,
|
||||||
targetPreset: starterMode || undefined,
|
targetPreset: starterMode || undefined,
|
||||||
toolbarOption: payload.slashScene || "",
|
toolbarOption: slashScene,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const referenceEdges = payload.references
|
const referenceEdges = payload.references
|
||||||
@ -3233,8 +3606,19 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (node.inputType === "image" || /IMAGE_GENERATE/.test(action)) {
|
if (node.inputType === "image" || /IMAGE_GENERATE/.test(action)) {
|
||||||
|
const savedFunctionKey = String(params.functionKey || node.draftValues?.functionKey || "").trim();
|
||||||
|
const explicitFunctionKey =
|
||||||
|
resolveOfficialImagePresetFunctionKey({
|
||||||
|
fallbackKey: savedFunctionKey,
|
||||||
|
option: params.option || node.draftValues?.toolbarOption,
|
||||||
|
title: node.title,
|
||||||
|
}) || savedFunctionKey;
|
||||||
const functionKey =
|
const functionKey =
|
||||||
action === "IMAGE_GENERATE_NINE_GRID"
|
explicitFunctionKey &&
|
||||||
|
(videoCanvasFunctionConfig.functions[explicitFunctionKey] ||
|
||||||
|
defaultVideoCanvasFunctionConfig.functions[explicitFunctionKey])
|
||||||
|
? explicitFunctionKey
|
||||||
|
: action === "IMAGE_GENERATE_NINE_GRID"
|
||||||
? MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
? MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
||||||
: node.draftValues?.generatorType === "ENHANCE"
|
: node.draftValues?.generatorType === "ENHANCE"
|
||||||
? "hd-upscale"
|
? "hd-upscale"
|
||||||
@ -3244,7 +3628,10 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
const prompt =
|
const prompt =
|
||||||
functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
||||||
? composeMultiViewNineGridPrompt(definition, String(params.option || node.title))
|
? composeMultiViewNineGridPrompt(definition, String(params.option || node.title))
|
||||||
|
: isWideImageGenerationFunction(functionKey)
|
||||||
|
? composeConfiguredImagePresetPrompt(definition, rawPrompt || String(params.option || node.title))
|
||||||
: composeFunctionPrompt(definition, rawPrompt || String(params.option || node.title));
|
: composeFunctionPrompt(definition, rawPrompt || String(params.option || node.title));
|
||||||
|
const gridLayout = getPresetGridLayout(functionKey);
|
||||||
let generatedUrl = "";
|
let generatedUrl = "";
|
||||||
|
|
||||||
if (definition.route === "enhance" && sourceUrl) {
|
if (definition.route === "enhance" && sourceUrl) {
|
||||||
@ -3253,10 +3640,12 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
if (!generatedUrl && !response.success) throw new Error(response.error || "图片增强失败");
|
if (!generatedUrl && !response.success) throw new Error(response.error || "图片增强失败");
|
||||||
} else {
|
} else {
|
||||||
const response = await imageGenerationService.generateImage({
|
const response = await imageGenerationService.generateImage({
|
||||||
aspectRatio: action === "IMAGE_GENERATE_NINE_GRID" ? "16:9" : undefined,
|
aspectRatio: isWideImageGenerationFunction(functionKey) ? "16:9" : undefined,
|
||||||
|
billingFeature: getImageGenerationBillingFeature(functionKey),
|
||||||
imageCount: 1,
|
imageCount: 1,
|
||||||
model: getFunctionModel(videoCanvasFunctionConfig, definition, nodeModelRef),
|
model: getFunctionModel(videoCanvasFunctionConfig, definition, nodeModelRef),
|
||||||
prompt,
|
prompt,
|
||||||
|
promptExtend: !isWideImageGenerationFunction(functionKey),
|
||||||
referenceImage: sourceUrl || undefined,
|
referenceImage: sourceUrl || undefined,
|
||||||
});
|
});
|
||||||
generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : "";
|
generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : "";
|
||||||
@ -3273,10 +3662,28 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
|||||||
subtitle: "生成结果",
|
subtitle: "生成结果",
|
||||||
draftValues: {
|
draftValues: {
|
||||||
...(item.draftValues || {}),
|
...(item.draftValues || {}),
|
||||||
|
functionKey,
|
||||||
generated: true,
|
generated: true,
|
||||||
|
generationProgress: 100,
|
||||||
|
generationStatus: "complete",
|
||||||
|
gridColumns: gridLayout?.columns || item.draftValues?.gridColumns,
|
||||||
|
gridPrompt: gridLayout ? prompt : item.draftValues?.gridPrompt,
|
||||||
|
gridRows: gridLayout?.rows || item.draftValues?.gridRows,
|
||||||
|
nineGridCells: gridLayout
|
||||||
|
? Array.from({ length: gridLayout.rows * gridLayout.columns }).map((_, index) => getGridCellVisual(index))
|
||||||
|
: item.draftValues?.nineGridCells,
|
||||||
|
nineGridComposite: gridLayout ? true : item.draftValues?.nineGridComposite,
|
||||||
mediaType: "image",
|
mediaType: "image",
|
||||||
mediaUrl: generatedUrl,
|
mediaUrl: generatedUrl,
|
||||||
nodeType: "image",
|
nodeType: "image",
|
||||||
|
outputSizeLabel: isWideImageGenerationFunction(functionKey)
|
||||||
|
? "2752 × 1536"
|
||||||
|
: item.draftValues?.outputSizeLabel,
|
||||||
|
params: {
|
||||||
|
...getNodeParams(item),
|
||||||
|
functionKey,
|
||||||
|
prompt,
|
||||||
|
},
|
||||||
prompt,
|
prompt,
|
||||||
sourceUrl,
|
sourceUrl,
|
||||||
url: [generatedUrl],
|
url: [generatedUrl],
|
||||||
@ -4523,8 +4930,8 @@ function PresetWorkflowControls({
|
|||||||
onPresetSubmit: (prompt: string, settings?: ComposerSettings) => void;
|
onPresetSubmit: (prompt: string, settings?: ComposerSettings) => void;
|
||||||
selectedNode: CloneNode;
|
selectedNode: CloneNode;
|
||||||
}) {
|
}) {
|
||||||
if (mode === "character-three-view") {
|
if (mode === CHARACTER_THREE_VIEW_FUNCTION_KEY) {
|
||||||
const definition = getConfiguredFunction(functionConfig, "character-three-view");
|
const definition = getConfiguredFunction(functionConfig, CHARACTER_THREE_VIEW_FUNCTION_KEY);
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||||
<CharacterPresetToolbar anchor={anchor} onToolAction={onImageToolAction} />
|
<CharacterPresetToolbar anchor={anchor} onToolAction={onImageToolAction} />
|
||||||
@ -7382,7 +7789,7 @@ function PresetMediaCanvasNode({
|
|||||||
) : mediaUrl ? (
|
) : mediaUrl ? (
|
||||||
<img
|
<img
|
||||||
alt={node.title}
|
alt={node.title}
|
||||||
className={cn("pointer-events-none h-full w-full", node.title === "角色三视图" ? "object-contain" : "object-cover")}
|
className={cn("pointer-events-none h-full w-full", isCharacterThreeViewNode(node) ? "object-contain" : "object-cover")}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
onDragStart={preventNativeImageDrag}
|
onDragStart={preventNativeImageDrag}
|
||||||
src={mediaUrl}
|
src={mediaUrl}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ArrowLeft,
|
||||||
Circle as CircleIcon,
|
Circle as CircleIcon,
|
||||||
Eraser,
|
Eraser,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
@ -850,8 +851,6 @@ export function RealtimeCanvasDialog({
|
|||||||
defaultRealtimeConfig.transport.throttleInterval
|
defaultRealtimeConfig.transport.throttleInterval
|
||||||
);
|
);
|
||||||
const [refreshToken, setRefreshToken] = useState(0);
|
const [refreshToken, setRefreshToken] = useState(0);
|
||||||
const [siteName, setSiteName] = useState("AI Chatbot");
|
|
||||||
const [siteLogo, setSiteLogo] = useState<string | null>(null);
|
|
||||||
const [isLayerSplitEnabled, setIsLayerSplitEnabled] = useState(false);
|
const [isLayerSplitEnabled, setIsLayerSplitEnabled] = useState(false);
|
||||||
const [modeIconConfig, setModeIconConfig] =
|
const [modeIconConfig, setModeIconConfig] =
|
||||||
useState<RealtimeCanvasModeIconConfig>({
|
useState<RealtimeCanvasModeIconConfig>({
|
||||||
@ -908,17 +907,6 @@ export function RealtimeCanvasDialog({
|
|||||||
.then((res) => (res.ok ? res.json() : null))
|
.then((res) => (res.ok ? res.json() : null))
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data || cancelled) return;
|
if (!data || cancelled) return;
|
||||||
if (typeof data.siteName === "string" && data.siteName.trim()) {
|
|
||||||
setSiteName(data.siteName.trim());
|
|
||||||
}
|
|
||||||
if (typeof data.siteLogo === "string" && data.siteLogo.trim()) {
|
|
||||||
setSiteLogo(data.siteLogo.trim());
|
|
||||||
} else if (
|
|
||||||
typeof data.siteLogoDark === "string" &&
|
|
||||||
data.siteLogoDark.trim()
|
|
||||||
) {
|
|
||||||
setSiteLogo(data.siteLogoDark.trim());
|
|
||||||
}
|
|
||||||
setIsLayerSplitEnabled(data.layerSplitEnabled === true);
|
setIsLayerSplitEnabled(data.layerSplitEnabled === true);
|
||||||
const iconConfig =
|
const iconConfig =
|
||||||
data.realtimeCanvasModeIconConfig &&
|
data.realtimeCanvasModeIconConfig &&
|
||||||
@ -3010,21 +2998,8 @@ export function RealtimeCanvasDialog({
|
|||||||
}, [previewUrl]);
|
}, [previewUrl]);
|
||||||
|
|
||||||
const handleCloseRealtime = useCallback(() => {
|
const handleCloseRealtime = useCallback(() => {
|
||||||
if (standalone && typeof window !== "undefined") {
|
|
||||||
if (window.history.length > 1) {
|
|
||||||
window.history.back();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.location.assign("/canvas");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}, [onOpenChange, standalone]);
|
}, [onOpenChange]);
|
||||||
|
|
||||||
const handleGoHome = useCallback(() => {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
window.location.assign("/");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeBrushSettingPopovers = useCallback(() => {
|
const closeBrushSettingPopovers = useCallback(() => {
|
||||||
setBrushPopoverOpen(false);
|
setBrushPopoverOpen(false);
|
||||||
@ -3157,27 +3132,15 @@ export function RealtimeCanvasDialog({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="inline-flex h-12 shrink-0 items-center gap-3 rounded-[22px] border border-white/90 bg-white/80 px-4 text-left text-[#111111] shadow-[inset_0_1px_0_rgba(255,255,255,0.95)] transition hover:bg-white"
|
className="inline-flex h-12 shrink-0 items-center gap-3 rounded-[22px] border border-white/90 bg-white/80 px-4 text-left text-[#111111] shadow-[inset_0_1px_0_rgba(255,255,255,0.95)] transition hover:bg-white"
|
||||||
onClick={handleGoHome}
|
onClick={handleCloseRealtime}
|
||||||
title="返回首页"
|
title={standalone ? "返回无限画布" : "关闭并返回画布"}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-[#ececec] bg-white shadow-[0_8px_18px_rgba(0,0,0,0.06)]">
|
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-[#ececec] bg-white shadow-[0_8px_18px_rgba(0,0,0,0.06)]">
|
||||||
{siteLogo ? (
|
<ArrowLeft className="h-4 w-4 text-[#111111]" />
|
||||||
<img
|
|
||||||
alt={siteName}
|
|
||||||
className="h-6 w-6 rounded-full object-contain"
|
|
||||||
src={
|
|
||||||
/^https?:\/\//i.test(siteLogo)
|
|
||||||
? buildImageProxyUrl(siteLogo)
|
|
||||||
: siteLogo
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Sparkles className="h-3.5 w-3.5 text-[#111111]" />
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="font-semibold text-[14px] tracking-[-0.02em]">
|
<span className="font-semibold text-[14px] tracking-[-0.02em]">
|
||||||
{siteName}
|
{standalone ? "返回无限画布" : "返回画布"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@ -2767,6 +2767,7 @@ export function ImageNode({
|
|||||||
referenceImage: data.src,
|
referenceImage: data.src,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
imageCount: 1,
|
imageCount: 1,
|
||||||
|
billingFeature: "repaint",
|
||||||
});
|
});
|
||||||
|
|
||||||
const image = result.data?.images?.[0];
|
const image = result.data?.images?.[0];
|
||||||
|
|||||||
@ -566,6 +566,7 @@ function CameraMovementControls({
|
|||||||
pressedKeysRef.current = createMovementState();
|
pressedKeysRef.current = createMovementState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
event.preventDefault();
|
||||||
pressedKeysRef.current[movement] = true;
|
pressedKeysRef.current[movement] = true;
|
||||||
};
|
};
|
||||||
const handleKeyUp = (event: KeyboardEvent) => {
|
const handleKeyUp = (event: KeyboardEvent) => {
|
||||||
@ -575,6 +576,7 @@ function CameraMovementControls({
|
|||||||
pressedKeysRef.current = createMovementState();
|
pressedKeysRef.current = createMovementState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
event.preventDefault();
|
||||||
pressedKeysRef.current[movement] = false;
|
pressedKeysRef.current[movement] = false;
|
||||||
};
|
};
|
||||||
const handleFocusIn = (event: FocusEvent) => {
|
const handleFocusIn = (event: FocusEvent) => {
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export const defaultAgentWorkflowPageFeatureSettings: AgentWorkflowPageFeatureSe
|
|||||||
presetStepEnabled: true,
|
presetStepEnabled: true,
|
||||||
materialStepEnabled: true,
|
materialStepEnabled: true,
|
||||||
psdTemplateEnabled: true,
|
psdTemplateEnabled: true,
|
||||||
|
batchProcessingEnabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AgentWorkflowBrandCategoryOption {
|
export interface AgentWorkflowBrandCategoryOption {
|
||||||
@ -258,6 +259,10 @@ export function normalizeAgentWorkflowPageFeatureSettings(
|
|||||||
record.psdTemplateEnabled ?? record.enablePsdTemplate,
|
record.psdTemplateEnabled ?? record.enablePsdTemplate,
|
||||||
defaultAgentWorkflowPageFeatureSettings.psdTemplateEnabled
|
defaultAgentWorkflowPageFeatureSettings.psdTemplateEnabled
|
||||||
),
|
),
|
||||||
|
batchProcessingEnabled: normalizeBoolean(
|
||||||
|
record.batchProcessingEnabled ?? record.enableBatchProcessing,
|
||||||
|
defaultAgentWorkflowPageFeatureSettings.batchProcessingEnabled
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1072,6 +1077,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
|||||||
presetStepEnabled: raw.presetStepEnabled ?? raw.modeStepEnabled,
|
presetStepEnabled: raw.presetStepEnabled ?? raw.modeStepEnabled,
|
||||||
materialStepEnabled: raw.materialStepEnabled ?? raw.skuStepEnabled,
|
materialStepEnabled: raw.materialStepEnabled ?? raw.skuStepEnabled,
|
||||||
psdTemplateEnabled: raw.psdTemplateEnabled,
|
psdTemplateEnabled: raw.psdTemplateEnabled,
|
||||||
|
batchProcessingEnabled: raw.batchProcessingEnabled,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
steps: sanitizedSteps,
|
steps: sanitizedSteps,
|
||||||
|
|||||||
@ -424,14 +424,14 @@ function getModeConstraintOptions(
|
|||||||
const constraint = mode ? modeConstraints?.[mode] : undefined;
|
const constraint = mode ? modeConstraints?.[mode] : undefined;
|
||||||
const ratioOptions = constraint?.ratioOptions?.length ? constraint.ratioOptions : [];
|
const ratioOptions = constraint?.ratioOptions?.length ? constraint.ratioOptions : [];
|
||||||
const durationOptions = constraint?.durationOptions?.length ? constraint.durationOptions : [];
|
const durationOptions = constraint?.durationOptions?.length ? constraint.durationOptions : [];
|
||||||
const resolutionOptions = constraint?.resolutionOptions?.length
|
const hasResolutionOptions = Array.isArray(constraint?.resolutionOptions);
|
||||||
? constraint.resolutionOptions
|
const resolutionOptions = hasResolutionOptions ? constraint?.resolutionOptions || [] : [];
|
||||||
: [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ratioOptions,
|
ratioOptions,
|
||||||
durationOptions,
|
durationOptions,
|
||||||
resolutionOptions,
|
resolutionOptions,
|
||||||
|
hasResolutionOptions,
|
||||||
defaults: {
|
defaults: {
|
||||||
...defaults,
|
...defaults,
|
||||||
...(constraint?.defaults || {}),
|
...(constraint?.defaults || {}),
|
||||||
@ -439,6 +439,31 @@ function getModeConstraintOptions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restrictModeConstraintResolutions(
|
||||||
|
modeConstraints: Partial<Record<VideoToolMode, VideoToolModeConstraint>> | undefined,
|
||||||
|
allowedResolutions: VideoToolResolution[]
|
||||||
|
) {
|
||||||
|
if (!modeConstraints || allowedResolutions.length === 0) {
|
||||||
|
return modeConstraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedSet = new Set<VideoToolResolution>(allowedResolutions);
|
||||||
|
return MODE_ORDER.reduce<Partial<Record<VideoToolMode, VideoToolModeConstraint>>>(
|
||||||
|
(acc, mode) => {
|
||||||
|
const current = modeConstraints[mode];
|
||||||
|
if (!current) return acc;
|
||||||
|
acc[mode] = {
|
||||||
|
...current,
|
||||||
|
resolutionOptions: Array.isArray(current.resolutionOptions)
|
||||||
|
? current.resolutionOptions.filter((item) => allowedSet.has(item))
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function applyConfiguredDurationOptions(
|
function applyConfiguredDurationOptions(
|
||||||
capability: VideoToolCapabilityContract,
|
capability: VideoToolCapabilityContract,
|
||||||
configuredDurationOptions: number[]
|
configuredDurationOptions: number[]
|
||||||
@ -501,6 +526,8 @@ export function getVideoToolOptionsForMode(
|
|||||||
: capability.durationOptions,
|
: capability.durationOptions,
|
||||||
resolutionOptions: scoped.resolutionOptions.length
|
resolutionOptions: scoped.resolutionOptions.length
|
||||||
? scoped.resolutionOptions
|
? scoped.resolutionOptions
|
||||||
|
: scoped.hasResolutionOptions
|
||||||
|
? []
|
||||||
: capability.resolutionOptions,
|
: capability.resolutionOptions,
|
||||||
defaults: scoped.defaults,
|
defaults: scoped.defaults,
|
||||||
};
|
};
|
||||||
@ -593,9 +620,15 @@ export function buildVideoToolCapability(params: {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
: base.modeConstraints;
|
: base.modeConstraints;
|
||||||
|
const restrictedModeConstraints = restrictModeConstraintResolutions(
|
||||||
|
nextModeConstraints,
|
||||||
|
parsed.supportedResolutions && parsed.supportedResolutions.length
|
||||||
|
? parsed.supportedResolutions
|
||||||
|
: []
|
||||||
|
);
|
||||||
const defaultMode = pickPreferredMode(modeOptions, base.defaults.mode);
|
const defaultMode = pickPreferredMode(modeOptions, base.defaults.mode);
|
||||||
const modeScopedDefaults = getModeConstraintOptions(
|
const modeScopedDefaults = getModeConstraintOptions(
|
||||||
nextModeConstraints,
|
restrictedModeConstraints,
|
||||||
defaultMode,
|
defaultMode,
|
||||||
base.defaults
|
base.defaults
|
||||||
);
|
);
|
||||||
@ -605,7 +638,7 @@ export function buildVideoToolCapability(params: {
|
|||||||
const effectiveDurationOptions = modeScopedDefaults.durationOptions.length
|
const effectiveDurationOptions = modeScopedDefaults.durationOptions.length
|
||||||
? modeScopedDefaults.durationOptions
|
? modeScopedDefaults.durationOptions
|
||||||
: durationOptions;
|
: durationOptions;
|
||||||
const effectiveResolutionOptions = modeScopedDefaults.resolutionOptions.length
|
const effectiveResolutionOptions = modeScopedDefaults.hasResolutionOptions
|
||||||
? modeScopedDefaults.resolutionOptions
|
? modeScopedDefaults.resolutionOptions
|
||||||
: resolutionOptions;
|
: resolutionOptions;
|
||||||
|
|
||||||
@ -617,7 +650,7 @@ export function buildVideoToolCapability(params: {
|
|||||||
ratioOptions,
|
ratioOptions,
|
||||||
resolutionOptions,
|
resolutionOptions,
|
||||||
durationOptions,
|
durationOptions,
|
||||||
modeConstraints: nextModeConstraints,
|
modeConstraints: restrictedModeConstraints,
|
||||||
ui: {
|
ui: {
|
||||||
...base.ui,
|
...base.ui,
|
||||||
showReturnLastFrame:
|
showReturnLastFrame:
|
||||||
|
|||||||
@ -19,8 +19,10 @@ export interface CanvasGenerateImageRequest {
|
|||||||
aspectRatio?: string;
|
aspectRatio?: string;
|
||||||
imageSize?: string;
|
imageSize?: string;
|
||||||
imageCount?: number;
|
imageCount?: number;
|
||||||
|
promptExtend?: boolean;
|
||||||
referenceImage?: string;
|
referenceImage?: string;
|
||||||
referenceImages?: string[];
|
referenceImages?: string[];
|
||||||
|
billingFeature?: "multi-view" | "repaint";
|
||||||
debugLabel?: string;
|
debugLabel?: string;
|
||||||
debugMeta?: Record<string, unknown>;
|
debugMeta?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
@ -123,6 +125,8 @@ export async function generateCanvasImage(
|
|||||||
typeof request.imageCount === "number" && Number.isFinite(request.imageCount)
|
typeof request.imageCount === "number" && Number.isFinite(request.imageCount)
|
||||||
? Math.max(1, Math.min(12, Math.floor(request.imageCount)))
|
? Math.max(1, Math.min(12, Math.floor(request.imageCount)))
|
||||||
: 1;
|
: 1;
|
||||||
|
const promptExtend =
|
||||||
|
typeof request.promptExtend === "boolean" ? request.promptExtend : undefined;
|
||||||
const referenceImage =
|
const referenceImage =
|
||||||
typeof request.referenceImage === "string" ? request.referenceImage : undefined;
|
typeof request.referenceImage === "string" ? request.referenceImage : undefined;
|
||||||
const referenceImages = Array.isArray(request.referenceImages)
|
const referenceImages = Array.isArray(request.referenceImages)
|
||||||
@ -241,6 +245,7 @@ export async function generateCanvasImage(
|
|||||||
imageSize,
|
imageSize,
|
||||||
imageCount: 1,
|
imageCount: 1,
|
||||||
negativePrompt,
|
negativePrompt,
|
||||||
|
promptExtend,
|
||||||
};
|
};
|
||||||
|
|
||||||
let executeGenerateBatch:
|
let executeGenerateBatch:
|
||||||
|
|||||||
@ -3946,6 +3946,7 @@ export async function createAiModel({
|
|||||||
fallbackModelId2,
|
fallbackModelId2,
|
||||||
excludeFromFailover,
|
excludeFromFailover,
|
||||||
imageGenConfig,
|
imageGenConfig,
|
||||||
|
capabilities,
|
||||||
isAgentModel,
|
isAgentModel,
|
||||||
isMultimodal,
|
isMultimodal,
|
||||||
agentCodingModelId,
|
agentCodingModelId,
|
||||||
@ -3965,6 +3966,7 @@ export async function createAiModel({
|
|||||||
fallbackModelId2?: string;
|
fallbackModelId2?: string;
|
||||||
excludeFromFailover?: boolean;
|
excludeFromFailover?: boolean;
|
||||||
imageGenConfig?: ImageGenConfig;
|
imageGenConfig?: ImageGenConfig;
|
||||||
|
capabilities?: Record<string, unknown> | null;
|
||||||
isAgentModel?: boolean;
|
isAgentModel?: boolean;
|
||||||
isMultimodal?: boolean;
|
isMultimodal?: boolean;
|
||||||
agentCodingModelId?: string;
|
agentCodingModelId?: string;
|
||||||
@ -4009,8 +4011,8 @@ export async function createAiModel({
|
|||||||
defaultParams,
|
defaultParams,
|
||||||
capabilities:
|
capabilities:
|
||||||
isMultimodal === undefined
|
isMultimodal === undefined
|
||||||
? null
|
? capabilities || null
|
||||||
: withMultimodalModelFlag(null, isMultimodal),
|
: withMultimodalModelFlag(capabilities || null, isMultimodal),
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@ -4038,6 +4040,7 @@ export async function updateAiModel({
|
|||||||
fallbackModelId2,
|
fallbackModelId2,
|
||||||
excludeFromFailover,
|
excludeFromFailover,
|
||||||
imageGenConfig,
|
imageGenConfig,
|
||||||
|
capabilities,
|
||||||
isAgentModel,
|
isAgentModel,
|
||||||
isMultimodal,
|
isMultimodal,
|
||||||
agentCodingModelId,
|
agentCodingModelId,
|
||||||
@ -4058,6 +4061,7 @@ export async function updateAiModel({
|
|||||||
fallbackModelId2?: string;
|
fallbackModelId2?: string;
|
||||||
excludeFromFailover?: boolean;
|
excludeFromFailover?: boolean;
|
||||||
imageGenConfig?: ImageGenConfig;
|
imageGenConfig?: ImageGenConfig;
|
||||||
|
capabilities?: Record<string, unknown> | null;
|
||||||
isAgentModel?: boolean;
|
isAgentModel?: boolean;
|
||||||
isMultimodal?: boolean;
|
isMultimodal?: boolean;
|
||||||
agentCodingModelId?: string;
|
agentCodingModelId?: string;
|
||||||
@ -4093,7 +4097,8 @@ export async function updateAiModel({
|
|||||||
if (
|
if (
|
||||||
isAgentModel !== undefined ||
|
isAgentModel !== undefined ||
|
||||||
isMultimodal !== undefined ||
|
isMultimodal !== undefined ||
|
||||||
agentCodingModelId !== undefined
|
agentCodingModelId !== undefined ||
|
||||||
|
capabilities !== undefined
|
||||||
) {
|
) {
|
||||||
const [existingModel] = await db
|
const [existingModel] = await db
|
||||||
.select({
|
.select({
|
||||||
@ -4125,11 +4130,13 @@ export async function updateAiModel({
|
|||||||
updateData.defaultParams = nextDefaultParams;
|
updateData.defaultParams = nextDefaultParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMultimodal !== undefined) {
|
if (capabilities !== undefined || isMultimodal !== undefined) {
|
||||||
updateData.capabilities = withMultimodalModelFlag(
|
const nextCapabilities =
|
||||||
existingModel.capabilities,
|
capabilities !== undefined ? capabilities || null : existingModel.capabilities;
|
||||||
isMultimodal
|
updateData.capabilities =
|
||||||
);
|
isMultimodal !== undefined
|
||||||
|
? withMultimodalModelFlag(nextCapabilities, isMultimodal)
|
||||||
|
: nextCapabilities;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,12 +30,40 @@ export type PointsSystemConfig = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
blockOnInsufficient: boolean;
|
blockOnInsufficient: boolean;
|
||||||
creditCostPer3DModel: number;
|
creditCostPer3DModel: number;
|
||||||
|
creditCostPer3DModelPbr: number;
|
||||||
|
creditCostPer3DModelSmartLowPoly: number;
|
||||||
|
creditCostPer3DModelQuad: number;
|
||||||
|
creditCostPer3DModelRefine: number;
|
||||||
|
creditCostPer3DModelTexture: number;
|
||||||
|
creditCostPer3DModelAutoRig: number;
|
||||||
|
creditCostPer3DModelHighpolyToLowpoly: number;
|
||||||
creditCostPer3DWorld: number;
|
creditCostPer3DWorld: number;
|
||||||
creditCostPer3DWorldMarble11: number;
|
creditCostPer3DWorldMarble11: number;
|
||||||
creditCostPer3DWorldMarble11Plus: number;
|
creditCostPer3DWorldMarble11Plus: number;
|
||||||
|
creditCostPerImageMultiView: number;
|
||||||
|
creditCostPerImageRemoveBackground: number;
|
||||||
|
creditCostPerImageBoxCutout: number;
|
||||||
|
creditCostPerImageWatermarkRemove: number;
|
||||||
|
creditCostPerImageExtend: number;
|
||||||
|
creditCostPerImageRepaint: number;
|
||||||
enhanceProPricing: EnhanceProPointsPricingConfig;
|
enhanceProPricing: EnhanceProPointsPricingConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ThreeDTaskCreditParams = {
|
||||||
|
taskType?: string | null;
|
||||||
|
pbr?: boolean | null;
|
||||||
|
quad?: boolean | null;
|
||||||
|
smartLowPoly?: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageToolCreditFeature =
|
||||||
|
| "multi-view"
|
||||||
|
| "remove-background"
|
||||||
|
| "box-cutout"
|
||||||
|
| "watermark-remove"
|
||||||
|
| "extend"
|
||||||
|
| "repaint";
|
||||||
|
|
||||||
export type PointsPackage = {
|
export type PointsPackage = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -49,9 +77,22 @@ export const defaultPointsSystemConfig: PointsSystemConfig = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
blockOnInsufficient: true,
|
blockOnInsufficient: true,
|
||||||
creditCostPer3DModel: 0,
|
creditCostPer3DModel: 0,
|
||||||
|
creditCostPer3DModelPbr: 0,
|
||||||
|
creditCostPer3DModelSmartLowPoly: 0,
|
||||||
|
creditCostPer3DModelQuad: 0,
|
||||||
|
creditCostPer3DModelRefine: 0,
|
||||||
|
creditCostPer3DModelTexture: 0,
|
||||||
|
creditCostPer3DModelAutoRig: 0,
|
||||||
|
creditCostPer3DModelHighpolyToLowpoly: 0,
|
||||||
creditCostPer3DWorld: 0,
|
creditCostPer3DWorld: 0,
|
||||||
creditCostPer3DWorldMarble11: 0,
|
creditCostPer3DWorldMarble11: 0,
|
||||||
creditCostPer3DWorldMarble11Plus: 0,
|
creditCostPer3DWorldMarble11Plus: 0,
|
||||||
|
creditCostPerImageMultiView: 0,
|
||||||
|
creditCostPerImageRemoveBackground: 0,
|
||||||
|
creditCostPerImageBoxCutout: 0,
|
||||||
|
creditCostPerImageWatermarkRemove: 0,
|
||||||
|
creditCostPerImageExtend: 0,
|
||||||
|
creditCostPerImageRepaint: 0,
|
||||||
enhanceProPricing: {
|
enhanceProPricing: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
chargeMode: "scale_factor",
|
chargeMode: "scale_factor",
|
||||||
@ -390,6 +431,67 @@ export function get3DWorldCreditCost(
|
|||||||
return normalizeCost(pointsConfig.creditCostPer3DWorld);
|
return normalizeCost(pointsConfig.creditCostPer3DWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function get3DTaskCreditCost(
|
||||||
|
pointsConfig: PointsSystemConfig,
|
||||||
|
params: ThreeDTaskCreditParams
|
||||||
|
) {
|
||||||
|
const taskType = String(params.taskType || "").trim();
|
||||||
|
if (
|
||||||
|
taskType === "text_to_model" ||
|
||||||
|
taskType === "image_to_model" ||
|
||||||
|
taskType === "multiview_to_model"
|
||||||
|
) {
|
||||||
|
let cost = normalizeCost(pointsConfig.creditCostPer3DModel);
|
||||||
|
if (params.pbr === true) {
|
||||||
|
cost += normalizeCost(pointsConfig.creditCostPer3DModelPbr);
|
||||||
|
}
|
||||||
|
if (params.smartLowPoly === true) {
|
||||||
|
cost += normalizeCost(pointsConfig.creditCostPer3DModelSmartLowPoly);
|
||||||
|
}
|
||||||
|
if (params.quad === true) {
|
||||||
|
cost += normalizeCost(pointsConfig.creditCostPer3DModelQuad);
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskType === "refine_model") {
|
||||||
|
return normalizeCost(pointsConfig.creditCostPer3DModelRefine);
|
||||||
|
}
|
||||||
|
if (taskType === "texture_model") {
|
||||||
|
return normalizeCost(pointsConfig.creditCostPer3DModelTexture);
|
||||||
|
}
|
||||||
|
if (taskType === "animate_rig") {
|
||||||
|
return normalizeCost(pointsConfig.creditCostPer3DModelAutoRig);
|
||||||
|
}
|
||||||
|
if (taskType === "highpoly_to_lowpoly") {
|
||||||
|
return normalizeCost(pointsConfig.creditCostPer3DModelHighpolyToLowpoly);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImageToolCreditCost(
|
||||||
|
pointsConfig: PointsSystemConfig,
|
||||||
|
feature: ImageToolCreditFeature | null | undefined
|
||||||
|
) {
|
||||||
|
switch (feature) {
|
||||||
|
case "multi-view":
|
||||||
|
return normalizeCost(pointsConfig.creditCostPerImageMultiView);
|
||||||
|
case "remove-background":
|
||||||
|
return normalizeCost(pointsConfig.creditCostPerImageRemoveBackground);
|
||||||
|
case "box-cutout":
|
||||||
|
return normalizeCost(pointsConfig.creditCostPerImageBoxCutout);
|
||||||
|
case "watermark-remove":
|
||||||
|
return normalizeCost(pointsConfig.creditCostPerImageWatermarkRemove);
|
||||||
|
case "extend":
|
||||||
|
return normalizeCost(pointsConfig.creditCostPerImageExtend);
|
||||||
|
case "repaint":
|
||||||
|
return normalizeCost(pointsConfig.creditCostPerImageRepaint);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig {
|
export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig {
|
||||||
if (!value || typeof value !== "object") {
|
if (!value || typeof value !== "object") {
|
||||||
return defaultPointsSystemConfig;
|
return defaultPointsSystemConfig;
|
||||||
@ -410,6 +512,17 @@ export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig
|
|||||||
creditCostPer3DModel: normalizeCost(
|
creditCostPer3DModel: normalizeCost(
|
||||||
raw.creditCostPer3DModel ?? legacy3DCost
|
raw.creditCostPer3DModel ?? legacy3DCost
|
||||||
),
|
),
|
||||||
|
creditCostPer3DModelPbr: normalizeCost(raw.creditCostPer3DModelPbr),
|
||||||
|
creditCostPer3DModelSmartLowPoly: normalizeCost(
|
||||||
|
raw.creditCostPer3DModelSmartLowPoly
|
||||||
|
),
|
||||||
|
creditCostPer3DModelQuad: normalizeCost(raw.creditCostPer3DModelQuad),
|
||||||
|
creditCostPer3DModelRefine: normalizeCost(raw.creditCostPer3DModelRefine),
|
||||||
|
creditCostPer3DModelTexture: normalizeCost(raw.creditCostPer3DModelTexture),
|
||||||
|
creditCostPer3DModelAutoRig: normalizeCost(raw.creditCostPer3DModelAutoRig),
|
||||||
|
creditCostPer3DModelHighpolyToLowpoly: normalizeCost(
|
||||||
|
raw.creditCostPer3DModelHighpolyToLowpoly
|
||||||
|
),
|
||||||
creditCostPer3DWorld: normalizeCost(
|
creditCostPer3DWorld: normalizeCost(
|
||||||
raw.creditCostPer3DWorld ?? legacy3DCost
|
raw.creditCostPer3DWorld ?? legacy3DCost
|
||||||
),
|
),
|
||||||
@ -419,6 +532,16 @@ export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig
|
|||||||
creditCostPer3DWorldMarble11Plus: normalizeCost(
|
creditCostPer3DWorldMarble11Plus: normalizeCost(
|
||||||
raw.creditCostPer3DWorldMarble11Plus ?? generic3DWorldCost
|
raw.creditCostPer3DWorldMarble11Plus ?? generic3DWorldCost
|
||||||
),
|
),
|
||||||
|
creditCostPerImageMultiView: normalizeCost(raw.creditCostPerImageMultiView),
|
||||||
|
creditCostPerImageRemoveBackground: normalizeCost(
|
||||||
|
raw.creditCostPerImageRemoveBackground
|
||||||
|
),
|
||||||
|
creditCostPerImageBoxCutout: normalizeCost(raw.creditCostPerImageBoxCutout),
|
||||||
|
creditCostPerImageWatermarkRemove: normalizeCost(
|
||||||
|
raw.creditCostPerImageWatermarkRemove
|
||||||
|
),
|
||||||
|
creditCostPerImageExtend: normalizeCost(raw.creditCostPerImageExtend),
|
||||||
|
creditCostPerImageRepaint: normalizeCost(raw.creditCostPerImageRepaint),
|
||||||
enhanceProPricing: normalizeEnhanceProPointsPricingConfig(
|
enhanceProPricing: normalizeEnhanceProPointsPricingConfig(
|
||||||
raw.enhanceProPricing,
|
raw.enhanceProPricing,
|
||||||
legacyEnhanceProCost
|
legacyEnhanceProCost
|
||||||
|
|||||||
97
lib/points/feature-charge.ts
Normal file
97
lib/points/feature-charge.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
consumeUserPoints,
|
||||||
|
getPointsSystemConfig,
|
||||||
|
getUserById,
|
||||||
|
} from "@/lib/db/queries";
|
||||||
|
import type { PointsSystemConfig } from "@/lib/points/config";
|
||||||
|
|
||||||
|
type FeaturePointsChargeInput = {
|
||||||
|
userId: string;
|
||||||
|
unitCost: number;
|
||||||
|
reason: string;
|
||||||
|
modelId?: string | null;
|
||||||
|
note: string;
|
||||||
|
pointsConfig?: PointsSystemConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FeaturePointsChargeOk = {
|
||||||
|
ok: true;
|
||||||
|
chargeable: boolean;
|
||||||
|
unitCost: number;
|
||||||
|
consume: (overrides?: {
|
||||||
|
modelId?: string | null;
|
||||||
|
note?: string;
|
||||||
|
reason?: string;
|
||||||
|
}) => Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
balance: number | null;
|
||||||
|
skipped: boolean;
|
||||||
|
} | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FeaturePointsChargeBlocked = {
|
||||||
|
ok: false;
|
||||||
|
status: number;
|
||||||
|
payload: {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
balance: number;
|
||||||
|
expectedCost: number;
|
||||||
|
unitCost: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeRuntimeCost(value: unknown) {
|
||||||
|
const parsedValue = Number(value);
|
||||||
|
return Number.isFinite(parsedValue) && parsedValue > 0
|
||||||
|
? Math.max(0, Math.round(parsedValue))
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareFeaturePointsCharge(
|
||||||
|
input: FeaturePointsChargeInput
|
||||||
|
): Promise<FeaturePointsChargeOk | FeaturePointsChargeBlocked> {
|
||||||
|
const unitCost = normalizeRuntimeCost(input.unitCost);
|
||||||
|
const pointsConfig = input.pointsConfig || (await getPointsSystemConfig());
|
||||||
|
const chargeable = pointsConfig.enabled === true && unitCost > 0;
|
||||||
|
|
||||||
|
if (chargeable && pointsConfig.blockOnInsufficient !== false) {
|
||||||
|
const userData = await getUserById({ id: input.userId });
|
||||||
|
if (!userData?.isHighVip) {
|
||||||
|
const currentBalance = Math.max(0, Number(userData?.pointsBalance || 0));
|
||||||
|
if (currentBalance < unitCost) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 402,
|
||||||
|
payload: {
|
||||||
|
success: false,
|
||||||
|
error: "积分不足",
|
||||||
|
code: "insufficient_points",
|
||||||
|
message: `积分不足,当前余额 ${currentBalance},本次预计消耗 ${unitCost}。`,
|
||||||
|
balance: currentBalance,
|
||||||
|
expectedCost: unitCost,
|
||||||
|
unitCost,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
chargeable,
|
||||||
|
unitCost,
|
||||||
|
consume: async (overrides = {}) => {
|
||||||
|
if (!chargeable) return null;
|
||||||
|
return consumeUserPoints({
|
||||||
|
userId: input.userId,
|
||||||
|
amount: unitCost,
|
||||||
|
reason: overrides.reason || input.reason,
|
||||||
|
modelId: overrides.modelId ?? input.modelId ?? null,
|
||||||
|
note: overrides.note || input.note,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -44,14 +44,25 @@ export type VideoCanvasFunctionConfig = {
|
|||||||
|
|
||||||
const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
||||||
{
|
{
|
||||||
key: "character-three-view",
|
key: "story-beat-grid-4",
|
||||||
label: "角色三视图",
|
label: "剧情推演四宫格",
|
||||||
kind: "image-edit",
|
kind: "image-edit",
|
||||||
route: "canvas-generate-image",
|
route: "canvas-generate-image",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
creditCost: 14,
|
creditCost: 14,
|
||||||
prompt:
|
prompt:
|
||||||
"以输入角色图为唯一角色参考,生成正面、侧面、背面三视图角色设定图。保持五官、发型、服装、颜色和体型一致,干净白底,三视图水平排布,适合后续视频角色一致性使用。",
|
"以选中图片作为角色身份、服装、发型、场景光线、镜头距离和画面质感的唯一参考,生成一张 2x2 剧情推演四宫格合成图。画面中只能有 4 个画面区域,只允许一条竖向分割线和一条横向分割线;严禁出现 3 行 3 列、9 个画面区域或九宫格布局。四格都必须保持参考图的近景/半身近中景人物构图,主体占画面主要面积,不要远景、全景、森林空镜或主体很小的画面。四格是同一角色在同一自然光场景中的连续轻微剧情变化:1 正面安静凝视,2 保留参考图抬手/袖摆动作的延续,3 视线或表情轻微变化,4 情绪收束的近中景反应。保持五官、发型、绿色服装、柔和森林逆光、色调和景深一致;每格角度和动作有轻微差异,但不能像 9 个不同机位分镜。输出单张 16:9 图片,2x2 等分排版,分割线清晰干净,不能出现文字、水印、编号、边框标签或 3x3/5x5 宫格。",
|
||||||
|
description: "官方剧情推演四宫格:基于参考图生成 2x2 连续剧情分镜合成图。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "character-three-view",
|
||||||
|
label: "角色三视图生成",
|
||||||
|
kind: "image-edit",
|
||||||
|
route: "canvas-generate-image",
|
||||||
|
enabled: true,
|
||||||
|
creditCost: 14,
|
||||||
|
prompt:
|
||||||
|
"以选中图片作为唯一角色参考,生成一张角色三视图设定图。只输出正面、侧面、背面 3 个完整站姿人物,三人横向并排在同一张 16:9 画面中,白色或浅灰干净背景。必须保持同一角色身份、五官、发型、绿色服装、材质、体型比例和配饰一致;正面、侧面、背面视角要清晰可辨,人物全身完整入画,站姿自然。禁止生成 3x3 九宫格、多机位分镜、黑色分割线、重复头像、文字、水印、编号、环境场景或额外人物。",
|
||||||
description: "官方角色三视图预设,使用参考图生图/编辑模型。",
|
description: "官方角色三视图预设,使用参考图生图/编辑模型。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -65,6 +76,50 @@ const defaultFunctions: VideoCanvasFunctionDefinition[] = [
|
|||||||
"以选中图片作为角色、服装、发型、场景光线的唯一参考,生成一张 3x3 多机位分镜合成图。必须是 9 个明显不同的镜头画面,不是同一张图裁切或重复人脸。九格分别包含:1 森林环境远景/主体很小,2 全身远景,3 中景正面,4 侧面半身,5 低机位或俯拍,6 面部近景,7 手部/衣袖/动作细节,8 背面或侧背,9 仰视或情绪特写。保持同一角色身份、绿色服装、自然森林光线一致,每格构图、景别、机位和主体大小必须不同。输出为单张 16:9 图片,九格之间使用清晰黑色分割线,不能出现文字、水印、编号,不能白底棚拍。",
|
"以选中图片作为角色、服装、发型、场景光线的唯一参考,生成一张 3x3 多机位分镜合成图。必须是 9 个明显不同的镜头画面,不是同一张图裁切或重复人脸。九格分别包含:1 森林环境远景/主体很小,2 全身远景,3 中景正面,4 侧面半身,5 低机位或俯拍,6 面部近景,7 手部/衣袖/动作细节,8 背面或侧背,9 仰视或情绪特写。保持同一角色身份、绿色服装、自然森林光线一致,每格构图、景别、机位和主体大小必须不同。输出为单张 16:9 图片,九格之间使用清晰黑色分割线,不能出现文字、水印、编号,不能白底棚拍。",
|
||||||
description: "官方九宫格工具:先生成带黑色分割线的 3x3 分镜图,再由宫格切分拆成单张镜头。",
|
description: "官方九宫格工具:先生成带黑色分割线的 3x3 分镜图,再由宫格切分拆成单张镜头。",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "continuous-storyboard-grid-25",
|
||||||
|
label: "25宫格连贯分镜",
|
||||||
|
kind: "image-edit",
|
||||||
|
route: "canvas-generate-image",
|
||||||
|
enabled: true,
|
||||||
|
creditCost: 14,
|
||||||
|
prompt:
|
||||||
|
"以选中图片作为角色、服装、发型、场景光线和电影质感的唯一参考,生成一张 5x5 连贯分镜合成图。必须是 25 个连续镜头,体现从远景建立场景、人物动作推进、情绪变化、细节特写到结尾反应的完整微型叙事,不是同一张图重复裁切。保持同一角色身份和绿色服装一致,森林自然光线统一;每格景别、机位、主体大小或动作都要有明显差异。输出为单张 16:9 图片,5x5 等分排版,使用清晰分割线,不能出现文字、水印、编号或多余边框说明。",
|
||||||
|
description: "官方 25 宫格连贯分镜:生成 5x5 连续镜头合成图。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cinematic-lighting-correction",
|
||||||
|
label: "电影级光影校正",
|
||||||
|
kind: "image-edit",
|
||||||
|
route: "canvas-generate-image",
|
||||||
|
enabled: true,
|
||||||
|
creditCost: 14,
|
||||||
|
prompt:
|
||||||
|
"以选中图片为唯一参考,在保持角色身份、发型、服装、姿态和构图一致的前提下,进行电影级光影校正。增强自然逆光、轮廓光、皮肤通透感、背景层次和柔和景深,保留原始角色与场景,不改变人物数量,不生成宫格或分镜。输出单张 16:9 图片,不能出现文字、水印、编号或额外边框。",
|
||||||
|
description: "官方电影级光影校正:对参考图做单张电影感光影重塑。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "frame-prediction-plus-3s",
|
||||||
|
label: "画面推演 - 3秒后",
|
||||||
|
kind: "image-edit",
|
||||||
|
route: "canvas-generate-image",
|
||||||
|
enabled: true,
|
||||||
|
creditCost: 14,
|
||||||
|
prompt:
|
||||||
|
"以选中图片作为当前画面,推演同一镜头时间线 3 秒后的画面。保持角色身份、服装、发型、场景和自然光线一致,只让动作、表情、衣袖/头发摆动、视线或镜头位置发生合理连续变化。输出单张 16:9 图片,不要生成宫格、分镜、文字、水印或编号。",
|
||||||
|
description: "官方画面推演:根据当前帧生成 3 秒后的单帧结果。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "frame-prediction-minus-5s",
|
||||||
|
label: "画面推演 - 5秒前",
|
||||||
|
kind: "image-edit",
|
||||||
|
route: "canvas-generate-image",
|
||||||
|
enabled: true,
|
||||||
|
creditCost: 14,
|
||||||
|
prompt:
|
||||||
|
"以选中图片作为当前画面,反向推演同一镜头时间线 5 秒前的画面。保持角色身份、服装、发型、场景和自然光线一致,只让动作起势、姿态、表情、衣袖/头发位置或镜头位置回到合理的前一状态。输出单张 16:9 图片,不要生成宫格、分镜、文字、水印或编号。",
|
||||||
|
description: "官方画面推演:根据当前帧生成 5 秒前的单帧结果。",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "grid-split",
|
key: "grid-split",
|
||||||
label: "宫格切分",
|
label: "宫格切分",
|
||||||
@ -242,6 +297,58 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyFunctionPrompts: Record<string, string[]> = {
|
||||||
|
"story-beat-grid-4": [
|
||||||
|
"以选中图片作为角色身份、服装、发型、场景光线、镜头距离和画面质感的唯一参考,生成一张 2x2 剧情推演四宫格合成图。四格都必须保持参考图的近景/半身近中景人物构图,主体占画面主要面积,不要远景、全景、森林空镜或主体很小的画面。四格是同一角色在同一自然光场景中的连续轻微剧情变化:1 正面安静凝视,2 保留参考图抬手/袖摆动作的延续,3 视线或表情轻微变化,4 情绪收束的近中景反应。保持五官、发型、绿色服装、柔和森林逆光、色调和景深一致;每格角度和动作有轻微差异,但不能像 9 个不同机位分镜。输出单张 16:9 图片,2x2 等分排版,分割线清晰干净,不能出现文字、水印、编号、边框标签或 3x3/5x5 宫格。",
|
||||||
|
"以选中图片作为角色、服装、发型、场景光线的唯一参考,生成一张 3x3 多机位分镜合成图。必须是 9 个明显不同的镜头画面,不是同一张图裁切或重复人脸。九格分别包含:1 森林环境远景/主体很小,2 全身远景,3 中景正面,4 侧面半身,5 低机位或俯拍,6 面部近景,7 手部/衣袖/动作细节,8 背面或侧背,9 仰视或情绪特写。保持同一角色身份、绿色服装、自然森林光线一致,每格构图、景别、机位和主体大小必须不同。输出为单张 16:9 图片,九格之间使用清晰黑色分割线,不能出现文字、水印、编号,不能白底棚拍。",
|
||||||
|
],
|
||||||
|
"character-three-view": [
|
||||||
|
"以输入角色图为唯一角色参考,生成正面、侧面、背面三视图角色设定图。保持五官、发型、服装、颜色和体型一致,干净白底,三视图水平排布,适合后续视频角色一致性使用。",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacyFunctionLabels: Record<string, string[]> = {
|
||||||
|
"character-three-view": ["角色三视图"],
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeComparableText(value: string) {
|
||||||
|
return value.replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFunctionLabel(
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
fallbackLabel: string,
|
||||||
|
) {
|
||||||
|
const label = typeof value === "string" ? value.trim() : "";
|
||||||
|
if (!label) return fallbackLabel;
|
||||||
|
const normalizedLabel = normalizeComparableText(label);
|
||||||
|
const isLegacyLabel = (legacyFunctionLabels[key] || []).some(
|
||||||
|
(legacyLabel) => normalizeComparableText(legacyLabel) === normalizedLabel,
|
||||||
|
);
|
||||||
|
return isLegacyLabel ? fallbackLabel : label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFunctionPrompt(
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
fallbackPrompt: string,
|
||||||
|
) {
|
||||||
|
const prompt = typeof value === "string" ? value.trim() : "";
|
||||||
|
if (!prompt) return fallbackPrompt;
|
||||||
|
const normalizedPrompt = normalizeComparableText(prompt);
|
||||||
|
const isStoryPromptClearlyNineGrid =
|
||||||
|
key === "story-beat-grid-4" &&
|
||||||
|
/(?:生成一张\s*3\s*[x×]\s*3|生成一张[^。]*九宫格|九格分别|必须是\s*9\s*个)/.test(
|
||||||
|
normalizedPrompt,
|
||||||
|
);
|
||||||
|
if (isStoryPromptClearlyNineGrid) return fallbackPrompt;
|
||||||
|
const isLegacyPrompt = (legacyFunctionPrompts[key] || []).some(
|
||||||
|
(legacyPrompt) => normalizeComparableText(legacyPrompt) === normalizedPrompt,
|
||||||
|
);
|
||||||
|
return isLegacyPrompt ? fallbackPrompt : prompt;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFunctionDefinition(
|
function normalizeFunctionDefinition(
|
||||||
key: string,
|
key: string,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
@ -251,7 +358,7 @@ function normalizeFunctionDefinition(
|
|||||||
const normalized: VideoCanvasFunctionDefinition = {
|
const normalized: VideoCanvasFunctionDefinition = {
|
||||||
...fallback,
|
...fallback,
|
||||||
key,
|
key,
|
||||||
label: String(incoming.label || fallback.label),
|
label: normalizeFunctionLabel(key, incoming.label, fallback.label),
|
||||||
kind: (incoming.kind as VideoCanvasFunctionKind) || fallback.kind,
|
kind: (incoming.kind as VideoCanvasFunctionKind) || fallback.kind,
|
||||||
route: (incoming.route as VideoCanvasBackendRoute) || fallback.route,
|
route: (incoming.route as VideoCanvasBackendRoute) || fallback.route,
|
||||||
enabled: typeof incoming.enabled === "boolean" ? incoming.enabled : fallback.enabled,
|
enabled: typeof incoming.enabled === "boolean" ? incoming.enabled : fallback.enabled,
|
||||||
@ -260,7 +367,7 @@ function normalizeFunctionDefinition(
|
|||||||
typeof incoming.creditCost === "number" && Number.isFinite(incoming.creditCost)
|
typeof incoming.creditCost === "number" && Number.isFinite(incoming.creditCost)
|
||||||
? Math.max(0, incoming.creditCost)
|
? Math.max(0, incoming.creditCost)
|
||||||
: fallback.creditCost,
|
: fallback.creditCost,
|
||||||
prompt: String(incoming.prompt || fallback.prompt),
|
prompt: normalizeFunctionPrompt(key, incoming.prompt, fallback.prompt),
|
||||||
description: String(incoming.description || fallback.description),
|
description: String(incoming.description || fallback.description),
|
||||||
requiresBackend:
|
requiresBackend:
|
||||||
typeof incoming.requiresBackend === "boolean"
|
typeof incoming.requiresBackend === "boolean"
|
||||||
|
|||||||
@ -253,7 +253,9 @@ export const imageGenerationService = {
|
|||||||
model?: string;
|
model?: string;
|
||||||
aspectRatio?: string;
|
aspectRatio?: string;
|
||||||
imageCount?: number;
|
imageCount?: number;
|
||||||
|
promptExtend?: boolean;
|
||||||
referenceImage?: string;
|
referenceImage?: string;
|
||||||
|
billingFeature?: 'multi-view' | 'repaint';
|
||||||
}): Promise<ApiResponse<{ images: Array<{ thumbnailUrl: string; originalUrl: string }>; model?: string; prompt?: string }>> {
|
}): Promise<ApiResponse<{ images: Array<{ thumbnailUrl: string; originalUrl: string }>; model?: string; prompt?: string }>> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/canvas/generate/image', {
|
const response = await fetch('/api/canvas/generate/image', {
|
||||||
|
|||||||
@ -109,6 +109,7 @@ export interface AgentWorkflowPageFeatureSettings {
|
|||||||
presetStepEnabled: boolean;
|
presetStepEnabled: boolean;
|
||||||
materialStepEnabled: boolean;
|
materialStepEnabled: boolean;
|
||||||
psdTemplateEnabled: boolean;
|
psdTemplateEnabled: boolean;
|
||||||
|
batchProcessingEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentWorkflowUiConfig {
|
export interface AgentWorkflowUiConfig {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user