Files
llmgw/internal/control/catalog/workflow.go
T
Ilker ManapandClaude Opus 5.5 b6bfc33ef7 Any ComfyUI workflow runs as a job, from the inputs its catalog entry declares
A model's catalog entry now says what its workflow takes and makes:
comfyui.inputs, each a placeholder of its name with a type (text, int,
float, bool, choice, image, mask, audio, video, file), bounds, choices
and a default; and comfyui.output, which gains video, audio and files
beside pictures and meshes. POST /v1/workflows/runs with {model, inputs}
checks a request against them before a job is written — a required input
left out, an unknown one, a number out of bounds, a song sent as a
picture, another owner's file id are each refused with what was wrong —
and the job worker replays it to a replica, where the files go into
ComfyUI's input directory, the workflow is filled and run, and the first
file of the model's output kind is the job's result. /v1/models lists
each ComfyUI model's workflow, so a client can build its form.

A PNG result carries the licence in an iTXt chunk, as a GLB does in
asset.extras. A file whose server named no type is typed by its name
before its bytes, so a WAV is audio/wav, not audio/wave.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 14:28:15 +02:00

157 lines
5.2 KiB
Go

package catalog
import (
"fmt"
"regexp"
"slices"
"strings"
)
// What a ComfyUI workflow takes and what it makes, said in the catalog so a
// model is data rather than code: a request is checked against its inputs
// before a job is written, a client can build a form from /v1/models, and
// the answer is picked from the files the workflow saved by its output kind.
// The kinds of thing a ComfyUI model makes, beside pictures and meshes.
const (
OutputVideo = "video"
OutputAudio = "audio"
// OutputFiles is whatever the workflow saves, of any type.
OutputFiles = "files"
)
// Outputs are every output kind a ComfyUI model may name.
var Outputs = []string{OutputPictures, OutputMeshes, OutputVideo, OutputAudio, OutputFiles}
// The types of a workflow's input.
const (
InputText = "text"
InputInt = "int"
InputFloat = "float"
InputBool = "bool"
InputChoice = "choice"
InputImage = "image"
InputMask = "mask"
InputAudio = "audio"
InputVideo = "video"
InputFile = "file"
)
// InputTypes are every type an input may have.
var InputTypes = []string{InputText, InputInt, InputFloat, InputBool, InputChoice,
InputImage, InputMask, InputAudio, InputVideo, InputFile}
// Input is one thing a workflow takes from a request: the placeholder
// {{name}} in its graph, filled with the request's inputs.name.
type Input struct {
// Name is the placeholder's, and the request's key.
Name string `json:"name" koanf:"name"`
// Type is one of text, int, float, bool, choice, image, mask, audio,
// video, file. The last five are files, sent as base64, a data URL, or
// the id of a file uploaded to /v1/files, and put in ComfyUI's input
// directory; the placeholder gets the name they were given there.
Type string `json:"type" koanf:"type"`
// Required inputs must be in every request; the others take Default,
// or the workflow's own {{name|default}}.
Required bool `json:"required,omitempty" koanf:"required"`
Default any `json:"default,omitempty" koanf:"default"`
// Min and Max bound a number; for text, Max bounds its length.
Min *float64 `json:"min,omitempty" koanf:"min"`
Max *float64 `json:"max,omitempty" koanf:"max"`
// Choices are what a choice may be.
Choices []string `json:"choices,omitempty" koanf:"choices"`
// MaxMB bounds a file input; zero is the type's own bound.
MaxMB int `json:"max_mb,omitempty" koanf:"max_mb"`
// Doc says what the input is for, to whoever fills it in.
Doc string `json:"doc,omitempty" koanf:"doc"`
}
// IsFile reports whether the input is a file put in ComfyUI's input directory.
func (in Input) IsFile() bool {
switch in.Type {
case InputImage, InputMask, InputAudio, InputVideo, InputFile:
return true
}
return false
}
// MaxBytes is the most a file input may be.
func (in Input) MaxBytes() int64 {
if in.MaxMB > 0 {
return int64(in.MaxMB) << 20
}
switch in.Type {
case InputImage, InputMask:
return 16 << 20
case InputVideo:
return 256 << 20
}
return 64 << 20
}
var inputName = regexp.MustCompile(`^[a-z][a-z0-9_]{0,63}$`)
func (in Input) validate(model string) error {
if !inputName.MatchString(in.Name) {
return fmt.Errorf("model %q: input %q is not a name of lowercase letters, digits and '_'", model, in.Name)
}
if !slices.Contains(InputTypes, in.Type) {
return fmt.Errorf("model %q: input %q has type %q, not one of %s", model, in.Name, in.Type, strings.Join(InputTypes, ", "))
}
if in.Type == InputChoice && len(in.Choices) == 0 {
return fmt.Errorf("model %q: input %q is a choice with no choices", model, in.Name)
}
if in.Type != InputChoice && len(in.Choices) > 0 {
return fmt.Errorf("model %q: input %q has choices but is not a choice", model, in.Name)
}
if in.Min != nil && in.Max != nil && *in.Min > *in.Max {
return fmt.Errorf("model %q: input %q has min above max", model, in.Name)
}
if in.Required && in.Default != nil {
return fmt.Errorf("model %q: input %q is required and has a default: it is one or the other", model, in.Name)
}
if in.IsFile() && in.Default != nil {
return fmt.Errorf("model %q: input %q is a file and cannot have a default", model, in.Name)
}
if in.MaxMB < 0 {
return fmt.Errorf("model %q: input %q has a negative max_mb", model, in.Name)
}
return nil
}
// ComfyOutput is what a ComfyUI model makes: pictures unless it says.
func (m Model) ComfyOutput() string { return m.comfyOutput() }
// ComfyInputs are what a ComfyUI model's workflow takes from a request.
func (m Model) ComfyInputs() []Input {
if m.ComfyUI == nil {
return nil
}
return m.ComfyUI.Inputs
}
// RunsWorkflows reports whether a model answers /v1/workflows/runs: any the
// cluster's ComfyUI or a ComfyUI outside it serves.
func (m Model) RunsWorkflows() bool { return m.ServesComfyUI() }
func validOutput(model, out string) error {
if !slices.Contains(Outputs, out) {
return fmt.Errorf("model %q: comfyui.output %q is not one of %s", model, out, strings.Join(Outputs, ", "))
}
return nil
}
func validateInputs(model string, inputs []Input) error {
seen := map[string]bool{}
for _, in := range inputs {
if err := in.validate(model); err != nil {
return err
}
if seen[in.Name] {
return fmt.Errorf("model %q: input %q is declared twice", model, in.Name)
}
seen[in.Name] = true
}
return nil
}