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>
157 lines
5.2 KiB
Go
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
|
|
}
|