diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 78c07e5f4..50aad92b9 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -134,6 +134,9 @@ type AiProvider = { type ImageGenConfig = { maxImages?: number; // 最大输出图片数,如阿里 1-6,Gemini 1-2 supportsSize?: boolean; // 是否支持自定义分辨率 + supports1K?: boolean; // 是否支持 1K 分辨率 + supports2K?: boolean; // 是否支持 2K 分辨率 + supports4K?: boolean; // 是否支持 4K 分辨率 supportsPromptExtend?: boolean; // 是否支持提示词智能优化 supportsNegativePrompt?: boolean; // 是否支持反向提示词 supportsWatermark?: boolean; // 是否支持水印开关 @@ -1117,6 +1120,9 @@ export function AdminSettingsClient() { imageGenConfig: { maxImages: 2, supportsSize: false, + supports1K: true, + supports2K: true, + supports4K: true, supportsPromptExtend: false, supportsNegativePrompt: false, supportsWatermark: false, @@ -6466,7 +6472,7 @@ export function AdminSettingsClient() { /> - +
启用加载占位图
+ + {/* 分辨率档位开关 */} +
+
分辨率档位控制
+

控制前台用户可选的分辨率档位,关闭后用户将无法选择对应分辨率

+
+
+ setModelForm({ + ...modelForm, + imageGenConfig: { + ...modelForm.imageGenConfig, + supports1K: checked, + }, + })} + /> + +
+
+ setModelForm({ + ...modelForm, + imageGenConfig: { + ...modelForm.imageGenConfig, + supports2K: checked, + }, + })} + /> + +
+
+ setModelForm({ + ...modelForm, + imageGenConfig: { + ...modelForm.imageGenConfig, + supports4K: checked, + }, + })} + /> + +
+
+
)} @@ -6545,6 +6598,9 @@ export function AdminSettingsClient() { imageGenConfig: (model as any).imageGenConfig || { maxImages: 2, supportsSize: false, + supports1K: true, + supports2K: true, + supports4K: true, supportsPromptExtend: false, supportsNegativePrompt: false, supportsWatermark: false, diff --git a/app/api/admin/ai-models/route.ts b/app/api/admin/ai-models/route.ts index 3dfd76b6d..ebeb2f508 100644 --- a/app/api/admin/ai-models/route.ts +++ b/app/api/admin/ai-models/route.ts @@ -20,6 +20,9 @@ import { const imageGenConfigSchema = z.object({ maxImages: z.number().min(1).max(10).optional(), supportsSize: z.boolean().optional(), + supports1K: z.boolean().optional(), + supports2K: z.boolean().optional(), + supports4K: z.boolean().optional(), supportsPromptExtend: z.boolean().optional(), supportsNegativePrompt: z.boolean().optional(), supportsWatermark: z.boolean().optional(), diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index c9de01e87..2afa9051b 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -2005,6 +2005,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF imageGenConfig?: { maxImages?: number; supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean; @@ -2020,6 +2023,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF imageGenConfig?: { maxImages?: number; supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean; diff --git a/components/chat.tsx b/components/chat.tsx index 9196694bf..fb366e9bf 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -378,6 +378,9 @@ export function Chat({ imageGenConfig?: { maxImages?: number; supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean; diff --git a/components/home/lovart-home.tsx b/components/home/lovart-home.tsx index 55604464c..f3ddcb8f5 100644 --- a/components/home/lovart-home.tsx +++ b/components/home/lovart-home.tsx @@ -99,6 +99,9 @@ type ModelOption = { imageGenConfig?: { maxImages?: number; supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean; @@ -408,6 +411,9 @@ export function LovartHome({ imageGenConfig?: { maxImages?: number; supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean; diff --git a/components/image-params-bar.tsx b/components/image-params-bar.tsx index e5f033b4f..ef2f9d39b 100644 --- a/components/image-params-bar.tsx +++ b/components/image-params-bar.tsx @@ -2,13 +2,7 @@ import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Check, Link2, Scan, SlidersHorizontal, Sparkles } from "lucide-react"; +import { Link2, Scan, SlidersHorizontal, Sparkles } from "lucide-react"; import { cn } from "@/lib/utils"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; @@ -17,7 +11,6 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { Label } from "@/components/ui/label"; // 图片生成参数接口 export interface ImageGenerationParams { @@ -44,6 +37,9 @@ interface ImageParamsBarProps { imageGenConfig?: { maxImages?: number; supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean; @@ -114,9 +110,22 @@ export function ImageParamsBar({ const options = getImageCountOptions(); const fallbackCount = options[0] ?? 1; const imageCount = options.includes(raw.imageCount || fallbackCount) ? (raw.imageCount || fallbackCount) : fallbackCount; + // 根据模型配置校正分辨率:如果当前选择的档位被禁用,则降级到可用的档位 + let imageSize = raw.imageSize || "2K"; + if (imageGenConfig) { + const sizeAllowed = (s: string) => { + if (s === "1K") return imageGenConfig.supports1K !== false; + if (s === "2K") return imageGenConfig.supports2K !== false; + if (s === "4K") return imageGenConfig.supports4K !== false; + return true; + }; + if (!sizeAllowed(imageSize)) { + imageSize = ["2K", "1K", "4K"].find(sizeAllowed) || "2K"; + } + } return { ...raw, - imageSize: raw.imageSize || "2K", + imageSize, imageCount, }; }; @@ -153,6 +162,16 @@ export function ImageParamsBar({ const showPromptExtend = isAlibabaImage || imageGenConfig?.supportsPromptExtend; const showNegativePrompt = isAlibabaImage || imageGenConfig?.supportsNegativePrompt; + // 根据模型配置过滤可用的分辨率档位 + const availableSizeOptions = IMAGE_SIZE_OPTIONS.filter((option) => { + if (option.disabled) return false; + if (!imageGenConfig) return true; // 无配置时全部显示 + if (option.value === "1K" && imageGenConfig.supports1K === false) return false; + if (option.value === "2K" && imageGenConfig.supports2K === false) return false; + if (option.value === "4K" && imageGenConfig.supports4K === false) return false; + return true; + }); + const getComputedOutputSize = (aspectRatio: string, imageSize: string) => { const size = IMAGE_SIZE_PIXEL_MAP[imageSize] || IMAGE_SIZE_PIXEL_MAP["2K"]; if (aspectRatio === "auto" || aspectRatio === "1:1") { @@ -221,7 +240,7 @@ export function ImageParamsBar({ if (initialParams !== undefined && onParamsChange && !isSameParams(rawParams, nextParams)) { onParamsChange(nextParams); } - }, [initialParams, providerType, imageGenConfig?.maxImages]); + }, [initialParams, providerType, imageGenConfig?.maxImages, imageGenConfig?.supports1K, imageGenConfig?.supports2K, imageGenConfig?.supports4K]); const updateParam = ( key: K, @@ -295,7 +314,7 @@ export function ImageParamsBar({

选择分辨率

- {IMAGE_SIZE_OPTIONS.filter((option) => !option.disabled).map((option) => ( + {availableSizeOptions.map((option) => ( - - - {imageCountOptions.map((count) => ( - updateParam("imageCount", count)} - className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0" - > - - {count}张 - - ))} - - + + +
+

生成数量

+
+ {imageCountOptions.map((count) => ( + + ))} +
+
+
+ )} {/* 🆕 阿里生图专用:提示词优化 & 反向提示词 */} {!hideAdvancedOptions && (showPromptExtend || showNegativePrompt) && ( - - -
-
高级选项
- + +
{/* 提示词智能优化 */} {showPromptExtend && (
- -

AI 自动优化您的描述

+ 提示词优化 +

AI 自动优化您的描述

)} - + {/* 反向提示词 */} {showNegativePrompt && ( -
- +
+ 反向提示词 updateParam("negativePrompt", e.target.value)} - className="h-8 text-xs" + className="h-7 rounded-[8px] border-[#dce1e8] bg-[#eef1f4]/90 text-[12px] text-[#2f3b48] placeholder:text-[#b0b8c4]" />
)} diff --git a/components/multimodal-input.tsx b/components/multimodal-input.tsx index c3d76f168..5bd17da1c 100644 --- a/components/multimodal-input.tsx +++ b/components/multimodal-input.tsx @@ -21,6 +21,9 @@ type DynamicModel = { imageGenConfig?: { maxImages?: number; supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean; @@ -153,6 +156,9 @@ function PureMultimodalInput({ imageGenConfig?: { maxImages?: number; supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean; diff --git a/components/video-params-bar.tsx b/components/video-params-bar.tsx index f548e86fa..1bd68bda7 100644 --- a/components/video-params-bar.tsx +++ b/components/video-params-bar.tsx @@ -8,6 +8,11 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { Dialog, DialogContent, @@ -18,7 +23,6 @@ import { import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; -import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { Video, @@ -32,6 +36,9 @@ import { Clapperboard, Monitor, Ratio, + SlidersHorizontal, + Volume2, + VolumeX, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -265,219 +272,266 @@ const getImageUploadConfig = () => { onParamsChange?.(newParams); }; - // 🆕 内联模式:只返回参数按钮,嵌入工具栏 + // 🆕 内联模式:只返回参数按钮,嵌入工具栏(统一玻璃面板风格) if (inline) { + const modeLabel = modeOptions.find((o) => o.value === params.mode)?.label || MODE_LABELS[params.mode]; + const durationLabel = `${params.duration}s`; + return ( -
- {/* 模式选择 */} - - +
+ {/* 主参数面板:模式 | 比例 | 时长 */} + + - - - {modeOptions.map((option) => ( - updateParam("mode", option.value)} - > - - {option.label} - - ))} - - - - {/* 宽高比 */} - - - - - -
- {ratioOptions.map((option) => ( - - ))} -
-
-
- - {!isFalVideo && ( - - - - - - {RESOLUTION_OPTIONS.map((option) => ( - updateParam("resolution", option.value)}> - - {option.label} - - ))} - - - )} - - {/* 时长 */} - - - - - - {durationOptions.map((duration) => ( - updateParam("duration", duration)}> - - {duration}s - - ))} - - - - {isFalVideo ? ( - - ) : ( - <> - - - - - - {FPS_OPTIONS.map((option) => ( - updateParam("fps", option.value as VideoGenerationParams["fps"])}> - - {option.label} - - ))} - - - - - - - - - {CAMERA_MOVE_OPTIONS.map((option) => ( - { - updateParam("cameraMove", option.value); - updateParam("cameraFixed", option.value === "static"); - }} - > - - {option.label} - - ))} - - - - )} - - {/* 高级设置 */} - - - - - - - 高级设置 - -
-
- - updateParam("returnLastFrame", checked)} - /> +
+ {/* 模式 */} +
+

选择模式

+
+ {modeOptions.map((option) => ( + + ))} +
- {isFalVideo && ( -
- - updateParam("generateAudio", checked)} - /> + + {/* 比例 */} +
+

选择比例

+
+ {ratioOptions.map((option) => ( + + ))} +
+
+ + {/* 分辨率(非 fal) */} + {!isFalVideo && ( +
+

选择分辨率

+
+ {RESOLUTION_OPTIONS.map((option) => ( + + ))} +
)} -
- - updateParam("seed", Number(e.target.value) || undefined)} - className="h-8 text-xs" - /> + + {/* 时长 */} +
+

选择时长

+
6 ? "grid-cols-5" : "grid-cols-2" + )}> + {durationOptions.map((duration) => ( + + ))} +
- -
+ +
+ + {/* 副参数:帧率 / 运镜 / 音频(合并为一个 Popover) */} + + + + + +
+ {isFalVideo ? ( + /* fal-video:音频开关 */ +
+

音频

+
+ + +
+
+ ) : ( + <> + {/* 帧率 */} +
+

帧率

+
+ {FPS_OPTIONS.map((option) => ( + + ))} +
+
+ + {/* 运镜 */} +
+

运镜模式

+
+ {CAMERA_MOVE_OPTIONS.map((option) => ( + + ))} +
+
+ + )} + + {/* 高级选项 */} +
+
+ 返回尾帧 + updateParam("returnLastFrame", checked)} + /> +
+ {isFalVideo && ( +
+ 生成音频 + updateParam("generateAudio", checked)} + /> +
+ )} +
+ 随机种子 + updateParam("seed", Number(e.target.value) || undefined)} + className="h-7 rounded-[8px] border-[#dce1e8] bg-[#eef1f4]/90 text-[12px] text-[#2f3b48] placeholder:text-[#b0b8c4]" + /> +
+
+
+
+
); } diff --git a/lib/db/queries.ts b/lib/db/queries.ts index 5340fa207..850bdb761 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -1414,6 +1414,9 @@ export async function getDefaultAiModel(): Promise { interface ImageGenConfig { maxImages?: number; supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; supportsPromptExtend?: boolean; supportsNegativePrompt?: boolean; supportsWatermark?: boolean;