From 294f2a6aa60fddbc6cccadaa64e83d351fdda4f3 Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 13:24:45 +0000 Subject: [PATCH 01/13] Add Dynamic Parameter Rendering Signed-off-by: Rafal Leszko Signed-off-by: Rafal Leszko --- .../components/DynamicParameterControls.tsx | 104 ++++++++ .../dynamic-controls/DynamicControl.tsx | 132 ++++++++++ .../dynamic-controls/NumberControl.tsx | 113 +++++++++ .../dynamic-controls/SelectControl.tsx | 44 ++++ .../dynamic-controls/SliderControl.tsx | 74 ++++++ .../dynamic-controls/TextControl.tsx | 32 +++ .../dynamic-controls/ToggleControl.tsx | 35 +++ .../src/components/dynamic-controls/index.ts | 19 ++ .../src/components/dynamic-controls/types.ts | 53 ++++ frontend/src/hooks/useDynamicParameters.ts | 126 ++++++++++ frontend/src/lib/schemaInference.ts | 230 ++++++++++++++++++ 11 files changed, 962 insertions(+) create mode 100644 frontend/src/components/DynamicParameterControls.tsx create mode 100644 frontend/src/components/dynamic-controls/DynamicControl.tsx create mode 100644 frontend/src/components/dynamic-controls/NumberControl.tsx create mode 100644 frontend/src/components/dynamic-controls/SelectControl.tsx create mode 100644 frontend/src/components/dynamic-controls/SliderControl.tsx create mode 100644 frontend/src/components/dynamic-controls/TextControl.tsx create mode 100644 frontend/src/components/dynamic-controls/ToggleControl.tsx create mode 100644 frontend/src/components/dynamic-controls/index.ts create mode 100644 frontend/src/components/dynamic-controls/types.ts create mode 100644 frontend/src/hooks/useDynamicParameters.ts create mode 100644 frontend/src/lib/schemaInference.ts 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/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/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/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; +} From 431b980504e891a6a55db534c4e6300dcf62d0c4 Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:09:21 +0000 Subject: [PATCH 02/13] Add Dynamic Parameter Rendering Signed-off-by: Rafal Leszko --- frontend/src/components/SettingsPanel.tsx | 764 ++------------ .../src/components/SettingsPanelRenderer.tsx | 969 ++++++++++++++++++ frontend/src/hooks/usePipelines.ts | 22 + frontend/src/lib/api.ts | 18 + frontend/src/pages/StreamPage.tsx | 4 +- frontend/src/types/index.ts | 5 + src/scope/core/pipelines/base_schema.py | 58 +- .../pipelines/krea_realtime_video/schema.py | 32 +- src/scope/core/pipelines/longlive/schema.py | 28 +- src/scope/core/pipelines/memflow/schema.py | 30 +- .../core/pipelines/reward_forcing/schema.py | 30 +- .../pipelines/streamdiffusionv2/schema.py | 36 +- 12 files changed, 1317 insertions(+), 679 deletions(-) create mode 100644 frontend/src/components/SettingsPanelRenderer.tsx diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index cf95b3dc..b9ff508a 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; @@ -107,6 +92,7 @@ export function SettingsPanel({ onPipelineIdChange, isStreaming = false, isLoading = false, + schema, resolution, onResolutionChange, seed = 42, @@ -129,7 +115,6 @@ export function SettingsPanel({ onLorasChange, loraMergeStrategy = "permanent_merge", inputMode, - supportsNoiseControls = false, spoutSender, onSpoutSenderChange, spoutAvailable = false, @@ -145,120 +130,57 @@ export function SettingsPanel({ preprocessorIds = [], onPreprocessorIdsChange, }: 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 +281,53 @@ 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..c04ec23a --- /dev/null +++ b/frontend/src/components/SettingsPanelRenderer.tsx @@ -0,0 +1,969 @@ +/** + * 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, +} 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; +} + +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, +}: 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": + if (!schema.supports_cache_management) return null; + 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) return null; + + const property = configSchema.properties[fieldName]; + if (!property) { + // Handle special field names that may not be in schema but are standard + if (fieldName === "height") { + return renderHeightControl(index); + } + if (fieldName === "width") { + return renderWidthControl(index); + } + if (fieldName === "base_seed") { + return renderSeedControl(index); + } + if (fieldName === "quantization") { + return renderQuantizationControl(index); + } + if ( + fieldName === "kv_cache_attention_bias" && + schema.supports_kv_cache_bias + ) { + return renderKvCacheBiasControl(index); + } + console.warn( + `[SettingsPanelRenderer] Unknown field name: "${fieldName}"` + ); + 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); + if (value === undefined || onChange === undefined) { + console.warn( + `[SettingsPanelRenderer] No handler for field "${fieldName}"` + ); + return null; + } + + return ( + onChange(val)} + disabled={isStreaming || isLoading} + /> + ); + }; + + // Get value and onChange handler for a field + const getFieldValueAndHandler = ( + fieldName: string + ): { 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), + }; + default: + return { value: undefined, onChange: undefined }; + } + }; + + // Render height control + const renderHeightControl = (index: number) => ( +
+
+ +
+ + { + 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 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 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; + return ( +
+
+ + +
+
+ ); + }; + + // Render quantization control + const renderQuantizationControl = (index: number) => { + if (!schema.supports_quantization) return null; + return ( +
+
+
+ + +
+ {vaceEnabled && ( +

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

+ )} +
+
+ ); + }; + + // Render KV cache bias control + const renderKvCacheBiasControl = (index: number) => ( + parseFloat(v) || 1.0} + /> + ); + + return ( +
+ {settingsPanel.map((item, index) => renderItem(item, index))} +
+ ); +} diff --git a/frontend/src/hooks/usePipelines.ts b/frontend/src/hooks/usePipelines.ts index 128d78ef..df34db8c 100644 --- a/frontend/src/hooks/usePipelines.ts +++ b/frontend/src/hooks/usePipelines.ts @@ -44,6 +44,23 @@ 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; + transformed[id] = { name: schema.name, about: schema.description, @@ -70,6 +87,11 @@ export function usePipelines() { vaeTypes, supportsControllerInput, supportsImages, + settingsPanel, + modeSettingsPanels: + modeSettingsPanels && Object.keys(modeSettingsPanels).length > 0 + ? modeSettingsPanels + : undefined, }; } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0a126b0e..a256ac0f 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 { @@ -409,6 +425,8 @@ export interface PipelineSchemaInfo { 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/pages/StreamPage.tsx b/frontend/src/pages/StreamPage.tsx index 300ca35f..450ac06b 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 @@ -1218,6 +1218,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 +1259,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} 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..b2e6637c 100644 --- a/src/scope/core/pipelines/base_schema.py +++ b/src/scope/core/pipelines/base_schema.py @@ -102,6 +102,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 +168,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 +228,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"]] = ( @@ -220,7 +259,7 @@ class BasePipelineConfig(BaseModel): 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() @@ -322,10 +361,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..4c26f6c9 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, @@ -59,6 +59,21 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) + # 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", + ] + modes = { "text": ModeDefaults(default=True), "video": ModeDefaults( @@ -68,5 +83,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..9323bb3e 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, @@ -51,6 +51,19 @@ class LongLiveConfig(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) + # 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", + ] + modes = { "text": ModeDefaults(default=True), "video": ModeDefaults( @@ -59,5 +72,18 @@ class LongLiveConfig(BasePipelineConfig): 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..c6c50071 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, @@ -51,6 +51,20 @@ class MemFlowConfig(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) + # 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", + ] + modes = { "text": ModeDefaults(default=True), "video": ModeDefaults( @@ -59,5 +73,19 @@ class MemFlowConfig(BasePipelineConfig): 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..21e573b9 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, @@ -50,6 +50,20 @@ class RewardForcingConfig(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) + # 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", + ] + modes = { "text": ModeDefaults(default=True), "video": ModeDefaults( @@ -58,5 +72,19 @@ class RewardForcingConfig(BasePipelineConfig): 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..6da4dcde 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, @@ -52,11 +52,43 @@ class StreamDiffusionV2Config(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) + # 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", + ] + modes = { "text": ModeDefaults( height=512, width=512, denoising_steps=[1000, 750], ), - "video": ModeDefaults(default=True), + "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", + ], + ), } From af79a2cdbcbde1be594aa6c3f766fd6fed374bee Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:11:39 +0000 Subject: [PATCH 03/13] Update Signed-off-by: Rafal Leszko --- .../pipelines/krea_realtime_video/schema.py | 33 ++++++++++--------- src/scope/core/pipelines/longlive/schema.py | 29 ++++++++-------- src/scope/core/pipelines/memflow/schema.py | 31 ++++++++--------- .../core/pipelines/reward_forcing/schema.py | 31 ++++++++--------- .../pipelines/streamdiffusionv2/schema.py | 27 ++++++++------- 5 files changed, 77 insertions(+), 74 deletions(-) diff --git a/src/scope/core/pipelines/krea_realtime_video/schema.py b/src/scope/core/pipelines/krea_realtime_video/schema.py index 4c26f6c9..6ee3d26e 100644 --- a/src/scope/core/pipelines/krea_realtime_video/schema.py +++ b/src/scope/core/pipelines/krea_realtime_video/schema.py @@ -59,23 +59,24 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) - # 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", - ] - 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, diff --git a/src/scope/core/pipelines/longlive/schema.py b/src/scope/core/pipelines/longlive/schema.py index 9323bb3e..bcd84402 100644 --- a/src/scope/core/pipelines/longlive/schema.py +++ b/src/scope/core/pipelines/longlive/schema.py @@ -51,21 +51,22 @@ class LongLiveConfig(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) - # 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", - ] - 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, diff --git a/src/scope/core/pipelines/memflow/schema.py b/src/scope/core/pipelines/memflow/schema.py index c6c50071..5e6ad928 100644 --- a/src/scope/core/pipelines/memflow/schema.py +++ b/src/scope/core/pipelines/memflow/schema.py @@ -51,22 +51,23 @@ class MemFlowConfig(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) - # 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", - ] - 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, diff --git a/src/scope/core/pipelines/reward_forcing/schema.py b/src/scope/core/pipelines/reward_forcing/schema.py index 21e573b9..36aadcb9 100644 --- a/src/scope/core/pipelines/reward_forcing/schema.py +++ b/src/scope/core/pipelines/reward_forcing/schema.py @@ -50,22 +50,23 @@ class RewardForcingConfig(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) - # 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", - ] - 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, diff --git a/src/scope/core/pipelines/streamdiffusionv2/schema.py b/src/scope/core/pipelines/streamdiffusionv2/schema.py index 6da4dcde..80078323 100644 --- a/src/scope/core/pipelines/streamdiffusionv2/schema.py +++ b/src/scope/core/pipelines/streamdiffusionv2/schema.py @@ -52,25 +52,24 @@ class StreamDiffusionV2Config(BasePipelineConfig): description="VAE type to use. 'wan' is the full VAE, 'lightvae' is 75% pruned (faster but lower quality).", ) - # 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", - ] - modes = { "text": ModeDefaults( 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, From 6f6cf5c057cdfae7e150079f82a0cc8682f86a9d Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:19:03 +0000 Subject: [PATCH 04/13] Update Signed-off-by: Rafal Leszko --- .../src/components/SettingsPanelRenderer.tsx | 363 ++++++++++-------- frontend/src/data/parameterMetadata.ts | 30 -- frontend/src/hooks/useStreamState.ts | 2 +- src/scope/core/pipelines/base_schema.py | 29 +- src/scope/core/pipelines/defaults.py | 2 +- src/scope/core/pipelines/helpers.py | 2 +- .../pipelines/krea_realtime_video/pipeline.py | 2 +- .../pipelines/krea_realtime_video/schema.py | 6 +- src/scope/core/pipelines/longlive/pipeline.py | 2 +- src/scope/core/pipelines/longlive/schema.py | 6 +- src/scope/core/pipelines/memflow/pipeline.py | 2 +- src/scope/core/pipelines/memflow/schema.py | 6 +- .../core/pipelines/reward_forcing/pipeline.py | 2 +- .../core/pipelines/reward_forcing/schema.py | 6 +- .../pipelines/streamdiffusionv2/pipeline.py | 2 +- .../pipelines/streamdiffusionv2/schema.py | 6 +- .../wan2_1/blocks/prepare_latents.py | 10 +- .../wan2_1/blocks/prepare_video_latents.py | 10 +- src/scope/server/schema.py | 6 +- 19 files changed, 259 insertions(+), 235 deletions(-) diff --git a/frontend/src/components/SettingsPanelRenderer.tsx b/frontend/src/components/SettingsPanelRenderer.tsx index c04ec23a..6842c2c4 100644 --- a/frontend/src/components/SettingsPanelRenderer.tsx +++ b/frontend/src/components/SettingsPanelRenderer.tsx @@ -615,7 +615,7 @@ export function SettingsPanelRenderer({ if (fieldName === "width") { return renderWidthControl(index); } - if (fieldName === "base_seed") { + if (fieldName === "seed") { return renderSeedControl(index); } if (fieldName === "quantization") { @@ -695,7 +695,7 @@ export function SettingsPanelRenderer({ onChange: val => onResolutionChange?.({ ...resolution, width: val as number }), }; - case "base_seed": + case "seed": return { value: seed, onChange: val => onSeedChange?.(val as number) }; case "vae_type": return { @@ -707,175 +707,204 @@ export function SettingsPanelRenderer({ } }; + // 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 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} + 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}

+ )}
- {heightError && ( -

{heightError}

- )} -
- ); + ); + }; // Render width control - const renderWidthControl = (index: number) => ( -
-
- -
- - { - 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} + 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} +

+
+ )}
- {widthError &&

{widthError}

} - {resolutionWarning && ( -
- -

- {resolutionWarning} -

-
- )} -
- ); + ); + }; // Render seed control - const renderSeedControl = (index: number) => ( -
-
- -
- - { - 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} + const renderSeedControl = (index: number) => { + const displayInfo = getDisplayInfo("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}

}
- {seedError &&

{seedError}

} -
- ); + ); + }; // Render VAE type control const renderVaeTypeControl = (index: number) => { if (!vaeTypes || vaeTypes.length === 0) return null; + const displayInfo = getDisplayInfo("vae_type"); return (
( - parseFloat(v) || 1.0} - /> - ); + const renderKvCacheBiasControl = (index: number) => { + const displayInfo = getDisplayInfo("kv_cache_attention_bias"); + return ( + parseFloat(v) || 1.0} + /> + ); + }; return (
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/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/src/scope/core/pipelines/base_schema.py b/src/scope/core/pipelines/base_schema.py index b2e6637c..863479b5 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: @@ -252,9 +261,9 @@ class BasePipelineConfig(BaseModel): default=True, description="Enable automatic cache management for performance optimization", ) - base_seed: Annotated[int, Field(ge=0)] = Field( + 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() @@ -267,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. diff --git a/src/scope/core/pipelines/defaults.py b/src/scope/core/pipelines/defaults.py index c4611138..30f28952 100644 --- a/src/scope/core/pipelines/defaults.py +++ b/src/scope/core/pipelines/defaults.py @@ -73,7 +73,7 @@ def extract_load_params( params = load_params or {} height = params.get("height", config.height) width = params.get("width", config.width) - seed = params.get("seed", config.base_seed) + seed = params.get("seed", config.seed) return height, width, seed diff --git a/src/scope/core/pipelines/helpers.py b/src/scope/core/pipelines/helpers.py index efeca6c3..d6e245b9 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("seed", getattr(config, "seed", pipeline_config.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 83dbbace..a88810b1 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("seed", getattr(config, "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 6ee3d26e..db244ef5 100644 --- a/src/scope/core/pipelines/krea_realtime_video/schema.py +++ b/src/scope/core/pipelines/krea_realtime_video/schema.py @@ -56,7 +56,7 @@ 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 = { @@ -70,7 +70,7 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): "vae_type", "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, "kv_cache_attention_bias", SettingsControlType.DENOISING_STEPS, @@ -92,7 +92,7 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): "vae_type", "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, "kv_cache_attention_bias", SettingsControlType.DENOISING_STEPS, diff --git a/src/scope/core/pipelines/longlive/pipeline.py b/src/scope/core/pipelines/longlive/pipeline.py index 7216cc2c..c881f36f 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("seed", getattr(config, "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 bcd84402..a42eb25b 100644 --- a/src/scope/core/pipelines/longlive/schema.py +++ b/src/scope/core/pipelines/longlive/schema.py @@ -48,7 +48,7 @@ class LongLiveConfig(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 = { @@ -61,7 +61,7 @@ class LongLiveConfig(BasePipelineConfig): SettingsControlType.PREPROCESSOR, "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, "quantization", @@ -80,7 +80,7 @@ class LongLiveConfig(BasePipelineConfig): SettingsControlType.PREPROCESSOR, "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, SettingsControlType.NOISE_CONTROLS, diff --git a/src/scope/core/pipelines/memflow/pipeline.py b/src/scope/core/pipelines/memflow/pipeline.py index c8de34c8..89c9434d 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("seed", getattr(config, "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 5e6ad928..0d052fc9 100644 --- a/src/scope/core/pipelines/memflow/schema.py +++ b/src/scope/core/pipelines/memflow/schema.py @@ -48,7 +48,7 @@ class MemFlowConfig(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 = { @@ -62,7 +62,7 @@ class MemFlowConfig(BasePipelineConfig): "vae_type", "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, "quantization", @@ -82,7 +82,7 @@ class MemFlowConfig(BasePipelineConfig): "vae_type", "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, SettingsControlType.NOISE_CONTROLS, diff --git a/src/scope/core/pipelines/reward_forcing/pipeline.py b/src/scope/core/pipelines/reward_forcing/pipeline.py index e8b0f4f8..d937de33 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("seed", getattr(config, "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 36aadcb9..1b66c72e 100644 --- a/src/scope/core/pipelines/reward_forcing/schema.py +++ b/src/scope/core/pipelines/reward_forcing/schema.py @@ -47,7 +47,7 @@ class RewardForcingConfig(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 = { @@ -61,7 +61,7 @@ class RewardForcingConfig(BasePipelineConfig): "vae_type", "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, "quantization", @@ -81,7 +81,7 @@ class RewardForcingConfig(BasePipelineConfig): "vae_type", "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, SettingsControlType.NOISE_CONTROLS, diff --git a/src/scope/core/pipelines/streamdiffusionv2/pipeline.py b/src/scope/core/pipelines/streamdiffusionv2/pipeline.py index 5c2cb89e..4c8aff08 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("seed", getattr(config, "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 80078323..76893e06 100644 --- a/src/scope/core/pipelines/streamdiffusionv2/schema.py +++ b/src/scope/core/pipelines/streamdiffusionv2/schema.py @@ -49,7 +49,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 = { @@ -65,7 +65,7 @@ class StreamDiffusionV2Config(BasePipelineConfig): "vae_type", "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, "quantization", @@ -83,7 +83,7 @@ class StreamDiffusionV2Config(BasePipelineConfig): "vae_type", "height", "width", - "base_seed", + "seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, SettingsControlType.NOISE_CONTROLS, diff --git a/src/scope/core/pipelines/wan2_1/blocks/prepare_latents.py b/src/scope/core/pipelines/wan2_1/blocks/prepare_latents.py index 13c80a26..535a58a9 100644 --- a/src/scope/core/pipelines/wan2_1/blocks/prepare_latents.py +++ b/src/scope/core/pipelines/wan2_1/blocks/prepare_latents.py @@ -37,7 +37,7 @@ def description(self) -> str: def inputs(self) -> list[InputParam]: return [ InputParam( - "base_seed", + "seed", type_hint=int, default=42, description="Base seed for random number generation", @@ -85,12 +85,12 @@ def __call__(self, components, state: PipelineState) -> tuple[Any, PipelineState # The default param for InputParam does not work right now # The workaround is to set the default values here - base_seed = block_state.base_seed - if base_seed is None: - base_seed = 42 + seed = block_state.seed + if seed is None: + seed = 42 # Create generator from seed for reproducible generation - block_seed = base_seed + block_state.current_start_frame + block_seed = seed + block_state.current_start_frame rng = torch.Generator(device=generator_param.device).manual_seed(block_seed) # Determine number of latent frames to generate diff --git a/src/scope/core/pipelines/wan2_1/blocks/prepare_video_latents.py b/src/scope/core/pipelines/wan2_1/blocks/prepare_video_latents.py index ad6ba024..12cb86bd 100644 --- a/src/scope/core/pipelines/wan2_1/blocks/prepare_video_latents.py +++ b/src/scope/core/pipelines/wan2_1/blocks/prepare_video_latents.py @@ -43,7 +43,7 @@ def inputs(self) -> list[InputParam]: description="Input video to convert into noisy latents", ), InputParam( - "base_seed", + "seed", type_hint=int, default=42, description="Base seed for random number generation", @@ -83,12 +83,12 @@ def __call__(self, components, state: PipelineState) -> tuple[Any, PipelineState # The default param for InputParam does not work right now # The workaround is to set the default values here - base_seed = block_state.base_seed - if base_seed is None: - base_seed = 42 + seed = block_state.seed + if seed is None: + seed = 42 # Create generator from seed for reproducible generation - block_seed = base_seed + block_state.current_start_frame + block_seed = seed + block_state.current_start_frame rng = torch.Generator(device=components.config.device).manual_seed(block_seed) # Generate empty latents (noise) diff --git a/src/scope/server/schema.py b/src/scope/server/schema.py index 189ce260..96afeb6d 100644 --- a/src/scope/server/schema.py +++ b/src/scope/server/schema.py @@ -10,9 +10,9 @@ # Default values for pipeline load params (duplicated from pipeline configs to avoid # importing torch-dependent modules). These should match the defaults in: -# - StreamDiffusionV2Config: height=512, width=512, base_seed=42 -# - LongLiveConfig: height=320, width=576, base_seed=42 -# - KreaRealtimeVideoConfig: height=320, width=576, base_seed=42 +# - StreamDiffusionV2Config: height=512, width=512, seed=42 +# - LongLiveConfig: height=320, width=576, seed=42 +# - KreaRealtimeVideoConfig: height=320, width=576, seed=42 _STREAMDIFFUSIONV2_HEIGHT = 512 _STREAMDIFFUSIONV2_WIDTH = 512 _LONGLIVE_HEIGHT = 320 From 74f313e57561c9f15ddf9e5aa58786a1544f0218 Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:24:18 +0000 Subject: [PATCH 05/13] Update Signed-off-by: Rafal Leszko --- .../src/components/SettingsPanelRenderer.tsx | 7 +----- frontend/src/hooks/usePipelines.ts | 25 ++++++++++++++++--- frontend/src/lib/api.ts | 3 --- frontend/src/pages/StreamPage.tsx | 15 ++++++++--- src/scope/core/pipelines/base_schema.py | 3 --- .../pipelines/krea_realtime_video/schema.py | 3 --- src/scope/core/pipelines/longlive/schema.py | 2 -- src/scope/core/pipelines/memflow/schema.py | 2 -- .../core/pipelines/reward_forcing/schema.py | 2 -- .../pipelines/streamdiffusionv2/schema.py | 2 -- 10 files changed, 34 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/SettingsPanelRenderer.tsx b/frontend/src/components/SettingsPanelRenderer.tsx index 6842c2c4..9f635232 100644 --- a/frontend/src/components/SettingsPanelRenderer.tsx +++ b/frontend/src/components/SettingsPanelRenderer.tsx @@ -451,7 +451,6 @@ export function SettingsPanelRenderer({ ); case "cache_management": - if (!schema.supports_cache_management) return null; return (
@@ -621,10 +620,7 @@ export function SettingsPanelRenderer({ if (fieldName === "quantization") { return renderQuantizationControl(index); } - if ( - fieldName === "kv_cache_attention_bias" && - schema.supports_kv_cache_bias - ) { + if (fieldName === "kv_cache_attention_bias") { return renderKvCacheBiasControl(index); } console.warn( @@ -932,7 +928,6 @@ export function SettingsPanelRenderer({ // Render quantization control const renderQuantizationControl = (index: number) => { - if (!schema.supports_quantization) return null; const displayInfo = getDisplayInfo("quantization"); return (
diff --git a/frontend/src/hooks/usePipelines.ts b/frontend/src/hooks/usePipelines.ts index df34db8c..2d81ea75 100644 --- a/frontend/src/hooks/usePipelines.ts +++ b/frontend/src/hooks/usePipelines.ts @@ -61,6 +61,23 @@ export function usePipelines() { ) : 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, @@ -77,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, diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index a256ac0f..b7365f01 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -419,9 +419,6 @@ 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; diff --git a/frontend/src/pages/StreamPage.tsx b/frontend/src/pages/StreamPage.tsx index 450ac06b..a3a7b753 100644 --- a/frontend/src/pages/StreamPage.tsx +++ b/frontend/src/pages/StreamPage.tsx @@ -934,13 +934,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; } diff --git a/src/scope/core/pipelines/base_schema.py b/src/scope/core/pipelines/base_schema.py index 863479b5..bc70097d 100644 --- a/src/scope/core/pipelines/base_schema.py +++ b/src/scope/core/pipelines/base_schema.py @@ -371,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 diff --git a/src/scope/core/pipelines/krea_realtime_video/schema.py b/src/scope/core/pipelines/krea_realtime_video/schema.py index db244ef5..cc893a27 100644 --- a/src/scope/core/pipelines/krea_realtime_video/schema.py +++ b/src/scope/core/pipelines/krea_realtime_video/schema.py @@ -41,9 +41,6 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): ), ] - supports_cache_management = True - supports_kv_cache_bias = True - supports_quantization = True min_dimension = 16 modified = True recommended_quantization_vram_threshold = 40.0 diff --git a/src/scope/core/pipelines/longlive/schema.py b/src/scope/core/pipelines/longlive/schema.py index a42eb25b..ace8e156 100644 --- a/src/scope/core/pipelines/longlive/schema.py +++ b/src/scope/core/pipelines/longlive/schema.py @@ -38,8 +38,6 @@ class LongLiveConfig(BasePipelineConfig): ), ] - supports_cache_management = True - supports_quantization = True min_dimension = 16 modified = True diff --git a/src/scope/core/pipelines/memflow/schema.py b/src/scope/core/pipelines/memflow/schema.py index 0d052fc9..eb9e0e71 100644 --- a/src/scope/core/pipelines/memflow/schema.py +++ b/src/scope/core/pipelines/memflow/schema.py @@ -38,8 +38,6 @@ class MemFlowConfig(BasePipelineConfig): ), ] - supports_cache_management = True - supports_quantization = True min_dimension = 16 modified = True diff --git a/src/scope/core/pipelines/reward_forcing/schema.py b/src/scope/core/pipelines/reward_forcing/schema.py index 1b66c72e..45cf13ef 100644 --- a/src/scope/core/pipelines/reward_forcing/schema.py +++ b/src/scope/core/pipelines/reward_forcing/schema.py @@ -37,8 +37,6 @@ class RewardForcingConfig(BasePipelineConfig): ), ] - supports_cache_management = True - supports_quantization = True min_dimension = 16 modified = True diff --git a/src/scope/core/pipelines/streamdiffusionv2/schema.py b/src/scope/core/pipelines/streamdiffusionv2/schema.py index 76893e06..bc16b119 100644 --- a/src/scope/core/pipelines/streamdiffusionv2/schema.py +++ b/src/scope/core/pipelines/streamdiffusionv2/schema.py @@ -38,8 +38,6 @@ class StreamDiffusionV2Config(BasePipelineConfig): ), ] - supports_cache_management = True - supports_quantization = True min_dimension = 16 modified = True From 76f71a72a2bcda4a59c2c15c5275db5738956c83 Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:44:22 +0000 Subject: [PATCH 06/13] Update Signed-off-by: Rafal Leszko --- frontend/src/components/SettingsPanel.tsx | 18 ++++ .../src/components/SettingsPanelRenderer.tsx | 80 +++++++++++----- frontend/src/pages/StreamPage.tsx | 93 ++++++++++++++++++- src/scope/core/pipelines/base_schema.py | 2 +- src/scope/core/pipelines/defaults.py | 2 +- src/scope/core/pipelines/helpers.py | 2 +- .../pipelines/krea_realtime_video/pipeline.py | 2 +- .../pipelines/krea_realtime_video/schema.py | 5 +- src/scope/core/pipelines/longlive/pipeline.py | 2 +- src/scope/core/pipelines/longlive/schema.py | 13 ++- src/scope/core/pipelines/memflow/pipeline.py | 2 +- src/scope/core/pipelines/memflow/schema.py | 5 +- .../core/pipelines/reward_forcing/pipeline.py | 2 +- .../core/pipelines/reward_forcing/schema.py | 5 +- .../pipelines/streamdiffusionv2/pipeline.py | 2 +- .../pipelines/streamdiffusionv2/schema.py | 5 +- src/scope/server/pipeline_manager.py | 6 +- src/scope/server/schema.py | 12 +-- 18 files changed, 209 insertions(+), 49 deletions(-) diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index b9ff508a..316932e8 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -83,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({ @@ -129,6 +143,8 @@ export function SettingsPanel({ vaeTypes, preprocessorIds = [], onPreprocessorIdsChange, + configValues, + onConfigValueChange, }: SettingsPanelProps) { const handlePipelineIdChange = (value: string) => { if (pipelines && value in pipelines) { @@ -326,6 +342,8 @@ export function SettingsPanel({ vaeTypes={vaeTypes} quantization={quantization} onQuantizationChange={onQuantizationChange} + configValues={configValues} + onConfigValueChange={onConfigValueChange} /> )} diff --git a/frontend/src/components/SettingsPanelRenderer.tsx b/frontend/src/components/SettingsPanelRenderer.tsx index 9f635232..1eace778 100644 --- a/frontend/src/components/SettingsPanelRenderer.tsx +++ b/frontend/src/components/SettingsPanelRenderer.tsx @@ -21,6 +21,7 @@ import type { SettingsPanelItem, SettingsControlType, PipelineSchemaInfo, + PipelineSchemaProperty, } from "../lib/api"; import type { LoRAConfig, @@ -151,6 +152,20 @@ export interface SettingsPanelRendererProps { // 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({ @@ -196,6 +211,8 @@ export function SettingsPanelRenderer({ vaeTypes, quantization, onQuantizationChange, + configValues, + onConfigValueChange, }: SettingsPanelRendererProps) { // Local slider state management hooks const noiseScaleSlider = useLocalSliderValue(noiseScale, onNoiseScaleChange); @@ -605,24 +622,25 @@ export function SettingsPanelRenderer({ const configSchema = schema.config_schema; if (!configSchema?.properties) 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) { - // Handle special field names that may not be in schema but are standard - if (fieldName === "height") { - return renderHeightControl(index); - } - if (fieldName === "width") { - return renderWidthControl(index); - } - if (fieldName === "seed") { - return renderSeedControl(index); - } - if (fieldName === "quantization") { - return renderQuantizationControl(index); - } - if (fieldName === "kv_cache_attention_bias") { - return renderKvCacheBiasControl(index); - } console.warn( `[SettingsPanelRenderer] Unknown field name: "${fieldName}"` ); @@ -647,8 +665,8 @@ export function SettingsPanelRenderer({ } // Get current value and onChange handler based on field name - const { value, onChange } = getFieldValueAndHandler(fieldName); - if (value === undefined || onChange === undefined) { + const { value, onChange } = getFieldValueAndHandler(fieldName, property); + if (onChange === undefined) { console.warn( `[SettingsPanelRenderer] No handler for field "${fieldName}"` ); @@ -674,7 +692,8 @@ export function SettingsPanelRenderer({ // Get value and onChange handler for a field const getFieldValueAndHandler = ( - fieldName: string + 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 @@ -691,14 +710,33 @@ export function SettingsPanelRenderer({ onChange: val => onResolutionChange?.({ ...resolution, width: val as number }), }; - case "seed": + 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) { + return configValues[fieldName]; + } + // 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), + }; + } return { value: undefined, onChange: undefined }; } }; @@ -840,7 +878,7 @@ export function SettingsPanelRenderer({ // Render seed control const renderSeedControl = (index: number) => { - const displayInfo = getDisplayInfo("seed"); + const displayInfo = getDisplayInfo("base_seed"); return (
diff --git a/frontend/src/pages/StreamPage.tsx b/frontend/src/pages/StreamPage.tsx index a3a7b753..b0682694 100644 --- a/frontend/src/pages/StreamPage.tsx +++ b/frontend/src/pages/StreamPage.tsx @@ -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,67 @@ 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) return; + + const newDynamicValues: Record = {}; + const properties = currentSchema.config_schema.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; + } + } + + if (Object.keys(newDynamicValues).length > 0) { + 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 +929,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 +955,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 @@ -1284,6 +1368,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/src/scope/core/pipelines/base_schema.py b/src/scope/core/pipelines/base_schema.py index bc70097d..4e2f0a03 100644 --- a/src/scope/core/pipelines/base_schema.py +++ b/src/scope/core/pipelines/base_schema.py @@ -261,7 +261,7 @@ class BasePipelineConfig(BaseModel): default=True, description="Enable automatic cache management for performance optimization", ) - seed: Annotated[int, Field(ge=0)] = Field( + base_seed: Annotated[int, Field(ge=0)] = Field( default=42, description="Random seed for reproducible generation. Using the same seed with the same settings will produce similar results.", ) diff --git a/src/scope/core/pipelines/defaults.py b/src/scope/core/pipelines/defaults.py index 30f28952..c4611138 100644 --- a/src/scope/core/pipelines/defaults.py +++ b/src/scope/core/pipelines/defaults.py @@ -73,7 +73,7 @@ def extract_load_params( params = load_params or {} height = params.get("height", config.height) width = params.get("width", config.width) - seed = params.get("seed", config.seed) + seed = params.get("seed", config.base_seed) return height, width, seed diff --git a/src/scope/core/pipelines/helpers.py b/src/scope/core/pipelines/helpers.py index d6e245b9..327e5a7d 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("seed", getattr(config, "seed", pipeline_config.seed)) + state.set("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 a88810b1..3f2ad738 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("seed", getattr(config, "seed", 42)) + self.state.set("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 cc893a27..8462b5d3 100644 --- a/src/scope/core/pipelines/krea_realtime_video/schema.py +++ b/src/scope/core/pipelines/krea_realtime_video/schema.py @@ -43,6 +43,7 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): min_dimension = 16 modified = True + supports_quantization = True recommended_quantization_vram_threshold = 40.0 default_temporal_interpolation_method = "linear" @@ -67,7 +68,7 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): "vae_type", "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, "kv_cache_attention_bias", SettingsControlType.DENOISING_STEPS, @@ -89,7 +90,7 @@ class KreaRealtimeVideoConfig(BasePipelineConfig): "vae_type", "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, "kv_cache_attention_bias", SettingsControlType.DENOISING_STEPS, diff --git a/src/scope/core/pipelines/longlive/pipeline.py b/src/scope/core/pipelines/longlive/pipeline.py index c881f36f..8cec2418 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("seed", getattr(config, "seed", 42)) + self.state.set("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 ace8e156..874700b7 100644 --- a/src/scope/core/pipelines/longlive/schema.py +++ b/src/scope/core/pipelines/longlive/schema.py @@ -40,6 +40,7 @@ class LongLiveConfig(BasePipelineConfig): min_dimension = 16 modified = True + supports_quantization = True height: int = 320 width: int = 576 @@ -48,6 +49,12 @@ class LongLiveConfig(BasePipelineConfig): default=VaeType.WAN, 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.", ) + new_param: float = Field( + default=0.5, + ge=0.0, + le=1.0, + description="Some new parameter that is not in the base schema", + ) modes = { "text": ModeDefaults( @@ -59,10 +66,11 @@ class LongLiveConfig(BasePipelineConfig): SettingsControlType.PREPROCESSOR, "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, "quantization", + "new_param", ], ), "video": ModeDefaults( @@ -78,11 +86,12 @@ class LongLiveConfig(BasePipelineConfig): SettingsControlType.PREPROCESSOR, "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, SettingsControlType.NOISE_CONTROLS, "quantization", + "new_param", ], ), } diff --git a/src/scope/core/pipelines/memflow/pipeline.py b/src/scope/core/pipelines/memflow/pipeline.py index 89c9434d..8214fb93 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("seed", getattr(config, "seed", 42)) + self.state.set("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 eb9e0e71..21db2178 100644 --- a/src/scope/core/pipelines/memflow/schema.py +++ b/src/scope/core/pipelines/memflow/schema.py @@ -40,6 +40,7 @@ class MemFlowConfig(BasePipelineConfig): min_dimension = 16 modified = True + supports_quantization = True height: int = 320 width: int = 576 @@ -60,7 +61,7 @@ class MemFlowConfig(BasePipelineConfig): "vae_type", "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, "quantization", @@ -80,7 +81,7 @@ class MemFlowConfig(BasePipelineConfig): "vae_type", "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, SettingsControlType.NOISE_CONTROLS, diff --git a/src/scope/core/pipelines/reward_forcing/pipeline.py b/src/scope/core/pipelines/reward_forcing/pipeline.py index d937de33..4722a842 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("seed", getattr(config, "seed", 42)) + self.state.set("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 45cf13ef..80ebdeac 100644 --- a/src/scope/core/pipelines/reward_forcing/schema.py +++ b/src/scope/core/pipelines/reward_forcing/schema.py @@ -39,6 +39,7 @@ class RewardForcingConfig(BasePipelineConfig): min_dimension = 16 modified = True + supports_quantization = True height: int = 320 width: int = 576 @@ -59,7 +60,7 @@ class RewardForcingConfig(BasePipelineConfig): "vae_type", "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, "quantization", @@ -79,7 +80,7 @@ class RewardForcingConfig(BasePipelineConfig): "vae_type", "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, SettingsControlType.NOISE_CONTROLS, diff --git a/src/scope/core/pipelines/streamdiffusionv2/pipeline.py b/src/scope/core/pipelines/streamdiffusionv2/pipeline.py index 4c8aff08..60d9a58a 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("seed", getattr(config, "seed", 42)) + self.state.set("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 bc16b119..ff2fb76c 100644 --- a/src/scope/core/pipelines/streamdiffusionv2/schema.py +++ b/src/scope/core/pipelines/streamdiffusionv2/schema.py @@ -40,6 +40,7 @@ class StreamDiffusionV2Config(BasePipelineConfig): min_dimension = 16 modified = True + supports_quantization = True denoising_steps: list[int] = [750, 250] noise_scale: float = 0.7 @@ -63,7 +64,7 @@ class StreamDiffusionV2Config(BasePipelineConfig): "vae_type", "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, "quantization", @@ -81,7 +82,7 @@ class StreamDiffusionV2Config(BasePipelineConfig): "vae_type", "height", "width", - "seed", + "base_seed", SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, SettingsControlType.NOISE_CONTROLS, diff --git a/src/scope/server/pipeline_manager.py b/src/scope/server/pipeline_manager.py index 2c8b3d97..e4be79f4 100644 --- a/src/scope/server/pipeline_manager.py +++ b/src/scope/server/pipeline_manager.py @@ -416,7 +416,7 @@ def _apply_load_params( 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 @@ -431,14 +431,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) + 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"] = seed config["vae_type"] = vae_type if loras: config["loras"] = loras diff --git a/src/scope/server/schema.py b/src/scope/server/schema.py index 96afeb6d..c12adcc6 100644 --- a/src/scope/server/schema.py +++ b/src/scope/server/schema.py @@ -10,9 +10,9 @@ # Default values for pipeline load params (duplicated from pipeline configs to avoid # importing torch-dependent modules). These should match the defaults in: -# - StreamDiffusionV2Config: height=512, width=512, seed=42 -# - LongLiveConfig: height=320, width=576, seed=42 -# - KreaRealtimeVideoConfig: height=320, width=576, seed=42 +# - StreamDiffusionV2Config: height=512, width=512, base_seed=42 +# - LongLiveConfig: height=320, width=576, base_seed=42 +# - KreaRealtimeVideoConfig: height=320, width=576, base_seed=42 _STREAMDIFFUSIONV2_HEIGHT = 512 _STREAMDIFFUSIONV2_WIDTH = 512 _LONGLIVE_HEIGHT = 320 @@ -317,7 +317,7 @@ class StreamDiffusionV2LoadParams(LoRAEnabledLoadParams): ge=64, le=2048, ) - seed: int = Field( + base_seed: int = Field( default=_DEFAULT_SEED, description="Random seed for generation", ge=0, @@ -360,7 +360,7 @@ class LongLiveLoadParams(LoRAEnabledLoadParams): ge=16, le=2048, ) - seed: int = Field( + base_seed: int = Field( default=_DEFAULT_SEED, description="Random seed for generation", ge=0, @@ -397,7 +397,7 @@ class KreaRealtimeVideoLoadParams(LoRAEnabledLoadParams): ge=64, le=2048, ) - seed: int = Field( + base_seed: int = Field( default=_DEFAULT_SEED, description="Random seed for generation", ge=0, From 196c4202f7e35e38a958b1714f778d097be4f198 Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:47:11 +0000 Subject: [PATCH 07/13] Update Signed-off-by: Rafal Leszko --- .../src/components/SettingsPanelRenderer.tsx | 37 +++++++++++++++++-- frontend/src/pages/StreamPage.tsx | 23 +++++++++++- src/scope/core/pipelines/defaults.py | 10 ++--- src/scope/core/pipelines/helpers.py | 2 +- .../pipelines/krea_realtime_video/pipeline.py | 2 +- src/scope/core/pipelines/longlive/pipeline.py | 2 +- src/scope/core/pipelines/longlive/schema.py | 8 ---- src/scope/core/pipelines/memflow/pipeline.py | 2 +- .../core/pipelines/reward_forcing/pipeline.py | 2 +- .../pipelines/streamdiffusionv2/pipeline.py | 2 +- .../wan2_1/blocks/prepare_latents.py | 10 ++--- .../wan2_1/blocks/prepare_video_latents.py | 10 ++--- src/scope/server/pipeline_manager.py | 20 +++++----- 13 files changed, 86 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/SettingsPanelRenderer.tsx b/frontend/src/components/SettingsPanelRenderer.tsx index 1eace778..1a1ef723 100644 --- a/frontend/src/components/SettingsPanelRenderer.tsx +++ b/frontend/src/components/SettingsPanelRenderer.tsx @@ -620,7 +620,12 @@ export function SettingsPanelRenderer({ // Render a dynamic control based on field name const renderDynamicControl = (fieldName: string, index: number) => { const configSchema = schema.config_schema; - if (!configSchema?.properties) return null; + 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") { @@ -642,7 +647,7 @@ export function SettingsPanelRenderer({ const property = configSchema.properties[fieldName]; if (!property) { console.warn( - `[SettingsPanelRenderer] Unknown field name: "${fieldName}"` + `[SettingsPanelRenderer] Field "${fieldName}" not found in schema properties. Available fields: ${Object.keys(configSchema.properties).join(", ")}` ); return null; } @@ -668,11 +673,20 @@ export function SettingsPanelRenderer({ const { value, onChange } = getFieldValueAndHandler(fieldName, property); if (onChange === undefined) { console.warn( - `[SettingsPanelRenderer] No handler for field "${fieldName}"` + `[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 ( onConfigValueChange(fieldName, val), + }; + } + return configValue; } // For dynamic fields not in configValues, use schema default and generic handler if (onConfigValueChange) { @@ -737,6 +763,9 @@ export function SettingsPanelRenderer({ onChange: (val: unknown) => onConfigValueChange(fieldName, val), }; } + console.warn( + `[SettingsPanelRenderer] No handler for dynamic field "${fieldName}" and onConfigValueChange not provided` + ); return { value: undefined, onChange: undefined }; } }; diff --git a/frontend/src/pages/StreamPage.tsx b/frontend/src/pages/StreamPage.tsx index b0682694..31bcb09d 100644 --- a/frontend/src/pages/StreamPage.tsx +++ b/frontend/src/pages/StreamPage.tsx @@ -735,10 +735,19 @@ export function StreamPage() { // Initialize dynamic config values from schema defaults when schema or pipeline changes useEffect(() => { const currentSchema = pipelineSchemas?.pipelines[settings.pipelineId]; - if (!currentSchema?.config_schema?.properties) return; + 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 = @@ -775,10 +784,22 @@ export function StreamPage() { 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 }; diff --git a/src/scope/core/pipelines/defaults.py b/src/scope/core/pipelines/defaults.py index c4611138..896ba10d 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 327e5a7d..81efb598 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("seed", getattr(config, "base_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 3f2ad738..9aa001d0 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("seed", getattr(config, "base_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/longlive/pipeline.py b/src/scope/core/pipelines/longlive/pipeline.py index 8cec2418..2743e333 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("seed", getattr(config, "base_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 874700b7..3afca5e1 100644 --- a/src/scope/core/pipelines/longlive/schema.py +++ b/src/scope/core/pipelines/longlive/schema.py @@ -49,12 +49,6 @@ class LongLiveConfig(BasePipelineConfig): default=VaeType.WAN, 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.", ) - new_param: float = Field( - default=0.5, - ge=0.0, - le=1.0, - description="Some new parameter that is not in the base schema", - ) modes = { "text": ModeDefaults( @@ -70,7 +64,6 @@ class LongLiveConfig(BasePipelineConfig): SettingsControlType.CACHE_MANAGEMENT, SettingsControlType.DENOISING_STEPS, "quantization", - "new_param", ], ), "video": ModeDefaults( @@ -91,7 +84,6 @@ class LongLiveConfig(BasePipelineConfig): SettingsControlType.DENOISING_STEPS, SettingsControlType.NOISE_CONTROLS, "quantization", - "new_param", ], ), } diff --git a/src/scope/core/pipelines/memflow/pipeline.py b/src/scope/core/pipelines/memflow/pipeline.py index 8214fb93..60b39a14 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("seed", getattr(config, "base_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/pipeline.py b/src/scope/core/pipelines/reward_forcing/pipeline.py index 4722a842..41be610e 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("seed", getattr(config, "base_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/pipeline.py b/src/scope/core/pipelines/streamdiffusionv2/pipeline.py index 60d9a58a..d4d28167 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("seed", getattr(config, "base_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/wan2_1/blocks/prepare_latents.py b/src/scope/core/pipelines/wan2_1/blocks/prepare_latents.py index 535a58a9..13c80a26 100644 --- a/src/scope/core/pipelines/wan2_1/blocks/prepare_latents.py +++ b/src/scope/core/pipelines/wan2_1/blocks/prepare_latents.py @@ -37,7 +37,7 @@ def description(self) -> str: def inputs(self) -> list[InputParam]: return [ InputParam( - "seed", + "base_seed", type_hint=int, default=42, description="Base seed for random number generation", @@ -85,12 +85,12 @@ def __call__(self, components, state: PipelineState) -> tuple[Any, PipelineState # The default param for InputParam does not work right now # The workaround is to set the default values here - seed = block_state.seed - if seed is None: - seed = 42 + base_seed = block_state.base_seed + if base_seed is None: + base_seed = 42 # Create generator from seed for reproducible generation - block_seed = seed + block_state.current_start_frame + block_seed = base_seed + block_state.current_start_frame rng = torch.Generator(device=generator_param.device).manual_seed(block_seed) # Determine number of latent frames to generate diff --git a/src/scope/core/pipelines/wan2_1/blocks/prepare_video_latents.py b/src/scope/core/pipelines/wan2_1/blocks/prepare_video_latents.py index 12cb86bd..ad6ba024 100644 --- a/src/scope/core/pipelines/wan2_1/blocks/prepare_video_latents.py +++ b/src/scope/core/pipelines/wan2_1/blocks/prepare_video_latents.py @@ -43,7 +43,7 @@ def inputs(self) -> list[InputParam]: description="Input video to convert into noisy latents", ), InputParam( - "seed", + "base_seed", type_hint=int, default=42, description="Base seed for random number generation", @@ -83,12 +83,12 @@ def __call__(self, components, state: PipelineState) -> tuple[Any, PipelineState # The default param for InputParam does not work right now # The workaround is to set the default values here - seed = block_state.seed - if seed is None: - seed = 42 + base_seed = block_state.base_seed + if base_seed is None: + base_seed = 42 # Create generator from seed for reproducible generation - block_seed = seed + block_state.current_start_frame + block_seed = base_seed + block_state.current_start_frame rng = torch.Generator(device=components.config.device).manual_seed(block_seed) # Generate empty latents (noise) diff --git a/src/scope/server/pipeline_manager.py b/src/scope/server/pipeline_manager.py index e4be79f4..46e9f1e9 100644 --- a/src/scope/server/pipeline_manager.py +++ b/src/scope/server/pipeline_manager.py @@ -412,18 +412,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 (resolution, base_seed, LoRAs, VAE type) to config. Args: config: Pipeline config dict to update 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 @@ -431,14 +431,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("base_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["base_seed"] = seed + config["base_seed"] = base_seed config["vae_type"] = vae_type if loras: config["loras"] = loras @@ -565,7 +565,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config + # Apply load parameters (resolution, base_seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -656,7 +656,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config + # Apply load parameters (resolution, base_seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -722,7 +722,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config + # Apply load parameters (resolution, base_seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -786,7 +786,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config + # Apply load parameters (resolution, base_seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -842,7 +842,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, seed, LoRAs) to config + # Apply load parameters (resolution, base_seed, LoRAs) to config self._apply_load_params( config, load_params, From 78c302d443cfb34c56365dd42b2dd72c0382ac15 Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:52:12 +0000 Subject: [PATCH 08/13] Update Signed-off-by: Rafal Leszko --- src/scope/core/pipelines/defaults.py | 10 +++++----- src/scope/core/pipelines/helpers.py | 2 +- .../core/pipelines/krea_realtime_video/pipeline.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/scope/core/pipelines/defaults.py b/src/scope/core/pipelines/defaults.py index 896ba10d..c4611138 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 base_seed from load_params with pipeline defaults as fallback. + """Extract height, width, and 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, base_seed overrides + load_params: Optional dictionary with height, width, seed overrides Returns: - Tuple of (height, width, base_seed) + Tuple of (height, width, seed) """ config = get_pipeline_config(pipeline_class) params = load_params or {} height = params.get("height", config.height) width = params.get("width", config.width) - base_seed = params.get("base_seed", config.base_seed) + seed = params.get("seed", config.base_seed) - return height, width, base_seed + return height, width, seed def apply_mode_defaults_to_state( diff --git a/src/scope/core/pipelines/helpers.py b/src/scope/core/pipelines/helpers.py index 81efb598..efeca6c3 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, "base_seed", pipeline_config.base_seed)) + state.set("base_seed", getattr(config, "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 9aa001d0..83dbbace 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, "base_seed", 42)) + self.state.set("base_seed", getattr(config, "seed", 42)) # Warm-up: Run enough iterations to fill the KV cache completely. # This ensures torch.compile compiles the flex_attention kernel at the From 2b43fd353d4e17c886d72845cc1d04ac942f21bd Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:53:06 +0000 Subject: [PATCH 09/13] Update Signed-off-by: Rafal Leszko --- src/scope/core/pipelines/longlive/pipeline.py | 2 +- src/scope/core/pipelines/reward_forcing/pipeline.py | 2 +- src/scope/core/pipelines/streamdiffusionv2/pipeline.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scope/core/pipelines/longlive/pipeline.py b/src/scope/core/pipelines/longlive/pipeline.py index 2743e333..7216cc2c 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, "base_seed", 42)) + self.state.set("base_seed", getattr(config, "seed", 42)) self.first_call = True self.last_mode = None # Track mode for transition detection diff --git a/src/scope/core/pipelines/reward_forcing/pipeline.py b/src/scope/core/pipelines/reward_forcing/pipeline.py index 41be610e..e8b0f4f8 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, "base_seed", 42)) + self.state.set("base_seed", getattr(config, "seed", 42)) self.first_call = True self.last_mode = None # Track mode for transition detection diff --git a/src/scope/core/pipelines/streamdiffusionv2/pipeline.py b/src/scope/core/pipelines/streamdiffusionv2/pipeline.py index d4d28167..5c2cb89e 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, "base_seed", 42)) + self.state.set("base_seed", getattr(config, "seed", 42)) self.first_call = True self.last_mode = None # Track mode for transition detection From 68c98331a47b54631c0ec6ad10ff11d2c4f439aa Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:53:38 +0000 Subject: [PATCH 10/13] Update Signed-off-by: Rafal Leszko --- src/scope/server/pipeline_manager.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/scope/server/pipeline_manager.py b/src/scope/server/pipeline_manager.py index 46e9f1e9..2c8b3d97 100644 --- a/src/scope/server/pipeline_manager.py +++ b/src/scope/server/pipeline_manager.py @@ -412,18 +412,18 @@ def _apply_load_params( default_width: int, default_seed: int = 42, ) -> None: - """Extract and apply common load parameters (resolution, base_seed, LoRAs, VAE type) to config. + """Extract and apply common load parameters (resolution, seed, LoRAs, VAE type) to config. Args: config: Pipeline config dict to update - load_params: Load parameters dict (may contain height, width, base_seed, loras, lora_merge_mode, vae_type) + load_params: Load parameters dict (may contain height, width, 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 base_seed if not in load_params + default_seed: Default seed if not in load_params """ height = default_height width = default_width - base_seed = default_seed + seed = default_seed loras = None lora_merge_mode = "permanent_merge" vae_type = "wan" # Default VAE type @@ -431,14 +431,14 @@ def _apply_load_params( if load_params: height = load_params.get("height", default_height) width = load_params.get("width", default_width) - base_seed = load_params.get("base_seed", default_seed) + seed = load_params.get("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["base_seed"] = base_seed + config["seed"] = seed config["vae_type"] = vae_type if loras: config["loras"] = loras @@ -565,7 +565,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, base_seed, LoRAs) to config + # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -656,7 +656,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, base_seed, LoRAs) to config + # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -722,7 +722,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, base_seed, LoRAs) to config + # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -786,7 +786,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, base_seed, LoRAs) to config + # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, @@ -842,7 +842,7 @@ def _load_pipeline_implementation( else: logger.info("VACE disabled by load_params, skipping VACE configuration") - # Apply load parameters (resolution, base_seed, LoRAs) to config + # Apply load parameters (resolution, seed, LoRAs) to config self._apply_load_params( config, load_params, From 95532994c3541c5ee290de68fe15c31e9ae44e4c Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:54:06 +0000 Subject: [PATCH 11/13] Update Signed-off-by: Rafal Leszko --- src/scope/core/pipelines/memflow/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scope/core/pipelines/memflow/pipeline.py b/src/scope/core/pipelines/memflow/pipeline.py index 60b39a14..c8de34c8 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, "base_seed", 42)) + self.state.set("base_seed", getattr(config, "seed", 42)) self.first_call = True self.last_mode = None # Track mode for transition detection From a56b44880309e9d23344ab251bdfcde243d51fe5 Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:54:31 +0000 Subject: [PATCH 12/13] Update Signed-off-by: Rafal Leszko --- src/scope/server/schema.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/scope/server/schema.py b/src/scope/server/schema.py index c12adcc6..c2e7564d 100644 --- a/src/scope/server/schema.py +++ b/src/scope/server/schema.py @@ -137,6 +137,14 @@ class Parameters(BaseModel): default=None, description="List of pipeline IDs to execute in a chain. If not provided, uses the currently loaded pipeline.", ) + first_frame_image: str | None = Field( + default=None, + description="Path to first frame reference image for extension mode. When provided alone, enables 'firstframe' mode (reference at start, generate continuation). When provided with last_frame_image, enables 'firstlastframe' mode (references at both ends). Images should be located in the assets directory.", + ) + last_frame_image: str | None = Field( + default=None, + description="Path to last frame reference image for extension mode. When provided alone, enables 'lastframe' mode (generate lead-up, reference at end). When provided with first_frame_image, enables 'firstlastframe' mode (references at both ends). Images should be located in the assets directory.", + ) class SpoutConfig(BaseModel): @@ -317,7 +325,7 @@ class StreamDiffusionV2LoadParams(LoRAEnabledLoadParams): ge=64, le=2048, ) - base_seed: int = Field( + seed: int = Field( default=_DEFAULT_SEED, description="Random seed for generation", ge=0, @@ -360,7 +368,7 @@ class LongLiveLoadParams(LoRAEnabledLoadParams): ge=16, le=2048, ) - base_seed: int = Field( + seed: int = Field( default=_DEFAULT_SEED, description="Random seed for generation", ge=0, @@ -397,7 +405,7 @@ class KreaRealtimeVideoLoadParams(LoRAEnabledLoadParams): ge=64, le=2048, ) - base_seed: int = Field( + seed: int = Field( default=_DEFAULT_SEED, description="Random seed for generation", ge=0, From cf0861aed5083d347421803290c9364cdb35a145 Mon Sep 17 00:00:00 2001 From: Rafal Leszko Date: Fri, 16 Jan 2026 15:55:16 +0000 Subject: [PATCH 13/13] Update Signed-off-by: Rafal Leszko --- src/scope/server/schema.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/scope/server/schema.py b/src/scope/server/schema.py index c2e7564d..189ce260 100644 --- a/src/scope/server/schema.py +++ b/src/scope/server/schema.py @@ -137,14 +137,6 @@ class Parameters(BaseModel): default=None, description="List of pipeline IDs to execute in a chain. If not provided, uses the currently loaded pipeline.", ) - first_frame_image: str | None = Field( - default=None, - description="Path to first frame reference image for extension mode. When provided alone, enables 'firstframe' mode (reference at start, generate continuation). When provided with last_frame_image, enables 'firstlastframe' mode (references at both ends). Images should be located in the assets directory.", - ) - last_frame_image: str | None = Field( - default=None, - description="Path to last frame reference image for extension mode. When provided alone, enables 'lastframe' mode (generate lead-up, reference at end). When provided with first_frame_image, enables 'firstlastframe' mode (references at both ends). Images should be located in the assets directory.", - ) class SpoutConfig(BaseModel):