mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-20 06:19:24 +00:00
- Add persistent media storage (internal/media/) replacing temp file deletion
- Add MediaRef type for lightweight media references in session messages
- Refactor media pipeline to use bus.MediaFile{Path, MimeType} across all channels
- Add read_document builtin tool for PDF/DOCX/XLSX analysis via Gemini native API
- Move image sanitization from Telegram to shared agent/media layer
- Add media reload for multi-turn conversations (images from last 5 messages)
- Add reply-to-message media resolution for Telegram (re-download on reply)
- Add media inventory to compaction summary to preserve awareness after truncation
- Fix coreToolSummaries for read_image, read_document, create_image tools
- Add real-time trace update events via WebSocket broadcast
- Improve trace detail UI with media refs and tool result display
64 lines
1.9 KiB
Go
64 lines
1.9 KiB
Go
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/disintegration/imaging"
|
|
)
|
|
|
|
// Image sanitization constants (moved from telegram/image_sanitize.go).
|
|
const (
|
|
// imageMaxSide is the maximum pixels per side before resize.
|
|
imageMaxSide = 1200
|
|
// imageSanitizeMaxBytes is the max file size after compression (5MB, Anthropic API limit).
|
|
imageSanitizeMaxBytes = 5 * 1024 * 1024
|
|
)
|
|
|
|
// jpegQualities is the grid of quality levels to try during sanitization.
|
|
var jpegQualities = []int{85, 75, 65, 55, 45, 35}
|
|
|
|
// Ensure standard image decoders are registered.
|
|
func init() {
|
|
image.RegisterFormat("jpeg", "\xff\xd8", jpeg.Decode, jpeg.DecodeConfig)
|
|
image.RegisterFormat("png", "\x89PNG", png.Decode, png.DecodeConfig)
|
|
}
|
|
|
|
// SanitizeImage resizes and compresses an image for LLM vision input.
|
|
// Applied uniformly to all channels at the agent loop level.
|
|
// Pipeline: decode → auto-orient EXIF → resize if >1200px → JPEG compress until <5MB.
|
|
func SanitizeImage(inputPath string) (string, error) {
|
|
img, err := imaging.Open(inputPath, imaging.AutoOrientation(true))
|
|
if err != nil {
|
|
return "", fmt.Errorf("open image: %w", err)
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
w, h := bounds.Dx(), bounds.Dy()
|
|
|
|
if w > imageMaxSide || h > imageMaxSide {
|
|
img = imaging.Fit(img, imageMaxSide, imageMaxSide, imaging.Lanczos)
|
|
}
|
|
|
|
for _, quality := range jpegQualities {
|
|
var buf bytes.Buffer
|
|
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}); err != nil {
|
|
return "", fmt.Errorf("encode jpeg (q=%d): %w", quality, err)
|
|
}
|
|
if buf.Len() <= imageSanitizeMaxBytes {
|
|
outPath := filepath.Join(os.TempDir(), fmt.Sprintf("goclaw_sanitized_%d.jpg", os.Getpid()))
|
|
if err := os.WriteFile(outPath, buf.Bytes(), 0644); err != nil {
|
|
return "", fmt.Errorf("write sanitized image: %w", err)
|
|
}
|
|
return outPath, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("image too large even at lowest quality (dimensions: %dx%d)", w, h)
|
|
}
|