diff --git a/frontend/src/components/ComplexFields.tsx b/frontend/src/components/ComplexFields.tsx new file mode 100644 index 000000000..f8e4dd283 --- /dev/null +++ b/frontend/src/components/ComplexFields.tsx @@ -0,0 +1,515 @@ +/** + * Complex schema-driven settings components: VACE, LoRA, resolution, + * cache, denoising steps, noise, quantization. + * Each block is rendered once per schema configuration (deduplicated by "component" or key). + */ + +import { Info, Minus, Plus } from "lucide-react"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; +import { Toggle } from "./ui/toggle"; +import { LabelWithTooltip } from "./ui/label-with-tooltip"; +import { SliderWithInput } from "./ui/slider-with-input"; +import { PARAMETER_METADATA } from "../data/parameterMetadata"; +import { DenoisingStepsSlider } from "./DenoisingStepsSlider"; +import { ImageManager } from "./ImageManager"; +import { LoRAManager } from "./LoRAManager"; +import { RotateCcw } from "lucide-react"; +import type { PipelineId, LoRAConfig, LoraMergeStrategy } from "../types"; +import type { SchemaFieldUI } from "../lib/schemaSettings"; + +/** Slider state from useLocalSliderValue, passed in from parent */ +export interface SliderState { + localValue: number; + handleValueChange: (v: number) => void; + handleValueCommit: (v: number) => void; + formatValue: (v: number) => number; +} + +/** All data and handlers needed to render complex schema fields. Passed from SettingsPanel. */ +export interface SchemaComplexFieldContext { + pipelineId: PipelineId; + resolution: { height: number; width: number }; + heightError: string | null; + widthError: string | null; + resolutionWarning: string | null; + minDimension: number; + onResolutionChange?: (dim: "height" | "width", value: number) => void; + decrementResolution?: (dim: "height" | "width") => void; + incrementResolution?: (dim: "height" | "width") => void; + vaceEnabled?: boolean; + onVaceEnabledChange?: (enabled: boolean) => void; + vaceUseInputVideo?: boolean; + onVaceUseInputVideoChange?: (enabled: boolean) => void; + vaceContextScaleSlider?: SliderState; + quantization?: "fp8_e4m3fn" | null; + loras?: LoRAConfig[]; + onLorasChange?: (loras: LoRAConfig[]) => void; + loraMergeStrategy?: LoraMergeStrategy; + manageCache?: boolean; + onManageCacheChange?: (enabled: boolean) => void; + onResetCache?: () => void; + kvCacheAttentionBiasSlider?: SliderState; + denoisingSteps?: number[]; + onDenoisingStepsChange?: (steps: number[]) => void; + defaultDenoisingSteps?: number[]; + noiseScaleSlider?: SliderState; + noiseController?: boolean; + onNoiseControllerChange?: (enabled: boolean) => void; + onQuantizationChange?: (q: "fp8_e4m3fn" | null) => void; + inputMode?: "text" | "video"; + supportsNoiseControls?: boolean; + supportsQuantization?: boolean; + supportsCacheManagement?: boolean; + supportsKvCacheBias?: boolean; + isStreaming?: boolean; + isLoading?: boolean; + /** Per-field overrides for schema-driven fields (e.g. image path). */ + schemaFieldOverrides?: Record; + onSchemaFieldOverrideChange?: ( + key: string, + value: unknown, + isRuntimeParam?: boolean + ) => void; +} + +export interface SchemaComplexFieldProps { + component: string; + fieldKey: string; + rendered: Set; + context: SchemaComplexFieldContext; + /** UI metadata for this field (label, is_load_param). Used for image component. */ + ui?: SchemaFieldUI; +} + +/** + * Renders one complex schema field block. Switches on component (and fieldKey for resolution / noise). + */ +export function SchemaComplexField({ + component, + fieldKey, + rendered, + context: ctx, + ui, +}: SchemaComplexFieldProps): React.ReactNode { + if (component === "image") { + const value = ctx.schemaFieldOverrides?.[fieldKey]; + const path = value == null ? null : String(value); + const isRuntimeParam = ui?.is_load_param === false; + const disabled = + ((ctx.isStreaming ?? false) && !isRuntimeParam) || + (ctx.isLoading ?? false); + return ( +
+ {ui?.label != null && ( + {ui.label} + )} + + ctx.onSchemaFieldOverrideChange?.( + fieldKey, + images[0] ?? null, + isRuntimeParam + ) + } + disabled={disabled} + maxImages={1} + hideLabel + /> +
+ ); + } + + if (component === "vace" && !rendered.has("vace")) { + rendered.add("vace"); + return ( +
+
+ + {})} + variant="outline" + size="sm" + className="h-7" + disabled={(ctx.isStreaming ?? false) || (ctx.isLoading ?? false)} + > + {(ctx.vaceEnabled ?? false) ? "ON" : "OFF"} + +
+ {ctx.vaceEnabled && + ctx.quantization !== null && + ctx.quantization !== undefined && ( +
+ +

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

+
+ )} + {ctx.vaceEnabled && ctx.vaceContextScaleSlider && ( +
+
+ + {})} + variant="outline" + size="sm" + className="h-7" + disabled={ + (ctx.isStreaming ?? false) || + (ctx.isLoading ?? false) || + ctx.inputMode !== "video" + } + > + {(ctx.vaceUseInputVideo ?? false) ? "ON" : "OFF"} + +
+
+ +
+ parseFloat(v) || 1.0} + /> +
+
+
+ )} +
+ ); + } + + if (component === "lora" && !rendered.has("lora")) { + rendered.add("lora"); + return ( +
+ {})} + disabled={ctx.isLoading ?? false} + isStreaming={ctx.isStreaming ?? false} + loraMergeStrategy={ctx.loraMergeStrategy ?? "permanent_merge"} + /> +
+ ); + } + + if (component === "resolution") { + if (rendered.has("resolution")) return null; + rendered.add("resolution"); + const minDim = ctx.minDimension; + const resolution = ctx.resolution; + const handleRes = (dim: "height" | "width", v: number) => + ctx.onResolutionChange?.(dim, v); + return ( +
+
+
+
+
+ +
+ + { + const v = parseInt(e.target.value); + if (!isNaN(v)) handleRes("height", v); + }} + disabled={ctx.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={minDim} + max={2048} + /> + +
+
+ {ctx.heightError && ( +

{ctx.heightError}

+ )} +
+
+
+ +
+ + { + const v = parseInt(e.target.value); + if (!isNaN(v)) handleRes("width", v); + }} + disabled={ctx.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={minDim} + max={2048} + /> + +
+
+ {ctx.widthError && ( +

{ctx.widthError}

+ )} +
+ {ctx.resolutionWarning && ( +
+ +

+ {ctx.resolutionWarning} +

+
+ )} +
+
+
+ ); + } + + if (component === "cache" && !rendered.has("cache")) { + rendered.add("cache"); + if (!ctx.supportsCacheManagement) return null; + return ( +
+
+
+ {ctx.supportsKvCacheBias && ctx.kvCacheAttentionBiasSlider && ( + parseFloat(v) || 1.0} + /> + )} +
+ + {})} + variant="outline" + size="sm" + className="h-7" + > + {(ctx.manageCache ?? true) ? "ON" : "OFF"} + +
+
+ + +
+
+
+
+ ); + } + + if (component === "denoising_steps" && !rendered.has("denoising_steps")) { + rendered.add("denoising_steps"); + return ( + {})} + defaultValues={ctx.defaultDenoisingSteps ?? [750, 250]} + tooltip={PARAMETER_METADATA.denoisingSteps.tooltip} + /> + ); + } + + if (component === "noise") { + if (rendered.has("noise")) return null; + rendered.add("noise"); + if (ctx.inputMode !== "video" || !ctx.supportsNoiseControls) return null; + return ( +
+
+
+
+ + {})} + disabled={ctx.isStreaming} + variant="outline" + size="sm" + className="h-7" + > + {(ctx.noiseController ?? true) ? "ON" : "OFF"} + +
+
+ {ctx.noiseScaleSlider && ( + parseFloat(v) || 0.0} + /> + )} +
+
+ ); + } + + if (component === "quantization" && !rendered.has("quantization")) { + rendered.add("quantization"); + if (!ctx.supportsQuantization) return null; + return ( +
+
+
+
+ + +
+ {ctx.vaceEnabled && ( +

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

+ )} +
+
+
+ ); + } + + return null; +} diff --git a/frontend/src/components/DenoisingStepsSlider.tsx b/frontend/src/components/DenoisingStepsSlider.tsx index 447d189d6..8a675eb4a 100644 --- a/frontend/src/components/DenoisingStepsSlider.tsx +++ b/frontend/src/components/DenoisingStepsSlider.tsx @@ -136,7 +136,7 @@ export function DenoisingStepsSlider({
+ + +
+ ); + return ( +
+
+ + {stepper} +
+
+ ); +} + +/** Slider field (number with min/max). Integer when prop.type !== "number". */ +export function SliderField({ + fieldKey, + prop, + value, + onChange, + disabled, + label, + tooltip, +}: BaseFieldProps) { + const { label: displayLabel, tooltip: displayTooltip } = + resolveLabelAndTooltip(fieldKey, prop.description, label, tooltip); + const rawVal = typeof value === "number" ? value : Number(prop.default) || 0; + const min = typeof prop.minimum === "number" ? prop.minimum : 0; + const max = typeof prop.maximum === "number" ? prop.maximum : 100; + const isFloat = prop.type === "number"; + const step = isFloat ? 0.01 : 1; + const incrementAmount = isFloat ? 0.01 : 1; + const numVal = isFloat ? rawVal : Math.round(rawVal); + return ( + onChange(isFloat ? v : Math.round(v))} + onValueCommit={v => onChange(isFloat ? v : Math.round(v))} + min={min} + max={max} + step={step} + incrementAmount={incrementAmount} + labelClassName="text-sm font-medium w-14 shrink-0" + valueFormatter={isFloat ? (v: number) => v : (v: number) => Math.round(v)} + inputParser={ + isFloat + ? (v: string) => parseFloat(v) || numVal + : (v: string) => Math.round(parseFloat(v) || numVal) + } + disabled={disabled} + /> + ); +} + +/** Toggle field (boolean). */ +export function ToggleField({ + fieldKey, + prop, + value, + onChange, + disabled, + label, + tooltip, +}: BaseFieldProps) { + const { label: displayLabel, tooltip: displayTooltip } = + resolveLabelAndTooltip(fieldKey, prop.description, label, tooltip); + const boolVal = value === true; + return ( +
+
+ + onChange(p)} + variant="outline" + size="sm" + className="h-7" + disabled={disabled} + > + {boolVal ? "ON" : "OFF"} + +
+
+ ); +} + +export interface EnumFieldProps extends BaseFieldProps { + /** Enum options from schema $defs when using $ref */ + enumValues?: string[]; +} + +/** Enum/dropdown field. */ +export function EnumField({ + fieldKey, + prop, + value, + onChange, + disabled, + label, + tooltip, + enumValues, +}: EnumFieldProps) { + const { label: displayLabel, tooltip: displayTooltip } = + resolveLabelAndTooltip(fieldKey, prop.description, label, tooltip); + const options = enumValues ?? (prop.enum as string[]) ?? []; + return ( +
+
+ + +
+
+ ); +} + +export interface SchemaPrimitiveFieldProps extends BaseFieldProps { + ui?: SchemaFieldUI; + /** When provided, dispatch by this type instead of inferring from prop */ + fieldType?: PrimitiveFieldType; + /** Enum options from schema $defs when field uses $ref */ + enumValues?: string[]; +} + +/** + * Renders a single primitive schema-driven field by dispatching to + * TextField, NumberField, SliderField, ToggleField, or EnumField. + * Uses fieldType when given, otherwise infers from prop. + */ +export function SchemaPrimitiveField({ + fieldKey, + prop, + value, + onChange, + disabled = false, + label, + tooltip, + fieldType, + enumValues, +}: SchemaPrimitiveFieldProps) { + const resolvedType: PrimitiveFieldType | null = + fieldType ?? inferPrimitiveFieldType(prop); + if (!resolvedType) return null; + + const base = { + fieldKey, + prop, + value, + onChange, + disabled, + label: label ?? (prop.description as string) ?? undefined, + tooltip: (tooltip ?? prop.description) as string | undefined, + }; + + switch (resolvedType) { + case "text": + return ; + case "number": + return ; + case "slider": + return ; + case "toggle": + return ; + case "enum": + return ; + default: + return null; + } +} diff --git a/frontend/src/components/PromptTimeline.tsx b/frontend/src/components/PromptTimeline.tsx index e8692472e..f98e21d77 100644 --- a/frontend/src/components/PromptTimeline.tsx +++ b/frontend/src/components/PromptTimeline.tsx @@ -423,7 +423,6 @@ export function PromptTimeline({ pipelineId: settings.pipelineId, inputMode: settings.inputMode, resolution: settings.resolution, - seed: settings.seed, denoisingSteps: settings.denoisingSteps, noiseScale: settings.noiseScale, noiseController: settings.noiseController, @@ -432,6 +431,7 @@ export function PromptTimeline({ kvCacheAttentionBias: settings.kvCacheAttentionBias, loras: settings.loras, loraMergeStrategy: settings.loraMergeStrategy, + schemaFieldOverrides: settings.schemaFieldOverrides, // Exclude paused state as it's runtime-specific } : undefined, diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index 9af3ed09a..6fc82cdea 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -34,9 +34,17 @@ import type { SettingsState, InputMode, PipelineInfo, - VaeType, } from "../types"; import { LoRAManager } from "./LoRAManager"; +import { + parseConfigurationFields, + COMPLEX_COMPONENTS, +} from "../lib/schemaSettings"; +import { + SchemaComplexField, + type SchemaComplexFieldContext, +} from "./ComplexFields"; +import { SchemaPrimitiveField } from "./PrimitiveFields"; // Minimum dimension for most pipelines (will be overridden by pipeline-specific minDimension from schema) const DEFAULT_MIN_DIMENSION = 1; @@ -54,8 +62,6 @@ interface SettingsPanelProps { width: number; }; onResolutionChange?: (resolution: { height: number; width: number }) => void; - seed?: number; - onSeedChange?: (seed: number) => void; denoisingSteps?: number[]; onDenoisingStepsChange?: (denoisingSteps: number[]) => void; // Default denoising steps for reset functionality - derived from backend schema @@ -90,17 +96,19 @@ interface SettingsPanelProps { onVaceUseInputVideoChange?: (enabled: boolean) => void; vaceContextScale?: number; onVaceContextScaleChange?: (scale: number) => void; - // VAE type selection - vaeType?: VaeType; - onVaeTypeChange?: (vaeType: VaeType) => void; - // Available VAE types from backend registry - vaeTypes?: string[]; // Preprocessors preprocessorIds?: string[]; onPreprocessorIdsChange?: (ids: string[]) => void; // Postprocessors postprocessorIds?: string[]; onPostprocessorIdsChange?: (ids: string[]) => void; + // Dynamic schema-driven primitive fields (key = schema field name) + schemaFieldOverrides?: Record; + onSchemaFieldOverrideChange?: ( + key: string, + value: unknown, + isRuntimeParam?: boolean + ) => void; } export function SettingsPanel({ @@ -112,8 +120,6 @@ export function SettingsPanel({ isLoading = false, resolution, onResolutionChange, - seed = 42, - onSeedChange, denoisingSteps = [700, 500], onDenoisingStepsChange, defaultDenoisingSteps, @@ -142,13 +148,12 @@ export function SettingsPanel({ onVaceUseInputVideoChange, vaceContextScale = 1.0, onVaceContextScaleChange, - vaeType = "wan", - onVaeTypeChange, - vaeTypes, preprocessorIds = [], onPreprocessorIdsChange, postprocessorIds = [], onPostprocessorIdsChange, + schemaFieldOverrides, + onSchemaFieldOverrideChange, }: SettingsPanelProps) { // Local slider state management hooks const noiseScaleSlider = useLocalSliderValue(noiseScale, onNoiseScaleChange); @@ -164,7 +169,6 @@ export function SettingsPanel({ // 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); @@ -233,35 +237,6 @@ export function SettingsPanel({ 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]; return ( @@ -364,101 +339,13 @@ 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 */} + {/* Preprocessor Selector - fixed, always shown */}
0 ? postprocessorIds[0] : "none"} @@ -526,8 +412,6 @@ export function SettingsPanel({ const isPostprocessor = info.usage?.includes("postprocessor") ?? false; if (!isPostprocessor) return false; - // Postprocessors run after the main pipeline, so they receive video output. - // Show any postprocessor that supports video mode, regardless of current input mode. return info.supportedModes?.includes("video") ?? false; }) .map(([pid]) => ( @@ -540,364 +424,518 @@ export function SettingsPanel({
- {/* 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 + {/* Schema-driven configuration (when configSchema has ui.category===configuration) or legacy */} + {(() => { + const configSchema = currentPipeline?.configSchema as + | import("../lib/schemaSettings").ConfigSchemaLike + | undefined; + const parsedFields = parseConfigurationFields( + configSchema, + inputMode + ); + const rendered = new Set(); + + // Enum values from schema $defs for $ref-based enums + const enumValuesByRef: Record = {}; + if (configSchema?.$defs) { + for (const [defName, def] of Object.entries(configSchema.$defs)) { + if (def?.enum && Array.isArray(def.enum)) { + enumValuesByRef[defName] = def.enum as string[]; + } + } + } + + if (parsedFields.length > 0) { + const schemaComplexContext: SchemaComplexFieldContext = { + pipelineId, + resolution, + heightError, + widthError, + resolutionWarning, + minDimension: + currentPipeline?.minDimension ?? DEFAULT_MIN_DIMENSION, + onResolutionChange: handleResolutionChange, + decrementResolution, + incrementResolution, + vaceEnabled, + onVaceEnabledChange, + vaceUseInputVideo, + onVaceUseInputVideoChange, + vaceContextScaleSlider, + quantization: quantization ?? null, + loras, + onLorasChange, + loraMergeStrategy, + manageCache, + onManageCacheChange, + onResetCache, + kvCacheAttentionBiasSlider, + denoisingSteps, + onDenoisingStepsChange, + defaultDenoisingSteps, + noiseScaleSlider, + noiseController, + onNoiseControllerChange, + onQuantizationChange, + inputMode, + supportsNoiseControls, + supportsQuantization: + pipelines?.[pipelineId]?.supportsQuantization, + supportsCacheManagement: + pipelines?.[pipelineId]?.supportsCacheManagement, + supportsKvCacheBias: pipelines?.[pipelineId]?.supportsKvCacheBias, + isStreaming, + isLoading, + schemaFieldOverrides, + onSchemaFieldOverrideChange, + }; + return ( + <> + {parsedFields + .map(({ key, prop, ui, fieldType }) => { + const comp = ui.component; + const complexNode = SchemaComplexField({ + component: comp ?? "", + fieldKey: key, + rendered, + context: schemaComplexContext, + ui, + }); + if (complexNode != null) return complexNode; + if ( + comp && + (COMPLEX_COMPONENTS as readonly string[]).includes(comp) + ) + return null; + // height/width already shown in resolution block – don't render as primitives + if (comp === "resolution" || fieldType === "resolution") + return null; + const value = schemaFieldOverrides?.[key] ?? prop.default; + const isRuntimeParam = ui.is_load_param === false; + const setValue = (v: unknown) => + onSchemaFieldOverrideChange?.(key, v, isRuntimeParam); + const primitiveDisabled = + (isStreaming && !isRuntimeParam) || isLoading; + const enumValues = + fieldType === "enum" && typeof prop.$ref === "string" + ? enumValuesByRef[prop.$ref.split("/").pop() ?? ""] + : undefined; + return ( + - -
-
- {heightError && ( -

{heightError}

- )} -
- -
-
+ ); + }) + .filter(Boolean)} + + ); + } + + // Legacy: no configSchema ui fields – use supportsVACE, supportsLoRA, etc. + return ( + <> + {currentPipeline?.supportsVACE && ( +
+
-
{})} + variant="outline" + size="sm" + className="h-7" + disabled={isStreaming || isLoading} > - - { - const value = parseInt(e.target.value); - if (!isNaN(value)) { - handleResolutionChange("width", value); + {vaceEnabled ? "ON" : "OFF"} + +
+ {vaceEnabled && quantization !== null && ( +
+ +

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

+
+ )} + {vaceEnabled && ( +
+
+ + {}) } - }} - 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} - /> - + variant="outline" + size="sm" + className="h-7" + disabled={ + isStreaming || isLoading || inputMode !== "video" + } + > + {vaceUseInputVideo ? "ON" : "OFF"} + +
+
+ +
+ parseFloat(v) || 1.0} + /> +
+
-
- {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"} - + )} + + {/* 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} +

+
+ )} +
+
- -
- - + )} + + {/* 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} - /> - )} + )} + + {/* Denoising steps - shown for pipelines that support quantization (implies advanced diffusion features) */} + {pipelines?.[pipelineId]?.supportsQuantization && ( + {})} + defaultValues={defaultDenoisingSteps} + tooltip={PARAMETER_METADATA.denoisingSteps.tooltip} + /> + )} + + {/* 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"} + +
+
- {/* 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} + /> +
-
- - parseFloat(v) || 0.0} - /> -
-
- )} - - {/* Quantization controls - shown for pipelines that support it */} - {pipelines?.[pipelineId]?.supportsQuantization && ( -
-
-
-
- - + )} + + {/* 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. +

+ )} +
+
- {/* 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 && ( @@ -906,7 +944,7 @@ export function SettingsPanel({ diff --git a/frontend/src/components/ui/slider-with-input.tsx b/frontend/src/components/ui/slider-with-input.tsx index 810463624..07a74c13e 100644 --- a/frontend/src/components/ui/slider-with-input.tsx +++ b/frontend/src/components/ui/slider-with-input.tsx @@ -45,7 +45,7 @@ export function SliderWithInput({ incrementAmount = step, disabled = false, className = "", - labelClassName = "text-sm text-foreground w-16", + labelClassName = "text-sm font-medium w-16", debounceMs = 100, valueFormatter = v => v, inputParser = v => { diff --git a/frontend/src/data/parameterMetadata.ts b/frontend/src/data/parameterMetadata.ts index 59fd929c5..bda562f88 100644 --- a/frontend/src/data/parameterMetadata.ts +++ b/frontend/src/data/parameterMetadata.ts @@ -13,27 +13,22 @@ export interface ParameterMetadata { export const PARAMETER_METADATA: Record = { height: { - label: "Height:", + label: "Height", tooltip: "Output video height in pixels. Higher values produce more detailed vertical resolution but reduces speed.", }, width: { - label: "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:", + label: "Manage Cache", tooltip: "Enables pipeline to automatically manage the cache which influences newly generated frames. Disable for manual control via Reset Cache.", }, resetCache: { - label: "Reset Cache:", + label: "Reset Cache", tooltip: "Clears previous frames from cache allowing new frames to be generated with fresh history. Only available when Manage Cache is disabled.", }, @@ -43,56 +38,51 @@ export const PARAMETER_METADATA: Record = { "List of denoising timesteps used in diffusion. Values must be in descending order. Lower values mean less noise to remove. More steps can improve quality but reduce speed.", }, noiseController: { - label: "Noise Controller:", + label: "Noise Controller", tooltip: "Enables automatic noise scale adjustment based on detected motion. Disable for manual control via Noise Scale.", }, noiseScale: { - label: "Noise Scale:", + label: "Noise Scale", 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:", + 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:", + 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:", + label: "LoRA Strategy", tooltip: "LoRA merge strategy affects performance and update capabilities. Permanent Merge: Maximum performance, no runtime updates. Runtime PEFT: Lower performance, instant runtime updates.", }, loraScale: { - label: "Scale:", + label: "Scale", tooltip: "Adjust LoRA strength. Updates automatically when you release the slider or use +/- buttons. Typical values: 0.0 = no effect, 1.0 = full strength. Full range -10.0 to 10.0 available depending on LoRA specifications.", }, loraScaleDisabledDuringStream: { - label: "Scale:", + label: "Scale", tooltip: "Runtime adjustment is disabled with Permanent Merge strategy. LoRA scales are fixed at load time. Typical values: 0.0 = no effect, 1.0 = full strength. Full range -10.0 to 10.0 available depending on LoRA specifications.", }, spoutSender: { - label: "Spout Sender:", + label: "Spout Sender", 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:", + label: "Preprocessor", tooltip: "Select a preprocessor to apply before the main pipeline.", }, postprocessor: { - label: "Postprocessor:", + label: "Postprocessor", tooltip: "Select a postprocessor to apply after the main pipeline.", }, }; diff --git a/frontend/src/hooks/usePipelines.ts b/frontend/src/hooks/usePipelines.ts index 439b30be8..b4095e2da 100644 --- a/frontend/src/hooks/usePipelines.ts +++ b/frontend/src/hooks/usePipelines.ts @@ -19,19 +19,6 @@ export function usePipelines() { // Transform to camelCase for TypeScript conventions const transformed: Record = {}; for (const [id, schema] of Object.entries(schemas.pipelines)) { - // Extract VAE types from JSON schema if vae_type field exists - // Pydantic v2 represents enum fields using $ref to definitions - let vaeTypes: string[] | undefined = undefined; - const vaeTypeProperty = schema.config_schema?.properties?.vae_type; - if (vaeTypeProperty?.$ref && schema.config_schema?.$defs) { - const refPath = vaeTypeProperty.$ref; - const defName = refPath.split("/").pop(); - const definition = schema.config_schema.$defs[defName || ""]; - if (definition && Array.isArray(definition.enum)) { - vaeTypes = definition.enum as string[]; - } - } - // Check if pipeline supports controller input (has ctrl_input field in schema) const supportsControllerInput = schema.config_schema?.properties?.ctrl_input !== undefined; @@ -65,9 +52,9 @@ export function usePipelines() { recommendedQuantizationVramThreshold: schema.recommended_quantization_vram_threshold ?? undefined, modified: schema.modified, - vaeTypes, supportsControllerInput, supportsImages, + configSchema: schema.config_schema, }; } diff --git a/frontend/src/hooks/useStreamState.ts b/frontend/src/hooks/useStreamState.ts index 176fc55b0..ec2ba1b20 100644 --- a/frontend/src/hooks/useStreamState.ts +++ b/frontend/src/hooks/useStreamState.ts @@ -20,7 +20,6 @@ const BASE_FALLBACK = { height: 320, width: 576, denoisingSteps: [1000, 750, 500, 250] as number[], - seed: 42, }; // Get fallback defaults for a pipeline before schemas are loaded @@ -38,7 +37,6 @@ function getFallbackDefaults(mode?: InputMode) { noiseController: isVideoMode ? true : undefined, defaultTemporalInterpolationSteps: undefined as number | undefined, inputMode: effectiveMode, - seed: BASE_FALLBACK.seed, quantization: undefined as "fp8_e4m3fn" | undefined, }; } @@ -107,7 +105,6 @@ export function useStreamState() { noiseController, defaultTemporalInterpolationSteps, inputMode: effectiveMode, - seed: (props.base_seed?.default as number) ?? 42, quantization: undefined as "fp8_e4m3fn" | undefined, }; } @@ -145,7 +142,6 @@ export function useStreamState() { height: initialDefaults.height, width: initialDefaults.width, }, - seed: initialDefaults.seed, denoisingSteps: initialDefaults.denoisingSteps, noiseScale: initialDefaults.noiseScale, noiseController: initialDefaults.noiseController, diff --git a/frontend/src/hooks/useWebRTC.ts b/frontend/src/hooks/useWebRTC.ts index 8d6dc990c..1683b16b1 100644 --- a/frontend/src/hooks/useWebRTC.ts +++ b/frontend/src/hooks/useWebRTC.ts @@ -336,6 +336,7 @@ export function useWebRTC(options?: UseWebRTCOptions) { images?: string[]; first_frame_image?: string; last_frame_image?: string; + [key: string]: unknown; }) => { if ( dataChannelRef.current && diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 85e124ff0..2ec4b1dbc 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -348,6 +348,16 @@ export const getAssetUrl = (assetPath: string): string => { return `/api/v1/assets/${encodeURIComponent(filename)}`; }; +// UI metadata from pipeline schema (json_schema_extra on fields) +export interface SchemaFieldUI { + category?: string; + order?: number; + component?: string; + modes?: ("text" | "video")[]; + /** If true, field is a load param (disabled when streaming); if false, runtime param (editable when streaming). Omit = treated as load param. */ + is_load_param?: boolean; +} + // Pipeline schema types - matches output of get_schema_with_metadata() export interface PipelineSchemaProperty { type?: string; @@ -360,6 +370,8 @@ export interface PipelineSchemaProperty { anyOf?: unknown[]; enum?: unknown[]; $ref?: string; + /** UI hints from backend (Field json_schema_extra) */ + ui?: SchemaFieldUI; } export interface PipelineConfigSchema { diff --git a/frontend/src/lib/schemaSettings.ts b/frontend/src/lib/schemaSettings.ts new file mode 100644 index 000000000..dc37c92d5 --- /dev/null +++ b/frontend/src/lib/schemaSettings.ts @@ -0,0 +1,216 @@ +/** + * Utilities for schema-driven settings UI. + * - ui.category === "configuration" (or undefined) => Settings panel + * - ui.category === "input" => Input & Controls panel, below app-defined sections (Prompts, etc.) + * - If category is missing, it is treated as "configuration". + */ + +export interface SchemaFieldUI { + category?: string; + order?: number; + component?: string; + modes?: ("text" | "video")[]; + /** If true, field is a load param (disabled when streaming); if false, runtime param (editable when streaming). Omit = treated as load param. */ + is_load_param?: boolean; + /** Short label for the UI. When set, used instead of description for the field label. */ + label?: string; +} + +export interface SchemaProperty { + type?: string; + default?: unknown; + description?: string; + minimum?: number; + maximum?: number; + enum?: unknown[]; + $ref?: string; + ui?: SchemaFieldUI; + [k: string]: unknown; +} + +/** Accepts pipeline config schema from API; only requires properties[].ui for filtering. */ +export type ConfigSchemaLike = { + properties?: Record; + $defs?: Record; +}; + +export interface ConfigurationField { + key: string; + prop: SchemaProperty; + ui: SchemaFieldUI; +} + +/** Primitive field types inferred from schema (used for widget selection). */ +export type PrimitiveFieldType = + | "text" + | "number" + | "slider" + | "toggle" + | "enum"; + +/** Parsed config field with resolved fieldType (primitive or complex component name). */ +export interface ParsedFieldConfig extends ConfigurationField { + fieldType: PrimitiveFieldType | ComplexComponentName; +} + +/** + * Infer primitive field type from a schema property. + * Handles anyOf (nullable), $ref, enum, and direct type + min/max. + */ +export function inferPrimitiveFieldType( + property: SchemaProperty +): PrimitiveFieldType | null { + type Sub = { + type?: string; + enum?: unknown[]; + minimum?: number; + maximum?: number; + }; + + function fromSub(sub: Sub): PrimitiveFieldType | null { + if (!sub) return null; + if (sub.enum) return "enum"; + if (sub.type === "string") return "text"; + if (sub.type === "integer" || sub.type === "number") { + if (sub.minimum !== undefined && sub.maximum !== undefined) { + return "slider"; + } + return "number"; + } + if (sub.type === "boolean") return "toggle"; + return null; + } + + const anyOf = property.anyOf as unknown[] | undefined; + if (anyOf?.length) { + const nonNull = anyOf.find( + (t: unknown) => + typeof t === "object" && t !== null && (t as Sub).type !== "null" + ) as Sub | undefined; + return nonNull ? fromSub(nonNull) : null; + } + + if (property.$ref) return "enum"; + if (property.enum) return "enum"; + + return fromSub(property as Sub); +} + +/** Internal: resolve fieldType for a list of fields. */ +function resolveFieldTypes(fields: ConfigurationField[]): ParsedFieldConfig[] { + const result: ParsedFieldConfig[] = []; + for (const { key, prop, ui } of fields) { + let fieldType: PrimitiveFieldType | ComplexComponentName; + if ( + ui.component && + COMPLEX_COMPONENTS.includes(ui.component as ComplexComponentName) + ) { + fieldType = ui.component as ComplexComponentName; + } else { + const inferred = inferPrimitiveFieldType(prop); + if (!inferred) { + console.warn(`Could not infer field type for ${key}, skipping`, prop); + continue; + } + fieldType = inferred; + } + result.push({ key, prop, ui, fieldType }); + } + return result; +} + +/** + * Parse configuration fields with resolved fieldType (component or inferred primitive). + */ +export function parseConfigurationFields( + configSchema: ConfigSchemaLike | undefined, + inputMode: "text" | "video" | undefined +): ParsedFieldConfig[] { + return resolveFieldTypes(getConfigurationFields(configSchema, inputMode)); +} + +/** Effective category: missing means "configuration". */ +function effectiveCategory(ui: SchemaFieldUI | undefined): string { + return ui?.category ?? "configuration"; +} + +/** + * Extract fields from pipeline config schema by category. Default category is "configuration". + * Only properties with json_schema_extra (i.e. a "ui" key) are included; base schema fields + * without explicit UI metadata are omitted. Filtered by input mode, sorted by ui.order. + */ +function getFieldsByCategory( + configSchema: ConfigSchemaLike | undefined, + inputMode: "text" | "video" | undefined, + category: "configuration" | "input" +): ConfigurationField[] { + const properties = configSchema?.properties ?? {}; + const fields: ConfigurationField[] = []; + + for (const [key, prop] of Object.entries(properties)) { + const ui = (prop as SchemaProperty)?.ui; + if (ui == null) continue; // only render fields that have json_schema_extra + if (effectiveCategory(ui) !== category) continue; + if ( + ui?.modes && + ui.modes.length > 0 && + inputMode && + !ui.modes.includes(inputMode) + ) + continue; + fields.push({ key, prop: prop as SchemaProperty, ui: ui ?? {} }); + } + + fields.sort((a, b) => { + const oA = a.ui.order ?? 999; + const oB = b.ui.order ?? 999; + if (oA !== oB) return oA - oB; + return a.key.localeCompare(b.key); + }); + + return fields; +} + +/** + * Configuration fields (category "configuration" or undefined) for the Settings panel. + */ +export function getConfigurationFields( + configSchema: ConfigSchemaLike | undefined, + inputMode: "text" | "video" | undefined +): ConfigurationField[] { + return getFieldsByCategory(configSchema, inputMode, "configuration"); +} + +/** + * Input fields (category "input") for the Input & Controls panel, shown below Prompts. + */ +export function getInputFields( + configSchema: ConfigSchemaLike | undefined, + inputMode: "text" | "video" | undefined +): ConfigurationField[] { + return getFieldsByCategory(configSchema, inputMode, "input"); +} + +/** + * Parse input fields (category "input") with resolved fieldType. + */ +export function parseInputFields( + configSchema: ConfigSchemaLike | undefined, + inputMode: "text" | "video" | undefined +): ParsedFieldConfig[] { + return resolveFieldTypes(getInputFields(configSchema, inputMode)); +} + +/** Complex component names that render a single block (render once per component). "image" renders one picker per field. */ +export const COMPLEX_COMPONENTS = [ + "vace", + "lora", + "resolution", + "cache", + "denoising_steps", + "noise", + "quantization", + "image", +] as const; + +export type ComplexComponentName = (typeof COMPLEX_COMPONENTS)[number]; diff --git a/frontend/src/pages/StreamPage.tsx b/frontend/src/pages/StreamPage.tsx index 53caedf5d..28011b77d 100644 --- a/frontend/src/pages/StreamPage.tsx +++ b/frontend/src/pages/StreamPage.tsx @@ -23,7 +23,6 @@ import type { LoRAConfig, LoraMergeStrategy, DownloadProgress, - VaeType, } from "../types"; import type { PromptItem, PromptTransition } from "../lib/api"; import { @@ -488,10 +487,6 @@ export function StreamPage() { updateSettings({ resolution }); }; - const handleSeedChange = (seed: number) => { - updateSettings({ seed }); - }; - const handleDenoisingStepsChange = (denoisingSteps: number[]) => { updateSettings({ denoisingSteps }); // Send denoising steps update to backend @@ -529,11 +524,6 @@ export function StreamPage() { // Note: This setting requires pipeline reload, so we don't send parameter update here }; - const handleVaeTypeChange = (vaeType: VaeType) => { - updateSettings({ vaeType }); - // Note: This setting requires pipeline reload, so we don't send parameter update here - }; - const handleKvCacheAttentionBiasChange = (bias: number) => { updateSettings({ kvCacheAttentionBias: bias }); // Send KV cache attention bias update to backend @@ -911,11 +901,9 @@ export function StreamPage() { width: resolution.width, }; - // Add seed if pipeline supports quantization (implies it needs seed) + // Add quantization when pipeline supports it if (currentPipeline?.supportsQuantization) { - loadParams.seed = settings.seed ?? 42; loadParams.quantization = settings.quantization ?? null; - loadParams.vae_type = settings.vaeType ?? "wan"; } // Add LoRA parameters if pipeline supports LoRA @@ -939,6 +927,14 @@ export function StreamPage() { loadParams = { ...loadParams, ...vaceParams }; } + // Merge schema-driven primitive fields (e.g. new_param) so backend receives them + if ( + settings.schemaFieldOverrides && + Object.keys(settings.schemaFieldOverrides).length > 0 + ) { + loadParams = { ...loadParams, ...settings.schemaFieldOverrides }; + } + console.log( `Loading ${pipelineIds.length} pipeline(s) (${pipelineIds.join(", ")}) with resolution ${resolution.width}x${resolution.height}`, loadParams @@ -1170,6 +1166,23 @@ export function StreamPage() { extensionMode={settings.extensionMode || "firstframe"} onExtensionModeChange={handleExtensionModeChange} onSendExtensionFrames={handleSendExtensionFrames} + configSchema={ + pipelines?.[settings.pipelineId]?.configSchema as + | import("../lib/schemaSettings").ConfigSchemaLike + | undefined + } + schemaFieldOverrides={settings.schemaFieldOverrides ?? {}} + onSchemaFieldOverrideChange={(key, value, isRuntimeParam) => { + updateSettings({ + schemaFieldOverrides: { + ...(settings.schemaFieldOverrides ?? {}), + [key]: value, + }, + }); + if (isRuntimeParam && isStreaming) { + sendParameterUpdate({ [key]: value }); + } + }} />
@@ -1338,8 +1351,6 @@ export function StreamPage() { } } onResolutionChange={handleResolutionChange} - seed={settings.seed ?? 42} - onSeedChange={handleSeedChange} denoisingSteps={ settings.denoisingSteps || getDefaults(settings.pipelineId, settings.inputMode) @@ -1383,13 +1394,22 @@ export function StreamPage() { onVaceUseInputVideoChange={handleVaceUseInputVideoChange} vaceContextScale={settings.vaceContextScale ?? 1.0} onVaceContextScaleChange={handleVaceContextScaleChange} - vaeType={settings.vaeType ?? "wan"} - onVaeTypeChange={handleVaeTypeChange} - vaeTypes={pipelines?.[settings.pipelineId]?.vaeTypes} preprocessorIds={settings.preprocessorIds ?? []} onPreprocessorIdsChange={handlePreprocessorIdsChange} postprocessorIds={settings.postprocessorIds ?? []} onPostprocessorIdsChange={handlePostprocessorIdsChange} + schemaFieldOverrides={settings.schemaFieldOverrides ?? {}} + onSchemaFieldOverrideChange={(key, value, isRuntimeParam) => { + updateSettings({ + schemaFieldOverrides: { + ...(settings.schemaFieldOverrides ?? {}), + [key]: value, + }, + }); + if (isRuntimeParam && isStreaming) { + sendParameterUpdate({ [key]: value }); + } + }} />
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 74495bc23..976546c99 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -4,9 +4,6 @@ export type PipelineId = string; // Input mode for pipeline operation export type InputMode = "text" | "video"; -// VAE type for model selection (dynamic from backend registry) -export type VaeType = string; - // Extension mode for FFLF (First-Frame-Last-Frame) feature export type ExtensionMode = "firstframe" | "lastframe" | "firstlastframe"; @@ -54,7 +51,6 @@ export interface SettingsState { height: number; width: number; }; - seed?: number; denoisingSteps?: number[]; noiseScale?: number; noiseController?: boolean; @@ -84,12 +80,12 @@ export interface SettingsState { firstFrameImage?: string; lastFrameImage?: string; extensionMode?: ExtensionMode; - // VAE type selection - vaeType?: VaeType; // Preprocessors preprocessorIds?: string[]; // Postprocessors postprocessorIds?: string[]; + // Dynamic schema-driven fields (key = schema field name snake_case, value = parsed value) + schemaFieldOverrides?: Record; } export interface PipelineInfo { @@ -119,12 +115,12 @@ export interface PipelineInfo { supportsQuantization?: boolean; minDimension?: number; recommendedQuantizationVramThreshold?: number | null; - // Available VAE types from config schema enum (derived from vae_type field presence) - vaeTypes?: string[]; // Controller input support - presence of ctrl_input field in pipeline schema supportsControllerInput?: boolean; // Images input support - presence of images field in pipeline schema supportsImages?: boolean; + // Raw config schema for dynamic settings UI + configSchema?: import("../lib/api").PipelineConfigSchema; } export interface DownloadProgress { diff --git a/src/scope/core/pipelines/base_schema.py b/src/scope/core/pipelines/base_schema.py index 1f4f1106e..714e988d0 100644 --- a/src/scope/core/pipelines/base_schema.py +++ b/src/scope/core/pipelines/base_schema.py @@ -66,7 +66,9 @@ def noise_controller_field(default: bool | None = None) -> FieldInfo: def input_size_field(default: int | None = 1) -> FieldInfo: - """Input size field with constraints.""" + """Input size field with constraints. No json_schema_extra in base — pipelines + that want to show this in the UI override with ui_field_config(category="input", ...). + """ return Field( default=default, ge=1, @@ -96,6 +98,55 @@ def vace_context_scale_field(default: float = 1.0) -> FieldInfo: InputMode = Literal["text", "video"] +def ui_field_config( + *, + order: int | None = None, + component: str | None = None, + modes: list[str] | None = None, + is_load_param: bool = False, + label: str | None = None, + category: Literal["configuration", "input"] | None = None, +) -> dict[str, Any]: + """Build json_schema_extra for a field so the frontend renders it in Settings or Input & Controls. + + Use with Field(..., json_schema_extra=ui_field_config(...)). + - category "configuration" (default): shown in the Settings panel. + - category "input": shown in the Input & Controls panel, below app-defined sections (Prompts). + If category is omitted, the frontend treats it as "configuration". + + Args: + order: Display order (lower first). If omitted, Pydantic field order is used. + component: Complex component name ("vace", "lora", "denoising_steps", + "quantization", "cache", "image"). Use "image" for image-path fields + (str | None); the UI renders an image picker like first_frame_image. + Omit for primitive widgets. + modes: Restrict to input modes, e.g. ["video"]. Omit to show in all modes. + is_load_param: If True, this field is a load param (passed when loading the + pipeline) and is disabled while the stream is active. Default False + means a runtime param, editable when streaming. + label: Short label for the UI. If set, used instead of description for + the field label; description remains available as tooltip. + category: "configuration" for Settings panel, "input" for Input & Controls + (below Prompts). Omit to default to "configuration". + + Returns: + Dict to pass as json_schema_extra (produces "ui" key in JSON schema). + """ + ui: dict[str, Any] = { + "category": category if category is not None else "configuration", + "is_load_param": is_load_param, + } + if order is not None: + ui["order"] = order + if component is not None: + ui["component"] = component + if modes is not None: + ui["modes"] = modes + if label is not None: + ui["label"] = label + return {"ui": ui} + + class UsageType(str, Enum): """Usage types for pipelines.""" @@ -223,6 +274,9 @@ class BasePipelineConfig(BaseModel): ) denoising_steps: list[int] | None = denoising_steps_field() + # LoRA merge strategy (optional; pipelines with supports_lora override with default + ui) + lora_merge_strategy: Literal["permanent_merge", "runtime_peft"] | None = None + # 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_controller: bool | None = noise_controller_field() diff --git a/src/scope/core/pipelines/defaults.py b/src/scope/core/pipelines/defaults.py index c46111386..896ba10df 100644 --- a/src/scope/core/pipelines/defaults.py +++ b/src/scope/core/pipelines/defaults.py @@ -57,25 +57,25 @@ def resolve_input_mode(kwargs: dict[str, Any]) -> str: def extract_load_params( pipeline_class: type["Pipeline"], load_params: dict | None = None ) -> tuple[int, int, int]: - """Extract height, width, and seed from load_params with pipeline defaults as fallback. + """Extract height, width, and base_seed from load_params with pipeline defaults as fallback. Uses the pipeline's default config values as fallbacks. Args: pipeline_class: The pipeline class to get defaults from - load_params: Optional dictionary with height, width, seed overrides + load_params: Optional dictionary with height, width, base_seed overrides Returns: - Tuple of (height, width, seed) + Tuple of (height, width, base_seed) """ config = get_pipeline_config(pipeline_class) params = load_params or {} height = params.get("height", config.height) width = params.get("width", config.width) - seed = params.get("seed", config.base_seed) + base_seed = params.get("base_seed", config.base_seed) - return height, width, seed + return height, width, base_seed def apply_mode_defaults_to_state( diff --git a/src/scope/core/pipelines/helpers.py b/src/scope/core/pipelines/helpers.py index efeca6c34..81efb5981 100644 --- a/src/scope/core/pipelines/helpers.py +++ b/src/scope/core/pipelines/helpers.py @@ -36,7 +36,7 @@ def initialize_state_from_config( state.set( "manage_cache", getattr(config, "manage_cache", pipeline_config.manage_cache) ) - state.set("base_seed", getattr(config, "seed", pipeline_config.base_seed)) + state.set("base_seed", getattr(config, "base_seed", pipeline_config.base_seed)) # Optional parameters - only set if defined in pipeline config if pipeline_config.denoising_steps is not None: diff --git a/src/scope/core/pipelines/krea_realtime_video/pipeline.py b/src/scope/core/pipelines/krea_realtime_video/pipeline.py index 5a00255ef..c411db2c9 100644 --- a/src/scope/core/pipelines/krea_realtime_video/pipeline.py +++ b/src/scope/core/pipelines/krea_realtime_video/pipeline.py @@ -184,7 +184,7 @@ def __init__( self.state.set("height", config.height) self.state.set("width", config.width) - self.state.set("base_seed", getattr(config, "seed", 42)) + self.state.set("base_seed", getattr(config, "base_seed", 42)) # Warm-up: Run enough iterations to fill the KV cache completely. # This ensures torch.compile compiles the flex_attention kernel at the diff --git a/src/scope/core/pipelines/krea_realtime_video/schema.py b/src/scope/core/pipelines/krea_realtime_video/schema.py index 78a9d23b0..52a9f24ff 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, ui_field_config from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -10,6 +10,7 @@ VACE_14B_ARTIFACT, WAN_1_3B_ARTIFACT, ) +from ..enums import Quantization from ..utils import VaeType @@ -51,12 +52,85 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): default_temporal_interpolation_method = "linear" default_temporal_interpolation_steps = 4 - height: int = 320 - width: int = 576 - denoising_steps: list[int] = [1000, 750, 500, 250] + vace_context_scale: float = Field( + default=1.0, + ge=0.0, + le=2.0, + description="Scaling factor for VACE hint injection (0.0 to 2.0)", + json_schema_extra=ui_field_config( + order=1, component="vace", is_load_param=True + ), + ) + lora_merge_strategy: str = Field( + default="permanent_merge", + description="LoRA merge strategy", + json_schema_extra=ui_field_config( + order=2, component="lora", is_load_param=True + ), + ) 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).", + json_schema_extra=ui_field_config(order=3, is_load_param=True, label="VAE"), + ) + height: int = Field( + default=320, + ge=1, + description="Output height in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + width: int = Field( + default=576, + ge=1, + description="Output width in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + base_seed: int = Field( + default=42, + ge=0, + description="Base random seed for reproducible generation", + json_schema_extra=ui_field_config(order=5, is_load_param=True, label="Seed"), + ) + manage_cache: bool = Field( + default=True, + description="Enable automatic cache management for performance optimization", + json_schema_extra=ui_field_config( + order=5, component="cache", is_load_param=True + ), + ) + denoising_steps: list[int] = Field( + default=[1000, 750, 500, 250], + description="Denoising step schedule for progressive generation", + json_schema_extra=ui_field_config( + order=6, component="denoising_steps", is_load_param=True + ), + ) + noise_scale: float = Field( + default=0.7, + ge=0.0, + le=1.0, + description="Amount of noise to add during video generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + noise_controller: bool = Field( + default=True, + description="Enable dynamic noise control during generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + quantization: Quantization | None = Field( + default=None, + description="Quantization method for the diffusion model.", + json_schema_extra=ui_field_config( + order=8, component="quantization", is_load_param=True + ), ) modes = { diff --git a/src/scope/core/pipelines/longlive/pipeline.py b/src/scope/core/pipelines/longlive/pipeline.py index 70372183f..f0b53b55d 100644 --- a/src/scope/core/pipelines/longlive/pipeline.py +++ b/src/scope/core/pipelines/longlive/pipeline.py @@ -184,7 +184,7 @@ def __init__( self.state.set("height", config.height) self.state.set("width", config.width) - self.state.set("base_seed", getattr(config, "seed", 42)) + self.state.set("base_seed", getattr(config, "base_seed", 42)) self.first_call = True self.last_mode = None # Track mode for transition detection diff --git a/src/scope/core/pipelines/longlive/schema.py b/src/scope/core/pipelines/longlive/schema.py index fa56d9e7d..c2598d2a9 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, ui_field_config from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -10,6 +10,7 @@ VACE_ARTIFACT, WAN_1_3B_ARTIFACT, ) +from ..enums import Quantization from ..utils import VaeType @@ -43,12 +44,86 @@ class LongLiveConfig(BasePipelineConfig): min_dimension = 16 modified = True - height: int = 320 - width: int = 576 - denoising_steps: list[int] = [1000, 750, 500, 250] + # Configuration fields with UI metadata (order, component, modes) + vace_context_scale: float = Field( + default=1.0, + ge=0.0, + le=2.0, + description="Scaling factor for VACE hint injection (0.0 to 2.0)", + json_schema_extra=ui_field_config( + order=1, component="vace", is_load_param=True + ), + ) + lora_merge_strategy: str = Field( + default="permanent_merge", + description="LoRA merge strategy", + json_schema_extra=ui_field_config( + order=2, component="lora", is_load_param=True + ), + ) 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).", + json_schema_extra=ui_field_config(order=3, is_load_param=True, label="VAE"), + ) + height: int = Field( + default=320, + ge=1, + description="Output height in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + width: int = Field( + default=576, + ge=1, + description="Output width in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + base_seed: int = Field( + default=42, + ge=0, + description="Base random seed for reproducible generation", + json_schema_extra=ui_field_config(order=5, is_load_param=True, label="Seed"), + ) + manage_cache: bool = Field( + default=True, + description="Enable automatic cache management for performance optimization", + json_schema_extra=ui_field_config( + order=5, component="cache", is_load_param=True + ), + ) + denoising_steps: list[int] = Field( + default=[1000, 750, 500, 250], + description="Denoising step schedule for progressive generation", + json_schema_extra=ui_field_config( + order=6, component="denoising_steps", is_load_param=True + ), + ) + noise_scale: float = Field( + default=0.7, + ge=0.0, + le=1.0, + description="Amount of noise to add during video generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + noise_controller: bool = Field( + default=True, + description="Enable dynamic noise control during generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + quantization: Quantization | None = Field( + default=None, + description="Quantization method for the diffusion model.", + json_schema_extra=ui_field_config( + order=8, component="quantization", is_load_param=True + ), ) modes = { diff --git a/src/scope/core/pipelines/memflow/pipeline.py b/src/scope/core/pipelines/memflow/pipeline.py index e4ad2f339..e4e5b9d78 100644 --- a/src/scope/core/pipelines/memflow/pipeline.py +++ b/src/scope/core/pipelines/memflow/pipeline.py @@ -184,7 +184,7 @@ def __init__( self.state.set("height", config.height) self.state.set("width", config.width) - self.state.set("base_seed", getattr(config, "seed", 42)) + self.state.set("base_seed", getattr(config, "base_seed", 42)) self.first_call = True self.last_mode = None # Track mode for transition detection diff --git a/src/scope/core/pipelines/memflow/schema.py b/src/scope/core/pipelines/memflow/schema.py index e1652c0c6..1aeef7106 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, ui_field_config from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -10,6 +10,7 @@ VACE_ARTIFACT, WAN_1_3B_ARTIFACT, ) +from ..enums import Quantization from ..utils import VaeType @@ -43,12 +44,85 @@ class MemFlowConfig(BasePipelineConfig): min_dimension = 16 modified = True - height: int = 320 - width: int = 576 - denoising_steps: list[int] = [1000, 750, 500, 250] + vace_context_scale: float = Field( + default=1.0, + ge=0.0, + le=2.0, + description="Scaling factor for VACE hint injection (0.0 to 2.0)", + json_schema_extra=ui_field_config( + order=1, component="vace", is_load_param=True + ), + ) + lora_merge_strategy: str = Field( + default="permanent_merge", + description="LoRA merge strategy", + json_schema_extra=ui_field_config( + order=2, component="lora", is_load_param=True + ), + ) 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).", + json_schema_extra=ui_field_config(order=3, is_load_param=True, label="VAE"), + ) + height: int = Field( + default=320, + ge=1, + description="Output height in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + width: int = Field( + default=576, + ge=1, + description="Output width in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + base_seed: int = Field( + default=42, + ge=0, + description="Base random seed for reproducible generation", + json_schema_extra=ui_field_config(order=5, is_load_param=True, label="Seed"), + ) + manage_cache: bool = Field( + default=True, + description="Enable automatic cache management for performance optimization", + json_schema_extra=ui_field_config( + order=5, component="cache", is_load_param=True + ), + ) + denoising_steps: list[int] = Field( + default=[1000, 750, 500, 250], + description="Denoising step schedule for progressive generation", + json_schema_extra=ui_field_config( + order=6, component="denoising_steps", is_load_param=True + ), + ) + noise_scale: float = Field( + default=0.7, + ge=0.0, + le=1.0, + description="Amount of noise to add during video generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + noise_controller: bool = Field( + default=True, + description="Enable dynamic noise control during generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + quantization: Quantization | None = Field( + default=None, + description="Quantization method for the diffusion model.", + json_schema_extra=ui_field_config( + order=8, component="quantization", is_load_param=True + ), ) modes = { diff --git a/src/scope/core/pipelines/reward_forcing/pipeline.py b/src/scope/core/pipelines/reward_forcing/pipeline.py index c6a0214de..f36263f55 100644 --- a/src/scope/core/pipelines/reward_forcing/pipeline.py +++ b/src/scope/core/pipelines/reward_forcing/pipeline.py @@ -158,7 +158,7 @@ def __init__( self.state.set("height", config.height) self.state.set("width", config.width) - self.state.set("base_seed", getattr(config, "seed", 42)) + self.state.set("base_seed", getattr(config, "base_seed", 42)) self.first_call = True self.last_mode = None # Track mode for transition detection diff --git a/src/scope/core/pipelines/reward_forcing/schema.py b/src/scope/core/pipelines/reward_forcing/schema.py index 22c28a3ba..8b2148bc1 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, ui_field_config from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -10,6 +10,7 @@ VACE_ARTIFACT, WAN_1_3B_ARTIFACT, ) +from ..enums import Quantization from ..utils import VaeType @@ -42,12 +43,85 @@ class RewardForcingConfig(BasePipelineConfig): min_dimension = 16 modified = True - height: int = 320 - width: int = 576 - denoising_steps: list[int] = [1000, 750, 500, 250] + vace_context_scale: float = Field( + default=1.0, + ge=0.0, + le=2.0, + description="Scaling factor for VACE hint injection (0.0 to 2.0)", + json_schema_extra=ui_field_config( + order=1, component="vace", is_load_param=True + ), + ) + lora_merge_strategy: str = Field( + default="permanent_merge", + description="LoRA merge strategy", + json_schema_extra=ui_field_config( + order=2, component="lora", is_load_param=True + ), + ) 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).", + json_schema_extra=ui_field_config(order=3, is_load_param=True, label="VAE"), + ) + height: int = Field( + default=320, + ge=1, + description="Output height in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + width: int = Field( + default=576, + ge=1, + description="Output width in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + base_seed: int = Field( + default=42, + ge=0, + description="Base random seed for reproducible generation", + json_schema_extra=ui_field_config(order=5, is_load_param=True, label="Seed"), + ) + manage_cache: bool = Field( + default=True, + description="Enable automatic cache management for performance optimization", + json_schema_extra=ui_field_config( + order=5, component="cache", is_load_param=True + ), + ) + denoising_steps: list[int] = Field( + default=[1000, 750, 500, 250], + description="Denoising step schedule for progressive generation", + json_schema_extra=ui_field_config( + order=6, component="denoising_steps", is_load_param=True + ), + ) + noise_scale: float = Field( + default=0.7, + ge=0.0, + le=1.0, + description="Amount of noise to add during video generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + noise_controller: bool = Field( + default=True, + description="Enable dynamic noise control during generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + quantization: Quantization | None = Field( + default=None, + description="Quantization method for the diffusion model.", + json_schema_extra=ui_field_config( + order=8, component="quantization", is_load_param=True + ), ) modes = { diff --git a/src/scope/core/pipelines/streamdiffusionv2/pipeline.py b/src/scope/core/pipelines/streamdiffusionv2/pipeline.py index f594e2e54..5c4bfb46b 100644 --- a/src/scope/core/pipelines/streamdiffusionv2/pipeline.py +++ b/src/scope/core/pipelines/streamdiffusionv2/pipeline.py @@ -162,7 +162,7 @@ def __init__( self.state.set("height", config.height) self.state.set("width", config.width) - self.state.set("base_seed", getattr(config, "seed", 42)) + self.state.set("base_seed", getattr(config, "base_seed", 42)) self.first_call = True self.last_mode = None # Track mode for transition detection diff --git a/src/scope/core/pipelines/streamdiffusionv2/schema.py b/src/scope/core/pipelines/streamdiffusionv2/schema.py index e6b1ed0a8..2c8612fbd 100644 --- a/src/scope/core/pipelines/streamdiffusionv2/schema.py +++ b/src/scope/core/pipelines/streamdiffusionv2/schema.py @@ -1,7 +1,12 @@ from pydantic import Field from ..artifacts import HuggingfaceRepoArtifact -from ..base_schema import BasePipelineConfig, ModeDefaults +from ..base_schema import ( + BasePipelineConfig, + ModeDefaults, + input_size_field, + ui_field_config, +) from ..common_artifacts import ( LIGHTTAE_ARTIFACT, LIGHTVAE_ARTIFACT, @@ -10,6 +15,7 @@ VACE_ARTIFACT, WAN_1_3B_ARTIFACT, ) +from ..enums import Quantization from ..utils import VaeType @@ -43,14 +49,87 @@ class StreamDiffusionV2Config(BasePipelineConfig): min_dimension = 16 modified = True - denoising_steps: list[int] = [750, 250] - noise_scale: float = 0.7 - noise_controller: bool = True - input_size: int = 4 + vace_context_scale: float = Field( + default=1.0, + ge=0.0, + le=2.0, + description="Scaling factor for VACE hint injection (0.0 to 2.0)", + json_schema_extra=ui_field_config( + order=1, component="vace", is_load_param=True + ), + ) + lora_merge_strategy: str = Field( + default="permanent_merge", + description="LoRA merge strategy", + json_schema_extra=ui_field_config( + order=2, component="lora", is_load_param=True + ), + ) 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).", + json_schema_extra=ui_field_config(order=3, is_load_param=True, label="VAE"), + ) + height: int = Field( + default=512, + ge=1, + description="Output height in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + width: int = Field( + default=512, + ge=1, + description="Output width in pixels", + json_schema_extra=ui_field_config( + order=4, component="resolution", is_load_param=True + ), + ) + base_seed: int = Field( + default=42, + ge=0, + description="Base random seed for reproducible generation", + json_schema_extra=ui_field_config(order=5, is_load_param=True, label="Seed"), + ) + manage_cache: bool = Field( + default=True, + description="Enable automatic cache management for performance optimization", + json_schema_extra=ui_field_config( + order=5, component="cache", is_load_param=True + ), + ) + denoising_steps: list[int] = Field( + default=[750, 250], + description="Denoising step schedule for progressive generation", + json_schema_extra=ui_field_config( + order=6, component="denoising_steps", is_load_param=True + ), + ) + noise_scale: float = Field( + default=0.7, + ge=0.0, + le=1.0, + description="Amount of noise to add during video generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + noise_controller: bool = Field( + default=True, + description="Enable dynamic noise control during generation (video mode only)", + json_schema_extra=ui_field_config( + order=7, component="noise", modes=["video"], is_load_param=True + ), + ) + quantization: Quantization | None = Field( + default=None, + description="Quantization method for the diffusion model.", + json_schema_extra=ui_field_config( + order=8, component="quantization", is_load_param=True + ), ) + input_size: int = input_size_field(default=4) modes = { "text": ModeDefaults( diff --git a/src/scope/server/app.py b/src/scope/server/app.py index 22f4e02b0..7414ca3dd 100644 --- a/src/scope/server/app.py +++ b/src/scope/server/app.py @@ -433,7 +433,7 @@ async def load_pipeline( # Convert pydantic model to dict for pipeline manager load_params_dict = None if request.load_params: - load_params_dict = request.load_params.model_dump() + load_params_dict = request.load_params # Get pipeline IDs to load pipeline_ids = request.pipeline_ids diff --git a/src/scope/server/pipeline_manager.py b/src/scope/server/pipeline_manager.py index 969bf05a2..8faa9e273 100644 --- a/src/scope/server/pipeline_manager.py +++ b/src/scope/server/pipeline_manager.py @@ -166,6 +166,7 @@ def _load_pipeline_by_id_sync( # Release lock during slow loading operation logger.info(f"Loading pipeline: {pipeline_id}") + logger.info("Initial load params: %s", load_params or {}) try: # Load the pipeline synchronously @@ -420,18 +421,18 @@ def _apply_load_params( default_width: int, default_seed: int = 42, ) -> None: - """Extract and apply common load parameters (resolution, seed, LoRAs, VAE type) to config. + """Extract and apply common load parameters Args: config: Pipeline config dict to update - load_params: Load parameters dict (may contain height, width, seed, loras, lora_merge_mode, vae_type) + load_params: Load parameters dict (may contain height, width, base_seed, loras, lora_merge_mode, vae_type) default_height: Default height if not in load_params default_width: Default width if not in load_params - default_seed: Default seed if not in load_params + default_seed: Default base_seed if not in load_params """ height = default_height width = default_width - seed = default_seed + base_seed = default_seed loras = None lora_merge_mode = "permanent_merge" vae_type = "wan" # Default VAE type @@ -439,14 +440,14 @@ def _apply_load_params( if load_params: height = load_params.get("height", default_height) width = load_params.get("width", default_width) - seed = load_params.get("seed", default_seed) + base_seed = load_params.get("base_seed", default_seed) loras = load_params.get("loras", None) lora_merge_mode = load_params.get("lora_merge_mode", lora_merge_mode) vae_type = load_params.get("vae_type", vae_type) config["height"] = height config["width"] = width - config["seed"] = seed + config["base_seed"] = base_seed config["vae_type"] = vae_type if loras: config["loras"] = loras @@ -584,7 +585,6 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -675,7 +675,6 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -741,7 +740,6 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -805,7 +803,6 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -861,7 +858,6 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -942,7 +938,6 @@ def _load_pipeline_implementation( # Create minimal config - RIFE pipeline handles its own model paths via artifacts config = OmegaConf.create({}) - # Apply load parameters (resolution, seed) to config for consistency # Note: RIFE doesn't use these parameters but we apply them for consistency self._apply_load_params( config, diff --git a/src/scope/server/schema.py b/src/scope/server/schema.py index b031dcfad..b695871c3 100644 --- a/src/scope/server/schema.py +++ b/src/scope/server/schema.py @@ -1,7 +1,7 @@ """Pydantic schemas for FastAPI application.""" from enum import Enum -from typing import Literal +from typing import Any, Literal from pydantic import BaseModel, Field @@ -334,9 +334,9 @@ class StreamDiffusionV2LoadParams(LoRAEnabledLoadParams): ge=64, le=2048, ) - seed: int = Field( + base_seed: int = Field( default=_DEFAULT_SEED, - description="Random seed for generation", + description="Base random seed for reproducible generation", ge=0, ) quantization: Quantization | None = Field( @@ -377,9 +377,9 @@ class LongLiveLoadParams(LoRAEnabledLoadParams): ge=16, le=2048, ) - seed: int = Field( + base_seed: int = Field( default=_DEFAULT_SEED, - description="Random seed for generation", + description="Base random seed for reproducible generation", ge=0, ) quantization: Quantization | None = Field( @@ -414,9 +414,9 @@ class KreaRealtimeVideoLoadParams(LoRAEnabledLoadParams): ge=64, le=2048, ) - seed: int = Field( + base_seed: int = Field( default=_DEFAULT_SEED, - description="Random seed for generation", + description="Base random seed for reproducible generation", ge=0, ) quantization: Quantization | None = Field( @@ -437,15 +437,10 @@ class PipelineLoadRequest(BaseModel): """Pipeline load request schema.""" pipeline_ids: list[str] = Field(..., description="List of pipeline IDs to load") - load_params: ( - StreamDiffusionV2LoadParams - | PassthroughLoadParams - | LongLiveLoadParams - | KreaRealtimeVideoLoadParams - | None - ) = Field( + load_params: dict[str, Any] | None = Field( default=None, - description="Pipeline-specific load parameters (applies to all pipelines)", + description="Pipeline-specific load parameters (applies to all pipelines). " + "Accepts raw dict; keys match pipeline config (e.g. base_seed).", )