274 lines
8.1 KiB
Go
274 lines
8.1 KiB
Go
|
|
package piagent
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"regexp"
|
|||
|
|
"sort"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"unicode/utf8"
|
|||
|
|
|
|||
|
|
"img_infinite_canvas/internal/domain/design"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type inlineReferenceMatch struct {
|
|||
|
|
start int
|
|||
|
|
end int
|
|||
|
|
index int
|
|||
|
|
name string
|
|||
|
|
url string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var (
|
|||
|
|
inlineReferenceDirectivePattern = regexp.MustCompile(`(?i)(参考图|reference image)\s*(\d+)?\s*(?:[((]([^))\n]+)[))])?\s*[::]\s*(https?://[^\s\],。]+|data:image/[^\s\],。]+|/[^\s\],。]+|attachment://reference-\d+)`)
|
|||
|
|
inlinePromptImageTokenPattern = regexp.MustCompile(`\[@image:#(\d+):([^:\]]+):(?:\[([^\]]+)\]\(([^)]+)\)|([^\]]+))\]`)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func inlineReferencePlacementContext(prompt string, messages []design.AgentChatMessage) string {
|
|||
|
|
messageText := agentMessageInlineReferenceText(messages)
|
|||
|
|
source := prompt
|
|||
|
|
if len(collectInlineReferenceMatches(messageText)) > 0 && (len(collectInlineReferenceMatches(prompt)) == 0 || utf8.RuneCountInString(messageText) >= utf8.RuneCountInString(prompt)) {
|
|||
|
|
source = messageText
|
|||
|
|
}
|
|||
|
|
return inlineReferencePlacementContextFromText(source, prefersEnglish(prompt+"\n"+messageText))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func inlineReferencePlacementContextFromText(text string, english bool) string {
|
|||
|
|
matches := collectInlineReferenceMatches(text)
|
|||
|
|
if len(matches) == 0 {
|
|||
|
|
return "none"
|
|||
|
|
}
|
|||
|
|
if len(matches) > 16 {
|
|||
|
|
matches = matches[:16]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lines := make([]string, 0, len(matches)+1)
|
|||
|
|
if english {
|
|||
|
|
lines = append(lines, "Image input order follows the inline reference order. Nearby text before or after a reference is semantic role/context for that image.")
|
|||
|
|
} else {
|
|||
|
|
lines = append(lines, "图片输入顺序按参考图出现顺序;某张图前后的相邻文字是这张图的角色/用途线索。")
|
|||
|
|
}
|
|||
|
|
for order, match := range matches {
|
|||
|
|
beforeStart := 0
|
|||
|
|
if order > 0 {
|
|||
|
|
beforeStart = matches[order-1].end
|
|||
|
|
}
|
|||
|
|
afterEnd := len(text)
|
|||
|
|
if order+1 < len(matches) {
|
|||
|
|
afterEnd = matches[order+1].start
|
|||
|
|
}
|
|||
|
|
before := compactReferenceNeighbor(text[beforeStart:match.start], true)
|
|||
|
|
after := compactReferenceNeighbor(text[match.end:afterEnd], false)
|
|||
|
|
if english {
|
|||
|
|
lines = append(lines, englishReferencePlacementLine(order+1, match, before, after))
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
lines = append(lines, chineseReferencePlacementLine(order+1, match, before, after))
|
|||
|
|
}
|
|||
|
|
return strings.Join(lines, "\n")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func agentMessageInlineReferenceText(messages []design.AgentChatMessage) string {
|
|||
|
|
var builder strings.Builder
|
|||
|
|
imageIndex := 0
|
|||
|
|
writeText := func(text string) {
|
|||
|
|
text = strings.TrimSpace(text)
|
|||
|
|
if text == "" {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if builder.Len() > 0 {
|
|||
|
|
builder.WriteString(" ")
|
|||
|
|
}
|
|||
|
|
builder.WriteString(text)
|
|||
|
|
}
|
|||
|
|
for _, message := range messages {
|
|||
|
|
if strings.TrimSpace(message.Role) != "" && strings.ToLower(strings.TrimSpace(message.Role)) != "user" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
for _, content := range message.Contents {
|
|||
|
|
switch strings.ToLower(strings.TrimSpace(content.Type)) {
|
|||
|
|
case "text":
|
|||
|
|
if strings.EqualFold(strings.TrimSpace(content.TextSource), "settings") {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
writeText(stripInlineImageGeneratorDirectiveLines(content.Text))
|
|||
|
|
case "image", "mask":
|
|||
|
|
imageIndex++
|
|||
|
|
name := compactInlineReferenceName(content.ImageName)
|
|||
|
|
if name == "" {
|
|||
|
|
name = "image"
|
|||
|
|
}
|
|||
|
|
url := sanitizeReferenceURL(content.ImageURL)
|
|||
|
|
if url == "" {
|
|||
|
|
url = fmt.Sprintf("attachment://reference-%d", imageIndex)
|
|||
|
|
}
|
|||
|
|
writeText(fmt.Sprintf("参考图 %d(%s):%s", imageIndex, name, url))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return builder.String()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func collectInlineReferenceMatches(text string) []inlineReferenceMatch {
|
|||
|
|
if strings.TrimSpace(text) == "" {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
matches := make([]inlineReferenceMatch, 0)
|
|||
|
|
|
|||
|
|
for _, indexes := range inlinePromptImageTokenPattern.FindAllStringSubmatchIndex(text, -1) {
|
|||
|
|
match := inlineReferenceMatch{start: indexes[0], end: indexes[1]}
|
|||
|
|
match.index = atoiSubmatch(text, indexes, 2)
|
|||
|
|
match.name = submatch(text, indexes, 4)
|
|||
|
|
match.url = sanitizeReferenceURL(firstNonEmptySubmatch(text, indexes, 8, 10))
|
|||
|
|
if match.url != "" {
|
|||
|
|
matches = append(matches, match)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, indexes := range inlineReferenceDirectivePattern.FindAllStringSubmatchIndex(text, -1) {
|
|||
|
|
match := inlineReferenceMatch{start: indexes[0], end: indexes[1]}
|
|||
|
|
match.index = atoiSubmatch(text, indexes, 4)
|
|||
|
|
match.name = submatch(text, indexes, 6)
|
|||
|
|
match.url = sanitizeReferenceURL(submatch(text, indexes, 8))
|
|||
|
|
if match.url != "" {
|
|||
|
|
matches = append(matches, match)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sort.SliceStable(matches, func(i, j int) bool {
|
|||
|
|
if matches[i].start == matches[j].start {
|
|||
|
|
return matches[i].end > matches[j].end
|
|||
|
|
}
|
|||
|
|
return matches[i].start < matches[j].start
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
deduped := make([]inlineReferenceMatch, 0, len(matches))
|
|||
|
|
for _, match := range matches {
|
|||
|
|
overlaps := false
|
|||
|
|
for _, existing := range deduped {
|
|||
|
|
if match.start < existing.end && match.end > existing.start {
|
|||
|
|
overlaps = true
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if !overlaps {
|
|||
|
|
deduped = append(deduped, match)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return deduped
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func chineseReferencePlacementLine(order int, match inlineReferenceMatch, before string, after string) string {
|
|||
|
|
parts := []string{fmt.Sprintf("- %s", referenceDisplayName(order, match, false))}
|
|||
|
|
if before != "" {
|
|||
|
|
parts = append(parts, fmt.Sprintf("前文“%s”", before))
|
|||
|
|
}
|
|||
|
|
if after != "" {
|
|||
|
|
parts = append(parts, fmt.Sprintf("后文“%s”", after))
|
|||
|
|
}
|
|||
|
|
if len(parts) == 1 {
|
|||
|
|
parts = append(parts, "无相邻文字")
|
|||
|
|
}
|
|||
|
|
return strings.Join(parts, ":")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func englishReferencePlacementLine(order int, match inlineReferenceMatch, before string, after string) string {
|
|||
|
|
parts := []string{fmt.Sprintf("- %s", referenceDisplayName(order, match, true))}
|
|||
|
|
if before != "" {
|
|||
|
|
parts = append(parts, fmt.Sprintf("previous text %q", before))
|
|||
|
|
}
|
|||
|
|
if after != "" {
|
|||
|
|
parts = append(parts, fmt.Sprintf("next text %q", after))
|
|||
|
|
}
|
|||
|
|
if len(parts) == 1 {
|
|||
|
|
parts = append(parts, "no adjacent text")
|
|||
|
|
}
|
|||
|
|
return strings.Join(parts, ": ")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func referenceDisplayName(order int, match inlineReferenceMatch, english bool) string {
|
|||
|
|
name := compactInlineReferenceName(match.name)
|
|||
|
|
if name == "" {
|
|||
|
|
name = "image"
|
|||
|
|
}
|
|||
|
|
if english {
|
|||
|
|
label := fmt.Sprintf("Reference image %d", order)
|
|||
|
|
if match.index > 0 && match.index != order {
|
|||
|
|
label += fmt.Sprintf(" / user label %d", match.index)
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%s (%s)", label, name)
|
|||
|
|
}
|
|||
|
|
label := fmt.Sprintf("参考图 %d", order)
|
|||
|
|
if match.index > 0 && match.index != order {
|
|||
|
|
label += fmt.Sprintf(" / 用户标注 %d", match.index)
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%s(%s)", label, name)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func compactReferenceNeighbor(value string, keepTail bool) string {
|
|||
|
|
value = strings.ReplaceAll(value, "\u00a0", " ")
|
|||
|
|
value = imagePromptURLPattern.ReplaceAllString(value, "[参考图]")
|
|||
|
|
value = strings.Join(strings.Fields(strings.TrimSpace(value)), " ")
|
|||
|
|
value = strings.Trim(value, " \t\r\n,。,.;;、")
|
|||
|
|
if value == "" {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
const maxRunes = 72
|
|||
|
|
if utf8.RuneCountInString(value) <= maxRunes {
|
|||
|
|
return value
|
|||
|
|
}
|
|||
|
|
runes := []rune(value)
|
|||
|
|
if keepTail {
|
|||
|
|
return "..." + string(runes[len(runes)-maxRunes:])
|
|||
|
|
}
|
|||
|
|
return string(runes[:maxRunes]) + "..."
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func sanitizeReferenceURL(value string) string {
|
|||
|
|
return strings.TrimSpace(strings.TrimRight(value, ",。,.;;、"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func compactInlineReferenceName(value string) string {
|
|||
|
|
return strings.Join(strings.Fields(value), " ")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func stripInlineImageGeneratorDirectiveLines(text string) string {
|
|||
|
|
lines := strings.Split(text, "\n")
|
|||
|
|
filtered := make([]string, 0, len(lines))
|
|||
|
|
for _, line := range lines {
|
|||
|
|
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(line)), "image generator target node:") {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
filtered = append(filtered, line)
|
|||
|
|
}
|
|||
|
|
return strings.TrimSpace(strings.Join(filtered, "\n"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func submatch(text string, indexes []int, offset int) string {
|
|||
|
|
if offset+1 >= len(indexes) || indexes[offset] < 0 || indexes[offset+1] < 0 {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return text[indexes[offset]:indexes[offset+1]]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func firstNonEmptySubmatch(text string, indexes []int, offsets ...int) string {
|
|||
|
|
for _, offset := range offsets {
|
|||
|
|
if value := submatch(text, indexes, offset); strings.TrimSpace(value) != "" {
|
|||
|
|
return value
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func atoiSubmatch(text string, indexes []int, offset int) int {
|
|||
|
|
value := strings.TrimSpace(submatch(text, indexes, offset))
|
|||
|
|
if value == "" {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
number, err := strconv.Atoi(value)
|
|||
|
|
if err != nil {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
return number
|
|||
|
|
}
|