mirror of
https://github.com/ollama/ollama.git
synced 2026-09-21 13:38:14 -05:00
llm: transcode WebP images for llama-server (#17755)
llama-server does not currently support WebP image payloads. Detect WebP media before forwarding, and transcode it to PNG. Pass all other media through unchanged. Replace an existing vision integration image with a lossless WebP version so we now have coverage of JPG/PNG/WebP formats. Fixes #17753
This commit is contained in:
+38
-8
@@ -22,6 +22,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
@@ -39,6 +40,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/webp"
|
||||
"golang.org/x/sync/semaphore"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
@@ -1604,7 +1606,11 @@ func (s *llamaServerRunner) Completion(ctx context.Context, req CompletionReques
|
||||
for _, media := range req.Media {
|
||||
marker := fmt.Sprintf("[img-%d]", media.ID)
|
||||
promptStr = strings.Replace(promptStr, marker, s.llamaServerMediaMarker(), 1)
|
||||
mediaData = append(mediaData, base64.StdEncoding.EncodeToString(media.Data))
|
||||
data, err := llamaServerMediaBytes(media.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mediaData = append(mediaData, base64.StdEncoding.EncodeToString(data))
|
||||
}
|
||||
lsReq.Prompt = llamaServerMultimodalPrompt{
|
||||
PromptString: promptStr,
|
||||
@@ -2227,34 +2233,58 @@ func llamaServerChatMessage(msg Message) (map[string]any, error) {
|
||||
})
|
||||
}
|
||||
for _, media := range msg.Media {
|
||||
parts = append(parts, llamaServerChatMediaPart(media))
|
||||
part, err := llamaServerChatMediaPart(media)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, part)
|
||||
}
|
||||
converted["content"] = parts
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
func llamaServerChatMediaPart(media MediaData) map[string]any {
|
||||
encoded := base64.StdEncoding.EncodeToString(media.Data)
|
||||
func llamaServerChatMediaPart(media MediaData) (map[string]any, error) {
|
||||
if format, ok := AudioFormat(media.Data); ok {
|
||||
return map[string]any{
|
||||
"type": "input_audio",
|
||||
"input_audio": map[string]any{
|
||||
"data": encoded,
|
||||
"data": base64.StdEncoding.EncodeToString(media.Data),
|
||||
"format": format,
|
||||
},
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
mime := http.DetectContentType(media.Data)
|
||||
data, err := llamaServerMediaBytes(media.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mime := http.DetectContentType(data)
|
||||
if !strings.HasPrefix(mime, "image/") {
|
||||
mime = "image/jpeg"
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "image_url",
|
||||
"image_url": map[string]any{
|
||||
"url": "data:" + mime + ";base64," + encoded,
|
||||
"url": "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(data),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func llamaServerMediaBytes(data []byte) ([]byte, error) {
|
||||
if http.DetectContentType(data) != "image/webp" {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
img, err := webp.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode WebP image: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
return nil, fmt.Errorf("encode WebP image as PNG: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func llamaServerChatToolCalls(tcs []api.ToolCall) ([]llamaServerChatToolCall, error) {
|
||||
|
||||
@@ -3639,27 +3639,37 @@ func TestLlamaServerChatMessageConvertsToolCalls(t *testing.T) {
|
||||
|
||||
func TestLlamaServerChatMessageConvertsMediaParts(t *testing.T) {
|
||||
png := []byte("\x89PNG\r\n\x1a\n")
|
||||
webp, err := base64.StdEncoding.DecodeString("UklGRhwAAABXRUJQVlA4TA8AAAAvAAAAAAcQ/Y/+ByKi/wEA")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wav := []byte("RIFF\x00\x00\x00\x00WAVE")
|
||||
mp3 := []byte("ID3\x04\x00\x00")
|
||||
|
||||
msg, err := llamaServerChatMessage(Message{
|
||||
Role: "user",
|
||||
Content: "describe these",
|
||||
Media: []MediaData{NewMediaData(0, png), NewMediaData(1, wav), NewMediaData(2, mp3)},
|
||||
Media: []MediaData{NewMediaData(0, png), NewMediaData(1, webp), NewMediaData(2, wav), NewMediaData(3, mp3)},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
parts, ok := msg["content"].([]map[string]any)
|
||||
if !ok || len(parts) != 4 {
|
||||
t.Fatalf("expected four content parts, got %#v", msg["content"])
|
||||
if !ok || len(parts) != 5 {
|
||||
t.Fatalf("expected five content parts, got %#v", msg["content"])
|
||||
}
|
||||
if parts[1]["type"] != "image_url" {
|
||||
t.Fatalf("expected image_url for PNG, got %#v", parts[1])
|
||||
}
|
||||
if imageURL := parts[1]["image_url"].(map[string]any)["url"]; imageURL != "data:image/png;base64,"+base64.StdEncoding.EncodeToString(png) {
|
||||
t.Fatalf("expected PNG to pass through unchanged, got %#v", imageURL)
|
||||
}
|
||||
if imageURL := parts[2]["image_url"].(map[string]any)["url"].(string); !strings.HasPrefix(imageURL, "data:image/png;base64,") {
|
||||
t.Fatalf("expected WebP to be converted to PNG, got %q", imageURL)
|
||||
}
|
||||
for i, want := range []string{"wav", "mp3"} {
|
||||
part := parts[i+2]
|
||||
part := parts[i+3]
|
||||
if part["type"] != "input_audio" {
|
||||
t.Fatalf("expected input_audio for %s, got %#v", want, part)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user