Ollama: Remove code fences and commentary from JSON API responses

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2025-10-04 16:46:55 +02:00
parent fba00a843c
commit 79654170eb
4 changed files with 128 additions and 4 deletions

View file

@ -8,7 +8,6 @@ import (
"fmt"
"io"
"net/http"
"strings"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/service/http/header"
@ -110,8 +109,8 @@ func decodeOllamaResponse(data []byte) (*ApiResponseOllama, error) {
}
func parseOllamaLabels(raw string) ([]LabelResult, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
cleaned := clean.JSON(raw)
if cleaned == "" {
return nil, nil
}
@ -119,7 +118,7 @@ func parseOllamaLabels(raw string) ([]LabelResult, error) {
Labels []LabelResult `json:"labels"`
}
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
if err := json.Unmarshal([]byte(cleaned), &payload); err != nil {
return nil, err
}

View file

@ -70,6 +70,29 @@ func TestPerformApiRequestOllama(t *testing.T) {
assert.Equal(t, "Test", resp.Result.Labels[0].Name)
assert.Nil(t, resp.Result.Caption)
})
t.Run("LabelsWithCodeFence", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.NoError(t, json.NewEncoder(w).Encode(ApiResponseOllama{
Model: "gemma3:latest",
Response: "```json\n{\"labels\":[{\"name\":\"lingerie\",\"confidence\":0.81,\"topicality\":0.73}]}\n```\nThe model provided additional commentary.",
}))
}))
defer server.Close()
apiRequest := &ApiRequest{
Id: "fenced",
Model: "gemma3:latest",
Format: FormatJSON,
Images: []string{"data:image/jpeg;base64,AA=="},
ResponseFormat: ApiFormatOllama,
}
resp, err := PerformApiRequest(apiRequest, server.URL, http.MethodPost, "")
assert.NoError(t, err)
if assert.Len(t, resp.Result.Labels, 1) {
assert.Equal(t, "Lingerie", resp.Result.Labels[0].Name)
}
})
t.Run("CaptionFallback", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.NoError(t, json.NewEncoder(w).Encode(ApiResponseOllama{

70
pkg/clean/json.go Normal file
View file

@ -0,0 +1,70 @@
package clean
import "strings"
// JSON attempts to extract a JSON object or array from raw text.
// It removes common wrappers such as Markdown code fences and trailing commentary.
// Returns an empty string when no JSON payload can be found.
func JSON(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
if strings.HasPrefix(trimmed, "```") {
trimmed = strings.TrimPrefix(trimmed, "```")
trimmed = strings.TrimSpace(trimmed)
if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") {
if idx := strings.Index(trimmed, "\n"); idx != -1 {
trimmed = trimmed[idx+1:]
} else {
return ""
}
}
if idx := strings.LastIndex(trimmed, "```"); idx != -1 {
trimmed = trimmed[:idx]
}
}
trimmed = strings.TrimSpace(trimmed)
startObj := strings.Index(trimmed, "{")
startArr := strings.Index(trimmed, "[")
start := -1
if startObj >= 0 && startArr >= 0 {
if startObj < startArr {
start = startObj
} else {
start = startArr
}
} else if startObj >= 0 {
start = startObj
} else if startArr >= 0 {
start = startArr
}
endObj := strings.LastIndex(trimmed, "}")
endArr := strings.LastIndex(trimmed, "]")
end := -1
if endObj >= 0 && endArr >= 0 {
if endObj > endArr {
end = endObj
} else {
end = endArr
}
} else if endObj >= 0 {
end = endObj
} else if endArr >= 0 {
end = endArr
}
if start >= 0 && end > start {
trimmed = trimmed[start : end+1]
}
return strings.TrimSpace(trimmed)
}

32
pkg/clean/json_test.go Normal file
View file

@ -0,0 +1,32 @@
package clean
import "testing"
func TestJSON(t *testing.T) {
t.Run("CodeFence", func(t *testing.T) {
payload := "```json\n{\"labels\":[]}\n```\nextra"
expected := "{\"labels\":[]}"
if got := JSON(payload); got != expected {
t.Fatalf("expected %q, got %q", expected, got)
}
})
t.Run("PlainWithPrefix", func(t *testing.T) {
payload := "Here you go: {\"labels\":[1]} thanks"
expected := "{\"labels\":[1]}"
if got := JSON(payload); got != expected {
t.Fatalf("expected %q, got %q", expected, got)
}
})
t.Run("Array", func(t *testing.T) {
payload := "```\n[1,2,3]\n```"
expected := "[1,2,3]"
if got := JSON(payload); got != expected {
t.Fatalf("expected %q, got %q", expected, got)
}
})
t.Run("Empty", func(t *testing.T) {
if got := JSON(" "); got != "" {
t.Fatalf("expected empty, got %q", got)
}
})
}