Merge branch 'master' of 192.168.0.2:photoassistant
This commit is contained in:
@@ -5,4 +5,3 @@ new_img_dir
|
|||||||
static/Bin/*
|
static/Bin/*
|
||||||
static/Import/*
|
static/Import/*
|
||||||
static/Storage/*
|
static/Storage/*
|
||||||
reference_images/*
|
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ RUN pip3 install --upgrade pillow --user
|
|||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
EXPOSE 55432
|
EXPOSE 55432
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN chown -R mythtv:mythtv ./static
|
RUN chown mythtv:mythtv ./static
|
||||||
|
RUN chown mythtv:mythtv ./static/*
|
||||||
CMD ["./wrapper.sh"]
|
CMD ["./wrapper.sh"]
|
||||||
|
|||||||
37
README
37
README
@@ -1,5 +1,10 @@
|
|||||||
In here we can put instructions on how to run this / any general info
|
In here we can put instructions on how to run this / any general info
|
||||||
|
|
||||||
|
to edit src:
|
||||||
|
|
||||||
|
git....
|
||||||
|
CAM: fill this in pls
|
||||||
|
|
||||||
|
|
||||||
ubuntu packages:
|
ubuntu packages:
|
||||||
sudo apt-get install -y mediainfo cmake python3-flask
|
sudo apt-get install -y mediainfo cmake python3-flask
|
||||||
@@ -21,10 +26,16 @@ pip packages:
|
|||||||
upstream packages...
|
upstream packages...
|
||||||
mkdir static/upstream
|
mkdir static/upstream
|
||||||
cd static/upstream
|
cd static/upstream
|
||||||
|
mkdir bootstrap-4.6.0-dist
|
||||||
|
cd bootstrap-4.6.0-dist
|
||||||
|
|
||||||
|
mkdir css
|
||||||
# for boostrap:
|
# for boostrap:
|
||||||
wget https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css
|
wget https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css
|
||||||
wget https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js
|
|
||||||
|
mkdir js
|
||||||
|
# to note we might need bootstrap.bundle.min.js if we use new features?
|
||||||
|
wget https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js
|
||||||
|
|
||||||
# for jquery
|
# for jquery
|
||||||
https://code.jquery.com/jquery-3.6.0.min.js
|
https://code.jquery.com/jquery-3.6.0.min.js
|
||||||
@@ -50,7 +61,7 @@ to run prod version of web server:
|
|||||||
gunicorn --bind="192.168.0.2:5000" --threads=2 --workers=2 main:app
|
gunicorn --bind="192.168.0.2:5000" --threads=2 --workers=2 main:app
|
||||||
|
|
||||||
Also have to run the job manager for jobs to work:
|
Also have to run the job manager for jobs to work:
|
||||||
python3 pa_job_manager.py
|
FLASK_ENV="development" python3 pa_job_manager.py
|
||||||
|
|
||||||
To rebuild DB from scratch/empty data:
|
To rebuild DB from scratch/empty data:
|
||||||
|
|
||||||
@@ -79,3 +90,25 @@ To get back a 'working' but scanned set of data:
|
|||||||
# gunzip -c /home/ddp/src/photoassistant/DB_BACKUP/20200126-all-imported-no-duplicates.sql.gz > /srv/docker/container/padb/docker-entrypoint-initdb.d/tables.sql
|
# gunzip -c /home/ddp/src/photoassistant/DB_BACKUP/20200126-all-imported-no-duplicates.sql.gz > /srv/docker/container/padb/docker-entrypoint-initdb.d/tables.sql
|
||||||
|
|
||||||
( cd /srv/docker/config/ ; sudo docker-compose stop padb ; yes | sudo docker-compose rm padb ; sudo rm -rf /srv/docker/container/padb/data/ ; sudo docker-compose up padb )
|
( cd /srv/docker/config/ ; sudo docker-compose stop padb ; yes | sudo docker-compose rm padb ; sudo rm -rf /srv/docker/container/padb/data/ ; sudo docker-compose up padb )
|
||||||
|
|
||||||
|
|
||||||
|
HANDY SQLs/commands:
|
||||||
|
# long-running AI job (in this case #46), which is not committing joblog per file, and isnt tracking counts properly (temporary bug)
|
||||||
|
|
||||||
|
sudo docker exec -it padb bash
|
||||||
|
echo 'select * from joblog where job_id = 46;' | psql --user=pa pa | grep 'ooking for' | awk '{ print $15 } ' | sort -u | wc -l
|
||||||
|
|
||||||
|
# how many entries are in a path
|
||||||
|
sudo docker exec -it padb bash
|
||||||
|
psql --user=pa pa
|
||||||
|
select count(entry_id) from entry_dir_link where dir_eid in ( select distinct dir_eid from path_dir_link where path_id = 2 );
|
||||||
|
|
||||||
|
# how many Images are in a path
|
||||||
|
sudo docker exec -it padb bash
|
||||||
|
psql --user=pa pa
|
||||||
|
select count(distinct e.id) from entry e, entry_dir_link edl where e.type_id = 1 and e.id = edl.entry_id and edl.dir_eid in ( select distinct dir_eid from path_dir_link where path_id = 2 );
|
||||||
|
|
||||||
|
|
||||||
|
# get abs filenames of matching files (for liz person.tag, but could easily add
|
||||||
|
# d.rel_path like 'liz' too :
|
||||||
|
select '"'||replace(replace(p.path_prefix,'static/Storage/',''),'static/Import/', '')||'/'||d.rel_path||'/'||e.name||'"' from entry e, entry_dir_link edl, path_dir_link pdl, path p, dir d where e.id = edl.entry_id and edl.dir_eid = pdl.dir_eid and pdl.path_id = p.id and d.eid = edl.dir_eid and e.id in ( select e.id from entry e, face_file_link ffl, face_refimg_link frl, person_refimg_link prl, person p where e.id = ffl.file_eid and ffl.face_id = frl.face_id and frl.refimg_id = prl.refimg_id and prl.person_id = p.id and p.tag = 'liz' );
|
||||||
|
|||||||
46
TODO
46
TODO
@@ -1,26 +1,26 @@
|
|||||||
## GENERAL
|
## GENERAL
|
||||||
|
|
||||||
* incorporate flask-login and flask-ldap3-login
|
* allow rotate of image (permanently on FS, so its right everywhere)
|
||||||
https://flask-login.readthedocs.io/en/latest/
|
|
||||||
https://flask-ldap3-login.readthedocs.io/en/latest/
|
* improve photo browser -> view file, rather than just allowing browser to show image
|
||||||
https://pythonhosted.org/Flask-Principal/
|
|
||||||
# this is an example:
|
* face locations:
|
||||||
https://code.tutsplus.com/tutorials/flask-authentication-with-ldap--cms-23101
|
START FORM SCRATCH so all images have face_locn data
|
||||||
* user management scope
|
right now GenThumb is in shared, and does width, height as well --> in person.py BUT need this for pa_job_manager
|
||||||
- do I want admins only? I definitely * want a read-only / share (but to a subset potentially?)
|
|
||||||
(see point above for how to do all this)
|
* allow for threshold/settings to be tweaked from the GUI
|
||||||
|
- it would be good to then say, just run the scanner against this image or maybe this DIR, to see how it IDs ppl
|
||||||
|
---> settings for default value
|
||||||
|
---> override table to do per file combos?
|
||||||
|
|
||||||
|
* refimg
|
||||||
|
- remove AI menu from top-level -> make a sub-of Person, and just have Match or AI
|
||||||
|
|
||||||
* fix up logging in general
|
* fix up logging in general
|
||||||
* comment your code
|
* comment your code
|
||||||
* more OO goodness :)
|
* more OO goodness :)
|
||||||
|
|
||||||
## DB
|
## DB
|
||||||
* Need to think about...
|
|
||||||
file (image) -> has X faces, Y matches
|
|
||||||
X == Y (optim: dont scan again)
|
|
||||||
say X-Y == 1, then to optimise, we need to only check the missing
|
|
||||||
face... at the moment, the DB structure is not that clever...
|
|
||||||
(file_refimg_link --> file_refimg_link needs a face_num?)
|
|
||||||
|
|
||||||
* Dir can have date in the DB, so we can do Oldest/Newest dirs in Folder view
|
* Dir can have date in the DB, so we can do Oldest/Newest dirs in Folder view
|
||||||
|
|
||||||
### BACKEND
|
### BACKEND
|
||||||
@@ -34,9 +34,6 @@
|
|||||||
|
|
||||||
*** Need to use thread-safe sessions per Thread, half-assed version did not work
|
*** Need to use thread-safe sessions per Thread, half-assed version did not work
|
||||||
|
|
||||||
- would it be quicker/smarter to use md5 hash matching on import (and if
|
|
||||||
so, not re-do face* ) ???
|
|
||||||
|
|
||||||
need a manual button to restart a job in the GUI,
|
need a manual button to restart a job in the GUI,
|
||||||
(based on file-level optims, just run the job as new and it will optim over already done parts and continue)
|
(based on file-level optims, just run the job as new and it will optim over already done parts and continue)
|
||||||
|
|
||||||
@@ -52,6 +49,7 @@
|
|||||||
|
|
||||||
Admin
|
Admin
|
||||||
-> delete old jobs / auto delete jobs older than ???
|
-> delete old jobs / auto delete jobs older than ???
|
||||||
|
-> do I want to have admin roles/users?
|
||||||
|
|
||||||
### UI
|
### UI
|
||||||
??? ipads can't do selections and contextMenus, do I want to re-factor to cater for this?
|
??? ipads can't do selections and contextMenus, do I want to re-factor to cater for this?
|
||||||
@@ -67,7 +65,6 @@
|
|||||||
need to copy into here the jquery/fa files so we don't need internet to function
|
need to copy into here the jquery/fa files so we don't need internet to function
|
||||||
- for that matter run lightspeed against all this
|
- for that matter run lightspeed against all this
|
||||||
|
|
||||||
|
|
||||||
timelineview? (I think maybe sunburst for large amounts of files, then maybe something more timeline-series for drilling in?)
|
timelineview? (I think maybe sunburst for large amounts of files, then maybe something more timeline-series for drilling in?)
|
||||||
(vertical timeline, date has thumbnails (small) horizontally along
|
(vertical timeline, date has thumbnails (small) horizontally along
|
||||||
a page, etc.?
|
a page, etc.?
|
||||||
@@ -76,12 +73,13 @@
|
|||||||
https://www.highcharts.com/demo/heatmap
|
https://www.highcharts.com/demo/heatmap
|
||||||
https://www.highcharts.com/demo/packed-bubble-split
|
https://www.highcharts.com/demo/packed-bubble-split
|
||||||
|
|
||||||
### AI
|
|
||||||
* allow for threshold/settings to be tweaked from the GUI
|
|
||||||
- it would be good to then say, just run the scanner against this image or maybe this DIR, to see how it IDs ppl
|
|
||||||
|
|
||||||
|
|
||||||
### SORTER
|
### SORTER
|
||||||
* exif processing?
|
* exif processing?
|
||||||
* location stuff - test a new photo from my camera out
|
* location stuff - test a new photo from my camera out
|
||||||
-- image is in dir, need to look at exifread output
|
-- image is in dir, need to look at exifread output
|
||||||
|
|
||||||
|
### FUTURE:
|
||||||
|
* can emby use nfo for images (for AI/tags?)
|
||||||
|
-NO sadly
|
||||||
|
|
||||||
|
|||||||
51
ai.py
51
ai.py
@@ -5,11 +5,14 @@ from main import db, app, ma
|
|||||||
from sqlalchemy import Sequence
|
from sqlalchemy import Sequence
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from status import st, Status
|
from status import st, Status
|
||||||
from files import Entry, File, FileRefimgLink
|
from files import Entry, File
|
||||||
from person import Person, PersonRefimgLink
|
from person import Refimg, Person, PersonRefimgLink
|
||||||
from refimg import Refimg
|
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
|
from job import Job, JobExtra, Joblog, NewJob
|
||||||
|
from face import Face, FaceFileLink, FaceRefimgLink
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -18,13 +21,35 @@ from flask_login import login_required, current_user
|
|||||||
@app.route("/aistats", methods=["GET", "POST"])
|
@app.route("/aistats", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def aistats():
|
def aistats():
|
||||||
tmp=db.session.query(Entry,Person).join(File).join(FileRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person).filter(FileRefimgLink.matched==True).all()
|
stats = db.session.execute( "select p.tag, count(f.id) from person p, face f, face_file_link ffl, face_refimg_link frl, person_refimg_link prl where p.id = prl.person_id and prl.refimg_id = frl.refimg_id and frl.face_id = ffl.face_id and ffl.face_id = f.id group by p.tag" )
|
||||||
entries=[]
|
fstats={}
|
||||||
last_fname=""
|
fstats['files_with_a_face'] = db.session.execute( "select count(distinct file_eid) as count from face_file_link" ).first()[0]
|
||||||
for e, p in tmp:
|
fstats['files_with_a_match'] = db.session.execute( "select count(distinct ffl.file_eid) as count from face_file_link ffl, face_refimg_link frl where frl.face_id = ffl.face_id" ).first()[0]
|
||||||
if last_fname != e.name:
|
fstats['files_with_missing_matches'] = db.session.execute( "select count(distinct ffl.file_eid) from face f left join face_refimg_link frl on f.id = frl.face_id join face_file_link ffl on f.id = ffl.face_id where frl.refimg_id is null" ).first()[0]
|
||||||
entry = { 'name': e.name, 'people': [] }
|
|
||||||
entries.append( entry )
|
# files_with_no_matches?
|
||||||
last_fname = e.name
|
|
||||||
entry['people'].append( { 'tag': p.tag } )
|
fstats['all_faces'] = db.session.execute( "select count(distinct face_id) as count from face_file_link" ).first()[0]
|
||||||
return render_template("aistats.html", page_title='Placeholder', entries=entries)
|
fstats['all_matched_faces'] = db.session.execute( "select count(distinct face_id) as count from face_refimg_link" ).first()[0]
|
||||||
|
fstats['all_unmatched_faces'] = db.session.execute( "select count(f.id) from face f left join face_refimg_link frl on f.id = frl.face_id where frl.refimg_id is null" ).first()[0]
|
||||||
|
|
||||||
|
return render_template("aistats.html", page_title='AI Statistics', stats=stats, fstats=fstats )
|
||||||
|
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# /run_ai_on -> CAM: needs more thought (what actual params, e.g list of file -
|
||||||
|
# tick, but which face or faces? are we forcing a re-finding of unknown faces
|
||||||
|
# or just looking for matches? (maybe in the long run there are different
|
||||||
|
# routes, not params - stuff we will work out as we go)
|
||||||
|
################################################################################
|
||||||
|
@app.route("/run_ai_on", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def run_ai_on():
|
||||||
|
jex=[]
|
||||||
|
for el in request.form:
|
||||||
|
jex.append( JobExtra( name=f"{el}", value=request.form[el] ) )
|
||||||
|
print( f"would create new job with extras={jex}" )
|
||||||
|
job=NewJob( "run_ai_on", 0, None, jex )
|
||||||
|
st.SetAlert("success")
|
||||||
|
st.SetMessage( f"Created <a href=/job/{job.id}>Job #{job.id}</a> to Look for face(s) in selected file(s)")
|
||||||
|
return render_template("base.html")
|
||||||
|
|||||||
29
face.py
Normal file
29
face.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from main import db, app, ma
|
||||||
|
from sqlalchemy import Sequence
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
|
||||||
|
class Face(db.Model):
|
||||||
|
__tablename__ = "face"
|
||||||
|
id = db.Column(db.Integer, db.Sequence('face_id_seq'), primary_key=True )
|
||||||
|
face = db.Column( db.LargeBinary )
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<id: {self.id}, face={self.face}"
|
||||||
|
|
||||||
|
class FaceFileLink(db.Model):
|
||||||
|
__tablename__ = "face_file_link"
|
||||||
|
face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True )
|
||||||
|
file_eid = db.Column(db.Integer, db.ForeignKey("file.eid"), primary_key=True )
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<face_id: {self.face_id}, file_eid={self.file_eid}"
|
||||||
|
|
||||||
|
class FaceRefimgLink(db.Model):
|
||||||
|
__tablename__ = "face_refimg_link"
|
||||||
|
face_id = db.Column(db.Integer, db.ForeignKey("face.id"), primary_key=True )
|
||||||
|
refimg_id = db.Column(db.Integer, db.ForeignKey("refimg.id"), primary_key=True )
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<face_id: {self.face_id}, refimg_id={self.refimg_id}"
|
||||||
|
|
||||||
43
files.py
43
files.py
@@ -23,11 +23,11 @@ from flask_login import login_required, current_user
|
|||||||
################################################################################
|
################################################################################
|
||||||
from job import Job, JobExtra, Joblog, NewJob
|
from job import Job, JobExtra, Joblog, NewJob
|
||||||
from path import PathType, Path
|
from path import PathType, Path
|
||||||
from person import Person, PersonRefimgLink
|
from person import Refimg, Person, PersonRefimgLink
|
||||||
from refimg import Refimg
|
|
||||||
from settings import Settings
|
from settings import Settings
|
||||||
from shared import SymlinkName
|
from shared import SymlinkName
|
||||||
from dups import Duplicates
|
from dups import Duplicates
|
||||||
|
from face import Face, FaceFileLink, FaceRefimgLink
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
|
||||||
@@ -73,15 +73,6 @@ class Entry(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<id: {}, name: {}, type={}, dir_details={}, file_details={}, in_dir={}>".format(self.id, self.name, self.type, self.dir_details, self.file_details, self.in_dir)
|
return "<id: {}, name: {}, type={}, dir_details={}, file_details={}, in_dir={}>".format(self.id, self.name, self.type, self.dir_details, self.file_details, self.in_dir)
|
||||||
|
|
||||||
class FileRefimgLink(db.Model):
|
|
||||||
__tablename__ = "file_refimg_link"
|
|
||||||
file_id = db.Column(db.Integer, db.ForeignKey('file.eid'), unique=True, nullable=False, primary_key=True)
|
|
||||||
refimg_id = db.Column(db.Integer, db.ForeignKey('refimg.id'), unique=True, nullable=False, primary_key=True)
|
|
||||||
when_processed = db.Column(db.Float)
|
|
||||||
matched = db.Column(db.Boolean)
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<file_id: {self.file_id}, refimg_id: {self.refimg_id} when_processed={self.when_processed}, matched={self.matched}"
|
|
||||||
|
|
||||||
class File(db.Model):
|
class File(db.Model):
|
||||||
__tablename__ = "file"
|
__tablename__ = "file"
|
||||||
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
|
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
|
||||||
@@ -248,17 +239,22 @@ def files_ip():
|
|||||||
noo, grouping, how_many, offset, size, folders, cwd, root = ViewingOptions( request )
|
noo, grouping, how_many, offset, size, folders, cwd, root = ViewingOptions( request )
|
||||||
entries=[]
|
entries=[]
|
||||||
|
|
||||||
|
people = Person.query.all()
|
||||||
|
|
||||||
|
|
||||||
# per import path, add entries to view
|
# per import path, add entries to view
|
||||||
settings=Settings.query.first()
|
settings=Settings.query.first()
|
||||||
paths = settings.import_path.split("#")
|
paths = settings.import_path.split("#")
|
||||||
for path in paths:
|
for path in paths:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
prefix = SymlinkName("Import",path,path+'/')
|
prefix = SymlinkName("Import",path,path+'/')
|
||||||
if folders:
|
if folders:
|
||||||
entries+=GetEntriesInFolderView( cwd, prefix, noo, offset, how_many )
|
entries+=GetEntriesInFolderView( cwd, prefix, noo, offset, how_many )
|
||||||
else:
|
else:
|
||||||
entries+=GetEntriesInFlatView( cwd, prefix, noo, offset, how_many )
|
entries+=GetEntriesInFlatView( cwd, prefix, noo, offset, how_many )
|
||||||
|
|
||||||
return render_template("files.html", page_title='View Files (Import Path)', entry_data=entries, noo=noo, grouping=grouping, how_many=how_many, offset=offset, size=size, folders=folders, cwd=cwd, root=root )
|
return render_template("files.html", page_title='View Files (Import Path)', entry_data=entries, noo=noo, grouping=grouping, how_many=how_many, offset=offset, size=size, folders=folders, cwd=cwd, root=root, people=people )
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# /files -> show thumbnail view of files from storage_path
|
# /files -> show thumbnail view of files from storage_path
|
||||||
@@ -269,17 +265,21 @@ def files_sp():
|
|||||||
noo, grouping, how_many, offset, size, folders, cwd, root = ViewingOptions( request )
|
noo, grouping, how_many, offset, size, folders, cwd, root = ViewingOptions( request )
|
||||||
entries=[]
|
entries=[]
|
||||||
|
|
||||||
|
people = Person.query.all()
|
||||||
|
|
||||||
# per storage path, add entries to view
|
# per storage path, add entries to view
|
||||||
settings=Settings.query.first()
|
settings=Settings.query.first()
|
||||||
paths = settings.storage_path.split("#")
|
paths = settings.storage_path.split("#")
|
||||||
for path in paths:
|
for path in paths:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
prefix = SymlinkName("Storage",path,path+'/')
|
prefix = SymlinkName("Storage",path,path+'/')
|
||||||
if folders:
|
if folders:
|
||||||
entries+=GetEntriesInFolderView( cwd, prefix, noo, offset, how_many )
|
entries+=GetEntriesInFolderView( cwd, prefix, noo, offset, how_many )
|
||||||
else:
|
else:
|
||||||
entries+=GetEntriesInFlatView( cwd, prefix, noo, offset, how_many )
|
entries+=GetEntriesInFlatView( cwd, prefix, noo, offset, how_many )
|
||||||
|
|
||||||
return render_template("files.html", page_title='View Files (Storage Path)', entry_data=entries, noo=noo, grouping=grouping, how_many=how_many, offset=offset, size=size, folders=folders, cwd=cwd, root=root )
|
return render_template("files.html", page_title='View Files (Storage Path)', entry_data=entries, noo=noo, grouping=grouping, how_many=how_many, offset=offset, size=size, folders=folders, cwd=cwd, root=root, people=people )
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -295,6 +295,8 @@ def files_rbp():
|
|||||||
settings=Settings.query.first()
|
settings=Settings.query.first()
|
||||||
paths = settings.recycle_bin_path.split("#")
|
paths = settings.recycle_bin_path.split("#")
|
||||||
for path in paths:
|
for path in paths:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
prefix = SymlinkName("Bin",path,path+'/')
|
prefix = SymlinkName("Bin",path,path+'/')
|
||||||
if folders:
|
if folders:
|
||||||
entries+=GetEntriesInFolderView( cwd, prefix, noo, offset, how_many )
|
entries+=GetEntriesInFolderView( cwd, prefix, noo, offset, how_many )
|
||||||
@@ -315,11 +317,16 @@ def search():
|
|||||||
# always show flat results for search to start with
|
# always show flat results for search to start with
|
||||||
folders=False
|
folders=False
|
||||||
|
|
||||||
file_data=Entry.query.join(File).filter(Entry.name.ilike(f"%{request.form['term']}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(offset).limit(how_many).all()
|
term=request.form['term']
|
||||||
dir_data=Entry.query.join(File).join(EntryDirLink).join(Dir).filter(Dir.rel_path.ilike(f"%{request.form['term']}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(offset).limit(how_many).all()
|
if 'AI:' in term:
|
||||||
ai_data=Entry.query.join(File).join(FileRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person).filter(FileRefimgLink.matched==True).filter(Person.tag.ilike(f"%{request.form['term']}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(offset).limit(how_many).all()
|
term = term.replace('AI:','')
|
||||||
|
all_entries = Entry.query.join(File).join(FaceFileLink).join(Face).join(FaceRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person).filter(Person.tag.ilike(f"%{term}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(offset).limit(how_many).all()
|
||||||
all_entries = file_data + dir_data + ai_data
|
print( all_entries )
|
||||||
|
else:
|
||||||
|
file_data=Entry.query.join(File).filter(Entry.name.ilike(f"%{request.form['term']}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(offset).limit(how_many).all()
|
||||||
|
dir_data=Entry.query.join(File).join(EntryDirLink).join(Dir).filter(Dir.rel_path.ilike(f"%{request.form['term']}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(offset).limit(how_many).all()
|
||||||
|
ai_data=Entry.query.join(File).join(FaceFileLink).join(Face).join(FaceRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person).filter(Person.tag.ilike(f"%{request.form['term']}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(offset).limit(how_many).all()
|
||||||
|
all_entries = file_data + dir_data + ai_data
|
||||||
|
|
||||||
return render_template("files.html", page_title='View Files', search_term=request.form['term'], entry_data=all_entries, noo=noo, grouping=grouping, how_many=how_many, offset=offset, size=size, folders=folders, cwd=cwd, root=root )
|
return render_template("files.html", page_title='View Files', search_term=request.form['term'], entry_data=all_entries, noo=noo, grouping=grouping, how_many=how_many, offset=offset, size=size, folders=folders, cwd=cwd, root=root )
|
||||||
|
|
||||||
|
|||||||
12
job.py
12
job.py
@@ -105,18 +105,24 @@ def jobs():
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# /job/<id> -> GET -> shows status/history of jobs
|
# /job/<id> -> GET -> shows status/history of jobs
|
||||||
################################################################################
|
################################################################################
|
||||||
@app.route("/job/<id>", methods=["GET"])
|
@app.route("/job/<id>", methods=["GET","POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def joblog(id):
|
def joblog(id):
|
||||||
page_title='Show Job Details'
|
page_title='Show Job Details'
|
||||||
joblog = Job.query.get(id)
|
joblog = Job.query.get(id)
|
||||||
logs=Joblog.query.filter(Joblog.job_id==id).order_by(Joblog.log_date).all()
|
log_cnt = db.session.execute( f"select count(id) from joblog where job_id = {id}" ).first()[0]
|
||||||
|
first_logs_only = True
|
||||||
|
if request.method == 'POST':
|
||||||
|
logs=Joblog.query.filter(Joblog.job_id==id).order_by(Joblog.log_date).all()
|
||||||
|
first_logs_only = False
|
||||||
|
else:
|
||||||
|
logs=Joblog.query.filter(Joblog.job_id==id).order_by(Joblog.log_date).limit(50).all()
|
||||||
if joblog.pa_job_state == "Completed":
|
if joblog.pa_job_state == "Completed":
|
||||||
duration=(joblog.last_update-joblog.start_time)
|
duration=(joblog.last_update-joblog.start_time)
|
||||||
else:
|
else:
|
||||||
duration=(datetime.now(pytz.utc)-joblog.start_time)
|
duration=(datetime.now(pytz.utc)-joblog.start_time)
|
||||||
duration= duration-timedelta(microseconds=duration.microseconds)
|
duration= duration-timedelta(microseconds=duration.microseconds)
|
||||||
return render_template("joblog.html", job=joblog, logs=logs, duration=duration, page_title=page_title)
|
return render_template("joblog.html", job=joblog, logs=logs, log_cnt=log_cnt, duration=duration, page_title=page_title, first_logs_only=first_logs_only)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# /job/<id> -> GET -> shows status/history of jobs
|
# /job/<id> -> GET -> shows status/history of jobs
|
||||||
|
|||||||
38
main.py
38
main.py
@@ -7,14 +7,13 @@ from wtforms import SubmitField, StringField, HiddenField, SelectField, IntegerF
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from status import st, Status
|
from status import st, Status
|
||||||
from shared import CreateSelect, CreateFoldersSelect, LocationIcon, DB_URL
|
from shared import CreateSelect, CreateFoldersSelect, LocationIcon, DB_URL
|
||||||
from flask_login import login_required, current_user
|
|
||||||
|
|
||||||
|
|
||||||
# for ldap auth
|
# for ldap auth
|
||||||
from flask_ldap3_login import LDAP3LoginManager
|
from flask_ldap3_login import LDAP3LoginManager
|
||||||
from flask_login import LoginManager, login_user, UserMixin, current_user
|
from flask_login import LoginManager, login_user, login_required, UserMixin, current_user
|
||||||
from flask_ldap3_login.forms import LDAPLoginForm
|
from flask_ldap3_login.forms import LDAPLoginForm
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
@@ -22,13 +21,16 @@ import socket
|
|||||||
|
|
||||||
####################################### Flask App globals #######################################
|
####################################### Flask App globals #######################################
|
||||||
PROD_HOST="pa_web"
|
PROD_HOST="pa_web"
|
||||||
|
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
print( "Running on: {}".format( hostname) )
|
print( "Running on: {}".format( hostname) )
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
### what is this value? I gather I should change it?
|
### what is this value? I gather I should change it?
|
||||||
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URL
|
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URL
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
app.config['ENV'] = os.environ['FLASK_ENV']
|
||||||
app.config.from_mapping( SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G')
|
app.config.from_mapping( SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G')
|
||||||
|
|
||||||
# ldap config vars: (the last one is required, or python ldap freaks out)
|
# ldap config vars: (the last one is required, or python ldap freaks out)
|
||||||
@@ -49,20 +51,14 @@ login_manager = LoginManager(app) # Setup a Flask-Login Manager
|
|||||||
ldap_manager = LDAP3LoginManager(app) # Setup a LDAP3 Login Manager.
|
ldap_manager = LDAP3LoginManager(app) # Setup a LDAP3 Login Manager.
|
||||||
login_manager.login_view = "login" # default login route, failed with url_for, so hard-coded
|
login_manager.login_view = "login" # default login route, failed with url_for, so hard-coded
|
||||||
|
|
||||||
# Create a dictionary to store the users in when they authenticate
|
|
||||||
# This example stores users in memory.
|
|
||||||
users = {}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
################################# Now, import non-book classes ###################################
|
################################# Now, import non-book classes ###################################
|
||||||
from settings import Settings
|
from settings import Settings
|
||||||
from files import Entry, GetJM_Message, ClearJM_Message
|
from files import Entry, GetJM_Message, ClearJM_Message
|
||||||
from person import Person
|
from person import Person
|
||||||
from refimg import Refimg
|
|
||||||
from job import Job, GetNumActiveJobs
|
from job import Job, GetNumActiveJobs
|
||||||
from ai import aistats
|
from ai import aistats
|
||||||
from path import StoragePathNames
|
from path import StoragePathNames
|
||||||
|
from user import PAUser
|
||||||
|
|
||||||
####################################### GLOBALS #######################################
|
####################################### GLOBALS #######################################
|
||||||
# allow jinja2 to call these python functions directly
|
# allow jinja2 to call these python functions directly
|
||||||
@@ -98,9 +94,8 @@ class User(UserMixin):
|
|||||||
# returns None.
|
# returns None.
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(id):
|
def load_user(id):
|
||||||
if id in users:
|
pau=PAUser.query.filter(PAUser.dn==id).first()
|
||||||
return users[id]
|
return pau
|
||||||
return None
|
|
||||||
|
|
||||||
# Declare The User Saver for Flask-Ldap3-Login
|
# Declare The User Saver for Flask-Ldap3-Login
|
||||||
# This method is called whenever a LDAPLoginForm() successfully validates.
|
# This method is called whenever a LDAPLoginForm() successfully validates.
|
||||||
@@ -108,9 +103,14 @@ def load_user(id):
|
|||||||
# login controller.
|
# login controller.
|
||||||
@ldap_manager.save_user
|
@ldap_manager.save_user
|
||||||
def save_user(dn, username, data, memberships):
|
def save_user(dn, username, data, memberships):
|
||||||
user = User(dn, username, data)
|
pau=PAUser.query.filter(PAUser.dn==dn).first()
|
||||||
users[dn] = user
|
# if we already have a valid user/session, and say the web has restarted, just re-use it, dont make more users
|
||||||
return user
|
if pau:
|
||||||
|
return pau
|
||||||
|
pau=PAUser(dn=dn)
|
||||||
|
db.session.add(pau)
|
||||||
|
db.session.commit()
|
||||||
|
return pau
|
||||||
|
|
||||||
# default page, just the navbar
|
# default page, just the navbar
|
||||||
@app.route("/", methods=["GET"])
|
@app.route("/", methods=["GET"])
|
||||||
@@ -129,9 +129,15 @@ def login():
|
|||||||
form = LDAPLoginForm()
|
form = LDAPLoginForm()
|
||||||
form.submit.label.text="Login"
|
form.submit.label.text="Login"
|
||||||
|
|
||||||
|
# the re matches on any special LDAP chars, we dont want someone
|
||||||
|
# ldap-injecting our username, so send them back to the login page instead
|
||||||
|
if request.method == 'POST' and re.search( r'[()\\*&!]', request.form['username']):
|
||||||
|
print( f"WARNING: Detected special LDAP chars in username: {request.form['username']}")
|
||||||
|
return redirect('/login')
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Successfully logged in, We can now access the saved user object
|
# Successfully logged in, We can now access the saved user object
|
||||||
# via form.user.
|
# via form.user.
|
||||||
|
print( f"form user = {form.user}" )
|
||||||
login_user(form.user, remember=True) # Tell flask-login to log them in.
|
login_user(form.user, remember=True) # Tell flask-login to log them in.
|
||||||
next = request.args.get("next")
|
next = request.args.get("next")
|
||||||
if next:
|
if next:
|
||||||
|
|||||||
@@ -137,25 +137,19 @@ class Entry(Base):
|
|||||||
in_dir = relationship ("Dir", secondary="entry_dir_link", uselist=False )
|
in_dir = relationship ("Dir", secondary="entry_dir_link", uselist=False )
|
||||||
|
|
||||||
def FullPathOnFS(self):
|
def FullPathOnFS(self):
|
||||||
s=self.in_dir.in_path.path_prefix + '/'
|
if self.in_dir:
|
||||||
if len(self.in_dir.rel_path) > 0:
|
s=self.in_dir.in_path.path_prefix + '/'
|
||||||
s += self.in_dir.rel_path + '/'
|
if len(self.in_dir.rel_path) > 0:
|
||||||
|
s += self.in_dir.rel_path + '/'
|
||||||
|
# this occurs when we have a dir that is the root of a path
|
||||||
|
else:
|
||||||
|
s=self.dir_details.in_path.path_prefix+'/'
|
||||||
s += self.name
|
s += self.name
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<id: {self.id}, name: {self.name}, type={self.type}, exists_on_fs={self.exists_on_fs}, dir_details={self.dir_details}, file_details={self.file_details}, in_dir={self.in_dir}>"
|
return f"<id: {self.id}, name: {self.name}, type={self.type}, exists_on_fs={self.exists_on_fs}, dir_details={self.dir_details}, file_details={self.file_details}, in_dir={self.in_dir}>"
|
||||||
|
|
||||||
class FileRefimgLink(Base):
|
|
||||||
__tablename__ = "file_refimg_link"
|
|
||||||
file_id = Column(Integer, ForeignKey('file.eid'), unique=True, nullable=False, primary_key=True)
|
|
||||||
refimg_id = Column(Integer, ForeignKey('refimg.id'), unique=True, nullable=False, primary_key=True)
|
|
||||||
when_processed = Column(Float)
|
|
||||||
matched = Column(Boolean)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<file_id: {self.file_id}, refimg_id: {self.refimg_id} when_processed={self.when_processed}, matched={self.matched}"
|
|
||||||
|
|
||||||
class File(Base):
|
class File(Base):
|
||||||
__tablename__ = "file"
|
__tablename__ = "file"
|
||||||
eid = Column(Integer, ForeignKey("entry.id"), primary_key=True )
|
eid = Column(Integer, ForeignKey("entry.id"), primary_key=True )
|
||||||
@@ -181,7 +175,6 @@ class DelFile(Base):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<file_eid: {self.file_eid}, orig_path_prefix={self.orig_path_prefix}>"
|
return f"<file_eid: {self.file_eid}, orig_path_prefix={self.orig_path_prefix}>"
|
||||||
|
|
||||||
|
|
||||||
class FileType(Base):
|
class FileType(Base):
|
||||||
__tablename__ = "file_type"
|
__tablename__ = "file_type"
|
||||||
id = Column(Integer, Sequence('file_type_id_seq'), primary_key=True )
|
id = Column(Integer, Sequence('file_type_id_seq'), primary_key=True )
|
||||||
@@ -223,11 +216,39 @@ class Refimg(Base):
|
|||||||
__tablename__ = "refimg"
|
__tablename__ = "refimg"
|
||||||
id = Column(Integer, Sequence('refimg_id_seq'), primary_key=True )
|
id = Column(Integer, Sequence('refimg_id_seq'), primary_key=True )
|
||||||
fname = Column(String(256), unique=True, nullable=False)
|
fname = Column(String(256), unique=True, nullable=False)
|
||||||
encodings = Column(LargeBinary)
|
face = Column(LargeBinary, unique=True, nullable=False)
|
||||||
|
thumbnail = Column(String, unique=False, nullable=True)
|
||||||
created_on = Column(Float)
|
created_on = Column(Float)
|
||||||
|
orig_w = Column(Integer)
|
||||||
|
orig_h = Column(Integer)
|
||||||
|
face_locn = Column(String)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<id: {self.id}, fname: {self.fname}, created_on: {self.created_on}, encodings: {self.encodings}>"
|
return f"<id: {self.id}, fname: {self.fname}, created_on: {self.created_on}>"
|
||||||
|
|
||||||
|
class Face(Base):
|
||||||
|
__tablename__ = "face"
|
||||||
|
id = Column(Integer, Sequence('face_id_seq'), primary_key=True )
|
||||||
|
face = Column( LargeBinary )
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<id: {self.id}, face={self.face}"
|
||||||
|
|
||||||
|
class FaceFileLink(Base):
|
||||||
|
__tablename__ = "face_file_link"
|
||||||
|
face_id = Column(Integer, ForeignKey("face.id"), primary_key=True )
|
||||||
|
file_eid = Column(Integer, ForeignKey("file.eid"), primary_key=True )
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<face_id: {self.face_id}, file_eid={self.file_eid}"
|
||||||
|
|
||||||
|
class FaceRefimgLink(Base):
|
||||||
|
__tablename__ = "face_refimg_link"
|
||||||
|
face_id = Column(Integer, ForeignKey("face.id"), primary_key=True )
|
||||||
|
refimg_id = Column(Integer, ForeignKey("refimg.id"), primary_key=True )
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<face_id: {self.face_id}, refimg_id={self.refimg_id}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -293,13 +314,23 @@ def MessageToFE( job_id, alert, message ):
|
|||||||
session.commit()
|
session.commit()
|
||||||
return msg.id
|
return msg.id
|
||||||
|
|
||||||
def ProcessRecycleBinDir(parent_job):
|
def ProcessRecycleBinDir(job):
|
||||||
settings = session.query(Settings).first()
|
settings = session.query(Settings).first()
|
||||||
if settings == None:
|
if settings == None:
|
||||||
raise Exception("Cannot create file data with no settings / recycle bin path is missing")
|
raise Exception("Cannot create file data with no settings / recycle bin path is missing")
|
||||||
paths = settings.recycle_bin_path.split("#")
|
|
||||||
ptype = session.query(PathType).filter(PathType.name=='Bin').first()
|
ptype = session.query(PathType).filter(PathType.name=='Bin').first()
|
||||||
JobsForPaths( parent_job, paths, ptype )
|
paths = settings.recycle_bin_path.split("#")
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
if not os.path.exists( path ):
|
||||||
|
AddLogForJob( job, f"Not Importing {path} -- Path does not exist" )
|
||||||
|
continue
|
||||||
|
symlink=SymlinkName(ptype.name, path, path)
|
||||||
|
# create the Path (and Dir objects for the Bin)
|
||||||
|
AddPath( job, symlink, ptype.id )
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def ProcessStorageDirs(parent_job):
|
def ProcessStorageDirs(parent_job):
|
||||||
@@ -380,7 +411,16 @@ def AddLogForJob(job, message):
|
|||||||
session.add(log)
|
session.add(log)
|
||||||
# some logs have DEBUG: in front, so clean that up
|
# some logs have DEBUG: in front, so clean that up
|
||||||
message = message.replace("DEBUG:", "" )
|
message = message.replace("DEBUG:", "" )
|
||||||
print( f"DEBUG: {message}" )
|
# if its been more than 5 seconds since our last log, then commit to the DB to show some progress
|
||||||
|
if hasattr(job, 'last_commit'):
|
||||||
|
if (now - job.last_commit).seconds > 5:
|
||||||
|
job.last_commmit=now
|
||||||
|
print( "DELME: we have taken longer than 5 seconds since last commit so do it")
|
||||||
|
session.commit()
|
||||||
|
else:
|
||||||
|
job.last_commit = now
|
||||||
|
if DEBUG:
|
||||||
|
print( f"DEBUG: {message}" )
|
||||||
return
|
return
|
||||||
|
|
||||||
def RunJob(job):
|
def RunJob(job):
|
||||||
@@ -408,6 +448,8 @@ def RunJob(job):
|
|||||||
JobRestoreFiles(job)
|
JobRestoreFiles(job)
|
||||||
elif job.name == "processai":
|
elif job.name == "processai":
|
||||||
JobProcessAI(job)
|
JobProcessAI(job)
|
||||||
|
elif job.name == "run_ai_on":
|
||||||
|
JobRunAIOn(job)
|
||||||
else:
|
else:
|
||||||
print("ERROR: Requested to process unknown job type: {}".format(job.name))
|
print("ERROR: Requested to process unknown job type: {}".format(job.name))
|
||||||
# okay, we finished a job, so check for any jobs that are dependant on this and run them...
|
# okay, we finished a job, so check for any jobs that are dependant on this and run them...
|
||||||
@@ -491,8 +533,9 @@ def JobScanStorageDir(job):
|
|||||||
|
|
||||||
def JobForceScan(job):
|
def JobForceScan(job):
|
||||||
JobProgressState( job, "In Progress" )
|
JobProgressState( job, "In Progress" )
|
||||||
|
session.query(FaceFileLink).delete()
|
||||||
|
session.query(FaceRefimgLink).delete()
|
||||||
session.query(DelFile).delete()
|
session.query(DelFile).delete()
|
||||||
session.query(FileRefimgLink).delete()
|
|
||||||
session.query(EntryDirLink).delete()
|
session.query(EntryDirLink).delete()
|
||||||
session.query(PathDirLink).delete()
|
session.query(PathDirLink).delete()
|
||||||
session.query(Path).delete()
|
session.query(Path).delete()
|
||||||
@@ -893,11 +936,11 @@ def JobImportDir(job):
|
|||||||
FinishJob(job, f"Finished Importing: {path} - Processed {overall_file_cnt} files, Removed {rm_cnt} file(s)")
|
FinishJob(job, f"Finished Importing: {path} - Processed {overall_file_cnt} files, Removed {rm_cnt} file(s)")
|
||||||
return
|
return
|
||||||
|
|
||||||
def RunFuncOnFilesInPath( job, path, file_func ):
|
def RunFuncOnFilesInPath( job, path, file_func, count_dirs ):
|
||||||
d = session.query(Dir).join(PathDirLink).join(Path).filter(Path.path_prefix==path).filter(Dir.rel_path=='').first()
|
d = session.query(Dir).join(PathDirLink).join(Path).filter(Path.path_prefix==path).filter(Dir.rel_path=='').first()
|
||||||
files = session.query(Entry).join(EntryDirLink).filter(EntryDirLink.dir_eid==d.eid).all()
|
files = session.query(Entry).join(EntryDirLink).filter(EntryDirLink.dir_eid==d.eid).all()
|
||||||
for e in files:
|
for e in files:
|
||||||
ProcessFilesInDir(job, e, file_func)
|
ProcessFilesInDir(job, e, file_func, count_dirs)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@@ -908,15 +951,76 @@ def JobProcessAI(job):
|
|||||||
p = session.query(Path).filter(Path.path_prefix==path).first()
|
p = session.query(Path).filter(Path.path_prefix==path).first()
|
||||||
job.num_files=p.num_files
|
job.num_files=p.num_files
|
||||||
|
|
||||||
people = session.query(Person).all()
|
RunFuncOnFilesInPath( job, path, ProcessAI, True )
|
||||||
for person in people:
|
|
||||||
generateKnownEncodings(person)
|
|
||||||
|
|
||||||
RunFuncOnFilesInPath( job, path, ProcessAI )
|
|
||||||
|
|
||||||
FinishJob(job, "Finished Processesing AI")
|
FinishJob(job, "Finished Processesing AI")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def WrapperForScanFileForPerson(job, entry):
|
||||||
|
which_person=[jex.value for jex in job.extra if jex.name == "person"][0]
|
||||||
|
|
||||||
|
if entry.type.name == 'Image':
|
||||||
|
if DEBUG:
|
||||||
|
AddLogForJob( job, f'INFO: processing File: {entry.name}' )
|
||||||
|
for pid in job.ppl:
|
||||||
|
ScanFileForPerson( job, entry, pid, force=False)
|
||||||
|
# processed this file, add 1 to count
|
||||||
|
job.current_file_num+=1
|
||||||
|
return
|
||||||
|
|
||||||
|
def AddToJobImageCount(job, entry ):
|
||||||
|
if entry.type.name == 'Image':
|
||||||
|
job.num_files += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def JobRunAIOn(job):
|
||||||
|
AddLogForJob(job, f"INFO: Starting looking For faces in files job...")
|
||||||
|
which_person=[jex.value for jex in job.extra if jex.name == "person"][0]
|
||||||
|
if which_person == "all":
|
||||||
|
ppl=session.query(Person).all()
|
||||||
|
else:
|
||||||
|
ppl=session.query(Person).filter(Person.tag==which_person).all()
|
||||||
|
|
||||||
|
# start by working out how many images in this selection we will need face match on
|
||||||
|
job.num_files = 0
|
||||||
|
for jex in job.extra:
|
||||||
|
if 'eid-' in jex.name:
|
||||||
|
entry=session.query(Entry).get(jex.value)
|
||||||
|
if entry.type.name == 'Directory':
|
||||||
|
# False in last param says, dont count dirs (we won't AI a dir entry itself)
|
||||||
|
ProcessFilesInDir( job, entry, AddToJobImageCount, False )
|
||||||
|
elif entry.type.name == 'Image':
|
||||||
|
job.num_files += 1
|
||||||
|
# update job, so file count UI progress bar will work
|
||||||
|
# remember that ProcessFilesInDir updates the current_file_num so zero it out so we can start again
|
||||||
|
job.current_file_num = 0
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
ppl_lst=[]
|
||||||
|
for person in ppl:
|
||||||
|
ppl_lst.append(person.id)
|
||||||
|
|
||||||
|
job.ppl = ppl_lst
|
||||||
|
|
||||||
|
for jex in job.extra:
|
||||||
|
if 'eid-' in jex.name:
|
||||||
|
entry=session.query(Entry).get(jex.value)
|
||||||
|
if entry.type.name == 'Directory':
|
||||||
|
# False in last param says, dont count dirs (we won't AI a dir entry itself)
|
||||||
|
ProcessFilesInDir( job, entry, WrapperForScanFileForPerson, False )
|
||||||
|
elif entry.type.name == 'Image':
|
||||||
|
which_file=session.query(Entry).join(File).filter(Entry.id==jex.value).first()
|
||||||
|
if DEBUG:
|
||||||
|
AddLogForJob( job, f'INFO: processing File: {entry.name}' )
|
||||||
|
for person in ppl:
|
||||||
|
ScanFileForPerson( job, which_file, person.id, force=False)
|
||||||
|
# processed this file, add 1 to count
|
||||||
|
job.current_file_num+=1
|
||||||
|
else:
|
||||||
|
AddLogForJob( job, f'Not processing Entry: {entry.name} - not an image' )
|
||||||
|
FinishJob(job, "Finished Processesing AI")
|
||||||
|
return
|
||||||
|
|
||||||
def GenHashAndThumb(job, e):
|
def GenHashAndThumb(job, e):
|
||||||
# commit every 100 files to see progress being made but not hammer the database
|
# commit every 100 files to see progress being made but not hammer the database
|
||||||
@@ -944,7 +1048,7 @@ def ProcessAI(job, e):
|
|||||||
job.current_file_num+=1
|
job.current_file_num+=1
|
||||||
return
|
return
|
||||||
|
|
||||||
file = e.FullPathOnFS()
|
file = e.FullPathOnFS()
|
||||||
stat = os.stat(file)
|
stat = os.stat(file)
|
||||||
# find if file is newer than when we found faces before (fyi: first time faces_created_on == 0)
|
# find if file is newer than when we found faces before (fyi: first time faces_created_on == 0)
|
||||||
if stat.st_ctime > e.file_details.faces_created_on:
|
if stat.st_ctime > e.file_details.faces_created_on:
|
||||||
@@ -980,27 +1084,9 @@ def ProcessAI(job, e):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def lookForPersonInImage(job, person, unknown_encoding, e):
|
def lookForPersonInImage(job, person, unknown_encoding, e):
|
||||||
for refimg in person.refimg:
|
FinishJob( job, "THIS CODE HAS BEEN REMOVED, need to use new Face* tables, and rethink", "Failed" )
|
||||||
# lets see if we have tried this check before
|
return
|
||||||
frl=session.query(FileRefimgLink).filter(FileRefimgLink.file_id==e.id, FileRefimgLink.refimg_id==refimg.id).first()
|
|
||||||
if not frl:
|
|
||||||
frl = FileRefimgLink(refimg_id=refimg.id, file_id=e.file_details.eid)
|
|
||||||
else:
|
|
||||||
stat=os.stat( e.FullPathOnFS() )
|
|
||||||
# file & refimg are not newer then we dont need to check
|
|
||||||
if frl.matched and stat.st_ctime < frl.when_processed and refimg.created_on < frl.when_processed:
|
|
||||||
print(f"OPTIM: lookForPersonInImage: file {e.name} has a previous match for: {refimg.fname}, and the file & refimg haven't changed")
|
|
||||||
return
|
|
||||||
|
|
||||||
session.add(frl)
|
|
||||||
frl.matched=False
|
|
||||||
frl.when_processed=time.time()
|
|
||||||
deserialized_bytes = numpy.frombuffer(refimg.encodings, dtype=numpy.float64)
|
|
||||||
results = compareAI(deserialized_bytes, unknown_encoding)
|
|
||||||
if results[0]:
|
|
||||||
AddLogForJob(job, f'Found a match between: {person.tag} and {e.name}')
|
|
||||||
frl.matched=True
|
|
||||||
return
|
|
||||||
|
|
||||||
def generateUnknownEncodings(im):
|
def generateUnknownEncodings(im):
|
||||||
unknown_image = numpy.array(im)
|
unknown_image = numpy.array(im)
|
||||||
@@ -1011,37 +1097,23 @@ def generateUnknownEncodings(im):
|
|||||||
return unknown_encodings
|
return unknown_encodings
|
||||||
|
|
||||||
|
|
||||||
def generateKnownEncodings(person):
|
|
||||||
for refimg in person.refimg:
|
|
||||||
file = 'reference_images/'+refimg.fname
|
|
||||||
stat = os.stat(file)
|
|
||||||
if refimg.created_on and stat.st_ctime < refimg.created_on:
|
|
||||||
print("OPTIM: skipping re-creating encoding for refimg because file has not changed")
|
|
||||||
continue
|
|
||||||
img = face_recognition.load_image_file(file)
|
|
||||||
location = face_recognition.face_locations(img)
|
|
||||||
encodings = face_recognition.face_encodings(img, known_face_locations=location)
|
|
||||||
refimg.encodings = encodings[0].tobytes()
|
|
||||||
refimg.created_on = time.time()
|
|
||||||
session.add(refimg)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
def compareAI(known_encoding, unknown_encoding):
|
def compareAI(known_encoding, unknown_encoding):
|
||||||
results = face_recognition.compare_faces([known_encoding], unknown_encoding, tolerance=0.55)
|
results = face_recognition.compare_faces([known_encoding], unknown_encoding, tolerance=0.55)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def ProcessFilesInDir(job, e, file_func):
|
def ProcessFilesInDir(job, e, file_func, count_dirs):
|
||||||
if DEBUG==1:
|
if DEBUG==1:
|
||||||
print( f"DEBUG: ProcessFilesInDir: {e.FullPathOnFS()}")
|
print( f"DEBUG: ProcessFilesInDir: {e.FullPathOnFS()}")
|
||||||
if e.type.name != 'Directory':
|
if e.type.name != 'Directory':
|
||||||
file_func(job, e)
|
file_func(job, e)
|
||||||
else:
|
else:
|
||||||
d=session.query(Dir).filter(Dir.eid==e.id).first()
|
d=session.query(Dir).filter(Dir.eid==e.id).first()
|
||||||
job.current_file_num+=1
|
if count_dirs:
|
||||||
|
job.current_file_num+=1
|
||||||
files = session.query(Entry).join(EntryDirLink).filter(EntryDirLink.dir_eid==d.eid).all()
|
files = session.query(Entry).join(EntryDirLink).filter(EntryDirLink.dir_eid==d.eid).all()
|
||||||
for sub in files:
|
for sub in files:
|
||||||
ProcessFilesInDir(job, sub, file_func)
|
ProcessFilesInDir(job, sub, file_func, count_dirs)
|
||||||
return
|
return
|
||||||
|
|
||||||
def JobGetFileDetails(job):
|
def JobGetFileDetails(job):
|
||||||
@@ -1054,7 +1126,7 @@ def JobGetFileDetails(job):
|
|||||||
job.current_file_num = 0
|
job.current_file_num = 0
|
||||||
job.num_files = p.num_files
|
job.num_files = p.num_files
|
||||||
session.commit()
|
session.commit()
|
||||||
RunFuncOnFilesInPath( job, path_prefix, GenHashAndThumb )
|
RunFuncOnFilesInPath( job, path_prefix, GenHashAndThumb, True )
|
||||||
FinishJob(job, "File Details job finished")
|
FinishJob(job, "File Details job finished")
|
||||||
session.commit()
|
session.commit()
|
||||||
return
|
return
|
||||||
@@ -1086,8 +1158,7 @@ def isImage(file):
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def GenImageThumbnail(job, file):
|
def GenThumb(file):
|
||||||
ProcessFileForJob( job, "Generate Thumbnail from Image file: {}".format( file ), file )
|
|
||||||
try:
|
try:
|
||||||
im_orig = Image.open(file)
|
im_orig = Image.open(file)
|
||||||
im = ImageOps.exif_transpose(im_orig)
|
im = ImageOps.exif_transpose(im_orig)
|
||||||
@@ -1100,10 +1171,14 @@ def GenImageThumbnail(job, file):
|
|||||||
img_bytearray = img_bytearray.getvalue()
|
img_bytearray = img_bytearray.getvalue()
|
||||||
thumbnail = base64.b64encode(img_bytearray)
|
thumbnail = base64.b64encode(img_bytearray)
|
||||||
thumbnail = str(thumbnail)[2:-1]
|
thumbnail = str(thumbnail)[2:-1]
|
||||||
|
return thumbnail
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
AddLogForJob(job, f"WARNING: No EXIF TAF found for: {file} - error={e}")
|
AddLogForJob(job, f"WARNING: No EXIF TAF found for: {file} - error={e}")
|
||||||
return None
|
return None
|
||||||
return thumbnail
|
|
||||||
|
def GenImageThumbnail(job, file):
|
||||||
|
ProcessFileForJob( job, "Generate Thumbnail from Image file: {}".format( file ), file )
|
||||||
|
return GenThumb(file)
|
||||||
|
|
||||||
def GenVideoThumbnail(job, file):
|
def GenVideoThumbnail(job, file):
|
||||||
ProcessFileForJob( job, "Generate Thumbnail from Video file: {}".format( file ), file )
|
ProcessFileForJob( job, "Generate Thumbnail from Video file: {}".format( file ), file )
|
||||||
@@ -1251,7 +1326,7 @@ def JobDeleteFiles(job):
|
|||||||
if 'eid-' in jex.name:
|
if 'eid-' in jex.name:
|
||||||
del_me=session.query(Entry).join(File).filter(Entry.id==jex.value).first()
|
del_me=session.query(Entry).join(File).filter(Entry.id==jex.value).first()
|
||||||
MoveFileToRecycleBin(job,del_me)
|
MoveFileToRecycleBin(job,del_me)
|
||||||
now=datetime.now(pytz.utc)
|
ynw=datetime.now(pytz.utc)
|
||||||
next_job=Job(start_time=now, last_update=now, name="checkdups", state="New", wait_for=None, pa_job_state="New", current_file_num=0 )
|
next_job=Job(start_time=now, last_update=now, name="checkdups", state="New", wait_for=None, pa_job_state="New", current_file_num=0 )
|
||||||
session.add(next_job)
|
session.add(next_job)
|
||||||
MessageToFE( job.id, "success", "Completed (delete of selected files)" )
|
MessageToFE( job.id, "success", "Completed (delete of selected files)" )
|
||||||
@@ -1292,6 +1367,10 @@ def InitialValidationChecks():
|
|||||||
break
|
break
|
||||||
if not rbp_exists:
|
if not rbp_exists:
|
||||||
AddLogForJob(job, "ERROR: The bin path in settings does not exist - Please fix now");
|
AddLogForJob(job, "ERROR: The bin path in settings does not exist - Please fix now");
|
||||||
|
else:
|
||||||
|
bin_path=session.query(Path).join(PathType).filter(PathType.name=='Bin').first()
|
||||||
|
if not bin_path:
|
||||||
|
ProcessRecycleBinDir(job)
|
||||||
sp_exists=0
|
sp_exists=0
|
||||||
paths = settings.storage_path.split("#")
|
paths = settings.storage_path.split("#")
|
||||||
for path in paths:
|
for path in paths:
|
||||||
@@ -1313,9 +1392,73 @@ def InitialValidationChecks():
|
|||||||
if not rbp_exists or not sp_exists or not ip_exists:
|
if not rbp_exists or not sp_exists or not ip_exists:
|
||||||
FinishJob(job,"ERROR: Job manager EXITing until above errors are fixed by paths being created or settings being updated to valid paths", "Failed" )
|
FinishJob(job,"ERROR: Job manager EXITing until above errors are fixed by paths being created or settings being updated to valid paths", "Failed" )
|
||||||
exit(-1)
|
exit(-1)
|
||||||
|
|
||||||
FinishJob(job,"Finished Initial Validation Checks")
|
FinishJob(job,"Finished Initial Validation Checks")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def AddFaceToFile( face_data, file_eid ):
|
||||||
|
face = Face( face=face_data.tobytes() )
|
||||||
|
session.add(face)
|
||||||
|
session.commit()
|
||||||
|
ffl = FaceFileLink( face_id=face.id, file_eid=file_eid )
|
||||||
|
session.add(ffl)
|
||||||
|
session.commit()
|
||||||
|
return face
|
||||||
|
|
||||||
|
def DelFacesForFile( eid ):
|
||||||
|
session.execute( f"delete from face where id in (select face_id from face_file_link where file_eid = {eid})" )
|
||||||
|
session.commit()
|
||||||
|
return
|
||||||
|
|
||||||
|
def MatchRefimgToFace( refimg_id, face_id ):
|
||||||
|
rfl = FaceRefimgLink( refimg_id = refimg_id, face_id = face_id )
|
||||||
|
session.add(rfl)
|
||||||
|
session.commit()
|
||||||
|
return
|
||||||
|
|
||||||
|
def UnmatchedFacesForFile( eid ):
|
||||||
|
rows = session.execute( f"select f.* from face f left join face_refimg_link frl on f.id = frl.face_id join face_file_link ffl on f.id = ffl.face_id where ffl.file_eid = {eid} and frl.refimg_id is null" )
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def ScanFileForPerson( job, e, person_id, force=False ):
|
||||||
|
file_h = session.query(File).get( e.id )
|
||||||
|
# if we are forcing this, delete any old faces (this will also delete linked tables), and reset faces_created_on to None
|
||||||
|
if force:
|
||||||
|
AddLogForJob( job, f'INFO: force is true, so deleting old face information for {e.name}' )
|
||||||
|
DelFacesForFile( e.id )
|
||||||
|
file_h.faces_created_on = 0
|
||||||
|
|
||||||
|
# optimise: dont rescan if we already have faces (we are just going to try
|
||||||
|
# to match (maybe?) a refimg
|
||||||
|
if file_h.faces_created_on == 0:
|
||||||
|
if DEBUG:
|
||||||
|
AddLogForJob( job, f"DEBUG: {e.name} is missing unknown faces, generating them" )
|
||||||
|
im = face_recognition.load_image_file(e.FullPathOnFS())
|
||||||
|
face_locations = face_recognition.face_locations(im)
|
||||||
|
unknown_encodings = face_recognition.face_encodings(im, known_face_locations=face_locations)
|
||||||
|
for face in unknown_encodings:
|
||||||
|
AddFaceToFile( face, e.id )
|
||||||
|
file_h.faces_created_on = time.time()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
## now look for person
|
||||||
|
refimgs = session.query(Refimg).join(PersonRefimgLink).filter(PersonRefimgLink.person_id==person_id).all()
|
||||||
|
uf = UnmatchedFacesForFile( e.id )
|
||||||
|
if DEBUG and not uf:
|
||||||
|
AddLogForJob( job, "DEBUG: {e.name} all faces already matched - finished" )
|
||||||
|
|
||||||
|
for face in uf:
|
||||||
|
for r in refimgs:
|
||||||
|
unknown_face_data = numpy.frombuffer(face.face, dtype=numpy.float64)
|
||||||
|
refimg_face_data = numpy.frombuffer(r.face, dtype=numpy.float64)
|
||||||
|
match = compareAI(refimg_face_data, unknown_face_data)
|
||||||
|
if match[0]:
|
||||||
|
AddLogForJob(job, f'WE MATCHED: {r.fname} with file: {e.name} ')
|
||||||
|
MatchRefimgToFace( r.id, face.id )
|
||||||
|
# no need to keep looking for this face, we found it, go to next unknown face
|
||||||
|
break
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("INFO: PA job manager starting - listening on {}:{}".format( PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT) )
|
print("INFO: PA job manager starting - listening on {}:{}".format( PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT) )
|
||||||
|
|||||||
98
person.py
98
person.py
@@ -1,18 +1,36 @@
|
|||||||
from wtforms import SubmitField, StringField, HiddenField, validators, Form
|
from wtforms import SubmitField, StringField, HiddenField, validators, Form
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask import request, render_template, redirect
|
from flask import request, render_template, redirect, url_for
|
||||||
from main import db, app, ma
|
from main import db, app, ma
|
||||||
from sqlalchemy import Sequence
|
from sqlalchemy import Sequence
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from status import st, Status
|
from status import st, Status
|
||||||
from refimg import Refimg
|
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
from werkzeug import secure_filename
|
||||||
|
from shared import GenFace, GenThumb
|
||||||
|
from face import FaceRefimgLink
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Class describing Person in the database, and via sqlalchemy, connected to the DB as well
|
# Class describing Person in the database, and via sqlalchemy, connected to the DB as well
|
||||||
################################################################################
|
################################################################################
|
||||||
|
class Refimg(db.Model):
|
||||||
|
id = db.Column(db.Integer, db.Sequence('refimg_id_seq'), primary_key=True )
|
||||||
|
fname = db.Column(db.String(256), unique=True, nullable=False)
|
||||||
|
face = db.Column(db.LargeBinary, unique=True, nullable=False)
|
||||||
|
orig_w = db.Column(db.Integer)
|
||||||
|
orig_h = db.Column(db.Integer)
|
||||||
|
face_locn = db.Column(db.String)
|
||||||
|
thumbnail = db.Column(db.String, unique=True, nullable=False)
|
||||||
|
created_on = db.Column(db.Float)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<id: {}, fname: {}>".format(self.id, self.fname )
|
||||||
|
|
||||||
class PersonRefimgLink(db.Model):
|
class PersonRefimgLink(db.Model):
|
||||||
__tablename__ = "person_refimg_link"
|
__tablename__ = "person_refimg_link"
|
||||||
person_id = db.Column(db.Integer, db.ForeignKey('person.id'), unique=True, nullable=False, primary_key=True)
|
person_id = db.Column(db.Integer, db.ForeignKey('person.id'), unique=True, nullable=False, primary_key=True)
|
||||||
@@ -27,7 +45,7 @@ class Person(db.Model):
|
|||||||
tag = db.Column(db.String(48), unique=False, nullable=False)
|
tag = db.Column(db.String(48), unique=False, nullable=False)
|
||||||
surname = db.Column(db.String(48), unique=False, nullable=False)
|
surname = db.Column(db.String(48), unique=False, nullable=False)
|
||||||
firstname = db.Column(db.String(48), unique=False, nullable=False)
|
firstname = db.Column(db.String(48), unique=False, nullable=False)
|
||||||
refimg = db.relationship('Refimg', secondary=PersonRefimgLink.__table__)
|
refimg = db.relationship('Refimg', secondary=PersonRefimgLink.__table__, order_by=Refimg.id)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<tag: {}, firstname: {}, surname: {}, refimg: {}>".format(self.tag,self.firstname, self.surname, self.refimg)
|
return "<tag: {}, firstname: {}, surname: {}, refimg: {}>".format(self.tag,self.firstname, self.surname, self.refimg)
|
||||||
@@ -48,7 +66,7 @@ class PersonForm(FlaskForm):
|
|||||||
tag = StringField('Tag (searchable name):', [validators.DataRequired()])
|
tag = StringField('Tag (searchable name):', [validators.DataRequired()])
|
||||||
firstname = StringField('FirstName(s):', [validators.DataRequired()])
|
firstname = StringField('FirstName(s):', [validators.DataRequired()])
|
||||||
surname = StringField('Surname:', [validators.DataRequired()])
|
surname = StringField('Surname:', [validators.DataRequired()])
|
||||||
submit = SubmitField('Save' )
|
save = SubmitField('Save' )
|
||||||
delete = SubmitField('Delete' )
|
delete = SubmitField('Delete' )
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -71,22 +89,20 @@ def persons():
|
|||||||
def new_person():
|
def new_person():
|
||||||
form = PersonForm(request.form)
|
form = PersonForm(request.form)
|
||||||
page_title='Create new Person'
|
page_title='Create new Person'
|
||||||
reference_imgs = Refimg.query.all()
|
|
||||||
|
|
||||||
if 'surname' not in request.form:
|
if 'surname' not in request.form:
|
||||||
return render_template("person.html", reference_imgs=reference_imgs, form=form, page_title=page_title )
|
return render_template("person.html", person=None, form=form, page_title=page_title )
|
||||||
else:
|
else:
|
||||||
person = Person( tag=request.form["tag"], surname=request.form["surname"], firstname=request.form["firstname"] )
|
person = Person( tag=request.form["tag"], surname=request.form["surname"], firstname=request.form["firstname"] )
|
||||||
try:
|
try:
|
||||||
db.session.add(person)
|
db.session.add(person)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print(person)
|
|
||||||
st.SetMessage( "Created new Person ({})".format(person.tag) )
|
st.SetMessage( "Created new Person ({})".format(person.tag) )
|
||||||
return redirect( '/persons' )
|
return redirect( f'/person/{person.id}' )
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
st.SetAlert( "danger" )
|
st.SetAlert( "danger" )
|
||||||
st.SetMessage( "<b>Failed to add Person:</b> {}".format(e.orig) )
|
st.SetMessage( "<b>Failed to add Person:</b> {}".format(e.orig) )
|
||||||
return render_template("person.html", object=person, form=form, reference_imgs=reference_imgs, page_title = page_title)
|
return render_template("person.html", person=person, form=form, page_title = page_title)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# /person/<id> -> GET/POST(save or delete) -> shows/edits/delets a single
|
# /person/<id> -> GET/POST(save or delete) -> shows/edits/delets a single
|
||||||
@@ -97,34 +113,76 @@ def new_person():
|
|||||||
def person(id):
|
def person(id):
|
||||||
form = PersonForm(request.form)
|
form = PersonForm(request.form)
|
||||||
page_title='Edit Person'
|
page_title='Edit Person'
|
||||||
reference_imgs = Refimg.query.all()
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
person = Person.query.get(id)
|
person = Person.query.get(id)
|
||||||
if 'delete' in request.form:
|
if 'delete' in request.form:
|
||||||
st.SetMessage("Successfully deleted Person: ({})".format( person.tag ) )
|
st.SetMessage("Successfully deleted Person: ({})".format( person.tag ) )
|
||||||
|
# do the linkage tables by hand
|
||||||
|
db.session.execute( f"delete from face_refimg_link frl where refimg_id in ( select refimg_id from person_refimg_link where person_id = {id} )" )
|
||||||
|
db.session.execute( f"delete from person_refimg_link where person_id = {id}" )
|
||||||
person = Person.query.filter(Person.id==id).delete()
|
person = Person.query.filter(Person.id==id).delete()
|
||||||
if 'submit' in request.form and form.validate():
|
db.session.commit()
|
||||||
|
return redirect( f'/persons' )
|
||||||
|
elif request.form and form.validate():
|
||||||
st.SetMessage("Successfully Updated Person: (From: {}, {}, {})".format(person.tag, person.firstname, person.surname) )
|
st.SetMessage("Successfully Updated Person: (From: {}, {}, {})".format(person.tag, person.firstname, person.surname) )
|
||||||
person.tag = request.form['tag']
|
person.tag = request.form['tag']
|
||||||
person.surname = request.form['surname']
|
person.surname = request.form['surname']
|
||||||
person.firstname = request.form['firstname']
|
person.firstname = request.form['firstname']
|
||||||
person.refimg =[]
|
new_refs=[]
|
||||||
for ref_img in reference_imgs:
|
for ref_img in person.refimg:
|
||||||
if "ref-img-id-{}".format(ref_img.id) in request.form:
|
if "ref-img-id-{}".format(ref_img.id) in request.form:
|
||||||
print('{} was checked, id: {}'.format(ref_img.fname, ref_img.id))
|
new_refs.append(ref_img)
|
||||||
person.refimg.append(ref_img)
|
person.refimg = new_refs
|
||||||
|
db.session.add(person)
|
||||||
st.AppendMessage(" To: ({}, {}, {})".format(person.tag, person.firstname, person.surname) )
|
st.AppendMessage(" To: ({}, {}, {})".format(person.tag, person.firstname, person.surname) )
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect( '/persons' )
|
return redirect( f'/person/{person.id}' )
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
st.SetAlert( "danger" )
|
st.SetAlert( "danger" )
|
||||||
st.SetMessage( "<b>Failed to modify Person:</b> {}".format(e) )
|
st.SetMessage( "<b>Failed to modify Person:</b> {}".format(e) )
|
||||||
return render_template("person.html", form=form, reference_imgs="test", page_title=page_title)
|
return render_template("person.html", form=form, page_title=page_title)
|
||||||
else:
|
else:
|
||||||
person = Person.query.get(id)
|
person = Person.query.get(id)
|
||||||
print(person)
|
for r in person.refimg:
|
||||||
|
r.face_locn=json.loads(r.face_locn)
|
||||||
form = PersonForm(request.values, obj=person)
|
form = PersonForm(request.values, obj=person)
|
||||||
return render_template("person.html", object=person, form=form, reference_imgs=reference_imgs, page_title = page_title)
|
return render_template("person.html", person=person, form=form, page_title = page_title)
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# /add_refimg -> POST(add new refimg to a person)
|
||||||
|
################################################################################
|
||||||
|
@app.route("/add_refimg", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def add_refimg():
|
||||||
|
# now save into the DB
|
||||||
|
person = Person.query.get(request.form['person_id']);
|
||||||
|
if not person:
|
||||||
|
raise Exception("could not find person to add reference image too!")
|
||||||
|
f=request.files['refimg_file']
|
||||||
|
refimg = Refimg( fname=f.filename )
|
||||||
|
try:
|
||||||
|
# save the actual uploaded image to reference_images/
|
||||||
|
fname=secure_filename(f.filename)
|
||||||
|
if fname == "":
|
||||||
|
raise Exception("invalid filename")
|
||||||
|
|
||||||
|
fname = f"/tmp/{fname}"
|
||||||
|
f.save( fname )
|
||||||
|
refimg.thumbnail, refimg.orig_w, refimg.orig_h = GenThumb( fname )
|
||||||
|
refimg.face, face_locn = GenFace( fname )
|
||||||
|
refimg.face_locn = json.dumps(face_locn)
|
||||||
|
os.remove(fname)
|
||||||
|
person.refimg.append(refimg)
|
||||||
|
db.session.add(person)
|
||||||
|
db.session.add(refimg)
|
||||||
|
db.session.commit()
|
||||||
|
st.SetMessage( f"Associated new Refimg ({refimg.fname}) with person: {person.tag}" )
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
st.SetAlert( "danger" )
|
||||||
|
st.SetMessage( f"<b>Failed to add Refimg:</b> {e.orig}" )
|
||||||
|
except Exception as e:
|
||||||
|
st.SetAlert( "danger" )
|
||||||
|
st.SetMessage( f"<b>Failed to modify Refimg:</b> {e}" )
|
||||||
|
return redirect( url_for( 'person', id=person.id) )
|
||||||
|
|||||||
124
refimg.py
124
refimg.py
@@ -1,124 +0,0 @@
|
|||||||
from wtforms import SubmitField, StringField, HiddenField, FileField, validators, Form
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from flask import request, render_template, redirect
|
|
||||||
from main import db, app, ma
|
|
||||||
from sqlalchemy import Sequence
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
|
||||||
from status import st, Status
|
|
||||||
import os
|
|
||||||
from flask_login import login_required, current_user
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Class describing Refimg in the database, and via sqlalchemy, connected to the DB as well
|
|
||||||
################################################################################
|
|
||||||
class Refimg(db.Model):
|
|
||||||
id = db.Column(db.Integer, db.Sequence('refimg_id_seq'), primary_key=True )
|
|
||||||
fname = db.Column(db.String(256), unique=True, nullable=False)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<id: {}, fname: {}>".format(self.id, self.fname )
|
|
||||||
|
|
||||||
#class Person_Refimg_Link(db.Model):
|
|
||||||
# __tablename__ = "person_refimg_link"
|
|
||||||
# person_id = db.Column(db.Integer, db.ForeignKey('person.id'), unique=True, nullable=False, primary_key=True)
|
|
||||||
# refimg_id = db.Column(db.Integer, db.ForeignKey('refimg.id'), unique=True, nullable=False, primary_key=True)
|
|
||||||
#
|
|
||||||
# def __repr__(self):
|
|
||||||
# return "<person_id: {}, refimg_id>".format(self.person_id, self.refimg_id)
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Helper class that inherits a .dump() method to turn class Refimg into json / useful in jinja2
|
|
||||||
################################################################################
|
|
||||||
class RefimgSchema(ma.SQLAlchemyAutoSchema):
|
|
||||||
class Meta:
|
|
||||||
model = Refimg
|
|
||||||
ordered = True
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Helper class that defines a form for refimg, used to make html <form>, with field validation (via wtforms)
|
|
||||||
################################################################################
|
|
||||||
class RefimgForm(FlaskForm):
|
|
||||||
id = HiddenField()
|
|
||||||
fname = StringField('File name:', [validators.DataRequired()])
|
|
||||||
refimg_file = FileField('File name:')
|
|
||||||
submit = SubmitField('Save' )
|
|
||||||
delete = SubmitField('Delete' )
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Routes for refimg data
|
|
||||||
#
|
|
||||||
# /refimgs -> GET only -> prints out list of all refimgs
|
|
||||||
################################################################################
|
|
||||||
@app.route("/refimgs", methods=["GET"])
|
|
||||||
@login_required
|
|
||||||
def refimgs():
|
|
||||||
refimgs = Refimg.query.all()
|
|
||||||
return render_template("refimgs.html", refimgs=refimgs)
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# /refimg -> GET/POST -> creates a new refimg type and when created, takes you back to /refimgs
|
|
||||||
################################################################################
|
|
||||||
@app.route("/refimg", methods=["GET", "POST"])
|
|
||||||
@login_required
|
|
||||||
def new_refimg():
|
|
||||||
form = RefimgForm(request.form)
|
|
||||||
page_title='Create new Reference Image'
|
|
||||||
if request.method == 'GET':
|
|
||||||
return render_template("refimg.html", form=form, page_title=page_title )
|
|
||||||
else:
|
|
||||||
# now save into the DB
|
|
||||||
refimg = Refimg( fname=request.form["fname"] )
|
|
||||||
try:
|
|
||||||
# save the actual uploaded image to reference_images/
|
|
||||||
f=request.files['refimg_file']
|
|
||||||
f.save(os.path.join("reference_images", request.form["fname"]))
|
|
||||||
db.session.add(refimg)
|
|
||||||
db.session.commit()
|
|
||||||
st.SetMessage( "Created new Refimg ({})".format(refimg.fname) )
|
|
||||||
return redirect( '/refimgs' )
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
st.SetAlert( "danger" )
|
|
||||||
st.SetMessage( "<b>Failed to add Refimg:</b> {}".format(e.orig) )
|
|
||||||
except Exception as e:
|
|
||||||
st.SetAlert( "danger" )
|
|
||||||
st.SetMessage( "<b>Failed to modify Refimg:</b> {}".format(e) )
|
|
||||||
return render_template("refimg.html", form=form, page_title=page_title)
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# /refimg/<id> -> GET/POST(save or delete) -> shows/edits/delets a single
|
|
||||||
# refimg
|
|
||||||
################################################################################
|
|
||||||
@app.route("/refimg/<id>", methods=["GET", "POST"])
|
|
||||||
@login_required
|
|
||||||
def refimg(id):
|
|
||||||
form = RefimgForm(request.form)
|
|
||||||
page_title='Edit Reference Image'
|
|
||||||
if request.method == 'POST':
|
|
||||||
try:
|
|
||||||
refimg = Refimg.query.get(id)
|
|
||||||
os.remove("reference_images/{}".format(refimg.fname) )
|
|
||||||
if 'delete' in request.form:
|
|
||||||
st.SetMessage("Successfully deleted Refimg: ({})".format( refimg.fname ) )
|
|
||||||
refimg = Refimg.query.filter(Refimg.id==id).delete()
|
|
||||||
if 'submit' in request.form and form.validate():
|
|
||||||
st.SetMessage("Successfully Updated Refimg: (From: {})".format(refimg.fname))
|
|
||||||
refimg.fname = request.form['fname']
|
|
||||||
st.AppendMessage(" To: ({})".format(refimg.fname) )
|
|
||||||
# save the actual uploaded image to reference_images/
|
|
||||||
f=request.files['refimg_file']
|
|
||||||
f.save(os.path.join("reference_images", request.form["fname"]))
|
|
||||||
db.session.commit()
|
|
||||||
return redirect( '/refimgs' )
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
st.SetAlert( "danger" )
|
|
||||||
st.SetMessage( "<b>Failed to modify Refimg:</b> {}".format(e.orig) )
|
|
||||||
except Exception as e:
|
|
||||||
st.SetAlert( "danger" )
|
|
||||||
st.SetMessage( "<b>Failed to modify Refimg:</b> {}".format(e) )
|
|
||||||
return render_template("refimg.html", form=form, page_title=page_title)
|
|
||||||
else:
|
|
||||||
refimg = Refimg.query.get(id)
|
|
||||||
form = RefimgForm(request.values, obj=refimg)
|
|
||||||
return render_template("refimg.html", object=refimg, form=form, page_title = page_title)
|
|
||||||
50
shared.py
50
shared.py
@@ -1,5 +1,9 @@
|
|||||||
import socket
|
import socket
|
||||||
import os
|
import os
|
||||||
|
import face_recognition
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
PROD_HOST="pa_web"
|
PROD_HOST="pa_web"
|
||||||
@@ -9,15 +13,18 @@ ICON["Import"]="fa-file-upload"
|
|||||||
ICON["Storage"]="fa-database"
|
ICON["Storage"]="fa-database"
|
||||||
ICON["Bin"]="fa-trash-alt"
|
ICON["Bin"]="fa-trash-alt"
|
||||||
|
|
||||||
if hostname == PROD_HOST:
|
if hostname == "lappy":
|
||||||
PA_JOB_MANAGER_HOST="192.168.0.2"
|
|
||||||
DB_URL = 'postgresql+psycopg2://pa:for_now_pa@192.168.0.2:55432/pa'
|
|
||||||
elif hostname == "lappy":
|
|
||||||
PA_JOB_MANAGER_HOST="localhost"
|
PA_JOB_MANAGER_HOST="localhost"
|
||||||
DB_URL = 'postgresql+psycopg2://pa:for_now_pa@localhost:5432/pa'
|
DB_URL = 'postgresql+psycopg2://pa:for_now_pa@localhost:5432/pa'
|
||||||
else:
|
elif 'FLASK_ENV' not in os.environ or os.environ['FLASK_ENV'] == "development":
|
||||||
PA_JOB_MANAGER_HOST="localhost"
|
PA_JOB_MANAGER_HOST="localhost"
|
||||||
DB_URL = 'postgresql+psycopg2://pa:for_now_pa@mara.ddp.net:55432/pa'
|
DB_URL = 'postgresql+psycopg2://pa:for_now_pa@mara.ddp.net:65432/pa'
|
||||||
|
elif os.environ['FLASK_ENV'] == "production":
|
||||||
|
PA_JOB_MANAGER_HOST="localhost"
|
||||||
|
DB_URL = 'postgresql+psycopg2://pa:for_now_pa@padb/pa'
|
||||||
|
else:
|
||||||
|
print( "ERROR: I do not know which environment (development, etc.) and which DB (on which host to use)" )
|
||||||
|
exit( -1 )
|
||||||
|
|
||||||
PA_JOB_MANAGER_PORT=55430
|
PA_JOB_MANAGER_PORT=55430
|
||||||
|
|
||||||
@@ -70,3 +77,34 @@ def SymlinkName(ptype, path, file):
|
|||||||
if symlink[-1] == '/':
|
if symlink[-1] == '/':
|
||||||
symlink=symlink[0:-1]
|
symlink=symlink[0:-1]
|
||||||
return symlink
|
return symlink
|
||||||
|
|
||||||
|
|
||||||
|
def GenThumb(fname):
|
||||||
|
print( f"GenThumb({fname})" )
|
||||||
|
|
||||||
|
try:
|
||||||
|
im_orig = Image.open(fname)
|
||||||
|
im = ImageOps.exif_transpose(im_orig)
|
||||||
|
bands = im.getbands()
|
||||||
|
if 'A' in bands:
|
||||||
|
im = im.convert('RGB')
|
||||||
|
orig_w, orig_h = im.size
|
||||||
|
im.thumbnail((THUMBSIZE,THUMBSIZE))
|
||||||
|
img_bytearray = io.BytesIO()
|
||||||
|
im.save(img_bytearray, format='JPEG')
|
||||||
|
img_bytearray = img_bytearray.getvalue()
|
||||||
|
thumbnail = base64.b64encode(img_bytearray)
|
||||||
|
thumbnail = str(thumbnail)[2:-1]
|
||||||
|
return thumbnail, orig_w, orig_h
|
||||||
|
except Exception as e:
|
||||||
|
print( f"GenThumb failed: {e}")
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
def GenFace(fname):
|
||||||
|
img = face_recognition.load_image_file(fname)
|
||||||
|
location = face_recognition.face_locations(img)
|
||||||
|
encodings = face_recognition.face_encodings(img, known_face_locations=location)
|
||||||
|
if len(encodings):
|
||||||
|
return encodings[0].tobytes(), location
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|||||||
30
tables.sql
30
tables.sql
@@ -2,6 +2,8 @@ alter database PA set timezone to 'Australia/Victoria';
|
|||||||
|
|
||||||
create table SETTINGS( ID integer, IMPORT_PATH varchar, STORAGE_PATH varchar, RECYCLE_BIN_PATH varchar, constraint PK_SETTINGS_ID primary key(ID) );
|
create table SETTINGS( ID integer, IMPORT_PATH varchar, STORAGE_PATH varchar, RECYCLE_BIN_PATH varchar, constraint PK_SETTINGS_ID primary key(ID) );
|
||||||
|
|
||||||
|
create table PA_USER( ID integer, dn varchar, constraint PK_PA_USER_ID primary key(ID) );
|
||||||
|
|
||||||
create table FILE_TYPE ( ID integer, NAME varchar(32) unique, constraint PK_FILE_TYPE_ID primary key(ID) );
|
create table FILE_TYPE ( ID integer, NAME varchar(32) unique, constraint PK_FILE_TYPE_ID primary key(ID) );
|
||||||
|
|
||||||
create table PATH_TYPE ( ID integer, NAME varchar(16) unique, constraint PK_PATH_TYPE_ID primary key(ID) );
|
create table PATH_TYPE ( ID integer, NAME varchar(16) unique, constraint PK_PATH_TYPE_ID primary key(ID) );
|
||||||
@@ -38,13 +40,19 @@ create table ENTRY_DIR_LINK ( entry_id integer, dir_eid integer,
|
|||||||
create table PERSON ( ID integer, TAG varchar(48), FIRSTNAME varchar(48), SURNAME varchar(48),
|
create table PERSON ( ID integer, TAG varchar(48), FIRSTNAME varchar(48), SURNAME varchar(48),
|
||||||
constraint PK_PERSON_ID primary key(ID) );
|
constraint PK_PERSON_ID primary key(ID) );
|
||||||
|
|
||||||
create table REFIMG ( ID integer, FNAME varchar(256), ENCODINGS bytea,
|
create table REFIMG ( ID integer, FNAME varchar(128), FACE bytea, ORIG_W integer, ORIG_H integer, FACE_LOCN varchar(32), CREATED_ON float, THUMBNAIL varchar,
|
||||||
CREATED_ON float,
|
|
||||||
constraint PK_REFIMG_ID primary key(ID) );
|
constraint PK_REFIMG_ID primary key(ID) );
|
||||||
|
|
||||||
create table FILE_REFIMG_LINK ( FILE_ID integer, REFIMG_ID integer, WHEN_PROCESSED float, MATCHED boolean,
|
create table FACE( ID integer, FACE bytea, constraint PK_FACE_ID primary key(ID) );
|
||||||
constraint PK_FRL primary key(FILE_ID, REFIMG_ID),
|
|
||||||
constraint FK_FRL_FILE_ID foreign key (FILE_ID) references FILE(EID),
|
create table FACE_FILE_LINK( FACE_ID integer, FILE_EID integer,
|
||||||
|
constraint PK_FFL_FACE_ID_FILE_ID primary key(FACE_ID, FILE_EID),
|
||||||
|
constraint FK_FFL_FACE_ID foreign key (FACE_ID) references FACE(ID) on delete cascade,
|
||||||
|
constraint FK_FFL_FILE_EID foreign key (FILE_EID) references FILE(EID) );
|
||||||
|
|
||||||
|
create table FACE_REFIMG_LINK( FACE_ID integer, REFIMG_ID integer,
|
||||||
|
constraint PK_FRL_FACE_ID_REFIMG_ID primary key(FACE_ID, REFIMG_ID),
|
||||||
|
constraint FK_FRL_FACE_ID foreign key (FACE_ID) references FACE(ID) on delete cascade,
|
||||||
constraint FK_FRL_REFIMG_ID foreign key (REFIMG_ID) references REFIMG(ID) );
|
constraint FK_FRL_REFIMG_ID foreign key (REFIMG_ID) references REFIMG(ID) );
|
||||||
|
|
||||||
create table PERSON_REFIMG_LINK ( PERSON_ID integer, REFIMG_ID integer,
|
create table PERSON_REFIMG_LINK ( PERSON_ID integer, REFIMG_ID integer,
|
||||||
@@ -69,6 +77,8 @@ create table PA_JOB_MANAGER_FE_MESSAGE ( ID integer, JOB_ID integer, ALERT varch
|
|||||||
constraint PA_JOB_MANAGER_FE_ACKS_ID primary key(ID),
|
constraint PA_JOB_MANAGER_FE_ACKS_ID primary key(ID),
|
||||||
constraint FK_PA_JOB_MANAGER_FE_MESSAGE_JOB_ID foreign key(JOB_ID) references JOB(ID) );
|
constraint FK_PA_JOB_MANAGER_FE_MESSAGE_JOB_ID foreign key(JOB_ID) references JOB(ID) );
|
||||||
|
|
||||||
|
create sequence PA_USER_ID_SEQ;
|
||||||
|
create sequence FACE_ID_SEQ;
|
||||||
create sequence PATH_ID_SEQ;
|
create sequence PATH_ID_SEQ;
|
||||||
create sequence PATH_TYPE_ID_SEQ;
|
create sequence PATH_TYPE_ID_SEQ;
|
||||||
create sequence FILE_ID_SEQ;
|
create sequence FILE_ID_SEQ;
|
||||||
@@ -98,15 +108,7 @@ insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'dad', 'Damien',
|
|||||||
insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'mum', 'Mandy', 'De Paoli' );
|
insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'mum', 'Mandy', 'De Paoli' );
|
||||||
insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'cam', 'Cameron', 'De Paoli' );
|
insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'cam', 'Cameron', 'De Paoli' );
|
||||||
insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'mich', 'Michelle', 'De Paoli' );
|
insert into PERSON values ( (select nextval('PERSON_ID_SEQ')), 'mich', 'Michelle', 'De Paoli' );
|
||||||
insert into REFIMG values ( (select nextval('REFIMG_ID_SEQ')), 'dad.jpg');
|
|
||||||
insert into REFIMG values ( (select nextval('REFIMG_ID_SEQ')), 'mum.jpg');
|
|
||||||
insert into REFIMG values ( (select nextval('REFIMG_ID_SEQ')), 'cam.jpg');
|
|
||||||
insert into REFIMG values ( (select nextval('REFIMG_ID_SEQ')), 'mich.jpg');
|
|
||||||
insert into PERSON_REFIMG_LINK values ( 1, 1 );
|
|
||||||
insert into PERSON_REFIMG_LINK values ( 2, 2 );
|
|
||||||
insert into PERSON_REFIMG_LINK values ( 3, 3 );
|
|
||||||
insert into PERSON_REFIMG_LINK values ( 4, 4 );
|
|
||||||
-- DEV:
|
-- DEV:
|
||||||
insert into SETTINGS ( id, import_path, storage_path, recycle_bin_path ) values ( (select nextval('SETTINGS_ID_SEQ')), '/home/ddp/src/photoassistant/images_to_process/#c:/Users/cam/Desktop/code/python/photoassistant/photos/#/home/ddp/src/photoassistant/new_img_dir/', '/home/ddp/src/photoassistant/storage/', '/home/ddp/src/photoassistant/.pa_bin/' );
|
insert into SETTINGS ( id, import_path, storage_path, recycle_bin_path ) values ( (select nextval('SETTINGS_ID_SEQ')), '/home/ddp/src/photoassistant/images_to_process/#c:/Users/cam/Desktop/code/python/photoassistant/photos/#/home/ddp/src/photoassistant/new_img_dir/', '/home/ddp/src/photoassistant/storage/#c:/Users/cam/Desktop/code/python/photoassistant/storage/', '/home/ddp/src/photoassistant/.pa_bin/#c:/Users/cam/Desktop/code/python/photoassistant/.pa_bin/' );
|
||||||
-- PROD:
|
-- PROD:
|
||||||
--insert into SETTINGS ( id, import_path, storage_path, recycle_bin_path ) values ( (select nextval('SETTINGS_ID_SEQ')), '/export/docker/storage/Camera_uploads/', '/export/docker/storage/photos/', '/export/docker/storage/.pa_bin/' );
|
--insert into SETTINGS ( id, import_path, storage_path, recycle_bin_path ) values ( (select nextval('SETTINGS_ID_SEQ')), '/export/docker/storage/Camera_uploads/', '/export/docker/storage/photos/', '/export/docker/storage/.pa_bin/' );
|
||||||
|
|||||||
@@ -3,13 +3,19 @@
|
|||||||
{% block main_content %}
|
{% block main_content %}
|
||||||
<h3>Basic AI stats</h3>
|
<h3>Basic AI stats</h3>
|
||||||
<table class="table table-striped table-sm">
|
<table class="table table-striped table-sm">
|
||||||
<tbody><thead class="thead-light"><tr><th>File</th><th>AI Matched people</th></thead>
|
<tbody><thead class="thead-light"><tr><th>What</th><th>Amount</th></tr></thead>
|
||||||
{% for e in entries %}
|
<tr><td>Files with a face</td><td>{{fstats['files_with_a_face']}}</td></tr>
|
||||||
<tr><td>{{e.name}}</td><td>
|
<tr><td>Files with a matched face</td><td>{{fstats['files_with_a_match']}}</td></tr>
|
||||||
{% for p in e.people %}
|
<tr><td>Files with missing matches</td><td>{{fstats['files_with_missing_matches']}}</td></tr>
|
||||||
{{p.tag}}
|
<tr><td>All faces found</td><td>{{fstats['all_faces']}}</td></tr>
|
||||||
{% endfor %}
|
<tr><td>All faces matched</td><td>{{fstats['all_matched_faces']}}</td></tr>
|
||||||
</td></tr>
|
<tr><td>All faces unmatched</td><td>{{fstats['all_unmatched_faces']}}</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<tbody><thead class="thead-light"><tr><th>Person (tag)</th><th>Number of files matched</th></thead>
|
||||||
|
{% for s in stats %}
|
||||||
|
<tr><td>{{s[0]}}</td><td>{{s[1]}}</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody></table>
|
</tbody></table>
|
||||||
{% endblock main_content %}
|
{% endblock main_content %}
|
||||||
|
|||||||
@@ -49,7 +49,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light justify-content-between">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light justify-content-between">
|
||||||
<a class="navbar-brand" href="/">Photo Assistant</a>
|
{% if config.env == "Production" %}
|
||||||
|
<a class="navbar-brand" href="/">Photo Assistant</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="navbar-brand bg-secondary text-white px-2" style="border-radius:4px" href="/">PA (DEV)</a>
|
||||||
|
{% endif %}
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -71,13 +75,6 @@
|
|||||||
<a class="dropdown-item" href="{{url_for('persons')}}">Show People</a>
|
<a class="dropdown-item" href="{{url_for('persons')}}">Show People</a>
|
||||||
</div>
|
</div>
|
||||||
</div class="nav-item dropdown">
|
</div class="nav-item dropdown">
|
||||||
<div class="nav-item dropdown">
|
|
||||||
<a class="nav-item dropdown nav-link dropdown-toggle" href="#" id="RefMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Ref Image</a>
|
|
||||||
<div class="dropdown-menu" aria-labelledby="AIMenu">
|
|
||||||
<a class="dropdown-item" href="{{url_for('new_refimg')}}">Create Reference Image</a>
|
|
||||||
<a class="dropdown-item" href="{{url_for('refimgs')}}">View Reference Images</a>
|
|
||||||
</div>
|
|
||||||
</div class="nav-item dropdown">
|
|
||||||
<div class="nav-item dropdown">
|
<div class="nav-item dropdown">
|
||||||
<a class="nav-item dropdown nav-link dropdown-toggle" href="#" id="AIMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">AI</a>
|
<a class="nav-item dropdown nav-link dropdown-toggle" href="#" id="AIMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">AI</a>
|
||||||
<div class="dropdown-menu" aria-labelledby="AIMenu">
|
<div class="dropdown-menu" aria-labelledby="AIMenu">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pagesize">{{DD.total_dups}} duplicate files ({{DD.uniq_dups}} Unique) -- Showing </label>
|
<label for="pagesize">{{DD.total_dups}} duplicate files ({{DD.uniq_dups}} Unique) -- Showing </label>
|
||||||
<select id="pagesize" class="form form-control" name="pagesize" onChange="ResetPageSize()">
|
<select id="pagesize" class="form form-control" name="pagesize" onChange="ResetPageSize()">
|
||||||
{% for o in "5", "10", "15", "20", "25", "50", "75", "100", "200" %}
|
{% for o in "5", "10", "15", "20", "25", "50", "75", "100", "200", "500", "1000", "5000", "20000" %}
|
||||||
<option
|
<option
|
||||||
{% if o|int == pagesize %}
|
{% if o|int == pagesize %}
|
||||||
selected
|
selected
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
caption-side: bottom;
|
caption-side: bottom;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<form id="main_form" method="POST">
|
<form id="main_form" method="POST">
|
||||||
<input type="hidden" name="cwd" id="cwd" value="{{cwd}}">
|
<input type="hidden" name="cwd" id="cwd" value="{{cwd}}">
|
||||||
@@ -122,7 +121,7 @@
|
|||||||
{# rare event of empty folder, still need to show back button #}
|
{# rare event of empty folder, still need to show back button #}
|
||||||
{% if folders and entry_data|length == 0 %}
|
{% if folders and entry_data|length == 0 %}
|
||||||
{% if cwd != root %}
|
{% if cwd != root %}
|
||||||
<figure class="px-1 dir" dir="{{cwd|ParentPath}}">
|
<figure class="px-1 dir entry" ecnt="1" dir="{{cwd|ParentPath}}">
|
||||||
<span style="font-size:{{(size|int-22)/2}}" class="fa-stack">
|
<span style="font-size:{{(size|int-22)/2}}" class="fa-stack">
|
||||||
<i style="color:grey" class="fas fa-folder fa-stack-2x"></i>
|
<i style="color:grey" class="fas fa-folder fa-stack-2x"></i>
|
||||||
<i class="fas fa-level-up-alt fa-flip-horizontal fa-stack-1x fa-inverse"></i>
|
<i class="fas fa-level-up-alt fa-flip-horizontal fa-stack-1x fa-inverse"></i>
|
||||||
@@ -140,7 +139,7 @@
|
|||||||
{% for obj in entry_data %}
|
{% for obj in entry_data %}
|
||||||
{% if loop.index==1 and folders %}
|
{% if loop.index==1 and folders %}
|
||||||
{% if cwd != root %}
|
{% if cwd != root %}
|
||||||
<figure class="px-1 dir" dir="{{cwd|ParentPath}}">
|
<figure class="px-1 dir entry" ecnt="{{loop.index}}" dir="{{cwd|ParentPath}}">
|
||||||
<span style="font-size:{{(size|int-22)/2}}" class="fa-stack">
|
<span style="font-size:{{(size|int-22)/2}}" class="fa-stack">
|
||||||
<i style="color:grey" class="fas fa-folder fa-stack-2x"></i>
|
<i style="color:grey" class="fas fa-folder fa-stack-2x"></i>
|
||||||
<i class="fas fa-level-up-alt fa-flip-horizontal fa-stack-1x fa-inverse"></i>
|
<i class="fas fa-level-up-alt fa-flip-horizontal fa-stack-1x fa-inverse"></i>
|
||||||
@@ -188,7 +187,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if obj.type.name != "Directory" %}
|
{% if obj.type.name != "Directory" %}
|
||||||
{% if (not folders) or ((obj.in_dir.in_path.path_prefix+'/'+obj.in_dir.rel_path+'/'+obj.name) | TopLevelFolderOf(cwd)) %}
|
{% if (not folders) or ((obj.in_dir.in_path.path_prefix+'/'+obj.in_dir.rel_path+'/'+obj.name) | TopLevelFolderOf(cwd)) %}
|
||||||
<figure id="{{obj.id}}" img="{{loop.index-1}}" class="figure mx-1" path_type="{{obj.in_dir.in_path.type.name}}" size="{{obj.file_details.size_mb}}" hash="{{obj.file_details.hash}}" in_dir="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}" fname="{{obj.name}}" yr="{{obj.file_details.year}}" date="{{obj.file_details.year}}{{"%02d" % obj.file_details.month}}{{"%02d" % obj.file_details.day}}" pretty_date="{{obj.file_details.day}}/{{obj.file_details.month}}/{{obj.file_details.year}}">
|
<figure id="{{obj.id}}" ecnt="{{loop.index}}" class="figure entry mx-1" path_type="{{obj.in_dir.in_path.type.name}}" size="{{obj.file_details.size_mb}}" hash="{{obj.file_details.hash}}" in_dir="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}" fname="{{obj.name}}" yr="{{obj.file_details.year}}" date="{{obj.file_details.year}}{{"%02d" % obj.file_details.month}}{{"%02d" % obj.file_details.day}}" pretty_date="{{obj.file_details.day}}/{{obj.file_details.month}}/{{obj.file_details.year}}">
|
||||||
{% if obj.type.name=="Image" %}
|
{% if obj.type.name=="Image" %}
|
||||||
<div style="position:relative; width:100%">
|
<div style="position:relative; width:100%">
|
||||||
<a href="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}/{{obj.name}}"><img class="thumb" height="{{size}}" src="data:image/jpeg;base64,{{obj.file_details.thumbnail}}"></img></a>
|
<a href="{{obj.in_dir.in_path.path_prefix}}/{{obj.in_dir.rel_path}}/{{obj.name}}"><img class="thumb" height="{{size}}" src="data:image/jpeg;base64,{{obj.file_details.thumbnail}}"></img></a>
|
||||||
@@ -223,7 +222,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{# if this dir is the toplevel of the cwd, show the folder icon #}
|
{# if this dir is the toplevel of the cwd, show the folder icon #}
|
||||||
{% if dirname| TopLevelFolderOf(cwd) %}
|
{% if dirname| TopLevelFolderOf(cwd) %}
|
||||||
<figure class="px-1 dir" dir="{{dirname}}">
|
<figure class="px-1 dir entry" id={{obj.id}} ecnt={{loop.index}} dir="{{dirname}}">
|
||||||
<i style="font-size:{{size|int-22}};" class="fas fa-folder"></i>
|
<i style="font-size:{{size|int-22}};" class="fas fa-folder"></i>
|
||||||
<figcaption class="figure-caption text-center text-wrap text-break">{{obj.name}}</figcaption>
|
<figcaption class="figure-caption text-center text-wrap text-break">{{obj.name}}</figcaption>
|
||||||
</figure class="figure">
|
</figure class="figure">
|
||||||
@@ -271,6 +270,13 @@ function GetSelnAsData()
|
|||||||
return to_del
|
return to_del
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RunAIOnSeln(person)
|
||||||
|
{
|
||||||
|
post_data = GetSelnAsData()
|
||||||
|
post_data += '&person='+person.replace('ai-','')
|
||||||
|
$.ajax({ type: 'POST', data: post_data, url: '/run_ai_on', success: function(data){ window.location='/'; return false; } })
|
||||||
|
}
|
||||||
|
|
||||||
function DelDBox(del_or_undel)
|
function DelDBox(del_or_undel)
|
||||||
{
|
{
|
||||||
to_del = GetSelnAsData()
|
to_del = GetSelnAsData()
|
||||||
@@ -398,19 +404,25 @@ function DoSel(e, el)
|
|||||||
}
|
}
|
||||||
if( e.shiftKey )
|
if( e.shiftKey )
|
||||||
{
|
{
|
||||||
st=Number($('.highlight').first().attr('img'))
|
st=Number($('.highlight').first().attr('ecnt'))
|
||||||
end=Number($('.highlight').last().attr('img'))
|
end=Number($('.highlight').last().attr('ecnt'))
|
||||||
clicked=Number($(el).attr('img'))
|
clicked=Number($(el).attr('ecnt'))
|
||||||
|
if( ! folders )
|
||||||
|
{
|
||||||
|
st -= 1
|
||||||
|
end -= 1
|
||||||
|
clicked -= 1
|
||||||
|
}
|
||||||
// if we shift-click first element, then st/end are NaN, so just highlightthe one clicked
|
// if we shift-click first element, then st/end are NaN, so just highlightthe one clicked
|
||||||
if( isNaN(st) )
|
if( isNaN(st) )
|
||||||
{
|
{
|
||||||
$('.figure').slice( clicked, clicked+1 ).addClass('highlight')
|
$('.entry').slice( clicked, clicked+1 ).addClass('highlight')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if( clicked > end )
|
if( clicked > end )
|
||||||
$('.figure').slice( end, clicked+1 ).addClass('highlight')
|
$('.entry').slice( end, clicked+1 ).addClass('highlight')
|
||||||
else
|
else
|
||||||
$('.figure').slice( clicked, st ).addClass('highlight')
|
$('.entry').slice( clicked, st ).addClass('highlight')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$('.highlight').removeClass('highlight')
|
$('.highlight').removeClass('highlight')
|
||||||
@@ -429,6 +441,24 @@ function SetButtonState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FiguresOrDirsOrBoth() {
|
||||||
|
var figure=false
|
||||||
|
var dir=false
|
||||||
|
$('.highlight').each(function( index ) {
|
||||||
|
if( $(this).hasClass('figure') ) {
|
||||||
|
figure=true
|
||||||
|
}
|
||||||
|
if( $(this).hasClass('dir') ) {
|
||||||
|
dir=true
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
if( figure & ! dir )
|
||||||
|
return "figure"
|
||||||
|
if( ! figure & dir )
|
||||||
|
return "dir"
|
||||||
|
return "both"
|
||||||
|
}
|
||||||
|
|
||||||
function SelContainsBinAndNotBin() {
|
function SelContainsBinAndNotBin() {
|
||||||
var bin=false
|
var bin=false
|
||||||
var not_bin=false
|
var not_bin=false
|
||||||
@@ -458,17 +488,35 @@ function NoSel() {
|
|||||||
$('.figure').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
|
$('.figure').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
|
||||||
$(document).on('click', function(e) { $('.highlight').removeClass('highlight') ; SetButtonState() });
|
$(document).on('click', function(e) { $('.highlight').removeClass('highlight') ; SetButtonState() });
|
||||||
|
|
||||||
|
|
||||||
|
// different context menu on files
|
||||||
$.contextMenu({
|
$.contextMenu({
|
||||||
selector: '.figure',
|
selector: '.entry',
|
||||||
build: function($triggerElement, e){
|
build: function($triggerElement, e){
|
||||||
if( NoSel() )
|
// when right-clicking & no selection add one OR deal with ctrl/shift right-lick as it always changes seln
|
||||||
|
if( NoSel() || e.ctrlKey || e.shiftKey )
|
||||||
DoSel(e, e.currentTarget )
|
DoSel(e, e.currentTarget )
|
||||||
item_list = {
|
|
||||||
details: { name: "Details..." },
|
if( FiguresOrDirsOrBoth() == "figure" )
|
||||||
view: { name: "View File" },
|
item_list = {
|
||||||
sep: "---",
|
details: { name: "Details..." },
|
||||||
move: { name: "Move selected file(s) to new storage folder" }
|
view: { name: "View File" },
|
||||||
}
|
sep: "---",
|
||||||
|
move: { name: "Move selected file(s) to new storage folder" },
|
||||||
|
sep2: "---" }
|
||||||
|
else
|
||||||
|
item_list = {}
|
||||||
|
|
||||||
|
item_list['ai'] = {
|
||||||
|
name: "Scan file for faces",
|
||||||
|
items: {
|
||||||
|
{% for p in people %}
|
||||||
|
"ai-{{p.tag}}": {"name": "{{p.tag}}"},
|
||||||
|
{% endfor %}
|
||||||
|
"ai-all": {"name": "all"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if( SelContainsBinAndNotBin() ) {
|
if( SelContainsBinAndNotBin() ) {
|
||||||
item_list['both']= { name: 'Cannot delete and restore at same time', disabled: true }
|
item_list['both']= { name: 'Cannot delete and restore at same time', disabled: true }
|
||||||
} else {
|
} else {
|
||||||
@@ -485,6 +533,7 @@ $.contextMenu({
|
|||||||
if( key == "move" ) { MoveDBox() }
|
if( key == "move" ) { MoveDBox() }
|
||||||
if( key == "del" ) { DelDBox('Delete') }
|
if( key == "del" ) { DelDBox('Delete') }
|
||||||
if( key == "undel" ) { DelDBox('Restore') }
|
if( key == "undel" ) { DelDBox('Restore') }
|
||||||
|
if( key.startsWith("ai")) { RunAIOnSeln(key) }
|
||||||
},
|
},
|
||||||
items: item_list
|
items: item_list
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,6 +66,14 @@
|
|||||||
{% for log in logs %}
|
{% for log in logs %}
|
||||||
<tr><td>{{log.log_date|vicdate}}</td><td>{{log.log|safe}}</td></tr>
|
<tr><td>{{log.log_date|vicdate}}</td><td>{{log.log|safe}}</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if log_cnt > logs|length %}
|
||||||
|
<tr>
|
||||||
|
<td class="align-middle">Remaining logs truncated</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-info my-0 py-1" onClick="document.body.innerHTML+='<form id=_fm method=POST action={{url_for('joblog', id=job.id)}}></form>';document.getElementById('_fm').submit();">Show all logs</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +81,7 @@
|
|||||||
{% endblock main_content %}
|
{% endblock main_content %}
|
||||||
{% block script_content %}
|
{% block script_content %}
|
||||||
<script>
|
<script>
|
||||||
{% if job.pa_job_state != "Completed" %}
|
{% if first_logs_only and job.pa_job_state != "Completed" %}
|
||||||
setTimeout(function(){ window.location.reload(1); }, 3000 )
|
setTimeout(function(){ window.location.reload(1); }, 3000 )
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
78
templates/login.html
Normal file
78
templates/login.html
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- Required meta tags -->
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
<!-- font awesome -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='upstream/fontawesome-free-5.15.3-web/css/all.min.css' ) }}">
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='upstream/bootstrap-4.6.0-dist/css/bootstrap.min.css' ) }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='upstream/jquery.contextMenu.css' ) }}">
|
||||||
|
|
||||||
|
<!-- code to get bootstrap to work -->
|
||||||
|
<script src="{{ url_for( 'static', filename='upstream/jquery-3.6.0.min.js')}}"></script>
|
||||||
|
<script src="{{ url_for( 'static', filename='upstream/bootstrap-4.6.0-dist/js/bootstrap.min.js')}}"></script>
|
||||||
|
<script src="{{ url_for( 'static', filename='upstream/jquery.contextMenu.min.js')}}"></script>
|
||||||
|
<script src="{{ url_for( 'static', filename='upstream/jquery.ui.position.min.js')}}"></script>
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
|
||||||
|
{% import "bootstrap/wtf.html" as wtf %}
|
||||||
|
<style>
|
||||||
|
.highlight { box-shadow: 0 0 7px 4px #5bc0de }
|
||||||
|
.sm-txt { font-size: 0.8rem }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% if form.errors|length > 0 %}
|
||||||
|
<div class="row my-5">
|
||||||
|
<alert class="alert alert-danger alert-dismissible fade show">
|
||||||
|
{% set last_err = namespace(txt="") %}
|
||||||
|
{% for e in form.errors %}
|
||||||
|
{% if last_err.txt != form.errors[e] %}
|
||||||
|
{% set err = form.errors[e]|replace("['", "" ) %}
|
||||||
|
{% set err = err|replace("']", "" ) %}
|
||||||
|
{{err}}
|
||||||
|
{% set last_err.txt=form.errors[e] %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</alert>
|
||||||
|
</div class="row">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# <div class="row my-5 col-lg-6 rounded border border-primary border-3"> #}
|
||||||
|
|
||||||
|
<div class="row my-5 col-lg-6" style="border: 3px solid #5bc0de; border-radius: 15px;">
|
||||||
|
<h3 class="col-lg-12 my-3 text-center" style="color: #5bc0de">
|
||||||
|
<span class="fa-stack">
|
||||||
|
<i class="fas fa-brain fa-stack-2x"></i>
|
||||||
|
<i class="far fa-image fa-stack-1x fa-inverse"></i>
|
||||||
|
</span>
|
||||||
|
Photo Assistant Login</h3>
|
||||||
|
<form class="form form-inline" method="POST">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<label labelfor="username" class="text-right form-control-plaintext col-lg-4 text-info">Username:</label>
|
||||||
|
<input class="form-control col-lg-8" type="text" name="username"></input>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<label labelfor="password" class="text-right form-control-plaintext col-lg-4 text-info">Password:</label>
|
||||||
|
<input class="form-control col-lg-8" type="password" name="password"></input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-12 my-2 text-center">
|
||||||
|
{{ form.submit( class="form-control text-info") }}
|
||||||
|
</div>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
</form>
|
||||||
|
</div class="row">
|
||||||
|
</div class="container">
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,42 +1,118 @@
|
|||||||
{% extends "base.html" %} {% block main_content %}
|
{% extends "base.html" %} {% block main_content %}
|
||||||
<div class="container">
|
<script>
|
||||||
<h3 class="offset-lg-2">{{page_title}}</h3>
|
// Define this once and before it will be called, hence at the top of this file
|
||||||
<div class="row">
|
function DrawRefimg(img, canvas, orig_face )
|
||||||
<form class="form form-inline col-lg-12" action="" method="POST">
|
{
|
||||||
{% for field in form %}
|
// FIXME: should get this from shared.py, not sure why this doesnt work at present
|
||||||
{% if field.type == 'HiddenField' or field.type == 'CSRFTokenField' %}
|
thumbsize=256
|
||||||
{{field}}<br>
|
|
||||||
{% elif field.type != 'SubmitField' %}
|
context=canvas.getContext('2d')
|
||||||
<div class="form-row col-lg-12">
|
// another call to this func will occur on load, so skip this one
|
||||||
{{ field.label( class="col-lg-2" ) }}
|
if( img.width == 0 )
|
||||||
{{ field( class="form-control col" ) }}
|
return
|
||||||
</div class="form-row col-lg-12">
|
|
||||||
{% endif %}
|
// only set canvas.width once we have valid img dimensions
|
||||||
{% endfor %}
|
canvas.width=img.width/2
|
||||||
<div class="form-row col-lg-12">
|
|
||||||
<span class="col-lg-2"><center>Reference Images:</center></span>
|
// actually draw the pixel images to the canvas at the right size
|
||||||
<div class="form-row col">
|
context.drawImage(img, 0, 0, img.width/(img.height/canvas.height), canvas.height);
|
||||||
{% for ref_img in reference_imgs %}
|
|
||||||
<div class="form-control col-lg">
|
// draw rectangle on face
|
||||||
<input id="ref-img-id-{{ref_img.id}}" name="ref-img-id-{{ref_img.id}}" type="checkbox"
|
context.beginPath();
|
||||||
{% if object is defined and ref_img in object.refimg %}
|
new_x=(orig_face.x/orig_face.orig_w)*img.width/(img.height/canvas.height)
|
||||||
checked
|
new_y=(orig_face.y/orig_face.orig_h)*thumbsize/(img.height/canvas.height)
|
||||||
{% endif %}
|
new_w=(orig_face.w/orig_face.orig_w)*img.width/(img.height/canvas.height)
|
||||||
> {{ref_img.fname}}</input>
|
new_h=(orig_face.h/orig_face.orig_h)*thumbsize/(img.height/canvas.height)
|
||||||
|
context.rect(new_x, new_y, new_w, new_h)
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeStyle = 'green';
|
||||||
|
context.stroke();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<h3 class="offset-lg-3">{{page_title}}</h3>
|
||||||
|
<form id="pfm" class="form form-inline" action="" method="POST">
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field.type == 'HiddenField' or field.type == 'CSRFTokenField' %}
|
||||||
|
{{field}}<br>
|
||||||
|
{% elif field.type != 'SubmitField' %}
|
||||||
|
<div class="form-row col-lg-12">
|
||||||
|
{{ field.label( class="col-lg-3" ) }}
|
||||||
|
{{ field( class="form-control col" ) }}
|
||||||
|
</div class="form-row col-lg-12">
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="form-row col-lg-12">
|
||||||
|
<span class="col-lg-3"><center>Reference Images:</center></span>
|
||||||
|
{% for refimg in person.refimg %}
|
||||||
|
{% set offset="" %}
|
||||||
|
{% if (loop.index % 10) == 0 %}
|
||||||
|
{% set offset= "offset-lg-3" %}
|
||||||
|
{% endif %}
|
||||||
|
<div id="RI{{refimg.id}}" class="px-0 col-lg-1 w-100 {{offset}}">
|
||||||
|
<center>
|
||||||
|
<input type="hidden" id="ref-img-id-{{refimg.id}}" name="ref-img-id-{{refimg.id}}" value="1"></input>
|
||||||
|
<figure style="border: 1px solid #5bc0de; border-radius: 3px;" class="figure my-auto h-100 w-100">
|
||||||
|
<div style="position:relative">
|
||||||
|
<canvas id="c_{{refimg.id}}" height="128"></canvas>
|
||||||
|
<script>
|
||||||
|
var im_{{refimg.id}}=new Image();
|
||||||
|
im_{{refimg.id}}.src="data:image/jpeg;base64,{{refimg.thumbnail}}";
|
||||||
|
|
||||||
|
// store this stuff in an javascript Object to use when document is ready event is triggered
|
||||||
|
var orig_face_{{refimg.id}}=new Object;
|
||||||
|
orig_face_{{refimg.id}}.x = {{refimg.face_locn[0][3]}}
|
||||||
|
orig_face_{{refimg.id}}.y = {{refimg.face_locn[0][0]}}
|
||||||
|
orig_face_{{refimg.id}}.w = {{refimg.face_locn[0][1]}}-{{refimg.face_locn[0][3]}}
|
||||||
|
orig_face_{{refimg.id}}.h = {{refimg.face_locn[0][2]}}-{{refimg.face_locn[0][0]}}
|
||||||
|
orig_face_{{refimg.id}}.orig_w = {{refimg.orig_w}}
|
||||||
|
orig_face_{{refimg.id}}.orig_h = {{refimg.orig_h}}
|
||||||
|
|
||||||
|
// when the document is ready, then DrawRefimg
|
||||||
|
$(function() { DrawRefimg(im_{{refimg.id}}, c_{{refimg.id}}, orig_face_{{refimg.id}} ) });
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<div style="position:absolute; top: 2; right: 2;">
|
||||||
|
<button type="button" style="font-size:12px" class="btn btn-danger"
|
||||||
|
onClick="DelImg({{refimg.id}})">X</button>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
<figcaption class="figure-caption text-center text-wrap text-break">{{refimg.fname}}</figcaption>
|
||||||
</div class="form-row col-lg-10">
|
</div>
|
||||||
</div class="form-row col-lg-12">
|
</figure>
|
||||||
<div class="form-row col-lg-12">
|
</center>
|
||||||
<br>
|
</div id="/RI*">
|
||||||
</div class="form-row">
|
{% endfor %}
|
||||||
<div class="form-row col-lg-12">
|
</div class="form-row col-lg-12">
|
||||||
{{ form.submit( class="btn btn-primary offset-lg-2 col-lg-2" )}}
|
<div class="form-row col-lg-12">
|
||||||
{% if 'Edit' in page_title %}
|
<br>
|
||||||
{{ form.delete( class="btn btn-outline-danger col-lg-2" )}}
|
</div class="form-row">
|
||||||
{% endif %}
|
<div class="form-row col-lg-12">
|
||||||
</div class="form-row">
|
{{ form.save( id="save", class="btn btn-primary offset-lg-3 col-lg-2" )}}
|
||||||
</form>
|
{% if 'Edit' in page_title %}
|
||||||
</div class="row">
|
{{ form.delete( class="btn btn-outline-danger col-lg-2" )}}
|
||||||
</div class="container">
|
{% endif %}
|
||||||
|
</div class="form-row">
|
||||||
|
</form>
|
||||||
|
{% if person.id %}
|
||||||
|
<form id="new_ri" class="form" action="{{url_for('add_refimg')}}" method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="person_id" value="{{person.id}}"></input>
|
||||||
|
<label class="btn btn-success offset-lg-3 col-lg-2">
|
||||||
|
Add reference image
|
||||||
|
<input name="refimg_file" type="file" onChange="$('#new_ri').submit()" style="display:none;" id="new_file_chooser">
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div class="container">
|
||||||
{% endblock main_content %}
|
{% endblock main_content %}
|
||||||
|
|
||||||
|
{% block script_content %}
|
||||||
|
<script>
|
||||||
|
function DelImg(ri_num)
|
||||||
|
{
|
||||||
|
$('#RI'+ri_num).remove()
|
||||||
|
$('#pfm').submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock script_content %}
|
||||||
|
|||||||
@@ -4,12 +4,18 @@
|
|||||||
<h3>Show All People</h3>
|
<h3>Show All People</h3>
|
||||||
<table id="person_table" class="table table-striped table-sm" data-toolbar="#toolbar" data-search="true">
|
<table id="person_table" class="table table-striped table-sm" data-toolbar="#toolbar" data-search="true">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="thead-light"><th>Tag</th><th>Firstname(s)</th><th>Surname</th></tr>
|
<tr class="thead-light"><th>Tag</th><th>Firstname(s)</th><th>Surname</th><th></th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for person in persons %}
|
{% for person in persons %}
|
||||||
<tr><td><a href="{{url_for('person', id=person.id )}}">{{person.tag}}</td><td>{{person.firstname}}</td>
|
<tr><td><a href="{{url_for('person', id=person.id )}}">{{person.tag}}</td><td>{{person.firstname}}</td>
|
||||||
<td>{{person.surname}}</td></tr>
|
<td>{{person.surname}}</td>
|
||||||
|
<td>
|
||||||
|
{% for refimg in person.refimg %}
|
||||||
|
<img height=24 src="data:image/jpeg;base64,{{refimg.thumbnail}}";</img>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
21
user.py
Normal file
21
user.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from main import db
|
||||||
|
from sqlalchemy import Sequence
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from status import st, Status
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Class describing Person in the database, and via sqlalchemy, connected to the DB as well
|
||||||
|
################################################################################
|
||||||
|
class PAUser(UserMixin,db.Model):
|
||||||
|
__tablename__ = "pa_user"
|
||||||
|
id = db.Column(db.Integer, db.Sequence('pa_user_id_seq'), primary_key=True)
|
||||||
|
dn = db.Column(db.String)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.dn
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return self.dn
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
su mythtv -g mythtv -c "python3 /code/pa_job_manager.py" &
|
su mythtv -g mythtv -c "FLASK_ENV="production" python3 /code/pa_job_manager.py" &
|
||||||
gunicorn --bind=0.0.0.0:443 --workers=8 --threads=8 --certfile /etc/letsencrypt/live/pa.depaoli.id.au/fullchain.pem --keyfile /etc/letsencrypt/live/pa.depaoli.id.au/privkey.pem main:app
|
gunicorn --bind=0.0.0.0:443 --workers=4 --threads=16 --certfile /etc/letsencrypt/live/pa.depaoli.id.au/fullchain.pem --keyfile /etc/letsencrypt/live/pa.depaoli.id.au/privkey.pem main:app --env FLASK_ENV="production"
|
||||||
|
|||||||
Reference in New Issue
Block a user