562 lines
19 KiB
Go
562 lines
19 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// RunningHubWorkflowFetchRequest 只用于独立工作流设置页,不进入 ModelChannel。
|
||
type RunningHubWorkflowFetchRequest struct {
|
||
BaseURL string `json:"baseUrl"`
|
||
APIKey string `json:"apiKey"`
|
||
WalletAPIKey string `json:"walletApiKey"`
|
||
UseWallet bool `json:"useWallet"`
|
||
WorkflowID string `json:"workflowId"`
|
||
WebappID string `json:"webappId,omitempty"`
|
||
Title string `json:"title,omitempty"`
|
||
Capability string `json:"capability,omitempty"`
|
||
}
|
||
|
||
func (s *Service) FetchRunningHubWorkflowInfo(ctx context.Context, req RunningHubWorkflowFetchRequest) (map[string]any, error) {
|
||
workflowID := strings.TrimSpace(req.WorkflowID)
|
||
if workflowID == "" {
|
||
return nil, BadAuthRequest("workflowId 不能为空")
|
||
}
|
||
config, err := runningHubManagementConfig(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
root := runningHubRootURL(config.BaseURL)
|
||
var response map[string]any
|
||
if err := s.runningHubJSON(ctx, config, root+"/api/openapi/getJsonApiFormat", map[string]any{"apiKey": runningHubAPIKey(config), "workflowId": workflowID}, &response); err != nil {
|
||
return nil, fmt.Errorf("拉取 RunningHub 工作流参数失败:%w", err)
|
||
}
|
||
data, _ := response["data"].(map[string]any)
|
||
if data == nil {
|
||
return nil, errors.New("RunningHub 工作流参数响应缺少 data")
|
||
}
|
||
workflow := map[string]any{}
|
||
if prompt, ok := data["prompt"].(string); ok && strings.TrimSpace(prompt) != "" {
|
||
if err := json.Unmarshal([]byte(prompt), &workflow); err != nil {
|
||
return nil, fmt.Errorf("RunningHub 工作流 JSON 解析失败:%w", err)
|
||
}
|
||
} else if prompt, ok := data["prompt"].(map[string]any); ok {
|
||
workflow = prompt
|
||
}
|
||
return map[string]any{"workflowId": workflowID, "kind": "workflow", "title": firstNonEmptyString(req.Title, workflowID), "fields": collectManagementWorkflowFields(workflow, req.Capability), "workflowJson": workflow, "raw": response}, nil
|
||
}
|
||
|
||
func (s *Service) FetchRunningHubAppInfo(ctx context.Context, req RunningHubWorkflowFetchRequest) (map[string]any, error) {
|
||
webappID := strings.TrimSpace(req.WebappID)
|
||
if webappID == "" {
|
||
return nil, BadAuthRequest("webappId 不能为空")
|
||
}
|
||
config, err := runningHubManagementConfig(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
root := runningHubRootURL(config.BaseURL)
|
||
var response map[string]any
|
||
if err := s.runningHubJSON(ctx, config, root+"/api/webapp/apiCallDemo", map[string]any{
|
||
"apiKey": runningHubAPIKey(config),
|
||
"webappId": webappID,
|
||
}, &response); err != nil {
|
||
return nil, fmt.Errorf("拉取 RunningHub App 参数失败:%w", err)
|
||
}
|
||
if code, valid := runningHubPayloadCode(response); valid && code != 0 {
|
||
return nil, fmt.Errorf("拉取 RunningHub App 参数失败:%s", runningHubFailureMessage(response))
|
||
}
|
||
rawData, _ := response["data"].(map[string]any)
|
||
result := map[string]any{"kind": "app", "webappId": webappID, "workflowId": webappID, "title": firstNonEmptyString(req.Title, webappID), "fields": []map[string]any{}, "raw": response}
|
||
if rawData != nil {
|
||
result["fields"] = normalizeManagementAppFields(rawData["nodeInfoList"], req.Capability)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func normalizeManagementAppFields(raw any, capability string) []map[string]any {
|
||
items, _ := raw.([]any)
|
||
fields := make([]map[string]any, 0, len(items))
|
||
for _, item := range items {
|
||
original, ok := item.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
field := make(map[string]any, len(original)+3)
|
||
for key, value := range original {
|
||
field[key] = value
|
||
}
|
||
nodeID := strings.TrimSpace(stringValue(field["nodeId"]))
|
||
fieldName := strings.TrimSpace(stringValue(field["fieldName"]))
|
||
if nodeID == "" || fieldName == "" {
|
||
continue
|
||
}
|
||
if stringValue(field["id"]) == "" {
|
||
field["id"] = nodeID + "::" + fieldName
|
||
}
|
||
if _, exists := field["enabled"]; !exists {
|
||
field["enabled"] = true
|
||
}
|
||
fieldType := strings.ToUpper(strings.TrimSpace(stringValue(field["fieldType"])))
|
||
if fieldType == "" {
|
||
fieldType = managementWorkflowFieldType(fieldName, field["fieldValue"], "")
|
||
field["fieldType"] = fieldType
|
||
}
|
||
if strings.TrimSpace(stringValue(field["source"])) == "" {
|
||
if isWorkflowPromptFieldName(fieldName) {
|
||
field["source"] = "prompt"
|
||
field["sourceAutomatic"] = true
|
||
} else if source := managementAutomaticInputSource(fieldName, fieldType); source != "" {
|
||
field["source"] = source
|
||
field["sourceAutomatic"] = true
|
||
} else {
|
||
field["source"] = ""
|
||
field["sourceAutomatic"] = false
|
||
}
|
||
}
|
||
field["role"] = managementWorkflowFieldRole("", fieldName, fieldType, stringValue(field["source"]))
|
||
field["safeToOverride"] = true
|
||
fields = append(fields, field)
|
||
}
|
||
return applyManagementFieldDefaults(fields, capability)
|
||
}
|
||
|
||
func runningHubManagementConfig(req RunningHubWorkflowFetchRequest) (providerConfig, error) {
|
||
config := providerConfig{BaseURL: strings.TrimSpace(req.BaseURL), APIKey: strings.TrimSpace(req.APIKey), RunningHubWalletKey: strings.TrimSpace(req.WalletAPIKey), RunningHubUseWallet: req.UseWallet}
|
||
if config.BaseURL == "" {
|
||
config.BaseURL = "https://www.runninghub.cn"
|
||
}
|
||
if _, err := ValidateOutboundURL(runningHubRootURL(config.BaseURL)); err != nil {
|
||
return config, err
|
||
}
|
||
if runningHubAPIKey(config) == "" {
|
||
return config, BadAuthRequest("请先填写 RunningHub 积分 API Key")
|
||
}
|
||
return config, nil
|
||
}
|
||
|
||
func collectManagementWorkflowFields(workflow map[string]any, capability string) []map[string]any {
|
||
fields := make([]map[string]any, 0)
|
||
promptNodeID := ""
|
||
promptFieldName := ""
|
||
if promptFields := runningHubPromptFallback(workflow, "prompt"); len(promptFields) > 0 {
|
||
promptNodeID = strings.TrimSpace(stringValue(promptFields[0]["nodeId"]))
|
||
promptFieldName = strings.TrimSpace(stringValue(promptFields[0]["fieldName"]))
|
||
}
|
||
nodeIDs := make([]string, 0, len(workflow))
|
||
for nodeID := range workflow {
|
||
nodeIDs = append(nodeIDs, nodeID)
|
||
}
|
||
sort.Slice(nodeIDs, func(i, j int) bool { return managementNodeIDLess(nodeIDs[i], nodeIDs[j]) })
|
||
for _, nodeID := range nodeIDs {
|
||
rawNode := workflow[nodeID]
|
||
node, ok := rawNode.(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
inputs, ok := node["inputs"].(map[string]any)
|
||
if !ok {
|
||
continue
|
||
}
|
||
classType := strings.TrimSpace(stringValue(node["class_type"]))
|
||
fieldNames := make([]string, 0, len(inputs))
|
||
for fieldName := range inputs {
|
||
fieldNames = append(fieldNames, fieldName)
|
||
}
|
||
sort.Strings(fieldNames)
|
||
for _, fieldName := range fieldNames {
|
||
value := inputs[fieldName]
|
||
if isWorkflowLinkValue(value) {
|
||
continue
|
||
}
|
||
// 所有静态输入都保留在设置页,但内部参数默认关闭;危险数值字段
|
||
// 额外标记为不可覆盖,并由提交端再次强制拦截。
|
||
fieldType := managementWorkflowFieldType(fieldName, value, classType)
|
||
label := fieldName
|
||
if title := strings.TrimSpace(workflowNodeMetaTitle(node)); title != "" {
|
||
label = title + " · " + fieldName
|
||
}
|
||
safeToOverride := managementWorkflowFieldSafeToOverride(classType, fieldName)
|
||
role := managementWorkflowFieldRole(classType, fieldName, fieldType, "")
|
||
field := map[string]any{"id": nodeID + "::" + fieldName, "nodeId": nodeID, "classType": classType, "fieldName": fieldName, "fieldValue": value, "fieldType": fieldType, "label": label, "role": role, "safeToOverride": safeToOverride, "enabled": safeToOverride && role != "internal", "source": "", "sourceAutomatic": false}
|
||
if nodeID == promptNodeID && fieldName == promptFieldName {
|
||
field["source"] = "prompt"
|
||
field["sourceAutomatic"] = true
|
||
field["required"] = true
|
||
field["role"] = "prompt"
|
||
field["enabled"] = true
|
||
} else if source := managementAutomaticInputSource(fieldName, fieldType); source != "" {
|
||
field["source"] = source
|
||
field["sourceAutomatic"] = true
|
||
field["role"] = "media"
|
||
field["enabled"] = true
|
||
}
|
||
if isManagementSeedField(fieldName) {
|
||
field["randomEnabled"] = true
|
||
}
|
||
fields = append(fields, field)
|
||
}
|
||
}
|
||
return applyManagementFieldDefaults(fields, capability)
|
||
}
|
||
|
||
func managementWorkflowFieldSafeToOverride(classType string, fieldName string) bool {
|
||
classKey := normalizeManagementFieldName(classType)
|
||
fieldKey := normalizeManagementFieldName(fieldName)
|
||
if classKey == "int" && fieldKey == "value" {
|
||
return false
|
||
}
|
||
return classKey != "imageresize+" || (fieldKey != "width" && fieldKey != "height" && fieldKey != "multipleof")
|
||
}
|
||
|
||
func managementWorkflowFieldRole(classType string, fieldName string, fieldType string, source string) string {
|
||
normalizedSource := normalizeManagementFieldName(source)
|
||
if normalizedSource == "prompt" || normalizedSource == "text" || normalizedSource == "positiveprompt" || normalizedSource == "positive" {
|
||
return "prompt"
|
||
}
|
||
if normalizedSource == "referenceimage" || normalizedSource == "image" || normalizedSource == "referencevideo" || normalizedSource == "video" || normalizedSource == "referenceaudio" || normalizedSource == "audio" || normalizedSource == "mask" {
|
||
return "media"
|
||
}
|
||
if fieldType == "IMAGE" || fieldType == "VIDEO" || fieldType == "AUDIO" {
|
||
return "media"
|
||
}
|
||
if !managementWorkflowFieldSafeToOverride(classType, fieldName) {
|
||
return "internal"
|
||
}
|
||
key := normalizeManagementFieldName(fieldName)
|
||
for _, candidate := range []string{"aspectratio", "ratio", "duration", "durationseconds", "seconds", "videoseconds", "quality", "resolution", "seed", "noiseseed", "steps", "step", "sigmapoints", "cfg", "cfgscale", "guidance", "guidancescale", "sampler", "samplername", "scheduler", "fps", "count", "batch", "batchsize", "generateaudio", "watermark", "negativeprompt", "systemprompt"} {
|
||
if key == candidate {
|
||
return "business"
|
||
}
|
||
}
|
||
if strings.TrimSpace(classType) == "" {
|
||
// AI App 返回的是公开参数列表,没有内部节点类型。
|
||
return "business"
|
||
}
|
||
return "internal"
|
||
}
|
||
|
||
func applyManagementFieldDefaults(fields []map[string]any, capability string) []map[string]any {
|
||
sort.SliceStable(fields, func(i, j int) bool {
|
||
leftNode := strings.TrimSpace(stringValue(fields[i]["nodeId"]))
|
||
rightNode := strings.TrimSpace(stringValue(fields[j]["nodeId"]))
|
||
if leftNode != rightNode {
|
||
return managementNodeIDLess(leftNode, rightNode)
|
||
}
|
||
return strings.TrimSpace(stringValue(fields[i]["fieldName"])) < strings.TrimSpace(stringValue(fields[j]["fieldName"]))
|
||
})
|
||
imageOrder := 0
|
||
videoOrder := 0
|
||
audioOrder := 0
|
||
for _, field := range fields {
|
||
fieldName := strings.TrimSpace(stringValue(field["fieldName"]))
|
||
fieldType := strings.ToUpper(strings.TrimSpace(stringValue(field["fieldType"])))
|
||
source := strings.TrimSpace(stringValue(field["source"]))
|
||
sourceAutomatic, sourceAutomaticConfigured := field["sourceAutomatic"].(bool)
|
||
if source == "" && (!sourceAutomaticConfigured || sourceAutomatic) {
|
||
source = workflowDynamicSourceForMode(fieldName, fieldType, capability)
|
||
if source != "" {
|
||
field["source"] = source
|
||
field["sourceAutomatic"] = true
|
||
}
|
||
}
|
||
switch normalizeManagementFieldName(source) {
|
||
case "prompt", "text", "positiveprompt", "positive":
|
||
if _, exists := field["required"]; !exists {
|
||
field["required"] = true
|
||
}
|
||
case "referenceimage", "image":
|
||
configuredOrder := managementIntValue(field["imageOrder"], 0)
|
||
if configuredOrder <= 0 {
|
||
imageOrder++
|
||
configuredOrder = imageOrder
|
||
field["imageOrder"] = configuredOrder
|
||
} else if configuredOrder > imageOrder {
|
||
imageOrder = configuredOrder
|
||
}
|
||
if _, exists := field["sourceIndex"]; !exists {
|
||
field["sourceIndex"] = configuredOrder - 1
|
||
}
|
||
if _, exists := field["required"]; !exists {
|
||
field["required"] = configuredOrder == 1
|
||
}
|
||
case "referencevideo", "video":
|
||
configuredIndex, exists := field["sourceIndex"]
|
||
index := managementIntValue(configuredIndex, videoOrder)
|
||
if !exists {
|
||
field["sourceIndex"] = index
|
||
}
|
||
if index >= videoOrder {
|
||
videoOrder = index + 1
|
||
}
|
||
if _, exists := field["required"]; !exists {
|
||
field["required"] = index == 0
|
||
}
|
||
case "referenceaudio", "audio":
|
||
configuredIndex, exists := field["sourceIndex"]
|
||
index := managementIntValue(configuredIndex, audioOrder)
|
||
if !exists {
|
||
field["sourceIndex"] = index
|
||
}
|
||
if index >= audioOrder {
|
||
audioOrder = index + 1
|
||
}
|
||
if _, exists := field["required"]; !exists {
|
||
field["required"] = index == 0
|
||
}
|
||
case "mask":
|
||
if _, exists := field["required"]; !exists {
|
||
field["required"] = false
|
||
}
|
||
}
|
||
if isManagementSeedField(fieldName) {
|
||
field["randomEnabled"] = true
|
||
}
|
||
}
|
||
return fields
|
||
}
|
||
|
||
func managementAutomaticInputSource(fieldName string, fieldType string) string {
|
||
if strings.Contains(normalizeManagementFieldName(fieldName), "mask") {
|
||
return "mask"
|
||
}
|
||
switch strings.ToUpper(strings.TrimSpace(fieldType)) {
|
||
case "IMAGE":
|
||
return "referenceImage"
|
||
case "VIDEO":
|
||
return "referenceVideo"
|
||
case "AUDIO":
|
||
return "referenceAudio"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func managementNodeIDLess(left string, right string) bool {
|
||
leftNumber, leftErr := strconv.ParseInt(strings.TrimSpace(left), 10, 64)
|
||
rightNumber, rightErr := strconv.ParseInt(strings.TrimSpace(right), 10, 64)
|
||
if leftErr == nil && rightErr == nil {
|
||
return leftNumber < rightNumber
|
||
}
|
||
return left < right
|
||
}
|
||
|
||
func managementIntValue(value any, fallback int) int {
|
||
parsed, err := strconv.Atoi(strings.TrimSpace(fmt.Sprint(value)))
|
||
if err != nil {
|
||
return fallback
|
||
}
|
||
return parsed
|
||
}
|
||
|
||
func workflowDynamicSource(name string, fieldType string) string {
|
||
return workflowDynamicSourceForMode(name, fieldType, "")
|
||
}
|
||
|
||
func workflowDynamicSourceForMode(name string, fieldType string, mode string) string {
|
||
if source := workflowNamedDynamicSource(name, mode); source != "" {
|
||
return source
|
||
}
|
||
normalizedFieldType := strings.ToUpper(strings.TrimSpace(fieldType))
|
||
switch normalizedFieldType {
|
||
case "IMAGE":
|
||
return "referenceImage"
|
||
case "VIDEO":
|
||
return "referenceVideo"
|
||
case "AUDIO":
|
||
return "referenceAudio"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func workflowNamedDynamicSource(name string, mode string) string {
|
||
key := normalizeManagementFieldName(name)
|
||
switch {
|
||
case strings.Contains(key, "mask"):
|
||
return "mask"
|
||
case workflowDimensionNameSource(key) != "":
|
||
return workflowDimensionNameSource(key)
|
||
case key == "ratio" || key == "aspectratio" || key == "imageaspectratio" || key == "imageratio" || key == "videoaspectratio" || key == "videoratio":
|
||
return "aspectRatio"
|
||
case key == "videoresolution" || key == "videoquality" || key == "vquality":
|
||
return "vquality"
|
||
case key == "size" || key == "imagesize" || key == "imageresolution":
|
||
return "size"
|
||
case key == "resolution" && strings.EqualFold(strings.TrimSpace(mode), "video"):
|
||
return "vquality"
|
||
case key == "resolution" && strings.EqualFold(strings.TrimSpace(mode), "image"):
|
||
return "size"
|
||
case key == "batch" || key == "batchsize" || key == "count" || key == "numimages" || key == "numberofimages" || key == "imagecount" || key == "imagescount":
|
||
return "count"
|
||
case key == "quality":
|
||
return "quality"
|
||
case key == "duration" || key == "seconds" || key == "durationseconds" || key == "videoseconds" || key == "videoduration" || key == "videodurationseconds" || key == "videolength" || key == "clipduration":
|
||
return "videoSeconds"
|
||
case key == "generateaudio" || key == "videogenerateaudio":
|
||
return "videoGenerateAudio"
|
||
case key == "watermark" || key == "videowatermark":
|
||
return "videoWatermark"
|
||
case key == "audioformat":
|
||
return "audioFormat"
|
||
case key == "voice" || key == "audiovoice":
|
||
return "audioVoice"
|
||
case key == "audiospeed":
|
||
return "audioSpeed"
|
||
case key == "audioinstructions":
|
||
return "audioInstructions"
|
||
case key == "transparentbackground" || key == "transparent":
|
||
return "transparentBackground"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func workflowDimensionNameSource(key string) string {
|
||
prefixes := []string{"", "image", "video", "size", "output", "target", "latent", "frame", "canvas", "source", "resolution", "final"}
|
||
for _, prefix := range prefixes {
|
||
if key == prefix+"width" {
|
||
return "width"
|
||
}
|
||
if key == prefix+"height" {
|
||
return "height"
|
||
}
|
||
}
|
||
if key == "pixelwidth" || key == "widthpixels" {
|
||
return "width"
|
||
}
|
||
if key == "pixelheight" || key == "heightpixels" {
|
||
return "height"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func normalizeManagementFieldName(value string) string {
|
||
return strings.ToLower(strings.NewReplacer("_", "", "-", "", " ", "").Replace(strings.TrimSpace(value)))
|
||
}
|
||
|
||
func isManagementSeedField(value string) bool {
|
||
key := normalizeManagementFieldName(value)
|
||
return key == "seed" || strings.HasSuffix(key, "seed") || strings.Contains(key, "noiseseed")
|
||
}
|
||
|
||
func managementFieldType(name string, value any) string {
|
||
return managementWorkflowFieldType(name, value, "")
|
||
}
|
||
|
||
func managementWorkflowFieldType(name string, value any, classType string) string {
|
||
key := normalizeManagementFieldName(name)
|
||
classKey := normalizeManagementFieldName(classType)
|
||
if strings.Contains(key, "mask") {
|
||
return "IMAGE"
|
||
}
|
||
if strings.Contains(classKey, "loadimage") && (key == "file" || key == "path") {
|
||
return "IMAGE"
|
||
}
|
||
if strings.Contains(classKey, "loadvideo") && (key == "file" || key == "path") {
|
||
return "VIDEO"
|
||
}
|
||
if strings.Contains(classKey, "loadaudio") && (key == "file" || key == "path") {
|
||
return "AUDIO"
|
||
}
|
||
if source := workflowNamedDynamicSource(name, ""); source != "" && source != "mask" {
|
||
return managementScalarFieldType(value)
|
||
}
|
||
switch value.(type) {
|
||
case bool:
|
||
return "BOOLEAN"
|
||
case float64, int, int64:
|
||
return "NUMBER"
|
||
}
|
||
if managementMediaFieldName(key, "image") {
|
||
return "IMAGE"
|
||
}
|
||
if managementMediaFieldName(key, "video") {
|
||
return "VIDEO"
|
||
}
|
||
if managementMediaFieldName(key, "audio") {
|
||
return "AUDIO"
|
||
}
|
||
// RunningHub 的部分自定义节点不会把素材字段命名为 image/video/audio,
|
||
// 但默认值仍是带扩展名的输入文件;与参考项目一致,使用文件类型补充识别。
|
||
if mediaType := managementMediaValueType(value); mediaType != "" {
|
||
return mediaType
|
||
}
|
||
return "TEXT"
|
||
}
|
||
|
||
func managementMediaValueType(value any) string {
|
||
switch items := value.(type) {
|
||
case []any:
|
||
if len(items) == 0 {
|
||
return ""
|
||
}
|
||
value = items[0]
|
||
case []string:
|
||
if len(items) == 0 {
|
||
return ""
|
||
}
|
||
value = items[0]
|
||
}
|
||
text := strings.ToLower(strings.TrimSpace(stringValue(value)))
|
||
if text == "" {
|
||
return ""
|
||
}
|
||
if index := strings.IndexAny(text, "?#"); index >= 0 {
|
||
text = text[:index]
|
||
}
|
||
for _, suffix := range []string{".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".avif"} {
|
||
if strings.HasSuffix(text, suffix) {
|
||
return "IMAGE"
|
||
}
|
||
}
|
||
for _, suffix := range []string{".mp4", ".webm", ".mov", ".m4v", ".mkv"} {
|
||
if strings.HasSuffix(text, suffix) {
|
||
return "VIDEO"
|
||
}
|
||
}
|
||
for _, suffix := range []string{".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac"} {
|
||
if strings.HasSuffix(text, suffix) {
|
||
return "AUDIO"
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func managementScalarFieldType(value any) string {
|
||
switch value.(type) {
|
||
case bool:
|
||
return "BOOLEAN"
|
||
case float32, float64, int, int32, int64:
|
||
return "NUMBER"
|
||
default:
|
||
return "TEXT"
|
||
}
|
||
}
|
||
|
||
func managementMediaFieldName(key string, mediaType string) bool {
|
||
if key == mediaType {
|
||
return true
|
||
}
|
||
prefixes := []string{"input", "reference", "ref", "source", "init", "start", "end", "first", "last", "control", "controlnet", "style", "subject"}
|
||
for _, prefix := range prefixes {
|
||
if key == prefix+mediaType {
|
||
return true
|
||
}
|
||
}
|
||
for _, suffix := range []string{"file", "path", "filename", "upload"} {
|
||
if key == mediaType+suffix {
|
||
return true
|
||
}
|
||
}
|
||
if mediaType == "image" {
|
||
return key == "firstframe" || key == "lastframe" || key == "startframe" || key == "endframe"
|
||
}
|
||
return false
|
||
}
|