fix(resource): 签发本地公开资源直链时追加文件扩展名,并对齐 AutoDL 工作流参数 (#341)
Co-authored-by: Danborad <danborad@github.com>
This commit is contained in:
@@ -312,7 +312,7 @@ func RegisterUserDataRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
}
|
||||
c.DataFromReader(stream.StatusCode, stream.ContentLength, resource.MimeType, stream.Body, nil)
|
||||
})
|
||||
r.GET("/public/resources/:id/file", func(c *gin.Context) {
|
||||
publicResourceHandler := func(c *gin.Context) {
|
||||
stream, err := svc.OpenPublicResourceRange(c.Param("id"), c.Query("expires"), c.Query("signature"), c.GetHeader("Range"))
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
@@ -336,7 +336,9 @@ func RegisterUserDataRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
return
|
||||
}
|
||||
c.DataFromReader(stream.StatusCode, stream.ContentLength, resource.MimeType, stream.Body, nil)
|
||||
})
|
||||
}
|
||||
r.GET("/public/resources/:id/file", publicResourceHandler)
|
||||
r.GET("/public/resources/:id/file/:filename", publicResourceHandler)
|
||||
r.GET("/assets", func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,11 +21,20 @@ func TestAutoDLPluginArtifact(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(adapters) != 1 {
|
||||
if len(adapters) == 0 {
|
||||
t.Fatalf("loaded adapters = %d", len(adapters))
|
||||
}
|
||||
adapter := adapters[0]
|
||||
if adapter.Metadata().ID != "autodl-comfyui" || adapter.Metadata().Version != "1.0.2" || adapter.Metadata().Execution != "declarative" {
|
||||
var adapter Adapter
|
||||
for _, a := range adapters {
|
||||
if a.Metadata().ID == "autodl-comfyui" {
|
||||
adapter = a
|
||||
break
|
||||
}
|
||||
}
|
||||
if adapter == nil {
|
||||
t.Fatal("autodl-comfyui provider adapter not found")
|
||||
}
|
||||
if adapter.Metadata().ID != "autodl-comfyui" || adapter.Metadata().Execution != "declarative" {
|
||||
t.Fatalf("metadata = %#v", adapter.Metadata())
|
||||
}
|
||||
if !adapter.Metadata().RequiresPublicMediaURLs {
|
||||
@@ -42,11 +51,135 @@ func TestAutoDLPluginArtifact(t *testing.T) {
|
||||
t.Fatalf("create spec = %#v", create)
|
||||
}
|
||||
body, ok := create.Body.(map[string]any)
|
||||
if !ok || body["prompt"] != "a cinematic test" || body["duration"] != 3 || body["resolution"] != "768p竖" || body["ref_image_0"] != "https://cdn.example/reference.png" {
|
||||
if !ok || body["prompt"] != "a cinematic test" || body["duration"] != 3 || body["resolution"] != "768p竖" {
|
||||
t.Fatalf("create body = %#v", create.Body)
|
||||
}
|
||||
if _, exists := body["ref_image_1"]; exists {
|
||||
t.Fatalf("missing reference image was not omitted: %#v", body)
|
||||
if _, exists := body["ref_image_0"]; exists {
|
||||
t.Fatalf("no_pic workflow must not send ref_image_0: %#v", body)
|
||||
}
|
||||
if _, exists := body["first_frame"]; exists {
|
||||
t.Fatalf("no_pic workflow must not send first_frame: %#v", body)
|
||||
}
|
||||
|
||||
createV5, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{
|
||||
Model: "minimax_h3_lightx2v_v5_15s", Prompt: "a cinematic test", Duration: 15, Resolution: "768p竖", Images: []MediaReference{{URL: "https://cdn.example/ref0.png"}, {URL: "https://cdn.example/ref1.png"}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyV5 := createV5.Body.(map[string]any)
|
||||
if bodyV5["ref_image_0"] != "https://cdn.example/ref0.png" || bodyV5["ref_image_1"] != "https://cdn.example/ref1.png" {
|
||||
t.Fatalf("v5 create body missing ref images = %#v", bodyV5)
|
||||
}
|
||||
if _, exists := bodyV5["first_frame"]; exists {
|
||||
t.Fatalf("v5 workflow must not send first_frame: %#v", bodyV5)
|
||||
}
|
||||
|
||||
createLightX2V, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{
|
||||
Model: "minimax_h3_lightx2v", Prompt: "a cinematic test", Duration: 5, Resolution: "768p竖", Images: []MediaReference{{URL: "https://cdn.example/first.png"}, {URL: "https://cdn.example/last.png"}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyLightX2V := createLightX2V.Body.(map[string]any)
|
||||
if bodyLightX2V["first_frame"] != "https://cdn.example/first.png" || bodyLightX2V["last_frame"] != "https://cdn.example/last.png" {
|
||||
t.Fatalf("lightx2v body = %#v", bodyLightX2V)
|
||||
}
|
||||
if _, exists := bodyLightX2V["ref_image_0"]; exists {
|
||||
t.Fatalf("lightx2v must not send ref_image_0: %#v", bodyLightX2V)
|
||||
}
|
||||
|
||||
createImageAudio, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{
|
||||
Model: "minimax_h3_image_audio_to_video", Prompt: "should be omitted", Duration: 5, Resolution: "768p横",
|
||||
Images: []MediaReference{{URL: "https://cdn.example/face.png"}},
|
||||
Audios: []MediaReference{{URL: "https://cdn.example/voice.mp3"}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyImageAudio := createImageAudio.Body.(map[string]any)
|
||||
if bodyImageAudio["ref_image_0"] != "https://cdn.example/face.png" || bodyImageAudio["ref_audio_0"] != "https://cdn.example/voice.mp3" || bodyImageAudio["audio_duration"] != 5 || bodyImageAudio["resolution"] != "768p横" {
|
||||
t.Fatalf("image_audio body = %#v", bodyImageAudio)
|
||||
}
|
||||
if _, exists := bodyImageAudio["prompt"]; exists {
|
||||
t.Fatalf("image_audio workflow must not send prompt: %#v", bodyImageAudio)
|
||||
}
|
||||
if _, exists := bodyImageAudio["duration"]; exists {
|
||||
t.Fatalf("image_audio workflow must not send duration: %#v", bodyImageAudio)
|
||||
}
|
||||
if _, exists := bodyImageAudio["ref_image_1"]; exists {
|
||||
t.Fatalf("image_audio workflow must not send ref_image_1: %#v", bodyImageAudio)
|
||||
}
|
||||
if _, exists := bodyImageAudio["ref_audio_1"]; exists {
|
||||
t.Fatalf("image_audio workflow must not send ref_audio_1: %#v", bodyImageAudio)
|
||||
}
|
||||
|
||||
createAudioToVideoV2, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{
|
||||
Model: "minimax_h3_image_audio_to_video_v2", Prompt: "cinematic sync", Duration: 8, Resolution: "1080p横",
|
||||
Images: []MediaReference{{URL: "https://cdn.example/img0.png"}, {URL: "https://cdn.example/img1.png"}},
|
||||
Audios: []MediaReference{{URL: "https://cdn.example/aud0.mp3"}, {URL: "https://cdn.example/aud1.mp3"}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyV2 := createAudioToVideoV2.Body.(map[string]any)
|
||||
if bodyV2["prompt"] != "cinematic sync" || bodyV2["duration"] != 8 || bodyV2["resolution"] != "1080p横" ||
|
||||
bodyV2["ref_image_0"] != "https://cdn.example/img0.png" || bodyV2["ref_image_1"] != "https://cdn.example/img1.png" ||
|
||||
bodyV2["ref_audio_0"] != "https://cdn.example/aud0.mp3" || bodyV2["ref_audio_1"] != "https://cdn.example/aud1.mp3" {
|
||||
t.Fatalf("v2 body = %#v", bodyV2)
|
||||
}
|
||||
if _, exists := bodyV2["audio_duration"]; exists {
|
||||
t.Fatalf("v2 must not send audio_duration: %#v", bodyV2)
|
||||
}
|
||||
|
||||
createB99001, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{
|
||||
Model: "minimax_h3_b99_001", Prompt: "a prompt test", Duration: 5, Resolution: "736p竖",
|
||||
Images: []MediaReference{{URL: "https://cdn.example/should-be-omitted.png"}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyB99001 := createB99001.Body.(map[string]any)
|
||||
if bodyB99001["prompt"] != "a prompt test" || bodyB99001["duration"] != 5 || bodyB99001["resolution"] != "736p竖" {
|
||||
t.Fatalf("b99_001 body = %#v", bodyB99001)
|
||||
}
|
||||
if _, exists := bodyB99001["ref_image_0"]; exists {
|
||||
t.Fatalf("b99_001 must not send ref_image_0: %#v", bodyB99001)
|
||||
}
|
||||
if _, exists := bodyB99001["first_frame"]; exists {
|
||||
t.Fatalf("b99_001 must not send first_frame: %#v", bodyB99001)
|
||||
}
|
||||
|
||||
createB99002, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{
|
||||
Model: "minimax_h3_b99_002", Prompt: "first to last", Duration: 6, Resolution: "736p横",
|
||||
Images: []MediaReference{{URL: "https://cdn.example/f0.png"}, {URL: "https://cdn.example/f1.png"}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyB99002 := createB99002.Body.(map[string]any)
|
||||
if bodyB99002["prompt"] != "first to last" || bodyB99002["duration"] != 6 || bodyB99002["resolution"] != "736p横" ||
|
||||
bodyB99002["first_frame"] != "https://cdn.example/f0.png" || bodyB99002["last_frame"] != "https://cdn.example/f1.png" {
|
||||
t.Fatalf("b99_002 body = %#v", bodyB99002)
|
||||
}
|
||||
if _, exists := bodyB99002["ref_image_0"]; exists {
|
||||
t.Fatalf("b99_002 must not send ref_image_0: %#v", bodyB99002)
|
||||
}
|
||||
|
||||
createB99003, err := adapter.BuildCreate(context.Background(), RequestContext{BaseURL: "https://autodl.art", Request: GenerationRequest{
|
||||
Model: "minimax_h3_b99_003_12s", Prompt: "multi image 12s", Duration: 12, Resolution: "736p(1:1)",
|
||||
Images: []MediaReference{{URL: "https://cdn.example/img0.png"}, {URL: "https://cdn.example/img1.png"}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bodyB99003 := createB99003.Body.(map[string]any)
|
||||
if bodyB99003["prompt"] != "multi image 12s" || bodyB99003["duration"] != 12 || bodyB99003["resolution"] != "736p(1:1)" ||
|
||||
bodyB99003["ref_image_0"] != "https://cdn.example/img0.png" || bodyB99003["ref_image_1"] != "https://cdn.example/img1.png" {
|
||||
t.Fatalf("b99_003 body = %#v", bodyB99003)
|
||||
}
|
||||
if _, exists := bodyB99003["first_frame"]; exists {
|
||||
t.Fatalf("b99_003 must not send first_frame: %#v", bodyB99003)
|
||||
}
|
||||
|
||||
failed, err := adapter.ParseCreate(context.Background(), []byte(`{"code":"RequestParameterIsWrong","data":null,"msg":"参数: resolution 的值: 768P 不在 options 列表中"}`))
|
||||
|
||||
@@ -372,7 +372,7 @@ func TestDeclarativeManifestKeepsLegacyVideoResolutionTransformCompatible(t *tes
|
||||
"768P横": "768p横",
|
||||
}
|
||||
for input, expected := range tests {
|
||||
if actual := applyManifestTransform(input, "video-resolution"); actual != expected {
|
||||
if actual := applyManifestTransform(input, "video-resolution", GenerationRequest{}); actual != expected {
|
||||
t.Errorf("legacy video resolution transform %q = %#v, want %q", input, actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -440,10 +441,38 @@ func (a manifestAdapter) parse(payload map[string]any, c PollContext) CreateResu
|
||||
return CreateResult{TaskID: id, Status: status, Result: result, Message: message}
|
||||
}
|
||||
|
||||
var (
|
||||
manifestMDImageRegex = regexp.MustCompile(`!\[[^\]]*\]\(([^)\s]+)\)`)
|
||||
manifestHTMLVideoRegex = regexp.MustCompile(`(?i)<video[^>]+src=['"]([^'"]+)['"]`)
|
||||
)
|
||||
|
||||
func mediaPathValues(payload map[string]any, path string) []string {
|
||||
value := pathValue(payload, path)
|
||||
if text, ok := value.(string); ok && strings.TrimSpace(text) != "" {
|
||||
return []string{strings.TrimSpace(text)}
|
||||
trimmed := strings.TrimSpace(text)
|
||||
if matches := manifestMDImageRegex.FindAllStringSubmatch(trimmed, -1); len(matches) > 0 {
|
||||
var res []string
|
||||
for _, m := range matches {
|
||||
if len(m) > 1 && m[1] != "" {
|
||||
res = append(res, m[1])
|
||||
}
|
||||
}
|
||||
if len(res) > 0 {
|
||||
return res
|
||||
}
|
||||
}
|
||||
if matches := manifestHTMLVideoRegex.FindAllStringSubmatch(trimmed, -1); len(matches) > 0 {
|
||||
var res []string
|
||||
for _, m := range matches {
|
||||
if len(m) > 1 && m[1] != "" {
|
||||
res = append(res, m[1])
|
||||
}
|
||||
}
|
||||
if len(res) > 0 {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return []string{trimmed}
|
||||
}
|
||||
items, ok := value.([]any)
|
||||
if !ok {
|
||||
@@ -477,9 +506,46 @@ func buildManifestOperation(operation ManifestOperation, request GenerationReque
|
||||
}
|
||||
setMapPath(body, key, value)
|
||||
}
|
||||
normalizedBody := normalizeManifestValue(body).(map[string]any)
|
||||
path := strings.ReplaceAll(operation.Path, "{{taskId}}", url.PathEscape(taskID))
|
||||
path = strings.ReplaceAll(path, "{{model}}", url.PathEscape(request.Model))
|
||||
return RequestSpec{Method: strings.ToUpper(operation.Method), Path: path, ContentType: defaultValue(operation.ContentType, "application/json"), Body: body}
|
||||
return RequestSpec{Method: strings.ToUpper(operation.Method), Path: path, ContentType: defaultValue(operation.ContentType, "application/json"), Body: normalizedBody}
|
||||
}
|
||||
|
||||
func normalizeManifestValue(value any) any {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
if len(v) == 0 {
|
||||
return v
|
||||
}
|
||||
isSequential := true
|
||||
for i := 0; i < len(v); i++ {
|
||||
if _, ok := v[strconv.Itoa(i)]; !ok {
|
||||
isSequential = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isSequential {
|
||||
arr := make([]any, len(v))
|
||||
for i := 0; i < len(v); i++ {
|
||||
arr[i] = normalizeManifestValue(v[strconv.Itoa(i)])
|
||||
}
|
||||
return arr
|
||||
}
|
||||
res := make(map[string]any, len(v))
|
||||
for k, val := range v {
|
||||
res[k] = normalizeManifestValue(val)
|
||||
}
|
||||
return res
|
||||
case []any:
|
||||
arr := make([]any, len(v))
|
||||
for i, val := range v {
|
||||
arr[i] = normalizeManifestValue(val)
|
||||
}
|
||||
return arr
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func manifestExpressionValue(expression string, request GenerationRequest, taskID string) any {
|
||||
@@ -501,7 +567,10 @@ func manifestExpressionValue(expression string, request GenerationRequest, taskI
|
||||
value = source
|
||||
}
|
||||
for _, transform := range transforms {
|
||||
value = applyManifestTransform(value, transform)
|
||||
value = applyManifestTransform(value, transform, request)
|
||||
if value == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -510,9 +579,40 @@ func manifestExpressionValue(expression string, request GenerationRequest, taskI
|
||||
// platform request. It lets uploaded manifests address media items by index
|
||||
// without exposing Go structs or adding provider-specific host code.
|
||||
func manifestRequestValues(request GenerationRequest) map[string]any {
|
||||
userContent := make([]any, 0)
|
||||
if strings.TrimSpace(request.Prompt) != "" {
|
||||
userContent = append(userContent, map[string]any{"type": "text", "text": request.Prompt})
|
||||
}
|
||||
for _, img := range request.Images {
|
||||
val := defaultValue(img.URL, img.DataURL)
|
||||
if val != "" {
|
||||
userContent = append(userContent, map[string]any{"type": "image_url", "image_url": map[string]any{"url": val}})
|
||||
}
|
||||
}
|
||||
for _, vid := range request.Videos {
|
||||
val := defaultValue(vid.URL, vid.DataURL)
|
||||
if val != "" {
|
||||
userContent = append(userContent, map[string]any{"type": "video_url", "video_url": map[string]any{"url": val}})
|
||||
}
|
||||
}
|
||||
for _, aud := range request.Audios {
|
||||
val := defaultValue(aud.URL, aud.DataURL)
|
||||
if val != "" {
|
||||
userContent = append(userContent, map[string]any{"type": "audio_url", "audio_url": map[string]any{"url": val}})
|
||||
}
|
||||
}
|
||||
messages := make([]any, 0)
|
||||
if len(userContent) > 0 {
|
||||
messages = append(messages, map[string]any{
|
||||
"role": "user",
|
||||
"content": userContent,
|
||||
})
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"model": request.Model,
|
||||
"prompt": request.Prompt,
|
||||
"messages": messages,
|
||||
"images": manifestMediaValues(request.Images),
|
||||
"videos": manifestMediaValues(request.Videos),
|
||||
"audios": manifestMediaValues(request.Audios),
|
||||
@@ -528,6 +628,14 @@ func manifestRequestValues(request GenerationRequest) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func manifestModelID(request GenerationRequest) string {
|
||||
modelID := strings.TrimSpace(request.Model)
|
||||
if separator := strings.LastIndex(modelID, "::"); separator >= 0 {
|
||||
modelID = modelID[separator+2:]
|
||||
}
|
||||
return strings.ToLower(modelID)
|
||||
}
|
||||
|
||||
func manifestMediaValues(values []MediaReference) []any {
|
||||
result := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
@@ -541,9 +649,90 @@ func manifestMediaValues(values []MediaReference) []any {
|
||||
return result
|
||||
}
|
||||
|
||||
func applyManifestTransform(value any, transform string) any {
|
||||
func applyManifestTransform(value any, transform string, request GenerationRequest) any {
|
||||
transform = strings.ToLower(strings.TrimSpace(transform))
|
||||
if transform == "bool" || transform == "boolean" {
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
return v
|
||||
case string:
|
||||
s := strings.ToLower(strings.TrimSpace(v))
|
||||
return s == "true" || s == "1" || s == "yes"
|
||||
case int, int64, float64:
|
||||
return v != 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
if transform == "int" || transform == "integer" {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int64:
|
||||
return int(v)
|
||||
case float64:
|
||||
return int(v)
|
||||
case string:
|
||||
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
|
||||
return n
|
||||
}
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if transform == "omit_zero" {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
if v == 0 {
|
||||
return nil
|
||||
}
|
||||
case int64:
|
||||
if v == 0 {
|
||||
return nil
|
||||
}
|
||||
case float64:
|
||||
if v == 0 {
|
||||
return nil
|
||||
}
|
||||
case string:
|
||||
if strings.TrimSpace(v) == "0" || strings.TrimSpace(v) == "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanModel := manifestModelID(request)
|
||||
if strings.HasPrefix(transform, "omit_unless_model_contains:") {
|
||||
needle := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(transform, "omit_unless_model_contains:")))
|
||||
if needle == "" || !strings.Contains(cleanModel, needle) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(transform, "omit_if_model_contains:") {
|
||||
needle := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(transform, "omit_if_model_contains:")))
|
||||
if needle != "" && strings.Contains(cleanModel, needle) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(transform, "omit_unless_model_equals:") {
|
||||
target := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(transform, "omit_unless_model_equals:")))
|
||||
if target == "" || cleanModel != target {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(transform, "omit_if_model_equals:") {
|
||||
target := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(transform, "omit_if_model_equals:")))
|
||||
if target != "" && cleanModel == target {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
text, ok := value.(string)
|
||||
if transform == "omit_empty" && (!ok || strings.TrimSpace(text) == "") {
|
||||
return nil
|
||||
}
|
||||
if transform == "omit_auto" && (!ok || strings.TrimSpace(text) == "" || strings.EqualFold(strings.TrimSpace(text), "auto") || strings.EqualFold(strings.TrimSpace(text), "default")) {
|
||||
return nil
|
||||
}
|
||||
if !ok {
|
||||
return value
|
||||
}
|
||||
@@ -554,6 +743,11 @@ func applyManifestTransform(value any, transform string) any {
|
||||
return strings.ToLower(text)
|
||||
case "upper", "uppercase":
|
||||
return strings.ToUpper(text)
|
||||
case "resolution_p":
|
||||
if regexp.MustCompile(`^\d+$`).MatchString(strings.TrimSpace(text)) {
|
||||
return strings.TrimSpace(text) + "p"
|
||||
}
|
||||
return text
|
||||
case "video-resolution", "video_resolution", "videoresolution":
|
||||
// Compatibility for already-installed 1.0.1 manifests. New plugins
|
||||
// should declare exact enum values and use generic string transforms.
|
||||
|
||||
@@ -176,7 +176,7 @@ func TestAutoDLPluginPackageInstallsThroughUploadRuntime(t *testing.T) {
|
||||
t.Fatal("legacy AutoDL provider ID leaked into administrator catalog")
|
||||
}
|
||||
}
|
||||
if autoDL == nil || len(autoDL.Workflows) != 1 {
|
||||
if autoDL == nil || len(autoDL.Workflows) == 0 {
|
||||
t.Fatalf("AutoDL administrator catalog = %#v", catalog)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func (s *Service) directResourceURL(resource *model.Resource, expiresAt time.Tim
|
||||
return "", BadAuthRequest("资源尚未上传完成")
|
||||
}
|
||||
if resource.Provider == "local" {
|
||||
return s.signedPublicResourceURL(resource.ID, expiresAt)
|
||||
return s.signedPublicResourceURL(resource, expiresAt)
|
||||
}
|
||||
setting, err := s.ossSettingForResource(resource.UserID, resource)
|
||||
if err != nil {
|
||||
@@ -104,7 +104,7 @@ func (s *Service) directResourceURL(resource *model.Resource, expiresAt time.Tim
|
||||
setting.Endpoint = firstNonEmpty(resource.Endpoint, setting.Endpoint)
|
||||
setting.Bucket = firstNonEmpty(resource.Bucket, setting.Bucket)
|
||||
if setting.Provider == s3Provider && !publicHTTPSStorageEndpoint(setting.Endpoint) {
|
||||
return s.signedHTTPSPublicResourceURL(resource.ID, expiresAt)
|
||||
return s.signedHTTPSPublicResourceURL(resource, expiresAt)
|
||||
}
|
||||
return signedOSSObjectURL(setting, resource.ObjectKey, expiresAt)
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (s *Service) prepareResourceDelivery(userID string, resource *model.Resourc
|
||||
}
|
||||
if options.ForceDirect {
|
||||
if setting.Provider == s3Provider && !publicHTTPSStorageEndpoint(setting.Endpoint) {
|
||||
redirectURL, err := s.signedHTTPSPublicResourceURL(resource.ID, time.Now().Add(directResourceURLTTL))
|
||||
redirectURL, err := s.signedHTTPSPublicResourceURL(resource, time.Now().Add(directResourceURLTTL))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -172,17 +172,28 @@ func (s *Service) prepareResourceDelivery(userID string, resource *model.Resourc
|
||||
return &ResourceDelivery{Resource: resource}, nil
|
||||
}
|
||||
|
||||
func (s *Service) signedPublicResourceURL(resourceID string, expiresAt time.Time) (string, error) {
|
||||
func (s *Service) signedPublicResourceURL(resource *model.Resource, expiresAt time.Time) (string, error) {
|
||||
if resource == nil {
|
||||
return "", errors.New("资源不存在")
|
||||
}
|
||||
baseURL, err := s.publicResourceBaseURL()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expires := strconv.FormatInt(expiresAt.UTC().Unix(), 10)
|
||||
signature, err := s.signPublicResource(resourceID, expires)
|
||||
signature, err := s.signPublicResource(resource.ID, expires)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
baseURL.Path = strings.TrimRight(baseURL.Path, "/") + "/api/public/resources/" + url.PathEscape(resourceID) + "/file"
|
||||
ext := resourceFileExtension(resource.ObjectKey, resource.MimeType, resource.Kind)
|
||||
filename := resource.ID
|
||||
if ext != "" {
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
ext = "." + ext
|
||||
}
|
||||
filename += ext
|
||||
}
|
||||
baseURL.Path = strings.TrimRight(baseURL.Path, "/") + "/api/public/resources/" + url.PathEscape(resource.ID) + "/file/" + url.PathEscape(filename)
|
||||
query := baseURL.Query()
|
||||
query.Set("expires", expires)
|
||||
query.Set("signature", signature)
|
||||
@@ -190,7 +201,7 @@ func (s *Service) signedPublicResourceURL(resourceID string, expiresAt time.Time
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
func (s *Service) signedHTTPSPublicResourceURL(resourceID string, expiresAt time.Time) (string, error) {
|
||||
func (s *Service) signedHTTPSPublicResourceURL(resource *model.Resource, expiresAt time.Time) (string, error) {
|
||||
baseURL, err := s.publicResourceBaseURL()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -198,7 +209,7 @@ func (s *Service) signedHTTPSPublicResourceURL(resourceID string, expiresAt time
|
||||
if baseURL.Scheme != "https" {
|
||||
return "", BadAuthRequest("私网或 HTTP S3 用于上游资源时,服务器公开访问地址必须使用 HTTPS")
|
||||
}
|
||||
return s.signedPublicResourceURL(resourceID, expiresAt)
|
||||
return s.signedPublicResourceURL(resource, expiresAt)
|
||||
}
|
||||
|
||||
func (s *Service) verifyPublicResourceSignature(resourceID string, expires string, signature string) error {
|
||||
|
||||
@@ -718,7 +718,7 @@ func TestHydrateNewAPIChannel1ResourceUsesSignedLocalURL(t *testing.T) {
|
||||
if err := svc.hydrateProviderMedia("user-1", &media, true); err != nil {
|
||||
t.Fatalf("hydrateProviderMedia() error = %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(media.URL, server.URL+"/api/public/resources/resource-local/file?") || !strings.Contains(media.URL, "signature=") || media.DataURL != "" {
|
||||
if !strings.HasPrefix(media.URL, server.URL+"/api/public/resources/resource-local/file/resource-local.png?") || !strings.Contains(media.URL, "signature=") || media.DataURL != "" {
|
||||
t.Fatalf("media = %#v", media)
|
||||
}
|
||||
stored, err := svc.repo.Resource("resource-local")
|
||||
|
||||
Binary file not shown.
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"apiVersion": "yingce.plugin/v1",
|
||||
"id": "autodl-comfyui",
|
||||
"name": "AutoDL ComfyUI 视频",
|
||||
"version": "1.0.2",
|
||||
"name": "AutoDL ComfyUI 视频与音频",
|
||||
"version": "1.0.3",
|
||||
"author": "影策社区",
|
||||
"description": "通过 AutoDL.Art ComfyUI 工作流 API 接入异步视频生成。",
|
||||
"documentation": "# AutoDL ComfyUI 视频\n\n## 接口\n\n创建任务:`POST /api/v1/comfyui/comfyui_workflow/{workflow_id}`。清单把渠道中的模型字段作为 `workflow_id`,因此每个 ComfyUI 工作流都可以通过工作流贡献加入,不需要修改宿主代码。\n\n查询任务:`GET /api/v1/comfyui/comfyui_workflow/result/{task_id}`。\n\n## 工作流\n\n示例 `minimax_h3_lightx2v_no_pic` 作为 MiniMax H3 文生视频入口,默认时长为 5 秒、分辨率为 `768p竖`。支持 `480p竖`、`768p竖`、`480p横`、`768p横`。上传后可以在渠道中直接选择该工作流;新增工作流只需扩展本清单。\n\n## 请求与鉴权\n\n创建请求为 JSON,发送 `prompt`、`duration`、`resolution`,并按存在的参考图片发送 `ref_image_0` 至 `ref_image_8`。分辨率选项由画布按工作流声明完整保留,插件在发送前仅统一转换为小写;`768p竖`、`768p横` 等值不会被宿主折叠为通用数字档位。参考图片由宿主转换为短期签名公网 URL。AutoDL Token 在渠道密钥字段中配置,宿主以后端出站请求发送 `Authorization: Bearer <token>`;清单不包含密钥。\n\n## 状态与结果\n\n`QUEUED` 和 `RUNNING` 映射为统一任务的排队与处理中,`SUCCESS` 和 `completed` 映射为成功,`FAILED` 映射为失败。错误响应的 `code` 和 `msg` 会保留为任务错误。成功结果从 `data.results[].url` 读取为视频地址。AutoDL 地址是短期地址,清单标记为临时结果,宿主会在完成时下载并保存到影策资源存储。\n\n官方文档:[AutoDL ComfyUI API](https://autodl.art/docs/comfyui_api/)。",
|
||||
"description": "通过 AutoDL.Art ComfyUI 工作流 API 接入异步视频与音频生成,支持 H3 视频系列及 IndexTTS 语音工作流。",
|
||||
"documentation": "# AutoDL ComfyUI 工作流\n\n## 接口\n\n- 创建任务:`POST /api/v1/comfyui/comfyui_workflow/{workflow_id}`\n- 查询结果:`GET /api/v1/comfyui/comfyui_workflow/result/{task_id}`\n\n## 鉴权\n\n在渠道密钥中配置 AutoDL ComfyUI 令牌(Authorization: <TOKEN>)。\n\n## 工作流清单(8 个工作流)\n\n1. `minimax_h3_image_audio_to_video_v2_15s` - H3 多图多音频生视频 15 秒\n2. `minimax_h3_lightx2v_v5_15s` - H3 多图生视频 15 秒\n3. `minimax_h3_image_audio_to_video_v2` - H3 多图多音频生视频\n4. `minimax_h3_image_audio_to_video` - H3 图生视频音频同步\n5. `minimax_h3_lightx2v_v5` - H3 多图参考生视频\n6. `minimax_h3_lightx2v_no_pic` - H3 文生视频\n7. `minimax_h3_lightx2v` - H3 首尾帧生成视频\n8. `indextts2-v1` - IndexTTS2 语音\n",
|
||||
"permissions": [
|
||||
"generation.run",
|
||||
"media.read"
|
||||
@@ -17,7 +17,7 @@
|
||||
"type": "secret",
|
||||
"label": "AutoDL Token",
|
||||
"required": true,
|
||||
"description": "填写 AutoDL Token;仅保存到渠道密钥配置。"
|
||||
"description": "填写 AutoDL 大模型令牌管理中创建的 ComfyUI 分组 Token。"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -38,7 +38,7 @@
|
||||
"baseUrl": "https://autodl.art",
|
||||
"requiresPublicMediaUrls": true,
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"type": "header",
|
||||
"field": "apiKey",
|
||||
"header": "Authorization"
|
||||
},
|
||||
@@ -54,13 +54,13 @@
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"mapping": "duration",
|
||||
"description": "视频时长,单位由工作流决定。"
|
||||
"description": "视频时长(秒)。"
|
||||
},
|
||||
{
|
||||
"name": "resolution",
|
||||
"type": "string",
|
||||
"mapping": "resolution",
|
||||
"description": "工作流支持的分辨率档位。"
|
||||
"description": "输出分辨率档位,如 480p竖、768p竖、1080p竖、480p横、768p横、1080p横、480p(1:1)、768p(1:1) 等。"
|
||||
}
|
||||
],
|
||||
"create": {
|
||||
@@ -68,18 +68,24 @@
|
||||
"path": "/api/v1/comfyui/comfyui_workflow/{{model}}",
|
||||
"contentType": "application/json",
|
||||
"fields": {
|
||||
"prompt": "request.prompt",
|
||||
"duration": "request.duration",
|
||||
"prompt": "request.prompt|omit_if_model_equals:minimax_h3_image_audio_to_video",
|
||||
"duration": "request.duration|omit_if_model_equals:minimax_h3_image_audio_to_video",
|
||||
"audio_duration": "request.duration|omit_unless_model_equals:minimax_h3_image_audio_to_video",
|
||||
"resolution": "request.resolution|lower",
|
||||
"ref_image_0": "request.images.0.url",
|
||||
"ref_image_1": "request.images.1.url",
|
||||
"ref_image_2": "request.images.2.url",
|
||||
"ref_image_3": "request.images.3.url",
|
||||
"ref_image_4": "request.images.4.url",
|
||||
"ref_image_5": "request.images.5.url",
|
||||
"ref_image_6": "request.images.6.url",
|
||||
"ref_image_7": "request.images.7.url",
|
||||
"ref_image_8": "request.images.8.url"
|
||||
"ref_image_0": "request.images.0.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001",
|
||||
"ref_image_1": "request.images.1.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001",
|
||||
"ref_image_2": "request.images.2.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001",
|
||||
"ref_image_3": "request.images.3.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001",
|
||||
"ref_image_4": "request.images.4.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001",
|
||||
"ref_image_5": "request.images.5.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001",
|
||||
"ref_image_6": "request.images.6.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001",
|
||||
"ref_image_7": "request.images.7.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001",
|
||||
"ref_image_8": "request.images.8.url|omit_if_model_equals:minimax_h3_lightx2v|omit_if_model_equals:minimax_h3_b99_002|omit_if_model_equals:minimax_h3_image_audio_to_video|omit_if_model_contains:no_pic|omit_if_model_contains:b99_001",
|
||||
"first_frame": "request.images.0.url|omit_if_model_contains:v5|omit_if_model_contains:no_pic|omit_if_model_contains:image_audio_to_video|omit_if_model_contains:b99_001|omit_if_model_contains:b99_003",
|
||||
"last_frame": "request.images.1.url|omit_if_model_contains:v5|omit_if_model_contains:no_pic|omit_if_model_contains:image_audio_to_video|omit_if_model_contains:b99_001|omit_if_model_contains:b99_003",
|
||||
"ref_audio_0": "request.audios.0.url|omit_unless_model_contains:image_audio_to_video",
|
||||
"ref_audio_1": "request.audios.1.url|omit_unless_model_contains:image_audio_to_video_v2",
|
||||
"ref_audio_2": "request.audios.2.url|omit_unless_model_contains:image_audio_to_video_v2"
|
||||
}
|
||||
},
|
||||
"poll": {
|
||||
@@ -106,44 +112,200 @@
|
||||
"resultKind": "video",
|
||||
"resultEphemeral": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "autodl-comfyui-audio",
|
||||
"label": "AutoDL ComfyUI 语音",
|
||||
"capabilities": [
|
||||
"audio"
|
||||
],
|
||||
"scopes": [
|
||||
"admin.system-channel",
|
||||
"user.custom-channel",
|
||||
"canvas",
|
||||
"creation"
|
||||
],
|
||||
"baseUrl": "https://autodl.art",
|
||||
"requiresPublicMediaUrls": true,
|
||||
"auth": {
|
||||
"type": "header",
|
||||
"field": "apiKey",
|
||||
"header": "Authorization"
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "prompt_text",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"mapping": "prompt_text",
|
||||
"description": "语音合成文本。"
|
||||
}
|
||||
],
|
||||
"create": {
|
||||
"method": "POST",
|
||||
"path": "/api/v1/comfyui/comfyui_workflow/{{model}}",
|
||||
"contentType": "application/json",
|
||||
"fields": {
|
||||
"prompt_text": "request.prompt",
|
||||
"prompt_simple": "request.audios.0.url",
|
||||
"emo_control_method": "与音色参考音频相同"
|
||||
}
|
||||
},
|
||||
"poll": {
|
||||
"method": "GET",
|
||||
"path": "/api/v1/comfyui/comfyui_workflow/result/{{taskId}}"
|
||||
},
|
||||
"response": {
|
||||
"taskIdPaths": [
|
||||
"data.task_id"
|
||||
],
|
||||
"statusPaths": [
|
||||
"data.status"
|
||||
],
|
||||
"errorPaths": [
|
||||
"code"
|
||||
],
|
||||
"messagePaths": [
|
||||
"data.message",
|
||||
"msg"
|
||||
],
|
||||
"resultPaths": [
|
||||
"data.results"
|
||||
],
|
||||
"resultKind": "audio",
|
||||
"resultEphemeral": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"workflows": [
|
||||
{
|
||||
"id": "minimax_h3_lightx2v_no_pic",
|
||||
"label": "MiniMax H3 文生视频",
|
||||
"id": "minimax_h3_b99_002",
|
||||
"label": "H3 首尾帧生成视频",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "prompt",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"mapping": "prompt"
|
||||
},
|
||||
{
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"mapping": "duration",
|
||||
"description": "视频时长。"
|
||||
},
|
||||
{
|
||||
"name": "resolution",
|
||||
"type": "string",
|
||||
"mapping": "resolution",
|
||||
"values": [
|
||||
"480p竖",
|
||||
"768p竖",
|
||||
"480p横",
|
||||
"768p横"
|
||||
],
|
||||
"description": "输出分辨率档位。"
|
||||
}
|
||||
{ "name": "prompt", "type": "string", "required": true, "mapping": "prompt" },
|
||||
{ "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["736p竖", "736p横", "736p(1:1)"] }
|
||||
],
|
||||
"defaults": {
|
||||
"duration": 5,
|
||||
"resolution": "768p竖"
|
||||
}
|
||||
"defaults": { "duration": 5, "resolution": "736p竖" }
|
||||
},
|
||||
{
|
||||
"id": "minimax_h3_b99_001",
|
||||
"label": "H3 文生视频",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{ "name": "prompt", "type": "string", "required": true, "mapping": "prompt" },
|
||||
{ "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["736p竖", "736p横", "736p(1:1)"] }
|
||||
],
|
||||
"defaults": { "duration": 5, "resolution": "736p竖" }
|
||||
},
|
||||
{
|
||||
"id": "minimax_h3_b99_003_12s",
|
||||
"label": "H3 多图生视频 12 秒",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{ "name": "prompt", "type": "string", "required": true, "mapping": "prompt" },
|
||||
{ "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~12 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["736p竖", "736p横", "736p(1:1)"] }
|
||||
],
|
||||
"defaults": { "duration": 5, "resolution": "736p竖" }
|
||||
},
|
||||
{
|
||||
"id": "minimax_h3_image_audio_to_video_v2_15s",
|
||||
"label": "H3 多图多音频生视频 15 秒",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{ "name": "prompt", "type": "string", "required": true, "mapping": "prompt" },
|
||||
{ "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "480p横", "768p横"] }
|
||||
],
|
||||
"defaults": { "duration": 5, "resolution": "768p竖" }
|
||||
},
|
||||
{
|
||||
"id": "minimax_h3_lightx2v_v5_15s",
|
||||
"label": "H3 多图生视频 15 秒",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{ "name": "prompt", "type": "string", "required": true, "mapping": "prompt" },
|
||||
{ "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "480p横", "768p横", "480p(1:1)", "768p(1:1)"] }
|
||||
],
|
||||
"defaults": { "duration": 5, "resolution": "768p竖" }
|
||||
},
|
||||
{
|
||||
"id": "minimax_h3_image_audio_to_video_v2",
|
||||
"label": "H3 多图多音频生视频",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{ "name": "prompt", "type": "string", "required": true, "mapping": "prompt" },
|
||||
{ "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~10 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "1080p竖", "480p横", "768p横", "1080p横"] }
|
||||
],
|
||||
"defaults": { "duration": 5, "resolution": "768p竖" }
|
||||
},
|
||||
{
|
||||
"id": "minimax_h3_image_audio_to_video",
|
||||
"label": "H3 图生视频音频同步",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{ "name": "audio_duration", "type": "integer", "mapping": "duration", "description": "音频时长 1~15 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "1080p竖", "480p横", "768p横", "1080p横"] }
|
||||
],
|
||||
"defaults": { "duration": 5, "resolution": "768p竖" }
|
||||
},
|
||||
{
|
||||
"id": "minimax_h3_lightx2v_v5",
|
||||
"label": "H3 多图参考生视频",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{ "name": "prompt", "type": "string", "required": true, "mapping": "prompt" },
|
||||
{ "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~10 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "1080p竖", "480p横", "768p横", "1080p横", "480p(1:1)", "768p(1:1)", "1080p(1:1)"] }
|
||||
],
|
||||
"defaults": { "duration": 5, "resolution": "768p竖" }
|
||||
},
|
||||
{
|
||||
"id": "minimax_h3_lightx2v_no_pic",
|
||||
"label": "H3 文生视频",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{ "name": "prompt", "type": "string", "required": true, "mapping": "prompt" },
|
||||
{ "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "480p横", "768p横"] }
|
||||
],
|
||||
"defaults": { "duration": 5, "resolution": "768p竖" }
|
||||
},
|
||||
{
|
||||
"id": "minimax_h3_lightx2v",
|
||||
"label": "H3 首尾帧生成视频",
|
||||
"providerId": "autodl-comfyui",
|
||||
"capability": "video",
|
||||
"parameters": [
|
||||
{ "name": "prompt", "type": "string", "required": true, "mapping": "prompt" },
|
||||
{ "name": "duration", "type": "integer", "mapping": "duration", "description": "视频时长 1~15 秒" },
|
||||
{ "name": "resolution", "type": "string", "mapping": "resolution", "values": ["480p竖", "768p竖", "480p横", "768p横"] }
|
||||
],
|
||||
"defaults": { "duration": 5, "resolution": "768p竖" }
|
||||
},
|
||||
{
|
||||
"id": "indextts2-v1",
|
||||
"label": "IndexTTS2 语音",
|
||||
"providerId": "autodl-comfyui-audio",
|
||||
"capability": "audio",
|
||||
"parameters": [
|
||||
{ "name": "prompt_text", "type": "string", "required": true, "mapping": "prompt" }
|
||||
],
|
||||
"defaults": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user