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:
@@ -12,6 +12,7 @@ import (
|
|||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
@@ -138,6 +139,7 @@ type upstreamModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type imageEditReference struct {
|
type imageEditReference struct {
|
||||||
|
Role string
|
||||||
Filename string
|
Filename string
|
||||||
ContentType string
|
ContentType string
|
||||||
Content []byte
|
Content []byte
|
||||||
@@ -479,18 +481,21 @@ func (c *newAPIClient) generateImageRefs(ctx context.Context, token, modelID str
|
|||||||
return decodeImageGenerationResponse(resp.StatusCode, respBody, modelID)
|
return decodeImageGenerationResponse(resp.StatusCode, respBody, modelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *newAPIClient) generateEditedImageRefs(ctx context.Context, token, modelID string, input map[string]any, ref imageEditReference) ([]resultRef, map[string]any, error) {
|
func (c *newAPIClient) generateEditedImageRefs(ctx context.Context, token, modelID string, input map[string]any, refs []imageEditReference) ([]resultRef, map[string]any, error) {
|
||||||
if !c.enabled() {
|
if !c.enabled() {
|
||||||
return nil, nil, errors.New("PopiNewAPI base URL is not configured")
|
return nil, nil, errors.New("PopiNewAPI base URL is not configured")
|
||||||
}
|
}
|
||||||
|
if len(refs) == 0 {
|
||||||
|
return nil, nil, errors.New("reference image content is required")
|
||||||
|
}
|
||||||
if useGeminiImageGenerateContent(modelID) {
|
if useGeminiImageGenerateContent(modelID) {
|
||||||
return c.generateGeminiImageRefs(ctx, token, modelID, input, []imageEditReference{ref})
|
return c.generateGeminiImageRefs(ctx, token, modelID, input, refs)
|
||||||
}
|
}
|
||||||
if useSeedreamImageGenerations(modelID) {
|
if useSeedreamImageGenerations(modelID) {
|
||||||
return c.generateSeedreamImageRefs(ctx, token, modelID, input, []imageEditReference{ref})
|
return c.generateSeedreamImageRefs(ctx, token, modelID, input, refs)
|
||||||
}
|
}
|
||||||
if useMiniMaxImageGenerations(modelID) {
|
if useMiniMaxImageGenerations(modelID) {
|
||||||
return c.generateMiniMaxImageRefs(ctx, token, modelID, input, []imageEditReference{ref})
|
return c.generateMiniMaxImageRefs(ctx, token, modelID, input, refs)
|
||||||
}
|
}
|
||||||
token = c.authorizedImageEditToken(token)
|
token = c.authorizedImageEditToken(token)
|
||||||
if token == "" {
|
if token == "" {
|
||||||
@@ -501,6 +506,7 @@ func (c *newAPIClient) generateEditedImageRefs(ctx context.Context, token, model
|
|||||||
if prompt == "" {
|
if prompt == "" {
|
||||||
return nil, nil, errors.New("prompt is required")
|
return nil, nil, errors.New("prompt is required")
|
||||||
}
|
}
|
||||||
|
ref := refs[0]
|
||||||
if len(ref.Content) == 0 {
|
if len(ref.Content) == 0 {
|
||||||
return nil, nil, errors.New("reference image content is required")
|
return nil, nil, errors.New("reference image content is required")
|
||||||
}
|
}
|
||||||
@@ -735,10 +741,13 @@ func (c *newAPIClient) generateGeminiImageRefs(ctx context.Context, token, model
|
|||||||
|
|
||||||
parts := make([]map[string]any, 0, 1+len(refs))
|
parts := make([]map[string]any, 0, 1+len(refs))
|
||||||
parts = append(parts, map[string]any{"text": prompt})
|
parts = append(parts, map[string]any{"text": prompt})
|
||||||
for _, ref := range refs {
|
for index, ref := range refs {
|
||||||
if len(ref.Content) == 0 {
|
if len(ref.Content) == 0 {
|
||||||
return nil, nil, errors.New("reference image content is required")
|
return nil, nil, errors.New("reference image content is required")
|
||||||
}
|
}
|
||||||
|
if label := geminiReferenceLabel(ref, index); label != "" {
|
||||||
|
parts = append(parts, map[string]any{"text": label})
|
||||||
|
}
|
||||||
contentType := strings.TrimSpace(ref.ContentType)
|
contentType := strings.TrimSpace(ref.ContentType)
|
||||||
if contentType == "" {
|
if contentType == "" {
|
||||||
contentType = http.DetectContentType(ref.Content)
|
contentType = http.DetectContentType(ref.Content)
|
||||||
@@ -770,6 +779,13 @@ func (c *newAPIClient) generateGeminiImageRefs(ctx context.Context, token, model
|
|||||||
if len(imageConfig) > 0 {
|
if len(imageConfig) > 0 {
|
||||||
generationConfig["imageConfig"] = imageConfig
|
generationConfig["imageConfig"] = imageConfig
|
||||||
}
|
}
|
||||||
|
log.Printf(
|
||||||
|
"popiartServer: Gemini image request model=%s parts=%d refs=%d image_config=%v",
|
||||||
|
modelID,
|
||||||
|
len(parts),
|
||||||
|
len(refs),
|
||||||
|
imageConfig,
|
||||||
|
)
|
||||||
|
|
||||||
body, err := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -899,6 +915,21 @@ func (c *newAPIClient) submitImageToVideoTask(ctx context.Context, token, modelI
|
|||||||
return taskID, nil
|
return taskID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func geminiReferenceLabel(ref imageEditReference, index int) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(ref.Role)) {
|
||||||
|
case "source":
|
||||||
|
return fmt.Sprintf("Image %d is the source scene. Keep the scene layout, camera framing, main action, and spatial relationships from this image.", index+1)
|
||||||
|
case "identity":
|
||||||
|
return fmt.Sprintf("Image %d is the identity reference. Keep the character face, hair, accessories, and recognizability from this image.", index+1)
|
||||||
|
case "style":
|
||||||
|
return fmt.Sprintf("Image %d 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.", index+1)
|
||||||
|
case "reference":
|
||||||
|
return fmt.Sprintf("Image %d is an additional reference. Use it only for supporting details explicitly requested in the prompt.", index+1)
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *newAPIClient) submitImageToVideoTaskByURL(ctx context.Context, token, modelID string, input map[string]any, ref imageEditReference, prompt string) (string, error) {
|
func (c *newAPIClient) submitImageToVideoTaskByURL(ctx context.Context, token, modelID string, input map[string]any, ref imageEditReference, prompt string) (string, error) {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"model": modelID,
|
"model": modelID,
|
||||||
|
|||||||
@@ -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{
|
refs, usage, err := client.generateEditedImageRefs(context.Background(), "sk-test", "gemini-3-pro-image-preview", map[string]any{
|
||||||
"prompt": "edit this image",
|
"prompt": "edit this image",
|
||||||
"size": "1024x1536",
|
"size": "1024x1536",
|
||||||
}, imageEditReference{
|
}, []imageEditReference{{
|
||||||
Filename: "source.png",
|
Filename: "source.png",
|
||||||
ContentType: "image/png",
|
ContentType: "image/png",
|
||||||
Content: refBytes,
|
Content: refBytes,
|
||||||
})
|
}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("generateEditedImageRefs: %v", err)
|
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{
|
refs, usage, err := client.generateEditedImageRefs(context.Background(), "sk-test", "seedream-4-5-251128", map[string]any{
|
||||||
"prompt": "edit this image into a dusk scene",
|
"prompt": "edit this image into a dusk scene",
|
||||||
"size": "2K",
|
"size": "2K",
|
||||||
}, imageEditReference{
|
}, []imageEditReference{{
|
||||||
Filename: "source.jpg",
|
Filename: "source.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
Content: []byte("binary-image"),
|
Content: []byte("binary-image"),
|
||||||
URL: "https://example.com/reference.jpg",
|
URL: "https://example.com/reference.jpg",
|
||||||
})
|
}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("generateEditedImageRefs: %v", err)
|
t.Fatalf("generateEditedImageRefs: %v", err)
|
||||||
}
|
}
|
||||||
@@ -347,9 +347,9 @@ func TestGenerateMiniMaxEditedImageRefsSendsStandardImageField(t *testing.T) {
|
|||||||
"prompt": "turn it into watercolor",
|
"prompt": "turn it into watercolor",
|
||||||
"size": "832x1248",
|
"size": "832x1248",
|
||||||
"response_format": "url",
|
"response_format": "url",
|
||||||
}, imageEditReference{
|
}, []imageEditReference{{
|
||||||
URL: "https://example.com/reference.jpg",
|
URL: "https://example.com/reference.jpg",
|
||||||
})
|
}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("generateEditedImageRefs: %v", err)
|
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"])
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+136
-25
@@ -1159,7 +1159,7 @@ func (s *Server) executeImageToImageJob(record *job) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
input := buildImageToImageInput(record)
|
input := buildImageToImageInput(record)
|
||||||
ref, err := s.resolveImageToImageReference(ctx, record, input)
|
imageRefs, err := s.resolveImageToImageReferences(ctx, record, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if repoErr := s.store.failJob(record.JobID, "REFERENCE_IMAGE_RESOLUTION_FAILED", err.Error(), map[string]any{
|
if repoErr := s.store.failJob(record.JobID, "REFERENCE_IMAGE_RESOLUTION_FAILED", err.Error(), map[string]any{
|
||||||
"skill_id": record.SkillID,
|
"skill_id": record.SkillID,
|
||||||
@@ -1169,15 +1169,23 @@ func (s *Server) executeImageToImageJob(record *job) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf(
|
||||||
|
"popiartServer: image job %s using model=%s refs=%s preserve_composition=%t strength=%v",
|
||||||
|
record.JobID,
|
||||||
|
modelID,
|
||||||
|
summarizeImageEditReferenceRoles(imageRefs),
|
||||||
|
boolValue(input["preserve_composition"]),
|
||||||
|
input["strength"],
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
refs []resultRef
|
resultRefs []resultRef
|
||||||
usage map[string]any
|
usage map[string]any
|
||||||
)
|
)
|
||||||
if useMiniMaxImageGenerations(modelID) {
|
if useMiniMaxImageGenerations(modelID) {
|
||||||
refs, usage, err = s.newapi.generateMiniMaxImageRefs(ctx, record.UpstreamKey, modelID, input, []imageEditReference{ref})
|
resultRefs, usage, err = s.newapi.generateMiniMaxImageRefs(ctx, record.UpstreamKey, modelID, input, imageRefs)
|
||||||
} else {
|
} else {
|
||||||
refs, usage, err = s.newapi.generateEditedImageRefs(ctx, record.UpstreamKey, modelID, input, ref)
|
resultRefs, usage, err = s.newapi.generateEditedImageRefs(ctx, record.UpstreamKey, modelID, input, imageRefs)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if repoErr := s.store.failJob(record.JobID, "MODEL_REQUEST_FAILED", err.Error(), map[string]any{
|
if repoErr := s.store.failJob(record.JobID, "MODEL_REQUEST_FAILED", err.Error(), map[string]any{
|
||||||
@@ -1189,7 +1197,7 @@ func (s *Server) executeImageToImageJob(record *job) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
refs, err = s.persistResultRefs(ctx, record, refs)
|
resultRefs, err = s.persistResultRefs(ctx, record, resultRefs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if repoErr := s.store.failJob(record.JobID, "MEDIA_PERSIST_FAILED", "failed to persist generated media", map[string]any{
|
if repoErr := s.store.failJob(record.JobID, "MEDIA_PERSIST_FAILED", "failed to persist generated media", map[string]any{
|
||||||
"details": err.Error(),
|
"details": err.Error(),
|
||||||
@@ -1201,7 +1209,7 @@ func (s *Server) executeImageToImageJob(record *job) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.store.completeJobWithResults(record.JobID, refs, usage); err != nil {
|
if err := s.store.completeJobWithResults(record.JobID, resultRefs, usage); err != nil {
|
||||||
log.Printf("popiartServer: complete job %s failed: %v", record.JobID, err)
|
log.Printf("popiartServer: complete job %s failed: %v", record.JobID, err)
|
||||||
if failErr := s.store.failJob(record.JobID, "RESULT_REF_PERSIST_FAILED", "failed to persist job result refs", map[string]any{
|
if failErr := s.store.failJob(record.JobID, "RESULT_REF_PERSIST_FAILED", "failed to persist job result refs", map[string]any{
|
||||||
"details": err.Error(),
|
"details": err.Error(),
|
||||||
@@ -1394,33 +1402,45 @@ func inferRouteKeyForModel(modelID string, input map[string]any) string {
|
|||||||
return "image.text2image"
|
return "image.text2image"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) resolveImageToImageReference(ctx context.Context, record *job, input map[string]any) (imageEditReference, error) {
|
func (s *Server) resolveImageToImageReferences(ctx context.Context, record *job, input map[string]any) ([]imageEditReference, error) {
|
||||||
if record == nil {
|
if record == nil {
|
||||||
return imageEditReference{}, fmt.Errorf("job record is required")
|
return nil, fmt.Errorf("job record is required")
|
||||||
}
|
}
|
||||||
if artifactID := strings.TrimSpace(stringValue(input["source_artifact_id"])); artifactID != "" {
|
if artifactID := strings.TrimSpace(stringValue(input["source_artifact_id"])); artifactID != "" {
|
||||||
item, ref, exists, err := s.store.artifactRef(record.UserID, artifactID)
|
sourceRef, err := s.resolveArtifactImageReference(ctx, record, artifactID, "source")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return imageEditReference{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !exists {
|
refs := []imageEditReference{sourceRef}
|
||||||
return imageEditReference{}, fmt.Errorf("source artifact not found: %s", artifactID)
|
seen := map[string]struct{}{artifactID: {}}
|
||||||
|
var appendArtifactRefs = func(role string, values []string) error {
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
contentType, _, reader, err := s.newapi.openResultRef(ctx, record.UpstreamKey, ref)
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ref, err := s.resolveArtifactImageReference(ctx, record, value, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return imageEditReference{}, err
|
return err
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
seen[value] = struct{}{}
|
||||||
content, err := io.ReadAll(reader)
|
refs = append(refs, ref)
|
||||||
if err != nil {
|
|
||||||
return imageEditReference{}, err
|
|
||||||
}
|
}
|
||||||
return imageEditReference{
|
return nil
|
||||||
Filename: item.Filename,
|
}
|
||||||
ContentType: defaultString(contentType, item.ContentType),
|
if err := appendArtifactRefs("identity", stringSliceValue(input["identity_reference_artifact_ids"])); err != nil {
|
||||||
Content: content,
|
return nil, err
|
||||||
URL: item.URL,
|
}
|
||||||
}, nil
|
if err := appendArtifactRefs("style", stringSliceValue(input["style_reference_artifact_ids"])); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := appendArtifactRefs("reference", stringSliceValue(input["reference_artifact_ids"])); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return refs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
refURL := strings.TrimSpace(stringValue(
|
refURL := strings.TrimSpace(stringValue(
|
||||||
@@ -1432,9 +1452,25 @@ func (s *Server) resolveImageToImageReference(ctx context.Context, record *job,
|
|||||||
refURL = defaultAliceReferenceURL
|
refURL = defaultAliceReferenceURL
|
||||||
}
|
}
|
||||||
if refURL == "" {
|
if refURL == "" {
|
||||||
|
return nil, fmt.Errorf("reference image is required")
|
||||||
|
}
|
||||||
|
ref, err := s.downloadReferenceImage(ctx, refURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ref.Role = "source"
|
||||||
|
return []imageEditReference{ref}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) resolveImageToImageReference(ctx context.Context, record *job, input map[string]any) (imageEditReference, error) {
|
||||||
|
refs, err := s.resolveImageToImageReferences(ctx, record, input)
|
||||||
|
if err != nil {
|
||||||
|
return imageEditReference{}, err
|
||||||
|
}
|
||||||
|
if len(refs) == 0 {
|
||||||
return imageEditReference{}, fmt.Errorf("reference image is required")
|
return imageEditReference{}, fmt.Errorf("reference image is required")
|
||||||
}
|
}
|
||||||
return s.downloadReferenceImage(ctx, refURL)
|
return refs[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) resolveVideoReferences(ctx context.Context, record *job, input map[string]any) ([]imageEditReference, error) {
|
func (s *Server) resolveVideoReferences(ctx context.Context, record *job, input map[string]any) ([]imageEditReference, error) {
|
||||||
@@ -1651,6 +1687,32 @@ func buildAliceShowcasePrompt(input map[string]any, scenePrompt string) string {
|
|||||||
return strings.Join(parts, ", ")
|
return strings.Join(parts, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) resolveArtifactImageReference(ctx context.Context, record *job, artifactID, role string) (imageEditReference, error) {
|
||||||
|
item, ref, exists, err := s.store.artifactRef(record.UserID, artifactID)
|
||||||
|
if err != nil {
|
||||||
|
return imageEditReference{}, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return imageEditReference{}, fmt.Errorf("%s artifact not found: %s", defaultString(role, "reference"), artifactID)
|
||||||
|
}
|
||||||
|
contentType, _, reader, err := s.newapi.openResultRef(ctx, record.UpstreamKey, ref)
|
||||||
|
if err != nil {
|
||||||
|
return imageEditReference{}, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
content, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return imageEditReference{}, err
|
||||||
|
}
|
||||||
|
return imageEditReference{
|
||||||
|
Role: role,
|
||||||
|
Filename: item.Filename,
|
||||||
|
ContentType: defaultString(contentType, item.ContentType),
|
||||||
|
Content: content,
|
||||||
|
URL: item.URL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func buildGenericImageToImagePrompt(input map[string]any, prompt string) string {
|
func buildGenericImageToImagePrompt(input map[string]any, prompt string) string {
|
||||||
parts := make([]string, 0, 6)
|
parts := make([]string, 0, 6)
|
||||||
if prompt != "" {
|
if prompt != "" {
|
||||||
@@ -1668,6 +1730,21 @@ func buildGenericImageToImagePrompt(input map[string]any, prompt string) string
|
|||||||
return strings.Join(parts, ", ")
|
return strings.Join(parts, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func summarizeImageEditReferenceRoles(refs []imageEditReference) string {
|
||||||
|
if len(refs) == 0 {
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(refs))
|
||||||
|
for _, ref := range refs {
|
||||||
|
role := strings.TrimSpace(ref.Role)
|
||||||
|
if role == "" {
|
||||||
|
role = "reference"
|
||||||
|
}
|
||||||
|
parts = append(parts, role)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
func stringValue(values ...any) string {
|
func stringValue(values ...any) string {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
if text, ok := value.(string); ok && strings.TrimSpace(text) != "" {
|
if text, ok := value.(string); ok && strings.TrimSpace(text) != "" {
|
||||||
@@ -1677,6 +1754,40 @@ func stringValue(values ...any) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func boolValue(value any) bool {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case bool:
|
||||||
|
return typed
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringSliceValue(value any) []string {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case []string:
|
||||||
|
return cleanedStringValues(typed)
|
||||||
|
case []any:
|
||||||
|
items := make([]string, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
items = append(items, strings.TrimSpace(fmt.Sprint(item)))
|
||||||
|
}
|
||||||
|
return cleanedStringValues(items)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanedStringValues(values []string) []string {
|
||||||
|
items := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||||
|
items = append(items, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
func buildVideoResultRef(baseURL, taskID, modelID string, task *videoTaskResult) resultRef {
|
func buildVideoResultRef(baseURL, taskID, modelID string, task *videoTaskResult) resultRef {
|
||||||
urlValue := strings.TrimSpace(task.URL)
|
urlValue := strings.TrimSpace(task.URL)
|
||||||
if urlValue == "" {
|
if urlValue == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user