Files
moteva/server/internal/infrastructure/piagent/reference_context.go
T

274 lines
8.1 KiB
Go
Raw Normal View History

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
}