Files
Luminary/internal/prediction/image_input.go
T
renjue 321683f863
CI / docker (push) Successful in 3m38s
Split ingress keys into text/image protocols with Replicate model listing.
Add model input defaults (including prompt), workflow model editing, ingress API docs, and fix img2img image upload filenames for signed URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 23:49:06 +08:00

134 lines
2.9 KiB
Go

package prediction
import (
"bytes"
"fmt"
"net/url"
"path"
"strings"
)
func imageFilenameFromURL(raw string, data []byte) string {
name := ""
if u, err := url.Parse(raw); err == nil {
if base := path.Base(u.Path); base != "" && base != "." && base != "/" {
name = base
}
}
if name == "" {
name = "input"
}
name = sanitizeUploadFilename(name)
if path.Ext(name) == "" {
name += sniffImageExt(data)
}
return name
}
func imageFilenameFromStored(filename, mime string, data []byte) string {
name := filename
if name == "" {
name = "input"
}
name = sanitizeUploadFilename(name)
if path.Ext(name) == "" {
if ext := extFromMime(mime); ext != "" {
name += ext
} else {
name += sniffImageExt(data)
}
}
return name
}
func sanitizeUploadFilename(name string) string {
name = path.Base(name)
replacer := strings.NewReplacer(
"?", "_",
"&", "_",
"=", "_",
"#", "_",
"%", "_",
"\\", "_",
":", "_",
"*", "_",
"\"", "_",
"<", "_",
">", "_",
"|", "_",
)
name = replacer.Replace(name)
name = strings.TrimSpace(name)
if name == "" || name == "." {
return "input"
}
return name
}
func sniffImageExt(data []byte) string {
switch {
case len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF:
return ".jpg"
case len(data) >= 8 && bytes.Equal(data[:8], []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}):
return ".png"
case len(data) >= 12 && bytes.Equal(data[:4], []byte("RIFF")) && bytes.Equal(data[8:12], []byte("WEBP")):
return ".webp"
case len(data) >= 6 && (bytes.Equal(data[:6], []byte("GIF87a")) || bytes.Equal(data[:6], []byte("GIF89a"))):
return ".gif"
default:
return ".png"
}
}
func extFromMime(mime string) string {
switch strings.ToLower(strings.TrimSpace(mime)) {
case "image/jpeg", "image/jpg":
return ".jpg"
case "image/png":
return ".png"
case "image/webp":
return ".webp"
case "image/gif":
return ".gif"
default:
return ""
}
}
func parseGatewayFileID(raw string) (string, bool) {
u, err := url.Parse(raw)
if err != nil {
return "", false
}
segments := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(segments) == 2 && segments[0] == "files" && segments[1] != "" {
return segments[1], true
}
return "", false
}
func validateImageBytes(data []byte) error {
if len(data) == 0 {
return fmt.Errorf("empty image payload")
}
if !looksLikeImage(data) {
return fmt.Errorf("unrecognized image format")
}
return nil
}
func looksLikeImage(data []byte) bool {
switch {
case len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF:
return true
case len(data) >= 8 && bytes.Equal(data[:8], []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}):
return true
case len(data) >= 12 && bytes.Equal(data[:4], []byte("RIFF")) && bytes.Equal(data[8:12], []byte("WEBP")):
return true
case len(data) >= 6 && (bytes.Equal(data[:6], []byte("GIF87a")) || bytes.Equal(data[:6], []byte("GIF89a"))):
return true
default:
return false
}
}