From a0e06717acbd1b82134ea75df2b47a6d8d242e4c Mon Sep 17 00:00:00 2001 From: Damien De Paoli Date: Tue, 30 Sep 2025 00:29:11 +1000 Subject: [PATCH] viewer now works for files_ip, still have broken bits everywhere - files_rbp, change_opts, do I want a back button? lots of dead/old code, probably cam move more js into *_support, and do I want to keep files_support separate to view_support --- files.py | 123 ++++++++-- internal/js/files_support.js | 14 +- internal/js/view_support.js | 123 +++++----- templates/files.html | 445 +++++++++++++++++++++++------------ 4 files changed, 477 insertions(+), 228 deletions(-) diff --git a/files.py b/files.py index ae00e35..efffa8d 100644 --- a/files.py +++ b/files.py @@ -1,8 +1,10 @@ from flask_wtf import FlaskForm from flask import request, render_template, redirect, send_from_directory, url_for, jsonify, make_response +from marshmallow import Schema, fields from main import db, app, ma from sqlalchemy import Sequence, text, select from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import joinedload import os import glob import json @@ -172,16 +174,43 @@ class FileTypeSchema(ma.SQLAlchemyAutoSchema): class Meta: model = FileType load_instance = True -class FileSchema(ma.SQLAlchemyAutoSchema): - class Meta: model = File - load_instance = True - class DirSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Dir load_instance = True eid = ma.auto_field() # Explicitly include eid in_path = ma.Nested(PathSchema) +class FaceFileLinkSchema(ma.SQLAlchemyAutoSchema): + class Meta: model = FaceFileLink + load_instance = True + +class PersonSchema(ma.SQLAlchemyAutoSchema): + class Meta: model=Person + load_instance = True + +class RefimgSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Refimg + exclude = ('face',) + load_instance = True + person = ma.Nested(PersonSchema) + +class FaceRefimgLinkSchema(ma.SQLAlchemyAutoSchema): + class Meta: model = FaceRefimgLink + load_instance = True + +class FaceSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model=Face + exclude = ('face',) + load_instance = True + refimg = ma.Nested(RefimgSchema,allow_none=True) + +class FileSchema(ma.SQLAlchemyAutoSchema): + class Meta: model = File + load_instance = True + faces = ma.Nested(FaceSchema,many=True,allow_none=True) + ################################################################################ # Schema for Entry so we can json for data to the client ################################################################################ @@ -191,11 +220,19 @@ class EntrySchema(ma.SQLAlchemyAutoSchema): load_instance = True type = ma.Nested(FileTypeSchema) - file_details = ma.Nested(FileSchema) + file_details = ma.Nested(FileSchema,allow_none=True) # noting dir_details needs in_path to work dir_details = ma.Nested(DirSchema) # noting in_dir needs in_path and in_path.type to work in_dir = ma.Nested(DirSchema) + # allow us to use FullPathOnFS() + FullPathOnFS = fields.Method("get_full_path") + + def get_full_path(self, obj): + return obj.FullPathOnFS() + +# global - this will be use more than once below, so do it once for efficiency +entries_schema = EntrySchema(many=True) ################################################################################ # util function to just update the current/first/last positions needed for @@ -396,14 +433,26 @@ def process_ids(): # DDP: debate here, do I get query_id, do I validate whether we are asking # for ids not in the query? OR, dont even make/store/have query? - # marshmallow will allow us to json the data the way we need for the client - entries_schema = EntrySchema(many=True) - # Query DB for matching entries - entries = Entry.query.filter(Entry.id.in_(ids)).all() + stmt = ( + select(Entry) + .options( + joinedload(Entry.file_details).joinedload(File.faces), + joinedload(Entry.file_details).joinedload(File.faces).joinedload(Face.refimg).joinedload(Refimg.person) + ) + .where(Entry.id.in_(ids)) + ) - # return entries as json - return jsonify(entries_schema.dump(entries)) + # unique as the ORM query returns a Cartesian product for the joins. E.g if file has 3 faces, the result has 3 rows of the same entry and file data, but different face data + data=db.session.execute(stmt).unique().scalars().all() + + # data is now in whatever order the DB returns- faster in python than DB supposedly. So, create a mapping from id to entry for quick lookup + entry_map = {entry.id: entry for entry in data} + + # Sort the entries according to the order of ids + sorted_data = [entry_map[id_] for id_ in ids if id_ in entry_map] + + return jsonify(entries_schema.dump(sorted_data)) ################################################################################ @@ -806,6 +855,40 @@ def view_list(): return make_response( resp ) + +@login_required +@app.route("/newview/", methods=["POST"]) +def newview(): + data = request.get_json() # Parse JSON body + eid = data.get('eid', 0) # Extract list of ids + + # need appropriate schema? to get FaceData with entry, lists should just be + # what we have in entryList so it can help with next/prev + + # include Entry for name/path, ffl (model_used), frl (distance), Face (for w/h, etc), Person (id,tag) + #stmt=select(Entry).filter(Entry.id==eid) + + stmt = ( + select(Entry) + .options( + joinedload(Entry.file_details).joinedload(File.faces), + joinedload(Entry.file_details).joinedload(File.faces).joinedload(Face.refimg).joinedload(Refimg.person) + ) + .where(Entry.id == eid) + ) + + print( stmt ) + # this needs unique because: + # entry (one row for id=660) + # file (one row, since file_details is a one-to-one relationship) + # face (many rows, since a file can have many faces) + # refimg and person (one row per face, via the link tables) + # The SQL query returns a Cartesian product for the joins involving collections (like faces). For example, if your file has 3 faces, + # the result set will have 3 rows, each with the same entry and file data, but different face, refimg, and person data. + data=db.session.execute(stmt).unique().scalars().all() + print( data ) + return jsonify(entries_schema.dump(data)) + ################################################################################ # /view/id -> grabs data from DB and views it (GET) ################################################################################ @@ -836,14 +919,16 @@ def view(id): eids=eids.rstrip(",") # jic, sometimes we trip this, and rather than show broken pages / destroy if id not in eids: - SetFELog( message=f"ERROR: viewing an id, but its not in eids OPT={OPT}, id={id}, eids={eids}", level="danger", persistent=True, cant_close=False) - msg="Sorry, viewing data is confused, cannot view this image now" - if os.environ['ENV'] == "production": - msg += "Clearing out all states. This means browser back buttons will not work, please start a new tab and try again" - PA_UserState.query.delete() - db.session.commit() - SetFELog( msg, "warning", persistent=True, cant_close=False ) - return redirect("/") +# SetFELog( message=f"ERROR: viewing an id, but its not in eids OPT={OPT}, id={id}, eids={eids}", level="danger", persistent=True, cant_close=False) +# msg="Sorry, viewing data is confused, cannot view this image now" +# if os.environ['ENV'] == "production": +# msg += "Clearing out all states. This means browser back buttons will not work, please start a new tab and try again" +# PA_UserState.query.delete() +# db.session.commit() +# SetFELog( msg, "warning", persistent=True, cant_close=False ) +# return redirect("/") + print( f"id={id}, eids={eids}" ) + return "200" else: NMO_data = FaceOverrideType.query.all() setting = Settings.query.first() diff --git a/internal/js/files_support.js b/internal/js/files_support.js index 780c3a3..ea31c1f 100644 --- a/internal/js/files_support.js +++ b/internal/js/files_support.js @@ -523,15 +523,19 @@ function drawPageOfFigures() ecnt++ } $('.figure').click( function(e) { DoSel(e, this ); SetButtonState(); return false; }); - $('.figure').dblclick( CallViewRouteWrapper ) + $('.figure').dblclick( function(e) { dblClickToViewEntry( $(this).attr('id') ) } ) // for dir, getDirEntries 2nd param is back (or "up" a dir) $(".dir").click( function(e) { document.back_id=this.id; getDirEntries(this.id,false) } ) $(".back").click( function(e) { getDirEntries(this.id,true) } ) } // Function to get the 'page' of entry ids out of entryList -function getPage(pageNumber) +function getPage(pageNumber,viewing_idx=0) { + // before we do anything, disabled left/right arrows on viewer to stop + // getting another event before we have the data for the page back + $('#la').prop('disabled', true) + $('#ra').prop('disabled', true) const startIndex = (pageNumber - 1) * OPT.howMany; const endIndex = startIndex + OPT.howMany; pageList = entryList.slice(startIndex, endIndex); @@ -549,7 +553,13 @@ function getPage(pageNumber) dataType: 'json', // Expect JSON response success: function(res) { document.entries=res + // add all the figures to files_div drawPageOfFigures() + // noting we could have been in files_div, or viewer_div, update both jic + // and fix viewer_div - update viewing, arrows and image/video too + document.viewing=document.entries[viewing_idx] + resetNextPrevButtons() + ViewImageOrVideo() }, error: function(xhr, status, error) { console.error("Error:", error); diff --git a/internal/js/view_support.js b/internal/js/view_support.js index 661c060..272d358 100644 --- a/internal/js/view_support.js +++ b/internal/js/view_support.js @@ -87,11 +87,11 @@ function DrawImg() // if we have faces, the enable the toggles, otherwise disable them // and reset model select too - if( objs[current].faces ) + if( document.viewing.faces ) { $('#faces').attr('disabled', false) $('#distance').attr('disabled', false) - $('#model').val( Number(objs[current].face_model) ) + $('#model').val( Number(document.viewing.face_model) ) } else { @@ -102,33 +102,33 @@ function DrawImg() } // okay, we want faces drawn so lets do it - if( $('#faces').prop('checked') && objs[current].faces ) + if( $('#faces').prop('checked') && document.viewing.faces ) { // draw rect on each face - for( i=0; i= fx && x <= fx+fw && y >= fy && y <= fy+fh ) { - if( objs[current].faces[i].override ) + if( document.viewing.faces[i].override ) { - item_list['remove_force_match_override']={ 'name': 'Remove override for this face', 'which_face': i, 'id': objs[current].faces[i].id } + item_list['remove_force_match_override']={ 'name': 'Remove override for this face', 'which_face': i, 'id': document.viewing.faces[i].id } } - else if( objs[current].faces[i].who ) + else if( document.viewing.faces[i].who ) { - item_list['match']={ 'name': objs[current].faces[i].who, 'which_face': i, 'id': objs[current].faces[i].id } - item_list['match_add_refimg']={ 'name': 'Add this as refimg for ' + objs[current].faces[i].who, - 'person_id': objs[current].faces[i].pid, 'who': objs[current].faces[i].who, 'which_face': i, 'id': objs[current].faces[i].id, } - item_list['wrong_person']={ 'name': 'wrong person', 'which_face': i, 'id': objs[current].faces[i].id } + item_list['match']={ 'name': document.viewing.faces[i].who, 'which_face': i, 'id': document.viewing.faces[i].id } + item_list['match_add_refimg']={ 'name': 'Add this as refimg for ' + document.viewing.faces[i].who, + 'person_id': document.viewing.faces[i].pid, 'who': document.viewing.faces[i].who, 'which_face': i, 'id': document.viewing.faces[i].id, } + item_list['wrong_person']={ 'name': 'wrong person', 'which_face': i, 'id': document.viewing.faces[i].id } } else { - item_list['no_match_new_person']={ 'name': 'Add as reference image to NEW person', 'which_face': i, 'id': objs[current].faces[i].id } - item_list['no_match_new_refimg']={ 'name': 'Add as reference image to EXISTING person', 'which_face': i, 'id': objs[current].faces[i].id } + item_list['no_match_new_person']={ 'name': 'Add as reference image to NEW person', 'which_face': i, 'id': document.viewing.faces[i].id } + item_list['no_match_new_refimg']={ 'name': 'Add as reference image to EXISTING person', 'which_face': i, 'id': document.viewing.faces[i].id } for( var el in NMO ) { - item_list['NMO_'+el]={'type_id': NMO[el].type_id, 'name': 'Override: ' + NMO[el].name, 'which_face': i, 'id': objs[current].faces[i].id } + item_list['NMO_'+el]={'type_id': NMO[el].type_id, 'name': 'Override: ' + NMO[el].name, 'which_face': i, 'id': document.viewing.faces[i].id } } } delete item_list['not_a_face'] @@ -280,11 +280,11 @@ function OverrideForceMatch( person_id, key ) } ofm='&person_id='+person_id+'&face_id='+item[key].id $.ajax({ type: 'POST', data: ofm, url: '/add_force_match_override', success: function(data) { - objs[current].faces[item[key].which_face].override={} - objs[current].faces[item[key].which_face].override.who=data.person_tag - objs[current].faces[item[key].which_face].override.distance='N/A' - objs[current].faces[item[key].which_face].override.type_id=NMO[fm_idx].id - objs[current].faces[item[key].which_face].override.type_name=NMO[fm_idx].name + document.viewing.faces[item[key].which_face].override={} + document.viewing.faces[item[key].which_face].override.who=data.person_tag + document.viewing.faces[item[key].which_face].override.distance='N/A' + document.viewing.faces[item[key].which_face].override.type_id=NMO[fm_idx].id + document.viewing.faces[item[key].which_face].override.type_name=NMO[fm_idx].name $('#dbox').modal('hide') $('#faces').prop('checked',true) @@ -303,8 +303,8 @@ function CreatePersonAndRefimg( key ) +'&refimg_data='+item[key].refimg_data $.ajax({ type: 'POST', data: d, url: '/match_with_create_person', success: function(data) { - objs[current].faces[item[key].which_face].who=data.who - objs[current].faces[item[key].which_face].distance=data.distance + document.viewing.faces[item[key].which_face].who=data.who + document.viewing.faces[item[key].which_face].distance=data.distance $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() @@ -318,8 +318,8 @@ function AddRefimgTo( person_id, key, search ) d='&face_id='+item[key].id+'&person_id='+person_id+'&refimg_data='+item[key].refimg_data+'&search='+search $.ajax({ type: 'POST', data: d, url: '/add_refimg_to_person', success: function(data) { - objs[current].faces[item[key].which_face].who=data.who - objs[current].faces[item[key].which_face].distance=data.distance + document.viewing.faces[item[key].which_face].who=data.who + document.viewing.faces[item[key].which_face].distance=data.distance $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() @@ -367,15 +367,15 @@ function SearchForPerson(content, key, face_id, face_pos, type_id) function RemoveOverrideForceMatch(face_pos) { - if( objs[current].faces[face_pos].override ) - who=objs[current].faces[face_pos].override.who + if( document.viewing.faces[face_pos].override ) + who=document.viewing.faces[face_pos].override.who else - who=objs[current].faces[face_pos].who + who=document.viewing.faces[face_pos].who - d='&face_id='+objs[current].faces[face_pos].id+'&person_tag='+who+'&file_eid='+current + d='&face_id='+document.viewing.faces[face_pos].id+'&person_tag='+who+'&file_eid='+current $.ajax({ type: 'POST', data: d, url: '/remove_force_match_override', success: function(data) { - delete objs[current].faces[face_pos].override + delete document.viewing.faces[face_pos].override $('#dbox').modal('hide') DrawImg() CheckForJobs() @@ -387,10 +387,10 @@ function RemoveOverrideForceMatch(face_pos) function RemoveOverrideNoMatch(face_pos, type_id) { - d='&face_id='+objs[current].faces[face_pos].id+'&type_id='+type_id + d='&face_id='+document.viewing.faces[face_pos].id+'&type_id='+type_id $.ajax({ type: 'POST', data: d, url: '/remove_no_match_override', success: function(data) { - delete objs[current].faces[face_pos].override + delete document.viewing.faces[face_pos].override $('#dbox').modal('hide') DrawImg() CheckForJobs() @@ -405,11 +405,11 @@ function AddNoMatchOverride(type_id, face_id, face_pos, type_id) d='&type_id='+type_id+'&face_id='+face_id $.ajax({ type: 'POST', data: d, url: '/add_no_match_override', success: function(data) { - objs[current].faces[face_pos].override={} - objs[current].faces[face_pos].override.who=NMO[type_id].name - objs[current].faces[face_pos].override.distance='N/A' - objs[current].faces[face_pos].override.type_id=type_id - objs[current].faces[face_pos].override.type_name=NMO[type_id].name + document.viewing.faces[face_pos].override={} + document.viewing.faces[face_pos].override.who=NMO[type_id].name + document.viewing.faces[face_pos].override.distance='N/A' + document.viewing.faces[face_pos].override.type_id=type_id + document.viewing.faces[face_pos].override.type_name=NMO[type_id].name $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() @@ -457,17 +457,17 @@ function FaceDBox(key, item) div+='
' if ( key == 'remove_force_match_override' ) { - if( objs[current].faces[face_pos].override.type_name == 'Manual match to existing person' ) - div+='
remove this override (force match to: ' + objs[current].faces[face_pos].override.who + ')
' + if( document.viewing.faces[face_pos].override.type_name == 'Manual match to existing person' ) + div+='
remove this override (force match to: ' + document.viewing.faces[face_pos].override.who + ')
' else div+='
remove this override (no match)
' div+='
' div+='' div+='' else - div+='onClick="RemoveOverrideNoMatch(' +face_pos+','+objs[current].faces[face_pos].override.type_id+ ')">Remove' + div+='onClick="RemoveOverrideNoMatch(' +face_pos+','+document.viewing.faces[face_pos].override.type_id+ ')">Remove' div+='
' } if ( key == 'no_match_new_person' ) @@ -559,3 +559,8 @@ function JoblogSearch() } }) } + +function setVideoSource(newSrc) { + $('#videoSource').attr('src', newSrc); + $('#video')[0].load(); +} diff --git a/templates/files.html b/templates/files.html index 0b8dc1a..832bc28 100644 --- a/templates/files.html +++ b/templates/files.html @@ -16,11 +16,15 @@ move_paths.push(p) {% endfor %} - // GLOBALS + // GLOBALS // OPTions set via GUI, will change if we alter drop-downs, etc. in GUI // TODO: reference these from GUI, so we can finally ditch the form to submit/change them. // BUT -- must handle noo changing with a form/post as it requires a new ordering + // this is which eid we are viewing an image/video (when we dbl-click & then next/prev) + document.viewing_eid=null; + document.viewing=null; + var OPT={} OPT.grouping='{{OPT.grouping}}' OPT.cwd='{{OPT.cwd}}' @@ -39,8 +43,8 @@ getPage(1) -
-
+
+
{% if search_term is defined %} @@ -105,7 +109,7 @@ -
+
{% if OPT.size == 64 %} @@ -139,159 +143,297 @@ {% endif %}
-
+
-
+
{% set eids=namespace( str="" ) %} {# gather all the file eids and collect them in case we go gallery mode #} {% for obj in query_data.entry_list %} {% set eids.str = eids.str + obj|string +"," %} {% endfor %} - - {% set ecnt=namespace( val=0 ) %} -
- - +
+
- -
-
- -  {{OPT.how_many}} files  - -
-
-
- + +
+
+ +  {{OPT.how_many}} files  + +
+
+ + +
+ + + + + + + + +
+
+ +
+ + + +
+
+
+
+ +
+
+
+ + + +
+ {# use this for color of toggles: https://www.codeply.com/p/4sL9uhevwJ #} +
+ {# this whole div, just takes up the same space as the left button and is hidden for alignment only #} +
+ +
+ Show: +
+ + +
+
+ + +
+
+ + +
+
+ AI Model: + {# can use 0 as default, it will be (re)set correctly in DrawImg() anyway #} + {{CreateSelect( "model", 0, ["N/A", "normal", "slow/accurate"], "", "rounded norm-txt", [0,1,2])|safe }} +
+
+ + + + + + + + + +
+
+
+
+ + + {% endblock main_content %} {% block script_content %} @@ -299,9 +441,16 @@ $(document).on('click', function(e) { $('.highlight').removeClass('highlight') ; SetButtonState() }); +function dblClickToViewEntry(id) { + $('#files_div').addClass('d-none') + $('#viewer_div').removeClass('d-none') + setEntryById( id ) + ViewImageOrVideo() +} + function CallViewRouteWrapper() { - CallViewRoute( $(this).attr("id") ) +// CallViewRoute( document.viewing.id ) } function CallViewRoute(id)