diff --git a/frontend/src/components/DynamicParameterControls.tsx b/frontend/src/components/DynamicParameterControls.tsx new file mode 100644 index 00000000..11c4e70f --- /dev/null +++ b/frontend/src/components/DynamicParameterControls.tsx @@ -0,0 +1,104 @@ +/** + * DynamicParameterControls - Schema-driven parameter UI rendering. + * + * This component renders UI controls dynamically based on the pipeline's + * JSON schema. It extracts renderable parameters from the schema and + * generates appropriate controls (sliders, toggles, selects, etc.) based + * on the parameter types and constraints. + * + * Usage: + * ```tsx + * handleChange(paramName, value)} + * /> + * ``` + * + * The component automatically: + * - Infers control types from JSON schema (slider for bounded numbers, toggle for booleans, etc.) + * - Applies mode-specific defaults + * - Generates labels from parameter names + * - Shows tooltips from schema descriptions + */ + +import { useDynamicParameters } from "../hooks/useDynamicParameters"; +import { DynamicControl } from "./dynamic-controls"; +import type { PipelineConfigSchema, ModeDefaults } from "../lib/api"; +import type { InputMode } from "../types"; + +export interface DynamicParameterControlsProps { + /** The pipeline config schema containing parameter definitions */ + schema: PipelineConfigSchema | undefined; + /** Mode-specific default overrides */ + modeDefaults?: Record; + /** Current input mode (text/video) */ + inputMode?: InputMode; + /** Current parameter values */ + values: Record; + /** Callback when a parameter value changes */ + onValueChange: (paramName: string, value: unknown) => void; + /** Whether controls should be disabled */ + disabled?: boolean; + /** Optional CSS class name */ + className?: string; + /** Optional filter to include only specific parameters */ + includeOnly?: string[]; + /** Optional filter to exclude specific parameters */ + exclude?: string[]; +} + +export function DynamicParameterControls({ + schema, + modeDefaults, + inputMode, + values, + onValueChange, + disabled = false, + className = "", + includeOnly, + exclude, +}: DynamicParameterControlsProps) { + const { parameters, getValue, handleChange } = useDynamicParameters({ + schema, + modeDefaults, + inputMode, + values, + onValueChange, + }); + + // Apply filters + let filteredParameters = parameters; + + if (includeOnly && includeOnly.length > 0) { + filteredParameters = filteredParameters.filter(p => + includeOnly.includes(p.name) + ); + } + + if (exclude && exclude.length > 0) { + filteredParameters = filteredParameters.filter( + p => !exclude.includes(p.name) + ); + } + + if (filteredParameters.length === 0) { + return null; + } + + return ( +
+ {filteredParameters.map(parameter => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index cf95b3dc..316932e8 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; import { Select, @@ -14,19 +13,7 @@ import { TooltipProvider, TooltipTrigger, } from "./ui/tooltip"; -import { LabelWithTooltip } from "./ui/label-with-tooltip"; -import { Input } from "./ui/input"; -import { Button } from "./ui/button"; -import { Toggle } from "./ui/toggle"; -import { SliderWithInput } from "./ui/slider-with-input"; -import { Hammer, Info, Minus, Plus, RotateCcw } from "lucide-react"; -import { PARAMETER_METADATA } from "../data/parameterMetadata"; -import { DenoisingStepsSlider } from "./DenoisingStepsSlider"; -import { - getResolutionScaleFactor, - adjustResolutionForPipeline, -} from "../lib/utils"; -import { useLocalSliderValue } from "../hooks/useLocalSliderValue"; +import { Hammer, Info } from "lucide-react"; import type { PipelineId, LoRAConfig, @@ -36,10 +23,8 @@ import type { PipelineInfo, VaeType, } from "../types"; -import { LoRAManager } from "./LoRAManager"; - -// Minimum dimension for most pipelines (will be overridden by pipeline-specific minDimension from schema) -const DEFAULT_MIN_DIMENSION = 1; +import { SettingsPanelRenderer } from "./SettingsPanelRenderer"; +import type { PipelineSchemaInfo } from "../lib/api"; interface SettingsPanelProps { className?: string; @@ -48,6 +33,8 @@ interface SettingsPanelProps { onPipelineIdChange?: (pipelineId: PipelineId) => void; isStreaming?: boolean; isLoading?: boolean; + // Pipeline schema info - required for SettingsPanelRenderer + schema?: PipelineSchemaInfo; // Resolution is required - parent should always provide from schema defaults resolution: { height: number; @@ -76,8 +63,6 @@ interface SettingsPanelProps { loraMergeStrategy?: LoraMergeStrategy; // Input mode for conditional rendering of noise controls inputMode?: InputMode; - // Whether this pipeline supports noise controls in video mode (schema-derived) - supportsNoiseControls?: boolean; // Spout settings spoutSender?: SettingsState["spoutSender"]; onSpoutSenderChange?: (spoutSender: SettingsState["spoutSender"]) => void; @@ -98,6 +83,20 @@ interface SettingsPanelProps { // Preprocessors preprocessorIds?: string[]; onPreprocessorIdsChange?: (ids: string[]) => void; + + // Generic config values for dynamic fields (e.g., new_param, new_field, etc.) + // Maps field names to their current values and onChange handlers + configValues?: Record< + string, + { + value: unknown; + onChange?: (value: unknown) => void; + } + >; + + // Generic handler for any config field that doesn't have an explicit handler + // Called with (fieldName, value) when a dynamic field changes + onConfigValueChange?: (fieldName: string, value: unknown) => void; } export function SettingsPanel({ @@ -107,6 +106,7 @@ export function SettingsPanel({ onPipelineIdChange, isStreaming = false, isLoading = false, + schema, resolution, onResolutionChange, seed = 42, @@ -129,7 +129,6 @@ export function SettingsPanel({ onLorasChange, loraMergeStrategy = "permanent_merge", inputMode, - supportsNoiseControls = false, spoutSender, onSpoutSenderChange, spoutAvailable = false, @@ -144,121 +143,60 @@ export function SettingsPanel({ vaeTypes, preprocessorIds = [], onPreprocessorIdsChange, + configValues, + onConfigValueChange, }: SettingsPanelProps) { - // Local slider state management hooks - const noiseScaleSlider = useLocalSliderValue(noiseScale, onNoiseScaleChange); - const kvCacheAttentionBiasSlider = useLocalSliderValue( - kvCacheAttentionBias, - onKvCacheAttentionBiasChange - ); - const vaceContextScaleSlider = useLocalSliderValue( - vaceContextScale, - onVaceContextScaleChange - ); - - // Validation error states - const [heightError, setHeightError] = useState(null); - const [widthError, setWidthError] = useState(null); - const [seedError, setSeedError] = useState(null); - - // Check if resolution needs adjustment - const scaleFactor = getResolutionScaleFactor(pipelineId); - const resolutionWarning = - scaleFactor && - (resolution.height % scaleFactor !== 0 || - resolution.width % scaleFactor !== 0) - ? `Resolution will be adjusted to ${adjustResolutionForPipeline(pipelineId, resolution).resolution.width}×${adjustResolutionForPipeline(pipelineId, resolution).resolution.height} when starting the stream (must be divisible by ${scaleFactor})` - : null; - const handlePipelineIdChange = (value: string) => { if (pipelines && value in pipelines) { onPipelineIdChange?.(value as PipelineId); } }; - const handleResolutionChange = ( - dimension: "height" | "width", - value: number - ) => { - // Get min dimension from pipeline schema, fallback to default - const currentPipeline = pipelines?.[pipelineId]; - const minValue = currentPipeline?.minDimension ?? DEFAULT_MIN_DIMENSION; - const maxValue = 2048; - - // Validate and set error state - if (value < minValue) { - if (dimension === "height") { - setHeightError(`Must be at least ${minValue}`); - } else { - setWidthError(`Must be at least ${minValue}`); - } - } else if (value > maxValue) { - if (dimension === "height") { - setHeightError(`Must be at most ${maxValue}`); - } else { - setWidthError(`Must be at most ${maxValue}`); - } - } else { - // Clear error if valid - if (dimension === "height") { - setHeightError(null); - } else { - setWidthError(null); - } - } - - // Always update the value (even if invalid) - onResolutionChange?.({ - ...resolution, - [dimension]: value, - }); - }; - - const incrementResolution = (dimension: "height" | "width") => { - const maxValue = 2048; - const newValue = Math.min(maxValue, resolution[dimension] + 1); - handleResolutionChange(dimension, newValue); - }; - - const decrementResolution = (dimension: "height" | "width") => { - // Get min dimension from pipeline schema, fallback to default - const currentPipeline = pipelines?.[pipelineId]; - const minValue = currentPipeline?.minDimension ?? DEFAULT_MIN_DIMENSION; - const newValue = Math.max(minValue, resolution[dimension] - 1); - handleResolutionChange(dimension, newValue); - }; - - const handleSeedChange = (value: number) => { - const minValue = 0; - const maxValue = 2147483647; - - // Validate and set error state - if (value < minValue) { - setSeedError(`Must be at least ${minValue}`); - } else if (value > maxValue) { - setSeedError(`Must be at most ${maxValue}`); - } else { - setSeedError(null); - } - - // Always update the value (even if invalid) - onSeedChange?.(value); - }; - - const incrementSeed = () => { - const maxValue = 2147483647; - const newValue = Math.min(maxValue, seed + 1); - handleSeedChange(newValue); - }; - - const decrementSeed = () => { - const minValue = 0; - const newValue = Math.max(minValue, seed - 1); - handleSeedChange(newValue); - }; - const currentPipeline = pipelines?.[pipelineId]; + // Get settings panel configuration from schema + // Use mode-specific settings_panel if available, otherwise use base settings_panel + const settingsPanelConfig = + schema?.mode_defaults?.[inputMode || "text"]?.settings_panel || + schema?.settings_panel || + []; + + // If no schema is provided, show a message (fallback for backwards compatibility) + if (!schema) { + return ( + + + Settings + + +
+

Pipeline ID

+ +
+

+ Schema information not available. Please wait for schemas to load. +

+
+
+ ); + } + return ( @@ -359,551 +297,55 @@ export function SettingsPanel({ )} - {/* VACE Toggle */} - {currentPipeline?.supportsVACE && ( -
-
- - {})} - variant="outline" - size="sm" - className="h-7" - disabled={isStreaming || isLoading} - > - {vaceEnabled ? "ON" : "OFF"} - -
- - {/* Warning when VACE is enabled and quantization is set */} - {vaceEnabled && quantization !== null && ( -
- -

- VACE is incompatible with FP8 quantization. Please disable - quantization to use VACE. -

-
- )} - - {vaceEnabled && ( -
-
- - {})} - variant="outline" - size="sm" - className="h-7" - disabled={isStreaming || isLoading || inputMode !== "video"} - > - {vaceUseInputVideo ? "ON" : "OFF"} - -
-
- -
- parseFloat(v) || 1.0} - /> -
-
-
- )} -
- )} - - {currentPipeline?.supportsLoRA && ( -
- -
- )} - - {/* Preprocessor Selector - shown for pipelines that support VACE */} - {currentPipeline?.supportsVACE && ( -
-
- - -
-
- )} - - {/* VAE Type Selection */} - {vaeTypes && vaeTypes.length > 0 && ( -
-
- - -
-
- )} - - {/* Resolution controls - shown for pipelines that support quantization (implies they need resolution config) */} - {pipelines?.[pipelineId]?.supportsQuantization && ( -
-
-
-
-
- -
- - { - const value = parseInt(e.target.value); - if (!isNaN(value)) { - handleResolutionChange("height", value); - } - }} - disabled={isStreaming} - className="text-center border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-8 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - min={ - pipelines?.[pipelineId]?.minDimension ?? - DEFAULT_MIN_DIMENSION - } - max={2048} - /> - -
-
- {heightError && ( -

{heightError}

- )} -
- -
-
- -
- - { - const value = parseInt(e.target.value); - if (!isNaN(value)) { - handleResolutionChange("width", value); - } - }} - disabled={isStreaming} - className="text-center border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-8 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - min={ - pipelines?.[pipelineId]?.minDimension ?? - DEFAULT_MIN_DIMENSION - } - max={2048} - /> - -
-
- {widthError && ( -

{widthError}

- )} -
- {resolutionWarning && ( -
- -

- {resolutionWarning} -

-
- )} -
- -
-
- -
- - { - const value = parseInt(e.target.value); - if (!isNaN(value)) { - handleSeedChange(value); - } - }} - disabled={isStreaming} - className="text-center border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-8 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" - min={0} - max={2147483647} - /> - -
-
- {seedError && ( -

{seedError}

- )} -
-
-
- )} - - {/* Cache management controls - shown for pipelines that support it */} - {pipelines?.[pipelineId]?.supportsCacheManagement && ( -
-
-
- {/* KV Cache bias control - shown for pipelines that support it */} - {pipelines?.[pipelineId]?.supportsKvCacheBias && ( - parseFloat(v) || 1.0} - /> - )} - -
- - {})} - variant="outline" - size="sm" - className="h-7" - > - {manageCache ? "ON" : "OFF"} - -
- -
- - -
-
-
-
- )} - - {/* Denoising steps - shown for pipelines that support quantization (implies advanced diffusion features) */} - {pipelines?.[pipelineId]?.supportsQuantization && ( - {})} - defaultValues={defaultDenoisingSteps} - tooltip={PARAMETER_METADATA.denoisingSteps.tooltip} + {/* Render settings controls using SettingsPanelRenderer */} + {settingsPanelConfig.length > 0 && ( + )} - - {/* Noise controls - show for video mode on supported pipelines (schema-derived) */} - {inputMode === "video" && supportsNoiseControls && ( -
-
-
-
- - {})} - disabled={isStreaming} - variant="outline" - size="sm" - className="h-7" - > - {noiseController ? "ON" : "OFF"} - -
-
- - parseFloat(v) || 0.0} - /> -
-
- )} - - {/* Quantization controls - shown for pipelines that support it */} - {pipelines?.[pipelineId]?.supportsQuantization && ( -
-
-
-
- - -
- {/* Note when quantization is disabled due to VACE */} - {vaceEnabled && ( -

- Disabled because VACE is enabled. Disable VACE to use FP8 - quantization. -

- )} -
-
-
- )} - - {/* Spout Sender Settings (available on native Windows only) */} - {spoutAvailable && ( -
-
- - { - onSpoutSenderChange?.({ - enabled, - name: spoutSender?.name ?? "ScopeOut", - }); - }} - variant="outline" - size="sm" - className="h-7" - > - {spoutSender?.enabled ? "ON" : "OFF"} - -
- - {spoutSender?.enabled && ( -
- - { - onSpoutSenderChange?.({ - enabled: spoutSender?.enabled ?? false, - name: e.target.value, - }); - }} - disabled={isStreaming} - className="h-8 text-sm flex-1" - placeholder="ScopeOut" - /> -
- )} -
- )} ); diff --git a/frontend/src/components/SettingsPanelRenderer.tsx b/frontend/src/components/SettingsPanelRenderer.tsx new file mode 100644 index 00000000..1a1ef723 --- /dev/null +++ b/frontend/src/components/SettingsPanelRenderer.tsx @@ -0,0 +1,1064 @@ +/** + * SettingsPanelRenderer - Renders settings controls based on schema configuration. + * + * This component reads the `settings_panel` configuration from the pipeline schema + * and renders controls in the specified order. It supports: + * + * 1. Special controls (SettingsControlType) - Complex UI that requires custom handling + * 2. Dynamic controls (field names) - Simple controls inferred from JSON schema + * + * Usage: + * ```tsx + * + * ``` + */ + +import type { + SettingsPanelItem, + SettingsControlType, + PipelineSchemaInfo, + PipelineSchemaProperty, +} from "../lib/api"; +import type { + LoRAConfig, + LoraMergeStrategy, + SettingsState, + InputMode, + PipelineInfo, + VaeType, +} from "../types"; +import { DynamicControl } from "./dynamic-controls"; +import { + resolveSchemaProperty, + inferControlType, + getParameterDisplayInfo, +} from "../lib/schemaInference"; + +// Import special control components +import { LabelWithTooltip } from "./ui/label-with-tooltip"; +import { Toggle } from "./ui/toggle"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; +import { SliderWithInput } from "./ui/slider-with-input"; +import { LoRAManager } from "./LoRAManager"; +import { DenoisingStepsSlider } from "./DenoisingStepsSlider"; +import { PARAMETER_METADATA } from "../data/parameterMetadata"; +import { useLocalSliderValue } from "../hooks/useLocalSliderValue"; +import { Info, Minus, Plus, RotateCcw } from "lucide-react"; +import { + getResolutionScaleFactor, + adjustResolutionForPipeline, +} from "../lib/utils"; +import { useState } from "react"; + +// Minimum dimension for most pipelines +const DEFAULT_MIN_DIMENSION = 1; + +// Check if a string is a special control type +const SPECIAL_CONTROL_TYPES: SettingsControlType[] = [ + "vace", + "lora", + "preprocessor", + "cache_management", + "denoising_steps", + "noise_controls", + "spout_sender", +]; + +function isSpecialControl( + item: SettingsPanelItem +): item is SettingsControlType { + return SPECIAL_CONTROL_TYPES.includes(item as SettingsControlType); +} + +export interface SettingsPanelRendererProps { + // The settings panel configuration (list of items to render in order) + settingsPanel: SettingsPanelItem[]; + // Pipeline schema info + schema: PipelineSchemaInfo; + // All pipelines for preprocessor dropdown + pipelines: Record | null; + pipelineId: string; + + // State flags + isStreaming: boolean; + isLoading: boolean; + inputMode?: InputMode; + + // VACE controls + vaceEnabled: boolean; + onVaceEnabledChange?: (enabled: boolean) => void; + vaceUseInputVideo: boolean; + onVaceUseInputVideoChange?: (enabled: boolean) => void; + vaceContextScale: number; + onVaceContextScaleChange?: (scale: number) => void; + + // LoRA controls + loras: LoRAConfig[]; + onLorasChange: (loras: LoRAConfig[]) => void; + loraMergeStrategy: LoraMergeStrategy; + + // Preprocessor controls + preprocessorIds: string[]; + onPreprocessorIdsChange?: (ids: string[]) => void; + + // Cache management controls + manageCache: boolean; + onManageCacheChange?: (enabled: boolean) => void; + onResetCache?: () => void; + kvCacheAttentionBias: number; + onKvCacheAttentionBiasChange?: (bias: number) => void; + + // Denoising steps controls + denoisingSteps: number[]; + onDenoisingStepsChange?: (steps: number[]) => void; + defaultDenoisingSteps: number[]; + + // Noise controls + noiseController: boolean; + onNoiseControllerChange?: (enabled: boolean) => void; + noiseScale: number; + onNoiseScaleChange?: (scale: number) => void; + + // Spout controls + spoutAvailable: boolean; + spoutSender?: SettingsState["spoutSender"]; + onSpoutSenderChange?: (spoutSender: SettingsState["spoutSender"]) => void; + + // Resolution controls + resolution: { height: number; width: number }; + onResolutionChange?: (resolution: { height: number; width: number }) => void; + + // Seed control + seed: number; + onSeedChange?: (seed: number) => void; + + // VAE type controls + vaeType: VaeType; + onVaeTypeChange?: (vaeType: VaeType) => void; + vaeTypes?: string[]; + + // Quantization controls + quantization: "fp8_e4m3fn" | null; + onQuantizationChange?: (quantization: "fp8_e4m3fn" | null) => void; + + // Generic config values for dynamic fields (e.g., new_param, new_field, etc.) + // Maps field names to their current values and onChange handlers + configValues?: Record< + string, + { + value: unknown; + onChange?: (value: unknown) => void; + } + >; + + // Generic handler for any config field that doesn't have an explicit handler + // Called with (fieldName, value) when a dynamic field changes + onConfigValueChange?: (fieldName: string, value: unknown) => void; +} + +export function SettingsPanelRenderer({ + settingsPanel, + schema, + pipelines, + pipelineId, + isStreaming, + isLoading, + inputMode, + vaceEnabled, + onVaceEnabledChange, + vaceUseInputVideo, + onVaceUseInputVideoChange, + vaceContextScale, + onVaceContextScaleChange, + loras, + onLorasChange, + loraMergeStrategy, + preprocessorIds, + onPreprocessorIdsChange, + manageCache, + onManageCacheChange, + onResetCache, + kvCacheAttentionBias, + onKvCacheAttentionBiasChange, + denoisingSteps, + onDenoisingStepsChange, + defaultDenoisingSteps, + noiseController, + onNoiseControllerChange, + noiseScale, + onNoiseScaleChange, + spoutAvailable, + spoutSender, + onSpoutSenderChange, + resolution, + onResolutionChange, + seed, + onSeedChange, + vaeType, + onVaeTypeChange, + vaeTypes, + quantization, + onQuantizationChange, + configValues, + onConfigValueChange, +}: SettingsPanelRendererProps) { + // Local slider state management hooks + const noiseScaleSlider = useLocalSliderValue(noiseScale, onNoiseScaleChange); + const kvCacheAttentionBiasSlider = useLocalSliderValue( + kvCacheAttentionBias, + onKvCacheAttentionBiasChange + ); + const vaceContextScaleSlider = useLocalSliderValue( + vaceContextScale, + onVaceContextScaleChange + ); + + // Validation error states + const [heightError, setHeightError] = useState(null); + const [widthError, setWidthError] = useState(null); + const [seedError, setSeedError] = useState(null); + + // Check if resolution needs adjustment + const scaleFactor = getResolutionScaleFactor(pipelineId); + const resolutionWarning = + scaleFactor && + (resolution.height % scaleFactor !== 0 || + resolution.width % scaleFactor !== 0) + ? `Resolution will be adjusted to ${adjustResolutionForPipeline(pipelineId, resolution).resolution.width}×${adjustResolutionForPipeline(pipelineId, resolution).resolution.height} when starting the stream (must be divisible by ${scaleFactor})` + : null; + + const handleResolutionChange = ( + dimension: "height" | "width", + value: number + ) => { + const currentPipeline = pipelines?.[pipelineId]; + const minValue = currentPipeline?.minDimension ?? DEFAULT_MIN_DIMENSION; + const maxValue = 2048; + + if (value < minValue) { + if (dimension === "height") { + setHeightError(`Must be at least ${minValue}`); + } else { + setWidthError(`Must be at least ${minValue}`); + } + } else if (value > maxValue) { + if (dimension === "height") { + setHeightError(`Must be at most ${maxValue}`); + } else { + setWidthError(`Must be at most ${maxValue}`); + } + } else { + if (dimension === "height") { + setHeightError(null); + } else { + setWidthError(null); + } + } + + onResolutionChange?.({ + ...resolution, + [dimension]: value, + }); + }; + + const incrementResolution = (dimension: "height" | "width") => { + const maxValue = 2048; + const newValue = Math.min(maxValue, resolution[dimension] + 1); + handleResolutionChange(dimension, newValue); + }; + + const decrementResolution = (dimension: "height" | "width") => { + const currentPipeline = pipelines?.[pipelineId]; + const minValue = currentPipeline?.minDimension ?? DEFAULT_MIN_DIMENSION; + const newValue = Math.max(minValue, resolution[dimension] - 1); + handleResolutionChange(dimension, newValue); + }; + + const handleSeedChange = (value: number) => { + const minValue = 0; + const maxValue = 2147483647; + + if (value < minValue) { + setSeedError(`Must be at least ${minValue}`); + } else if (value > maxValue) { + setSeedError(`Must be at most ${maxValue}`); + } else { + setSeedError(null); + } + + onSeedChange?.(value); + }; + + const incrementSeed = () => { + const maxValue = 2147483647; + const newValue = Math.min(maxValue, seed + 1); + handleSeedChange(newValue); + }; + + const decrementSeed = () => { + const minValue = 0; + const newValue = Math.max(minValue, seed - 1); + handleSeedChange(newValue); + }; + + // Render a single item from the settings panel + const renderItem = (item: SettingsPanelItem, index: number) => { + if (isSpecialControl(item)) { + return renderSpecialControl(item, index); + } else { + return renderDynamicControl(item, index); + } + }; + + // Render a special control based on type + const renderSpecialControl = ( + controlType: SettingsControlType, + index: number + ) => { + switch (controlType) { + case "vace": + if (!schema.supports_vace) return null; + return ( +
+
+ + {})} + variant="outline" + size="sm" + className="h-7" + disabled={isStreaming || isLoading} + > + {vaceEnabled ? "ON" : "OFF"} + +
+ + {vaceEnabled && quantization !== null && ( +
+ +

+ VACE is incompatible with FP8 quantization. Please disable + quantization to use VACE. +

+
+ )} + + {vaceEnabled && ( +
+
+ + {})} + variant="outline" + size="sm" + className="h-7" + disabled={isStreaming || isLoading || inputMode !== "video"} + > + {vaceUseInputVideo ? "ON" : "OFF"} + +
+
+ +
+ parseFloat(v) || 1.0} + /> +
+
+
+ )} +
+ ); + + case "lora": + if (!schema.supports_lora) return null; + return ( +
+ +
+ ); + + case "preprocessor": + if (!schema.supports_vace) return null; + return ( +
+
+ + +
+
+ ); + + case "cache_management": + return ( +
+
+
+ + {})} + variant="outline" + size="sm" + className="h-7" + > + {manageCache ? "ON" : "OFF"} + +
+ +
+ + +
+
+
+ ); + + case "denoising_steps": + return ( + {})} + defaultValues={defaultDenoisingSteps} + tooltip={PARAMETER_METADATA.denoisingSteps.tooltip} + /> + ); + + case "noise_controls": + return ( +
+
+
+ + {})} + disabled={isStreaming} + variant="outline" + size="sm" + className="h-7" + > + {noiseController ? "ON" : "OFF"} + +
+
+ + parseFloat(v) || 0.0} + /> +
+ ); + + case "spout_sender": + if (!spoutAvailable) return null; + return ( +
+
+ + { + onSpoutSenderChange?.({ + enabled, + name: spoutSender?.name ?? "ScopeOut", + }); + }} + variant="outline" + size="sm" + className="h-7" + > + {spoutSender?.enabled ? "ON" : "OFF"} + +
+ + {spoutSender?.enabled && ( +
+ + { + onSpoutSenderChange?.({ + enabled: spoutSender?.enabled ?? false, + name: e.target.value, + }); + }} + disabled={isStreaming} + className="h-8 text-sm flex-1" + placeholder="ScopeOut" + /> +
+ )} +
+ ); + + default: + return null; + } + }; + + // Render a dynamic control based on field name + const renderDynamicControl = (fieldName: string, index: number) => { + const configSchema = schema.config_schema; + if (!configSchema?.properties) { + console.warn( + `[SettingsPanelRenderer] No config schema properties for field "${fieldName}"` + ); + return null; + } + + // Handle special field names that should use custom handlers (even if in schema) + if (fieldName === "quantization") { + return renderQuantizationControl(index); + } + if (fieldName === "height") { + return renderHeightControl(index); + } + if (fieldName === "width") { + return renderWidthControl(index); + } + if (fieldName === "base_seed") { + return renderSeedControl(index); + } + if (fieldName === "kv_cache_attention_bias") { + return renderKvCacheBiasControl(index); + } + + const property = configSchema.properties[fieldName]; + if (!property) { + console.warn( + `[SettingsPanelRenderer] Field "${fieldName}" not found in schema properties. Available fields: ${Object.keys(configSchema.properties).join(", ")}` + ); + return null; + } + + // Handle vae_type specially (uses the vaeTypes array from schema) + if (fieldName === "vae_type") { + return renderVaeTypeControl(index); + } + + // For other fields, use dynamic control rendering + const resolved = resolveSchemaProperty(property, configSchema.$defs); + const controlType = inferControlType(resolved); + const displayInfo = getParameterDisplayInfo(fieldName, property); + + if (controlType === "unknown") { + console.warn( + `[SettingsPanelRenderer] Unknown control type for field "${fieldName}"` + ); + return null; + } + + // Get current value and onChange handler based on field name + const { value, onChange } = getFieldValueAndHandler(fieldName, property); + if (onChange === undefined) { + console.warn( + `[SettingsPanelRenderer] No handler for field "${fieldName}". onConfigValueChange provided: ${!!onConfigValueChange}, configValues provided: ${!!configValues}` + ); + return null; + } + + // Log for debugging dynamic fields + if (fieldName === "new_param") { + console.log(`[SettingsPanelRenderer] Rendering new_param:`, { + value, + hasOnChange: !!onChange, + propertyDefault: property.default, + }); + } + + return ( + onChange(val)} + disabled={isStreaming || isLoading} + /> + ); + }; + + // Get value and onChange handler for a field + const getFieldValueAndHandler = ( + fieldName: string, + property?: PipelineSchemaProperty + ): { value: unknown; onChange: ((value: unknown) => void) | undefined } => { + // Map field names to their corresponding props + // This is a simplified version - in practice you'd want more comprehensive mapping + switch (fieldName) { + case "height": + return { + value: resolution.height, + onChange: val => + onResolutionChange?.({ ...resolution, height: val as number }), + }; + case "width": + return { + value: resolution.width, + onChange: val => + onResolutionChange?.({ ...resolution, width: val as number }), + }; + case "base_seed": + return { value: seed, onChange: val => onSeedChange?.(val as number) }; + case "vae_type": + return { + value: vaeType, + onChange: val => onVaeTypeChange?.(val as VaeType), + }; + case "quantization": + return { + value: quantization, + onChange: val => onQuantizationChange?.(val as "fp8_e4m3fn" | null), + }; + default: + // Check if this field has a value in configValues (for dynamic fields) + if (configValues && fieldName in configValues) { + const configValue = configValues[fieldName]; + // If configValue has an onChange handler, use it; otherwise use generic handler + if (configValue.onChange) { + return configValue; + } + // If only value is provided, use generic handler + if (onConfigValueChange) { + return { + value: configValue.value, + onChange: (val: unknown) => onConfigValueChange(fieldName, val), + }; + } + return configValue; + } + // For dynamic fields not in configValues, use schema default and generic handler + if (onConfigValueChange) { + // Get value from configValues if provided, otherwise use schema default + const configValue = configValues?.[fieldName]?.value; + const defaultValue = property?.default; + return { + value: configValue !== undefined ? configValue : defaultValue, + onChange: (val: unknown) => onConfigValueChange(fieldName, val), + }; + } + console.warn( + `[SettingsPanelRenderer] No handler for dynamic field "${fieldName}" and onConfigValueChange not provided` + ); + return { value: undefined, onChange: undefined }; + } + }; + + // Helper to get display info from schema + const getDisplayInfo = (fieldName: string) => { + const configSchema = schema.config_schema; + if (!configSchema?.properties) { + return { label: fieldName, tooltip: undefined }; + } + const property = configSchema.properties[fieldName]; + if (!property) { + return { label: fieldName, tooltip: undefined }; + } + return getParameterDisplayInfo(fieldName, property); + }; + + // Render height control + const renderHeightControl = (index: number) => { + const displayInfo = getDisplayInfo("height"); + return ( +
+
+ +
+ + { + const value = parseInt(e.target.value); + if (!isNaN(value)) { + handleResolutionChange("height", value); + } + }} + disabled={isStreaming} + className="text-center border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-8 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + min={ + pipelines?.[pipelineId]?.minDimension ?? DEFAULT_MIN_DIMENSION + } + max={2048} + /> + +
+
+ {heightError && ( +

{heightError}

+ )} +
+ ); + }; + + // Render width control + const renderWidthControl = (index: number) => { + const displayInfo = getDisplayInfo("width"); + return ( +
+
+ +
+ + { + const value = parseInt(e.target.value); + if (!isNaN(value)) { + handleResolutionChange("width", value); + } + }} + disabled={isStreaming} + className="text-center border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-8 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + min={ + pipelines?.[pipelineId]?.minDimension ?? DEFAULT_MIN_DIMENSION + } + max={2048} + /> + +
+
+ {widthError && ( +

{widthError}

+ )} + {resolutionWarning && ( +
+ +

+ {resolutionWarning} +

+
+ )} +
+ ); + }; + + // Render seed control + const renderSeedControl = (index: number) => { + const displayInfo = getDisplayInfo("base_seed"); + return ( +
+
+ +
+ + { + const value = parseInt(e.target.value); + if (!isNaN(value)) { + handleSeedChange(value); + } + }} + disabled={isStreaming} + className="text-center border-0 focus-visible:ring-0 focus-visible:ring-offset-0 h-8 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + min={0} + max={2147483647} + /> + +
+
+ {seedError &&

{seedError}

} +
+ ); + }; + + // Render VAE type control + const renderVaeTypeControl = (index: number) => { + if (!vaeTypes || vaeTypes.length === 0) return null; + const displayInfo = getDisplayInfo("vae_type"); + return ( +
+
+ + +
+
+ ); + }; + + // Render quantization control + const renderQuantizationControl = (index: number) => { + const displayInfo = getDisplayInfo("quantization"); + return ( +
+
+
+ + +
+ {vaceEnabled && ( +

+ Disabled because VACE is enabled. Disable VACE to use FP8 + quantization. +

+ )} +
+
+ ); + }; + + // Render KV cache bias control + const renderKvCacheBiasControl = (index: number) => { + const displayInfo = getDisplayInfo("kv_cache_attention_bias"); + return ( + parseFloat(v) || 1.0} + /> + ); + }; + + return ( +
+ {settingsPanel.map((item, index) => renderItem(item, index))} +
+ ); +} diff --git a/frontend/src/components/dynamic-controls/DynamicControl.tsx b/frontend/src/components/dynamic-controls/DynamicControl.tsx new file mode 100644 index 00000000..6219c197 --- /dev/null +++ b/frontend/src/components/dynamic-controls/DynamicControl.tsx @@ -0,0 +1,132 @@ +/** + * DynamicControl - The main component for schema-driven UI rendering. + * + * This component inspects the JSON schema property and renders the appropriate + * control type. It acts as a factory that maps schema types to UI components. + * + * The rendering logic follows these rules: + * - `$ref` to enum definition → SelectControl + * - `type: "boolean"` → ToggleControl + * - `type: "number"` or `"integer"` with bounds → SliderControl + * - `type: "number"` or `"integer"` without full bounds → NumberControl + * - `type: "string"` → TextControl + */ + +import type { RenderableParameter } from "../../lib/schemaInference"; +import { SliderControl } from "./SliderControl"; +import { NumberControl } from "./NumberControl"; +import { ToggleControl } from "./ToggleControl"; +import { SelectControl } from "./SelectControl"; +import { TextControl } from "./TextControl"; + +export interface DynamicControlRendererProps { + /** The parameter metadata including name, schema, and inferred control type */ + parameter: RenderableParameter; + /** Current value for this parameter */ + value: unknown; + /** Callback when value changes */ + onChange: (paramName: string, value: unknown) => void; + /** Whether the control is disabled */ + disabled?: boolean; +} + +/** + * Renders the appropriate control based on the inferred control type. + */ +export function DynamicControl({ + parameter, + value, + onChange, + disabled = false, +}: DynamicControlRendererProps) { + const { name, property, controlType, label, tooltip } = parameter; + + const handleChange = (newValue: unknown) => { + onChange(name, newValue); + }; + + switch (controlType) { + case "slider": + return ( + void} + label={label} + tooltip={tooltip} + disabled={disabled} + /> + ); + + case "number": + return ( + void} + label={label} + tooltip={tooltip} + disabled={disabled} + /> + ); + + case "toggle": + return ( + void} + label={label} + tooltip={tooltip} + disabled={disabled} + /> + ); + + case "select": { + // Get options from resolved enum or direct enum + const options = + property.resolvedEnum ?? (property.enum as string[]) ?? []; + return ( + void} + options={options} + label={label} + tooltip={tooltip} + disabled={disabled} + /> + ); + } + + case "text": + return ( + void} + label={label} + tooltip={tooltip} + disabled={disabled} + /> + ); + + case "unknown": + default: + // Fallback: show the value as read-only text + console.warn( + `[DynamicControl] Unknown control type for parameter "${name}"` + ); + return ( +
+ {label} + {JSON.stringify(value)} +
+ ); + } +} diff --git a/frontend/src/components/dynamic-controls/NumberControl.tsx b/frontend/src/components/dynamic-controls/NumberControl.tsx new file mode 100644 index 00000000..c6b9b43f --- /dev/null +++ b/frontend/src/components/dynamic-controls/NumberControl.tsx @@ -0,0 +1,113 @@ +/** + * Dynamic number input control for unbounded or partially bounded numeric parameters. + * Displays increment/decrement buttons with optional min/max constraints. + */ + +import { useState } from "react"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { LabelWithTooltip } from "../ui/label-with-tooltip"; +import { Plus, Minus } from "lucide-react"; +import type { NumberControlProps } from "./types"; + +export function NumberControl({ + schema, + value, + onChange, + label, + tooltip, + disabled = false, +}: NumberControlProps) { + const [error, setError] = useState(null); + + const min = schema.minimum; + const max = schema.maximum; + const isInteger = schema.type === "integer"; + const step = isInteger ? 1 : 0.1; + + const validate = (newValue: number): string | null => { + if (min !== undefined && newValue < min) { + return `Must be at least ${min}`; + } + if (max !== undefined && newValue > max) { + return `Must be at most ${max}`; + } + return null; + }; + + const handleChange = (newValue: number) => { + const validationError = validate(newValue); + setError(validationError); + onChange(newValue); + }; + + const handleIncrement = () => { + let newValue = value + step; + if (max !== undefined) { + newValue = Math.min(max, newValue); + } + handleChange(newValue); + }; + + const handleDecrement = () => { + let newValue = value - step; + if (min !== undefined) { + newValue = Math.max(min, newValue); + } + handleChange(newValue); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const parsed = isInteger + ? parseInt(e.target.value, 10) + : parseFloat(e.target.value); + if (!isNaN(parsed)) { + handleChange(parsed); + } + }; + + return ( +
+
+ +
+ + + +
+
+ {error &&

{error}

} +
+ ); +} diff --git a/frontend/src/components/dynamic-controls/SelectControl.tsx b/frontend/src/components/dynamic-controls/SelectControl.tsx new file mode 100644 index 00000000..66778464 --- /dev/null +++ b/frontend/src/components/dynamic-controls/SelectControl.tsx @@ -0,0 +1,44 @@ +/** + * Dynamic select control for enum parameters. + */ + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { LabelWithTooltip } from "../ui/label-with-tooltip"; +import type { SelectControlProps } from "./types"; + +export function SelectControl({ + value, + onChange, + options, + label, + tooltip, + disabled = false, +}: SelectControlProps) { + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/dynamic-controls/SliderControl.tsx b/frontend/src/components/dynamic-controls/SliderControl.tsx new file mode 100644 index 00000000..e17e84be --- /dev/null +++ b/frontend/src/components/dynamic-controls/SliderControl.tsx @@ -0,0 +1,74 @@ +/** + * Dynamic slider control for bounded numeric parameters. + * Infers min/max/step from JSON schema constraints. + */ + +import { SliderWithInput } from "../ui/slider-with-input"; +import { useLocalSliderValue } from "../../hooks/useLocalSliderValue"; +import type { SliderControlProps } from "./types"; + +/** + * Determines an appropriate step value based on the range and type. + */ +function inferStep(min: number, max: number, isInteger: boolean): number { + if (isInteger) { + return 1; + } + + const range = max - min; + if (range <= 1) return 0.01; + if (range <= 10) return 0.1; + if (range <= 100) return 1; + return Math.floor(range / 100); +} + +export function SliderControl({ + schema, + value, + onChange, + label, + tooltip, + disabled = false, +}: SliderControlProps) { + const min = schema.minimum ?? 0; + const max = schema.maximum ?? 100; + const isInteger = schema.type === "integer"; + const step = inferStep(min, max, isInteger); + + // Use local slider value hook for smooth dragging + const slider = useLocalSliderValue(value, onChange); + + // Value formatter for display + const valueFormatter = (v: number) => { + if (isInteger) { + return Math.round(v); + } + // Round to step precision + const precision = step < 1 ? Math.ceil(-Math.log10(step)) : 0; + return Number(v.toFixed(precision)); + }; + + // Input parser + const inputParser = (v: string) => { + const parsed = isInteger ? parseInt(v, 10) : parseFloat(v); + return isNaN(parsed) ? min : parsed; + }; + + return ( + + ); +} diff --git a/frontend/src/components/dynamic-controls/TextControl.tsx b/frontend/src/components/dynamic-controls/TextControl.tsx new file mode 100644 index 00000000..9a8d5918 --- /dev/null +++ b/frontend/src/components/dynamic-controls/TextControl.tsx @@ -0,0 +1,32 @@ +/** + * Dynamic text input control for string parameters. + */ + +import { Input } from "../ui/input"; +import { LabelWithTooltip } from "../ui/label-with-tooltip"; +import type { TextControlProps } from "./types"; + +export function TextControl({ + value, + onChange, + label, + tooltip, + disabled = false, +}: TextControlProps) { + return ( +
+ + onChange(e.target.value)} + disabled={disabled} + className="h-8 text-sm flex-1" + /> +
+ ); +} diff --git a/frontend/src/components/dynamic-controls/ToggleControl.tsx b/frontend/src/components/dynamic-controls/ToggleControl.tsx new file mode 100644 index 00000000..80ce9dc3 --- /dev/null +++ b/frontend/src/components/dynamic-controls/ToggleControl.tsx @@ -0,0 +1,35 @@ +/** + * Dynamic toggle control for boolean parameters. + */ + +import { Toggle } from "../ui/toggle"; +import { LabelWithTooltip } from "../ui/label-with-tooltip"; +import type { ToggleControlProps } from "./types"; + +export function ToggleControl({ + value, + onChange, + label, + tooltip, + disabled = false, +}: ToggleControlProps) { + return ( +
+ + + {value ? "ON" : "OFF"} + +
+ ); +} diff --git a/frontend/src/components/dynamic-controls/index.ts b/frontend/src/components/dynamic-controls/index.ts new file mode 100644 index 00000000..81c6b583 --- /dev/null +++ b/frontend/src/components/dynamic-controls/index.ts @@ -0,0 +1,19 @@ +/** + * Dynamic control components for schema-driven UI rendering. + */ + +export { SliderControl } from "./SliderControl"; +export { NumberControl } from "./NumberControl"; +export { ToggleControl } from "./ToggleControl"; +export { SelectControl } from "./SelectControl"; +export { TextControl } from "./TextControl"; +export { DynamicControl } from "./DynamicControl"; + +export type { + DynamicControlProps, + SliderControlProps, + NumberControlProps, + ToggleControlProps, + SelectControlProps, + TextControlProps, +} from "./types"; diff --git a/frontend/src/components/dynamic-controls/types.ts b/frontend/src/components/dynamic-controls/types.ts new file mode 100644 index 00000000..0794c2f4 --- /dev/null +++ b/frontend/src/components/dynamic-controls/types.ts @@ -0,0 +1,53 @@ +/** + * Shared types for dynamic control components. + */ + +import type { ResolvedSchemaProperty } from "../../lib/schemaInference"; + +/** + * Base props for all dynamic control components. + * Uses a generic type parameter T for type-safe value handling. + */ +export interface DynamicControlProps { + /** Parameter name (used as key for updates) */ + paramName: string; + /** Resolved JSON schema property with constraints */ + schema: ResolvedSchemaProperty; + /** Current value */ + value: T; + /** Callback when value changes */ + onChange: (value: T) => void; + /** Display label */ + label: string; + /** Optional tooltip description */ + tooltip?: string; + /** Whether the control is disabled */ + disabled?: boolean; +} + +/** + * Props for slider control (bounded numeric values). + */ +export type SliderControlProps = DynamicControlProps; + +/** + * Props for number input control (unbounded or partially bounded numeric values). + */ +export type NumberControlProps = DynamicControlProps; + +/** + * Props for toggle control (boolean values). + */ +export type ToggleControlProps = DynamicControlProps; + +/** + * Props for select control (enum values). + */ +export interface SelectControlProps extends DynamicControlProps { + options: string[]; +} + +/** + * Props for text input control (string values). + */ +export type TextControlProps = DynamicControlProps; diff --git a/frontend/src/data/parameterMetadata.ts b/frontend/src/data/parameterMetadata.ts index 8dd9519f..28234329 100644 --- a/frontend/src/data/parameterMetadata.ts +++ b/frontend/src/data/parameterMetadata.ts @@ -12,21 +12,6 @@ export interface ParameterMetadata { } export const PARAMETER_METADATA: Record = { - height: { - label: "Height:", - tooltip: - "Output video height in pixels. Higher values produce more detailed vertical resolution but reduces speed.", - }, - width: { - label: "Width:", - tooltip: - "Output video width in pixels. Higher values produce more detailed horizontal resolution but reduces speed.", - }, - seed: { - label: "Seed:", - tooltip: - "Random seed for reproducible generation. Using the same seed with the same settings will produce similar results.", - }, manageCache: { label: "Manage Cache:", tooltip: @@ -52,16 +37,6 @@ export const PARAMETER_METADATA: Record = { tooltip: "Controls the amount of noise added during generation. Higher values add more variation and creativity and lower values produce more stable results.", }, - quantization: { - label: "Quantization:", - tooltip: - "Quantization method for the diffusion model. fp8_e4m3fn (Dynamic) reduces memory usage, but might affect performance and quality. None uses full precision and uses more memory, but does not affect performance and quality.", - }, - kvCacheAttentionBias: { - label: "Cache Bias:", - tooltip: - "Controls how much to rely on past frames in the cache during generation. A lower value can help mitigate error accumulation and prevent repetitive motion. Uses log scale: 1.0 = full reliance on past frames, smaller values = less reliance on past frames. Typical values: 0.3-0.7 for moderate effect, 0.1-0.2 for strong effect.", - }, loraMergeStrategy: { label: "LoRA Strategy:", tooltip: @@ -82,11 +57,6 @@ export const PARAMETER_METADATA: Record = { tooltip: "The configuration of the sender that will send video to Spout-compatible apps like TouchDesigner, Resolume, OBS.", }, - vaeType: { - label: "VAE:", - tooltip: - "VAE type to use for encoding/decoding. 'wan' is the full VAE with best quality. 'lightvae' is 75% pruned for faster performance but lower quality. 'tae' is a tiny autoencoder for fast preview quality. 'lighttae' is LightTAE with WanVAE normalization for faster performance with consistent latent space.", - }, preprocessor: { label: "Preprocessor:", tooltip: "Select a preprocessor to apply before the main pipeline.", diff --git a/frontend/src/hooks/useDynamicParameters.ts b/frontend/src/hooks/useDynamicParameters.ts new file mode 100644 index 00000000..f98b72e3 --- /dev/null +++ b/frontend/src/hooks/useDynamicParameters.ts @@ -0,0 +1,126 @@ +/** + * Hook for managing dynamic pipeline parameters from JSON schema. + * + * This hook: + * 1. Extracts renderable parameters from the pipeline schema + * 2. Manages parameter values with defaults from schema + * 3. Provides a unified onChange handler for parameter updates + */ + +import { useMemo, useCallback } from "react"; +import type { PipelineConfigSchema, ModeDefaults } from "../lib/api"; +import { + extractRenderableParameters, + type RenderableParameter, +} from "../lib/schemaInference"; +import type { InputMode } from "../types"; + +export interface UseDynamicParametersOptions { + /** The pipeline config schema */ + schema: PipelineConfigSchema | undefined; + /** Mode-specific default overrides */ + modeDefaults?: Record; + /** Current input mode */ + inputMode?: InputMode; + /** Current parameter values (controlled) */ + values: Record; + /** Callback when a parameter value changes */ + onValueChange: (paramName: string, value: unknown) => void; +} + +export interface UseDynamicParametersResult { + /** List of parameters that can be rendered dynamically */ + parameters: RenderableParameter[]; + /** Get the current value for a parameter (with default fallback) */ + getValue: (paramName: string) => unknown; + /** Handle parameter change */ + handleChange: (paramName: string, value: unknown) => void; + /** Get default value for a parameter (considering mode) */ + getDefaultValue: (paramName: string) => unknown; +} + +/** + * Converts a camelCase or PascalCase key to snake_case. + */ +function toSnakeCase(key: string): string { + return key.replace(/([A-Z])/g, "_$1").toLowerCase(); +} + +/** + * Converts a snake_case key to camelCase. + */ +function toCamelCase(key: string): string { + return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); +} + +export function useDynamicParameters({ + schema, + modeDefaults, + inputMode, + values, + onValueChange, +}: UseDynamicParametersOptions): UseDynamicParametersResult { + // Extract renderable parameters from schema + const parameters = useMemo(() => { + if (!schema) return []; + return extractRenderableParameters(schema); + }, [schema]); + + // Get default value for a parameter, considering mode overrides + const getDefaultValue = useCallback( + (paramName: string): unknown => { + if (!schema?.properties) return undefined; + + const property = schema.properties[paramName]; + if (!property) return undefined; + + // Check for mode-specific override first + if (inputMode && modeDefaults?.[inputMode]) { + const modeDefault = modeDefaults[inputMode]; + // Mode defaults use snake_case, so we need to check both + const snakeKey = toSnakeCase(paramName); + if (snakeKey in modeDefault) { + return modeDefault[snakeKey as keyof ModeDefaults]; + } + if (paramName in modeDefault) { + return modeDefault[paramName as keyof ModeDefaults]; + } + } + + // Fall back to schema default + return property.default; + }, + [schema, modeDefaults, inputMode] + ); + + // Get current value with fallback to default + const getValue = useCallback( + (paramName: string): unknown => { + // Check both snake_case and camelCase in values + const snakeKey = toSnakeCase(paramName); + const camelKey = toCamelCase(paramName); + + if (paramName in values) return values[paramName]; + if (snakeKey in values) return values[snakeKey]; + if (camelKey in values) return values[camelKey]; + + return getDefaultValue(paramName); + }, + [values, getDefaultValue] + ); + + // Handle parameter change + const handleChange = useCallback( + (paramName: string, value: unknown) => { + onValueChange(paramName, value); + }, + [onValueChange] + ); + + return { + parameters, + getValue, + handleChange, + getDefaultValue, + }; +} diff --git a/frontend/src/hooks/usePipelines.ts b/frontend/src/hooks/usePipelines.ts index 128d78ef..2d81ea75 100644 --- a/frontend/src/hooks/usePipelines.ts +++ b/frontend/src/hooks/usePipelines.ts @@ -44,6 +44,40 @@ export function usePipelines() { const supportsImages = schema.config_schema?.properties?.images !== undefined; + // Extract settings panel configuration + const settingsPanel = schema.settings_panel; + + // Extract mode-specific settings panels + const modeSettingsPanels: Record | undefined = + schema.mode_defaults + ? Object.entries(schema.mode_defaults).reduce( + (acc, [mode, defaults]) => { + if (defaults.settings_panel) { + acc[mode as InputMode] = defaults.settings_panel; + } + return acc; + }, + {} as Record + ) + : undefined; + + // Helper to check if item exists in any settings_panel (base or mode-specific) + const hasItemInSettingsPanel = (item: string): boolean => { + // Check base settings_panel + if (settingsPanel?.includes(item)) { + return true; + } + // Check mode-specific settings_panels + if (modeSettingsPanels) { + for (const panel of Object.values(modeSettingsPanels)) { + if (panel.includes(item)) { + return true; + } + } + } + return false; + }; + transformed[id] = { name: schema.name, about: schema.description, @@ -60,9 +94,11 @@ export function usePipelines() { supportsLoRA: schema.supports_lora, supportsVACE: schema.supports_vace, usage: schema.usage, - supportsCacheManagement: schema.supports_cache_management, - supportsKvCacheBias: schema.supports_kv_cache_bias, - supportsQuantization: schema.supports_quantization, + supportsCacheManagement: hasItemInSettingsPanel("cache_management"), + supportsKvCacheBias: hasItemInSettingsPanel( + "kv_cache_attention_bias" + ), + supportsQuantization: hasItemInSettingsPanel("quantization"), minDimension: schema.min_dimension, recommendedQuantizationVramThreshold: schema.recommended_quantization_vram_threshold ?? undefined, @@ -70,6 +106,11 @@ export function usePipelines() { vaeTypes, supportsControllerInput, supportsImages, + settingsPanel, + modeSettingsPanels: + modeSettingsPanels && Object.keys(modeSettingsPanels).length > 0 + ? modeSettingsPanels + : undefined, }; } diff --git a/frontend/src/hooks/useStreamState.ts b/frontend/src/hooks/useStreamState.ts index 176fc55b..6b6c7564 100644 --- a/frontend/src/hooks/useStreamState.ts +++ b/frontend/src/hooks/useStreamState.ts @@ -107,7 +107,7 @@ export function useStreamState() { noiseController, defaultTemporalInterpolationSteps, inputMode: effectiveMode, - seed: (props.base_seed?.default as number) ?? 42, + seed: (props.seed?.default as number) ?? 42, quantization: undefined as "fp8_e4m3fn" | undefined, }; } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0a126b0e..b7365f01 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -370,6 +370,20 @@ export interface PipelineConfigSchema { $defs?: Record; } +// Special control types that require custom UI handling +// Must match SettingsControlType enum in backend base_schema.py +export type SettingsControlType = + | "vace" + | "lora" + | "preprocessor" + | "cache_management" + | "denoising_steps" + | "noise_controls" + | "spout_sender"; + +// Settings panel item: either a special control type or a field name string +export type SettingsPanelItem = SettingsControlType | string; + // Mode-specific default overrides export interface ModeDefaults { height?: number; @@ -378,6 +392,8 @@ export interface ModeDefaults { noise_scale?: number | null; noise_controller?: boolean | null; default_temporal_interpolation_steps?: number; + // Settings panel configuration for this mode + settings_panel?: SettingsPanelItem[]; } export interface PipelineSchemaInfo { @@ -403,12 +419,11 @@ export interface PipelineSchemaInfo { // Mode-specific default overrides (optional) mode_defaults?: Record<"text" | "video", ModeDefaults>; // UI capabilities - supports_cache_management: boolean; - supports_kv_cache_bias: boolean; - supports_quantization: boolean; min_dimension: number; recommended_quantization_vram_threshold: number | null; modified: boolean; + // Settings panel configuration (base, can be overridden per-mode in mode_defaults) + settings_panel?: SettingsPanelItem[]; } export interface PipelineSchemasResponse { diff --git a/frontend/src/lib/schemaInference.ts b/frontend/src/lib/schemaInference.ts new file mode 100644 index 00000000..fdf26073 --- /dev/null +++ b/frontend/src/lib/schemaInference.ts @@ -0,0 +1,230 @@ +/** + * Schema inference utilities for dynamic control rendering. + * + * Maps JSON Schema types from Pydantic to UI control types. + * This is the core logic that enables type-driven dynamic rendering. + */ + +import type { PipelineSchemaProperty, PipelineConfigSchema } from "./api"; + +/** + * Control types that can be rendered dynamically. + */ +export type ControlType = + | "slider" + | "number" + | "toggle" + | "select" + | "text" + | "unknown"; + +/** + * Resolved schema property with enum values extracted from $defs. + */ +export interface ResolvedSchemaProperty extends PipelineSchemaProperty { + resolvedEnum?: string[]; +} + +/** + * Infers the appropriate control type from a JSON Schema property. + * + * Mapping rules: + * - `$ref` to enum definition → select dropdown + * - `type: "boolean"` → toggle + * - `type: "number"` or `"integer"` with `minimum` AND `maximum` → slider + * - `type: "number"` or `"integer"` (no bounds or partial bounds) → number input + * - `type: "string"` → text input + * - Unknown → fallback (logs warning) + */ +export function inferControlType( + property: ResolvedSchemaProperty +): ControlType { + // Check for enum reference first (highest priority) + if (property.$ref || property.resolvedEnum) { + return "select"; + } + + // Check for direct enum on property + if (property.enum && Array.isArray(property.enum)) { + return "select"; + } + + // Handle anyOf (often used for nullable types) + if (property.anyOf && Array.isArray(property.anyOf)) { + // Find the non-null type in anyOf + const nonNullType = property.anyOf.find( + (t: unknown) => + typeof t === "object" && + t !== null && + (t as { type?: string }).type !== "null" + ) as { type?: string; minimum?: number; maximum?: number } | undefined; + + if (nonNullType) { + if (nonNullType.type === "boolean") return "toggle"; + if (nonNullType.type === "number" || nonNullType.type === "integer") { + // Check for bounds in the anyOf type or the parent property + const hasMin = + nonNullType.minimum !== undefined || property.minimum !== undefined; + const hasMax = + nonNullType.maximum !== undefined || property.maximum !== undefined; + if (hasMin && hasMax) return "slider"; + return "number"; + } + if (nonNullType.type === "string") return "text"; + } + } + + // Standard type checks + if (property.type === "boolean") { + return "toggle"; + } + + if (property.type === "number" || property.type === "integer") { + // Slider requires both min and max bounds + if (property.minimum !== undefined && property.maximum !== undefined) { + return "slider"; + } + return "number"; + } + + if (property.type === "string") { + return "text"; + } + + // Unknown type - will use fallback renderer + return "unknown"; +} + +/** + * Resolves a $ref to its enum values from $defs. + */ +export function resolveEnumFromRef( + ref: string, + defs?: Record +): string[] | undefined { + if (!ref || !defs) return undefined; + + // Extract definition name from $ref (e.g., "#/$defs/VaeType" -> "VaeType") + const defName = ref.split("/").pop(); + if (!defName) return undefined; + + const definition = defs[defName]; + if (definition && Array.isArray(definition.enum)) { + return definition.enum as string[]; + } + + return undefined; +} + +/** + * Resolves a schema property, extracting enum values from $defs if needed. + */ +export function resolveSchemaProperty( + property: PipelineSchemaProperty, + defs?: Record +): ResolvedSchemaProperty { + const resolved: ResolvedSchemaProperty = { ...property }; + + if (property.$ref) { + resolved.resolvedEnum = resolveEnumFromRef(property.$ref, defs); + } + + return resolved; +} + +/** + * Parameters that should be excluded from dynamic rendering. + * These are either handled specially or are not user-facing. + */ +const EXCLUDED_PARAMETERS = new Set([ + // Internal/computed fields + "pipeline_id", + "pipeline_name", + "pipeline_description", + "pipeline_version", + // Complex types that need custom handling + "denoising_steps", // Handled by DenoisingStepsSlider + "ref_images", // Handled by file picker + "loras", // Handled by LoRAManager + // Fields that are handled elsewhere in the UI + "modes", +]); + +/** + * Checks if a parameter should be rendered dynamically. + */ +export function shouldRenderParameter(paramName: string): boolean { + return !EXCLUDED_PARAMETERS.has(paramName); +} + +/** + * Gets display metadata for a parameter from the schema. + */ +export function getParameterDisplayInfo( + paramName: string, + property: PipelineSchemaProperty +): { label: string; tooltip?: string } { + // Convert snake_case to Title Case for label + const label = + paramName + .split("_") + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" ") + ":"; + + return { + label, + tooltip: property.description, + }; +} + +/** + * Extracts renderable parameters from a pipeline config schema. + * Returns parameters in a format ready for dynamic rendering. + */ +export interface RenderableParameter { + name: string; + property: ResolvedSchemaProperty; + controlType: ControlType; + label: string; + tooltip?: string; +} + +export function extractRenderableParameters( + schema: PipelineConfigSchema +): RenderableParameter[] { + const parameters: RenderableParameter[] = []; + + if (!schema.properties) { + return parameters; + } + + for (const [name, property] of Object.entries(schema.properties)) { + if (!shouldRenderParameter(name)) { + continue; + } + + const resolved = resolveSchemaProperty(property, schema.$defs); + const controlType = inferControlType(resolved); + + // Skip unknown types for now (they would need custom handling) + if (controlType === "unknown") { + console.warn( + `[schemaInference] Unknown control type for parameter "${name}":`, + property + ); + continue; + } + + const displayInfo = getParameterDisplayInfo(name, property); + + parameters.push({ + name, + property: resolved, + controlType, + label: displayInfo.label, + tooltip: displayInfo.tooltip, + }); + } + + return parameters; +} diff --git a/frontend/src/pages/StreamPage.tsx b/frontend/src/pages/StreamPage.tsx index 300ca35f..31bcb09d 100644 --- a/frontend/src/pages/StreamPage.tsx +++ b/frontend/src/pages/StreamPage.tsx @@ -78,8 +78,8 @@ export function StreamPage() { settings, updateSettings, getDefaults, - supportsNoiseControls, spoutAvailable, + pipelineSchemas, } = useStreamState(); // Prompt state - use unified default prompts based on mode @@ -117,6 +117,12 @@ export function StreamPage() { // Video display state const [videoScaleMode, setVideoScaleMode] = useState<"fit" | "native">("fit"); + // Dynamic config values for fields not in SettingsState (e.g., new_param) + // Maps field names to their current values, initialized from schema defaults + const [dynamicConfigValues, setDynamicConfigValues] = useState< + Record + >({}); + // External control of timeline selection const [externalSelectedPromptId, setExternalSelectedPromptId] = useState< string | null @@ -595,6 +601,20 @@ export function StreamPage() { updateSettings({ preprocessorIds: ids }); }; + // Handle dynamic config value changes (for fields not in SettingsState) + const handleConfigValueChange = (fieldName: string, value: unknown) => { + setDynamicConfigValues(prev => ({ + ...prev, + [fieldName]: value, + })); + // Send parameter update to backend if streaming + if (isStreaming) { + sendParameterUpdate({ + [fieldName]: value, + }); + } + }; + const handleVaceContextScaleChange = (scale: number) => { updateSettings({ vaceContextScale: scale }); // Send VACE context scale update to backend if streaming @@ -712,6 +732,88 @@ export function StreamPage() { return () => document.removeEventListener("keydown", handleKeyDown); }, [selectedTimelinePrompt]); + // Initialize dynamic config values from schema defaults when schema or pipeline changes + useEffect(() => { + const currentSchema = pipelineSchemas?.pipelines[settings.pipelineId]; + if (!currentSchema?.config_schema?.properties) { + console.log( + `[StreamPage] No schema properties for pipeline ${settings.pipelineId}` + ); + return; + } + + const newDynamicValues: Record = {}; + const properties = currentSchema.config_schema.properties; + console.log( + `[StreamPage] Initializing dynamic config values. Available properties:`, + Object.keys(properties) + ); + + // Get mode-specific defaults + const modeDefaults = + currentSchema.mode_defaults?.[settings.inputMode || "text"]; + + // Initialize values for fields that are in the schema but not in SettingsState + // Exclude fields that are already handled explicitly + const excludedFields = new Set([ + "height", + "width", + "base_seed", + "denoising_steps", + "noise_scale", + "noise_controller", + "manage_cache", + "quantization", + "kv_cache_attention_bias", + "vae_type", + "vace_context_scale", + "ref_images", + "preprocessor_ids", + ]); + + for (const [fieldName, property] of Object.entries(properties)) { + if (excludedFields.has(fieldName)) continue; + + // Check if we already have a value for this field + if (dynamicConfigValues[fieldName] !== undefined) continue; + + // Get default from mode overrides or schema + const defaultValue = + modeDefaults?.[fieldName as keyof typeof modeDefaults] ?? + property.default; + + if (defaultValue !== undefined) { + newDynamicValues[fieldName] = defaultValue; + console.log( + `[StreamPage] Initializing ${fieldName} with default value:`, + defaultValue + ); + } else { + console.log( + `[StreamPage] Field ${fieldName} has no default value, skipping` + ); + } + } + + if (Object.keys(newDynamicValues).length > 0) { + console.log( + `[StreamPage] Setting dynamic config values:`, + newDynamicValues + ); + setDynamicConfigValues(prev => { + // Only add values that don't already exist to avoid overwriting user changes + const updated = { ...prev }; + for (const [key, value] of Object.entries(newDynamicValues)) { + if (updated[key] === undefined) { + updated[key] = value; + } + } + return updated; + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pipelineSchemas, settings.pipelineId, settings.inputMode]); + // Update temporal interpolation defaults and clear prompts when pipeline changes useEffect(() => { const pipeline = pipelines?.[settings.pipelineId]; @@ -848,7 +950,7 @@ export function StreamPage() { // Add seed if pipeline supports quantization (implies it needs seed) if (currentPipeline?.supportsQuantization) { - loadParams.seed = settings.seed ?? 42; + loadParams.base_seed = settings.seed ?? 42; loadParams.quantization = settings.quantization ?? null; loadParams.vae_type = settings.vaeType ?? "wan"; } @@ -874,6 +976,9 @@ export function StreamPage() { loadParams = { ...loadParams, ...vaceParams }; } + // Add dynamic config values (e.g., new_param) + loadParams = { ...loadParams, ...dynamicConfigValues }; + console.log( `Loading ${pipelineIds.length} pipeline(s) (${pipelineIds.join(", ")}) with resolution ${resolution.width}x${resolution.height}`, loadParams @@ -934,13 +1039,20 @@ export function StreamPage() { ]; } - // Cache management for pipelines that support it - if (currentPipeline?.supportsCacheManagement) { + // Check if items are in settings_panel for current mode + const currentSchema = pipelineSchemas?.pipelines[pipelineIdToUse]; + const modeSettingsPanel = + currentSchema?.mode_defaults?.[currentMode]?.settings_panel || + currentSchema?.settings_panel || + []; + + // Cache management for pipelines that support it (check settings_panel) + if (modeSettingsPanel.includes("cache_management")) { initialParameters.manage_cache = settings.manageCache ?? true; } - // KV cache bias for pipelines that support it - if (currentPipeline?.supportsKvCacheBias) { + // KV cache bias for pipelines that support it (check settings_panel) + if (modeSettingsPanel.includes("kv_cache_attention_bias")) { initialParameters.kv_cache_attention_bias = settings.kvCacheAttentionBias ?? 1.0; } @@ -1218,6 +1330,7 @@ export function StreamPage() { onPipelineIdChange={handlePipelineIdChange} isStreaming={isStreaming} isLoading={isLoading} + schema={pipelineSchemas?.pipelines[settings.pipelineId]} resolution={ settings.resolution || { height: getDefaults(settings.pipelineId, settings.inputMode) @@ -1258,7 +1371,6 @@ export function StreamPage() { onLorasChange={handleLorasChange} loraMergeStrategy={settings.loraMergeStrategy ?? "permanent_merge"} inputMode={settings.inputMode} - supportsNoiseControls={supportsNoiseControls(settings.pipelineId)} spoutSender={settings.spoutSender} onSpoutSenderChange={handleSpoutSenderChange} spoutAvailable={spoutAvailable} @@ -1277,6 +1389,13 @@ export function StreamPage() { vaeTypes={pipelines?.[settings.pipelineId]?.vaeTypes} preprocessorIds={settings.preprocessorIds ?? []} onPreprocessorIdsChange={handlePreprocessorIdsChange} + configValues={Object.fromEntries( + Object.entries(dynamicConfigValues).map(([key, value]) => [ + key, + { value }, + ]) + )} + onConfigValueChange={handleConfigValueChange} /> diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index ab11303f..911fea93 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -115,6 +115,11 @@ export interface PipelineInfo { supportsControllerInput?: boolean; // Images input support - presence of images field in pipeline schema supportsImages?: boolean; + // Settings panel configuration - defines order and which controls to show + // Can be overridden per mode via modeSettingsPanels + settingsPanel?: string[]; + // Mode-specific settings panel overrides + modeSettingsPanels?: Record; } export interface DownloadProgress { diff --git a/src/scope/core/pipelines/base_schema.py b/src/scope/core/pipelines/base_schema.py index 1f1aaa52..4e2f0a03 100644 --- a/src/scope/core/pipelines/base_schema.py +++ b/src/scope/core/pipelines/base_schema.py @@ -23,6 +23,7 @@ # Re-export CtrlInput for convenient import by pipeline schemas from scope.core.pipelines.controller import CtrlInput as CtrlInput # noqa: PLC0414 +from scope.core.pipelines.enums import Quantization if TYPE_CHECKING: from .artifacts import Artifact @@ -31,12 +32,20 @@ # Field templates - use these to override defaults while keeping constraints/descriptions def height_field(default: int = 512) -> FieldInfo: """Height field with standard constraints.""" - return Field(default=default, ge=1, description="Output height in pixels") + return Field( + default=default, + ge=1, + description="Output video height in pixels. Higher values produce more detailed vertical resolution but reduces speed.", + ) def width_field(default: int = 512) -> FieldInfo: """Width field with standard constraints.""" - return Field(default=default, ge=1, description="Output width in pixels") + return Field( + default=default, + ge=1, + description="Output video width in pixels. Higher values produce more detailed horizontal resolution but reduces speed.", + ) def denoising_steps_field(default: list[int] | None = None) -> FieldInfo: @@ -102,6 +111,33 @@ class UsageType(str, Enum): PREPROCESSOR = "preprocessor" +class SettingsControlType(str, Enum): + """Special control types that require custom UI handling. + + These controls have complex UI behavior that cannot be inferred + from the field definition alone (e.g., enabling one control affects others). + """ + + # VACE toggle with nested controls (use_input_video, context_scale) + VACE = "vace" + # LoRA adapters manager + LORA = "lora" + # Preprocessor selector + PREPROCESSOR = "preprocessor" + # Cache management (manage_cache toggle + reset_cache button) + CACHE_MANAGEMENT = "cache_management" + # Denoising step list with custom slider UI + DENOISING_STEPS = "denoising_steps" + # Noise controls group (noise_controller toggle + noise_scale slider) + NOISE_CONTROLS = "noise_controls" + # Spout sender configuration + SPOUT_SENDER = "spout_sender" + + +# Type for settings panel items: either a special control type or a field name string +SettingsPanelItem = SettingsControlType | str + + class ModeDefaults(BaseModel): """Mode-specific default values. @@ -141,6 +177,10 @@ class ModeDefaults(BaseModel): # Temporal interpolation default_temporal_interpolation_steps: int | None = None + # Settings panel configuration for this mode (overrides base settings_panel) + # If not set, uses the pipeline's base settings_panel + settings_panel: list[SettingsControlType | str] | None = None + class BasePipelineConfig(BaseModel): """Base configuration for all pipelines. @@ -197,6 +237,14 @@ class BasePipelineConfig(BaseModel): # Use default=True to mark the default mode. Only include fields that differ from base. modes: ClassVar[dict[str, ModeDefaults]] = {"text": ModeDefaults(default=True)} + # Settings panel configuration - defines order and which controls to show + # Items can be: + # - SettingsControlType enum values for special controls (VACE, LORA, etc.) + # - Field name strings for dynamic controls (e.g., "vae_type", "height") + # If not set, frontend uses legacy hardcoded logic. + # Can be overridden per-mode in ModeDefaults.settings_panel + settings_panel: ClassVar[list[SettingsControlType | str] | None] = None + # Prompt and temporal interpolation support supports_prompts: ClassVar[bool] = True default_temporal_interpolation_method: ClassVar[Literal["linear", "slerp"]] = ( @@ -215,12 +263,12 @@ class BasePipelineConfig(BaseModel): ) base_seed: Annotated[int, Field(ge=0)] = Field( default=42, - description="Base random seed for reproducible generation", + description="Random seed for reproducible generation. Using the same seed with the same settings will produce similar results.", ) denoising_steps: list[int] | None = denoising_steps_field() # Video mode parameters (None means not applicable/text mode) - noise_scale: Annotated[float, Field(ge=0.0, le=1.0)] | None = noise_scale_field() + noise_scale: Annotated[float, Field(ge=0.0, le=5.0)] | None = noise_scale_field() noise_controller: bool | None = noise_controller_field() input_size: int | None = input_size_field() @@ -228,6 +276,18 @@ class BasePipelineConfig(BaseModel): ref_images: list[str] | None = ref_images_field() vace_context_scale: float = vace_context_scale_field() + # Quantization (optional, only used if supports_quantization is True) + quantization: Quantization | None = Field( + default=None, + description="Quantization method for the diffusion model. fp8_e4m3fn (Dynamic) reduces memory usage, but might affect performance and quality. None uses full precision and uses more memory, but does not affect performance and quality.", + ) + + # KV cache attention bias (optional, only used if supports_kv_cache_bias is True) + kv_cache_attention_bias: Annotated[float, Field(ge=0.01, le=1.0)] | None = Field( + default=None, + description="Controls how much to rely on past frames in the cache during generation. A lower value can help mitigate error accumulation and prevent repetitive motion. Uses log scale: 1.0 = full reliance on past frames, smaller values = less reliance on past frames. Typical values: 0.3-0.7 for moderate effect, 0.1-0.2 for strong effect.", + ) + @classmethod def get_pipeline_metadata(cls) -> dict[str, str]: """Return pipeline identification metadata. @@ -311,9 +371,6 @@ def get_schema_with_metadata(cls) -> dict[str, Any]: metadata["requires_models"] = cls.requires_models or bool(cls.artifacts) metadata["supports_lora"] = cls.supports_lora metadata["supports_vace"] = cls.supports_vace - metadata["supports_cache_management"] = cls.supports_cache_management - metadata["supports_kv_cache_bias"] = cls.supports_kv_cache_bias - metadata["supports_quantization"] = cls.supports_quantization metadata["min_dimension"] = cls.min_dimension metadata["recommended_quantization_vram_threshold"] = ( cls.recommended_quantization_vram_threshold @@ -322,10 +379,27 @@ def get_schema_with_metadata(cls) -> dict[str, Any]: metadata["usage"] = cls.usage metadata["config_schema"] = cls.model_json_schema() + # Include base settings_panel if defined + if cls.settings_panel is not None: + # Convert enum values to their string representation + metadata["settings_panel"] = [ + item.value if isinstance(item, SettingsControlType) else item + for item in cls.settings_panel + ] + # Include mode-specific defaults (excluding None values and the "default" flag) mode_defaults = {} for mode_name, mode_config in cls.modes.items(): overrides = mode_config.model_dump(exclude={"default"}, exclude_none=True) + # Convert settings_panel enum values to strings if present + if ( + "settings_panel" in overrides + and overrides["settings_panel"] is not None + ): + overrides["settings_panel"] = [ + item.value if isinstance(item, SettingsControlType) else item + for item in overrides["settings_panel"] + ] if overrides: mode_defaults[mode_name] = overrides if mode_defaults: diff --git a/src/scope/core/pipelines/krea_realtime_video/schema.py b/src/scope/core/pipelines/krea_realtime_video/schema.py index 78a9d23b..8462b5d3 100644 --- a/src/scope/core/pipelines/krea_realtime_video/schema.py +++ b/src/scope/core/pipelines/krea_realtime_video/schema.py @@ -1,7 +1,7 @@ from pydantic import Field from ..artifacts import HuggingfaceRepoArtifact -from ..base_schema import BasePipelineConfig, ModeDefaults +from ..base_schema import BasePipelineConfig, ModeDefaults, SettingsControlType from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -41,11 +41,9 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): ), ] - supports_cache_management = True - supports_kv_cache_bias = True - supports_quantization = True min_dimension = 16 modified = True + supports_quantization = True recommended_quantization_vram_threshold = 40.0 default_temporal_interpolation_method = "linear" @@ -56,11 +54,27 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): denoising_steps: list[int] = [1000, 750, 500, 250] vae_type: VaeType = Field( default=VaeType.WAN, - description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", + description="VAE type to use for encoding/decoding. 'wan' is the full VAE with best quality. 'lightvae' is 75% pruned for faster performance but lower quality. 'tae' is a tiny autoencoder for fast preview quality. 'lighttae' is LightTAE with WanVAE normalization for faster performance with consistent latent space.", ) modes = { - "text": ModeDefaults(default=True), + "text": ModeDefaults( + default=True, + # Settings panel for text mode (no noise controls) + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "vae_type", + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + "kv_cache_attention_bias", + SettingsControlType.DENOISING_STEPS, + "quantization", + ], + ), "video": ModeDefaults( height=256, width=256, @@ -68,5 +82,20 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): noise_controller=True, denoising_steps=[1000, 750], default_temporal_interpolation_steps=0, + # Video mode includes noise controls + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "vae_type", + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + "kv_cache_attention_bias", + SettingsControlType.DENOISING_STEPS, + SettingsControlType.NOISE_CONTROLS, + "quantization", + ], ), } diff --git a/src/scope/core/pipelines/longlive/schema.py b/src/scope/core/pipelines/longlive/schema.py index fa56d9e7..3afca5e1 100644 --- a/src/scope/core/pipelines/longlive/schema.py +++ b/src/scope/core/pipelines/longlive/schema.py @@ -1,7 +1,7 @@ from pydantic import Field from ..artifacts import HuggingfaceRepoArtifact -from ..base_schema import BasePipelineConfig, ModeDefaults +from ..base_schema import BasePipelineConfig, ModeDefaults, SettingsControlType from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -38,26 +38,52 @@ class LongLiveConfig(BasePipelineConfig): ), ] - supports_cache_management = True - supports_quantization = True min_dimension = 16 modified = True + supports_quantization = True height: int = 320 width: int = 576 denoising_steps: list[int] = [1000, 750, 500, 250] vae_type: VaeType = Field( default=VaeType.WAN, - description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", + description="VAE type to use for encoding/decoding. 'wan' is the full VAE with best quality. 'lightvae' is 75% pruned for faster performance but lower quality. 'tae' is a tiny autoencoder for fast preview quality. 'lighttae' is LightTAE with WanVAE normalization for faster performance with consistent latent space.", ) modes = { - "text": ModeDefaults(default=True), + "text": ModeDefaults( + default=True, + # Settings panel for text mode (no noise controls) + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + SettingsControlType.DENOISING_STEPS, + "quantization", + ], + ), "video": ModeDefaults( height=512, width=512, noise_scale=0.7, noise_controller=True, denoising_steps=[1000, 750], + # Video mode includes noise controls + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + SettingsControlType.DENOISING_STEPS, + SettingsControlType.NOISE_CONTROLS, + "quantization", + ], ), } diff --git a/src/scope/core/pipelines/memflow/schema.py b/src/scope/core/pipelines/memflow/schema.py index e1652c0c..21db2178 100644 --- a/src/scope/core/pipelines/memflow/schema.py +++ b/src/scope/core/pipelines/memflow/schema.py @@ -1,7 +1,7 @@ from pydantic import Field from ..artifacts import HuggingfaceRepoArtifact -from ..base_schema import BasePipelineConfig, ModeDefaults +from ..base_schema import BasePipelineConfig, ModeDefaults, SettingsControlType from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -38,26 +38,54 @@ class MemFlowConfig(BasePipelineConfig): ), ] - supports_cache_management = True - supports_quantization = True min_dimension = 16 modified = True + supports_quantization = True height: int = 320 width: int = 576 denoising_steps: list[int] = [1000, 750, 500, 250] vae_type: VaeType = Field( default=VaeType.WAN, - description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", + description="VAE type to use for encoding/decoding. 'wan' is the full VAE with best quality. 'lightvae' is 75% pruned for faster performance but lower quality. 'tae' is a tiny autoencoder for fast preview quality. 'lighttae' is LightTAE with WanVAE normalization for faster performance with consistent latent space.", ) modes = { - "text": ModeDefaults(default=True), + "text": ModeDefaults( + default=True, + # Settings panel for text mode (no noise controls) + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "vae_type", + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + SettingsControlType.DENOISING_STEPS, + "quantization", + ], + ), "video": ModeDefaults( height=512, width=512, noise_scale=0.7, noise_controller=True, denoising_steps=[1000, 750], + # Video mode includes noise controls + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "vae_type", + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + SettingsControlType.DENOISING_STEPS, + SettingsControlType.NOISE_CONTROLS, + "quantization", + ], ), } diff --git a/src/scope/core/pipelines/reward_forcing/schema.py b/src/scope/core/pipelines/reward_forcing/schema.py index 22c28a3b..80ebdeac 100644 --- a/src/scope/core/pipelines/reward_forcing/schema.py +++ b/src/scope/core/pipelines/reward_forcing/schema.py @@ -1,7 +1,7 @@ from pydantic import Field from ..artifacts import HuggingfaceRepoArtifact -from ..base_schema import BasePipelineConfig, ModeDefaults +from ..base_schema import BasePipelineConfig, ModeDefaults, SettingsControlType from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -37,26 +37,54 @@ class RewardForcingConfig(BasePipelineConfig): ), ] - supports_cache_management = True - supports_quantization = True min_dimension = 16 modified = True + supports_quantization = True height: int = 320 width: int = 576 denoising_steps: list[int] = [1000, 750, 500, 250] vae_type: VaeType = Field( default=VaeType.WAN, - description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", + description="VAE type to use for encoding/decoding. 'wan' is the full VAE with best quality. 'lightvae' is 75% pruned for faster performance but lower quality. 'tae' is a tiny autoencoder for fast preview quality. 'lighttae' is LightTAE with WanVAE normalization for faster performance with consistent latent space.", ) modes = { - "text": ModeDefaults(default=True), + "text": ModeDefaults( + default=True, + # Settings panel for text mode (no noise controls) + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "vae_type", + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + SettingsControlType.DENOISING_STEPS, + "quantization", + ], + ), "video": ModeDefaults( height=512, width=512, noise_scale=0.7, noise_controller=True, denoising_steps=[1000, 750], + # Video mode includes noise controls + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "vae_type", + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + SettingsControlType.DENOISING_STEPS, + SettingsControlType.NOISE_CONTROLS, + "quantization", + ], ), } diff --git a/src/scope/core/pipelines/streamdiffusionv2/schema.py b/src/scope/core/pipelines/streamdiffusionv2/schema.py index e6b1ed0a..ff2fb76c 100644 --- a/src/scope/core/pipelines/streamdiffusionv2/schema.py +++ b/src/scope/core/pipelines/streamdiffusionv2/schema.py @@ -1,7 +1,7 @@ from pydantic import Field from ..artifacts import HuggingfaceRepoArtifact -from ..base_schema import BasePipelineConfig, ModeDefaults +from ..base_schema import BasePipelineConfig, ModeDefaults, SettingsControlType from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -38,10 +38,9 @@ class StreamDiffusionV2Config(BasePipelineConfig): ), ] - supports_cache_management = True - supports_quantization = True min_dimension = 16 modified = True + supports_quantization = True denoising_steps: list[int] = [750, 250] noise_scale: float = 0.7 @@ -49,7 +48,7 @@ class StreamDiffusionV2Config(BasePipelineConfig): input_size: int = 4 vae_type: VaeType = Field( default=VaeType.WAN, - description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", + description="VAE type to use for encoding/decoding. 'wan' is the full VAE with best quality. 'lightvae' is 75% pruned for faster performance but lower quality. 'tae' is a tiny autoencoder for fast preview quality. 'lighttae' is LightTAE with WanVAE normalization for faster performance with consistent latent space.", ) modes = { @@ -57,6 +56,37 @@ class StreamDiffusionV2Config(BasePipelineConfig): height=512, width=512, denoising_steps=[1000, 750], + # Settings panel for text mode (no noise controls) + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "vae_type", + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + SettingsControlType.DENOISING_STEPS, + "quantization", + ], + ), + "video": ModeDefaults( + default=True, + noise_scale=0.7, + noise_controller=True, + # Video mode includes noise controls + settings_panel=[ + SettingsControlType.VACE, + SettingsControlType.LORA, + SettingsControlType.PREPROCESSOR, + "vae_type", + "height", + "width", + "base_seed", + SettingsControlType.CACHE_MANAGEMENT, + SettingsControlType.DENOISING_STEPS, + SettingsControlType.NOISE_CONTROLS, + "quantization", + ], ), - "video": ModeDefaults(default=True), }