Keep Gemini image edits role-aware on the server path
The server now resolves source, identity, and style artifacts separately, emits role-labeled Gemini parts, and logs the effective request shape so deployed debugging can confirm which references reached upstream. Constraint: The test deployment must stay on gemini-3.1-flash-image-preview while gaining better multi-image editing semantics Rejected: Change the default model to gemini-3-pro-image-preview | test-server parity required keeping flash as default Confidence: high Scope-risk: moderate Reversibility: clean Directive: Preserve the source/identity/style logging until the upstream gateway exposes equivalent structured traces Tested: go test ./internal/server Tested: image img2img against http://101.42.99.35:18080/v1 (job_5af212df2c27) Not-tested: Non-Gemini multi-image providers still fall back to the first reference only
This commit is contained in:
@@ -52,11 +52,11 @@ func TestGenerateGeminiImageRefsUsesGenerateContentEndpoint(t *testing.T) {
|
||||
refs, usage, err := client.generateEditedImageRefs(context.Background(), "sk-test", "gemini-3-pro-image-preview", map[string]any{
|
||||
"prompt": "edit this image",
|
||||
"size": "1024x1536",
|
||||
}, imageEditReference{
|
||||
}, []imageEditReference{{
|
||||
Filename: "source.png",
|
||||
ContentType: "image/png",
|
||||
Content: refBytes,
|
||||
})
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("generateEditedImageRefs: %v", err)
|
||||
}
|
||||
@@ -185,12 +185,12 @@ func TestGenerateEditedImageRefsUsesSeedreamImagesGenerationsEndpoint(t *testing
|
||||
refs, usage, err := client.generateEditedImageRefs(context.Background(), "sk-test", "seedream-4-5-251128", map[string]any{
|
||||
"prompt": "edit this image into a dusk scene",
|
||||
"size": "2K",
|
||||
}, imageEditReference{
|
||||
}, []imageEditReference{{
|
||||
Filename: "source.jpg",
|
||||
ContentType: "image/jpeg",
|
||||
Content: []byte("binary-image"),
|
||||
URL: "https://example.com/reference.jpg",
|
||||
})
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("generateEditedImageRefs: %v", err)
|
||||
}
|
||||
@@ -347,9 +347,9 @@ func TestGenerateMiniMaxEditedImageRefsSendsStandardImageField(t *testing.T) {
|
||||
"prompt": "turn it into watercolor",
|
||||
"size": "832x1248",
|
||||
"response_format": "url",
|
||||
}, imageEditReference{
|
||||
}, []imageEditReference{{
|
||||
URL: "https://example.com/reference.jpg",
|
||||
})
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("generateEditedImageRefs: %v", err)
|
||||
}
|
||||
@@ -360,3 +360,70 @@ func TestGenerateMiniMaxEditedImageRefsSendsStandardImageField(t *testing.T) {
|
||||
t.Fatalf("expected size passthrough for minimax mapping, got %#v", gotBody["size"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateGeminiImageRefsAnnotatesMultiImageRoles(t *testing.T) {
|
||||
const png1x1 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Z7xkAAAAASUVORK5CYII="
|
||||
|
||||
var gotBody map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("decode request body: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"candidates": [{
|
||||
"content": {
|
||||
"parts": [{
|
||||
"inlineData": {
|
||||
"mimeType": "image/png",
|
||||
"data": "` + png1x1 + `"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
refBytes, err := base64.StdEncoding.DecodeString(png1x1)
|
||||
if err != nil {
|
||||
t.Fatalf("decode fixture: %v", err)
|
||||
}
|
||||
client := &newAPIClient{
|
||||
baseURL: strings.TrimRight(srv.URL, "/"),
|
||||
httpClient: srv.Client(),
|
||||
}
|
||||
|
||||
_, _, err = client.generateGeminiImageRefs(context.Background(), "sk-test", "gemini-3-pro-image-preview", map[string]any{
|
||||
"prompt": "Replace the person in Image 1 with the character from Image 2 and apply the style from Image 3.",
|
||||
}, []imageEditReference{
|
||||
{Role: "source", Filename: "source.png", ContentType: "image/png", Content: refBytes},
|
||||
{Role: "identity", Filename: "identity.png", ContentType: "image/png", Content: refBytes},
|
||||
{Role: "style", Filename: "style.png", ContentType: "image/png", Content: refBytes},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generateGeminiImageRefs: %v", err)
|
||||
}
|
||||
|
||||
contents, ok := gotBody["contents"].([]any)
|
||||
if !ok || len(contents) != 1 {
|
||||
t.Fatalf("unexpected contents: %#v", gotBody["contents"])
|
||||
}
|
||||
content, ok := contents[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected content entry: %#v", contents[0])
|
||||
}
|
||||
parts, ok := content["parts"].([]any)
|
||||
if !ok || len(parts) != 7 {
|
||||
t.Fatalf("expected prompt plus three labeled images, got %#v", content["parts"])
|
||||
}
|
||||
if got := parts[1].(map[string]any)["text"]; got != "Image 1 is the source scene. Keep the scene layout, camera framing, main action, and spatial relationships from this image." {
|
||||
t.Fatalf("unexpected source label: %#v", got)
|
||||
}
|
||||
if got := parts[3].(map[string]any)["text"]; got != "Image 2 is the identity reference. Keep the character face, hair, accessories, and recognizability from this image." {
|
||||
t.Fatalf("unexpected identity label: %#v", got)
|
||||
}
|
||||
if got := parts[5].(map[string]any)["text"]; got != "Image 3 is the style reference. Apply only the visual style, palette, texture, and illustration treatment from this image. Do not change the character identity because of this image." {
|
||||
t.Fatalf("unexpected style label: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user