b1394a2817
CI / docker (push) Successful in 3m49s
Text keys use OpenAI routes; image keys use predictions with ingress Model ID, OpenAPI input schemas, owner/name or short name resolution, protocol-aware admin whitelist, and in-app API docs aligned to UI conventions. Co-authored-by: Cursor <cursoragent@cursor.com>
176 lines
4.3 KiB
Go
176 lines
4.3 KiB
Go
package prediction
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/rose_cat707/luminary/internal/model"
|
|
)
|
|
|
|
type ParamType string
|
|
|
|
const (
|
|
ParamString ParamType = "string"
|
|
ParamInt ParamType = "int"
|
|
ParamFloat ParamType = "float"
|
|
)
|
|
|
|
type ParamDef struct {
|
|
Name string `json:"name"`
|
|
Label string `json:"label"`
|
|
Type ParamType `json:"type"`
|
|
Required bool `json:"required"`
|
|
Default any `json:"default,omitempty"`
|
|
}
|
|
|
|
var Txt2ImgParams = []ParamDef{
|
|
{Name: "prompt", Label: "正向提示词", Type: ParamString, Required: true},
|
|
{Name: "negative_prompt", Label: "负向提示词", Type: ParamString, Default: ""},
|
|
{Name: "seed", Label: "随机种子", Type: ParamInt, Default: -1},
|
|
{Name: "width", Label: "宽度", Type: ParamInt, Default: 1024},
|
|
{Name: "height", Label: "高度", Type: ParamInt, Default: 1024},
|
|
{Name: "steps", Label: "采样步数", Type: ParamInt, Default: 20},
|
|
{Name: "cfg_scale", Label: "CFG", Type: ParamFloat, Default: 7.0},
|
|
}
|
|
|
|
var Img2ImgParams = []ParamDef{
|
|
{Name: "prompt", Label: "正向提示词", Type: ParamString, Required: true},
|
|
{Name: "negative_prompt", Label: "负向提示词", Type: ParamString, Default: ""},
|
|
{Name: "image", Label: "输入图 URL", Type: ParamString, Required: true},
|
|
{Name: "seed", Label: "随机种子", Type: ParamInt, Default: -1},
|
|
{Name: "steps", Label: "采样步数", Type: ParamInt, Default: 20},
|
|
{Name: "cfg_scale", Label: "CFG", Type: ParamFloat, Default: 7.0},
|
|
{Name: "denoise", Label: "重绘幅度", Type: ParamFloat, Default: 0.75},
|
|
}
|
|
|
|
func ParamsForWorkflowType(wt model.WorkflowType) []ParamDef {
|
|
switch wt {
|
|
case model.WorkflowImg2Img:
|
|
return Img2ImgParams
|
|
default:
|
|
return Txt2ImgParams
|
|
}
|
|
}
|
|
|
|
func openAPIType(pt ParamType) string {
|
|
switch pt {
|
|
case ParamInt:
|
|
return "integer"
|
|
case ParamFloat:
|
|
return "number"
|
|
default:
|
|
return "string"
|
|
}
|
|
}
|
|
|
|
// OpenAPISchemaForWorkflow builds a Replicate-compatible schema with components.schemas.Input.
|
|
func OpenAPISchemaForWorkflow(wt model.WorkflowType, title, description string) map[string]any {
|
|
defs := ParamsForWorkflowType(wt)
|
|
properties := make(map[string]any, len(defs))
|
|
required := make([]string, 0, len(defs))
|
|
for i, d := range defs {
|
|
prop := map[string]any{
|
|
"type": openAPIType(d.Type),
|
|
"title": d.Label,
|
|
"description": d.Label,
|
|
"x-order": i,
|
|
}
|
|
if d.Default != nil {
|
|
prop["default"] = d.Default
|
|
}
|
|
properties[d.Name] = prop
|
|
if d.Required {
|
|
required = append(required, d.Name)
|
|
}
|
|
}
|
|
inputSchema := map[string]any{
|
|
"type": "object",
|
|
"properties": properties,
|
|
}
|
|
if len(required) > 0 {
|
|
inputSchema["required"] = required
|
|
}
|
|
return map[string]any{
|
|
"openapi": "3.0.0",
|
|
"info": map[string]any{
|
|
"title": title,
|
|
"version": "1.0.0",
|
|
"description": description,
|
|
},
|
|
"components": map[string]any{
|
|
"schemas": map[string]any{
|
|
"Input": inputSchema,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func RequiredBindings(wt model.WorkflowType) []string {
|
|
switch wt {
|
|
case model.WorkflowImg2Img:
|
|
return []string{"prompt", "image"}
|
|
default:
|
|
return []string{"prompt"}
|
|
}
|
|
}
|
|
|
|
func ValidateInput(wt model.WorkflowType, input map[string]any) (map[string]any, error) {
|
|
defs := ParamsForWorkflowType(wt)
|
|
out := make(map[string]any, len(defs))
|
|
for _, d := range defs {
|
|
v, ok := input[d.Name]
|
|
if !ok || v == nil {
|
|
if d.Required {
|
|
return nil, fmt.Errorf("missing required field: %s", d.Name)
|
|
}
|
|
if d.Default != nil {
|
|
out[d.Name] = d.Default
|
|
}
|
|
continue
|
|
}
|
|
coerced, err := coerceValue(d, v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", d.Name, err)
|
|
}
|
|
out[d.Name] = coerced
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func coerceValue(d ParamDef, v any) (any, error) {
|
|
switch d.Type {
|
|
case ParamString:
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected string")
|
|
}
|
|
if d.Required && s == "" {
|
|
return nil, fmt.Errorf("cannot be empty")
|
|
}
|
|
return s, nil
|
|
case ParamInt:
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return int(n), nil
|
|
case int:
|
|
return n, nil
|
|
case int64:
|
|
return int(n), nil
|
|
default:
|
|
return nil, fmt.Errorf("expected integer")
|
|
}
|
|
case ParamFloat:
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return n, nil
|
|
case int:
|
|
return float64(n), nil
|
|
case int64:
|
|
return float64(n), nil
|
|
default:
|
|
return nil, fmt.Errorf("expected number")
|
|
}
|
|
default:
|
|
return v, nil
|
|
}
|
|
}
|