People: Simplify thumbnail API for face crops #22
This commit is contained in:
parent
895d41cd61
commit
8e5a97ed4a
|
@ -82,21 +82,11 @@ export class Face extends RestModel {
|
|||
}
|
||||
|
||||
thumbnailUrl(size) {
|
||||
if (!this.Marker || !this.Marker.FileHash) {
|
||||
if (!this.Marker) {
|
||||
return `${config.contentUri}/svg/portrait`;
|
||||
}
|
||||
|
||||
if (!size) {
|
||||
size = "tile_160";
|
||||
}
|
||||
|
||||
if (this.Marker.CropArea && (size === "tile_160" || size === "tile_320")) {
|
||||
return `${config.contentUri}/t/${this.Marker.FileHash}/${config.previewToken()}/${size}/${
|
||||
this.Marker.CropArea
|
||||
}`;
|
||||
} else {
|
||||
return `${config.contentUri}/t/${this.Marker.FileHash}/${config.previewToken()}/${size}`;
|
||||
}
|
||||
return this.Marker.thumbnailUrl(size);
|
||||
}
|
||||
|
||||
getDateString() {
|
||||
|
@ -114,7 +104,7 @@ export class Face extends RestModel {
|
|||
}
|
||||
|
||||
static batchSize() {
|
||||
return 500;
|
||||
return 120;
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
|
|
|
@ -40,8 +40,7 @@ export class Marker extends RestModel {
|
|||
return {
|
||||
UID: "",
|
||||
FileUID: "",
|
||||
FileHash: "",
|
||||
CropArea: "",
|
||||
Thumb: "",
|
||||
Type: "",
|
||||
Src: "",
|
||||
Name: "",
|
||||
|
@ -87,10 +86,8 @@ export class Marker extends RestModel {
|
|||
size = "tile_160";
|
||||
}
|
||||
|
||||
if (this.FileHash && this.CropArea) {
|
||||
return `${config.contentUri}/t/${this.FileHash}/${config.previewToken()}/${size}/${
|
||||
this.CropArea
|
||||
}`;
|
||||
if (this.Thumb) {
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
|
||||
} else {
|
||||
return `${config.contentUri}/svg/portrait`;
|
||||
}
|
||||
|
|
|
@ -40,8 +40,6 @@ export class Subject extends RestModel {
|
|||
getDefaults() {
|
||||
return {
|
||||
UID: "",
|
||||
MarkerUID: "",
|
||||
MarkerSrc: "",
|
||||
Type: "",
|
||||
Src: "",
|
||||
Slug: "",
|
||||
|
@ -53,8 +51,8 @@ export class Subject extends RestModel {
|
|||
Private: false,
|
||||
Excluded: false,
|
||||
FileCount: 0,
|
||||
FileHash: "",
|
||||
CropArea: "",
|
||||
Thumb: "",
|
||||
ThumbSrc: "",
|
||||
Metadata: {},
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
|
@ -90,7 +88,7 @@ export class Subject extends RestModel {
|
|||
}
|
||||
|
||||
thumbnailUrl(size) {
|
||||
if (!this.FileHash) {
|
||||
if (!this.Thumb) {
|
||||
return `${config.contentUri}/svg/portrait`;
|
||||
}
|
||||
|
||||
|
@ -98,13 +96,7 @@ export class Subject extends RestModel {
|
|||
size = "tile_160";
|
||||
}
|
||||
|
||||
if (this.CropArea && (size === "tile_160" || size === "tile_320")) {
|
||||
return `${config.contentUri}/t/${this.FileHash}/${config.previewToken()}/${size}/${
|
||||
this.CropArea
|
||||
}`;
|
||||
} else {
|
||||
return `${config.contentUri}/t/${this.FileHash}/${config.previewToken()}/${size}`;
|
||||
}
|
||||
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
|
||||
}
|
||||
|
||||
getDateString() {
|
||||
|
@ -132,7 +124,7 @@ export class Subject extends RestModel {
|
|||
}
|
||||
|
||||
static batchSize() {
|
||||
return 500;
|
||||
return 480;
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
|
|
|
@ -29,17 +29,6 @@
|
|||
import Subjects from "pages/people/subjects.vue";
|
||||
import Faces from "pages/people/faces.vue";
|
||||
|
||||
function initTabs(flag, tabs) {
|
||||
let i = 0;
|
||||
while (i < tabs.length) {
|
||||
if (!tabs[i][flag]) {
|
||||
tabs.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PPagePeople',
|
||||
props: {
|
||||
|
@ -61,8 +50,6 @@ export default {
|
|||
'class': '',
|
||||
'path': '/people',
|
||||
'icon': 'people_alt',
|
||||
'readonly': true,
|
||||
'demo': true,
|
||||
},
|
||||
{
|
||||
'name': 'people-faces',
|
||||
|
@ -72,8 +59,6 @@ export default {
|
|||
'class': '',
|
||||
'path': '/people/new',
|
||||
'icon': 'person_add',
|
||||
'readonly': true,
|
||||
'demo': true,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -182,15 +182,19 @@ export default {
|
|||
this.filter.q = query["q"] ? query["q"] : "";
|
||||
this.filter.all = query["all"] ? query["all"] : "";
|
||||
this.filter.order = this.sortOrder();
|
||||
this.lastFilter = {};
|
||||
this.routeName = this.$route.name;
|
||||
|
||||
if (this.dirty) {
|
||||
this.lastFilter = {};
|
||||
}
|
||||
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.search();
|
||||
|
||||
// this.subscriptions.push(Event.subscribe("subjects", (ev, data) => this.onUpdate(ev, data)));
|
||||
this.subscriptions.push(Event.subscribe("faces", (ev, data) => this.onUpdate(ev, data)));
|
||||
|
||||
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
|
||||
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
|
||||
|
|
|
@ -198,8 +198,12 @@ export default {
|
|||
this.filter.q = query["q"] ? query["q"] : "";
|
||||
this.filter.all = query["all"] ? query["all"] : "";
|
||||
this.filter.order = this.sortOrder();
|
||||
this.lastFilter = {};
|
||||
this.routeName = this.$route.name;
|
||||
|
||||
if (this.dirty) {
|
||||
this.lastFilter = {};
|
||||
}
|
||||
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/crop"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
|
@ -15,16 +17,16 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// GetThumb returns a thumbnail image matching the hash and type.
|
||||
// GetThumb returns a thumbnail image matching the file hash, crop area, and type.
|
||||
//
|
||||
// GET /api/v1/t/:hash/:token/:size
|
||||
// GET /api/v1/t/:thumb/:token/:size
|
||||
//
|
||||
// Parameters:
|
||||
// hash: string sha1 file hash
|
||||
// thumb: string sha1 file hash plus optional crop area
|
||||
// token: string url security token, see config
|
||||
// size: string thumb type, see thumb.Sizes
|
||||
func GetThumb(router *gin.RouterGroup) {
|
||||
router.GET("/t/:hash/:token/:size", func(c *gin.Context) {
|
||||
router.GET("/t/:thumb/:token/:size", func(c *gin.Context) {
|
||||
if InvalidPreviewToken(c) {
|
||||
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
|
||||
return
|
||||
|
@ -34,9 +36,45 @@ func GetThumb(router *gin.RouterGroup) {
|
|||
|
||||
start := time.Now()
|
||||
conf := service.Config()
|
||||
fileHash := c.Param("hash")
|
||||
thumbName := thumb.Name(c.Param("size"))
|
||||
download := c.Query("download") != ""
|
||||
fileHash, cropArea := crop.ParseThumb(c.Param("thumb"))
|
||||
|
||||
// Is cropped thumbnail?
|
||||
if cropArea != "" {
|
||||
cropName := crop.Name(c.Param("size"))
|
||||
|
||||
cropSize, ok := crop.Sizes[cropName]
|
||||
|
||||
if !ok {
|
||||
log.Errorf("%s: invalid size %s", logPrefix, cropName)
|
||||
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
fileName, err := crop.FromRequest(fileHash, cropArea, cropSize, conf.ThumbPath())
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("%s: %s", logPrefix, err)
|
||||
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
|
||||
return
|
||||
} else if fileName == "" {
|
||||
log.Errorf("%s: empty file name, potential bug", logPrefix)
|
||||
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
AddThumbCacheHeader(c)
|
||||
|
||||
if download {
|
||||
c.FileAttachment(fileName, cropName.Jpeg())
|
||||
} else {
|
||||
c.File(fileName)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
thumbName := thumb.Name(c.Param("size"))
|
||||
|
||||
size, ok := thumb.Sizes[thumbName]
|
||||
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/crop"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
)
|
||||
|
||||
// GetThumbCrop returns a cropped thumbnail image matching the hash and type.
|
||||
//
|
||||
// GET /api/v1/t/:hash/:token/:size/:crop
|
||||
//
|
||||
// Parameters:
|
||||
// hash: string sha1 file hash
|
||||
// token: string url security token, see config
|
||||
// size: string crop size, see crop.Sizes
|
||||
// area: string image area identifier, e.g. 1690960ff17f
|
||||
func GetThumbCrop(router *gin.RouterGroup) {
|
||||
router.GET("/t/:hash/:token/:size/:area", func(c *gin.Context) {
|
||||
if InvalidPreviewToken(c) {
|
||||
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
logPrefix := "thumb-crop"
|
||||
|
||||
conf := service.Config()
|
||||
fileHash := c.Param("hash")
|
||||
cropName := crop.Name(c.Param("size"))
|
||||
cropArea := c.Param("area")
|
||||
download := c.Query("download") != ""
|
||||
|
||||
cropSize, ok := crop.Sizes[cropName]
|
||||
|
||||
if !ok {
|
||||
log.Errorf("%s: invalid size %s", logPrefix, cropName)
|
||||
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
fileName, err := crop.FromRequest(fileHash, cropArea, cropSize, conf.ThumbPath())
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("%s: %s", logPrefix, err)
|
||||
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
|
||||
return
|
||||
} else if fileName == "" {
|
||||
log.Errorf("%s: empty file name, potential bug", logPrefix)
|
||||
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
|
||||
return
|
||||
}
|
||||
|
||||
AddThumbCacheHeader(c)
|
||||
|
||||
if download {
|
||||
c.FileAttachment(fileName, cropName.Jpeg())
|
||||
} else {
|
||||
c.File(fileName)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -46,34 +46,31 @@ func TestGetThumb(t *testing.T) {
|
|||
r := PerformRequest(app, "GET", "/api/v1/t/acad9168fa6acc5c5c2965ddf6ec465ca42fd832/"+conf.PreviewToken()+"/fit_7680")
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetThumbCrop(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumbCrop(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/46f5b5c0c027f0c1b15136644f404c57210bf20c/"+conf.PreviewToken()+"/tile_160/016014058037")
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/46f5b5c0c027f0c1b15136644f404c57210bf20c-016014058037/"+conf.PreviewToken()+"/tile_160")
|
||||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("InvalidType", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumbCrop(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/xxx/016014058037")
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/1-016014058037/"+conf.PreviewToken()+"/xxx")
|
||||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("InvalidHash", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetThumbCrop(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/tile_500/016014058037")
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/1-016014058037/"+conf.PreviewToken()+"/tile_500")
|
||||
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid token", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetThumbCrop(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818/xxx/tile_500/016014058037")
|
||||
GetThumb(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818-016014058037/xxx/tile_500")
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
@ -20,11 +21,29 @@ type Area struct {
|
|||
H float32 `json:"h,omitempty"`
|
||||
}
|
||||
|
||||
// Empty tests if the area is empty.
|
||||
func (a Area) Empty() bool {
|
||||
return a.X == 0 && a.Y == 0 && a.W == 0 && a.H == 0
|
||||
}
|
||||
|
||||
// String returns a string identifying the crop area.
|
||||
func (a Area) String() string {
|
||||
if a.Empty() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%03x%03x%03x%03x", int(a.X*1000), int(a.Y*1000), int(a.W*1000), int(a.H*1000))
|
||||
}
|
||||
|
||||
// Thumb returns a string identifying the file and crop area to create a thumb.
|
||||
func (a Area) Thumb(fileHash string) string {
|
||||
if len(fileHash) < 40 {
|
||||
return a.String()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%040s-%012s", fileHash, a.String())
|
||||
}
|
||||
|
||||
// Bounds returns absolute coordinates and dimension.
|
||||
func (a Area) Bounds(img image.Image) (min, max image.Point, dim int) {
|
||||
size := img.Bounds().Max
|
||||
|
@ -76,3 +95,37 @@ func AreaFromString(s string) Area {
|
|||
|
||||
return NewArea("crop", float32(x)/1000, float32(y)/1000, float32(w)/1000, float32(h)/1000)
|
||||
}
|
||||
|
||||
// IsCroppedThumb tests if the string represents a cropped thumbnail and returns the split position if true.
|
||||
func IsCroppedThumb(thumb string) int {
|
||||
if thumb == "" || len(thumb) < 41 {
|
||||
return -1
|
||||
}
|
||||
|
||||
if i := strings.IndexRune(thumb, '-'); i >= 40 && i < len(thumb)-1 {
|
||||
return i
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// ParseThumb splits a thumbnail string into the crop area and file hash.
|
||||
func ParseThumb(thumb string) (fileHash, area string) {
|
||||
if len(thumb) == 12 {
|
||||
return "", thumb
|
||||
} else if len(thumb) < 41 {
|
||||
return thumb, ""
|
||||
}
|
||||
|
||||
s := strings.SplitN(strings.Trim(thumb, "/ -"), "-", 2)
|
||||
|
||||
fileHash = s[0]
|
||||
|
||||
if len(s) < 2 {
|
||||
// Do nothing.
|
||||
} else if len(s[1]) >= 12 {
|
||||
area = s[1]
|
||||
}
|
||||
|
||||
return fileHash, area
|
||||
}
|
||||
|
|
|
@ -33,6 +33,42 @@ func TestArea_String(t *testing.T) {
|
|||
m := NewArea("", -2.0001, 0.123, -0.1, 4.00000001)
|
||||
assert.Equal(t, "00007b0003e8", m.String())
|
||||
})
|
||||
t.Run("00007b0003e8", func(t *testing.T) {
|
||||
m := NewArea("", 0, 0, 0, 0)
|
||||
assert.Equal(t, "", m.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestArea_Thumb(t *testing.T) {
|
||||
t.Run("3e814d0011f4", func(t *testing.T) {
|
||||
expected := fmt.Sprintf("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-%x%x00%x%x", 1000, 333, 1, 500)
|
||||
m := NewArea("face", 1.000, 0.33333, 0.001, 0.5)
|
||||
assert.Equal(t, expected, m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
|
||||
})
|
||||
t.Run("3360a7064042_face", func(t *testing.T) {
|
||||
m := NewArea("face", 0.822059, 0.167969, 0.1, 0.0664062)
|
||||
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-3360a7064042", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
|
||||
})
|
||||
t.Run("3360a7064042_back", func(t *testing.T) {
|
||||
m := NewArea("back", 0.822059, 0.167969, 0.1, 0.0664062)
|
||||
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-3360a7064042", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
|
||||
})
|
||||
t.Run("0c93e801e000", func(t *testing.T) {
|
||||
m := NewArea("face", 0.201, 1.000, 0.03, 0.00000001)
|
||||
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-0c93e801e000", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
|
||||
})
|
||||
t.Run("0003e8000000", func(t *testing.T) {
|
||||
m := NewArea("face", 0.0001, 1.000, 0, 0.00000001)
|
||||
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-0003e8000000", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
|
||||
})
|
||||
t.Run("ShortHash", func(t *testing.T) {
|
||||
m := NewArea("", -2.0001, 0.123, -0.1, 4.00000001)
|
||||
assert.Equal(t, "00007b0003e8", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41"))
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
m := NewArea("", 0, 0, 0, 0)
|
||||
assert.Equal(t, "", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAreaFromString(t *testing.T) {
|
||||
|
@ -48,3 +84,56 @@ func TestAreaFromString(t *testing.T) {
|
|||
assert.Equal(t, NewArea("crop", 0.822, 0.167, 0.1, 0.066), a)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsCroppedThumb(t *testing.T) {
|
||||
t.Run("True", func(t *testing.T) {
|
||||
assert.Equal(t, 40, IsCroppedThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-00007b0003e8"))
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
assert.Equal(t, -1, IsCroppedThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e3100007b0003e8-"))
|
||||
})
|
||||
t.Run("CropArea", func(t *testing.T) {
|
||||
assert.Equal(t, -1, IsCroppedThumb("00007b0003e8"))
|
||||
})
|
||||
t.Run("ShortHash", func(t *testing.T) {
|
||||
assert.Equal(t, -1, IsCroppedThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41"))
|
||||
})
|
||||
t.Run("HashOnly", func(t *testing.T) {
|
||||
assert.Equal(t, -1, IsCroppedThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseThumb(t *testing.T) {
|
||||
t.Run("True", func(t *testing.T) {
|
||||
h, a := ParseThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-00007b0003e8")
|
||||
assert.Equal(t, 40, len(h))
|
||||
assert.Equal(t, 12, len(a))
|
||||
assert.False(t, AreaFromString(a).Empty())
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
h, a := ParseThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e3100007b0003e8-")
|
||||
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e3100007b0003e8", h)
|
||||
assert.Equal(t, 0, len(a))
|
||||
assert.True(t, AreaFromString(a).Empty())
|
||||
})
|
||||
t.Run("CropArea", func(t *testing.T) {
|
||||
h, a := ParseThumb("00007b0003e8")
|
||||
assert.Equal(t, 0, len(h))
|
||||
assert.Equal(t, 12, len(a))
|
||||
assert.False(t, AreaFromString(a).Empty())
|
||||
})
|
||||
t.Run("ShortHash", func(t *testing.T) {
|
||||
h, a := ParseThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41")
|
||||
assert.Equal(t, 37, len(h))
|
||||
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41", h)
|
||||
assert.Equal(t, 0, len(a))
|
||||
assert.True(t, AreaFromString(a).Empty())
|
||||
})
|
||||
t.Run("HashOnly", func(t *testing.T) {
|
||||
h, a := ParseThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31")
|
||||
assert.Equal(t, 40, len(h))
|
||||
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31", h)
|
||||
assert.Equal(t, 0, len(a))
|
||||
assert.True(t, AreaFromString(a).Empty())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -12,6 +12,10 @@ func (n Name) Jpeg() string {
|
|||
|
||||
// Names of standard crop sizes.
|
||||
const (
|
||||
Tile50 Name = "tile_50"
|
||||
Tile100 Name = "tile_100"
|
||||
Tile160 Name = "tile_160"
|
||||
Tile224 Name = "tile_224"
|
||||
Tile320 Name = "tile_320"
|
||||
Tile500 Name = "tile_500"
|
||||
)
|
||||
|
|
|
@ -19,6 +19,10 @@ type SizeMap map[Name]Size
|
|||
|
||||
// Sizes contains the properties of all thumbnail sizes.
|
||||
var Sizes = SizeMap{
|
||||
Tile50: {Tile50, Tile320, "Lists", 50, 50, DefaultOptions},
|
||||
Tile100: {Tile100, Tile320, "Maps", 100, 100, DefaultOptions},
|
||||
Tile160: {Tile160, Tile320, "FaceNet", 160, 160, DefaultOptions},
|
||||
Tile224: {Tile224, Tile320, "TensorFlow, Mosaic", 224, 224, DefaultOptions},
|
||||
Tile320: {Tile320, "", "UI", 320, 320, DefaultOptions},
|
||||
Tile500: {Tile500, "", "FaceNet", 500, 500, DefaultOptions},
|
||||
}
|
||||
|
|
|
@ -30,8 +30,6 @@ type Album struct {
|
|||
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
||||
AlbumUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
|
||||
ParentUID string `gorm:"type:VARBINARY(42);default:''" json:"ParentUID,omitempty" yaml:"ParentUID,omitempty"`
|
||||
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
|
||||
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
|
||||
AlbumSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"Slug"`
|
||||
AlbumPath string `gorm:"type:VARBINARY(500);index;" json:"Path,omitempty" yaml:"Path,omitempty"`
|
||||
AlbumType string `gorm:"type:VARBINARY(8);default:'album';" json:"Type" yaml:"Type,omitempty"`
|
||||
|
@ -50,6 +48,8 @@ type Album struct {
|
|||
AlbumDay int `gorm:"index:idx_albums_ymd" json:"Day" yaml:"Day,omitempty"`
|
||||
AlbumFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
|
||||
AlbumPrivate bool `json:"Private" yaml:"Private,omitempty"`
|
||||
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb" yaml:"Thumb,omitempty"`
|
||||
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"CreatedAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt,omitempty"`
|
||||
DeletedAt *time.Time `sql:"index" json:"DeletedAt" yaml:"DeletedAt,omitempty"`
|
||||
|
|
|
@ -20,8 +20,6 @@ type Labels []Label
|
|||
type Label struct {
|
||||
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
||||
LabelUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
|
||||
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
|
||||
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
|
||||
LabelSlug string `gorm:"type:VARBINARY(255);unique_index;" json:"Slug" yaml:"-"`
|
||||
CustomSlug string `gorm:"type:VARBINARY(255);index;" json:"CustomSlug" yaml:"-"`
|
||||
LabelName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name"`
|
||||
|
@ -31,6 +29,8 @@ type Label struct {
|
|||
LabelNotes string `gorm:"type:TEXT;" json:"Notes" yaml:"Notes,omitempty"`
|
||||
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id" json:"-" yaml:"-"`
|
||||
PhotoCount int `gorm:"default:1" json:"PhotoCount" yaml:"-"`
|
||||
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb" yaml:"Thumb,omitempty"`
|
||||
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
||||
|
|
|
@ -27,8 +27,6 @@ const (
|
|||
type Marker struct {
|
||||
MarkerUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
|
||||
FileUID string `gorm:"type:VARBINARY(42);index;" json:"FileUID" yaml:"FileUID"`
|
||||
FileHash string `gorm:"type:VARBINARY(128);index" json:"FileHash" yaml:"FileHash,omitempty"`
|
||||
CropArea string `gorm:"type:VARBINARY(16);default:''" json:"CropArea" yaml:"CropArea,omitempty"`
|
||||
MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"`
|
||||
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
||||
MarkerName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name,omitempty"`
|
||||
|
@ -50,6 +48,7 @@ type Marker struct {
|
|||
Q int `json:"Q" yaml:"Q,omitempty"`
|
||||
Size int `gorm:"default:-1" json:"Size" yaml:"Size,omitempty"`
|
||||
Score int `gorm:"type:SMALLINT" json:"Score" yaml:"Score,omitempty"`
|
||||
Thumb string `gorm:"type:VARBINARY(128);index" json:"Thumb" yaml:"Thumb,omitempty"`
|
||||
MatchedAt *time.Time `sql:"index" json:"MatchedAt" yaml:"MatchedAt,omitempty"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
@ -70,19 +69,24 @@ func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
|
|||
}
|
||||
|
||||
// NewMarker creates a new entity.
|
||||
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string) *Marker {
|
||||
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string, size, score int) *Marker {
|
||||
m := &Marker{
|
||||
FileUID: file.FileUID,
|
||||
FileHash: file.FileHash,
|
||||
CropArea: area.String(),
|
||||
MarkerSrc: markerSrc,
|
||||
MarkerType: markerType,
|
||||
SubjUID: subjUID,
|
||||
X: area.X,
|
||||
Y: area.Y,
|
||||
W: area.W,
|
||||
H: area.H,
|
||||
MatchedAt: nil,
|
||||
FileUID: file.FileUID,
|
||||
MarkerSrc: markerSrc,
|
||||
MarkerType: markerType,
|
||||
MarkerReview: score < 30,
|
||||
MarkerInvalid: false,
|
||||
SubjUID: subjUID,
|
||||
FaceDist: -1,
|
||||
X: area.X,
|
||||
Y: area.Y,
|
||||
W: area.W,
|
||||
H: area.H,
|
||||
Q: int(float32(math.Log(float64(score))) * float32(size) * area.W),
|
||||
Size: size,
|
||||
Score: score,
|
||||
Thumb: area.Thumb(file.FileHash),
|
||||
MatchedAt: nil,
|
||||
}
|
||||
|
||||
return m
|
||||
|
@ -90,13 +94,8 @@ func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string)
|
|||
|
||||
// NewFaceMarker creates a new entity.
|
||||
func NewFaceMarker(f face.Face, file File, subjUID string) *Marker {
|
||||
m := NewMarker(file, f.CropArea(), subjUID, SrcImage, MarkerFace)
|
||||
m := NewMarker(file, f.CropArea(), subjUID, SrcImage, MarkerFace, f.Size(), f.Score)
|
||||
|
||||
m.Size = f.Size()
|
||||
m.Q = int(float32(math.Log(float64(f.Score))) * float32(m.Size) * m.W)
|
||||
m.Score = f.Score
|
||||
m.MarkerReview = f.Score < 30
|
||||
m.FaceDist = -1
|
||||
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
||||
m.LandmarksJSON = f.RelativeLandmarksJSON()
|
||||
|
||||
|
@ -403,6 +402,11 @@ func (m *Marker) ClearSubject(src string) error {
|
|||
|
||||
// Face returns a matching face entity if possible.
|
||||
func (m *Marker) Face() (f *Face) {
|
||||
if m.MarkerUID == "" {
|
||||
log.Debugf("marker: empty uid while finding face")
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.face != nil {
|
||||
if m.FaceID == m.face.ID {
|
||||
return m.face
|
||||
|
@ -412,7 +416,7 @@ func (m *Marker) Face() (f *Face) {
|
|||
// Add face if size
|
||||
if m.SubjSrc != SrcAuto && m.FaceID == "" {
|
||||
if m.Size < face.ClusterMinSize || m.Score < face.ClusterMinScore {
|
||||
log.Debugf("faces: skipped adding face for low-quality marker %s, size %d, score %d", m.MarkerUID, m.Size, m.Score)
|
||||
log.Debugf("marker: skipped adding face due to low-quality (uid %s, size %d, score %d)", txt.Quote(m.MarkerUID), m.Size, m.Score)
|
||||
return nil
|
||||
} else if emb := m.Embeddings(); len(emb) == 0 {
|
||||
log.Warnf("marker: %s has no embeddings", m.MarkerUID)
|
||||
|
@ -537,7 +541,7 @@ func FindFaceMarker(faceId string) *Marker {
|
|||
var result Marker
|
||||
|
||||
if err := Db().Where("face_id = ?", faceId).
|
||||
Where("file_hash <> '' AND marker_invalid = 0").
|
||||
Where("thumb <> '' AND marker_invalid = 0").
|
||||
Order("face_dist ASC, q DESC").First(&result).Error; err != nil {
|
||||
log.Warnf("face: no marker for %s", txt.Quote(faceId))
|
||||
return nil
|
||||
|
@ -567,7 +571,6 @@ func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
|
|||
err := result.Updates(map[string]interface{}{
|
||||
"MarkerType": m.MarkerType,
|
||||
"MarkerSrc": m.MarkerSrc,
|
||||
"CropArea": m.CropArea,
|
||||
"X": m.X,
|
||||
"Y": m.Y,
|
||||
"W": m.W,
|
||||
|
@ -575,6 +578,7 @@ func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
|
|||
"Q": m.Q,
|
||||
"Size": m.Size,
|
||||
"Score": m.Score,
|
||||
"Thumb": m.Thumb,
|
||||
"LandmarksJSON": m.LandmarksJSON,
|
||||
"EmbeddingsJSON": m.EmbeddingsJSON,
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ package entity
|
|||
import "github.com/photoprism/photoprism/internal/crop"
|
||||
|
||||
// UnknownMarker can be used as a default for unknown markers.
|
||||
var UnknownMarker = NewMarker(File{}, crop.Area{}, "", SrcDefault, MarkerUnknown)
|
||||
var UnknownMarker = NewMarker(File{}, crop.Area{}, "", SrcDefault, MarkerUnknown, 0, 0)
|
||||
|
||||
type MarkerMap map[string]Marker
|
||||
|
||||
|
@ -27,7 +27,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-1": Marker{ //Photo04
|
||||
MarkerUID: "mqu0xs11qekk9jx8",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
SubjUID: "jqu0xs11qekk9jx8",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerLabel,
|
||||
|
@ -41,7 +41,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-2": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy3c3",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
SubjUID: "lt9k3pw1wowuy3c3",
|
||||
FaceID: "LRG2HJBDZE66LYG7Q5SRFXO2MDTOES52",
|
||||
MarkerName: "Unknown",
|
||||
|
@ -57,7 +57,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-3": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy111",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerLabel,
|
||||
|
@ -72,7 +72,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-4": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy222",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
|
@ -89,7 +89,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-5": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy333",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("unknown").ID,
|
||||
SubjUID: "",
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -108,7 +108,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-6": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy444",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("john-doe").ID,
|
||||
FaceDist: 0.2,
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -128,7 +128,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"ma-ba-1": Marker{ //Photo27
|
||||
MarkerUID: "mt9k3pw1wowuy555",
|
||||
FileUID: "ft2es49qhhinlple",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.5,
|
||||
SubjSrc: "",
|
||||
|
@ -148,7 +148,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"fa-gr-1": Marker{ //Photo27
|
||||
MarkerUID: "mt9k3pw1wowuy666",
|
||||
FileUID: "ft2es49qhhinlple",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.6,
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -168,7 +168,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"fa-gr-2": Marker{ //Photo03
|
||||
MarkerUID: "mt9k3pw1wowuy777",
|
||||
FileUID: "ft2es49w15bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.6,
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -188,7 +188,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"fa-gr-3": Marker{ //19800101_000002_D640C559
|
||||
MarkerUID: "mt9k3pw1wowuy888",
|
||||
FileUID: "ft8es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.6,
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -208,9 +208,8 @@ var MarkerFixtures = MarkerMap{
|
|||
"actress-a-1": Marker{ //Photo27
|
||||
MarkerUID: "mt9k3pw1wowuy999",
|
||||
FileUID: "ft2es49qhhinlple",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818-045038063041",
|
||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||
CropArea: "045038063041",
|
||||
FaceDist: 0.26852392873736236,
|
||||
SubjSrc: SrcManual,
|
||||
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
|
||||
|
@ -229,9 +228,8 @@ var MarkerFixtures = MarkerMap{
|
|||
"actress-a-2": Marker{ //Photo03 - non primary file
|
||||
MarkerUID: "mt9k3pw1wowu1000",
|
||||
FileUID: "ft2es49whhbnlqdn",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818-046045043065",
|
||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||
CropArea: "046045043065",
|
||||
FaceDist: 0.4507357278575355,
|
||||
SubjSrc: "",
|
||||
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
|
||||
|
@ -250,9 +248,8 @@ var MarkerFixtures = MarkerMap{
|
|||
"actress-a-3": Marker{ //19800101_000002_D640C559
|
||||
MarkerUID: "mt9k3pw1wowu1001",
|
||||
FileUID: "ft8es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818-05403304060446",
|
||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||
CropArea: "05403304060446",
|
||||
FaceDist: 0.5099754448545762,
|
||||
SubjSrc: "",
|
||||
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
|
||||
|
@ -271,7 +268,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actor-a-1": Marker{ //Photo05
|
||||
MarkerUID: "mt9k3pw1wowu1002",
|
||||
FileUID: "ft3es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.5223304453393212,
|
||||
SubjSrc: "",
|
||||
|
@ -291,7 +288,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actor-a-2": Marker{ //Photo02
|
||||
MarkerUID: "mt9k3pw1wowu1003",
|
||||
FileUID: "ft2es39q45bnlqd0",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.5088545446490167,
|
||||
SubjSrc: "",
|
||||
|
@ -311,7 +308,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actor-a-3": Marker{ //Photo10
|
||||
MarkerUID: "mt9k3pw1wowu1004",
|
||||
FileUID: "fikjs39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.3139983399779298,
|
||||
SubjSrc: "",
|
||||
|
@ -331,7 +328,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actor-a-4": Marker{ //19800101_000002_D640C559
|
||||
MarkerUID: "mt9k3pw1wowu1005",
|
||||
FileUID: "ft8es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.3139983399779298,
|
||||
SubjSrc: "",
|
||||
|
|
|
@ -19,8 +19,6 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
|
|||
return json.Marshal(&struct {
|
||||
UID string
|
||||
FileUID string
|
||||
FileHash string
|
||||
CropArea string
|
||||
Type string
|
||||
Src string
|
||||
Name string
|
||||
|
@ -36,12 +34,11 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
|
|||
Q int `json:",omitempty"`
|
||||
Size int `json:",omitempty"`
|
||||
Score int `json:",omitempty"`
|
||||
Thumb string
|
||||
CreatedAt time.Time
|
||||
}{
|
||||
UID: m.MarkerUID,
|
||||
FileUID: m.FileUID,
|
||||
FileHash: m.FileHash,
|
||||
CropArea: m.CropArea,
|
||||
Type: m.MarkerType,
|
||||
Src: m.MarkerSrc,
|
||||
Name: name,
|
||||
|
@ -57,6 +54,7 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
|
|||
Q: m.Q,
|
||||
Size: m.Size,
|
||||
Score: m.Score,
|
||||
Thumb: m.Thumb,
|
||||
CreatedAt: m.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -22,12 +22,14 @@ func TestMarker_TableName(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNewMarker(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel)
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 100, 29)
|
||||
assert.IsType(t, &Marker{}, m)
|
||||
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
||||
assert.Equal(t, "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818", m.FileHash)
|
||||
assert.Equal(t, "1340ce163163", m.CropArea)
|
||||
assert.Equal(t, "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818-1340ce163163", m.Thumb)
|
||||
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjUID)
|
||||
assert.True(t, m.MarkerReview)
|
||||
assert.Equal(t, 119, m.Q)
|
||||
assert.Equal(t, 29, m.Score)
|
||||
assert.Equal(t, SrcImage, m.MarkerSrc)
|
||||
assert.Equal(t, MarkerLabel, m.MarkerType)
|
||||
}
|
||||
|
@ -93,7 +95,7 @@ func TestMarker_SaveForm(t *testing.T) {
|
|||
|
||||
func TestUpdateOrCreateMarker(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel)
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 100, 65)
|
||||
assert.IsType(t, &Marker{}, m)
|
||||
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
||||
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjUID)
|
||||
|
@ -118,7 +120,7 @@ func TestUpdateOrCreateMarker(t *testing.T) {
|
|||
|
||||
func TestMarker_Updates(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel)
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
|
||||
m, err := UpdateOrCreateMarker(m)
|
||||
|
||||
if err != nil {
|
||||
|
@ -143,7 +145,7 @@ func TestMarker_Updates(t *testing.T) {
|
|||
|
||||
func TestMarker_Update(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel)
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
|
||||
m, err := UpdateOrCreateMarker(m)
|
||||
|
||||
if err != nil {
|
||||
|
@ -167,7 +169,7 @@ func TestMarker_Update(t *testing.T) {
|
|||
|
||||
func TestMarker_Save(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel)
|
||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
|
||||
|
||||
m, err := UpdateOrCreateMarker(m)
|
||||
|
||||
|
@ -200,8 +202,6 @@ func TestMarker_Save(t *testing.T) {
|
|||
assert.Empty(t, p.Files)
|
||||
p.PreloadFiles()
|
||||
assert.NotEmpty(t, p.Files)
|
||||
|
||||
// t.Logf("FILES: %#v", p.Files)
|
||||
})
|
||||
t.Run("invalid position", func(t *testing.T) {
|
||||
m := Marker{X: 0, Y: 0}
|
||||
|
@ -431,7 +431,7 @@ func TestMarker_Subject(t *testing.T) {
|
|||
|
||||
func TestMarker_GetFace(t *testing.T) {
|
||||
t.Run("ExistingFaceID", func(t *testing.T) {
|
||||
m := Marker{face: &Face{ID: "1234"}, FaceID: "1234"}
|
||||
m := Marker{MarkerUID: "mqzop6s14ahkyd24", FaceID: "1234", face: &Face{ID: "1234"}}
|
||||
|
||||
if f := m.Face(); f == nil {
|
||||
t.Fatal("return value must not be nil")
|
||||
|
@ -441,7 +441,7 @@ func TestMarker_GetFace(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("ConflictingFaceID", func(t *testing.T) {
|
||||
m := Marker{face: &Face{ID: "1234"}, FaceID: "8888"}
|
||||
m := Marker{MarkerUID: "mqzop6s14ahkyd24", FaceID: "8888", face: &Face{ID: "1234"}}
|
||||
|
||||
if f := m.Face(); f != nil {
|
||||
t.Fatal("return value must be nil")
|
||||
|
@ -451,7 +451,7 @@ func TestMarker_GetFace(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("find face with ID", func(t *testing.T) {
|
||||
m := Marker{FaceID: "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG6"}
|
||||
m := Marker{MarkerUID: "mqzop6s14ahkyd24", FaceID: "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG6"}
|
||||
|
||||
if f := m.Face(); f == nil {
|
||||
t.Fatal("return value must not be nil")
|
||||
|
@ -460,12 +460,13 @@ func TestMarker_GetFace(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("low quality marker", func(t *testing.T) {
|
||||
m := Marker{FaceID: "", SubjSrc: SrcManual, Size: 130}
|
||||
m := Marker{MarkerUID: "", FaceID: "", SubjSrc: SrcManual, Size: 130}
|
||||
|
||||
assert.Nil(t, m.Face())
|
||||
})
|
||||
t.Run("create face", func(t *testing.T) {
|
||||
m := Marker{
|
||||
MarkerUID: "mqzop6s14ahkyd24",
|
||||
FaceID: "",
|
||||
EmbeddingsJSON: MarkerFixtures.Get("actress-a-1").EmbeddingsJSON,
|
||||
SubjSrc: SrcManual,
|
||||
|
@ -522,7 +523,6 @@ func TestMarker_SetFace(t *testing.T) {
|
|||
assert.True(t, updated2)
|
||||
assert.Empty(t, m.FaceID)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestMarker_RefreshPhotos(t *testing.T) {
|
||||
|
|
|
@ -15,9 +15,9 @@ var cropArea4 = crop.Area{Name: "face", X: 0.298133, Y: 0.216944, W: 0.255556, H
|
|||
|
||||
func TestMarkers_Contains(t *testing.T) {
|
||||
t.Run("Examples", func(t *testing.T) {
|
||||
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace)
|
||||
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea2, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace)
|
||||
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace)
|
||||
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace, 100, 65)
|
||||
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea2, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace, 100, 65)
|
||||
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace, 100, 65)
|
||||
|
||||
m := Markers{m1}
|
||||
|
||||
|
@ -28,11 +28,11 @@ func TestMarkers_Contains(t *testing.T) {
|
|||
file := File{FileHash: "cca7c46a4d39e933c30805e546028fe3eab361b5"}
|
||||
|
||||
markers := Markers{
|
||||
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.549479, Y: 0.179688, W: 0.393229, H: 0.294922}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace),
|
||||
*NewMarker(file, crop.Area{Name: "subj-2", X: 0.0833333, Y: 0.321289, W: 0.476562, H: 0.357422}, "jqyzml91cf2yyfi7", SrcImage, MarkerFace),
|
||||
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.549479, Y: 0.179688, W: 0.393229, H: 0.294922}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
|
||||
*NewMarker(file, crop.Area{Name: "subj-2", X: 0.0833333, Y: 0.321289, W: 0.476562, H: 0.357422}, "jqyzml91cf2yyfi7", SrcImage, MarkerFace, 100, 65),
|
||||
}
|
||||
|
||||
conflicting := *NewMarker(file, crop.Area{Name: "subj-2", X: 0.190104, Y: 0.40918, W: 0.31901, H: 0.239258}, "jqyzml91cf2yyfi7", SrcImage, MarkerFace)
|
||||
conflicting := *NewMarker(file, crop.Area{Name: "subj-2", X: 0.190104, Y: 0.40918, W: 0.31901, H: 0.239258}, "jqyzml91cf2yyfi7", SrcImage, MarkerFace, 100, 65)
|
||||
|
||||
assert.True(t, markers.Contains(conflicting))
|
||||
})
|
||||
|
@ -40,10 +40,10 @@ func TestMarkers_Contains(t *testing.T) {
|
|||
file := File{FileHash: "a6c46e43b83fc02309b1c49e1ed7273f1f414610"}
|
||||
|
||||
markers := Markers{
|
||||
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.388021, Y: 0.365234, W: 0.179688, H: 0.134766}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace),
|
||||
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.388021, Y: 0.365234, W: 0.179688, H: 0.134766}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
|
||||
}
|
||||
|
||||
conflicting := *NewMarker(file, crop.Area{Name: "subj-1", X: 0.34375, Y: 0.291992, W: 0.266927, H: 0.200195}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace)
|
||||
conflicting := *NewMarker(file, crop.Area{Name: "subj-1", X: 0.34375, Y: 0.291992, W: 0.266927, H: 0.200195}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65)
|
||||
|
||||
assert.True(t, markers.Contains(conflicting))
|
||||
})
|
||||
|
@ -51,19 +51,19 @@ func TestMarkers_Contains(t *testing.T) {
|
|||
file := File{FileHash: "243cdbe99b865607f98a951e748d528bc22f3143"}
|
||||
|
||||
markers := Markers{
|
||||
*NewMarker(file, crop.Area{Name: "no-face", X: 0.322656, Y: 0.3, W: 0.180469, H: 0.240625}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace),
|
||||
*NewMarker(file, crop.Area{Name: "no-face", X: 0.322656, Y: 0.3, W: 0.180469, H: 0.240625}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
|
||||
}
|
||||
|
||||
conflicting := *NewMarker(file, crop.Area{Name: "face", X: 0.325, Y: 0.0510417, W: 0.136719, H: 0.182292}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace)
|
||||
conflicting := *NewMarker(file, crop.Area{Name: "face", X: 0.325, Y: 0.0510417, W: 0.136719, H: 0.182292}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65)
|
||||
|
||||
assert.False(t, markers.Contains(conflicting))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMarkers_FaceCount(t *testing.T) {
|
||||
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace)
|
||||
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea4, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace)
|
||||
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace)
|
||||
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace, 100, 65)
|
||||
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea4, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace, 100, 65)
|
||||
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace, 100, 65)
|
||||
m3.MarkerInvalid = true
|
||||
|
||||
m := Markers{m1, m2, m3}
|
||||
|
|
|
@ -21,8 +21,6 @@ type Subjects []Subject
|
|||
// Subject represents a named photo subject, typically a person.
|
||||
type Subject struct {
|
||||
SubjUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
|
||||
MarkerUID string `gorm:"type:VARBINARY(42);index" json:"MarkerUID" yaml:"MarkerUID,omitempty"`
|
||||
MarkerSrc string `gorm:"type:VARBINARY(8);default:''" json:"MarkerSrc,omitempty" yaml:"MarkerSrc,omitempty"`
|
||||
SubjType string `gorm:"type:VARBINARY(8);default:''" json:"Type,omitempty" yaml:"Type,omitempty"`
|
||||
SubjSrc string `gorm:"type:VARBINARY(8);default:''" json:"Src,omitempty" yaml:"Src,omitempty"`
|
||||
SubjSlug string `gorm:"type:VARBINARY(255);index;default:''" json:"Slug" yaml:"-"`
|
||||
|
@ -34,6 +32,8 @@ type Subject struct {
|
|||
SubjPrivate bool `gorm:"default:false" json:"Private" yaml:"Private,omitempty"`
|
||||
SubjExcluded bool `gorm:"default:false" json:"Excluded" yaml:"Excluded,omitempty"`
|
||||
FileCount int `gorm:"default:0" json:"FileCount" yaml:"-"`
|
||||
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb" yaml:"Thumb,omitempty"`
|
||||
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
|
||||
MetadataJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"Metadata,omitempty" yaml:"Metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
|
|
|
@ -170,13 +170,13 @@ func UpdateSubjectPreviews() (err error) {
|
|||
Error */
|
||||
|
||||
err = Db().Table(entity.Subject{}.TableName()).
|
||||
UpdateColumn("marker_uid", gorm.Expr("(SELECT m.marker_uid FROM "+
|
||||
UpdateColumn("thumb", gorm.Expr("(SELECT m.thumb FROM "+
|
||||
fmt.Sprintf(
|
||||
"%s m WHERE m.subj_uid = %s.subj_uid ",
|
||||
entity.Marker{}.TableName(),
|
||||
entity.Subject{}.TableName())+
|
||||
` AND m.file_hash <> '' ORDER BY m.subj_src DESC, m.q DESC LIMIT 1)
|
||||
WHERE marker_src = '' AND deleted_at IS NULL`)).
|
||||
` AND m.thumb <> '' ORDER BY m.subj_src DESC, m.q DESC LIMIT 1)
|
||||
WHERE (thumb_src = '' OR thumb_src IS NULL) AND deleted_at IS NULL`)).
|
||||
Error
|
||||
|
||||
/** err = Db().Table(entity.Subject{}.TableName()).
|
||||
|
|
|
@ -10,7 +10,7 @@ type Album struct {
|
|||
AlbumUID string `json:"UID"`
|
||||
ParentUID string `json:"ParentUID"`
|
||||
Thumb string `json:"Thumb"`
|
||||
ThumbSrc string `json:"ThumbSrc"`
|
||||
ThumbSrc string `json:"ThumbSrc,omitempty"`
|
||||
AlbumSlug string `json:"Slug"`
|
||||
AlbumType string `json:"Type"`
|
||||
AlbumTitle string `json:"Title"`
|
||||
|
|
|
@ -9,7 +9,7 @@ type Label struct {
|
|||
ID uint `json:"ID"`
|
||||
LabelUID string `json:"UID"`
|
||||
Thumb string `json:"Thumb"`
|
||||
ThumbSrc string `json:"ThumbSrc"`
|
||||
ThumbSrc string `json:"ThumbSrc,omitempty"`
|
||||
LabelSlug string `json:"Slug"`
|
||||
CustomSlug string `json:"CustomSlug"`
|
||||
LabelName string `json:"Name"`
|
||||
|
|
|
@ -19,10 +19,7 @@ func Subjects(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||
|
||||
// Base query.
|
||||
s := UnscopedDb().Table(entity.Subject{}.TableName()).
|
||||
Select(fmt.Sprintf("%s.*, m.file_hash, m.crop_area", entity.Subject{}.TableName()))
|
||||
|
||||
// Join markers table for face thumbs.
|
||||
s = s.Joins(fmt.Sprintf("LEFT JOIN %s m ON m.marker_uid = %s.marker_uid", entity.Marker{}.TableName(), entity.Subject{}.TableName()))
|
||||
Select(fmt.Sprintf("%s.*", entity.Subject{}.TableName()))
|
||||
|
||||
// Limit result count.
|
||||
if f.Count > 0 && f.Count <= MaxResults {
|
||||
|
|
|
@ -13,8 +13,8 @@ type Subject struct {
|
|||
SubjPrivate bool `json:"Private"`
|
||||
SubjExcluded bool `json:"Excluded"`
|
||||
FileCount int `json:"FileCount"`
|
||||
FileHash string `json:"FileHash"`
|
||||
CropArea string `json:"CropArea"`
|
||||
Thumb string `json:"Thumb"`
|
||||
ThumbSrc string `json:"ThumbSrc,omitempty"`
|
||||
}
|
||||
|
||||
// SubjectResults represents subject search results.
|
||||
|
|
|
@ -65,7 +65,6 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
|
||||
// Thumbnails and downloads.
|
||||
api.GetThumb(v1)
|
||||
api.GetThumbCrop(v1)
|
||||
api.GetDownload(v1)
|
||||
api.GetVideo(v1)
|
||||
api.CreateZip(v1)
|
||||
|
|
Loading…
Reference in a new issue