mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 08:44:04 +01:00
People: Add autocomplete for selecting a person #22
This commit is contained in:
980
frontend/package-lock.json
generated
980
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,7 @@ export default class Config {
|
|||||||
|
|
||||||
Event.subscribe("config.updated", (ev, data) => this.setValues(data.config));
|
Event.subscribe("config.updated", (ev, data) => this.setValues(data.config));
|
||||||
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
|
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
|
||||||
|
Event.subscribe("people", (ev, data) => this.onPeople(ev, data));
|
||||||
|
|
||||||
if (this.has("settings")) {
|
if (this.has("settings")) {
|
||||||
this.setTheme(this.get("settings").ui.theme);
|
this.setTheme(this.get("settings").ui.theme);
|
||||||
@@ -134,6 +135,53 @@ export default class Config {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPeople(ev, data) {
|
||||||
|
const type = ev.split(".")[1];
|
||||||
|
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(ev, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.values.people) {
|
||||||
|
this.values.people = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "created":
|
||||||
|
this.values.people.push(...data.entities);
|
||||||
|
break;
|
||||||
|
case "updated":
|
||||||
|
for (let i = 0; i < data.entities.length; i++) {
|
||||||
|
const values = data.entities[i];
|
||||||
|
|
||||||
|
this.values.people
|
||||||
|
.filter((m) => m.UID === values.UID)
|
||||||
|
.forEach((m) => {
|
||||||
|
for (let key in values) {
|
||||||
|
if (
|
||||||
|
key !== "UID" &&
|
||||||
|
values.hasOwnProperty(key) &&
|
||||||
|
values[key] != null &&
|
||||||
|
typeof values[key] !== "object"
|
||||||
|
) {
|
||||||
|
m[key] = values[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "deleted":
|
||||||
|
for (let i = 0; i < data.entities.length; i++) {
|
||||||
|
const index = this.values.people.findIndex((m) => m.UID === data.entities[i]);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.values.people.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onCount(ev, data) {
|
onCount(ev, data) {
|
||||||
const type = ev.split(".")[1];
|
const type = ev.split(".")[1];
|
||||||
|
|
||||||
|
|||||||
@@ -360,6 +360,6 @@ body.chrome #photoprism .search-results .result {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#photoprism .face-results .invalid {
|
#photoprism .face-results .is-marker.is-invalid {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
<v-flex
|
<v-flex
|
||||||
v-for="(marker, index) in markers"
|
v-for="(marker, index) in markers"
|
||||||
:key="index"
|
:key="index"
|
||||||
xs6 sm4 md3 lg2 xl1 d-flex
|
xs6 md3 xl2 d-flex
|
||||||
>
|
>
|
||||||
<v-card tile
|
<v-card tile
|
||||||
:data-id="marker.UID"
|
:data-id="marker.UID"
|
||||||
style="user-select: none"
|
style="user-select: none;"
|
||||||
:class="{invalid: marker.Invalid}"
|
:class="marker.classes()"
|
||||||
class="result accent lighten-3">
|
class="result accent lighten-3">
|
||||||
<div class="card-background accent lighten-3"></div>
|
<div class="card-background accent lighten-3"></div>
|
||||||
<canvas :id="'face-' + marker.UID" :key="marker.UID" width="300" height="300" style="width: 100%"
|
<canvas :id="'face-' + marker.UID" :key="marker.UID" width="300" height="300" style="width: 100%"
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
<v-layout v-else row wrap align-center>
|
<v-layout v-else-if="marker.SubjectUID" row wrap align-center>
|
||||||
<v-flex xs12 class="text-xs-left pa-0">
|
<v-flex xs12 class="text-xs-left pa-0">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="marker.Name"
|
v-model="marker.Name"
|
||||||
@@ -59,12 +59,41 @@
|
|||||||
single-line
|
single-line
|
||||||
solo-inverted
|
solo-inverted
|
||||||
clearable
|
clearable
|
||||||
|
clear-icon="eject"
|
||||||
@click:clear="clearSubject(marker)"
|
@click:clear="clearSubject(marker)"
|
||||||
@change="rename(marker)"
|
@change="rename(marker)"
|
||||||
@keyup.enter.native="rename(marker)"
|
@keyup.enter.native="rename(marker)"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
|
<v-layout v-else row wrap align-center>
|
||||||
|
<v-flex xs12 class="text-xs-left pa-0">
|
||||||
|
<v-combobox
|
||||||
|
v-model="marker.Name"
|
||||||
|
style="z-index: 250"
|
||||||
|
:items="$config.values.people"
|
||||||
|
item-value="Name"
|
||||||
|
item-text="Name"
|
||||||
|
:disabled="busy"
|
||||||
|
:return-object="false"
|
||||||
|
:menu-props="menuProps"
|
||||||
|
:allow-overflow="false"
|
||||||
|
:filter="filterSubjects"
|
||||||
|
:hint="$gettext('Name')"
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
solo-inverted
|
||||||
|
open-on-clear
|
||||||
|
append-icon=""
|
||||||
|
prepend-inner-icon="person_add"
|
||||||
|
browser-autocomplete="off"
|
||||||
|
class="input-name pa-0 ma-0"
|
||||||
|
@change="rename(marker)"
|
||||||
|
@keyup.enter.native="rename(marker)"
|
||||||
|
>
|
||||||
|
</v-combobox>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
@@ -89,6 +118,7 @@ export default {
|
|||||||
disabled: !this.$config.feature("edit"),
|
disabled: !this.$config.feature("edit"),
|
||||||
config: this.$config.values,
|
config: this.$config.values,
|
||||||
readonly: this.$config.get("readonly"),
|
readonly: this.$config.get("readonly"),
|
||||||
|
menuProps:{"closeOnClick":false, "closeOnContentClick":true, "openOnClick":false, "maxHeight":300},
|
||||||
textRule: (v) => {
|
textRule: (v) => {
|
||||||
if (!v || !v.length) {
|
if (!v || !v.length) {
|
||||||
return this.$gettext("Name");
|
return this.$gettext("Name");
|
||||||
@@ -148,6 +178,15 @@ export default {
|
|||||||
this.busy = true;
|
this.busy = true;
|
||||||
marker.approve().finally(() => this.busy = false);
|
marker.approve().finally(() => this.busy = false);
|
||||||
},
|
},
|
||||||
|
filterSubjects(item, queryText) {
|
||||||
|
if (!item || !item.Keywords) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = queryText.toLocaleLowerCase();
|
||||||
|
|
||||||
|
return item.Keywords.findIndex((w) => w.indexOf(q) > -1) > -1;
|
||||||
|
},
|
||||||
clearSubject(marker) {
|
clearSubject(marker) {
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
marker.clearSubject(marker).finally(() => this.busy = false);
|
marker.clearSubject(marker).finally(() => this.busy = false);
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
deletable-chips multiple color="secondary-dark"
|
deletable-chips multiple color="secondary-dark"
|
||||||
class="my-0 input-albums"
|
class="my-0 input-albums"
|
||||||
:items="albums"
|
:items="albums"
|
||||||
item-text="Title"
|
|
||||||
item-value="UID"
|
item-value="UID"
|
||||||
|
item-text="Title"
|
||||||
:allow-overflow="false"
|
:allow-overflow="false"
|
||||||
:label="$gettext('Select albums or create a new one')"
|
:label="$gettext('Select albums or create a new one')"
|
||||||
return-object
|
return-object
|
||||||
|
|||||||
@@ -40,10 +40,13 @@ export class Marker extends RestModel {
|
|||||||
return {
|
return {
|
||||||
UID: "",
|
UID: "",
|
||||||
FileUID: "",
|
FileUID: "",
|
||||||
|
FileHash: "",
|
||||||
|
FileArea: "",
|
||||||
Type: "",
|
Type: "",
|
||||||
Src: "",
|
Src: "",
|
||||||
Name: "",
|
Name: "",
|
||||||
Invalid: false,
|
Invalid: false,
|
||||||
|
Review: false,
|
||||||
X: 0.0,
|
X: 0.0,
|
||||||
Y: 0.0,
|
Y: 0.0,
|
||||||
W: 0.0,
|
W: 0.0,
|
||||||
@@ -54,7 +57,6 @@ export class Marker extends RestModel {
|
|||||||
SubjectUID: "",
|
SubjectUID: "",
|
||||||
Score: 0,
|
Score: 0,
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Review: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +82,15 @@ export class Marker extends RestModel {
|
|||||||
return this.Name;
|
return this.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnailUrl(fileHash, size) {
|
thumbnailUrl(size) {
|
||||||
if (fileHash && this.CropID) {
|
if (!size) {
|
||||||
return `${config.contentUri}/t/${fileHash}/${config.previewToken()}/${size}_${this.CropID}`;
|
size = "crop_160";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.FileHash && this.FileArea) {
|
||||||
|
return `${config.contentUri}/t/${this.FileHash}/${config.previewToken()}/${size}/${
|
||||||
|
this.FileArea
|
||||||
|
}`;
|
||||||
} else {
|
} else {
|
||||||
return `${config.contentUri}/svg/portrait`;
|
return `${config.contentUri}/svg/portrait`;
|
||||||
}
|
}
|
||||||
@@ -106,8 +114,8 @@ export class Marker extends RestModel {
|
|||||||
|
|
||||||
rename() {
|
rename() {
|
||||||
if (!this.Name || this.Name.trim() === "") {
|
if (!this.Name || this.Name.trim() === "") {
|
||||||
// Don't update empty name.
|
// Can't save an empty name.
|
||||||
return Promise.reject();
|
return Promise.resolve(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.SubjectSrc = src.Manual;
|
this.SubjectSrc = src.Manual;
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -55,7 +55,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8
|
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8
|
||||||
github.com/tensorflow/tensorflow v1.15.2
|
github.com/tensorflow/tensorflow v1.15.2
|
||||||
github.com/tidwall/gjson v1.8.1
|
github.com/tidwall/gjson v1.9.0
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/ugorji/go v1.2.6 // indirect
|
github.com/ugorji/go v1.2.6 // indirect
|
||||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
||||||
@@ -63,7 +63,7 @@ require (
|
|||||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a
|
golang.org/x/net v0.0.0-20210902165921-8d991716f632
|
||||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect
|
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
gonum.org/v1/gonum v0.9.3
|
gonum.org/v1/gonum v0.9.3
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -288,8 +288,8 @@ github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8 h1:ipNUBPHSUmH
|
|||||||
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
|
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
|
||||||
github.com/tensorflow/tensorflow v1.15.2 h1:7/f/A664Tml/nRJg04+p3StcrsT53mkcvmxYHXI21Qo=
|
github.com/tensorflow/tensorflow v1.15.2 h1:7/f/A664Tml/nRJg04+p3StcrsT53mkcvmxYHXI21Qo=
|
||||||
github.com/tensorflow/tensorflow v1.15.2/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90=
|
github.com/tensorflow/tensorflow v1.15.2/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90=
|
||||||
github.com/tidwall/gjson v1.8.1 h1:8j5EE9Hrh3l9Od1OIEDAb7IpezNA20UdRngNAj5N0WU=
|
github.com/tidwall/gjson v1.9.0 h1:+Od7AE26jAaMgVC31cQV/Ope5iKXulNMflrlB7k+F9E=
|
||||||
github.com/tidwall/gjson v1.8.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
|
github.com/tidwall/gjson v1.9.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
|
||||||
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
|
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
|
||||||
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
@@ -385,8 +385,8 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/
|
|||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw=
|
golang.org/x/net v0.0.0-20210902165921-8d991716f632 h1:900XJE4Rn/iPU+xD5ZznOe4GKKc4AdFK0IO1P6Z3/lQ=
|
||||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210902165921-8d991716f632/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
|
|||||||
"albums.*",
|
"albums.*",
|
||||||
"labels.*",
|
"labels.*",
|
||||||
"subjects.*",
|
"subjects.*",
|
||||||
|
"people.*",
|
||||||
"sync.*",
|
"sync.*",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,20 +18,20 @@ import (
|
|||||||
// IndexCommand registers the index cli command.
|
// IndexCommand registers the index cli command.
|
||||||
var IndexCommand = cli.Command{
|
var IndexCommand = cli.Command{
|
||||||
Name: "index",
|
Name: "index",
|
||||||
Usage: "Indexes media files in originals folder",
|
Usage: "indexes media files in the originals folder",
|
||||||
UsageText: `To limit scope, a sub folder may be passed as first argument.`,
|
ArgsUsage: "[path]",
|
||||||
Flags: indexFlags,
|
Flags: indexFlags,
|
||||||
Action: indexAction,
|
Action: indexAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
var indexFlags = []cli.Flag{
|
var indexFlags = []cli.Flag{
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "all, a",
|
Name: "force, f",
|
||||||
Usage: "re-index all originals, including unchanged files",
|
Usage: "re-index all originals, including unchanged files",
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "cleanup",
|
Name: "cleanup, c",
|
||||||
Usage: "removes orphan index entries and thumbnails",
|
Usage: "remove orphan index entries and thumbnails",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
@@ -43,7 +42,7 @@ type ClientConfig struct {
|
|||||||
Cameras entity.Cameras `json:"cameras"`
|
Cameras entity.Cameras `json:"cameras"`
|
||||||
Lenses entity.Lenses `json:"lenses"`
|
Lenses entity.Lenses `json:"lenses"`
|
||||||
Countries entity.Countries `json:"countries"`
|
Countries entity.Countries `json:"countries"`
|
||||||
Subjects query.SubjectResults `json:"subjects"`
|
People entity.People `json:"people"`
|
||||||
Thumbs ThumbTypes `json:"thumbs"`
|
Thumbs ThumbTypes `json:"thumbs"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
MapKey string `json:"mapKey"`
|
MapKey string `json:"mapKey"`
|
||||||
@@ -386,12 +385,6 @@ func (c *Config) UserConfig() ClientConfig {
|
|||||||
Select("(COUNT(*) - 1) AS countries").
|
Select("(COUNT(*) - 1) AS countries").
|
||||||
Take(&result.Count)
|
Take(&result.Count)
|
||||||
|
|
||||||
c.Db().
|
|
||||||
Table(entity.Subject{}.TableName()).
|
|
||||||
Select("SUM(deleted_at IS NULL) AS people").
|
|
||||||
Where("subject_src <> ? AND subject_type = ?", entity.SrcDefault, entity.SubjectPerson).
|
|
||||||
Take(&result.Count)
|
|
||||||
|
|
||||||
c.Db().
|
c.Db().
|
||||||
Table("places").
|
Table("places").
|
||||||
Select("SUM(photo_count > 0) AS places").
|
Select("SUM(photo_count > 0) AS places").
|
||||||
@@ -402,7 +395,9 @@ func (c *Config) UserConfig() ClientConfig {
|
|||||||
Order("country_slug").
|
Order("country_slug").
|
||||||
Find(&result.Countries)
|
Find(&result.Countries)
|
||||||
|
|
||||||
result.Subjects, _ = query.SubjectSearch(form.SubjectSearch{Type: entity.SubjectPerson})
|
// People are subjects with type person.
|
||||||
|
result.Count.People, _ = query.PeopleCount()
|
||||||
|
result.People, _ = query.People()
|
||||||
|
|
||||||
c.Db().
|
c.Db().
|
||||||
Where("id IN (SELECT photos.camera_id FROM photos WHERE photos.photo_quality >= 0 OR photos.deleted_at IS NULL)").
|
Where("id IN (SELECT photos.camera_id FROM photos WHERE photos.photo_quality >= 0 OR photos.deleted_at IS NULL)").
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ func CreateDefaultFixtures() {
|
|||||||
CreateUnknownCountry()
|
CreateUnknownCountry()
|
||||||
CreateUnknownCamera()
|
CreateUnknownCamera()
|
||||||
CreateUnknownLens()
|
CreateUnknownLens()
|
||||||
CreateUnknownFace()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigrateDb creates all tables and inserts default entities as needed.
|
// MigrateDb creates all tables and inserts default entities as needed.
|
||||||
|
|||||||
@@ -31,23 +31,9 @@ type Face struct {
|
|||||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt,omitempty"`
|
UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnknownFace can be used as a placeholder for unknown faces.
|
|
||||||
var UnknownFace = Face{
|
|
||||||
ID: UnknownID,
|
|
||||||
FaceSrc: SrcDefault,
|
|
||||||
MatchedAt: TimePointer(),
|
|
||||||
SubjectUID: "",
|
|
||||||
EmbeddingJSON: []byte{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Faceless can be used as argument to match unmatched face markers.
|
// Faceless can be used as argument to match unmatched face markers.
|
||||||
var Faceless = []string{""}
|
var Faceless = []string{""}
|
||||||
|
|
||||||
// CreateUnknownFace initializes the database with a placeholder for unknown faces.
|
|
||||||
func CreateUnknownFace() {
|
|
||||||
_ = UnknownFace.Create()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName returns the entity database table name.
|
// TableName returns the entity database table name.
|
||||||
func (Face) TableName() string {
|
func (Face) TableName() string {
|
||||||
return "faces_dev6"
|
return "faces_dev6"
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
|
// UnknownFace can be used as a placeholder for unknown faces.
|
||||||
|
var UnknownFace = Face{
|
||||||
|
ID: UnknownID,
|
||||||
|
FaceSrc: SrcDefault,
|
||||||
|
MatchedAt: TimePointer(),
|
||||||
|
SubjectUID: "",
|
||||||
|
EmbeddingJSON: []byte{},
|
||||||
|
}
|
||||||
|
|
||||||
type FaceMap map[string]Face
|
type FaceMap map[string]Face
|
||||||
|
|
||||||
func (m FaceMap) Get(name string) Face {
|
func (m FaceMap) Get(name string) Face {
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ func (m *File) AddFaces(faces face.Faces) {
|
|||||||
|
|
||||||
// AddFace adds a face marker to the file.
|
// AddFace adds a face marker to the file.
|
||||||
func (m *File) AddFace(f face.Face, subjectUID string) {
|
func (m *File) AddFace(f face.Face, subjectUID string) {
|
||||||
marker := *NewFaceMarker(f, m.FileUID, subjectUID)
|
marker := *NewFaceMarker(f, *m, subjectUID)
|
||||||
|
|
||||||
if markers := m.Markers(); !markers.Contains(marker) {
|
if markers := m.Markers(); !markers.Contains(marker) {
|
||||||
markers.Append(marker)
|
markers.Append(marker)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/crop"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/photoprism/photoprism/pkg/rnd"
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
|
|
||||||
@@ -26,14 +28,15 @@ const (
|
|||||||
type Marker struct {
|
type Marker struct {
|
||||||
MarkerUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
|
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"`
|
FileUID string `gorm:"type:VARBINARY(42);index;" json:"FileUID" yaml:"FileUID"`
|
||||||
|
FileHash string `gorm:"type:VARBINARY(128);index" json:"FileHash" yaml:"FileHash,omitempty"`
|
||||||
|
FileArea string `gorm:"type:VARBINARY(16);default:''" json:"FileArea" yaml:"FileArea,omitempty"`
|
||||||
MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"`
|
MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"`
|
||||||
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
||||||
MarkerName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name,omitempty"`
|
MarkerName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name,omitempty"`
|
||||||
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
|
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
|
||||||
SubjectUID string `gorm:"type:VARBINARY(42);index;" json:"SubjectUID" yaml:"SubjectUID,omitempty"`
|
SubjectUID string `gorm:"type:VARBINARY(42);index:idx_markers_subject;" json:"SubjectUID" yaml:"SubjectUID,omitempty"`
|
||||||
SubjectSrc string `gorm:"type:VARBINARY(8);default:'';" json:"SubjectSrc" yaml:"SubjectSrc,omitempty"`
|
SubjectSrc string `gorm:"type:VARBINARY(8);index:idx_markers_subject;default:'';" json:"SubjectSrc" yaml:"SubjectSrc,omitempty"`
|
||||||
subject *Subject `gorm:"foreignkey:SubjectUID;association_foreignkey:SubjectUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
subject *Subject `gorm:"foreignkey:SubjectUID;association_foreignkey:SubjectUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
||||||
CropID string `gorm:"type:VARBINARY(16);default:''" json:"CropID,omitempty" yaml:"CropID,omitempty"`
|
|
||||||
FaceID string `gorm:"type:VARBINARY(42);index;" json:"FaceID" yaml:"FaceID,omitempty"`
|
FaceID string `gorm:"type:VARBINARY(42);index;" json:"FaceID" yaml:"FaceID,omitempty"`
|
||||||
FaceDist float64 `gorm:"default:-1" json:"FaceDist" yaml:"FaceDist,omitempty"`
|
FaceDist float64 `gorm:"default:-1" json:"FaceDist" yaml:"FaceDist,omitempty"`
|
||||||
face *Face `gorm:"foreignkey:FaceID;association_foreignkey:ID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
face *Face `gorm:"foreignkey:FaceID;association_foreignkey:ID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
||||||
@@ -52,9 +55,6 @@ type Marker struct {
|
|||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnknownMarker can be used as a default for unknown markers.
|
|
||||||
var UnknownMarker = NewMarker("", "", SrcDefault, MarkerUnknown, 0, 0, 0, 0)
|
|
||||||
|
|
||||||
// TableName returns the entity database table name.
|
// TableName returns the entity database table name.
|
||||||
func (Marker) TableName() string {
|
func (Marker) TableName() string {
|
||||||
return "markers_dev6"
|
return "markers_dev6"
|
||||||
@@ -70,35 +70,34 @@ func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMarker creates a new entity.
|
// NewMarker creates a new entity.
|
||||||
func NewMarker(fileUID, subjectUID, markerSrc, markerType string, x, y, w, h float32) *Marker {
|
func NewMarker(file File, area crop.Area, subjectUID, markerSrc, markerType string) *Marker {
|
||||||
m := &Marker{
|
m := &Marker{
|
||||||
FileUID: fileUID,
|
FileUID: file.FileUID,
|
||||||
SubjectUID: subjectUID,
|
FileHash: file.FileHash,
|
||||||
|
FileArea: area.String(),
|
||||||
MarkerSrc: markerSrc,
|
MarkerSrc: markerSrc,
|
||||||
MarkerType: markerType,
|
MarkerType: markerType,
|
||||||
X: x,
|
SubjectUID: subjectUID,
|
||||||
Y: y,
|
X: area.X,
|
||||||
W: w,
|
Y: area.Y,
|
||||||
H: h,
|
W: area.W,
|
||||||
|
H: area.H,
|
||||||
|
MatchedAt: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFaceMarker creates a new entity.
|
// NewFaceMarker creates a new entity.
|
||||||
func NewFaceMarker(f face.Face, fileUID, subjectUID string) *Marker {
|
func NewFaceMarker(f face.Face, file File, subjectUID string) *Marker {
|
||||||
pos := f.Crop()
|
m := NewMarker(file, f.CropArea(), subjectUID, SrcImage, MarkerFace)
|
||||||
|
|
||||||
m := NewMarker(fileUID, subjectUID, SrcImage, MarkerFace, pos.X, pos.Y, pos.W, pos.H)
|
|
||||||
|
|
||||||
m.MatchedAt = nil
|
|
||||||
m.FaceDist = -1
|
|
||||||
m.CropID = f.Crop().ID()
|
|
||||||
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
|
||||||
m.LandmarksJSON = f.RelativeLandmarksJSON()
|
|
||||||
m.Size = f.Size()
|
m.Size = f.Size()
|
||||||
m.Score = f.Score
|
m.Score = f.Score
|
||||||
m.Review = f.Score < 30
|
m.Review = f.Score < 30
|
||||||
|
m.FaceDist = -1
|
||||||
|
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
||||||
|
m.LandmarksJSON = f.RelativeLandmarksJSON()
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
@@ -372,27 +371,26 @@ func (m *Marker) Subject() (subj *Subject) {
|
|||||||
|
|
||||||
// ClearSubject removes an existing subject association, and reports a collision.
|
// ClearSubject removes an existing subject association, and reports a collision.
|
||||||
func (m *Marker) ClearSubject(src string) error {
|
func (m *Marker) ClearSubject(src string) error {
|
||||||
|
// Find the matching face.
|
||||||
if m.face == nil {
|
if m.face == nil {
|
||||||
m.face = FindFace(m.FaceID)
|
m.face = FindFace(m.FaceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.face == nil {
|
// Update index & resolve collisions.
|
||||||
// Do nothing
|
if err := m.Updates(Values{"MarkerName": "", "FaceID": "", "FaceDist": -1.0, "SubjectUID": "", "SubjectSrc": src}); err != nil {
|
||||||
} else if resolved, err := m.face.ResolveCollision(m.Embeddings()); err != nil {
|
|
||||||
return err
|
return err
|
||||||
} else if err := m.Updates(Values{"MarkerName": "", "FaceID": "", "FaceDist": -1.0, "SubjectUID": "", "SubjectSrc": src}); err != nil {
|
} else if m.face == nil {
|
||||||
|
m.subject = nil
|
||||||
|
return nil
|
||||||
|
} else if resolved, err := m.face.ResolveCollision(m.Embeddings()); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if resolved {
|
} else if resolved {
|
||||||
log.Debugf("faces: resolved collision with %s", m.face.ID)
|
log.Debugf("faces: resolved collision with %s", m.face.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear references.
|
||||||
m.face = nil
|
m.face = nil
|
||||||
|
m.subject = nil
|
||||||
m.MarkerName = ""
|
|
||||||
m.FaceID = ""
|
|
||||||
m.FaceDist = -1.0
|
|
||||||
m.SubjectUID = ""
|
|
||||||
m.SubjectSrc = src
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -495,7 +493,7 @@ func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
|
|||||||
err := result.Updates(map[string]interface{}{
|
err := result.Updates(map[string]interface{}{
|
||||||
"MarkerType": m.MarkerType,
|
"MarkerType": m.MarkerType,
|
||||||
"MarkerSrc": m.MarkerSrc,
|
"MarkerSrc": m.MarkerSrc,
|
||||||
"CropID": m.CropID,
|
"FileArea": m.FileArea,
|
||||||
"X": m.X,
|
"X": m.X,
|
||||||
"Y": m.Y,
|
"Y": m.Y,
|
||||||
"W": m.W,
|
"W": m.W,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
|
import "github.com/photoprism/photoprism/pkg/crop"
|
||||||
|
|
||||||
|
// UnknownMarker can be used as a default for unknown markers.
|
||||||
|
var UnknownMarker = NewMarker(File{}, crop.Area{}, "", SrcDefault, MarkerUnknown)
|
||||||
|
|
||||||
type MarkerMap map[string]Marker
|
type MarkerMap map[string]Marker
|
||||||
|
|
||||||
func (m MarkerMap) Get(name string) Marker {
|
func (m MarkerMap) Get(name string) Marker {
|
||||||
@@ -194,7 +199,7 @@ var MarkerFixtures = MarkerMap{
|
|||||||
MarkerUID: "mt9k3pw1wowuy999",
|
MarkerUID: "mt9k3pw1wowuy999",
|
||||||
FileUID: "ft2es49qhhinlple",
|
FileUID: "ft2es49qhhinlple",
|
||||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||||
CropID: "045038063041",
|
FileArea: "045038063041",
|
||||||
FaceDist: 0.26852392873736236,
|
FaceDist: 0.26852392873736236,
|
||||||
SubjectSrc: SrcManual,
|
SubjectSrc: SrcManual,
|
||||||
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
||||||
@@ -214,7 +219,7 @@ var MarkerFixtures = MarkerMap{
|
|||||||
MarkerUID: "mt9k3pw1wowu1000",
|
MarkerUID: "mt9k3pw1wowu1000",
|
||||||
FileUID: "ft2es49whhbnlqdn",
|
FileUID: "ft2es49whhbnlqdn",
|
||||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||||
CropID: "046045043065",
|
FileArea: "046045043065",
|
||||||
FaceDist: 0.4507357278575355,
|
FaceDist: 0.4507357278575355,
|
||||||
SubjectSrc: "",
|
SubjectSrc: "",
|
||||||
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
||||||
@@ -234,7 +239,7 @@ var MarkerFixtures = MarkerMap{
|
|||||||
MarkerUID: "mt9k3pw1wowu1001",
|
MarkerUID: "mt9k3pw1wowu1001",
|
||||||
FileUID: "ft8es39w45bnlqdw",
|
FileUID: "ft8es39w45bnlqdw",
|
||||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||||
CropID: "05403304060446",
|
FileArea: "05403304060446",
|
||||||
FaceDist: 0.5099754448545762,
|
FaceDist: 0.5099754448545762,
|
||||||
SubjectSrc: "",
|
SubjectSrc: "",
|
||||||
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
||||||
|
|||||||
@@ -19,40 +19,42 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
|
|||||||
return json.Marshal(&struct {
|
return json.Marshal(&struct {
|
||||||
UID string
|
UID string
|
||||||
FileUID string
|
FileUID string
|
||||||
|
FileHash string
|
||||||
|
FileArea string
|
||||||
Type string
|
Type string
|
||||||
Src string
|
Src string
|
||||||
Name string
|
Name string
|
||||||
Invalid bool `json:",omitempty"`
|
Invalid bool
|
||||||
X float32 `json:",omitempty"`
|
Review bool
|
||||||
Y float32 `json:",omitempty"`
|
FaceID string
|
||||||
|
SubjectUID string
|
||||||
|
SubjectSrc string
|
||||||
|
X float32
|
||||||
|
Y float32
|
||||||
W float32 `json:",omitempty"`
|
W float32 `json:",omitempty"`
|
||||||
H float32 `json:",omitempty"`
|
H float32 `json:",omitempty"`
|
||||||
Size int `json:",omitempty"`
|
Size int `json:",omitempty"`
|
||||||
Score int `json:",omitempty"`
|
Score int `json:",omitempty"`
|
||||||
Review bool `json:",omitempty"`
|
|
||||||
CropID string `json:",omitempty"`
|
|
||||||
FaceID string `json:",omitempty"`
|
|
||||||
SubjectUID string `json:",omitempty"`
|
|
||||||
SubjectSrc string `json:",omitempty"`
|
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}{
|
}{
|
||||||
UID: m.MarkerUID,
|
UID: m.MarkerUID,
|
||||||
FileUID: m.FileUID,
|
FileUID: m.FileUID,
|
||||||
|
FileHash: m.FileHash,
|
||||||
|
FileArea: m.FileArea,
|
||||||
Type: m.MarkerType,
|
Type: m.MarkerType,
|
||||||
Src: m.MarkerSrc,
|
Src: m.MarkerSrc,
|
||||||
Name: name,
|
Name: name,
|
||||||
Invalid: m.MarkerInvalid,
|
Invalid: m.MarkerInvalid,
|
||||||
|
Review: m.Review,
|
||||||
|
FaceID: m.FaceID,
|
||||||
|
SubjectUID: m.SubjectUID,
|
||||||
|
SubjectSrc: m.SubjectSrc,
|
||||||
X: m.X,
|
X: m.X,
|
||||||
Y: m.Y,
|
Y: m.Y,
|
||||||
W: m.W,
|
W: m.W,
|
||||||
H: m.H,
|
H: m.H,
|
||||||
Size: m.Size,
|
Size: m.Size,
|
||||||
Score: m.Score,
|
Score: m.Score,
|
||||||
Review: m.Review,
|
|
||||||
CropID: m.CropID,
|
|
||||||
FaceID: m.FaceID,
|
|
||||||
SubjectUID: m.SubjectUID,
|
|
||||||
SubjectSrc: m.SubjectSrc,
|
|
||||||
CreatedAt: m.CreatedAt,
|
CreatedAt: m.CreatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,27 @@ package entity
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/crop"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var testArea = crop.Area{
|
||||||
|
Name: "face",
|
||||||
|
X: 0.308333,
|
||||||
|
Y: 0.206944,
|
||||||
|
W: 0.355556,
|
||||||
|
H: 0.355556,
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarker_TableName(t *testing.T) {
|
func TestMarker_TableName(t *testing.T) {
|
||||||
m := &Marker{}
|
m := &Marker{}
|
||||||
assert.Contains(t, m.TableName(), "markers")
|
assert.Contains(t, m.TableName(), "markers")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewMarker(t *testing.T) {
|
func TestNewMarker(t *testing.T) {
|
||||||
m := NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel)
|
||||||
assert.IsType(t, &Marker{}, m)
|
assert.IsType(t, &Marker{}, m)
|
||||||
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
||||||
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjectUID)
|
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjectUID)
|
||||||
@@ -79,7 +89,7 @@ func TestMarker_SaveForm(t *testing.T) {
|
|||||||
|
|
||||||
func TestUpdateOrCreateMarker(t *testing.T) {
|
func TestUpdateOrCreateMarker(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel)
|
||||||
assert.IsType(t, &Marker{}, m)
|
assert.IsType(t, &Marker{}, m)
|
||||||
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
||||||
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjectUID)
|
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjectUID)
|
||||||
@@ -104,7 +114,7 @@ func TestUpdateOrCreateMarker(t *testing.T) {
|
|||||||
|
|
||||||
func TestMarker_Updates(t *testing.T) {
|
func TestMarker_Updates(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel)
|
||||||
m, err := UpdateOrCreateMarker(m)
|
m, err := UpdateOrCreateMarker(m)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -129,7 +139,7 @@ func TestMarker_Updates(t *testing.T) {
|
|||||||
|
|
||||||
func TestMarker_Update(t *testing.T) {
|
func TestMarker_Update(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel)
|
||||||
m, err := UpdateOrCreateMarker(m)
|
m, err := UpdateOrCreateMarker(m)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -153,7 +163,8 @@ func TestMarker_Update(t *testing.T) {
|
|||||||
|
|
||||||
func TestMarker_Save(t *testing.T) {
|
func TestMarker_Save(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel)
|
||||||
|
|
||||||
m, err := UpdateOrCreateMarker(m)
|
m, err := UpdateOrCreateMarker(m)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,13 +3,20 @@ package entity
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/crop"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cropArea1 = crop.Area{Name: "face", X: 0.308333, Y: 0.206944, W: 0.355556, H: 0.355556}
|
||||||
|
var cropArea2 = crop.Area{Name: "face", X: 0.308313, Y: 0.206914, W: 0.655556, H: 0.655556}
|
||||||
|
var cropArea3 = crop.Area{Name: "face", X: 0.998133, Y: 0.816944, W: 0.0001, H: 0.0001}
|
||||||
|
var cropArea4 = crop.Area{Name: "face", X: 0.298133, Y: 0.216944, W: 0.255556, H: 0.155556}
|
||||||
|
|
||||||
func TestMarkers_Contains(t *testing.T) {
|
func TestMarkers_Contains(t *testing.T) {
|
||||||
m1 := *NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy1c1", SrcImage, MarkerFace, 0.308333, 0.206944, 0.355556, 0.355556)
|
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace)
|
||||||
m2 := *NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy1c2", SrcImage, MarkerFace, 0.308313, 0.206914, 0.655556, 0.655556)
|
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea2, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace)
|
||||||
m3 := *NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy1c3", SrcImage, MarkerFace, 0.998133, 0.816944, 0.0001, 0.0001)
|
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace)
|
||||||
|
|
||||||
m := Markers{m1}
|
m := Markers{m1}
|
||||||
|
|
||||||
@@ -18,9 +25,9 @@ func TestMarkers_Contains(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMarkers_FaceCount(t *testing.T) {
|
func TestMarkers_FaceCount(t *testing.T) {
|
||||||
m1 := *NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy1c1", SrcImage, MarkerFace, 0.308333, 0.206944, 0.355556, 0.355556)
|
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace)
|
||||||
m2 := *NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy1c2", SrcImage, MarkerFace, 0.298133, 0.216944, 0.255556, 0.155556)
|
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea4, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace)
|
||||||
m3 := *NewMarker("ft8es39w45bnlqdw", "lt9k3pw1wowuy1c3", SrcImage, MarkerFace, 0.998133, 0.816944, 0.0001, 0.0001)
|
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace)
|
||||||
m3.MarkerInvalid = true
|
m3.MarkerInvalid = true
|
||||||
|
|
||||||
m := Markers{m1, m2, m3}
|
m := Markers{m1, m2, m3}
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
SubjectPerson = "person"
|
|
||||||
)
|
|
||||||
|
|
||||||
var subjectMutex = sync.Mutex{}
|
var subjectMutex = sync.Mutex{}
|
||||||
|
|
||||||
// Subjects represents a list of subjects.
|
// Subjects represents a list of subjects.
|
||||||
@@ -99,8 +95,21 @@ func (m *Subject) Create() error {
|
|||||||
return Db().Create(m).Error
|
return Db().Create(m).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the entity from the database.
|
// Delete marks the entity as deleted in the database.
|
||||||
func (m *Subject) Delete() error {
|
func (m *Subject) Delete() error {
|
||||||
|
if m.Deleted() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("subject: deleting %s %s", m.SubjectType, txt.Quote(m.SubjectName))
|
||||||
|
|
||||||
|
if m.IsPerson() {
|
||||||
|
event.EntitiesDeleted("people", []string{m.SubjectUID})
|
||||||
|
event.Publish("count.people", event.Data{
|
||||||
|
"count": -1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return Db().Delete(m).Error
|
return Db().Delete(m).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +122,17 @@ func (m *Subject) Deleted() bool {
|
|||||||
func (m *Subject) Restore() error {
|
func (m *Subject) Restore() error {
|
||||||
if m.Deleted() {
|
if m.Deleted() {
|
||||||
m.DeletedAt = nil
|
m.DeletedAt = nil
|
||||||
|
|
||||||
|
log.Infof("subject: restoring %s %s", m.SubjectType, txt.Quote(m.SubjectName))
|
||||||
|
|
||||||
|
if m.IsPerson() {
|
||||||
|
event.EntitiesCreated("people", []*Person{m.Person()})
|
||||||
|
|
||||||
|
event.Publish("count.people", event.Data{
|
||||||
|
"count": 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return UnscopedDb().Model(m).Update("DeletedAt", nil).Error
|
return UnscopedDb().Model(m).Update("DeletedAt", nil).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,9 +160,10 @@ func FirstOrCreateSubject(m *Subject) *Subject {
|
|||||||
if found := FindSubjectByName(m.SubjectName); found != nil {
|
if found := FindSubjectByName(m.SubjectName); found != nil {
|
||||||
return found
|
return found
|
||||||
} else if createErr := m.Create(); createErr == nil {
|
} else if createErr := m.Create(); createErr == nil {
|
||||||
if !m.Excluded && m.SubjectType == SubjectPerson {
|
log.Infof("subject: added %s %s", m.SubjectType, txt.Quote(m.SubjectName))
|
||||||
event.EntitiesCreated("people", []*Subject{m})
|
|
||||||
|
|
||||||
|
if m.IsPerson() {
|
||||||
|
event.EntitiesCreated("people", []*Person{m.Person()})
|
||||||
event.Publish("count.people", event.Data{
|
event.Publish("count.people", event.Data{
|
||||||
"count": 1,
|
"count": 1,
|
||||||
})
|
})
|
||||||
@@ -200,6 +221,16 @@ func FindSubjectByName(s string) *Subject {
|
|||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsPerson tests if the subject is a person.
|
||||||
|
func (m *Subject) IsPerson() bool {
|
||||||
|
return m.SubjectType == SubjectPerson
|
||||||
|
}
|
||||||
|
|
||||||
|
// Person creates and returns a Person based on this subject.
|
||||||
|
func (m *Subject) Person() *Person {
|
||||||
|
return NewPerson(*m)
|
||||||
|
}
|
||||||
|
|
||||||
// SetName changes the subject's name.
|
// SetName changes the subject's name.
|
||||||
func (m *Subject) SetName(name string) error {
|
func (m *Subject) SetName(name string) error {
|
||||||
newName := txt.Clip(name, txt.ClipDefault)
|
newName := txt.Clip(name, txt.ClipDefault)
|
||||||
@@ -219,6 +250,12 @@ func (m *Subject) UpdateName(name string) (*Subject, error) {
|
|||||||
if err := m.SetName(name); err != nil {
|
if err := m.SetName(name); err != nil {
|
||||||
return m, err
|
return m, err
|
||||||
} else if err := m.Updates(Values{"SubjectName": m.SubjectName, "SubjectSlug": m.SubjectSlug}); err == nil {
|
} else if err := m.Updates(Values{"SubjectName": m.SubjectName, "SubjectSlug": m.SubjectSlug}); err == nil {
|
||||||
|
log.Infof("subject: renamed %s %s", m.SubjectType, txt.Quote(m.SubjectName))
|
||||||
|
|
||||||
|
if m.IsPerson() {
|
||||||
|
event.EntitiesUpdated("people", []*Person{m.Person()})
|
||||||
|
}
|
||||||
|
|
||||||
return m, m.UpdateMarkerNames()
|
return m, m.UpdateMarkerNames()
|
||||||
} else if existing := FindSubjectByName(m.SubjectName); existing == nil {
|
} else if existing := FindSubjectByName(m.SubjectName); existing == nil {
|
||||||
return m, err
|
return m, err
|
||||||
|
|||||||
53
internal/entity/subject_person.go
Normal file
53
internal/entity/subject_person.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SubjectPerson = "person"
|
||||||
|
)
|
||||||
|
|
||||||
|
// People represents a list of people.
|
||||||
|
type People []Person
|
||||||
|
|
||||||
|
// Person represents a subject with type person.
|
||||||
|
type Person struct {
|
||||||
|
SubjectUID string `json:"UID"`
|
||||||
|
SubjectName string `json:"Name"`
|
||||||
|
SubjectAlias string `json:"Alias,omitempty"`
|
||||||
|
Favorite bool `json:"Favorite,omitempty"`
|
||||||
|
Thumb string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPerson returns a new entity.
|
||||||
|
func NewPerson(subj Subject) *Person {
|
||||||
|
result := &Person{
|
||||||
|
SubjectUID: subj.SubjectUID,
|
||||||
|
SubjectName: subj.SubjectName,
|
||||||
|
SubjectAlias: subj.SubjectAlias,
|
||||||
|
Favorite: subj.Favorite,
|
||||||
|
Thumb: subj.Thumb,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON returns the JSON encoding.
|
||||||
|
func (m *Person) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(&struct {
|
||||||
|
UID string
|
||||||
|
Name string
|
||||||
|
Keywords []string `json:",omitempty"`
|
||||||
|
Favorite bool `json:",omitempty"`
|
||||||
|
Thumb string `json:",omitempty"`
|
||||||
|
}{
|
||||||
|
UID: m.SubjectUID,
|
||||||
|
Name: m.SubjectName,
|
||||||
|
Keywords: txt.NameKeywords(m.SubjectName, m.SubjectAlias),
|
||||||
|
Favorite: m.Favorite,
|
||||||
|
Thumb: m.Thumb,
|
||||||
|
})
|
||||||
|
}
|
||||||
40
internal/entity/subject_person_test.go
Normal file
40
internal/entity/subject_person_test.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPerson(t *testing.T) {
|
||||||
|
t.Run("BillGates", func(t *testing.T) {
|
||||||
|
subj := Subject{
|
||||||
|
SubjectUID: "jqytw12v8jjeu3e6",
|
||||||
|
SubjectName: "William Henry Gates III",
|
||||||
|
SubjectAlias: "Windows Guru",
|
||||||
|
Favorite: true,
|
||||||
|
Thumb: "622c7287967f2800e873fbc55f0328973056ce1d",
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewPerson(subj)
|
||||||
|
|
||||||
|
assert.Equal(t, "jqytw12v8jjeu3e6", m.SubjectUID)
|
||||||
|
assert.Equal(t, "William Henry Gates III", m.SubjectName)
|
||||||
|
assert.Equal(t, "Windows Guru", m.SubjectAlias)
|
||||||
|
assert.Equal(t, true, m.Favorite)
|
||||||
|
assert.Equal(t, "622c7287967f2800e873fbc55f0328973056ce1d", m.Thumb)
|
||||||
|
|
||||||
|
if j, err := m.MarshalJSON(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
s := string(j)
|
||||||
|
|
||||||
|
expected := "{\"UID\":\"jqytw12v8jjeu3e6\",\"Name\":\"William Henry Gates III\"," +
|
||||||
|
"\"Keywords\":[\"william\",\"henry\",\"gates\",\"iii\",\"windows\",\"guru\"]," +
|
||||||
|
"\"Favorite\":true,\"Thumb\":\"622c7287967f2800e873fbc55f0328973056ce1d\"}"
|
||||||
|
|
||||||
|
assert.Equal(t, expected, s)
|
||||||
|
t.Logf("person json: %s", s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ func TestSubject_TableName(t *testing.T) {
|
|||||||
assert.Contains(t, m.TableName(), "subjects")
|
assert.Contains(t, m.TableName(), "subjects")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewPerson(t *testing.T) {
|
func TestNewSubject(t *testing.T) {
|
||||||
t.Run("Jens_Mander", func(t *testing.T) {
|
t.Run("Jens_Mander", func(t *testing.T) {
|
||||||
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
||||||
assert.Equal(t, "Jens Mander", m.SubjectName)
|
assert.Equal(t, "Jens Mander", m.SubjectName)
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ func (f *Face) Dim() float32 {
|
|||||||
return float32(1)
|
return float32(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crop returns the relative image area for cropping.
|
// CropArea returns the relative image area for cropping.
|
||||||
func (f *Face) Crop() crop.Area {
|
func (f *Face) CropArea() crop.Area {
|
||||||
if f.Rows < 1 {
|
if f.Rows < 1 {
|
||||||
f.Cols = 1
|
f.Cols = 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func TestDetect(t *testing.T) {
|
|||||||
t.Logf("results: %#v", faces)
|
t.Logf("results: %#v", faces)
|
||||||
|
|
||||||
for i, f := range faces {
|
for i, f := range faces {
|
||||||
t.Logf("marker[%d]: %#v %#v", i, f.Crop(), f.Area)
|
t.Logf("marker[%d]: %#v %#v", i, f.CropArea(), f.Area)
|
||||||
t.Logf("landmarks[%d]: %s", i, f.RelativeLandmarksJSON())
|
t.Logf("landmarks[%d]: %s", i, f.RelativeLandmarksJSON())
|
||||||
|
|
||||||
img, err := tfInstance.getFaceCrop(fileName, fileHash, &faces[i])
|
img, err := tfInstance.getFaceCrop(fileName, fileHash, &faces[i])
|
||||||
@@ -249,7 +249,7 @@ func TestFace_EmbeddingsJSON(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFace_Crop(t *testing.T) {
|
func TestFace_CropArea(t *testing.T) {
|
||||||
t.Run("Position", func(t *testing.T) {
|
t.Run("Position", func(t *testing.T) {
|
||||||
f := Face{
|
f := Face{
|
||||||
Cols: 1000,
|
Cols: 1000,
|
||||||
@@ -265,6 +265,6 @@ func TestFace_Crop(t *testing.T) {
|
|||||||
Landmarks: nil,
|
Landmarks: nil,
|
||||||
Embeddings: nil,
|
Embeddings: nil,
|
||||||
}
|
}
|
||||||
t.Logf("marker: %#v", f.Crop())
|
t.Logf("marker: %#v", f.CropArea())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ func (t *Net) getFaceCrop(fileName, cacheHash string, f *Face) (img image.Image,
|
|||||||
cacheFolder := t.getCacheFolder(fileName, cacheHash)
|
cacheFolder := t.getCacheFolder(fileName, cacheHash)
|
||||||
|
|
||||||
if cacheHash != "" {
|
if cacheHash != "" {
|
||||||
f.Thumb = fmt.Sprintf("%s_%dx%d_crop_%s", cacheHash, CropSize, CropSize, f.Crop().ID())
|
f.Thumb = fmt.Sprintf("%s_%dx%d_crop_%s", cacheHash, CropSize, CropSize, f.CropArea().String())
|
||||||
} else {
|
} else {
|
||||||
base := filepath.Base(fileName)
|
base := filepath.Base(fileName)
|
||||||
i := strings.Index(base, "_")
|
i := strings.Index(base, "_")
|
||||||
@@ -140,7 +140,7 @@ func (t *Net) getFaceCrop(fileName, cacheHash string, f *Face) (img image.Image,
|
|||||||
base = base[:i]
|
base = base[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Thumb = fmt.Sprintf("%s_%dx%d_crop_%s", base, CropSize, CropSize, f.Crop().ID())
|
f.Thumb = fmt.Sprintf("%s_%dx%d_crop_%s", base, CropSize, CropSize, f.CropArea().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheFile := filepath.Join(cacheFolder, f.Thumb+fs.JpegExt)
|
cacheFile := filepath.Join(cacheFolder, f.Thumb+fs.JpegExt)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
// Faces returns all (known / unmatched) faces from the index.
|
// Faces returns all (known / unmatched) faces from the index.
|
||||||
func Faces(knownOnly, unmatched bool) (result entity.Faces, err error) {
|
func Faces(knownOnly, unmatched bool) (result entity.Faces, err error) {
|
||||||
stmt := Db().Where("face_src <> ?", entity.SrcDefault)
|
stmt := Db()
|
||||||
|
|
||||||
if unmatched {
|
if unmatched {
|
||||||
stmt = stmt.Where("matched_at IS NULL")
|
stmt = stmt.Where("matched_at IS NULL")
|
||||||
@@ -72,7 +72,7 @@ func RemoveAnonymousFaceClusters() (removed int64, err error) {
|
|||||||
// RemoveAutoFaceClusters removes automatically added face clusters from the index.
|
// RemoveAutoFaceClusters removes automatically added face clusters from the index.
|
||||||
func RemoveAutoFaceClusters() (removed int64, err error) {
|
func RemoveAutoFaceClusters() (removed int64, err error) {
|
||||||
res := UnscopedDb().
|
res := UnscopedDb().
|
||||||
Delete(entity.Face{}, "id <> ? AND face_src = ?", entity.UnknownFace.ID, entity.SrcAuto)
|
Delete(entity.Face{}, "face_src = ?", entity.SrcAuto)
|
||||||
|
|
||||||
return res.RowsAffected, res.Error
|
return res.RowsAffected, res.Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
func UpdateAlbumDefaultPreviews() error {
|
func UpdateAlbumDefaultPreviews() error {
|
||||||
return Db().Table(entity.Album{}.TableName()).
|
return Db().Table(entity.Album{}.TableName()).
|
||||||
UpdateColumn("thumb", gorm.Expr(`(
|
UpdateColumn("thumb", gorm.Expr(`(
|
||||||
SELECT file_hash FROM files f
|
SELECT f.file_hash FROM files f
|
||||||
JOIN photos_albums pa ON pa.album_uid = albums.album_uid AND pa.photo_uid = f.photo_uid AND pa.hidden = 0
|
JOIN photos_albums pa ON pa.album_uid = albums.album_uid AND pa.photo_uid = f.photo_uid AND pa.hidden = 0
|
||||||
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
||||||
WHERE f.deleted_at IS NULL AND f.file_missing = 0 AND f.file_hash <> '' AND f.file_primary = 1 AND f.file_type = 'jpg'
|
WHERE f.deleted_at IS NULL AND f.file_missing = 0 AND f.file_hash <> '' AND f.file_primary = 1 AND f.file_type = 'jpg'
|
||||||
@@ -23,7 +23,7 @@ func UpdateAlbumDefaultPreviews() error {
|
|||||||
func UpdateAlbumFolderPreviews() error {
|
func UpdateAlbumFolderPreviews() error {
|
||||||
return Db().Table(entity.Album{}.TableName()).
|
return Db().Table(entity.Album{}.TableName()).
|
||||||
UpdateColumn("thumb", gorm.Expr(`(
|
UpdateColumn("thumb", gorm.Expr(`(
|
||||||
SELECT file_hash FROM files f
|
SELECT f.file_hash FROM files f
|
||||||
JOIN photos p ON p.id = f.photo_id AND p.photo_path = albums.album_path AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
JOIN photos p ON p.id = f.photo_id AND p.photo_path = albums.album_path AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
||||||
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
|
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
|
||||||
ORDER BY p.taken_at DESC LIMIT 1
|
ORDER BY p.taken_at DESC LIMIT 1
|
||||||
@@ -35,7 +35,7 @@ func UpdateAlbumFolderPreviews() error {
|
|||||||
func UpdateAlbumMonthPreviews() error {
|
func UpdateAlbumMonthPreviews() error {
|
||||||
return Db().Table(entity.Album{}.TableName()).
|
return Db().Table(entity.Album{}.TableName()).
|
||||||
UpdateColumn("thumb", gorm.Expr(`(
|
UpdateColumn("thumb", gorm.Expr(`(
|
||||||
SELECT file_hash FROM files f
|
SELECT f.file_hash FROM files f
|
||||||
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
||||||
AND p.photo_year = albums.album_year AND p.photo_month = albums.album_month AND p.photo_month = albums.album_month
|
AND p.photo_year = albums.album_year AND p.photo_month = albums.album_month AND p.photo_month = albums.album_month
|
||||||
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
|
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
|
||||||
@@ -69,7 +69,7 @@ func UpdateLabelPreviews() (err error) {
|
|||||||
// Labels.
|
// Labels.
|
||||||
if err = Db().Table(entity.Label{}.TableName()).
|
if err = Db().Table(entity.Label{}.TableName()).
|
||||||
UpdateColumn("thumb", gorm.Expr(`(
|
UpdateColumn("thumb", gorm.Expr(`(
|
||||||
SELECT file_hash FROM files f
|
SELECT f.file_hash FROM files f
|
||||||
JOIN photos_labels pl ON pl.label_id = labels.id AND pl.photo_id = f.photo_id AND pl.uncertainty < 100
|
JOIN photos_labels pl ON pl.label_id = labels.id AND pl.photo_id = f.photo_id AND pl.uncertainty < 100
|
||||||
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
||||||
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
|
WHERE f.deleted_at IS NULL AND f.file_hash <> '' AND f.file_missing = 0 AND f.file_primary = 1 AND f.file_type = 'jpg'
|
||||||
@@ -82,7 +82,7 @@ func UpdateLabelPreviews() (err error) {
|
|||||||
// Categories.
|
// Categories.
|
||||||
if err = Db().Table(entity.Label{}.TableName()).
|
if err = Db().Table(entity.Label{}.TableName()).
|
||||||
UpdateColumn("thumb", gorm.Expr(`(
|
UpdateColumn("thumb", gorm.Expr(`(
|
||||||
SELECT file_hash FROM files f
|
SELECT f.file_hash FROM files f
|
||||||
JOIN photos_labels pl ON pl.photo_id = f.photo_id AND pl.uncertainty < 100
|
JOIN photos_labels pl ON pl.photo_id = f.photo_id AND pl.uncertainty < 100
|
||||||
JOIN categories c ON c.label_id = pl.label_id AND c.category_id = labels.id
|
JOIN categories c ON c.label_id = pl.label_id AND c.category_id = labels.id
|
||||||
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
JOIN photos p ON p.id = f.photo_id AND p.photo_private = 0 AND p.deleted_at IS NULL AND p.photo_quality > -1
|
||||||
@@ -98,8 +98,10 @@ func UpdateLabelPreviews() (err error) {
|
|||||||
|
|
||||||
// UpdateSubjectPreviews updates subject preview images.
|
// UpdateSubjectPreviews updates subject preview images.
|
||||||
func UpdateSubjectPreviews() error {
|
func UpdateSubjectPreviews() error {
|
||||||
|
/* Previous implementation for reference:
|
||||||
|
|
||||||
return Db().Table(entity.Subject{}.TableName()).
|
return Db().Table(entity.Subject{}.TableName()).
|
||||||
UpdateColumn("thumb", gorm.Expr("(SELECT file_hash FROM files f "+
|
UpdateColumn("thumb", gorm.Expr("(SELECT f.file_hash FROM files f "+
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"JOIN %s m ON f.file_uid = m.file_uid AND m.subject_uid = %s.subject_uid",
|
"JOIN %s m ON f.file_uid = m.file_uid AND m.subject_uid = %s.subject_uid",
|
||||||
entity.Marker{}.TableName(),
|
entity.Marker{}.TableName(),
|
||||||
@@ -108,7 +110,17 @@ func UpdateSubjectPreviews() error {
|
|||||||
WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL AND f.file_hash <> '' AND p.deleted_at IS NULL
|
WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL AND f.file_hash <> '' AND p.deleted_at IS NULL
|
||||||
AND f.file_primary = 1 AND f.file_missing = 0 AND p.photo_private = 0 AND p.photo_quality > -1
|
AND f.file_primary = 1 AND f.file_missing = 0 AND p.photo_private = 0 AND p.photo_quality > -1
|
||||||
ORDER BY p.taken_at DESC LIMIT 1)
|
ORDER BY p.taken_at DESC LIMIT 1)
|
||||||
WHERE thumb_src='' AND deleted_at IS NULL AND subject_src <> 'default'`)).
|
WHERE thumb_src='' AND deleted_at IS NULL`)).
|
||||||
|
Error */
|
||||||
|
|
||||||
|
return Db().Table(entity.Subject{}.TableName()).
|
||||||
|
UpdateColumn("thumb", gorm.Expr("(SELECT m.file_hash FROM "+
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s m WHERE m.subject_uid = %s.subject_uid AND m.subject_src = 'manual' ",
|
||||||
|
entity.Marker{}.TableName(),
|
||||||
|
entity.Subject{}.TableName())+
|
||||||
|
` AND m.file_hash <> '' AND ORDER BY m.size DESC LIMIT 1)
|
||||||
|
WHERE thumb_src='' AND deleted_at IS NULL`)).
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,30 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// People returns the sorted names of the first 2000 people.
|
||||||
|
func People() (people entity.People, err error) {
|
||||||
|
err = UnscopedDb().
|
||||||
|
Table(entity.Subject{}.TableName()).
|
||||||
|
Select("subject_uid, subject_name, subject_alias, favorite").
|
||||||
|
Where("deleted_at IS NULL AND subject_type = ?", entity.SubjectPerson).
|
||||||
|
Order("subject_name").
|
||||||
|
Limit(2000).Offset(0).
|
||||||
|
Scan(&people).Error
|
||||||
|
|
||||||
|
return people, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeopleCount returns the total number of people in the index.
|
||||||
|
func PeopleCount() (count int, err error) {
|
||||||
|
err = Db().
|
||||||
|
Table(entity.Subject{}.TableName()).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Where("subject_type = ?", entity.SubjectPerson).
|
||||||
|
Count(&count).Error
|
||||||
|
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
// Subjects returns subjects from the index.
|
// Subjects returns subjects from the index.
|
||||||
func Subjects(limit, offset int) (result entity.Subjects, err error) {
|
func Subjects(limit, offset int) (result entity.Subjects, err error) {
|
||||||
stmt := Db()
|
stmt := Db()
|
||||||
@@ -26,7 +50,7 @@ func SubjectMap() (result map[string]entity.Subject, err error) {
|
|||||||
|
|
||||||
var subj entity.Subjects
|
var subj entity.Subjects
|
||||||
|
|
||||||
stmt := Db().Where("subject_src <> ?", entity.SrcDefault)
|
stmt := Db()
|
||||||
|
|
||||||
if err = stmt.Find(&subj).Error; err != nil {
|
if err = stmt.Find(&subj).Error; err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
@@ -111,7 +135,6 @@ func SearchSubjectUIDs(s string) (result []string, names []string, remaining str
|
|||||||
var matches []Matches
|
var matches []Matches
|
||||||
|
|
||||||
stmt := Db().Model(entity.Subject{})
|
stmt := Db().Model(entity.Subject{})
|
||||||
stmt = stmt.Where("subject_src <> ?", entity.SrcDefault)
|
|
||||||
|
|
||||||
if where := LikeAllNames(Cols{"subject_name", "subject_alias"}, s); len(where) == 0 {
|
if where := LikeAllNames(Cols{"subject_name", "subject_alias"}, s); len(where) == 0 {
|
||||||
return result, names, s
|
return result, names, s
|
||||||
|
|||||||
@@ -9,6 +9,24 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestPeople(t *testing.T) {
|
||||||
|
if results, err := People(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
assert.LessOrEqual(t, 3, len(results))
|
||||||
|
t.Logf("people: %#v", results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPeopleCount(t *testing.T) {
|
||||||
|
if result, err := PeopleCount(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
assert.LessOrEqual(t, 3, result)
|
||||||
|
t.Logf("there are %d people", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSubjects(t *testing.T) {
|
func TestSubjects(t *testing.T) {
|
||||||
results, err := Subjects(3, 0)
|
results, err := Subjects(3, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ type Area struct {
|
|||||||
H float32 `json:"h,omitempty"`
|
H float32 `json:"h,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns a string identifying the approximate marker area.
|
// String returns a string identifying the approximate marker area.
|
||||||
func (m Area) ID() string {
|
func (m Area) String() string {
|
||||||
return fmt.Sprintf("%03d%03d%03d%03d", int(m.X*100), int(m.Y*100), int(m.W*100), int(m.H*100))
|
return fmt.Sprintf("%03d%03d%03d%03d", int(m.X*100), int(m.Y*100), int(m.W*100), int(m.H*100))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,25 +6,25 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestArea_ID(t *testing.T) {
|
func TestArea_String(t *testing.T) {
|
||||||
t.Run("082016010006_face", func(t *testing.T) {
|
t.Run("082016010006_face", func(t *testing.T) {
|
||||||
m := NewArea("face", 0.822059, 0.167969, 0.1, 0.0664062)
|
m := NewArea("face", 0.822059, 0.167969, 0.1, 0.0664062)
|
||||||
assert.Equal(t, "082016010006", m.ID())
|
assert.Equal(t, "082016010006", m.String())
|
||||||
})
|
})
|
||||||
t.Run("082016010006_back", func(t *testing.T) {
|
t.Run("082016010006_back", func(t *testing.T) {
|
||||||
m := NewArea("back", 0.822059, 0.167969, 0.1, 0.0664062)
|
m := NewArea("back", 0.822059, 0.167969, 0.1, 0.0664062)
|
||||||
assert.Equal(t, "082016010006", m.ID())
|
assert.Equal(t, "082016010006", m.String())
|
||||||
})
|
})
|
||||||
t.Run("020100003000", func(t *testing.T) {
|
t.Run("020100003000", func(t *testing.T) {
|
||||||
m := NewArea("face", 0.201, 1.000, 0.03, 0.00000001)
|
m := NewArea("face", 0.201, 1.000, 0.03, 0.00000001)
|
||||||
assert.Equal(t, "020100003000", m.ID())
|
assert.Equal(t, "020100003000", m.String())
|
||||||
})
|
})
|
||||||
t.Run("000100000000", func(t *testing.T) {
|
t.Run("000100000000", func(t *testing.T) {
|
||||||
m := NewArea("face", 0.0001, 1.000, 0, 0.00000001)
|
m := NewArea("face", 0.0001, 1.000, 0, 0.00000001)
|
||||||
assert.Equal(t, "000100000000", m.ID())
|
assert.Equal(t, "000100000000", m.String())
|
||||||
})
|
})
|
||||||
t.Run("000012000100", func(t *testing.T) {
|
t.Run("000012000100", func(t *testing.T) {
|
||||||
m := NewArea("", -2.0001, 0.123, -0.1, 4.00000001)
|
m := NewArea("", -2.0001, 0.123, -0.1, 4.00000001)
|
||||||
assert.Equal(t, "000012000100", m.ID())
|
assert.Equal(t, "000012000100", m.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,3 +35,15 @@ func JoinNames(names []string) string {
|
|||||||
return fmt.Sprintf("%s & %s", strings.Join(names[:l-1], ", "), names[l-1])
|
return fmt.Sprintf("%s & %s", strings.Join(names[:l-1], ", "), names[l-1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NameKeywords returns a list of unique, lowercase keywords based on a person's names and aliases.
|
||||||
|
func NameKeywords(names, aliases string) (results []string) {
|
||||||
|
if names == "" && aliases == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
names = strings.ToLower(names)
|
||||||
|
aliases = strings.ToLower(aliases)
|
||||||
|
|
||||||
|
return UniqueNames(append(Words(names), Words(aliases)...))
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,3 +43,10 @@ func TestJoinNames(t *testing.T) {
|
|||||||
assert.Equal(t, "Jens Mander, Name 2, Name 3 & Name 4", result)
|
assert.Equal(t, "Jens Mander, Name 2, Name 3 & Name 4", result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNameKeywords(t *testing.T) {
|
||||||
|
t.Run("BillGates", func(t *testing.T) {
|
||||||
|
result := NameKeywords("William Henry Gates III", "Windows Guru")
|
||||||
|
assert.Equal(t, []string{"william", "henry", "gates", "iii", "windows", "guru"}, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user