AI: Set default vision API client timeout to one minute #127 #1090

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-04-10 21:07:57 +02:00
parent 190be2a1b5
commit 0304ed37c3
12 changed files with 91 additions and 122 deletions

View File

@@ -0,0 +1,54 @@
package vision
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"github.com/photoprism/photoprism/pkg/media/http/header"
)
// PerformApiRequest performs a Vision API request and returns the result.
func PerformApiRequest(apiRequest *ApiRequest, uri, method, key string) (apiResponse *ApiResponse, err error) {
if apiRequest == nil {
return apiResponse, errors.New("api request is nil")
}
data, jsonErr := apiRequest.MarshalJSON()
if jsonErr != nil {
return apiResponse, jsonErr
}
// Create HTTP client and authenticated service API request.
client := http.Client{Timeout: ServiceTimeout}
req, reqErr := http.NewRequest(method, uri, bytes.NewReader(data))
if key != "" {
header.SetAuthorization(req, key)
}
if reqErr != nil {
return apiResponse, reqErr
}
// Perform API request.
clientResp, clientErr := client.Do(req)
if clientErr != nil {
return apiResponse, clientErr
}
apiResponse = &ApiResponse{}
// Unmarshal response and add labels, if returned.
if apiJson, apiErr := io.ReadAll(clientResp.Body); apiErr != nil {
return apiResponse, apiErr
} else if apiErr = json.Unmarshal(apiJson, apiResponse); apiErr != nil {
return apiResponse, apiErr
}
return apiResponse, nil
}

View File

@@ -1,6 +1,8 @@
package vision
import (
"time"
"github.com/photoprism/photoprism/pkg/fs"
)
@@ -11,6 +13,7 @@ var (
CachePath = fs.Abs("../../../storage/cache")
ServiceUri = ""
ServiceKey = ""
ServiceTimeout = time.Minute
DownloadUrl = ""
DefaultResolution = 224
)

View File

@@ -1,43 +1,38 @@
package vision
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"github.com/photoprism/photoprism/internal/ai/face"
"github.com/photoprism/photoprism/internal/thumb/crop"
"github.com/photoprism/photoprism/pkg/media/http/header"
"github.com/photoprism/photoprism/pkg/media/http/scheme"
)
// Faces runs face detection and facenet algorithms over the provided source image.
func Faces(fileName string, minSize int, cacheCrop bool, expected int) (faces face.Faces, err error) {
func Faces(fileName string, minSize int, cacheCrop bool, expected int) (result face.Faces, err error) {
if fileName == "" {
return faces, errors.New("missing image filename")
return result, errors.New("missing image filename")
}
// Return if there is no configuration or no image classification models are configured.
if Config == nil {
return faces, errors.New("vision service is not configured")
return result, errors.New("vision service is not configured")
} else if model := Config.Model(ModelTypeFaceEmbeddings); model != nil {
faces, err = face.Detect(fileName, false, minSize)
result, err = face.Detect(fileName, false, minSize)
if err != nil {
return faces, err
return result, err
}
// Skip embeddings?
if c := len(faces); c == 0 || expected > 0 && c == expected {
return faces, nil
if c := len(result); c == 0 || expected > 0 && c == expected {
return result, nil
}
if uri, method := model.Endpoint(); uri != "" && method != "" {
faceCrops := make([]string, len(faces))
faceCrops := make([]string, len(result))
for i, f := range faces {
for i, f := range result {
if f.Area.Col == 0 && f.Area.Row == 0 {
faceCrops[i] = ""
continue
@@ -54,50 +49,26 @@ func Faces(fileName string, minSize int, cacheCrop bool, expected int) (faces fa
apiRequest, apiRequestErr := NewClientRequest(faceCrops, scheme.Data)
if apiRequestErr != nil {
return faces, apiRequestErr
return result, apiRequestErr
}
if model.Name != "" {
apiRequest.Model = model.Name
}
data, jsonErr := apiRequest.MarshalJSON()
apiResponse, apiErr := PerformApiRequest(apiRequest, uri, method, model.EndpointKey())
if jsonErr != nil {
return faces, jsonErr
if apiErr != nil {
return result, apiErr
}
// Create HTTP client and authenticated service API request.
client := http.Client{}
req, reqErr := http.NewRequest(method, uri, bytes.NewReader(data))
header.SetAuthorization(req, model.EndpointKey())
if reqErr != nil {
return faces, reqErr
}
// Perform API request.
clientResp, clientErr := client.Do(req)
if clientErr != nil {
return faces, clientErr
}
apiResponse := &ApiResponse{}
if apiJson, apiErr := io.ReadAll(clientResp.Body); apiErr != nil {
return faces, apiErr
} else if apiErr = json.Unmarshal(apiJson, apiResponse); apiErr != nil {
return faces, apiErr
}
for i := range faces {
for i := range result {
if len(apiResponse.Result.Embeddings) > i {
faces[i].Embeddings = apiResponse.Result.Embeddings[i]
result[i].Embeddings = apiResponse.Result.Embeddings[i]
}
}
} else if tf := model.FaceModel(); tf != nil {
for i, f := range faces {
for i, f := range result {
if f.Area.Col == 0 && f.Area.Row == 0 {
continue
}
@@ -105,15 +76,15 @@ func Faces(fileName string, minSize int, cacheCrop bool, expected int) (faces fa
if img, _, imgErr := crop.ImageFromThumb(fileName, f.CropArea(), face.CropSize, cacheCrop); imgErr != nil {
log.Errorf("faces: failed to decode image: %s", imgErr)
} else if embeddings := tf.Run(img); !embeddings.Empty() {
faces[i].Embeddings = embeddings
result[i].Embeddings = embeddings
}
}
} else {
return faces, errors.New("invalid face model configuration")
return result, errors.New("invalid face model configuration")
}
} else {
return faces, errors.New("missing face model")
return result, errors.New("missing face model")
}
return faces, nil
return result, nil
}

View File

@@ -1,18 +1,13 @@
package vision
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sort"
"github.com/photoprism/photoprism/internal/ai/classify"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/media/http/header"
"github.com/photoprism/photoprism/pkg/media/http/scheme"
)
@@ -39,34 +34,9 @@ func Labels(images Files, src media.Src) (result classify.Labels, err error) {
apiRequest.Model = model.Name
}
data, jsonErr := apiRequest.MarshalJSON()
apiResponse, apiErr := PerformApiRequest(apiRequest, uri, method, model.EndpointKey())
if jsonErr != nil {
return result, jsonErr
}
// Create HTTP client and authenticated service API request.
client := http.Client{}
req, reqErr := http.NewRequest(method, uri, bytes.NewReader(data))
header.SetAuthorization(req, model.EndpointKey())
if reqErr != nil {
return result, reqErr
}
// Perform API request.
clientResp, clientErr := client.Do(req)
if clientErr != nil {
return result, clientErr
}
apiResponse := &ApiResponse{}
// Unmarshal response and add labels, if returned.
if apiJson, apiErr := io.ReadAll(clientResp.Body); apiErr != nil {
return result, apiErr
} else if apiErr = json.Unmarshal(apiJson, apiResponse); apiErr != nil {
if apiErr != nil {
return result, apiErr
}

View File

@@ -1,17 +1,12 @@
package vision
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"github.com/photoprism/photoprism/internal/ai/nsfw"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/media/http/header"
"github.com/photoprism/photoprism/pkg/media/http/scheme"
)
@@ -40,34 +35,9 @@ func Nsfw(images Files, src media.Src) (result []nsfw.Result, err error) {
apiRequest.Model = model.Name
}
data, jsonErr := apiRequest.MarshalJSON()
apiResponse, apiErr := PerformApiRequest(apiRequest, uri, method, model.EndpointKey())
if jsonErr != nil {
return result, jsonErr
}
// Create HTTP client and authenticated service API request.
client := http.Client{}
req, reqErr := http.NewRequest(method, uri, bytes.NewReader(data))
header.SetAuthorization(req, model.EndpointKey())
if reqErr != nil {
return result, reqErr
}
// Perform API request.
clientResp, clientErr := client.Do(req)
if clientErr != nil {
return result, clientErr
}
apiResponse := &ApiResponse{}
// Unmarshal response and add labels, if returned.
if apiJson, apiErr := io.ReadAll(clientResp.Body); apiErr != nil {
return result, apiErr
} else if apiErr = json.Unmarshal(apiJson, apiResponse); apiErr != nil {
if apiErr != nil {
return result, apiErr
}

View File

@@ -23,7 +23,7 @@ import (
// @Router /api/v1/vision/caption [post]
func PostVisionCaption(router *gin.RouterGroup) {
router.POST("/vision/caption", func(c *gin.Context) {
s := Auth(c, acl.ResourceVision, acl.AccessAll)
s := Auth(c, acl.ResourceVision, acl.Use)
// Abort if permission is not granted.
if s.Abort(c) {

View File

@@ -26,7 +26,7 @@ import (
// @Router /api/v1/vision/face/embeddings [post]
func PostVisionFaceEmbeddings(router *gin.RouterGroup) {
router.POST("/vision/face/embeddings", func(c *gin.Context) {
s := Auth(c, acl.ResourceVision, acl.AccessAll)
s := Auth(c, acl.ResourceVision, acl.Use)
// Abort if permission is not granted.
if s.Abort(c) {

View File

@@ -25,7 +25,7 @@ import (
// @Router /api/v1/vision/labels [post]
func PostVisionLabels(router *gin.RouterGroup) {
router.POST("/vision/labels", func(c *gin.Context) {
s := Auth(c, acl.ResourceVision, acl.AccessAll)
s := Auth(c, acl.ResourceVision, acl.Use)
// Abort if permission is not granted.
if s.Abort(c) {

View File

@@ -25,7 +25,7 @@ import (
// @Router /api/v1/vision/nsfw [post]
func PostVisionNsfw(router *gin.RouterGroup) {
router.POST("/vision/nsfw", func(c *gin.Context) {
s := Auth(c, acl.ResourceVision, acl.AccessAll)
s := Auth(c, acl.ResourceVision, acl.Use)
// Abort if permission is not granted.
if s.Abort(c) {

View File

@@ -20,6 +20,7 @@ const (
AccessPrivate Permission = "access_private"
AccessOwn Permission = "access_own"
AccessAll Permission = "access_all"
Use Permission = "use"
ActionSearch Permission = "search"
ActionView Permission = "view"
ActionUpload Permission = "upload"

View File

@@ -67,10 +67,6 @@ var (
AccessOwn: true,
ActionUpdate: true,
}
GrantCreateAll = Grant{
AccessAll: true,
ActionCreate: true,
}
GrantViewOwn = Grant{
AccessOwn: true,
ActionView: true,
@@ -126,6 +122,10 @@ var (
AccessAll: true,
ActionSubscribe: true,
}
GrantUse = Grant{
Use: true,
ActionCreate: true,
}
GrantNone = Grant{}
)

View File

@@ -97,7 +97,7 @@ var Rules = ACL{
},
ResourceVision: Roles{
RoleAdmin: GrantFullAccess,
RoleClient: GrantCreateAll,
RoleClient: GrantUse,
},
ResourceFeedback: Roles{
RoleAdmin: GrantFullAccess,