feat(image): pass reference images to GPT image model for edits
Server: forward canvas/reference images to the image model so extract, continue, and edit requests stay faithful to the source product. - Add ImageRequestOptions.Images and route requests with inputs to the /v1/images/edits endpoint, via multipart upload (default) or JSON image_url payload, selectable through Agent.Image.InputImageTransport. - Extract reference URLs from prompt directives, inline @image tokens, and chat message image/mask contents; fold them into the idempotency key and prepend a reference-fidelity instruction to the prompt. Frontend: - Shrink CanvasToast to a compact top pill. - Paste an internal transparent clipboard node instead of re-uploading when it matches the clipboard image. - Keep agent-project polling alive on stream error while streaming.
This commit is contained in:
@@ -283,6 +283,9 @@ func (a *DesignAgent) generatePlannedImagesIncremental(ctx context.Context, prom
|
||||
}
|
||||
|
||||
func (a *DesignAgent) generateImageWithFallback(ctx context.Context, req design.AgentRequest, prompt string, fallback string, index int) (GeneratedImage, error) {
|
||||
referenceImages := referenceImageURLs(req)
|
||||
prompt = withReferenceImageInstruction(prompt, len(referenceImages))
|
||||
fallback = withReferenceImageInstruction(fallback, len(referenceImages))
|
||||
prompt = prepareGPTImagePrompt(prompt, imageGenerationPromptMaxRunes)
|
||||
fallback = prepareGPTImagePrompt(fallback, imageGenerationFallbackMaxRunes)
|
||||
if imagePromptTooLarge(prompt) && strings.TrimSpace(fallback) != "" {
|
||||
@@ -293,6 +296,7 @@ func (a *DesignAgent) generateImageWithFallback(ctx context.Context, req design.
|
||||
Model: req.ImageModel,
|
||||
Size: req.ImageSize,
|
||||
Count: 1,
|
||||
Images: referenceImages,
|
||||
IdempotencyKey: imageGenerationIdempotencyKey(req, index, prompt, false),
|
||||
})
|
||||
if err == nil {
|
||||
@@ -308,6 +312,7 @@ func (a *DesignAgent) generateImageWithFallback(ctx context.Context, req design.
|
||||
Model: req.ImageModel,
|
||||
Size: req.ImageSize,
|
||||
Count: 1,
|
||||
Images: referenceImages,
|
||||
IdempotencyKey: imageGenerationIdempotencyKey(req, index, fallback, true),
|
||||
})
|
||||
if fallbackErr == nil {
|
||||
@@ -329,12 +334,79 @@ func imageGenerationIdempotencyKey(req design.AgentRequest, index int, prompt st
|
||||
strings.TrimSpace(req.ImageSize),
|
||||
strconv.Itoa(index),
|
||||
kind,
|
||||
strings.Join(referenceImageURLs(req), "\n"),
|
||||
strings.TrimSpace(prompt),
|
||||
}, "\n")
|
||||
sum := sha256.Sum256([]byte(seed))
|
||||
return "img-gen-" + kind + "-" + hex.EncodeToString(sum[:16])
|
||||
}
|
||||
|
||||
func referenceImageURLs(req design.AgentRequest) []string {
|
||||
urls := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
add := func(value string) {
|
||||
value = strings.TrimSpace(strings.TrimRight(value, ",。,."))
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
return
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
urls = append(urls, value)
|
||||
}
|
||||
addFromText := func(text string) {
|
||||
for _, match := range referenceImageDirectiveURLPattern.FindAllStringSubmatch(text, -1) {
|
||||
if len(match) > 1 {
|
||||
add(match[1])
|
||||
}
|
||||
}
|
||||
for _, match := range promptImageTokenURLPattern.FindAllStringSubmatch(text, -1) {
|
||||
if len(match) > 1 && strings.TrimSpace(match[1]) != "" {
|
||||
add(match[1])
|
||||
continue
|
||||
}
|
||||
if len(match) > 2 {
|
||||
add(match[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addFromText(req.Prompt)
|
||||
for _, message := range req.Messages {
|
||||
for _, content := range message.Contents {
|
||||
switch strings.ToLower(strings.TrimSpace(content.Type)) {
|
||||
case "image", "mask":
|
||||
add(content.ImageURL)
|
||||
case "text":
|
||||
addFromText(content.Text)
|
||||
}
|
||||
if len(urls) >= 16 {
|
||||
return urls[:16]
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(urls) > 16 {
|
||||
return urls[:16]
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func withReferenceImageInstruction(prompt string, imageCount int) string {
|
||||
prompt = strings.TrimSpace(prompt)
|
||||
if prompt == "" || imageCount <= 0 {
|
||||
return prompt
|
||||
}
|
||||
lower := strings.ToLower(prompt)
|
||||
if strings.Contains(prompt, "附带参考图") || strings.Contains(lower, "attached reference image") || strings.Contains(lower, "input reference image") {
|
||||
return prompt
|
||||
}
|
||||
if prefersEnglish(prompt) {
|
||||
return "Input reference image(s) are attached. Use them as the visual source of truth for product shape, fabric, color, structure, and identity; when extracting, continuing, or editing a product, do not invent a different item.\n" + prompt
|
||||
}
|
||||
return "已随请求附带参考图;生成时必须以参考图中的商品外形、材质、颜色、结构细节和身份为准,拆出、延续或编辑商品时不要另造不同商品。\n" + prompt
|
||||
}
|
||||
|
||||
func imagePromptTooLarge(prompt string) bool {
|
||||
return utf8.RuneCountInString(strings.TrimSpace(prompt)) > imageGenerationPromptTooLargeRunes
|
||||
}
|
||||
@@ -404,6 +476,8 @@ var explicitPixelSizePattern = regexp.MustCompile(`(?i)([1-9]\d{2,4})\s*(?:x|×|
|
||||
var explicitAspectRatioPattern = regexp.MustCompile(`(?i)(?:^|[^\d])((?:1|2|3|4|9|16)\s*:\s*(?:1|2|3|4|9|16))(?:[^\d]|$)`)
|
||||
|
||||
var imagePromptURLPattern = regexp.MustCompile(`https?://\S+`)
|
||||
var referenceImageDirectiveURLPattern = regexp.MustCompile(`(?i)(?:参考图|reference image)\s*\d*\s*(?:[((][^))\n]+[))])?\s*[::]\s*(https?://[^\s\]]+|data:image/[^\s\]]+|/[^\s\]]+)`)
|
||||
var promptImageTokenURLPattern = regexp.MustCompile(`\[@image:#\d+:[^:\]]+:(?:\[[^\]]+\]\(([^)]+)\)|([^\]]+))\]`)
|
||||
|
||||
func fullFrameImagePrompt(prompt string) string {
|
||||
prompt = strings.TrimSpace(prompt)
|
||||
|
||||
Reference in New Issue
Block a user