package providers import ( "context" "fmt" "net/http" "strings" ) type EvoLink struct{ *httpAdapter } func NewEvoLink(c Config, client HTTPClient) *EvoLink { if client == nil { client = http.DefaultClient } a := &httpAdapter{name: "evolink", config: c, client: client, submitPath: func(Request) string { return "/v1/images/generations" }, queryPath: func(id string) string { return "/v1/tasks/" + id }, payload: func(r Request) any { payload := map[string]any{"model": requestModel(r, c.Model), "prompt": r.Prompt, "n": 1, "resolution": "1K"} if len(r.InputURLs) > 0 { payload["image_urls"] = r.InputURLs } if quality := strings.TrimSpace(stringValue(r.Settings["quality"])); quality != "" { payload["quality"] = quality } if size := strings.TrimSpace(stringValue(r.Settings["size"])); size != "" { payload["size"] = size } else if width, widthOK := positiveInteger(r.Settings["width"]); widthOK { if height, heightOK := positiveInteger(r.Settings["height"]); heightOK { payload["size"] = supportedRatio(width, height) } } return payload }, decode: decodeEvoLink} return &EvoLink{a} } func (a *EvoLink) Submit(c context.Context, r Request) (Result, error) { return a.submit(c, r) } func (a *EvoLink) Query(c context.Context, id string) (Result, error) { return a.query(c, id) } func decodeEvoLink(raw []byte) Result { r := record(raw) d := object(r["data"]) out := []string{} for _, v := range []any{r["results"], d["results"], d["images"], d["image_urls"], d["output"], d["outputs"]} { collectURLs(v, &out) } return Result{TaskID: stringValue(r["id"], r["task_id"], d["id"], d["task_id"]), Status: status(first(r["status"], d["status"])), OutputURLs: out} } type Bailian struct{ *httpAdapter } func NewBailian(c Config, client HTTPClient) *Bailian { if client == nil { client = http.DefaultClient } a := &httpAdapter{name: "bailian", config: c, client: client, submitPath: func(r Request) string { if r.Capability == "video.generate" { return "/api/v1/services/aigc/video-generation/video-synthesis" } return "/api/v1/services/aigc/image-generation/generation" }, queryPath: func(id string) string { return "/api/v1/tasks/" + id }, payload: func(r Request) any { if r.Capability == "video.generate" { media := make([]any, 0, len(r.InputURLs)) for index, inputURL := range r.InputURLs { frameType := "first_frame" if index > 0 { frameType = "last_frame" } media = append(media, map[string]any{"type": frameType, "url": inputURL}) } parameters := map[string]any{ "resolution": strings.ToUpper(stringValue(r.Settings["resolution"])), "duration": r.Settings["duration"], "prompt_extend": true, "watermark": false, } if parameters["resolution"] == "" { parameters["resolution"] = "720P" } if parameters["duration"] == nil { parameters["duration"] = 10 } return map[string]any{"model": requestModel(r, c.Model), "input": map[string]any{"prompt": r.Prompt, "media": media}, "parameters": parameters} } content := make([]any, 0, len(r.InputURLs)+1) for _, inputURL := range r.InputURLs { content = append(content, map[string]any{"image": inputURL}) } content = append(content, map[string]any{"text": r.Prompt}) parameters := map[string]any{"size": "2K", "n": 1, "watermark": false} if width, widthOK := positiveInteger(r.Settings["width"]); widthOK { if height, heightOK := positiveInteger(r.Settings["height"]); heightOK { parameters["size"] = fmt.Sprintf("%d*%d", width, height) } } if len(r.InputURLs) == 0 { parameters["thinking_mode"] = true } return map[string]any{"model": requestModel(r, c.Model), "input": map[string]any{"messages": []any{map[string]any{"role": "user", "content": content}}}, "parameters": parameters} }, headers: func(r *http.Request) { r.Header.Set("X-DashScope-Async", "enable") }, decode: decodeBailian} return &Bailian{a} } func (a *Bailian) Submit(c context.Context, r Request) (Result, error) { return a.submit(c, r) } func (a *Bailian) Query(c context.Context, id string) (Result, error) { return a.query(c, id) } func decodeBailian(raw []byte) Result { r := record(raw) o := object(r["output"]) out := []string{} collectURLs(o["results"], &out) if choices, ok := o["choices"].([]any); ok { for _, choice := range choices { message := object(object(choice)["message"]) if content, ok := message["content"].([]any); ok { for _, item := range content { collectURLs(object(item)["image"], &out) } } } } collectURLs(o["video_url"], &out) return Result{TaskID: stringValue(o["task_id"], r["task_id"]), Status: status(o["task_status"]), OutputURLs: out, ErrorMessage: stringValue(r["message"])} } type Seedance struct{ *httpAdapter } func NewSeedance(c Config, client HTTPClient) *Seedance { if client == nil { client = http.DefaultClient } a := &httpAdapter{name: "seedance", config: c, client: client, submitPath: func(Request) string { return "/contents/generations/tasks" }, queryPath: func(id string) string { return "/contents/generations/tasks/" + id }, payload: func(r Request) any { content := []any{map[string]any{"type": "text", "text": r.Prompt}} materials := r.Materials if len(materials) == 0 { materials = make([]Material, 0, len(r.InputURLs)) for _, inputURL := range r.InputURLs { materials = append(materials, Material{URL: inputURL, Type: MaterialImage}) } } for _, material := range materials { materialType, urlKey, role := "image_url", "image_url", "reference_image" switch material.Type { case MaterialVideo: materialType, urlKey, role = "video_url", "video_url", "reference_video" case MaterialAudio: materialType, urlKey, role = "audio_url", "audio_url", "reference_audio" } item := map[string]any{"type": materialType, urlKey: map[string]any{"url": material.URL}, "role": role} if material.Label != "" { item["label"] = material.Label } content = append(content, item) } p := map[string]any{"model": requestModel(r, c.Model), "content": content, "generate_audio": true, "watermark": false} for k, v := range r.Settings { p[k] = v } return p }, decode: decodeSeedance} return &Seedance{a} } func (a *Seedance) Submit(c context.Context, r Request) (Result, error) { return a.submit(c, r) } func (a *Seedance) Query(c context.Context, id string) (Result, error) { return a.query(c, id) } func decodeSeedance(raw []byte) Result { r := record(raw) d := object(r["data"]) content := object(r["content"]) if len(content) == 0 { content = object(d["content"]) } out := []string{} for _, v := range []any{content, r["video_url"], r["url"], r["output"], d} { collectURLs(v, &out) } usage := object(r["usage"]) if len(usage) == 0 { usage = object(d["usage"]) } u := map[string]int{} if n, ok := usage["completion_tokens"].(float64); ok && n > 0 { u["completionTokens"] = int(n) } return Result{TaskID: stringValue(r["id"], r["task_id"], d["id"], d["task_id"]), Status: status(first(r["status"], d["status"])), OutputURLs: out, ErrorMessage: stringValue(object(r["error"])["message"], object(d["error"])["message"]), Usage: u} } func first(values ...any) any { for _, v := range values { if v != nil { return v } } return nil } func positiveInteger(value any) (int, bool) { switch number := value.(type) { case int: return number, number > 0 case int64: return int(number), number > 0 case float64: integer := int(number) return integer, number > 0 && float64(integer) == number default: return 0, false } } func supportedRatio(width, height int) string { divisor := greatestCommonDivisor(width, height) ratio := fmt.Sprintf("%d:%d", width/divisor, height/divisor) switch ratio { case "1:1", "1:2", "2:1", "1:3", "3:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "9:21", "21:9": return ratio default: return fmt.Sprintf("%dx%d", width, height) } } func greatestCommonDivisor(left, right int) int { for right != 0 { left, right = right, left%right } return left } var _ = fmt.Sprint