Add multipart image edit support
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"log/slog"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -75,6 +77,87 @@ func TestCanonicalConversationMessageHashUsesTextAndAssetRefs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageEditMultipartFormBodyMapsFilesAndFields(t *testing.T) {
|
||||
var raw bytes.Buffer
|
||||
writer := multipart.NewWriter(&raw)
|
||||
if err := writer.WriteField("model", "doubao-5.0图像编辑"); err != nil {
|
||||
t.Fatalf("write model field: %v", err)
|
||||
}
|
||||
if err := writer.WriteField("prompt", "换个姿势"); err != nil {
|
||||
t.Fatalf("write prompt field: %v", err)
|
||||
}
|
||||
if err := writer.WriteField("n", "2"); err != nil {
|
||||
t.Fatalf("write n field: %v", err)
|
||||
}
|
||||
if err := writer.WriteField("sequential_image_generation_options", `{"max_images":2}`); err != nil {
|
||||
t.Fatalf("write sequential options field: %v", err)
|
||||
}
|
||||
writeMultipartFixtureFile(t, writer, "image", "single.png")
|
||||
writeMultipartFixtureFile(t, writer, "images", "ref-a.png")
|
||||
writeMultipartFixtureFile(t, writer, "images[]", "ref-b.png")
|
||||
writeMultipartFixtureFile(t, writer, "mask", "mask.png")
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("close multipart writer: %v", err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/images/edits", &raw)
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
if err := request.ParseMultipartForm(multipartTaskMemoryBytes); err != nil {
|
||||
t.Fatalf("parse multipart form: %v", err)
|
||||
}
|
||||
defer request.MultipartForm.RemoveAll()
|
||||
|
||||
body, err := imageEditMultipartFormBody(context.Background(), request.MultipartForm, func(_ context.Context, field string, header *multipart.FileHeader) (map[string]any, error) {
|
||||
ref := map[string]any{
|
||||
"sha256": field + "-" + header.Filename,
|
||||
"url": "https://cdn.example/" + header.Filename,
|
||||
"contentType": header.Header.Get("Content-Type"),
|
||||
"storageProvider": "server_main_openapi",
|
||||
}
|
||||
return requestAssetWrapper(ref), nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("build multipart image edit body: %v", err)
|
||||
}
|
||||
if body["model"] != "doubao-5.0图像编辑" || body["prompt"] != "换个姿势" {
|
||||
t.Fatalf("unexpected scalar fields: %+v", body)
|
||||
}
|
||||
if body["n"] != float64(2) {
|
||||
t.Fatalf("n should be parsed as number, got %#v", body["n"])
|
||||
}
|
||||
options, _ := body["sequential_image_generation_options"].(map[string]any)
|
||||
if options["max_images"] != float64(2) {
|
||||
t.Fatalf("sequential options should parse JSON object, got %+v", options)
|
||||
}
|
||||
image, _ := body["image"].(map[string]any)
|
||||
if image["url"] != "https://cdn.example/single.png" {
|
||||
t.Fatalf("single image should map to image URL wrapper, got %+v", image)
|
||||
}
|
||||
images, _ := body["images"].([]any)
|
||||
if len(images) != 2 {
|
||||
t.Fatalf("multi image fields should map to images array, got %+v", body["images"])
|
||||
}
|
||||
firstMulti, _ := images[0].(map[string]any)
|
||||
secondMulti, _ := images[1].(map[string]any)
|
||||
if firstMulti["url"] != "https://cdn.example/ref-a.png" || secondMulti["url"] != "https://cdn.example/ref-b.png" {
|
||||
t.Fatalf("unexpected images array: %+v", images)
|
||||
}
|
||||
mask, _ := body["mask"].(map[string]any)
|
||||
if mask["url"] != "https://cdn.example/mask.png" {
|
||||
t.Fatalf("mask should map to mask URL wrapper, got %+v", mask)
|
||||
}
|
||||
}
|
||||
|
||||
func writeMultipartFixtureFile(t *testing.T, writer *multipart.Writer, field string, filename string) {
|
||||
t.Helper()
|
||||
part, err := writer.CreateFormFile(field, filename)
|
||||
if err != nil {
|
||||
t.Fatalf("create multipart file %s/%s: %v", field, filename, err)
|
||||
}
|
||||
if _, err := part.Write([]byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}); err != nil {
|
||||
t.Fatalf("write multipart file %s/%s: %v", field, filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanupExpiredLocalTempAssetsDeletesExpiredStaticFiles(t *testing.T) {
|
||||
uploadedDir := t.TempDir()
|
||||
generatedDir := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user