This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user