Unify ComfyUI ingress model ID and fix seed validation errors.
CI / docker (push) Successful in 3m46s
CI / docker (push) Successful in 3m46s
Merge model_id/alias in the image provider UI, and ensure ComfyUI never receives seed -1 after cloneMap type coercion. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -275,6 +275,9 @@ func (h *Handler) CreateModel(c *gin.Context) {
|
||||
VersionHash: prediction.HashWorkflowJSON(req.WorkflowJSON),
|
||||
Enabled: true,
|
||||
}
|
||||
if prov.Category == model.CategoryImage {
|
||||
m.Alias = ""
|
||||
}
|
||||
if m.DisplayName == "" {
|
||||
m.DisplayName = req.ModelID
|
||||
}
|
||||
@@ -347,6 +350,9 @@ func (h *Handler) UpdateModel(c *gin.Context) {
|
||||
m.InputBindingJSON = string(b)
|
||||
}
|
||||
prov, _ := h.cache.GetProvider(m.ProviderID)
|
||||
if prov.Category == model.CategoryImage {
|
||||
m.Alias = ""
|
||||
}
|
||||
if prov.Category == model.CategoryImage && m.WorkflowJSON != "" {
|
||||
if err := validateImageModelCreate(struct {
|
||||
ModelID, DisplayName string
|
||||
|
||||
@@ -22,10 +22,7 @@ func BuildPrompt(workflowJSON string, binding InputBinding, input map[string]any
|
||||
continue
|
||||
}
|
||||
if name == "seed" {
|
||||
if iv, ok := val.(int); ok && iv < 0 {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(1<<31-1))
|
||||
val = int(n.Int64())
|
||||
}
|
||||
val = resolveSeed(val)
|
||||
}
|
||||
node, ok := prompt[target.Node].(map[string]any)
|
||||
if !ok {
|
||||
@@ -37,9 +34,61 @@ func BuildPrompt(workflowJSON string, binding InputBinding, input map[string]any
|
||||
}
|
||||
inputs[target.Field] = val
|
||||
}
|
||||
sanitizeSamplerSeeds(prompt)
|
||||
return prompt, nil
|
||||
}
|
||||
|
||||
// resolveSeed maps seed < 0 (random) to a non-negative value ComfyUI accepts.
|
||||
func resolveSeed(val any) any {
|
||||
n, ok := seedAsInt64(val)
|
||||
if !ok || n >= 0 {
|
||||
return val
|
||||
}
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(1<<31-1))
|
||||
if err != nil {
|
||||
return int(0)
|
||||
}
|
||||
return int(r.Int64())
|
||||
}
|
||||
|
||||
func seedAsInt64(v any) (int64, bool) {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return int64(n), true
|
||||
case int64:
|
||||
return n, true
|
||||
case float64:
|
||||
return int64(n), true
|
||||
case json.Number:
|
||||
i, err := n.Int64()
|
||||
return i, err == nil
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeSamplerSeeds(prompt map[string]any) {
|
||||
seedFields := []string{"seed", "noise_seed"}
|
||||
for _, raw := range prompt {
|
||||
node, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if class, _ := node["class_type"].(string); class != "KSampler" {
|
||||
continue
|
||||
}
|
||||
inputs, ok := node["inputs"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, field := range seedFields {
|
||||
if v, ok := inputs[field]; ok {
|
||||
inputs[field] = resolveSeed(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deepCopyMap(src map[string]any) map[string]any {
|
||||
b, _ := json.Marshal(src)
|
||||
var dst map[string]any
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package prediction
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveSeedNegativeInt(t *testing.T) {
|
||||
got := resolveSeed(-1)
|
||||
n, ok := got.(int)
|
||||
if !ok || n < 0 {
|
||||
t.Fatalf("expected non-negative int, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSeedNegativeFloat64(t *testing.T) {
|
||||
got := resolveSeed(float64(-1))
|
||||
n, ok := got.(int)
|
||||
if !ok || n < 0 {
|
||||
t.Fatalf("expected non-negative int from float64 -1, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSeedExplicit(t *testing.T) {
|
||||
if got := resolveSeed(42); got != 42 {
|
||||
t.Fatalf("expected 42, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPromptSanitizesUnboundKSamplerSeed(t *testing.T) {
|
||||
workflow := map[string]any{
|
||||
"17": map[string]any{
|
||||
"class_type": "KSampler",
|
||||
"inputs": map[string]any{
|
||||
"seed": float64(-1),
|
||||
"steps": 20,
|
||||
},
|
||||
},
|
||||
}
|
||||
raw, _ := json.Marshal(workflow)
|
||||
prompt, err := BuildPrompt(string(raw), InputBinding{}, map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node := prompt["17"].(map[string]any)
|
||||
inputs := node["inputs"].(map[string]any)
|
||||
seed, ok := inputs["seed"].(int)
|
||||
if !ok || seed < 0 {
|
||||
t.Fatalf("expected sanitized seed, got %v", inputs["seed"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPromptSeedBindingAfterCloneMap(t *testing.T) {
|
||||
workflow := map[string]any{
|
||||
"17": map[string]any{
|
||||
"class_type": "KSampler",
|
||||
"inputs": map[string]any{"seed": 0},
|
||||
},
|
||||
}
|
||||
raw, _ := json.Marshal(workflow)
|
||||
binding := InputBinding{
|
||||
"seed": {Node: "17", Field: "seed"},
|
||||
}
|
||||
// cloneMap round-trip produces float64, as in execute().
|
||||
input := cloneMap(map[string]any{"seed": -1})
|
||||
prompt, err := BuildPrompt(string(raw), binding, input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inputs := prompt["17"].(map[string]any)["inputs"].(map[string]any)
|
||||
seed, ok := inputs["seed"].(int)
|
||||
if !ok || seed < 0 {
|
||||
t.Fatalf("expected non-negative seed after binding, got %v", inputs["seed"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user