People: Improve Facial Recognition Accuracy, Command, and UI #22
Work in progress. Performed refactoring along the way.
This commit is contained in:
parent
9c99c35db1
commit
a974b3a7ea
|
@ -31,6 +31,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/commands"
|
"github.com/photoprism/photoprism/internal/commands"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
@ -44,6 +45,7 @@ var log = event.Log
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "PhotoPrism"
|
app.Name = "PhotoPrism"
|
||||||
|
app.HelpName = filepath.Base(os.Args[0])
|
||||||
app.Usage = "Browse Your Life in Pictures"
|
app.Usage = "Browse Your Life in Pictures"
|
||||||
app.Version = version
|
app.Version = version
|
||||||
app.Copyright = "(c) 2018-2021 Michael Mayer <hello@photoprism.org>"
|
app.Copyright = "(c) 2018-2021 Michael Mayer <hello@photoprism.org>"
|
||||||
|
@ -56,7 +58,7 @@ func main() {
|
||||||
commands.IndexCommand,
|
commands.IndexCommand,
|
||||||
commands.ImportCommand,
|
commands.ImportCommand,
|
||||||
commands.MomentsCommand,
|
commands.MomentsCommand,
|
||||||
commands.PeopleCommand,
|
commands.FacesCommand,
|
||||||
commands.OptimizeCommand,
|
commands.OptimizeCommand,
|
||||||
commands.PurgeCommand,
|
commands.PurgeCommand,
|
||||||
commands.CleanUpCommand,
|
commands.CleanUpCommand,
|
||||||
|
|
148
frontend/package-lock.json
generated
148
frontend/package-lock.json
generated
|
@ -1670,9 +1670,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||||
"version": "13.10.0",
|
"version": "13.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz",
|
||||||
"integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
|
"integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^0.20.2"
|
"type-fest": "^0.20.2"
|
||||||
},
|
},
|
||||||
|
@ -1895,9 +1895,9 @@
|
||||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
|
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "16.4.14",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz",
|
||||||
"integrity": "sha512-GZpnVRNtv7sHDXIFncsERt+qvj4rzAgRQtnvzk3Z7OVNtThD2dHXYCMDNc80D5mv4JE278qo8biZCwcmkbdpqw=="
|
"integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
@ -2758,12 +2758,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios-mock-adapter": {
|
"node_modules/axios-mock-adapter": {
|
||||||
"version": "1.19.0",
|
"version": "1.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.20.0.tgz",
|
||||||
"integrity": "sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==",
|
"integrity": "sha512-shZRhTjLP0WWdcvHKf3rH3iW9deb3UdKbdnKUoHmmsnBhVXN3sjPJM6ZvQ2r/ywgvBVQrMnjrSyQab60G1sr2w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"is-buffer": "^2.0.3"
|
"is-blob": "^2.1.0",
|
||||||
|
"is-buffer": "^2.0.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"axios": ">= 0.9.0"
|
"axios": ">= 0.9.0"
|
||||||
|
@ -3209,9 +3210,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001249",
|
"version": "1.0.30001251",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
|
||||||
"integrity": "sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==",
|
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/browserslist"
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
@ -4740,9 +4741,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.3.802",
|
"version": "1.3.805",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.802.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.805.tgz",
|
||||||
"integrity": "sha512-dXB0SGSypfm3iEDxrb5n/IVKeX4uuTnFHdve7v+yKJqNpEP0D4mjFJ8e1znmSR+OOVlVC+kDO6f2kAkTFXvJBg=="
|
"integrity": "sha512-uUJF59M6pNSRHQaXwdkaNB4BhSQ9lldRdG1qCjlrAFkynPGDc5wPoUcYEQQeQGmKyAWJPvGkYAWmtVrxWmDAkg=="
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
|
@ -5723,9 +5724,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/globals": {
|
"node_modules/eslint/node_modules/globals": {
|
||||||
"version": "13.10.0",
|
"version": "13.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz",
|
||||||
"integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
|
"integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^0.20.2"
|
"type-fest": "^0.20.2"
|
||||||
},
|
},
|
||||||
|
@ -7450,9 +7451,12 @@
|
||||||
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
|
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
|
||||||
},
|
},
|
||||||
"node_modules/is-bigint": {
|
"node_modules/is-bigint": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
|
||||||
"integrity": "sha512-ZU538ajmYJmzysE5yU4Y7uIrPQ2j704u+hXFiIPQExpqzzUbpe5jCPdTfmz7jXRxZdvjY3KZ3ZNenoXQovX+Dg==",
|
"integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-bigints": "^1.0.1"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
|
@ -7469,6 +7473,17 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-blob": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-boolean-object": {
|
"node_modules/is-boolean-object": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
|
||||||
|
@ -12202,9 +12217,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/protocol-buffers-schema": {
|
"node_modules/protocol-buffers-schema": {
|
||||||
"version": "3.5.1",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.2.tgz",
|
||||||
"integrity": "sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw=="
|
"integrity": "sha512-LPzSaBYp/TcbuSlpGwqT5jR9kvJ3Zp5ic2N5c2ybx6XB/lSfEHq2D7ja8AgoxHoMD91wXFALJoXsvshKPuXyew=="
|
||||||
},
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
|
@ -14393,9 +14408,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "6.1.7",
|
"version": "6.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz",
|
||||||
"integrity": "sha512-PBoRkOJU0X3lejJ8GaRCsobjXTgFofRDSPdSUhRSdlwJfifRlQBwGXitDItdGFu0/h0XDMCkig0RN1iT7DBxhA==",
|
"integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chownr": "^2.0.0",
|
"chownr": "^2.0.0",
|
||||||
"fs-minipass": "^2.0.0",
|
"fs-minipass": "^2.0.0",
|
||||||
|
@ -14690,9 +14705,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||||
},
|
},
|
||||||
"node_modules/tsscmp": {
|
"node_modules/tsscmp": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
|
@ -17059,9 +17074,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"globals": {
|
"globals": {
|
||||||
"version": "13.10.0",
|
"version": "13.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz",
|
||||||
"integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
|
"integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"type-fest": "^0.20.2"
|
"type-fest": "^0.20.2"
|
||||||
}
|
}
|
||||||
|
@ -17250,9 +17265,9 @@
|
||||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
|
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "16.4.14",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz",
|
||||||
"integrity": "sha512-GZpnVRNtv7sHDXIFncsERt+qvj4rzAgRQtnvzk3Z7OVNtThD2dHXYCMDNc80D5mv4JE278qo8biZCwcmkbdpqw=="
|
"integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw=="
|
||||||
},
|
},
|
||||||
"@types/parse-json": {
|
"@types/parse-json": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
@ -17931,12 +17946,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"axios-mock-adapter": {
|
"axios-mock-adapter": {
|
||||||
"version": "1.19.0",
|
"version": "1.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.20.0.tgz",
|
||||||
"integrity": "sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==",
|
"integrity": "sha512-shZRhTjLP0WWdcvHKf3rH3iW9deb3UdKbdnKUoHmmsnBhVXN3sjPJM6ZvQ2r/ywgvBVQrMnjrSyQab60G1sr2w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"is-buffer": "^2.0.3"
|
"is-blob": "^2.1.0",
|
||||||
|
"is-buffer": "^2.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-loader": {
|
"babel-loader": {
|
||||||
|
@ -18287,9 +18303,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001249",
|
"version": "1.0.30001251",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
|
||||||
"integrity": "sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw=="
|
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A=="
|
||||||
},
|
},
|
||||||
"chai": {
|
"chai": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
|
@ -19432,9 +19448,9 @@
|
||||||
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
|
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
|
||||||
},
|
},
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.3.802",
|
"version": "1.3.805",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.802.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.805.tgz",
|
||||||
"integrity": "sha512-dXB0SGSypfm3iEDxrb5n/IVKeX4uuTnFHdve7v+yKJqNpEP0D4mjFJ8e1znmSR+OOVlVC+kDO6f2kAkTFXvJBg=="
|
"integrity": "sha512-uUJF59M6pNSRHQaXwdkaNB4BhSQ9lldRdG1qCjlrAFkynPGDc5wPoUcYEQQeQGmKyAWJPvGkYAWmtVrxWmDAkg=="
|
||||||
},
|
},
|
||||||
"emoji-regex": {
|
"emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
|
@ -19739,9 +19755,9 @@
|
||||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"version": "13.10.0",
|
"version": "13.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz",
|
||||||
"integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
|
"integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"type-fest": "^0.20.2"
|
"type-fest": "^0.20.2"
|
||||||
}
|
}
|
||||||
|
@ -21482,9 +21498,12 @@
|
||||||
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
|
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
|
||||||
},
|
},
|
||||||
"is-bigint": {
|
"is-bigint": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
|
||||||
"integrity": "sha512-ZU538ajmYJmzysE5yU4Y7uIrPQ2j704u+hXFiIPQExpqzzUbpe5jCPdTfmz7jXRxZdvjY3KZ3ZNenoXQovX+Dg=="
|
"integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
|
||||||
|
"requires": {
|
||||||
|
"has-bigints": "^1.0.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"is-binary-path": {
|
"is-binary-path": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
@ -21495,6 +21514,11 @@
|
||||||
"binary-extensions": "^1.0.0"
|
"binary-extensions": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"is-blob": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw=="
|
||||||
|
},
|
||||||
"is-boolean-object": {
|
"is-boolean-object": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
|
||||||
|
@ -24917,9 +24941,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"protocol-buffers-schema": {
|
"protocol-buffers-schema": {
|
||||||
"version": "3.5.1",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.2.tgz",
|
||||||
"integrity": "sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw=="
|
"integrity": "sha512-LPzSaBYp/TcbuSlpGwqT5jR9kvJ3Zp5ic2N5c2ybx6XB/lSfEHq2D7ja8AgoxHoMD91wXFALJoXsvshKPuXyew=="
|
||||||
},
|
},
|
||||||
"proxy-addr": {
|
"proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
|
@ -26691,9 +26715,9 @@
|
||||||
"integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw=="
|
"integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw=="
|
||||||
},
|
},
|
||||||
"tar": {
|
"tar": {
|
||||||
"version": "6.1.7",
|
"version": "6.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz",
|
||||||
"integrity": "sha512-PBoRkOJU0X3lejJ8GaRCsobjXTgFofRDSPdSUhRSdlwJfifRlQBwGXitDItdGFu0/h0XDMCkig0RN1iT7DBxhA==",
|
"integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"chownr": "^2.0.0",
|
"chownr": "^2.0.0",
|
||||||
"fs-minipass": "^2.0.0",
|
"fs-minipass": "^2.0.0",
|
||||||
|
@ -26924,9 +26948,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||||
},
|
},
|
||||||
"tsscmp": {
|
"tsscmp": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
|
|
|
@ -29,17 +29,17 @@
|
||||||
|
|
||||||
<v-card-actions class="card-details pa-0">
|
<v-card-actions class="card-details pa-0">
|
||||||
<v-layout v-if="marker.Score < 30" row wrap align-center>
|
<v-layout v-if="marker.Score < 30" row wrap align-center>
|
||||||
<v-flex xs6 class="text-xs-center pa-1">
|
<v-flex xs6 class="text-xs-center pa-0">
|
||||||
<v-btn color="accent lighten-2"
|
<v-btn color="transparent"
|
||||||
small depressed dark block :round="false"
|
large depressed block :round="false"
|
||||||
class="action-archive text-xs-center"
|
class="action-archive text-xs-center"
|
||||||
:title="$gettext('Reject')" @click.stop="reject(marker)">
|
:title="$gettext('Reject')" @click.stop="reject(marker)">
|
||||||
<v-icon dark>clear</v-icon>
|
<v-icon dark>clear</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
<v-flex xs6 class="text-xs-center pa-1">
|
<v-flex xs6 class="text-xs-center pa-0">
|
||||||
<v-btn color="accent lighten-2"
|
<v-btn color="transparent"
|
||||||
small depressed dark block :round="false"
|
large depressed block :round="false"
|
||||||
class="action-approve text-xs-center"
|
class="action-approve text-xs-center"
|
||||||
:title="$gettext('Approve')" @click.stop="confirm(marker)">
|
:title="$gettext('Approve')" @click.stop="confirm(marker)">
|
||||||
<v-icon dark>check</v-icon>
|
<v-icon dark>check</v-icon>
|
||||||
|
@ -47,15 +47,15 @@
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
<v-layout v-else row wrap align-center>
|
<v-layout v-else row wrap align-center>
|
||||||
<v-flex xs12 class="text-xs-left pa-1">
|
<v-flex xs12 class="text-xs-left pa-0">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="marker.Label"
|
v-model="marker.Label"
|
||||||
:rules="[textRule]"
|
:rules="[textRule]"
|
||||||
color="secondary-dark"
|
|
||||||
browser-autocomplete="off"
|
browser-autocomplete="off"
|
||||||
class="input-name pa-0 ma-1"
|
class="input-name pa-0 ma-0"
|
||||||
hide-details
|
hide-details
|
||||||
single-line
|
single-line
|
||||||
|
solo-inverted
|
||||||
clearable
|
clearable
|
||||||
@click:clear="clearName(marker)"
|
@click:clear="clearName(marker)"
|
||||||
@change="updateName(marker)"
|
@change="updateName(marker)"
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -45,6 +45,7 @@ require (
|
||||||
github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c
|
github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.6.6 // indirect
|
||||||
github.com/mpraski/clusters v0.0.0-20171016094157-18104487c312
|
github.com/mpraski/clusters v0.0.0-20171016094157-18104487c312
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/paulmach/go.geojson v1.4.0
|
github.com/paulmach/go.geojson v1.4.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -236,6 +236,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
|
||||||
|
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/mpraski/clusters v0.0.0-20171016094157-18104487c312 h1:XDW24M0xpJ83twch860OuhzUPKWfVOg2qoDBtYOo+UY=
|
github.com/mpraski/clusters v0.0.0-20171016094157-18104487c312 h1:XDW24M0xpJ83twch860OuhzUPKWfVOg2qoDBtYOo+UY=
|
||||||
github.com/mpraski/clusters v0.0.0-20171016094157-18104487c312/go.mod h1:1wDbOlBLClLuyu3ggcgsE1QGcWd1/LywIS9JymHVgZg=
|
github.com/mpraski/clusters v0.0.0-20171016094157-18104487c312/go.mod h1:1wDbOlBLClLuyu3ggcgsE1QGcWd1/LywIS9JymHVgZg=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
|
|
|
@ -45,13 +45,13 @@ func cleanUpAction(ctx *cli.Context) error {
|
||||||
log.Infof("cleanup: read-only mode enabled")
|
log.Infof("cleanup: read-only mode enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanUp := service.CleanUp()
|
w := service.CleanUp()
|
||||||
|
|
||||||
opt := photoprism.CleanUpOptions{
|
opt := photoprism.CleanUpOptions{
|
||||||
Dry: ctx.Bool("dry"),
|
Dry: ctx.Bool("dry"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if thumbs, orphans, err := cleanUp.Start(opt); err != nil {
|
if thumbs, orphans, err := w.Start(opt); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
|
@ -50,9 +50,9 @@ func convertAction(ctx *cli.Context) error {
|
||||||
|
|
||||||
log.Infof("converting originals in %s", txt.Quote(convertPath))
|
log.Infof("converting originals in %s", txt.Quote(convertPath))
|
||||||
|
|
||||||
convert := service.Convert()
|
w := service.Convert()
|
||||||
|
|
||||||
if err := convert.Start(convertPath); err != nil {
|
if err := w.Start(convertPath); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,10 +63,10 @@ func copyAction(ctx *cli.Context) error {
|
||||||
|
|
||||||
log.Infof("copying media files from %s to %s", sourcePath, conf.OriginalsPath())
|
log.Infof("copying media files from %s to %s", sourcePath, conf.OriginalsPath())
|
||||||
|
|
||||||
imp := service.Import()
|
w := service.Import()
|
||||||
opt := photoprism.ImportOptionsCopy(sourcePath)
|
opt := photoprism.ImportOptionsCopy(sourcePath)
|
||||||
|
|
||||||
imp.Start(opt)
|
w.Start(opt)
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
|
90
internal/commands/faces.go
Normal file
90
internal/commands/faces.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FacesCommand registers the faces cli command.
|
||||||
|
var FacesCommand = cli.Command{
|
||||||
|
Name: "faces",
|
||||||
|
Usage: "Runs facial recognition sub-commands",
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "stats",
|
||||||
|
Usage: "Shows stats on face embeddings",
|
||||||
|
Action: facesStatsAction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "index",
|
||||||
|
Usage: "Performs face clustering and recognition",
|
||||||
|
Action: facesIndexAction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// facesStatsAction shows stats on face embeddings.
|
||||||
|
func facesStatsAction(ctx *cli.Context) error {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
conf := config.NewConfig(ctx)
|
||||||
|
service.SetConfig(conf)
|
||||||
|
|
||||||
|
_, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := conf.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.InitDb()
|
||||||
|
|
||||||
|
w := service.Faces()
|
||||||
|
|
||||||
|
if err := w.Analyze(); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
log.Infof("completed in %s", elapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Shutdown()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// facesIndexAction performs face clustering and recognition.
|
||||||
|
func facesIndexAction(ctx *cli.Context) error {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
conf := config.NewConfig(ctx)
|
||||||
|
service.SetConfig(conf)
|
||||||
|
|
||||||
|
_, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := conf.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.InitDb()
|
||||||
|
|
||||||
|
w := service.Faces()
|
||||||
|
|
||||||
|
if err := w.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
log.Infof("completed in %s", elapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.Shutdown()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -63,10 +63,10 @@ func importAction(ctx *cli.Context) error {
|
||||||
|
|
||||||
log.Infof("moving media files from %s to %s", sourcePath, conf.OriginalsPath())
|
log.Infof("moving media files from %s to %s", sourcePath, conf.OriginalsPath())
|
||||||
|
|
||||||
imp := service.Import()
|
w := service.Import()
|
||||||
opt := photoprism.ImportOptionsMove(sourcePath)
|
opt := photoprism.ImportOptionsMove(sourcePath)
|
||||||
|
|
||||||
imp.Start(opt)
|
w.Start(opt)
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
|
@ -62,38 +64,40 @@ func indexAction(ctx *cli.Context) error {
|
||||||
log.Infof("index: read-only mode enabled")
|
log.Infof("index: read-only mode enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
ind := service.Index()
|
var indexed fs.Done
|
||||||
|
|
||||||
indOpt := photoprism.IndexOptions{
|
if w := service.Index(); w != nil {
|
||||||
|
opt := photoprism.IndexOptions{
|
||||||
Path: subPath,
|
Path: subPath,
|
||||||
Rescan: ctx.Bool("all"),
|
Rescan: ctx.Bool("all"),
|
||||||
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
|
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
|
||||||
Stack: true,
|
Stack: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
indexed := ind.Start(indOpt)
|
indexed = w.Start(opt)
|
||||||
|
}
|
||||||
|
|
||||||
prg := service.Purge()
|
if w := service.Purge(); w != nil {
|
||||||
|
opt := photoprism.PurgeOptions{
|
||||||
prgOpt := photoprism.PurgeOptions{
|
|
||||||
Path: subPath,
|
Path: subPath,
|
||||||
Ignore: indexed,
|
Ignore: indexed,
|
||||||
}
|
}
|
||||||
|
|
||||||
if files, photos, err := prg.Start(prgOpt); err != nil {
|
if files, photos, err := w.Start(opt); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
} else if len(files) > 0 || len(photos) > 0 {
|
} else if len(files) > 0 || len(photos) > 0 {
|
||||||
log.Infof("purge: removed %d files and %d photos", len(files), len(photos))
|
log.Infof("purge: removed %d files and %d photos", len(files), len(photos))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.Bool("cleanup") {
|
if ctx.Bool("cleanup") {
|
||||||
cleanUp := service.CleanUp()
|
w := service.CleanUp()
|
||||||
|
|
||||||
opt := photoprism.CleanUpOptions{
|
opt := photoprism.CleanUpOptions{
|
||||||
Dry: false,
|
Dry: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if thumbs, orphans, err := cleanUp.Start(opt); err != nil {
|
if thumbs, orphans, err := w.Start(opt); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
log.Infof("cleanup: removed %d index entries and %d orphan thumbnails", orphans, thumbs)
|
log.Infof("cleanup: removed %d index entries and %d orphan thumbnails", orphans, thumbs)
|
||||||
|
|
|
@ -36,9 +36,9 @@ func momentsAction(ctx *cli.Context) error {
|
||||||
log.Infof("moments: read-only mode enabled")
|
log.Infof("moments: read-only mode enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
moments := service.Moments()
|
w := service.Moments()
|
||||||
|
|
||||||
if err := moments.Start(); err != nil {
|
if err := w.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PeopleCommand registers the people cli command.
|
|
||||||
var PeopleCommand = cli.Command{
|
|
||||||
Name: "people",
|
|
||||||
Usage: "Performs face clustering and recognition",
|
|
||||||
Action: peopleAction,
|
|
||||||
}
|
|
||||||
|
|
||||||
// peopleAction performs face clustering and recognition.
|
|
||||||
func peopleAction(ctx *cli.Context) error {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
conf := config.NewConfig(ctx)
|
|
||||||
service.SetConfig(conf)
|
|
||||||
|
|
||||||
_, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := conf.Init(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.InitDb()
|
|
||||||
|
|
||||||
people := service.People()
|
|
||||||
|
|
||||||
if err := people.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
elapsed := time.Since(start)
|
|
||||||
|
|
||||||
log.Infof("completed in %s", elapsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.Shutdown()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -62,7 +62,7 @@ func purgeAction(ctx *cli.Context) error {
|
||||||
log.Infof("purge: read-only mode enabled")
|
log.Infof("purge: read-only mode enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
prg := service.Purge()
|
w := service.Purge()
|
||||||
|
|
||||||
opt := photoprism.PurgeOptions{
|
opt := photoprism.PurgeOptions{
|
||||||
Path: subPath,
|
Path: subPath,
|
||||||
|
@ -70,7 +70,7 @@ func purgeAction(ctx *cli.Context) error {
|
||||||
Hard: ctx.Bool("hard"),
|
Hard: ctx.Bool("hard"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if files, photos, err := prg.Start(opt); err != nil {
|
if files, photos, err := w.Start(opt); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
|
@ -125,7 +125,7 @@ func resetAction(ctx *cli.Context) error {
|
||||||
|
|
||||||
log.Infof("removed files in %s", time.Since(start))
|
log.Infof("removed files in %s", time.Since(start))
|
||||||
} else {
|
} else {
|
||||||
log.Infof("no backup files found")
|
log.Infof("no metadata backups found for removal")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Infof("keeping backup files")
|
log.Infof("keeping backup files")
|
||||||
|
@ -160,7 +160,7 @@ func resetAction(ctx *cli.Context) error {
|
||||||
|
|
||||||
log.Infof("removed files in %s", time.Since(start))
|
log.Infof("removed files in %s", time.Since(start))
|
||||||
} else {
|
} else {
|
||||||
log.Infof("no backup files found")
|
log.Infof("no album backups found for removal")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Infof("keeping backup files")
|
log.Infof("keeping backup files")
|
||||||
|
|
|
@ -93,8 +93,6 @@ func Detect(fileName string) (faces Faces, err error) {
|
||||||
return faces, fmt.Errorf("faces: file '%s' not found", txt.Quote(filepath.Base(fileName)))
|
return faces, fmt.Errorf("faces: file '%s' not found", txt.Quote(filepath.Base(fileName)))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("faces: analyzing %s", txt.Quote(filepath.Base(fileName)))
|
|
||||||
|
|
||||||
det, params, err := fd.Detect(fileName)
|
det, params, err := fd.Detect(fileName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -116,7 +116,7 @@ func (t *Net) getFaceCrop(fileName, fileHash string, f Point) (img image.Image,
|
||||||
} else if img, err := imaging.Open(cacheFile); err != nil {
|
} else if img, err := imaging.Open(cacheFile); err != nil {
|
||||||
log.Errorf("faces: failed loading cached crop %s", filepath.Base(cacheFile))
|
log.Errorf("faces: failed loading cached crop %s", filepath.Base(cacheFile))
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("faces: found cached crop %s", filepath.Base(cacheFile))
|
log.Debugf("faces: using cached crop %s", filepath.Base(cacheFile))
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,43 +6,156 @@ import (
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/montanaflynn/stats"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/mutex"
|
"github.com/photoprism/photoprism/internal/mutex"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
|
|
||||||
"github.com/mpraski/clusters"
|
"github.com/mpraski/clusters"
|
||||||
)
|
)
|
||||||
|
|
||||||
// People represents a worker for face clustering and recognition.
|
// Faces represents a worker for face clustering and recognition.
|
||||||
type People struct {
|
type Faces struct {
|
||||||
conf *config.Config
|
conf *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPeople returns a new People worker.
|
// NewFaces returns a new Faces worker.
|
||||||
func NewPeople(conf *config.Config) *People {
|
func NewFaces(conf *config.Config) *Faces {
|
||||||
instance := &People{
|
instance := &Faces{
|
||||||
conf: conf,
|
conf: conf,
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Analyze face embeddings.
|
||||||
|
func (w *Faces) Analyze() (err error) {
|
||||||
|
if embeddings, err := query.Embeddings(true); err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(embeddings) == 0 {
|
||||||
|
log.Infof("faces: no embeddings found")
|
||||||
|
} else {
|
||||||
|
|
||||||
|
c := len(embeddings)
|
||||||
|
|
||||||
|
log.Debugf("faces: found %d embeddings to analyze", c)
|
||||||
|
|
||||||
|
distMin := make([]float64, c)
|
||||||
|
distMax := make([]float64, c)
|
||||||
|
|
||||||
|
for i := 0; i < c; i++ {
|
||||||
|
min := -1.0
|
||||||
|
max := -1.0
|
||||||
|
|
||||||
|
for j := 0; j < c; j++ {
|
||||||
|
if i == j {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
d := clusters.EuclideanDistance(embeddings[i], embeddings[j])
|
||||||
|
|
||||||
|
if min < 0 || d < min {
|
||||||
|
min = d
|
||||||
|
}
|
||||||
|
|
||||||
|
if max < 0 || d > max {
|
||||||
|
max = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
distMin[i] = min
|
||||||
|
distMax[i] = max
|
||||||
|
}
|
||||||
|
|
||||||
|
minMedian, _ := stats.Median(distMin)
|
||||||
|
minMin, _ := stats.Min(distMin)
|
||||||
|
minMax, _ := stats.Max(distMin)
|
||||||
|
|
||||||
|
log.Infof("faces: min Ø %f < median %f < %f", minMin, minMedian, minMax)
|
||||||
|
|
||||||
|
maxMedian, _ := stats.Median(distMax)
|
||||||
|
maxMin, _ := stats.Min(distMax)
|
||||||
|
maxMax, _ := stats.Max(distMax)
|
||||||
|
|
||||||
|
log.Infof("faces: max Ø %f < median %f < %f", maxMin, maxMedian, maxMax)
|
||||||
|
}
|
||||||
|
|
||||||
|
if known, err := query.PeopleFaces(); err != nil {
|
||||||
|
log.Errorf("faces: %s", err)
|
||||||
|
} else if len(known) == 0 {
|
||||||
|
log.Infof("faces: no faces found")
|
||||||
|
} else {
|
||||||
|
c := len(known)
|
||||||
|
dist := make(map[string][]float64)
|
||||||
|
|
||||||
|
for i := 0; i < c; i++ {
|
||||||
|
f1 := known[i]
|
||||||
|
|
||||||
|
if f1.PersonUID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
e1 := f1.UnmarshalEmbedding()
|
||||||
|
min := -1.0
|
||||||
|
max := -1.0
|
||||||
|
|
||||||
|
if k, ok := dist[f1.PersonUID]; ok {
|
||||||
|
min = k[0]
|
||||||
|
max = k[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := 0; j < c; j++ {
|
||||||
|
if i == j {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f2 := known[j]
|
||||||
|
|
||||||
|
if f1.PersonUID != f2.PersonUID || f2.PersonUID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
e2 := f2.UnmarshalEmbedding()
|
||||||
|
|
||||||
|
d := clusters.EuclideanDistance(e1, e2)
|
||||||
|
|
||||||
|
if min < 0 || d < min {
|
||||||
|
min = d
|
||||||
|
}
|
||||||
|
|
||||||
|
if max < 0 || d > max {
|
||||||
|
max = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if max > 0 {
|
||||||
|
dist[f1.PersonUID] = []float64{min, max}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for personUID, d := range dist {
|
||||||
|
log.Infof("faces: %s Ø min %f, max %f", personUID, d[0], d[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Start face clustering and recognition.
|
// Start face clustering and recognition.
|
||||||
func (m *People) Start() (err error) {
|
func (w *Faces) Start() (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = fmt.Errorf("people: %s (panic)\nstack: %s", r, debug.Stack())
|
err = fmt.Errorf("faces: %s (panic)\nstack: %s", r, debug.Stack())
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !m.conf.Experimental() {
|
if !w.conf.Experimental() {
|
||||||
return fmt.Errorf("people: experimental features disabled")
|
return fmt.Errorf("faces: experimental features disabled")
|
||||||
} else if !m.conf.Settings().Features.People {
|
} else if !w.conf.Settings().Features.People {
|
||||||
return fmt.Errorf("people: disabled in settings")
|
return fmt.Errorf("faces: disabled in settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := mutex.MainWorker.Start(); err != nil {
|
if err := mutex.MainWorker.Start(); err != nil {
|
||||||
|
@ -51,32 +164,32 @@ func (m *People) Start() (err error) {
|
||||||
|
|
||||||
defer mutex.MainWorker.Stop()
|
defer mutex.MainWorker.Stop()
|
||||||
|
|
||||||
embeddings, err := query.Embeddings()
|
embeddings, err := query.Embeddings(false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(embeddings) == 0 {
|
if len(embeddings) == 0 {
|
||||||
log.Infof("people: no faces detected")
|
log.Infof("faces: no faces detected")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// see https://fse.studenttheses.ub.rug.nl/18064/1/Report_research_internship.pdf
|
// see https://fse.studenttheses.ub.rug.nl/18064/1/Report_research_internship.pdf
|
||||||
|
|
||||||
c, e := clusters.DBSCAN(1, 0.42, m.conf.Workers(), clusters.EuclideanDistance)
|
c, e := clusters.DBSCAN(1, 1.0, w.conf.Workers(), clusters.EuclideanDistance)
|
||||||
|
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Learn(embeddings); err != nil {
|
if err := c.Learn(embeddings); err != nil {
|
||||||
log.Errorf("people: %s", err)
|
log.Errorf("faces: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sizes := c.Sizes()
|
sizes := c.Sizes()
|
||||||
|
|
||||||
log.Infof("people: found %d embeddings, %d clusters", len(embeddings), len(sizes))
|
log.Infof("faces: found %d embeddings, %d clusters", len(embeddings), len(sizes))
|
||||||
|
|
||||||
faceClusters := make([]entity.Embeddings, len(sizes))
|
faceClusters := make([]entity.Embeddings, len(sizes))
|
||||||
|
|
||||||
|
@ -102,22 +215,22 @@ func (m *People) Start() (err error) {
|
||||||
for _, clusterEmb := range faceClusters {
|
for _, clusterEmb := range faceClusters {
|
||||||
if emb, err := json.Marshal(entity.EmbeddingsMidpoint(clusterEmb)); err != nil {
|
if emb, err := json.Marshal(entity.EmbeddingsMidpoint(clusterEmb)); err != nil {
|
||||||
updateErrors++
|
updateErrors++
|
||||||
log.Errorf("people: %s", err)
|
log.Errorf("faces: %s", err)
|
||||||
} else if f := entity.NewPersonFace("", string(emb)); f == nil {
|
} else if f := entity.NewPersonFace("", string(emb)); f == nil {
|
||||||
updateErrors++
|
updateErrors++
|
||||||
log.Errorf("people: face should not be nil - bug?")
|
log.Errorf("faces: face should not be nil - bug?")
|
||||||
} else if err := f.Create(); err == nil {
|
} else if err := f.Create(); err == nil {
|
||||||
addedFaces++
|
addedFaces++
|
||||||
log.Tracef("people: 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.Val{"UpdatedAt": entity.Timestamp()}); err != nil {
|
||||||
updateErrors++
|
updateErrors++
|
||||||
log.Errorf("people: %s", err)
|
log.Errorf("faces: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := query.PurgeUnknownFaces(); err != nil {
|
if err := query.PurgeUnknownFaces(); err != nil {
|
||||||
updateErrors++
|
updateErrors++
|
||||||
log.Errorf("people: %s", err)
|
log.Errorf("faces: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
peopleFaces, err := query.PeopleFaces()
|
peopleFaces, err := query.PeopleFaces()
|
||||||
|
@ -153,7 +266,7 @@ func (m *People) Start() (err error) {
|
||||||
|
|
||||||
for _, marker := range markers {
|
for _, marker := range markers {
|
||||||
if mutex.MainWorker.Canceled() {
|
if mutex.MainWorker.Canceled() {
|
||||||
return fmt.Errorf("people: worker canceled")
|
return fmt.Errorf("faces: worker canceled")
|
||||||
}
|
}
|
||||||
|
|
||||||
var faceId string
|
var faceId string
|
||||||
|
@ -180,24 +293,23 @@ func (m *People) Start() (err error) {
|
||||||
if marker.MarkerLabel == "" {
|
if marker.MarkerLabel == "" {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
} else if person := entity.NewPerson(marker.MarkerLabel, entity.SrcMarker, 1); person == nil {
|
} else if person := entity.NewPerson(marker.MarkerLabel, entity.SrcMarker, 1); person == nil {
|
||||||
log.Errorf("people: person should not be nil - bug?")
|
log.Errorf("faces: person should not be nil - bug?")
|
||||||
} else if person = entity.FirstOrCreatePerson(person); person == nil {
|
} else if person = entity.FirstOrCreatePerson(person); person == nil {
|
||||||
log.Errorf("people: failed adding %s", txt.Quote(marker.MarkerLabel))
|
log.Errorf("faces: failed adding %s", txt.Quote(marker.MarkerLabel))
|
||||||
} else if f, ok := faceMap[faceId]; ok {
|
} else if f, ok := faceMap[faceId]; ok {
|
||||||
faceMap[faceId] = Face{Embedding: f.Embedding, PersonUID: person.PersonUID}
|
faceMap[faceId] = Face{Embedding: f.Embedding, PersonUID: person.PersonUID}
|
||||||
entity.Db().Model(&entity.PersonFace{}).Where("id = ?", faceId).Update("PersonUID", person.PersonUID)
|
entity.Db().Model(&entity.PersonFace{}).Where("id = ?", faceId).Update("PersonUID", person.PersonUID)
|
||||||
log.Infof("people: added %s", txt.Quote(person.PersonName))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing person?
|
// Existing person?
|
||||||
if refUID := faceMap[faceId].PersonUID; refUID != "" {
|
if refUID := faceMap[faceId].PersonUID; refUID != "" {
|
||||||
if err := marker.Updates(entity.Val{"RefUID": refUID, "RefSrc": entity.SrcPeople, "FaceID": ""}); err != nil {
|
if err := marker.Updates(entity.Val{"RefUID": refUID, "RefSrc": entity.SrcPeople, "FaceID": ""}); err != nil {
|
||||||
log.Errorf("people: %s while updating person uid", err)
|
log.Errorf("faces: %s while updating person uid", err)
|
||||||
} else {
|
} else {
|
||||||
recognized++
|
recognized++
|
||||||
}
|
}
|
||||||
} else if err := marker.Updates(entity.Val{"FaceID": faceId}); err != nil {
|
} else if err := marker.Updates(entity.Val{"FaceID": faceId}); err != nil {
|
||||||
log.Errorf("people: %s while updating marker face id", err)
|
log.Errorf("faces: %s while updating marker face id", err)
|
||||||
} else {
|
} else {
|
||||||
markersUpdated++
|
markersUpdated++
|
||||||
}
|
}
|
||||||
|
@ -208,12 +320,12 @@ func (m *People) Start() (err error) {
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("people: %d faces added, %d recognized, %d unknown, %d errors", addedFaces, recognized, markersUpdated, updateErrors)
|
log.Infof("faces: %d added, %d recognized, %d unknown, %d errors", addedFaces, recognized, markersUpdated, updateErrors)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel stops the current operation.
|
// Cancel stops the current operation.
|
||||||
func (m *People) Cancel() {
|
func (w *Faces) Cancel() {
|
||||||
mutex.MainWorker.Cancel()
|
mutex.MainWorker.Cancel()
|
||||||
}
|
}
|
|
@ -9,7 +9,7 @@ import (
|
||||||
func TestPeople_Start(t *testing.T) {
|
func TestPeople_Start(t *testing.T) {
|
||||||
conf := config.TestConfig()
|
conf := config.TestConfig()
|
||||||
|
|
||||||
m := NewPeople(conf)
|
m := NewFaces(conf)
|
||||||
err := m.Start()
|
err := m.Start()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
|
@ -822,9 +822,9 @@ func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
if len(labels) > 0 {
|
||||||
|
log.Infof("index: found %d labels for %s [%s]", len(labels), txt.Quote(jpeg.BaseName()), time.Since(start))
|
||||||
log.Debugf("index: image classification took %s", elapsed)
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
@ -864,9 +864,9 @@ func (ind *Index) detectFaces(jpeg *MediaFile) face.Faces {
|
||||||
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
|
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
|
||||||
}
|
}
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
if len(faces) > 0 {
|
||||||
|
log.Infof("index: detected %d faces in %s [%s]", len(faces), txt.Quote(jpeg.BaseName()), time.Since(start))
|
||||||
log.Debugf("index: face detection took %s", elapsed)
|
}
|
||||||
|
|
||||||
return faces
|
return faces
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ func NewMoments(conf *config.Config) *Moments {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start creates albums based on popular locations, dates and categories.
|
// Start creates albums based on popular locations, dates and categories.
|
||||||
func (m *Moments) Start() (err error) {
|
func (w *Moments) Start() (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = fmt.Errorf("moments: %s (panic)\nstack: %s", r, debug.Stack())
|
err = fmt.Errorf("moments: %s (panic)\nstack: %s", r, debug.Stack())
|
||||||
|
@ -230,7 +230,7 @@ func (m *Moments) Start() (err error) {
|
||||||
log.Errorf("moments: %s (update album dates)", err.Error())
|
log.Errorf("moments: %s (update album dates)", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if count, err := BackupAlbums(m.conf.AlbumsPath(), false); err != nil {
|
if count, err := BackupAlbums(w.conf.AlbumsPath(), false); err != nil {
|
||||||
log.Errorf("moments: %s (backup albums)", err.Error())
|
log.Errorf("moments: %s (backup albums)", err.Error())
|
||||||
} else if count > 0 {
|
} else if count > 0 {
|
||||||
log.Debugf("moments: %d albums saved as yaml files", count)
|
log.Debugf("moments: %d albums saved as yaml files", count)
|
||||||
|
@ -240,6 +240,6 @@ func (m *Moments) Start() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel stops the current operation.
|
// Cancel stops the current operation.
|
||||||
func (m *Moments) Cancel() {
|
func (w *Moments) Cancel() {
|
||||||
mutex.MainWorker.Cancel()
|
mutex.MainWorker.Cancel()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Resample represents a thumbnail generator.
|
// Resample represents a thumbnail generator worker.
|
||||||
type Resample struct {
|
type Resample struct {
|
||||||
conf *config.Config
|
conf *config.Config
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ func NewResample(conf *config.Config) *Resample {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start creates default thumbnails for all files in originalsPath.
|
// Start creates default thumbnails for all files in originalsPath.
|
||||||
func (rs *Resample) Start(force bool) (err error) {
|
func (w *Resample) Start(force bool) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = fmt.Errorf("resample: %s (panic)\nstack: %s", r, debug.Stack())
|
err = fmt.Errorf("resample: %s (panic)\nstack: %s", r, debug.Stack())
|
||||||
|
@ -40,14 +40,14 @@ func (rs *Resample) Start(force bool) (err error) {
|
||||||
|
|
||||||
defer mutex.MainWorker.Stop()
|
defer mutex.MainWorker.Stop()
|
||||||
|
|
||||||
originalsPath := rs.conf.OriginalsPath()
|
originalsPath := w.conf.OriginalsPath()
|
||||||
thumbnailsPath := rs.conf.ThumbPath()
|
thumbnailsPath := w.conf.ThumbPath()
|
||||||
|
|
||||||
jobs := make(chan ResampleJob)
|
jobs := make(chan ResampleJob)
|
||||||
|
|
||||||
// Start a fixed number of goroutines to read and digest files.
|
// Start a fixed number of goroutines to read and digest files.
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var numWorkers = rs.conf.Workers()
|
var numWorkers = w.conf.Workers()
|
||||||
wg.Add(numWorkers)
|
wg.Add(numWorkers)
|
||||||
for i := 0; i < numWorkers; i++ {
|
for i := 0; i < numWorkers; i++ {
|
||||||
go func() {
|
go func() {
|
||||||
|
|
|
@ -36,8 +36,8 @@ func Markers(limit, offset int, markerType string, embeddings, unmatched bool) (
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embeddings finds all face embeddings.
|
// Embeddings returns existing face embeddings.
|
||||||
func Embeddings() (result entity.Embeddings, err error) {
|
func Embeddings(single bool) (result entity.Embeddings, err error) {
|
||||||
var col []string
|
var col []string
|
||||||
|
|
||||||
stmt := Db().
|
stmt := Db().
|
||||||
|
@ -52,9 +52,15 @@ func Embeddings() (result entity.Embeddings, err error) {
|
||||||
|
|
||||||
for _, embeddingsJson := range col {
|
for _, embeddingsJson := range col {
|
||||||
if embeddings := entity.UnmarshalEmbeddings(embeddingsJson); len(embeddings) > 0 {
|
if embeddings := entity.UnmarshalEmbeddings(embeddingsJson); len(embeddings) > 0 {
|
||||||
|
if single {
|
||||||
|
// Single embedding per face detected.
|
||||||
|
result = append(result, embeddings[0])
|
||||||
|
} else {
|
||||||
|
// Return all embedding otherwise.
|
||||||
result = append(result, embeddings...)
|
result = append(result, embeddings...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
19
internal/service/faces.go
Normal file
19
internal/service/faces.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
)
|
||||||
|
|
||||||
|
var onceFaces sync.Once
|
||||||
|
|
||||||
|
func initFaces() {
|
||||||
|
services.Faces = photoprism.NewFaces(Config())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Faces() *photoprism.Faces {
|
||||||
|
onceFaces.Do(initFaces)
|
||||||
|
|
||||||
|
return services.Faces
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
|
||||||
)
|
|
||||||
|
|
||||||
var oncePeople sync.Once
|
|
||||||
|
|
||||||
func initPeople() {
|
|
||||||
services.People = photoprism.NewPeople(Config())
|
|
||||||
}
|
|
||||||
|
|
||||||
func People() *photoprism.People {
|
|
||||||
oncePeople.Do(initPeople)
|
|
||||||
|
|
||||||
return services.People
|
|
||||||
}
|
|
|
@ -25,7 +25,7 @@ var services struct {
|
||||||
Import *photoprism.Import
|
Import *photoprism.Import
|
||||||
Index *photoprism.Index
|
Index *photoprism.Index
|
||||||
Moments *photoprism.Moments
|
Moments *photoprism.Moments
|
||||||
People *photoprism.People
|
Faces *photoprism.Faces
|
||||||
Purge *photoprism.Purge
|
Purge *photoprism.Purge
|
||||||
CleanUp *photoprism.CleanUp
|
CleanUp *photoprism.CleanUp
|
||||||
Nsfw *nsfw.Detector
|
Nsfw *nsfw.Detector
|
||||||
|
|
Loading…
Reference in a new issue