76ba500417
CI / docker (push) Successful in 2m4s
OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress, ingress key governance, monitoring, and security controls. Co-authored-by: Cursor <cursoragent@cursor.com>
269 lines
7.2 KiB
Go
269 lines
7.2 KiB
Go
package prediction
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/rose_cat707/luminary/internal/model"
|
|
)
|
|
|
|
type WorkflowNode struct {
|
|
ID string `json:"id"`
|
|
ClassType string `json:"class_type"`
|
|
Title string `json:"title"`
|
|
InjectableFields []string `json:"injectable_fields"`
|
|
}
|
|
|
|
type BindingCandidate struct {
|
|
Node string `json:"node"`
|
|
Field string `json:"field"`
|
|
}
|
|
|
|
type BindingSuggestion struct {
|
|
Param string `json:"param"`
|
|
Node string `json:"node,omitempty"`
|
|
Field string `json:"field,omitempty"`
|
|
Type ParamType `json:"type"`
|
|
DefaultFromWorkflow any `json:"default_from_workflow,omitempty"`
|
|
Confidence string `json:"confidence"`
|
|
Candidates []BindingCandidate `json:"candidates"`
|
|
}
|
|
|
|
type AnalyzeResult struct {
|
|
VersionHashPreview string `json:"version_hash_preview"`
|
|
Nodes []WorkflowNode `json:"nodes"`
|
|
Suggestions []BindingSuggestion `json:"suggestions"`
|
|
}
|
|
|
|
func HashWorkflowJSON(raw string) string {
|
|
sum := sha256.Sum256([]byte(raw))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func AnalyzeWorkflow(workflowJSON string, wt model.WorkflowType) (*AnalyzeResult, error) {
|
|
var workflow map[string]any
|
|
if err := json.Unmarshal([]byte(workflowJSON), &workflow); err != nil {
|
|
return nil, fmt.Errorf("invalid workflow json: %w", err)
|
|
}
|
|
nodes := parseNodes(workflow)
|
|
suggestions := suggestBindings(workflow, nodes, wt)
|
|
return &AnalyzeResult{
|
|
VersionHashPreview: HashWorkflowJSON(workflowJSON),
|
|
Nodes: nodes,
|
|
Suggestions: suggestions,
|
|
}, nil
|
|
}
|
|
|
|
func parseNodes(workflow map[string]any) []WorkflowNode {
|
|
var out []WorkflowNode
|
|
for id, raw := range workflow {
|
|
node, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
classType, _ := node["class_type"].(string)
|
|
title := ""
|
|
if meta, ok := node["_meta"].(map[string]any); ok {
|
|
title, _ = meta["title"].(string)
|
|
}
|
|
var fields []string
|
|
if inputs, ok := node["inputs"].(map[string]any); ok {
|
|
for k, v := range inputs {
|
|
if isInjectableValue(v) {
|
|
fields = append(fields, k)
|
|
}
|
|
}
|
|
}
|
|
out = append(out, WorkflowNode{
|
|
ID: id, ClassType: classType, Title: title, InjectableFields: fields,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func isInjectableValue(v any) bool {
|
|
switch v.(type) {
|
|
case string, float64, bool, int, int64:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func suggestBindings(workflow map[string]any, nodes []WorkflowNode, wt model.WorkflowType) []BindingSuggestion {
|
|
defs := ParamsForWorkflowType(wt)
|
|
var clipNodes []WorkflowNode
|
|
var samplerNodes []WorkflowNode
|
|
var latentNodes []WorkflowNode
|
|
var loadImageNodes []WorkflowNode
|
|
for _, n := range nodes {
|
|
switch n.ClassType {
|
|
case "CLIPTextEncode":
|
|
clipNodes = append(clipNodes, n)
|
|
case "KSampler", "KSamplerAdvanced", "RandomNoise":
|
|
samplerNodes = append(samplerNodes, n)
|
|
case "EmptyLatentImage":
|
|
latentNodes = append(latentNodes, n)
|
|
case "LoadImage":
|
|
loadImageNodes = append(loadImageNodes, n)
|
|
}
|
|
}
|
|
var out []BindingSuggestion
|
|
for _, d := range defs {
|
|
s := BindingSuggestion{Param: d.Name, Type: d.Type, Confidence: "low"}
|
|
switch d.Name {
|
|
case "prompt":
|
|
s = suggestCLIP(clipNodes, workflow, false)
|
|
case "negative_prompt":
|
|
s = suggestCLIP(clipNodes, workflow, true)
|
|
case "seed":
|
|
s = suggestSamplerField(samplerNodes, workflow, "seed", d.Type)
|
|
if s.Node == "" {
|
|
s = suggestSamplerField(samplerNodes, workflow, "noise_seed", d.Type)
|
|
}
|
|
case "steps":
|
|
s = suggestSamplerField(samplerNodes, workflow, "steps", d.Type)
|
|
case "cfg_scale":
|
|
s = suggestSamplerField(samplerNodes, workflow, "cfg", d.Type)
|
|
case "denoise":
|
|
s = suggestSamplerField(samplerNodes, workflow, "denoise", d.Type)
|
|
case "width":
|
|
s = suggestLatentField(latentNodes, workflow, "width", d.Type)
|
|
case "height":
|
|
s = suggestLatentField(latentNodes, workflow, "height", d.Type)
|
|
case "image":
|
|
s = suggestLoadImage(loadImageNodes, workflow, d.Type)
|
|
default:
|
|
s.Param = d.Name
|
|
s.Type = d.Type
|
|
}
|
|
if s.Param == "" {
|
|
s.Param = d.Name
|
|
s.Type = d.Type
|
|
}
|
|
if s.Node != "" {
|
|
s.Confidence = "high"
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func suggestCLIP(nodes []WorkflowNode, workflow map[string]any, negative bool) BindingSuggestion {
|
|
s := BindingSuggestion{Param: "prompt", Type: ParamString, Confidence: "low"}
|
|
if negative {
|
|
s.Param = "negative_prompt"
|
|
}
|
|
var candidates []BindingCandidate
|
|
var picked *WorkflowNode
|
|
for i := range nodes {
|
|
n := nodes[i]
|
|
titleLower := strings.ToLower(n.Title)
|
|
isNeg := strings.Contains(titleLower, "negative") || strings.Contains(titleLower, "neg")
|
|
if !hasField(n, "text") {
|
|
continue
|
|
}
|
|
candidates = append(candidates, BindingCandidate{Node: n.ID, Field: "text"})
|
|
if negative && isNeg && picked == nil {
|
|
picked = &nodes[i]
|
|
}
|
|
if !negative && !isNeg && picked == nil {
|
|
picked = &nodes[i]
|
|
}
|
|
}
|
|
if picked == nil && len(candidates) > 0 {
|
|
idx := 0
|
|
if negative && len(candidates) > 1 {
|
|
idx = 1
|
|
}
|
|
for i := range nodes {
|
|
if nodes[i].ID == candidates[idx].Node {
|
|
picked = &nodes[i]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
s.Candidates = candidates
|
|
if picked != nil {
|
|
s.Node = picked.ID
|
|
s.Field = "text"
|
|
s.DefaultFromWorkflow = nodeFieldValue(workflow, picked.ID, "text")
|
|
}
|
|
return s
|
|
}
|
|
|
|
func suggestSamplerField(nodes []WorkflowNode, workflow map[string]any, field string, pt ParamType) BindingSuggestion {
|
|
paramName := field
|
|
if field == "cfg" {
|
|
paramName = "cfg_scale"
|
|
}
|
|
s := BindingSuggestion{Param: paramName, Type: pt, Confidence: "low"}
|
|
var candidates []BindingCandidate
|
|
for _, n := range nodes {
|
|
if hasField(n, field) {
|
|
candidates = append(candidates, BindingCandidate{Node: n.ID, Field: field})
|
|
if s.Node == "" {
|
|
s.Node = n.ID
|
|
s.Field = field
|
|
s.DefaultFromWorkflow = nodeFieldValue(workflow, n.ID, field)
|
|
}
|
|
}
|
|
}
|
|
s.Candidates = candidates
|
|
return s
|
|
}
|
|
|
|
func suggestLatentField(nodes []WorkflowNode, workflow map[string]any, field string, pt ParamType) BindingSuggestion {
|
|
s := BindingSuggestion{Param: field, Type: pt, Confidence: "low"}
|
|
for _, n := range nodes {
|
|
if hasField(n, field) {
|
|
s.Candidates = append(s.Candidates, BindingCandidate{Node: n.ID, Field: field})
|
|
if s.Node == "" {
|
|
s.Node = n.ID
|
|
s.Field = field
|
|
s.DefaultFromWorkflow = nodeFieldValue(workflow, n.ID, field)
|
|
}
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func suggestLoadImage(nodes []WorkflowNode, workflow map[string]any, pt ParamType) BindingSuggestion {
|
|
s := BindingSuggestion{Param: "image", Type: pt, Confidence: "low"}
|
|
for _, n := range nodes {
|
|
if hasField(n, "image") {
|
|
s.Candidates = append(s.Candidates, BindingCandidate{Node: n.ID, Field: "image"})
|
|
if s.Node == "" {
|
|
s.Node = n.ID
|
|
s.Field = "image"
|
|
s.DefaultFromWorkflow = nodeFieldValue(workflow, n.ID, "image")
|
|
}
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func hasField(n WorkflowNode, field string) bool {
|
|
for _, f := range n.InjectableFields {
|
|
if f == field {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func nodeFieldValue(workflow map[string]any, nodeID, field string) any {
|
|
node, ok := workflow[nodeID].(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
inputs, ok := node["inputs"].(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return inputs[field]
|
|
}
|