mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
54
internal/ai/vision/client.go
Normal file
54
internal/ai/vision/client.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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{}
|
||||
)
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ var Rules = ACL{
|
||||
},
|
||||
ResourceVision: Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
RoleClient: GrantCreateAll,
|
||||
RoleClient: GrantUse,
|
||||
},
|
||||
ResourceFeedback: Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
|
||||
Reference in New Issue
Block a user