mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Ollama: Remove code fences and commentary from JSON API responses
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
fba00a843c
commit
79654170eb
4 changed files with 128 additions and 4 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
70
pkg/clean/json.go
Normal 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
32
pkg/clean/json_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue