RC-5
All checks were successful
release-tag / release-image (push) Successful in 2m5s

This commit is contained in:
2026-08-10 16:32:07 +02:00
parent 43dc48418c
commit 551ed007e6
3 changed files with 41 additions and 8 deletions

View File

@@ -638,3 +638,9 @@ POST /api/admin/tasks/ensure
## V2.9: lokaler RIFT-Pipeline-Test
Im Adminbereich unter **TASK ACTIONS** kann für den ausgewählten Task eine komplette lokale Testkarte erzeugt werden. Der Test verwendet den vorhandenen `character_anchor.png` als Mock-Character-Artwork und die Style-Referenz des Tasks als Hintergrund, führt die deterministische Trait-Auswahl, das programmatische Kartenlayout und das Schreiben der Testdateien aus, ruft aber **keine externe Bild-API** auf. Die Dateien landen unter `data/artifacts/_test/<task-id>/`; der echte Task- und Artifact-Status bleibt unverändert.
### OpenAI reference-image MIME handling
Reference images sent to the OpenAI Images Edit endpoint are uploaded with an explicit per-part MIME type (`image/png`, `image/jpeg` or `image/webp`). This is required because Go's `multipart.CreateFormFile` otherwise labels file parts as `application/octet-stream`, which the Images API rejects. The character anchor is always sent as `image/png`; task style references preserve their detected image MIME type.

View File

@@ -120,6 +120,9 @@ func TestOpenAIRequestSendsReferenceAsImageEdit(t *testing.T) {
if len(files) != 1 {
t.Fatalf("expected one reference image, got %d", len(files))
}
if got := files[0].Header.Get("Content-Type"); got != "image/png" {
t.Fatalf("reference content type = %q, want image/png", got)
}
f, err := files[0].Open()
if err != nil {
t.Fatal(err)
@@ -141,7 +144,7 @@ func TestOpenAIRequestSendsReferenceAsImageEdit(t *testing.T) {
cfg.ArtifactWidth = 1024
cfg.ArtifactHeight = 1536
cfg.ArtifactQuality = "medium"
res, err := w.openAIRequest(context.Background(), cfg, "test prompt", []referenceImage{{Name: "reference.png", Bytes: []byte("reference-bytes")}})
res, err := w.openAIRequest(context.Background(), cfg, "test prompt", []referenceImage{{Name: "reference.png", ContentType: "image/png", Bytes: []byte("reference-bytes")}})
if err != nil {
t.Fatal(err)
}
@@ -204,6 +207,7 @@ func TestTaskStyleReferenceDefaultsAndCustom(t *testing.T) {
func TestOpenAIRequestSendsCharacterAndStyleReferences(t *testing.T) {
var names []string
var contentTypes []string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(4 << 20); err != nil {
t.Fatalf("parse multipart: %v", err)
@@ -214,6 +218,7 @@ func TestOpenAIRequestSendsCharacterAndStyleReferences(t *testing.T) {
}
for _, f := range files {
names = append(names, f.Filename)
contentTypes = append(contentTypes, f.Header.Get("Content-Type"))
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"data":[{"b64_json":%q}]}`, base64.StdEncoding.EncodeToString([]byte("generated")))
@@ -224,8 +229,8 @@ func TestOpenAIRequestSendsCharacterAndStyleReferences(t *testing.T) {
w := &Worker{http: ts.Client()}
cfg := settings.Defaults()
_, err := w.openAIRequest(context.Background(), cfg, "prompt", []referenceImage{
{Name: "character_anchor.png", Bytes: []byte("anchor")},
{Name: "task_style.jpg", Bytes: []byte("style")},
{Name: "character_anchor.png", ContentType: "image/png", Bytes: []byte("anchor")},
{Name: "task_style.jpg", ContentType: "image/jpeg", Bytes: []byte("style")},
})
if err != nil {
t.Fatal(err)
@@ -233,4 +238,7 @@ func TestOpenAIRequestSendsCharacterAndStyleReferences(t *testing.T) {
if strings.Join(names, ",") != "character_anchor.png,task_style.jpg" {
t.Fatalf("unexpected reference order: %v", names)
}
if strings.Join(contentTypes, ",") != "image/png,image/jpeg" {
t.Fatalf("unexpected reference content types: %v", contentTypes)
}
}

View File

@@ -10,8 +10,10 @@ import (
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"path/filepath"
"strings"
@@ -21,8 +23,9 @@ import (
)
type referenceImage struct {
Name string
Bytes []byte
Name string
ContentType string
Bytes []byte
}
var collectionAnchorMu sync.Mutex
@@ -130,8 +133,8 @@ func (w *Worker) openAI(ctx context.Context, cfg settings.Runtime, x win, prompt
return imageResult{}, err
}
res, err := w.openAIRequest(ctx, cfg, prompt, []referenceImage{
{Name: "character_anchor.png", Bytes: anchor},
{Name: styleRef.Name, Bytes: styleRef.Bytes},
{Name: "character_anchor.png", ContentType: "image/png", Bytes: anchor},
{Name: styleRef.Name, ContentType: styleRef.ContentType, Bytes: styleRef.Bytes},
})
if err != nil {
return imageResult{}, err
@@ -198,7 +201,23 @@ func (w *Worker) openAIRequest(ctx context.Context, cfg settings.Runtime, prompt
if name == "" {
name = fmt.Sprintf("reference_%d.png", i+1)
}
part, err := mw.CreateFormFile("image[]", name)
contentType := strings.TrimSpace(ref.ContentType)
if contentType == "" {
contentType = styleContentType(name, ref.Bytes)
}
switch contentType {
case "image/jpeg", "image/png", "image/webp":
default:
return imageResult{}, fmt.Errorf("unsupported reference image content type %q for %s", contentType, name)
}
header := make(textproto.MIMEHeader)
header.Set("Content-Disposition", mime.FormatMediaType("form-data", map[string]string{
"name": "image[]",
"filename": name,
}))
header.Set("Content-Type", contentType)
part, err := mw.CreatePart(header)
if err != nil {
return imageResult{}, err
}