mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-05 08:47:12 +08:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
79654170eb | ||
![]() |
fba00a843c | ||
![]() |
47b7a0faf7 |
@@ -1,5 +1,5 @@
|
||||
# Ubuntu 25.04 (Plucky Puffin)
|
||||
FROM photoprism/develop:250930-plucky
|
||||
FROM photoprism/develop:251004-plucky
|
||||
|
||||
# Harden npm usage by default (applies to npm ci / install in dev container)
|
||||
ENV NPM_CONFIG_IGNORE_SCRIPTS=true
|
||||
|
4
Makefile
4
Makefile
@@ -73,6 +73,7 @@ build-all: build-go build-js
|
||||
pull: docker-pull
|
||||
test: test-js test-go
|
||||
test-go: run-test-go
|
||||
test-hub: run-test-hub
|
||||
test-pkg: run-test-pkg
|
||||
test-ai: run-test-ai
|
||||
test-api: run-test-api
|
||||
@@ -403,6 +404,9 @@ run-test-short:
|
||||
run-test-go:
|
||||
$(info Running all Go tests...)
|
||||
$(GOTEST) -parallel 1 -count 1 -cpu 1 -tags="slow,develop" -timeout 20m ./pkg/... ./internal/...
|
||||
run-test-hub:
|
||||
$(info Running all Go tests with hub requests...)
|
||||
env PHOTOPRISM_TEST_HUB="true" $(GOTEST) -parallel 1 -count 1 -cpu 1 -tags="slow,develop,debug" -timeout 20m ./pkg/... ./internal/...
|
||||
run-test-mariadb:
|
||||
$(info Running all Go tests on MariaDB...)
|
||||
PHOTOPRISM_TEST_DRIVER="mysql" PHOTOPRISM_TEST_DSN="root:photoprism@tcp(mariadb:4001)/acceptance?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true" $(GOTEST) -parallel 1 -count 1 -cpu 1 -tags="slow,develop" -timeout 20m ./pkg/... ./internal/...
|
||||
|
@@ -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{
|
||||
|
@@ -226,7 +226,7 @@ func (c *Config) ReSync(token string) (err error) {
|
||||
|
||||
// Return if no endpoint URL is set.
|
||||
if endpointUrl == "" {
|
||||
log.Debugf("config: unable to obtain API key for maps and places (service disabled)")
|
||||
log.Debugf("config: unable to obtain key for maps and places (service disabled)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -235,10 +235,10 @@ func (c *Config) ReSync(token string) (err error) {
|
||||
|
||||
if c.Key == "" {
|
||||
method = http.MethodPost
|
||||
log.Tracef("config: requesting new API key for maps and places")
|
||||
log.Tracef("config: requesting new key for maps and places")
|
||||
} else {
|
||||
method = http.MethodPut
|
||||
log.Tracef("config: requesting API key for maps and places")
|
||||
log.Tracef("config: requesting key for maps and places")
|
||||
}
|
||||
|
||||
// Create JSON request.
|
||||
@@ -275,7 +275,7 @@ func (c *Config) ReSync(token string) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
} else if r.StatusCode >= 400 {
|
||||
err = fmt.Errorf("requesting api key from %s failed (error %d)", GetServiceHost(), r.StatusCode)
|
||||
err = fmt.Errorf("failed to request key from %s (error %d)", GetServiceHost(), r.StatusCode)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@@ -90,7 +90,7 @@ func TestConfig_Refresh(t *testing.T) {
|
||||
if sess, err := c.DecodeSession(false); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if sess.Expired() {
|
||||
t.Fatalf("session expired: %+v", sess)
|
||||
t.Fatalf("(1) session expired: %+v", sess)
|
||||
} else {
|
||||
t.Logf("(1) session: %#v", sess)
|
||||
}
|
||||
@@ -114,9 +114,9 @@ func TestConfig_Refresh(t *testing.T) {
|
||||
if sess, err := c.DecodeSession(false); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if sess.Expired() {
|
||||
t.Fatal("session expired")
|
||||
t.Fatal("(2) session expired")
|
||||
} else {
|
||||
t.Logf("session: %#v", sess)
|
||||
t.Logf("(2) session: %#v", sess)
|
||||
}
|
||||
|
||||
if err := c.Save(); err != nil {
|
||||
|
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)
|
||||
}
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user