mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-08 02:03:33 +08:00
People: Refactor faces worker and related entities #22
This commit is contained in:
83
frontend/package-lock.json
generated
83
frontend/package-lock.json
generated
@@ -1839,9 +1839,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@polka/url": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.15",
|
"version": "1.0.0-next.17",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.17.tgz",
|
||||||
"integrity": "sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA=="
|
"integrity": "sha512-0p1rCgM3LLbAdwBnc7gqgnvjHg9KpbhcSphergHShlkWz8EdPawoMJ3/VbezI0mGC5eKCDzMaPgF9Yca6cKvrg=="
|
||||||
},
|
},
|
||||||
"node_modules/@trysound/sax": {
|
"node_modules/@trysound/sax": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
@@ -2248,9 +2248,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@webpack-cli/serve": {
|
"node_modules/@webpack-cli/serve": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.2.tgz",
|
||||||
"integrity": "sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw==",
|
"integrity": "sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"webpack-cli": "4.x.x"
|
"webpack-cli": "4.x.x"
|
||||||
},
|
},
|
||||||
@@ -4741,9 +4741,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.3.805",
|
"version": "1.3.806",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.805.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.806.tgz",
|
||||||
"integrity": "sha512-uUJF59M6pNSRHQaXwdkaNB4BhSQ9lldRdG1qCjlrAFkynPGDc5wPoUcYEQQeQGmKyAWJPvGkYAWmtVrxWmDAkg=="
|
"integrity": "sha512-AH/otJLAAecgyrYp0XK1DPiGVWcOgwPeJBOLeuFQ5l//vhQhwC9u6d+GijClqJAmsHG4XDue81ndSQPohUu0xA=="
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -5214,9 +5214,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-import-resolver-node": {
|
"node_modules/eslint-import-resolver-node": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
|
||||||
"integrity": "sha512-XMoPKjSpXbkeJ7ZZ9icLnJMTY5Mc1kZbCakHquaFsXPpyWOwK0TK6CODO+0ca54UoM9LKOxyUNnoVZRl8TeaAg==",
|
"integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^3.2.7",
|
"debug": "^3.2.7",
|
||||||
"resolve": "^1.20.0"
|
"resolve": "^1.20.0"
|
||||||
@@ -6517,7 +6517,8 @@
|
|||||||
"node_modules/flatten": {
|
"node_modules/flatten": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz",
|
||||||
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg=="
|
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
|
||||||
|
"deprecated": "flatten is deprecated in favor of utility frameworks such as lodash."
|
||||||
},
|
},
|
||||||
"node_modules/flow-parser": {
|
"node_modules/flow-parser": {
|
||||||
"version": "0.157.0",
|
"version": "0.157.0",
|
||||||
@@ -13464,11 +13465,11 @@
|
|||||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||||
},
|
},
|
||||||
"node_modules/sirv": {
|
"node_modules/sirv": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.14.tgz",
|
||||||
"integrity": "sha512-+jQoCxndz7L2tqQL4ZyzfDhky0W/4ZJip3XoOuxyQWnAwMxindLl3Xv1qT4x1YX/re0leShvTm8Uk0kQspGhBg==",
|
"integrity": "sha512-czTFDFjK9lXj0u9mJ3OmJoXFztoilYS+NdRPcJoT182w44wSEkHSiO7A2517GLJ8wKM4GjCm2OXE66Dhngbzjg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@polka/url": "^1.0.0-next.15",
|
"@polka/url": "^1.0.0-next.17",
|
||||||
"mime": "^2.3.1",
|
"mime": "^2.3.1",
|
||||||
"totalist": "^1.0.0"
|
"totalist": "^1.0.0"
|
||||||
},
|
},
|
||||||
@@ -15484,14 +15485,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-cli": {
|
"node_modules/webpack-cli": {
|
||||||
"version": "4.7.2",
|
"version": "4.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.8.0.tgz",
|
||||||
"integrity": "sha512-mEoLmnmOIZQNiRl0ebnjzQ74Hk0iKS5SiEEnpq3dRezoyR3yPaeQZCMCe+db4524pj1Pd5ghZXjT41KLzIhSLw==",
|
"integrity": "sha512-+iBSWsX16uVna5aAYN6/wjhJy1q/GKk4KjKvfg90/6hykCTSgozbfz5iRgDTSJt/LgSbYxdBX3KBHeobIs+ZEw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discoveryjs/json-ext": "^0.5.0",
|
"@discoveryjs/json-ext": "^0.5.0",
|
||||||
"@webpack-cli/configtest": "^1.0.4",
|
"@webpack-cli/configtest": "^1.0.4",
|
||||||
"@webpack-cli/info": "^1.3.0",
|
"@webpack-cli/info": "^1.3.0",
|
||||||
"@webpack-cli/serve": "^1.5.1",
|
"@webpack-cli/serve": "^1.5.2",
|
||||||
"colorette": "^1.2.1",
|
"colorette": "^1.2.1",
|
||||||
"commander": "^7.0.0",
|
"commander": "^7.0.0",
|
||||||
"execa": "^5.0.0",
|
"execa": "^5.0.0",
|
||||||
@@ -17148,9 +17149,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@polka/url": {
|
"@polka/url": {
|
||||||
"version": "1.0.0-next.15",
|
"version": "1.0.0-next.17",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.17.tgz",
|
||||||
"integrity": "sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA=="
|
"integrity": "sha512-0p1rCgM3LLbAdwBnc7gqgnvjHg9KpbhcSphergHShlkWz8EdPawoMJ3/VbezI0mGC5eKCDzMaPgF9Yca6cKvrg=="
|
||||||
},
|
},
|
||||||
"@trysound/sax": {
|
"@trysound/sax": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
@@ -17523,9 +17524,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@webpack-cli/serve": {
|
"@webpack-cli/serve": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.2.tgz",
|
||||||
"integrity": "sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw==",
|
"integrity": "sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@xtuc/ieee754": {
|
"@xtuc/ieee754": {
|
||||||
@@ -19384,9 +19385,9 @@
|
|||||||
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
|
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
|
||||||
},
|
},
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.3.805",
|
"version": "1.3.806",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.805.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.806.tgz",
|
||||||
"integrity": "sha512-uUJF59M6pNSRHQaXwdkaNB4BhSQ9lldRdG1qCjlrAFkynPGDc5wPoUcYEQQeQGmKyAWJPvGkYAWmtVrxWmDAkg=="
|
"integrity": "sha512-AH/otJLAAecgyrYp0XK1DPiGVWcOgwPeJBOLeuFQ5l//vhQhwC9u6d+GijClqJAmsHG4XDue81ndSQPohUu0xA=="
|
||||||
},
|
},
|
||||||
"emoji-regex": {
|
"emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -19840,9 +19841,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-import-resolver-node": {
|
"eslint-import-resolver-node": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
|
||||||
"integrity": "sha512-XMoPKjSpXbkeJ7ZZ9icLnJMTY5Mc1kZbCakHquaFsXPpyWOwK0TK6CODO+0ca54UoM9LKOxyUNnoVZRl8TeaAg==",
|
"integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "^3.2.7",
|
"debug": "^3.2.7",
|
||||||
"resolve": "^1.20.0"
|
"resolve": "^1.20.0"
|
||||||
@@ -25893,11 +25894,11 @@
|
|||||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
||||||
},
|
},
|
||||||
"sirv": {
|
"sirv": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.14.tgz",
|
||||||
"integrity": "sha512-+jQoCxndz7L2tqQL4ZyzfDhky0W/4ZJip3XoOuxyQWnAwMxindLl3Xv1qT4x1YX/re0leShvTm8Uk0kQspGhBg==",
|
"integrity": "sha512-czTFDFjK9lXj0u9mJ3OmJoXFztoilYS+NdRPcJoT182w44wSEkHSiO7A2517GLJ8wKM4GjCm2OXE66Dhngbzjg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@polka/url": "^1.0.0-next.15",
|
"@polka/url": "^1.0.0-next.17",
|
||||||
"mime": "^2.3.1",
|
"mime": "^2.3.1",
|
||||||
"totalist": "^1.0.0"
|
"totalist": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -27483,14 +27484,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"webpack-cli": {
|
"webpack-cli": {
|
||||||
"version": "4.7.2",
|
"version": "4.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.8.0.tgz",
|
||||||
"integrity": "sha512-mEoLmnmOIZQNiRl0ebnjzQ74Hk0iKS5SiEEnpq3dRezoyR3yPaeQZCMCe+db4524pj1Pd5ghZXjT41KLzIhSLw==",
|
"integrity": "sha512-+iBSWsX16uVna5aAYN6/wjhJy1q/GKk4KjKvfg90/6hykCTSgozbfz5iRgDTSJt/LgSbYxdBX3KBHeobIs+ZEw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@discoveryjs/json-ext": "^0.5.0",
|
"@discoveryjs/json-ext": "^0.5.0",
|
||||||
"@webpack-cli/configtest": "^1.0.4",
|
"@webpack-cli/configtest": "^1.0.4",
|
||||||
"@webpack-cli/info": "^1.3.0",
|
"@webpack-cli/info": "^1.3.0",
|
||||||
"@webpack-cli/serve": "^1.5.1",
|
"@webpack-cli/serve": "^1.5.2",
|
||||||
"colorette": "^1.2.1",
|
"colorette": "^1.2.1",
|
||||||
"commander": "^7.0.0",
|
"commander": "^7.0.0",
|
||||||
"execa": "^5.0.0",
|
"execa": "^5.0.0",
|
||||||
|
@@ -160,6 +160,9 @@ export default class Config {
|
|||||||
case "states":
|
case "states":
|
||||||
this.values.count.states += data.count;
|
this.values.count.states += data.count;
|
||||||
break;
|
break;
|
||||||
|
case "people":
|
||||||
|
this.values.count.people += data.count;
|
||||||
|
break;
|
||||||
case "places":
|
case "places":
|
||||||
this.values.count.places += data.count;
|
this.values.count.places += data.count;
|
||||||
break;
|
break;
|
||||||
|
@@ -49,7 +49,7 @@
|
|||||||
<v-layout v-else row wrap align-center>
|
<v-layout v-else 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.Label"
|
v-model="marker.Name"
|
||||||
:rules="[textRule]"
|
:rules="[textRule]"
|
||||||
browser-autocomplete="off"
|
browser-autocomplete="off"
|
||||||
class="input-name pa-0 ma-0"
|
class="input-name pa-0 ma-0"
|
||||||
@@ -132,8 +132,9 @@ export default {
|
|||||||
this.model.updateMarker(marker);
|
this.model.updateMarker(marker);
|
||||||
},
|
},
|
||||||
clearName(marker) {
|
clearName(marker) {
|
||||||
marker.Label = "";
|
marker.Name = "";
|
||||||
marker.RefUID = "";
|
marker.SubjectUID = "";
|
||||||
|
marker.SubjectSrc = "";
|
||||||
this.model.updateMarker(marker);
|
this.model.updateMarker(marker);
|
||||||
},
|
},
|
||||||
updateName(marker) {
|
updateName(marker) {
|
||||||
|
@@ -763,8 +763,8 @@ export class Photo extends RestModel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m.hasOwnProperty("Person") && !!m.Person && !!m.Person.Name) {
|
if (m.hasOwnProperty("Subject") && !!m.Subject && !!m.Subject.Name) {
|
||||||
m.Label = m.Person.Name;
|
m.Name = m.Subject.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push(m);
|
result.push(m);
|
||||||
|
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
@@ -27,6 +29,12 @@ var FacesCommand = cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "index",
|
Name: "index",
|
||||||
Usage: "Performs facial recognition",
|
Usage: "Performs facial recognition",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "force, f",
|
||||||
|
Usage: "reindex existing faces",
|
||||||
|
},
|
||||||
|
},
|
||||||
Action: facesIndexAction,
|
Action: facesIndexAction,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -110,9 +118,13 @@ func facesIndexAction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
conf.InitDb()
|
conf.InitDb()
|
||||||
|
|
||||||
|
opt := photoprism.FacesOptions{
|
||||||
|
Force: ctx.Bool("force"),
|
||||||
|
}
|
||||||
|
|
||||||
w := service.Faces()
|
w := service.Faces()
|
||||||
|
|
||||||
if err := w.Start(); err != nil {
|
if err := w.Start(opt); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
@@ -378,9 +378,9 @@ func (c *Config) UserConfig() ClientConfig {
|
|||||||
Take(&result.Count)
|
Take(&result.Count)
|
||||||
|
|
||||||
c.Db().
|
c.Db().
|
||||||
Table(entity.Person{}.TableName()).
|
Table(entity.Subject{}.TableName()).
|
||||||
Select("SUM(deleted_at IS NULL) AS people").
|
Select("SUM(deleted_at IS NULL) AS people").
|
||||||
Where("id <> ?", entity.UnknownPerson.ID).
|
Where("id <> ? AND subject_type = ?", entity.UnknownPerson.ID, entity.SubjectPerson).
|
||||||
Take(&result.Count)
|
Take(&result.Count)
|
||||||
|
|
||||||
c.Db().
|
c.Db().
|
||||||
|
@@ -11,10 +11,10 @@ const (
|
|||||||
SortOrderEdited = "edited"
|
SortOrderEdited = "edited"
|
||||||
|
|
||||||
// Unknown values:
|
// Unknown values:
|
||||||
YearUnknown = -1
|
UnknownYear = -1
|
||||||
MonthUnknown = -1
|
UnknownMonth = -1
|
||||||
DayUnknown = -1
|
UnknownDay = -1
|
||||||
TitleUnknown = "Unknown"
|
UnknownName = "Unknown"
|
||||||
|
|
||||||
// Content types:
|
// Content types:
|
||||||
TypeDefault = ""
|
TypeDefault = ""
|
||||||
|
@@ -125,7 +125,7 @@ func TestNewDetails(t *testing.T) {
|
|||||||
t.Run("add to photo", func(t *testing.T) {
|
t.Run("add to photo", func(t *testing.T) {
|
||||||
p := NewPhoto(true)
|
p := NewPhoto(true)
|
||||||
|
|
||||||
assert.Equal(t, TitleUnknown, p.PhotoTitle)
|
assert.Equal(t, UnknownName, p.PhotoTitle)
|
||||||
|
|
||||||
d := NewDetails(p)
|
d := NewDetails(p)
|
||||||
p.Details = &d
|
p.Details = &d
|
||||||
|
@@ -55,7 +55,7 @@ var Entities = Types{
|
|||||||
"photos_keywords": &PhotoKeyword{},
|
"photos_keywords": &PhotoKeyword{},
|
||||||
"passwords": &Password{},
|
"passwords": &Password{},
|
||||||
"links": &Link{},
|
"links": &Link{},
|
||||||
Person{}.TableName(): &Person{},
|
Subject{}.TableName(): &Subject{},
|
||||||
Face{}.TableName(): &Face{},
|
Face{}.TableName(): &Face{},
|
||||||
Marker{}.TableName(): &Marker{},
|
Marker{}.TableName(): &Marker{},
|
||||||
}
|
}
|
||||||
|
@@ -4,17 +4,20 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var faceMutex = sync.Mutex{}
|
||||||
|
|
||||||
// Faces represents a Face slice.
|
// Faces represents a Face slice.
|
||||||
type Faces []Face
|
type Faces []Face
|
||||||
|
|
||||||
// Face represents the face of a Person.
|
// Face represents the face of a Subject.
|
||||||
type Face struct {
|
type Face struct {
|
||||||
ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
|
ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
|
||||||
FaceSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src,omitempty"`
|
FaceSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src,omitempty"`
|
||||||
PersonUID string `gorm:"type:VARBINARY(42);index;" json:"PersonUID" yaml:"PersonUID,omitempty"`
|
SubjectUID string `gorm:"type:VARBINARY(42);index;" json:"SubjectUID" yaml:"SubjectUID,omitempty"`
|
||||||
Collisions int `json:"Collisions" yaml:"Collisions,omitempty"`
|
Collisions int `json:"Collisions" yaml:"Collisions,omitempty"`
|
||||||
Samples int `json:"Samples" yaml:"Samples,omitempty"`
|
Samples int `json:"Samples" yaml:"Samples,omitempty"`
|
||||||
Radius float64 `json:"Radius" yaml:"Radius,omitempty"`
|
Radius float64 `json:"Radius" yaml:"Radius,omitempty"`
|
||||||
@@ -28,7 +31,7 @@ type Face struct {
|
|||||||
var UnknownFace = Face{
|
var UnknownFace = Face{
|
||||||
ID: "zz",
|
ID: "zz",
|
||||||
FaceSrc: SrcDefault,
|
FaceSrc: SrcDefault,
|
||||||
PersonUID: UnknownPerson.PersonUID,
|
SubjectUID: UnknownPerson.SubjectUID,
|
||||||
EmbeddingJSON: []byte{},
|
EmbeddingJSON: []byte{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,13 +42,13 @@ func CreateUnknownFace() {
|
|||||||
|
|
||||||
// TableName returns the entity database table name.
|
// TableName returns the entity database table name.
|
||||||
func (Face) TableName() string {
|
func (Face) TableName() string {
|
||||||
return "faces_dev2"
|
return "faces_dev3"
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFace returns a new face.
|
// NewFace returns a new face.
|
||||||
func NewFace(personUID string, embeddings Embeddings) *Face {
|
func NewFace(subjectUID string, embeddings Embeddings) *Face {
|
||||||
result := &Face{
|
result := &Face{
|
||||||
PersonUID: personUID,
|
SubjectUID: subjectUID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := result.SetEmbeddings(embeddings); err != nil {
|
if err := result.SetEmbeddings(embeddings); err != nil {
|
||||||
@@ -90,16 +93,16 @@ func (m *Face) Embedding() Embedding {
|
|||||||
|
|
||||||
// Save updates the existing or inserts a new face.
|
// Save updates the existing or inserts a new face.
|
||||||
func (m *Face) Save() error {
|
func (m *Face) Save() error {
|
||||||
peopleMutex.Lock()
|
faceMutex.Lock()
|
||||||
defer peopleMutex.Unlock()
|
defer faceMutex.Unlock()
|
||||||
|
|
||||||
return Save(m, "ID")
|
return Save(m, "ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create inserts the face to the database.
|
// Create inserts the face to the database.
|
||||||
func (m *Face) Create() error {
|
func (m *Face) Create() error {
|
||||||
peopleMutex.Lock()
|
faceMutex.Lock()
|
||||||
defer peopleMutex.Unlock()
|
defer faceMutex.Unlock()
|
||||||
|
|
||||||
return Db().Create(m).Error
|
return Db().Create(m).Error
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@ func CreateTestFixtures() {
|
|||||||
CreateFileShareFixtures()
|
CreateFileShareFixtures()
|
||||||
CreateFileSyncFixtures()
|
CreateFileSyncFixtures()
|
||||||
CreateLensFixtures()
|
CreateLensFixtures()
|
||||||
CreatePersonFixtures()
|
CreateSubjectFixtures()
|
||||||
CreateMarkerFixtures()
|
CreateMarkerFixtures()
|
||||||
CreateFaceFixtures()
|
CreateFaceFixtures()
|
||||||
CreateUserFixtures()
|
CreateUserFixtures()
|
||||||
|
@@ -14,31 +14,31 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
MarkerUnknown = ""
|
MarkerUnknown = ""
|
||||||
MarkerFace = "Face"
|
MarkerFace = "face"
|
||||||
MarkerLabel = "Label"
|
MarkerLabel = "label"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Marker represents an image marker point.
|
// Marker represents an image marker point.
|
||||||
type Marker struct {
|
type Marker struct {
|
||||||
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
||||||
FileID uint `gorm:"index;" json:"-" yaml:"-"`
|
FileID uint `gorm:"index;" json:"-" yaml:"-"`
|
||||||
FaceID string `gorm:"type:VARBINARY(42);index;" json:"FaceID" yaml:"FaceID,omitempty"`
|
MarkerType string `gorm:"type:VARBINARY(8);index:idx_markers_subject;default:'';" json:"Type" yaml:"Type"`
|
||||||
RefUID string `gorm:"type:VARBINARY(42);index:idx_markers_uid_type;" json:"RefUID" yaml:"RefUID,omitempty"`
|
|
||||||
RefSrc string `gorm:"type:VARBINARY(8);default:'';" json:"RefSrc" yaml:"RefSrc,omitempty"`
|
|
||||||
MarkerType string `gorm:"type:VARBINARY(8);index:idx_markers_uid_type;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"`
|
||||||
|
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"`
|
||||||
|
FaceID string `gorm:"type:VARBINARY(42);index;" json:"FaceID" yaml:"FaceID,omitempty"`
|
||||||
|
EmbeddingsJSON []byte `gorm:"type:MEDIUMBLOB;" json:"EmbeddingsJSON" yaml:"EmbeddingsJSON,omitempty"`
|
||||||
MarkerScore int `gorm:"type:SMALLINT" json:"Score" yaml:"Score,omitempty"`
|
MarkerScore int `gorm:"type:SMALLINT" json:"Score" yaml:"Score,omitempty"`
|
||||||
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
|
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
|
||||||
MarkerLabel string `gorm:"type:VARCHAR(255);" json:"Label" yaml:"Label,omitempty"`
|
MarkerJSON []byte `gorm:"type:MEDIUMBLOB;" json:"MarkerJSON" yaml:"MarkerJSON,omitempty"`
|
||||||
MetaJSON []byte `gorm:"type:MEDIUMBLOB;" json:"MetaJSON" yaml:"MetaJSON,omitempty"`
|
|
||||||
EmbeddingsJSON []byte `gorm:"type:MEDIUMBLOB;" json:"EmbeddingsJSON" yaml:"EmbeddingsJSON,omitempty"`
|
|
||||||
X float32 `gorm:"type:FLOAT;" json:"X" yaml:"X,omitempty"`
|
X float32 `gorm:"type:FLOAT;" json:"X" yaml:"X,omitempty"`
|
||||||
Y float32 `gorm:"type:FLOAT;" json:"Y" yaml:"Y,omitempty"`
|
Y float32 `gorm:"type:FLOAT;" json:"Y" yaml:"Y,omitempty"`
|
||||||
W float32 `gorm:"type:FLOAT;" json:"W" yaml:"W,omitempty"`
|
W float32 `gorm:"type:FLOAT;" json:"W" yaml:"W,omitempty"`
|
||||||
H float32 `gorm:"type:FLOAT;" json:"H" yaml:"H,omitempty"`
|
H float32 `gorm:"type:FLOAT;" json:"H" yaml:"H,omitempty"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
Person *Person `gorm:"foreignkey:RefUID;association_foreignkey:PersonUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Person" yaml:"-"`
|
Subject *Subject `gorm:"foreignkey:SubjectUID;association_foreignkey:SubjectUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Subject,omitempty" yaml:"-"`
|
||||||
embeddings Embeddings `gorm:"-"`
|
embeddings Embeddings `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,14 +47,14 @@ var UnknownMarker = NewMarker(0, "", 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_dev2"
|
return "markers_dev3"
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMarker creates a new entity.
|
// NewMarker creates a new entity.
|
||||||
func NewMarker(fileUID uint, refUID, markerSrc, markerType string, x, y, w, h float32) *Marker {
|
func NewMarker(fileUID uint, refUID, markerSrc, markerType string, x, y, w, h float32) *Marker {
|
||||||
m := &Marker{
|
m := &Marker{
|
||||||
FileID: fileUID,
|
FileID: fileUID,
|
||||||
RefUID: refUID,
|
SubjectUID: refUID,
|
||||||
MarkerSrc: markerSrc,
|
MarkerSrc: markerSrc,
|
||||||
MarkerType: markerType,
|
MarkerType: markerType,
|
||||||
X: x,
|
X: x,
|
||||||
@@ -73,7 +73,7 @@ func NewFaceMarker(f face.Face, fileID uint, refUID string) *Marker {
|
|||||||
m := NewMarker(fileID, refUID, SrcImage, MarkerFace, pos.X, pos.Y, pos.W, pos.H)
|
m := NewMarker(fileID, refUID, SrcImage, MarkerFace, pos.X, pos.Y, pos.W, pos.H)
|
||||||
|
|
||||||
m.MarkerScore = f.Score
|
m.MarkerScore = f.Score
|
||||||
m.MetaJSON = f.RelativeLandmarksJSON()
|
m.MarkerJSON = f.RelativeLandmarksJSON()
|
||||||
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
||||||
|
|
||||||
return m
|
return m
|
||||||
@@ -95,8 +95,8 @@ func (m *Marker) SaveForm(f form.Marker) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.MarkerLabel != "" {
|
if f.MarkerName != "" {
|
||||||
m.MarkerLabel = txt.Title(txt.Clip(f.MarkerLabel, txt.ClipKeyword))
|
m.MarkerName = txt.Title(txt.Clip(f.MarkerName, txt.ClipKeyword))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.Save(); err != nil {
|
if err := m.Save(); err != nil {
|
||||||
@@ -105,35 +105,44 @@ func (m *Marker) SaveForm(f form.Marker) error {
|
|||||||
|
|
||||||
faceId := m.FaceID
|
faceId := m.FaceID
|
||||||
|
|
||||||
if faceId != "" && m.MarkerLabel != "" && m.RefUID == "" && m.MarkerType == MarkerFace {
|
if faceId != "" && m.MarkerName != "" && m.SubjectUID == "" && m.MarkerType == MarkerFace {
|
||||||
if p := NewPerson(m.MarkerLabel, SrcMarker, 1); p == nil {
|
if subj := NewSubject(m.MarkerName, SubjectPerson, SrcMarker); subj == nil {
|
||||||
return fmt.Errorf("marker: person should not be nil (save form)")
|
return fmt.Errorf("marker: subject should not be nil (save form)")
|
||||||
} else if p = FirstOrCreatePerson(p); p == nil {
|
} else if subj = FirstOrCreateSubject(subj); subj == nil {
|
||||||
return fmt.Errorf("marker: failed adding person %s for marker %d (save form)", txt.Quote(m.MarkerLabel), m.ID)
|
return fmt.Errorf("marker: failed adding subject %s for marker %d (save form)", txt.Quote(m.MarkerName), m.ID)
|
||||||
} else if err := m.Updates(Val{"RefUID": p.PersonUID, "RefSrc": SrcManual, "FaceID": ""}); err != nil {
|
} else if err := m.Updates(Values{"SubjectUID": subj.SubjectUID, "SubjectSrc": SrcManual, "FaceID": ""}); err != nil {
|
||||||
return fmt.Errorf("marker: %s (save form)", err)
|
return fmt.Errorf("marker: %s (save form)", err)
|
||||||
} else if err := Db().Model(&Face{}).Where("id = ? AND person_uid = ''", faceId).Update("PersonUID", p.PersonUID).Error; err != nil {
|
} else if err := Db().Model(&Face{}).Where("id = ? AND subject_uid = ''", faceId).Update("SubjectUID", subj.SubjectUID).Error; err != nil {
|
||||||
return fmt.Errorf("marker: %s (update face)", err)
|
return fmt.Errorf("marker: %s (update face)", err)
|
||||||
} else if err := Db().Model(&Marker{}).
|
} else if err := Db().Model(&Marker{}).
|
||||||
Where("face_id = ?", faceId).
|
Where("face_id = ?", faceId).
|
||||||
Updates(Val{"RefUID": p.PersonUID, "RefSrc": SrcManual, "FaceID": ""}).Error; err != nil {
|
Updates(Values{"SubjectUID": subj.SubjectUID, "SubjectSrc": SrcManual, "FaceID": ""}).Error; err != nil {
|
||||||
return fmt.Errorf("marker: %s (update related markers)", err)
|
return fmt.Errorf("marker: %s (update related markers)", err)
|
||||||
} else {
|
} else {
|
||||||
log.Infof("marker: matched person %s with label %s", p.PersonUID, txt.Quote(m.MarkerLabel))
|
log.Infof("marker: matched subject %s with %s", subj.SubjectUID, txt.Quote(m.MarkerName))
|
||||||
}
|
|
||||||
} else if m.MarkerLabel != "" && m.RefUID != "" && m.MarkerType == MarkerFace {
|
|
||||||
if p := FindPerson(m.RefUID); p != nil {
|
|
||||||
p.SetName(m.MarkerLabel)
|
|
||||||
|
|
||||||
if err := p.Save(); err != nil {
|
|
||||||
return fmt.Errorf("marker: %s (update person)", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if err := m.UpdateSubject(); err != nil {
|
||||||
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateSubject changes and saves the related subject's name in the index.
|
||||||
|
func (m *Marker) UpdateSubject() error {
|
||||||
|
if m.MarkerName == "" || m.SubjectUID == "" || m.MarkerType == MarkerFace {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
subj := FindSubject(m.SubjectUID)
|
||||||
|
|
||||||
|
if subj == nil {
|
||||||
|
return fmt.Errorf("marker: subject %s not found", m.SubjectUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return subj.UpdateName(m.MarkerName)
|
||||||
|
}
|
||||||
|
|
||||||
// Save updates the existing or inserts a new row.
|
// Save updates the existing or inserts a new row.
|
||||||
func (m *Marker) Save() error {
|
func (m *Marker) Save() error {
|
||||||
if m.X == 0 || m.Y == 0 || m.X > 1 || m.Y > 1 || m.X < -1 || m.Y < -1 {
|
if m.X == 0 || m.Y == 0 || m.X > 1 || m.Y > 1 || m.X < -1 || m.Y < -1 {
|
||||||
@@ -200,9 +209,9 @@ func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
|
|||||||
"W": m.W,
|
"W": m.W,
|
||||||
"H": m.H,
|
"H": m.H,
|
||||||
"MarkerScore": m.MarkerScore,
|
"MarkerScore": m.MarkerScore,
|
||||||
"MetaJSON": m.MetaJSON,
|
"MarkerJSON": m.MarkerJSON,
|
||||||
"EmbeddingsJSON": m.EmbeddingsJSON,
|
"EmbeddingsJSON": m.EmbeddingsJSON,
|
||||||
"RefUID": m.RefUID,
|
"SubjectUID": m.SubjectUID,
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Debugf("faces: updated existing marker %d for file %d", result.ID, result.FileID)
|
log.Debugf("faces: updated existing marker %d for file %d", result.ID, result.FileID)
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@ import (
|
|||||||
func TestMarkerMap_Get(t *testing.T) {
|
func TestMarkerMap_Get(t *testing.T) {
|
||||||
t.Run("get existing marker", func(t *testing.T) {
|
t.Run("get existing marker", func(t *testing.T) {
|
||||||
r := MarkerFixtures.Get("1000003-3")
|
r := MarkerFixtures.Get("1000003-3")
|
||||||
assert.Equal(t, "Center", r.MarkerLabel)
|
assert.Equal(t, "Center", r.MarkerName)
|
||||||
assert.Equal(t, float32(0.5), r.Y)
|
assert.Equal(t, float32(0.5), r.Y)
|
||||||
assert.IsType(t, Marker{}, r)
|
assert.IsType(t, Marker{}, r)
|
||||||
})
|
})
|
||||||
@@ -23,7 +23,7 @@ func TestMarkerMap_Get(t *testing.T) {
|
|||||||
func TestMarkerMap_Pointer(t *testing.T) {
|
func TestMarkerMap_Pointer(t *testing.T) {
|
||||||
t.Run("get existing marker pointer", func(t *testing.T) {
|
t.Run("get existing marker pointer", func(t *testing.T) {
|
||||||
r := MarkerFixtures.Pointer("1000003-3")
|
r := MarkerFixtures.Pointer("1000003-3")
|
||||||
assert.Equal(t, "Center", r.MarkerLabel)
|
assert.Equal(t, "Center", r.MarkerName)
|
||||||
assert.Equal(t, float32(0.5), r.Y)
|
assert.Equal(t, float32(0.5), r.Y)
|
||||||
assert.IsType(t, &Marker{}, r)
|
assert.IsType(t, &Marker{}, r)
|
||||||
})
|
})
|
||||||
|
@@ -15,7 +15,7 @@ func TestNewMarker(t *testing.T) {
|
|||||||
m := NewMarker(1000000, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
m := NewMarker(1000000, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
||||||
assert.IsType(t, &Marker{}, m)
|
assert.IsType(t, &Marker{}, m)
|
||||||
assert.Equal(t, uint(1000000), m.FileID)
|
assert.Equal(t, uint(1000000), m.FileID)
|
||||||
assert.Equal(t, "lt9k3pw1wowuy3c3", m.RefUID)
|
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjectUID)
|
||||||
assert.Equal(t, SrcImage, m.MarkerSrc)
|
assert.Equal(t, SrcImage, m.MarkerSrc)
|
||||||
assert.Equal(t, MarkerLabel, m.MarkerType)
|
assert.Equal(t, MarkerLabel, m.MarkerType)
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ func TestUpdateOrCreateMarker(t *testing.T) {
|
|||||||
m := NewMarker(1000000, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
m := NewMarker(1000000, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
||||||
assert.IsType(t, &Marker{}, m)
|
assert.IsType(t, &Marker{}, m)
|
||||||
assert.Equal(t, uint(1000000), m.FileID)
|
assert.Equal(t, uint(1000000), m.FileID)
|
||||||
assert.Equal(t, "lt9k3pw1wowuy3c3", m.RefUID)
|
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjectUID)
|
||||||
assert.Equal(t, SrcImage, m.MarkerSrc)
|
assert.Equal(t, SrcImage, m.MarkerSrc)
|
||||||
assert.Equal(t, MarkerLabel, m.MarkerType)
|
assert.Equal(t, MarkerLabel, m.MarkerType)
|
||||||
|
|
||||||
|
@@ -46,7 +46,7 @@ func FindMarkers(fileID uint) (Markers, error) {
|
|||||||
m := Markers{}
|
m := Markers{}
|
||||||
err := Db().
|
err := Db().
|
||||||
Where(`file_id = ?`, fileID).
|
Where(`file_id = ?`, fileID).
|
||||||
Preload("Person").
|
Preload("Subject").
|
||||||
Order("id").
|
Order("id").
|
||||||
Offset(0).Limit(1000).
|
Offset(0).Limit(1000).
|
||||||
Find(&m).Error
|
Find(&m).Error
|
||||||
|
@@ -1,195 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gosimple/slug"
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
|
||||||
"github.com/photoprism/photoprism/pkg/rnd"
|
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var peopleMutex = sync.Mutex{}
|
|
||||||
|
|
||||||
type People []Person
|
|
||||||
|
|
||||||
// Person represents a person on one or more photos.
|
|
||||||
type Person struct {
|
|
||||||
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
|
||||||
PersonUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
|
|
||||||
PersonSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"-"`
|
|
||||||
PersonName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name"`
|
|
||||||
PersonSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src"`
|
|
||||||
PersonFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
|
|
||||||
PersonPrivate bool `json:"Private" yaml:"Private,omitempty"`
|
|
||||||
PersonHidden bool `json:"Hidden" yaml:"Hidden,omitempty"`
|
|
||||||
PersonDescription string `gorm:"type:TEXT;" json:"Description" yaml:"Description,omitempty"`
|
|
||||||
PersonNotes string `gorm:"type:TEXT;" json:"Notes" yaml:"Notes,omitempty"`
|
|
||||||
PersonMeta string `gorm:"type:LONGTEXT;" json:"Meta" yaml:"Meta,omitempty"`
|
|
||||||
PhotoCount int `gorm:"default:0" json:"PhotoCount" yaml:"-"`
|
|
||||||
BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"`
|
|
||||||
BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,omitempty"`
|
|
||||||
BirthDay int `json:"BirthDay" yaml:"BirthDay,omitempty"`
|
|
||||||
PassedAway *time.Time `json:"PassedAway" yaml:"PassedAway,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
|
||||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
|
||||||
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnknownPerson can be used as a placeholder for unknown people.
|
|
||||||
var UnknownPerson = Person{
|
|
||||||
ID: 1,
|
|
||||||
PersonUID: "r000000000000001",
|
|
||||||
PersonSlug: "zz",
|
|
||||||
PersonName: "Unknown",
|
|
||||||
PersonSrc: SrcDefault,
|
|
||||||
PersonFavorite: false,
|
|
||||||
BirthYear: YearUnknown,
|
|
||||||
BirthMonth: MonthUnknown,
|
|
||||||
BirthDay: DayUnknown,
|
|
||||||
PhotoCount: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateUnknownPerson initializes the database with a placeholder for unknown people if not exists.
|
|
||||||
func CreateUnknownPerson() {
|
|
||||||
FirstOrCreatePerson(&UnknownPerson)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName returns the entity database table name.
|
|
||||||
func (Person) TableName() string {
|
|
||||||
return "people_dev2"
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
|
||||||
func (m *Person) BeforeCreate(scope *gorm.Scope) error {
|
|
||||||
if rnd.IsUID(m.PersonUID, 'r') {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return scope.SetColumn("PersonUID", rnd.PPID('r'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPerson returns a new person.
|
|
||||||
func NewPerson(personName, personSrc string, photoCount int) *Person {
|
|
||||||
personName = txt.Title(txt.Clip(personName, txt.ClipDefault))
|
|
||||||
personSlug := slug.Make(txt.Clip(personName, txt.ClipSlug))
|
|
||||||
|
|
||||||
result := &Person{
|
|
||||||
PersonSlug: personSlug,
|
|
||||||
PersonName: personName,
|
|
||||||
PersonSrc: personSrc,
|
|
||||||
BirthYear: YearUnknown,
|
|
||||||
BirthMonth: MonthUnknown,
|
|
||||||
BirthDay: DayUnknown,
|
|
||||||
PhotoCount: photoCount,
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save updates the existing or inserts a new person.
|
|
||||||
func (m *Person) Save() error {
|
|
||||||
peopleMutex.Lock()
|
|
||||||
defer peopleMutex.Unlock()
|
|
||||||
|
|
||||||
return Db().Save(m).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create inserts the person to the database.
|
|
||||||
func (m *Person) Create() error {
|
|
||||||
peopleMutex.Lock()
|
|
||||||
defer peopleMutex.Unlock()
|
|
||||||
|
|
||||||
return Db().Create(m).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the person from the database.
|
|
||||||
func (m *Person) Delete() error {
|
|
||||||
return Db().Delete(m).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deleted returns true if the person is deleted.
|
|
||||||
func (m *Person) Deleted() bool {
|
|
||||||
return m.DeletedAt != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore restores the person in the database.
|
|
||||||
func (m *Person) Restore() error {
|
|
||||||
if m.Deleted() {
|
|
||||||
return UnscopedDb().Model(m).Update("DeletedAt", nil).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update a person property in the database.
|
|
||||||
func (m *Person) Update(attr string, value interface{}) error {
|
|
||||||
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// FirstOrCreatePerson returns the existing person, inserts a new person or nil in case of errors.
|
|
||||||
func FirstOrCreatePerson(m *Person) *Person {
|
|
||||||
result := Person{}
|
|
||||||
|
|
||||||
if err := UnscopedDb().Where("person_slug = ?", m.PersonSlug).First(&result).Error; err == nil {
|
|
||||||
return &result
|
|
||||||
} else if createErr := m.Create(); createErr == nil {
|
|
||||||
if !m.PersonHidden {
|
|
||||||
event.EntitiesCreated("people", []*Person{m})
|
|
||||||
|
|
||||||
event.Publish("count.people", event.Data{
|
|
||||||
"count": 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
} else if err := UnscopedDb().Where("person_slug = ?", m.PersonSlug).First(&result).Error; err == nil {
|
|
||||||
return &result
|
|
||||||
} else {
|
|
||||||
log.Errorf("person: %s (find or create %s)", createErr, m.PersonSlug)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindPerson returns an existing row if exists.
|
|
||||||
func FindPerson(s string) *Person {
|
|
||||||
if s == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := Person{}
|
|
||||||
|
|
||||||
db := Db()
|
|
||||||
|
|
||||||
if rnd.IsPPID(s, 'r') {
|
|
||||||
db = db.Where("person_uid = ?", s)
|
|
||||||
} else {
|
|
||||||
db = db.Where("person_slug = ?", slug.Make(txt.Clip(s, txt.ClipSlug)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.First(&result).Error; err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &result
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetName changes the person's name.
|
|
||||||
func (m *Person) SetName(name string) {
|
|
||||||
newName := txt.Clip(name, txt.ClipDefault)
|
|
||||||
|
|
||||||
if newName == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.PersonName = txt.Title(newName)
|
|
||||||
m.PersonSlug = slug.Make(txt.Clip(name, txt.ClipSlug))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Links returns all share links for this entity.
|
|
||||||
func (m *Person) Links() Links {
|
|
||||||
return FindLinks("", m.PersonUID)
|
|
||||||
}
|
|
@@ -1,50 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
type PersonMap map[string]Person
|
|
||||||
|
|
||||||
func (m PersonMap) Get(name string) Person {
|
|
||||||
if result, ok := m[name]; ok {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return UnknownPerson
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m PersonMap) Pointer(name string) *Person {
|
|
||||||
if result, ok := m[name]; ok {
|
|
||||||
return &result
|
|
||||||
}
|
|
||||||
|
|
||||||
return &UnknownPerson
|
|
||||||
}
|
|
||||||
|
|
||||||
var PersonFixtures = PersonMap{
|
|
||||||
"known_face": Person{
|
|
||||||
ID: 2,
|
|
||||||
PersonUID: "rqu0xs11qekk9jx8",
|
|
||||||
PersonSlug: "john-doe",
|
|
||||||
PersonName: "John Doe",
|
|
||||||
PersonSrc: "xmp",
|
|
||||||
PersonFavorite: true,
|
|
||||||
PersonPrivate: false,
|
|
||||||
PersonHidden: false,
|
|
||||||
PersonDescription: "Person Description",
|
|
||||||
PersonNotes: "Short Note",
|
|
||||||
PersonMeta: "",
|
|
||||||
PhotoCount: 1,
|
|
||||||
BirthYear: 2000,
|
|
||||||
BirthMonth: 5,
|
|
||||||
BirthDay: 22,
|
|
||||||
PassedAway: nil,
|
|
||||||
CreatedAt: Timestamp(),
|
|
||||||
UpdatedAt: Timestamp(),
|
|
||||||
DeletedAt: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatePersonFixtures inserts known entities into the database for testing.
|
|
||||||
func CreatePersonFixtures() {
|
|
||||||
for _, entity := range PersonFixtures {
|
|
||||||
Db().Create(&entity)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,160 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPerson_TableName(t *testing.T) {
|
|
||||||
m := &Person{}
|
|
||||||
assert.Contains(t, m.TableName(), "people")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewPerson(t *testing.T) {
|
|
||||||
t.Run("Jens_Mander", func(t *testing.T) {
|
|
||||||
m := NewPerson("Jens Mander", SrcAuto, 0)
|
|
||||||
assert.Equal(t, "Jens Mander", m.PersonName)
|
|
||||||
assert.Equal(t, "jens-mander", m.PersonSlug)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPerson_SetName(t *testing.T) {
|
|
||||||
t.Run("success", func(t *testing.T) {
|
|
||||||
m := NewPerson("Jens Mander", SrcAuto, 0)
|
|
||||||
|
|
||||||
assert.Equal(t, "Jens Mander", m.PersonName)
|
|
||||||
assert.Equal(t, "jens-mander", m.PersonSlug)
|
|
||||||
|
|
||||||
m.SetName("Foo McBar")
|
|
||||||
|
|
||||||
assert.Equal(t, "Foo McBar", m.PersonName)
|
|
||||||
assert.Equal(t, "foo-mcbar", m.PersonSlug)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFirstOrCreatePerson(t *testing.T) {
|
|
||||||
m := NewPerson("Create Me", SrcAuto, 0)
|
|
||||||
result := FirstOrCreatePerson(m)
|
|
||||||
|
|
||||||
if result == nil {
|
|
||||||
t.Fatal("result should not be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "Create Me", m.PersonName)
|
|
||||||
assert.Equal(t, "create-me", m.PersonSlug)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPerson_Save(t *testing.T) {
|
|
||||||
t.Run("success", func(t *testing.T) {
|
|
||||||
m := NewPerson("Save Me", SrcAuto, 0)
|
|
||||||
initialDate := m.UpdatedAt
|
|
||||||
err := m.Save()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
afterDate := m.UpdatedAt
|
|
||||||
|
|
||||||
assert.True(t, afterDate.After(initialDate))
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPerson_Delete(t *testing.T) {
|
|
||||||
t.Run("success", func(t *testing.T) {
|
|
||||||
m := NewPerson("Jens Mander", SrcAuto, 0)
|
|
||||||
err := m.Save()
|
|
||||||
assert.False(t, m.Deleted())
|
|
||||||
|
|
||||||
var people People
|
|
||||||
|
|
||||||
if err := Db().Where("person_name = ?", m.PersonName).Find(&people).Error; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, people, 1)
|
|
||||||
|
|
||||||
err = m.Delete()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := Db().Where("person_name = ?", m.PersonName).Find(&people).Error; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, people, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPerson_Restore(t *testing.T) {
|
|
||||||
t.Run("success", func(t *testing.T) {
|
|
||||||
var deleteTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
m := &Person{DeletedAt: &deleteTime, PersonName: "ToBeRestored"}
|
|
||||||
err := m.Save()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assert.True(t, m.Deleted())
|
|
||||||
|
|
||||||
err = m.Restore()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assert.False(t, m.Deleted())
|
|
||||||
})
|
|
||||||
t.Run("person not deleted", func(t *testing.T) {
|
|
||||||
m := &Person{DeletedAt: nil, PersonName: "NotDeleted1234"}
|
|
||||||
err := m.Restore()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assert.False(t, m.Deleted())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindPerson(t *testing.T) {
|
|
||||||
t.Run("success", func(t *testing.T) {
|
|
||||||
m := NewPerson("Find Me", SrcAuto, 0)
|
|
||||||
err := m.Save()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
found := FindPerson("find me")
|
|
||||||
assert.Equal(t, "Find Me", found.PersonName)
|
|
||||||
})
|
|
||||||
t.Run("nil", func(t *testing.T) {
|
|
||||||
r := FindPerson("XXX")
|
|
||||||
assert.Nil(t, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPerson_Links(t *testing.T) {
|
|
||||||
t.Run("no-result", func(t *testing.T) {
|
|
||||||
m := UnknownPerson
|
|
||||||
links := m.Links()
|
|
||||||
assert.Empty(t, links)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPerson_Update(t *testing.T) {
|
|
||||||
t.Run("success", func(t *testing.T) {
|
|
||||||
m := NewPerson("Update Me", SrcAuto, 0)
|
|
||||||
|
|
||||||
if err := m.Save(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.Update("PersonName", "Updated Name"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, "Updated Name", m.PersonName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
@@ -105,7 +105,7 @@ type Photo struct {
|
|||||||
// NewPhoto creates a photo entity.
|
// NewPhoto creates a photo entity.
|
||||||
func NewPhoto(stackable bool) Photo {
|
func NewPhoto(stackable bool) Photo {
|
||||||
m := Photo{
|
m := Photo{
|
||||||
PhotoTitle: TitleUnknown,
|
PhotoTitle: UnknownName,
|
||||||
PhotoType: TypeImage,
|
PhotoType: TypeImage,
|
||||||
PhotoCountry: UnknownCountry.ID,
|
PhotoCountry: UnknownCountry.ID,
|
||||||
CameraID: UnknownCamera.ID,
|
CameraID: UnknownCamera.ID,
|
||||||
@@ -761,9 +761,9 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||||||
m.SetTitle(fileTitle, SrcAuto)
|
m.SetTitle(fileTitle, SrcAuto)
|
||||||
} else {
|
} else {
|
||||||
if m.TakenSrc != SrcAuto {
|
if m.TakenSrc != SrcAuto {
|
||||||
m.SetTitle(fmt.Sprintf("%s / %s", TitleUnknown, m.TakenAt.Format("2006")), SrcAuto)
|
m.SetTitle(fmt.Sprintf("%s / %s", UnknownName, m.TakenAt.Format("2006")), SrcAuto)
|
||||||
} else {
|
} else {
|
||||||
m.SetTitle(TitleUnknown, SrcAuto)
|
m.SetTitle(UnknownName, SrcAuto)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -931,9 +931,9 @@ func (m *Photo) UpdateDateFields() {
|
|||||||
|
|
||||||
// Set date to unknown if file system date is about the same as indexing time.
|
// Set date to unknown if file system date is about the same as indexing time.
|
||||||
if m.TakenSrc == SrcAuto && m.TakenAt.After(m.CreatedAt.Add(-24*time.Hour)) {
|
if m.TakenSrc == SrcAuto && m.TakenAt.After(m.CreatedAt.Add(-24*time.Hour)) {
|
||||||
m.PhotoYear = YearUnknown
|
m.PhotoYear = UnknownYear
|
||||||
m.PhotoMonth = MonthUnknown
|
m.PhotoMonth = UnknownMonth
|
||||||
m.PhotoDay = DayUnknown
|
m.PhotoDay = UnknownDay
|
||||||
} else if m.TakenSrc != SrcManual {
|
} else if m.TakenSrc != SrcManual {
|
||||||
m.PhotoYear = m.TakenAtLocal.Year()
|
m.PhotoYear = m.TakenAtLocal.Year()
|
||||||
m.PhotoMonth = int(m.TakenAtLocal.Month())
|
m.PhotoMonth = int(m.TakenAtLocal.Month())
|
||||||
|
@@ -1232,7 +1232,7 @@ func TestPhoto_UpdateDateFields(t *testing.T) {
|
|||||||
t.Run("set to unknown", func(t *testing.T) {
|
t.Run("set to unknown", func(t *testing.T) {
|
||||||
photo := &Photo{TakenAt: time.Date(1900, 11, 11, 9, 7, 18, 0, time.UTC), TakenSrc: SrcAuto, CreatedAt: time.Date(1900, 11, 11, 5, 7, 18, 0, time.UTC)}
|
photo := &Photo{TakenAt: time.Date(1900, 11, 11, 9, 7, 18, 0, time.UTC), TakenSrc: SrcAuto, CreatedAt: time.Date(1900, 11, 11, 5, 7, 18, 0, time.UTC)}
|
||||||
photo.UpdateDateFields()
|
photo.UpdateDateFields()
|
||||||
assert.Equal(t, YearUnknown, photo.PhotoYear)
|
assert.Equal(t, UnknownYear, photo.PhotoYear)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,36 +6,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Values returns entity values as string map.
|
|
||||||
func Values(m interface{}, omit ...string) (result map[string]interface{}) {
|
|
||||||
skip := func(name string) bool {
|
|
||||||
for _, s := range omit {
|
|
||||||
if name == s {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
result = make(map[string]interface{})
|
|
||||||
|
|
||||||
elem := reflect.ValueOf(m).Elem()
|
|
||||||
relType := elem.Type()
|
|
||||||
|
|
||||||
for i := 0; i < relType.NumField(); i++ {
|
|
||||||
name := relType.Field(i).Name
|
|
||||||
|
|
||||||
if skip(name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
result[name] = elem.Field(i).Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save updates an entity in the database, or inserts if it doesn't exist.
|
// Save updates an entity in the database, or inserts if it doesn't exist.
|
||||||
func Save(m interface{}, primaryKeys ...string) (err error) {
|
func Save(m interface{}, primaryKeys ...string) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -77,7 +47,7 @@ func Update(m interface{}, primaryKeys ...string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update all values except primary keys.
|
// Update all values except primary keys.
|
||||||
if res := UnscopedDb().Model(m).Updates(Values(m, primaryKeys...)); res.Error != nil {
|
if res := UnscopedDb().Model(m).Updates(GetValues(m, primaryKeys...)); res.Error != nil {
|
||||||
return res.Error
|
return res.Error
|
||||||
} else if res.RowsAffected > 1 {
|
} else if res.RowsAffected > 1 {
|
||||||
log.Warnf("update: more than one row affected")
|
log.Warnf("update: more than one row affected")
|
||||||
|
@@ -14,7 +14,6 @@ const (
|
|||||||
SrcMeta = "meta"
|
SrcMeta = "meta"
|
||||||
SrcXmp = "xmp"
|
SrcXmp = "xmp"
|
||||||
SrcYaml = "yaml"
|
SrcYaml = "yaml"
|
||||||
SrcPeople = "people"
|
|
||||||
SrcMarker = "marker"
|
SrcMarker = "marker"
|
||||||
SrcImage = classify.SrcImage
|
SrcImage = classify.SrcImage
|
||||||
SrcKeyword = classify.SrcKeyword
|
SrcKeyword = classify.SrcKeyword
|
||||||
@@ -29,7 +28,6 @@ var SrcPriority = Priorities{
|
|||||||
SrcName: 4,
|
SrcName: 4,
|
||||||
SrcYaml: 8,
|
SrcYaml: 8,
|
||||||
SrcLocation: 8,
|
SrcLocation: 8,
|
||||||
SrcPeople: 8,
|
|
||||||
SrcMarker: 8,
|
SrcMarker: 8,
|
||||||
SrcImage: 8,
|
SrcImage: 8,
|
||||||
SrcKeyword: 16,
|
SrcKeyword: 16,
|
||||||
|
227
internal/entity/subject.go
Normal file
227
internal/entity/subject.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gosimple/slug"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SubjectPerson = "person"
|
||||||
|
)
|
||||||
|
|
||||||
|
var subjectMutex = sync.Mutex{}
|
||||||
|
|
||||||
|
type Subjects []Subject
|
||||||
|
|
||||||
|
// Subject represents a named photo subject, typically a person.
|
||||||
|
type Subject struct {
|
||||||
|
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
|
||||||
|
SubjectUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
|
||||||
|
SubjectType string `gorm:"type:VARBINARY(8);" json:"Type" yaml:"Type"`
|
||||||
|
SubjectSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src"`
|
||||||
|
SubjectSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"-"`
|
||||||
|
SubjectName string `gorm:"type:VARCHAR(255);index;" json:"Name" yaml:"Name"`
|
||||||
|
SubjectDescription string `gorm:"type:TEXT;" json:"Description" yaml:"Description,omitempty"`
|
||||||
|
SubjectNotes string `gorm:"type:TEXT;" json:"Notes,omitempty" yaml:"Notes,omitempty"`
|
||||||
|
SubjectJSON []byte `gorm:"type:MEDIUMBLOB;" json:"JSON,omitempty" yaml:"JSON,omitempty"`
|
||||||
|
Favorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
|
||||||
|
Hidden bool `json:"Hidden" yaml:"Hidden,omitempty"`
|
||||||
|
Private bool `json:"Private" yaml:"Private,omitempty"`
|
||||||
|
PhotoCount int `gorm:"default:0" json:"PhotoCount" yaml:"-"`
|
||||||
|
BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"`
|
||||||
|
BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,omitempty"`
|
||||||
|
BirthDay int `json:"BirthDay" yaml:"BirthDay,omitempty"`
|
||||||
|
PassedAway *time.Time `json:"PassedAway,omitempty" yaml:"PassedAway,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||||
|
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||||
|
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnknownPerson can be used as a placeholder for unknown people.
|
||||||
|
var UnknownPerson = Subject{
|
||||||
|
ID: 1,
|
||||||
|
SubjectUID: "j000000000000001",
|
||||||
|
SubjectSlug: "unknown",
|
||||||
|
SubjectName: "Unknown",
|
||||||
|
SubjectType: SubjectPerson,
|
||||||
|
SubjectSrc: SrcDefault,
|
||||||
|
Favorite: false,
|
||||||
|
BirthYear: UnknownYear,
|
||||||
|
BirthMonth: UnknownMonth,
|
||||||
|
BirthDay: UnknownDay,
|
||||||
|
PhotoCount: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUnknownPerson initializes the database with a placeholder for unknown people if not exists.
|
||||||
|
func CreateUnknownPerson() {
|
||||||
|
FirstOrCreateSubject(&UnknownPerson)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the entity database table name.
|
||||||
|
func (Subject) TableName() string {
|
||||||
|
return "subjects_dev3"
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||||
|
func (m *Subject) BeforeCreate(scope *gorm.Scope) error {
|
||||||
|
if rnd.IsUID(m.SubjectUID, 'j') {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return scope.SetColumn("SubjectUID", rnd.PPID('j'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubject returns a new entity.
|
||||||
|
func NewSubject(name, subjectType, subjectSrc string) *Subject {
|
||||||
|
if subjectType == "" {
|
||||||
|
subjectType = SubjectPerson
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
name = UnknownName
|
||||||
|
}
|
||||||
|
|
||||||
|
subjectName := txt.Title(txt.Clip(name, txt.ClipDefault))
|
||||||
|
subjectSlug := slug.Make(txt.Clip(name, txt.ClipSlug))
|
||||||
|
|
||||||
|
result := &Subject{
|
||||||
|
SubjectSlug: subjectSlug,
|
||||||
|
SubjectName: subjectName,
|
||||||
|
SubjectType: subjectType,
|
||||||
|
SubjectSrc: subjectSrc,
|
||||||
|
BirthYear: UnknownYear,
|
||||||
|
BirthMonth: UnknownMonth,
|
||||||
|
BirthDay: UnknownDay,
|
||||||
|
PhotoCount: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updates the existing or inserts a new entity.
|
||||||
|
func (m *Subject) Save() error {
|
||||||
|
subjectMutex.Lock()
|
||||||
|
defer subjectMutex.Unlock()
|
||||||
|
|
||||||
|
return Db().Save(m).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create inserts the entity to the database.
|
||||||
|
func (m *Subject) Create() error {
|
||||||
|
subjectMutex.Lock()
|
||||||
|
defer subjectMutex.Unlock()
|
||||||
|
|
||||||
|
return Db().Create(m).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the entity from the database.
|
||||||
|
func (m *Subject) Delete() error {
|
||||||
|
return Db().Delete(m).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleted returns true if the entity is deleted.
|
||||||
|
func (m *Subject) Deleted() bool {
|
||||||
|
return m.DeletedAt != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the entity in the database.
|
||||||
|
func (m *Subject) Restore() error {
|
||||||
|
if m.Deleted() {
|
||||||
|
return UnscopedDb().Model(m).Update("DeletedAt", nil).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an entity value in the database.
|
||||||
|
func (m *Subject) Update(attr string, value interface{}) error {
|
||||||
|
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates multiple values in the database.
|
||||||
|
func (m *Subject) Updates(values interface{}) error {
|
||||||
|
return UnscopedDb().Model(m).Updates(values).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstOrCreateSubject returns the existing subject, inserts a new subject or nil in case of errors.
|
||||||
|
func FirstOrCreateSubject(m *Subject) *Subject {
|
||||||
|
result := Subject{}
|
||||||
|
|
||||||
|
if err := UnscopedDb().Where("subject_type = ? AND subject_slug = ?", m.SubjectType, m.SubjectSlug).First(&result).Error; err == nil {
|
||||||
|
return &result
|
||||||
|
} else if createErr := m.Create(); createErr == nil {
|
||||||
|
if !m.Hidden && m.SubjectType == SubjectPerson {
|
||||||
|
event.EntitiesCreated("people", []*Subject{m})
|
||||||
|
|
||||||
|
event.Publish("count.people", event.Data{
|
||||||
|
"count": 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
} else if err := UnscopedDb().Where("subject_type = ? AND subject_slug = ?", m.SubjectType, m.SubjectSlug).First(&result).Error; err == nil {
|
||||||
|
return &result
|
||||||
|
} else {
|
||||||
|
log.Errorf("subject: %s (find or create %s)", createErr, m.SubjectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSubject returns an existing row if exists.
|
||||||
|
func FindSubject(s string) *Subject {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := Subject{}
|
||||||
|
|
||||||
|
db := Db()
|
||||||
|
|
||||||
|
if rnd.IsPPID(s, 'j') {
|
||||||
|
db = db.Where("subject_uid = ?", s)
|
||||||
|
} else {
|
||||||
|
db = db.Where("subject_slug = ?", slug.Make(txt.Clip(s, txt.ClipSlug)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.First(&result).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetName changes the subject's name.
|
||||||
|
func (m *Subject) SetName(name string) error {
|
||||||
|
newName := txt.Clip(name, txt.ClipDefault)
|
||||||
|
|
||||||
|
if newName == "" {
|
||||||
|
return fmt.Errorf("subject: name must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SubjectName = txt.Title(newName)
|
||||||
|
m.SubjectSlug = slug.Make(txt.Clip(name, txt.ClipSlug))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateName changes and saves the subject's name in the index.
|
||||||
|
func (m *Subject) UpdateName(name string) error {
|
||||||
|
if err := m.SetName(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Updates(Values{"SubjectName": m.SubjectName, "SubjectSlug": m.SubjectSlug})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links returns all share links for this entity.
|
||||||
|
func (m *Subject) Links() Links {
|
||||||
|
return FindLinks("", m.SubjectUID)
|
||||||
|
}
|
50
internal/entity/subject_fixtures.go
Normal file
50
internal/entity/subject_fixtures.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
type SubjectMap map[string]Subject
|
||||||
|
|
||||||
|
func (m SubjectMap) Get(name string) Subject {
|
||||||
|
if result, ok := m[name]; ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return Subject{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m SubjectMap) Pointer(name string) *Subject {
|
||||||
|
if result, ok := m[name]; ok {
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Subject{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var SubjectFixtures = SubjectMap{
|
||||||
|
"known_face": Subject{
|
||||||
|
ID: 2,
|
||||||
|
SubjectUID: "jqu0xs11qekk9jx8",
|
||||||
|
SubjectSlug: "john-doe",
|
||||||
|
SubjectName: "John Doe",
|
||||||
|
SubjectSrc: "xmp",
|
||||||
|
Favorite: true,
|
||||||
|
Private: false,
|
||||||
|
Hidden: false,
|
||||||
|
SubjectDescription: "Subject Description",
|
||||||
|
SubjectNotes: "Short Note",
|
||||||
|
SubjectJSON: []byte(""),
|
||||||
|
PhotoCount: 1,
|
||||||
|
BirthYear: 2000,
|
||||||
|
BirthMonth: 5,
|
||||||
|
BirthDay: 22,
|
||||||
|
PassedAway: nil,
|
||||||
|
CreatedAt: Timestamp(),
|
||||||
|
UpdatedAt: Timestamp(),
|
||||||
|
DeletedAt: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSubjectFixtures inserts known entities into the database for testing.
|
||||||
|
func CreateSubjectFixtures() {
|
||||||
|
for _, entity := range SubjectFixtures {
|
||||||
|
Db().Create(&entity)
|
||||||
|
}
|
||||||
|
}
|
162
internal/entity/subject_test.go
Normal file
162
internal/entity/subject_test.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubject_TableName(t *testing.T) {
|
||||||
|
m := &Subject{}
|
||||||
|
assert.Contains(t, m.TableName(), "subjects")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPerson(t *testing.T) {
|
||||||
|
t.Run("Jens_Mander", func(t *testing.T) {
|
||||||
|
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
||||||
|
assert.Equal(t, "Jens Mander", m.SubjectName)
|
||||||
|
assert.Equal(t, "jens-mander", m.SubjectSlug)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubject_SetName(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
||||||
|
|
||||||
|
assert.Equal(t, "Jens Mander", m.SubjectName)
|
||||||
|
assert.Equal(t, "jens-mander", m.SubjectSlug)
|
||||||
|
|
||||||
|
if err := m.SetName("Foo McBar"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "Foo McBar", m.SubjectName)
|
||||||
|
assert.Equal(t, "foo-mcbar", m.SubjectSlug)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFirstOrCreatePerson(t *testing.T) {
|
||||||
|
m := NewSubject("Create Me", SubjectPerson, SrcAuto)
|
||||||
|
result := FirstOrCreateSubject(m)
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
t.Fatal("result should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "Create Me", m.SubjectName)
|
||||||
|
assert.Equal(t, "create-me", m.SubjectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubject_Save(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
m := NewSubject("Save Me", SubjectPerson, SrcAuto)
|
||||||
|
initialDate := m.UpdatedAt
|
||||||
|
err := m.Save()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterDate := m.UpdatedAt
|
||||||
|
|
||||||
|
assert.True(t, afterDate.After(initialDate))
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubject_Delete(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
||||||
|
err := m.Save()
|
||||||
|
assert.False(t, m.Deleted())
|
||||||
|
|
||||||
|
var subj Subjects
|
||||||
|
|
||||||
|
if err := Db().Where("subject_name = ?", m.SubjectName).Find(&subj).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, subj, 1)
|
||||||
|
|
||||||
|
err = m.Delete()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Db().Where("subject_name = ?", m.SubjectName).Find(&subj).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, subj, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubject_Restore(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
var deleteTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
m := &Subject{DeletedAt: &deleteTime, SubjectName: "ToBeRestored"}
|
||||||
|
err := m.Save()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.True(t, m.Deleted())
|
||||||
|
|
||||||
|
err = m.Restore()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.False(t, m.Deleted())
|
||||||
|
})
|
||||||
|
t.Run("subject not deleted", func(t *testing.T) {
|
||||||
|
m := &Subject{DeletedAt: nil, SubjectName: "NotDeleted1234"}
|
||||||
|
err := m.Restore()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.False(t, m.Deleted())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindSubject(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
m := NewSubject("Find Me", SubjectPerson, SrcAuto)
|
||||||
|
err := m.Save()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
found := FindSubject("find me")
|
||||||
|
assert.Equal(t, "Find Me", found.SubjectName)
|
||||||
|
})
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
r := FindSubject("XXX")
|
||||||
|
assert.Nil(t, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubject_Links(t *testing.T) {
|
||||||
|
t.Run("no-result", func(t *testing.T) {
|
||||||
|
m := UnknownPerson
|
||||||
|
links := m.Links()
|
||||||
|
assert.Empty(t, links)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubject_Update(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
m := NewSubject("Update Me", SubjectPerson, SrcAuto)
|
||||||
|
|
||||||
|
if err := m.Save(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Update("SubjectName", "Updated Name"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, "Updated Name", m.SubjectName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
@@ -1,4 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
// Val is a shortcut for map[string]interface{}
|
|
||||||
type Val map[string]interface{}
|
|
36
internal/entity/values.go
Normal file
36
internal/entity/values.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
// Values is a shortcut for map[string]interface{}
|
||||||
|
type Values map[string]interface{}
|
||||||
|
|
||||||
|
// GetValues extracts entity Values.
|
||||||
|
func GetValues(m interface{}, omit ...string) (result Values) {
|
||||||
|
skip := func(name string) bool {
|
||||||
|
for _, s := range omit {
|
||||||
|
if name == s {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
result = make(map[string]interface{})
|
||||||
|
|
||||||
|
elem := reflect.ValueOf(m).Elem()
|
||||||
|
relType := elem.Type()
|
||||||
|
|
||||||
|
for i := 0; i < relType.NumField(); i++ {
|
||||||
|
name := relType.Field(i).Name
|
||||||
|
|
||||||
|
if skip(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result[name] = elem.Field(i).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
@@ -4,13 +4,13 @@ import "github.com/ulule/deepcopier"
|
|||||||
|
|
||||||
// Marker represents an image marker edit form.
|
// Marker represents an image marker edit form.
|
||||||
type Marker struct {
|
type Marker struct {
|
||||||
RefUID string `json:"RefUID"`
|
|
||||||
RefSrc string `json:"RefSrc"`
|
|
||||||
MarkerSrc string `json:"Src"`
|
|
||||||
MarkerType string `json:"Type"`
|
MarkerType string `json:"Type"`
|
||||||
|
SubjectUID string `json:"SubjectUID"`
|
||||||
|
SubjectSrc string `json:"SubjectSrc"`
|
||||||
|
MarkerName string `json:"Name"`
|
||||||
|
MarkerSrc string `json:"Src"`
|
||||||
MarkerScore int `json:"Score"`
|
MarkerScore int `json:"Score"`
|
||||||
MarkerInvalid bool `json:"Invalid"`
|
MarkerInvalid bool `json:"Invalid"`
|
||||||
MarkerLabel string `json:"Label"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMarker(m interface{}) (f Marker, err error) {
|
func NewMarker(m interface{}) (f Marker, err error) {
|
||||||
|
@@ -9,21 +9,21 @@ import (
|
|||||||
func TestNewMarker(t *testing.T) {
|
func TestNewMarker(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
var m = struct {
|
var m = struct {
|
||||||
RefUID string
|
|
||||||
RefSrc string
|
|
||||||
MarkerSrc string
|
|
||||||
MarkerType string
|
MarkerType string
|
||||||
|
MarkerSrc string
|
||||||
|
MarkerName string
|
||||||
|
SubjectUID string
|
||||||
|
SubjectSrc string
|
||||||
MarkerScore int
|
MarkerScore int
|
||||||
MarkerInvalid bool
|
MarkerInvalid bool
|
||||||
MarkerLabel string
|
|
||||||
}{
|
}{
|
||||||
RefUID: "3h59wvth837b5vyiub35",
|
MarkerType: "face",
|
||||||
RefSrc: "meta",
|
|
||||||
MarkerSrc: "image",
|
MarkerSrc: "image",
|
||||||
MarkerType: "Face",
|
MarkerName: "Foo",
|
||||||
|
SubjectUID: "3h59wvth837b5vyiub35",
|
||||||
|
SubjectSrc: "meta",
|
||||||
MarkerScore: 100,
|
MarkerScore: 100,
|
||||||
MarkerInvalid: true,
|
MarkerInvalid: true,
|
||||||
MarkerLabel: "Foo",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := NewMarker(m)
|
f, err := NewMarker(m)
|
||||||
@@ -32,12 +32,12 @@ func TestNewMarker(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "3h59wvth837b5vyiub35", f.RefUID)
|
assert.Equal(t, "face", f.MarkerType)
|
||||||
assert.Equal(t, "meta", f.RefSrc)
|
|
||||||
assert.Equal(t, "image", f.MarkerSrc)
|
assert.Equal(t, "image", f.MarkerSrc)
|
||||||
assert.Equal(t, "Face", f.MarkerType)
|
assert.Equal(t, "Foo", f.MarkerName)
|
||||||
|
assert.Equal(t, "3h59wvth837b5vyiub35", f.SubjectUID)
|
||||||
|
assert.Equal(t, "meta", f.SubjectSrc)
|
||||||
assert.Equal(t, 100, f.MarkerScore)
|
assert.Equal(t, 100, f.MarkerScore)
|
||||||
assert.Equal(t, true, f.MarkerInvalid)
|
assert.Equal(t, true, f.MarkerInvalid)
|
||||||
assert.Equal(t, "Foo", f.MarkerLabel)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -17,10 +17,6 @@ import (
|
|||||||
"github.com/mpraski/clusters"
|
"github.com/mpraski/clusters"
|
||||||
)
|
)
|
||||||
|
|
||||||
const FaceSampleThreshold = 25
|
|
||||||
const FaceClusterDistance = 0.66
|
|
||||||
const FaceClusterSamples = 3
|
|
||||||
|
|
||||||
// Faces represents a worker for face clustering and matching.
|
// Faces represents a worker for face clustering and matching.
|
||||||
type Faces struct {
|
type Faces struct {
|
||||||
conf *config.Config
|
conf *config.Config
|
||||||
@@ -98,7 +94,7 @@ func (w *Faces) Analyze() (err error) {
|
|||||||
min := -1.0
|
min := -1.0
|
||||||
max := -1.0
|
max := -1.0
|
||||||
|
|
||||||
if k, ok := dist[f1.PersonUID]; ok {
|
if k, ok := dist[f1.SubjectUID]; ok {
|
||||||
min = k[0]
|
min = k[0]
|
||||||
max = k[1]
|
max = k[1]
|
||||||
}
|
}
|
||||||
@@ -110,7 +106,7 @@ func (w *Faces) Analyze() (err error) {
|
|||||||
|
|
||||||
f2 := faces[j]
|
f2 := faces[j]
|
||||||
|
|
||||||
if f1.PersonUID != f2.PersonUID {
|
if f1.SubjectUID != f2.SubjectUID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +124,7 @@ func (w *Faces) Analyze() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if max > 0 {
|
if max > 0 {
|
||||||
dist[f1.PersonUID] = []float64{min, max}
|
dist[f1.SubjectUID] = []float64{min, max}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,8 +134,8 @@ func (w *Faces) Analyze() (err error) {
|
|||||||
log.Infof("faces: %d faces match to the same person", l)
|
log.Infof("faces: %d faces match to the same person", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
for personUID, d := range dist {
|
for subj, d := range dist {
|
||||||
log.Infof("faces: %s Ø min %f, max %f", personUID, d[0], d[1])
|
log.Infof("faces: %s Ø min %f, max %f", subj, d[0], d[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +165,7 @@ func (w *Faces) Disabled() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start face clustering and matching.
|
// Start face clustering and matching.
|
||||||
func (w *Faces) Start() (err error) {
|
func (w *Faces) Start(opt FacesOptions) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = fmt.Errorf("%s (panic)\nstack: %s", r, debug.Stack())
|
err = fmt.Errorf("%s (panic)\nstack: %s", r, debug.Stack())
|
||||||
@@ -188,13 +184,15 @@ func (w *Faces) Start() (err error) {
|
|||||||
defer mutex.MainWorker.Stop()
|
defer mutex.MainWorker.Stop()
|
||||||
|
|
||||||
// Skip clustering if index contains no new face markers.
|
// Skip clustering if index contains no new face markers.
|
||||||
if n := query.CountNewFaceMarkers(); n < 1 {
|
if opt.Force {
|
||||||
|
log.Infof("faces: reindexing")
|
||||||
|
} else if n := query.CountNewFaceMarkers(); n < 1 {
|
||||||
log.Debugf("faces: no new samples")
|
log.Debugf("faces: no new samples")
|
||||||
|
|
||||||
if affected, err := query.MatchMarkersWithPeople(); err != nil {
|
if affected, err := query.MatchMarkersWithSubjects(); err != nil {
|
||||||
log.Errorf("faces: %s (create people from markers)", err)
|
log.Errorf("faces: %s (match markers with subjects)", err)
|
||||||
} else if affected > 0 {
|
} else if affected > 0 {
|
||||||
log.Infof("faces: matched %d markers with people", affected)
|
log.Infof("faces: matched %d markers with subjects", affected)
|
||||||
}
|
}
|
||||||
|
|
||||||
if matched, err := query.MatchKnownFaces(); err != nil {
|
if matched, err := query.MatchKnownFaces(); err != nil {
|
||||||
@@ -257,7 +255,7 @@ func (w *Faces) Start() (err error) {
|
|||||||
} else if err := f.Create(); err == nil {
|
} else if err := f.Create(); err == nil {
|
||||||
added++
|
added++
|
||||||
log.Tracef("faces: added face %s", f.ID)
|
log.Tracef("faces: added face %s", f.ID)
|
||||||
} else if err := f.Updates(entity.Val{"UpdatedAt": entity.Timestamp()}); err != nil {
|
} else if err := f.Updates(entity.Values{"UpdatedAt": entity.Timestamp()}); err != nil {
|
||||||
dbErrors++
|
dbErrors++
|
||||||
log.Errorf("faces: %s", err)
|
log.Errorf("faces: %s", err)
|
||||||
}
|
}
|
||||||
@@ -317,31 +315,31 @@ func (w *Faces) Start() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Already matched?
|
// Already matched?
|
||||||
if marker.RefUID != "" && marker.RefUID == f.PersonUID {
|
if marker.SubjectUID != "" && marker.SubjectUID == f.SubjectUID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create person from marker label?
|
// Create subject from marker label?
|
||||||
if marker.MarkerLabel == "" {
|
if marker.MarkerName == "" {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
} else if p := entity.NewPerson(marker.MarkerLabel, entity.SrcMarker, 1); p == nil {
|
} else if subj := entity.NewSubject(marker.MarkerName, entity.SubjectPerson, entity.SrcMarker); subj == nil {
|
||||||
log.Errorf("faces: person should not be nil - bug?")
|
log.Errorf("faces: subject should not be nil - bug?")
|
||||||
} else if p = entity.FirstOrCreatePerson(p); p == nil {
|
} else if subj = entity.FirstOrCreateSubject(subj); subj == nil {
|
||||||
log.Errorf("faces: failed adding %s", txt.Quote(marker.MarkerLabel))
|
log.Errorf("faces: failed adding subject %s", txt.Quote(marker.MarkerName))
|
||||||
} else {
|
} else {
|
||||||
f.PersonUID = p.PersonUID
|
f.SubjectUID = subj.SubjectUID
|
||||||
entity.Db().Model(&entity.Face{}).Where("id = ? AND person_uid = ''", f.ID).Update("PersonUID", p.PersonUID)
|
entity.Db().Model(&entity.Face{}).Where("id = ? AND subject_uid = ''", f.ID).Update("SubjectUID", subj.SubjectUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing person?
|
// Existing subject?
|
||||||
if f.PersonUID != "" {
|
if f.SubjectUID != "" {
|
||||||
if err := marker.Updates(entity.Val{"RefUID": f.PersonUID, "RefSrc": entity.SrcPeople, "FaceID": ""}); err != nil {
|
if err := marker.Updates(entity.Values{"SubjectUID": f.SubjectUID, "SubjectSrc": entity.SrcAuto, "FaceID": ""}); err != nil {
|
||||||
log.Errorf("faces: %s while updating person uid", err)
|
log.Errorf("faces: %s while updating subject uid of marker %d", err, marker.ID)
|
||||||
} else {
|
} else {
|
||||||
matched++
|
matched++
|
||||||
}
|
}
|
||||||
} else if err := marker.Updates(entity.Val{"FaceID": f.ID}); err != nil {
|
} else if err := marker.Updates(entity.Values{"FaceID": f.ID}); err != nil {
|
||||||
log.Errorf("faces: %s while updating marker face id", err)
|
log.Errorf("faces: %s while updating face id of marker %d", err, marker.ID)
|
||||||
} else {
|
} else {
|
||||||
unknown++
|
unknown++
|
||||||
}
|
}
|
||||||
|
5
internal/photoprism/faces_options.go
Normal file
5
internal/photoprism/faces_options.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package photoprism
|
||||||
|
|
||||||
|
type FacesOptions struct {
|
||||||
|
Force bool
|
||||||
|
}
|
@@ -10,7 +10,12 @@ func TestFaces_Start(t *testing.T) {
|
|||||||
conf := config.TestConfig()
|
conf := config.TestConfig()
|
||||||
|
|
||||||
m := NewFaces(conf)
|
m := NewFaces(conf)
|
||||||
err := m.Start()
|
|
||||||
|
opt := FacesOptions{
|
||||||
|
Force: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := m.Start(opt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@@ -61,7 +61,7 @@ func (ind *Index) Cancel() {
|
|||||||
mutex.MainWorker.Cancel()
|
mutex.MainWorker.Cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start indexes media files in the originals directory.
|
// Start indexes media files in the "originals" folder.
|
||||||
func (ind *Index) Start(opt IndexOptions) fs.Done {
|
func (ind *Index) Start(opt IndexOptions) fs.Done {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
63
internal/photoprism/index_classify.go
Normal file
63
internal/photoprism/index_classify.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package photoprism
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/classify"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// classifyImage classifies a JPEG image and returns matching labels.
|
||||||
|
func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
var thumbs []string
|
||||||
|
|
||||||
|
if jpeg.AspectRatio() == 1 {
|
||||||
|
thumbs = []string{"tile_224"}
|
||||||
|
} else {
|
||||||
|
thumbs = []string{"tile_224", "left_224", "right_224"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var labels classify.Labels
|
||||||
|
|
||||||
|
for _, thumb := range thumbs {
|
||||||
|
filename, err := jpeg.Thumbnail(Config().ThumbPath(), thumb)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
imageLabels, err := ind.tensorFlow.File(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = append(labels, imageLabels...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by priority and uncertainty
|
||||||
|
sort.Sort(labels)
|
||||||
|
|
||||||
|
var confidence int
|
||||||
|
|
||||||
|
for _, label := range labels {
|
||||||
|
if confidence == 0 {
|
||||||
|
confidence = 100 - label.Uncertainty
|
||||||
|
}
|
||||||
|
|
||||||
|
if (100 - label.Uncertainty) > (confidence / 3) {
|
||||||
|
results = append(results, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(labels) > 0 {
|
||||||
|
log.Infof("index: found %d matching labels for %s [%s]", len(labels), txt.Quote(jpeg.BaseName()), time.Since(start))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// detectFaces detects faces in a JPEG image and returns them.
|
// detectFaces extracts faces from a JPEG image and returns them.
|
||||||
func (ind *Index) detectFaces(jpeg *MediaFile) face.Faces {
|
func (ind *Index) detectFaces(jpeg *MediaFile) face.Faces {
|
||||||
if jpeg == nil {
|
if jpeg == nil {
|
||||||
return face.Faces{}
|
return face.Faces{}
|
||||||
@@ -43,7 +43,7 @@ func (ind *Index) detectFaces(jpeg *MediaFile) face.Faces {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(faces) > 0 {
|
if len(faces) > 0 {
|
||||||
log.Infof("index: %d faces in %s [%s]", len(faces), txt.Quote(jpeg.BaseName()), time.Since(start))
|
log.Infof("index: extracted %d faces from %s [%s]", len(faces), txt.Quote(jpeg.BaseName()), time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
return faces
|
return faces
|
||||||
|
@@ -4,7 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -773,57 +772,3 @@ func (ind *Index) NSFW(jpeg *MediaFile) bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// classifyImage classifies a JPEG image and returns matching labels.
|
|
||||||
func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
var thumbs []string
|
|
||||||
|
|
||||||
if jpeg.AspectRatio() == 1 {
|
|
||||||
thumbs = []string{"tile_224"}
|
|
||||||
} else {
|
|
||||||
thumbs = []string{"tile_224", "left_224", "right_224"}
|
|
||||||
}
|
|
||||||
|
|
||||||
var labels classify.Labels
|
|
||||||
|
|
||||||
for _, thumb := range thumbs {
|
|
||||||
filename, err := jpeg.Thumbnail(Config().ThumbPath(), thumb)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
imageLabels, err := ind.tensorFlow.File(filename)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
labels = append(labels, imageLabels...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by priority and uncertainty
|
|
||||||
sort.Sort(labels)
|
|
||||||
|
|
||||||
var confidence int
|
|
||||||
|
|
||||||
for _, label := range labels {
|
|
||||||
if confidence == 0 {
|
|
||||||
confidence = 100 - label.Uncertainty
|
|
||||||
}
|
|
||||||
|
|
||||||
if (100 - label.Uncertainty) > (confidence / 3) {
|
|
||||||
results = append(results, label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(labels) > 0 {
|
|
||||||
log.Infof("index: %d matching labels for %s [%s]", len(labels), txt.Quote(jpeg.BaseName()), time.Since(start))
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
@@ -167,15 +167,15 @@ func AlbumSearch(f form.AlbumSearch) (results AlbumResults, err error) {
|
|||||||
s = s.Where("albums.album_favorite = 1")
|
s = s.Where("albums.album_favorite = 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.YearUnknown {
|
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.UnknownYear {
|
||||||
s = s.Where("albums.album_year = ?", f.Year)
|
s = s.Where("albums.album_year = ?", f.Year)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.MonthUnknown {
|
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.UnknownMonth {
|
||||||
s = s.Where("albums.album_month = ?", f.Month)
|
s = s.Where("albums.album_month = ?", f.Month)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.DayUnknown {
|
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.UnknownDay {
|
||||||
s = s.Where("albums.album_day = ?", f.Day)
|
s = s.Where("albums.album_day = ?", f.Day)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ func Faces(knownOnly bool) (result entity.Faces, err error) {
|
|||||||
Order("id")
|
Order("id")
|
||||||
|
|
||||||
if knownOnly {
|
if knownOnly {
|
||||||
stmt = stmt.Where("person_uid <> ''")
|
stmt = stmt.Where("subject_uid <> ''")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = stmt.Find(&result).Error
|
err = stmt.Find(&result).Error
|
||||||
@@ -30,7 +30,7 @@ func MatchKnownFaces() (affected int64, err error) {
|
|||||||
for _, match := range faces {
|
for _, match := range faces {
|
||||||
if res := Db().Model(&entity.Marker{}).
|
if res := Db().Model(&entity.Marker{}).
|
||||||
Where("face_id = ?", match.ID).
|
Where("face_id = ?", match.ID).
|
||||||
Updates(entity.Val{"RefUID": match.PersonUID, "RefSrc": entity.SrcPeople, "FaceID": ""}); res.Error != nil {
|
Updates(entity.Values{"SubjectUID": match.SubjectUID, "SubjectSrc": entity.SrcAuto, "FaceID": ""}); res.Error != nil {
|
||||||
return affected, err
|
return affected, err
|
||||||
} else if res.RowsAffected > 0 {
|
} else if res.RowsAffected > 0 {
|
||||||
affected += res.RowsAffected
|
affected += res.RowsAffected
|
||||||
@@ -44,7 +44,7 @@ func MatchKnownFaces() (affected int64, err error) {
|
|||||||
func PurgeAnonymousFaces() error {
|
func PurgeAnonymousFaces() error {
|
||||||
return UnscopedDb().Delete(
|
return UnscopedDb().Delete(
|
||||||
entity.Face{},
|
entity.Face{},
|
||||||
"id <> ? AND person_uid = '' AND updated_at < ?", entity.UnknownFace.ID, entity.Yesterday()).Error
|
"id <> ? AND subject_uid = '' AND updated_at < ?", entity.UnknownFace.ID, entity.Yesterday()).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetFaces removes all face clusters from the index.
|
// ResetFaces removes all face clusters from the index.
|
||||||
|
@@ -28,7 +28,7 @@ func TestMatchKnownFaces(t *testing.T) {
|
|||||||
if m, err := MarkerByID(faceFixtureId); err != nil {
|
if m, err := MarkerByID(faceFixtureId); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
assert.Empty(t, m.RefUID)
|
assert.Empty(t, m.SubjectUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
affected, err := MatchKnownFaces()
|
affected, err := MatchKnownFaces()
|
||||||
@@ -42,7 +42,7 @@ func TestMatchKnownFaces(t *testing.T) {
|
|||||||
if m, err := MarkerByID(faceFixtureId); err != nil {
|
if m, err := MarkerByID(faceFixtureId); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "rqu0xs11qekk9jx8", m.RefUID)
|
assert.Equal(t, "rqu0xs11qekk9jx8", m.SubjectUID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -91,15 +91,15 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||||||
s = s.Where("photos.lens_id = ?", f.Lens)
|
s = s.Where("photos.lens_id = ?", f.Lens)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.YearUnknown {
|
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.UnknownYear {
|
||||||
s = s.Where("photos.photo_year = ?", f.Year)
|
s = s.Where("photos.photo_year = ?", f.Year)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.MonthUnknown {
|
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.UnknownMonth {
|
||||||
s = s.Where("photos.photo_month = ?", f.Month)
|
s = s.Where("photos.photo_month = ?", f.Month)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.DayUnknown {
|
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.UnknownDay {
|
||||||
s = s.Where("photos.photo_day = ?", f.Day)
|
s = s.Where("photos.photo_day = ?", f.Day)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,7 +28,7 @@ func Markers(limit, offset int, markerType string, embeddings, unmatched bool) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if unmatched {
|
if unmatched {
|
||||||
db = db.Where("ref_uid = ''")
|
db = db.Where("subject_uid = ''")
|
||||||
}
|
}
|
||||||
|
|
||||||
db = db.Order("id").Limit(limit).Offset(offset)
|
db = db.Order("id").Limit(limit).Offset(offset)
|
||||||
@@ -67,15 +67,15 @@ func Embeddings(single bool) (result entity.Embeddings, err error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchMarkersWithPeople creates and assigns a person to labeled face markers as needed.
|
// MatchMarkersWithSubjects automatically creates and assigns subjects to markers.
|
||||||
func MatchMarkersWithPeople() (affected int, err error) {
|
func MatchMarkersWithSubjects() (affected int, err error) {
|
||||||
var markers entity.Markers
|
var markers entity.Markers
|
||||||
|
|
||||||
if err := Db().
|
if err := Db().
|
||||||
Where("face_id <> '' AND ref_uid = '' AND ref_src = ''").
|
Where("face_id <> '' AND subject_uid = '' AND subject_src = ''").
|
||||||
Where("marker_invalid = 0 AND marker_type = ?", entity.MarkerFace).
|
Where("marker_invalid = 0 AND marker_type = ?", entity.MarkerFace).
|
||||||
Where("marker_label <> ''").
|
Where("marker_name <> ''").
|
||||||
Order("marker_label").
|
Order("marker_name").
|
||||||
Find(&markers).Error; err != nil {
|
Find(&markers).Error; err != nil {
|
||||||
return affected, err
|
return affected, err
|
||||||
} else if len(markers) == 0 {
|
} else if len(markers) == 0 {
|
||||||
@@ -85,13 +85,13 @@ func MatchMarkersWithPeople() (affected int, err error) {
|
|||||||
for _, m := range markers {
|
for _, m := range markers {
|
||||||
faceId := m.FaceID
|
faceId := m.FaceID
|
||||||
|
|
||||||
if p := entity.NewPerson(m.MarkerLabel, entity.SrcMarker, 1); p == nil {
|
if subj := entity.NewSubject(m.MarkerName, entity.SubjectPerson, entity.SrcMarker); subj == nil {
|
||||||
log.Errorf("faces: person should not be nil - bug?")
|
log.Errorf("faces: subject should not be nil - bug?")
|
||||||
} else if p = entity.FirstOrCreatePerson(p); p == nil {
|
} else if subj = entity.FirstOrCreateSubject(subj); subj == nil {
|
||||||
log.Errorf("faces: failed adding person %s for marker %d", txt.Quote(m.MarkerLabel), m.ID)
|
log.Errorf("faces: failed adding subject %s for marker %d", txt.Quote(m.MarkerName), m.ID)
|
||||||
} else if err := m.Updates(entity.Val{"RefUID": p.PersonUID, "RefSrc": entity.SrcPeople, "FaceID": ""}); err != nil {
|
} else if err := m.Updates(entity.Values{"SubjectUID": subj.SubjectUID, "SubjectSrc": entity.SrcAuto, "FaceID": ""}); err != nil {
|
||||||
return affected, err
|
return affected, err
|
||||||
} else if err := Db().Model(&entity.Face{}).Where("id = ? AND person_uid = ''", faceId).Update("PersonUID", p.PersonUID).Error; err != nil {
|
} else if err := Db().Model(&entity.Face{}).Where("id = ? AND subject_uid = ''", faceId).Update("SubjectUID", subj.SubjectUID).Error; err != nil {
|
||||||
return affected, err
|
return affected, err
|
||||||
} else {
|
} else {
|
||||||
affected++
|
affected++
|
||||||
@@ -103,7 +103,7 @@ func MatchMarkersWithPeople() (affected int, err error) {
|
|||||||
|
|
||||||
// ResetFaceMarkerMatches removes people and face matches from face markers.
|
// ResetFaceMarkerMatches removes people and face matches from face markers.
|
||||||
func ResetFaceMarkerMatches() error {
|
func ResetFaceMarkerMatches() error {
|
||||||
v := entity.Val{"face_id": "", "ref_uid": "", "ref_src": ""}
|
v := entity.Values{"subject_uid": "", "subject_src": "", "face_id": ""}
|
||||||
|
|
||||||
return Db().Model(&entity.Marker{}).Where("marker_type = ?", entity.MarkerFace).UpdateColumns(v).Error
|
return Db().Model(&entity.Marker{}).Where("marker_type = ?", entity.MarkerFace).UpdateColumns(v).Error
|
||||||
}
|
}
|
||||||
|
@@ -35,8 +35,8 @@ func TestEmbeddings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatchMarkersWithPeople(t *testing.T) {
|
func TestMatchMarkersWithSubjects(t *testing.T) {
|
||||||
affected, err := MatchMarkersWithPeople()
|
affected, err := MatchMarkersWithSubjects()
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.GreaterOrEqual(t, affected, 1)
|
assert.GreaterOrEqual(t, affected, 1)
|
||||||
|
@@ -200,15 +200,15 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||||||
s = s.Where("photos.lens_id = ?", f.Lens)
|
s = s.Where("photos.lens_id = ?", f.Lens)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.YearUnknown {
|
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.UnknownYear {
|
||||||
s = s.Where("photos.photo_year = ?", f.Year)
|
s = s.Where("photos.photo_year = ?", f.Year)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.MonthUnknown {
|
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.UnknownMonth {
|
||||||
s = s.Where("photos.photo_month = ?", f.Month)
|
s = s.Where("photos.photo_month = ?", f.Month)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.DayUnknown {
|
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.UnknownDay {
|
||||||
s = s.Where("photos.photo_day = ?", f.Day)
|
s = s.Where("photos.photo_day = ?", f.Day)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,8 +4,8 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
// People returns people from the index.
|
// Subjects returns subjects from the index.
|
||||||
func People(limit, offset int) (result entity.People, err error) {
|
func Subjects(limit, offset int) (result entity.Subjects, err error) {
|
||||||
stmt := Db()
|
stmt := Db()
|
||||||
|
|
||||||
stmt = stmt.Order("id").Limit(limit).Offset(offset)
|
stmt = stmt.Order("id").Limit(limit).Offset(offset)
|
@@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPeople(t *testing.T) {
|
func TestSubjects(t *testing.T) {
|
||||||
results, err := People(3, 0)
|
results, err := Subjects(3, 0)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -18,6 +18,6 @@ func TestPeople(t *testing.T) {
|
|||||||
assert.GreaterOrEqual(t, len(results), 1)
|
assert.GreaterOrEqual(t, len(results), 1)
|
||||||
|
|
||||||
for _, val := range results {
|
for _, val := range results {
|
||||||
assert.IsType(t, entity.Person{}, val)
|
assert.IsType(t, entity.Subject{}, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -128,7 +128,7 @@ func (m *Meta) Start(delay time.Duration) (err error) {
|
|||||||
log.Errorf("faces: failed creating worker")
|
log.Errorf("faces: failed creating worker")
|
||||||
} else if w.Disabled() {
|
} else if w.Disabled() {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
} else if err := w.Start(); err != nil {
|
} else if err := w.Start(photoprism.FacesOptions{}); err != nil {
|
||||||
log.Warnf("faces: %s", err)
|
log.Warnf("faces: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user