"use client"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Separator } from "@/components/ui/separator"; import { Video, Check, ChevronDown, ChevronUp, Sparkles, Camera, Clock, Settings2, Clapperboard, Monitor, Ratio, SlidersHorizontal, Volume2, VolumeX, } from "lucide-react"; import { cn } from "@/lib/utils"; // 视频生成模式类型 type VideoGenerationMode = | "text-to-video" | "first-frame" | "first-last-frame" | "reference-images"; // 运镜模式类型(与官方一致) type CameraMove = | "none" // 智能运镜 | "static" // 固定镜头 | "hitchcock-push" // 希区柯克推进 | "hitchcock-pull" // 希区柯克拉远 | "mechanical-arm" // 机械臂 | "dynamic-orbit" // 动感环绕 | "center-orbit" // 中心环绕 | "crane" // 起重机 | "super-pull" // 超级拉远 | "ccw-rotate" // 逆时针回旋 | "cw-rotate" // 顺时针回旋 | "handheld" // 手持运镜 | "fast-push-pull"; // 快速推拉 // 视频参数接口 export interface VideoGenerationParams { mode: VideoGenerationMode; resolution: "480p" | "720p" | "1080p"; ratio: "16:9" | "4:3" | "1:1" | "3:4" | "9:16" | "21:9" | "adaptive"; duration: number; generateAudio?: boolean; watermark: boolean; cameraFixed: boolean; cameraMove?: CameraMove; // 新增:运镜模式 fps?: number; seed?: number; returnLastFrame?: boolean; } interface VideoParamsBarProps { modelName?: string; initialParams?: Partial; onParamsChange?: (params: VideoGenerationParams) => void; inline?: boolean; // 内联模式:只显示参数按钮,不显示外层容器 providerType?: string; } const MODE_LABELS: Record = { "text-to-video": "文生视频", "first-last-frame": "首尾帧", "reference-images": "智能多帧", "first-frame": "主体参考", }; const RATIO_OPTIONS = [ { value: "21:9", label: "21:9" }, { value: "16:9", label: "16:9" }, { value: "4:3", label: "4:3" }, { value: "1:1", label: "1:1" }, { value: "3:4", label: "3:4" }, { value: "9:16", label: "9:16" }, ] as const; const RESOLUTION_OPTIONS = [ { value: "480p", label: "480P" }, { value: "720p", label: "720P" }, { value: "1080p", label: "1080P" }, ] as const; // 🎬 运镜模式选项(与官方一致) const CAMERA_MOVE_OPTIONS = [ { value: "none", label: "智能运镜" }, { value: "static", label: "固定镜头" }, { value: "hitchcock-push", label: "希区柯克推进" }, { value: "hitchcock-pull", label: "希区柯克拉远" }, { value: "mechanical-arm", label: "机械臂" }, { value: "dynamic-orbit", label: "动感环绕" }, { value: "center-orbit", label: "中心环绕" }, { value: "crane", label: "起重机" }, { value: "super-pull", label: "超级拉远" }, { value: "ccw-rotate", label: "逆时针回旋" }, { value: "cw-rotate", label: "顺时针回旋" }, { value: "handheld", label: "手持运镜" }, { value: "fast-push-pull", label: "快速推拉" }, ] as const; // 帧率选项 const FPS_OPTIONS = [ { value: 24, label: "24fps" }, { value: 30, label: "30fps" }, { value: 60, label: "60fps" }, ] as const; const STANDARD_MODE_OPTIONS: Array<{ value: VideoGenerationMode; label: string }> = [ { value: "text-to-video", label: "文生视频" }, { value: "first-last-frame", label: "首尾帧" }, { value: "reference-images", label: "智能多帧" }, { value: "first-frame", label: "主体参考" }, ]; const FAL_MODE_OPTIONS: Array<{ value: VideoGenerationMode; label: string }> = [ { value: "first-frame", label: "图生视频" }, { value: "first-last-frame", label: "首尾帧" }, ]; const FAL_RATIO_OPTIONS = [ { value: "16:9", label: "16:9" }, { value: "9:16", label: "9:16" }, { value: "1:1", label: "1:1" }, ] as const; const STANDARD_DURATION_OPTIONS = [5, 10]; const FAL_DURATION_OPTIONS = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; const STANDARD_RATIO_SET = new Set(["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"]); const FAL_RATIO_SET = new Set(["16:9", "9:16", "1:1"]); function normalizeVideoParamsByProvider( source: Partial | undefined, isFalVideo: boolean ): VideoGenerationParams { const base: VideoGenerationParams = { mode: isFalVideo ? "first-frame" : "text-to-video", resolution: "720p", ratio: "16:9", duration: 5, generateAudio: true, watermark: false, cameraFixed: false, cameraMove: "none", fps: 24, }; const merged = { ...base, ...source }; if (isFalVideo) { if (merged.mode !== "first-frame" && merged.mode !== "first-last-frame") { merged.mode = "first-frame"; } merged.ratio = FAL_RATIO_SET.has(merged.ratio) ? merged.ratio : "16:9"; merged.duration = Math.max(3, Math.min(15, Math.round(Number(merged.duration) || 5))); merged.generateAudio = merged.generateAudio ?? true; return merged; } if ( merged.mode !== "text-to-video" && merged.mode !== "first-last-frame" && merged.mode !== "reference-images" && merged.mode !== "first-frame" ) { merged.mode = "text-to-video"; } merged.ratio = STANDARD_RATIO_SET.has(merged.ratio) ? merged.ratio : "16:9"; merged.duration = merged.duration >= 8 ? 10 : 5; return merged; } export function VideoParamsBar({ modelName = "视频 3.0", initialParams, onParamsChange, inline = false, providerType = "volcano-video", }: VideoParamsBarProps) { const isFalVideo = providerType === "fal-video"; const modeOptions = isFalVideo ? FAL_MODE_OPTIONS : STANDARD_MODE_OPTIONS; const ratioOptions = isFalVideo ? FAL_RATIO_OPTIONS : RATIO_OPTIONS; const durationOptions = isFalVideo ? FAL_DURATION_OPTIONS : STANDARD_DURATION_OPTIONS; const [params, setParams] = useState(() => normalizeVideoParamsByProvider(initialParams, isFalVideo) ); const [collapsed, setCollapsed] = useState(false); useEffect(() => { const normalized = normalizeVideoParamsByProvider(initialParams, isFalVideo); setParams((prev) => { if (JSON.stringify(prev) === JSON.stringify(normalized)) { return prev; } return normalized; }); }, [initialParams, isFalVideo]); // 获取当前运镜模式的标签 const getCurrentCameraLabel = () => { if (params.cameraMove) { const option = CAMERA_MOVE_OPTIONS.find(opt => opt.value === params.cameraMove); return option ? option.label : "智能运镜"; } return "智能运镜"; }; // 🆕 根据模式获取图片上传配置(根据火山引擎官方文档) const getImageUploadConfig = () => { if (isFalVideo) { switch (params.mode) { case "first-last-frame": return { min: 2, max: 2, label: "首尾帧(2 张)" }; case "first-frame": default: return { min: 1, max: 1, label: "图生视频(1 张起始图)" }; } } switch (params.mode) { case "text-to-video": return { min: 0, max: 0, label: "文生视频(无需图片)" }; case "first-last-frame": return { min: 2, max: 2, label: "首尾帧(各 1 张)" }; case "first-frame": return { min: 1, max: 1, label: "首帧模式(1 张)" }; case "reference-images": return { min: 1, max: 4, label: "参考图(1-4 张)" }; default: return { min: 0, max: 0, label: "" }; } }; const imageConfig = getImageUploadConfig(); const updateParam = ( key: K, value: VideoGenerationParams[K] ) => { const newParams = { ...params, [key]: value }; setParams(newParams); 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) => ( ))}
{/* 比例 */}

选择比例

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

选择分辨率

{RESOLUTION_OPTIONS.map((option) => ( ))}
)} {/* 时长 */}

选择时长

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]" />
); } // 非内联模式(独立显示) if (collapsed) { return (
视频参数:{modeOptions.find((option) => option.value === params.mode)?.label || MODE_LABELS[params.mode]} | {params.ratio} | {params.duration}s
); } return (
{/* 🆕 图片上传区域(根据模式动态显示) */} {imageConfig.max > 0 && (
{params.mode === "first-last-frame" && (
)} {params.mode === "first-frame" && ( )} {params.mode === "reference-images" && (
{[1, 2, 3, 4].slice(0, imageConfig.max).map((idx) => ( ))}
)}
)} {/* 参数配置栏 - 移除背景色和顶部边框,适应嵌入式布局 */}
{/* 模式选择(无边框,更简洁) */} {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)} className="scale-75" />
{isFalVideo && (
updateParam("generateAudio", checked)} className="scale-75" />
)}
updateParam("seed", Number(e.target.value) || undefined) } className="h-8 text-xs" />
); }