diff options
-rw-r--r-- | eslint.config.mjs | 8 | ||||
-rw-r--r-- | src/.DS_Store | bin | 10244 -> 10244 bytes | |||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 5 | ||||
-rw-r--r-- | src/client/util/DocumentManager.ts | 1 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx | 121 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 8 | ||||
-rw-r--r-- | src/client/views/search/FaceRecognitionHandler.tsx | 190 | ||||
-rw-r--r-- | src/fields/Doc.ts | 6 | ||||
-rw-r--r-- | src/fields/RichTextUtils.ts | 4 | ||||
-rw-r--r-- | src/server/database.ts | 3 | ||||
-rw-r--r-- | src/server/server_Initialization.ts | 2 |
13 files changed, 198 insertions, 156 deletions
diff --git a/eslint.config.mjs b/eslint.config.mjs index 04655ce13..119f2f486 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,6 +29,7 @@ export default [ 'import/prefer-default-export': 'off', 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': 'off', 'prefer-template': 'off', 'no-inner-declarations': 'off', 'no-plusplus': 'off', @@ -42,6 +43,13 @@ export default [ '@typescript-eslint/no-unused-vars': 'error', '@typescript-eslint/no-namespace': 'off', 'react/destructuring-assignment': 0, + 'prefer-arrow-callback': 'error', + 'no-return-assign': 'error', + 'no-await-in-loop': 'error', + 'no-loop-func': 'error', + 'no-conditional-assign': 'error', + 'no-use-before-define': 'error', + 'no-explicit-any': 'error', 'no-restricted-globals': ['error', 'event'], }, }, diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 7a0b53ce0..426a2ee90 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 2962682c2..0681a21cb 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -95,7 +95,6 @@ export class CurrentUserUtils { }); const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, isSystem: true}; - // eslint-disable-next-line no-return-assign return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); } @@ -120,7 +119,6 @@ export class CurrentUserUtils { }); const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, isSystem: true}; - // eslint-disable-next-line no-return-assign return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); } @@ -138,7 +136,6 @@ export class CurrentUserUtils { }), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))]; const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true }; - // eslint-disable-next-line no-return-assign return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); } static setupUserTemplates(doc: Doc, field="template_user") { @@ -146,7 +143,6 @@ export class CurrentUserUtils { const reqdUserList = DocListCast(tempUsers?.data); const reqdOpts:DocumentOptions = { title: "User Layouts", _height: 75, isSystem: true }; - // eslint-disable-next-line no-return-assign return DocUtils.AssignOpts(tempUsers, reqdOpts, reqdUserList) ?? (doc[field] = Docs.Create.TreeDocument(reqdUserList, reqdOpts)); } @@ -183,7 +179,6 @@ export class CurrentUserUtils { default: return labelBox; }})(); const allopts = {isSystem: true, onClickScriptDisable: "never", ...opts, title}; - // eslint-disable-next-line no-return-assign return DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ? DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[title] = MakeTemplate(creator(allopts, templateField)))), {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", }); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index a10237fcc..83b83240e 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -17,7 +17,6 @@ export class DocumentManager { // eslint-disable-next-line no-use-before-define private static _instance: DocumentManager; public static get Instance(): DocumentManager { - // eslint-disable-next-line no-return-assign return this._instance || (this._instance = new this()); } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 62ad0a3fb..9f1c7da3d 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -75,7 +75,7 @@ import { AnchorMenu } from './pdf/AnchorMenu'; import { GPTPopup } from './pdf/GPTPopup/GPTPopup'; import { TopBar } from './topbar/TopBar'; -// eslint-disable-next-line @typescript-eslint/no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore @observer diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a6768ab35..5782d407e 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import * as React from 'react'; import * as rp from 'request-promise'; import { ClientUtils, returnFalse } from '../../../ClientUtils'; @@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index d5a2809dc..94f9a3c94 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -3,28 +3,28 @@ import { IconButton, Size } from 'browndash-components'; import * as faceapi from 'face-api.js'; import { FaceMatcher } from 'face-api.js'; import 'ldrs/ring'; -import { action, computed, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; -import { Utils } from '../../../../Utils'; -import { Doc, DocListCast } from '../../../../fields/Doc'; -import { DocData } from '../../../../fields/DocSymbols'; +import { setupMoveUpEvents } from '../../../../ClientUtils'; +import { Utils, emptyFunction } from '../../../../Utils'; +import { Doc, Opt } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; -import { listSpec } from '../../../../fields/Schema'; -import { Cast, ImageCast, StrCast } from '../../../../fields/Types'; +import { ImageCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; +import { dropActionType } from '../../../util/DropActionTypes'; import { SnappingManager } from '../../../util/SnappingManager'; import { undoable } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler'; import './FaceCollectionBox.scss'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; -import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler'; interface FaceDocumentProps { faceDoc: Doc; @@ -50,49 +50,29 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps }; protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { - const { docDragData } = de.complete; - if (docDragData) { - const filteredDocs = docDragData.droppedDocuments.filter(doc => doc.type === DocumentType.IMG); - filteredDocs.forEach(doc => { - // If the current Face Document has no items, and the doc has more than one face descriptor, don't let the user add the document first. - if ((this._props.faceDoc[DocData].face_descriptors as List<List<number>>).length === 0 && (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List<List<number>>).length > 1) { + de.complete.docDragData?.droppedDocuments + ?.filter(doc => doc.type === DocumentType.IMG) + .forEach(imgDoc => { + // If the current Face Document has no faces, and the doc has more than one face descriptor, don't let the user add the document first. Or should we just use the first face ? + if (FaceRecognitionHandler.FaceDocDescriptors(this._props.faceDoc).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) { alert('Cannot add a document with multiple faces as the first item!'); } else { - // Loop through the documents' face descriptors. - // Choose the face with the smallest distance to add. - const float32Array = (this._props.faceDoc[DocData].face_descriptors as List<List<number>>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor))); - const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(StrCast(this._props.faceDoc[DocData].face_label), float32Array); - const faceDescriptors: faceapi.LabeledFaceDescriptors[] = [labeledFaceDescriptor]; - - const faceMatcher = new FaceMatcher(faceDescriptors, 1); - let cur_lowest_distance = 1; - let cur_matching_face = new List<number>(); - - (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List<List<number>>).forEach(face => { - // If the face has the current lowest distance, mark it as such - // Once that lowest distance is found, add the face descriptor to the faceDoc, and add the associated doc - const convered_32_array: Float32Array = new Float32Array(Array.from(face)); - const match = faceMatcher.matchDescriptor(convered_32_array); - - if (match.distance < cur_lowest_distance) { - cur_lowest_distance = match.distance; - cur_matching_face = face; - } - }); - - const faceFieldKey = FaceRecognitionHandler.FaceField(doc, this._props.faceDoc); - if (doc[DocData][faceFieldKey]) { - Cast(doc[DocData][faceFieldKey], listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that - } else { - doc[DocData][faceFieldKey] = new List<List<number>>([cur_matching_face]); - } - - Doc.AddDocToList(this._props.faceDoc[DocData], 'face_docList', doc); - Cast(this._props.faceDoc[DocData].face_descriptors, listSpec('number'), null).push(cur_matching_face as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that + // Loop through the documents' face descriptors and choose the face in the iage with the smallest distance (most similar to the face colleciton) + const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.FaceDocDescriptors(this._props.faceDoc).map(fd => new Float32Array(Array.from(fd))); + const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.FaceDocLabel(this._props.faceDoc), faceDescriptorsAsFloat32Array); + const faceMatcher = new FaceMatcher([labeledFaceDescriptor], 1); + const { face_match } = FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).reduce( + (prev, face) => { + const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(face))); + return match.distance < prev.dist ? { dist: match.distance, face_match: face } : prev; + }, + { dist: 1, face_match: new List<number>() as Opt<List<number>> } + ); + + // assign the face in the image that's closest to the face collection to be the face that's assigned to the collection + face_match && FaceRecognitionHandler.ImageDocAddFace(imgDoc, face_match, this._props.faceDoc); } }); - return false; - } return false; } @@ -108,23 +88,16 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps * Deletes a Face Document. */ deleteFaceDocument = undoable(() => { - if (Doc.ActiveDashboard) { - Doc.RemoveDocFromList(Doc.ActiveDashboard[DocData], 'faceDocuments', this._props.faceDoc); - } - }, 'remove face'); + FaceRecognitionHandler.DeleteFaceDoc(this._props.faceDoc); + }, 'delete face'); /** * Deletes a document from a Face Document's associated docs list. * @param doc */ - @action - deleteAssociatedDoc = (doc: Doc) => { - this._props.faceDoc[DocData].face_descriptors = new List<List<number>>( - (this._props.faceDoc[DocData].face_descriptors as List<List<number>>).filter(fd => !(doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] as List<List<number>>).includes(fd)) - ); - doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] = new List<List<number>>(); - Doc.RemoveDocFromList(this._props.faceDoc[DocData], 'face_docList', doc); - }; + deleteAssociatedDoc = undoable((imgDoc: Doc) => { + FaceRecognitionHandler.FaceDocRemoveImageDocFace(imgDoc, this._props.faceDoc); + }, 'remove doc from face'); render() { return ( @@ -133,7 +106,7 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps <IconButton tooltip="Delete Face From Collection" onPointerDown={this.deleteFaceDocument} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} /> </div> <div className="face-document-top"> - <h1>{StrCast(this._props.faceDoc[DocData].face_label)}</h1> + <h1>{FaceRecognitionHandler.FaceDocLabel(this._props.faceDoc)}</h1> </div> <IconButton tooltip="See image information" @@ -144,10 +117,24 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps /> {this._displayImages ? ( <div className="face-document-image-container"> - {DocListCast(this._props.faceDoc[DocData].face_docList).map(doc => { + {FaceRecognitionHandler.FaceDocFaces(this._props.faceDoc).map(doc => { const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); return ( - <div className="image-wrapper" key={Utils.GenerateGuid()}> + <div + className="image-wrapper" + key={Utils.GenerateGuid()} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + () => { + DragManager.StartDocumentDrag([e.target as HTMLElement], new DragManager.DocumentDragData([doc], dropActionType.embed), e.clientX, e.clientY); + return true; + }, + emptyFunction, + emptyFunction + ) + }> <img onClick={() => DocumentView.showDocument(doc, { willZoomCentered: true })} style={{ maxWidth: '60px', margin: '10px' }} src={`${name}_o.${type}`} /> <div className="remove-item"> <IconButton tooltip={'Remove Doc From Face Collection'} onPointerDown={() => this.deleteAssociatedDoc(doc)} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} /> @@ -168,25 +155,15 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() { return FieldView.LayoutString(FaceCollectionBox, fieldKey); } - public static Instance: FaceCollectionBox; - - @computed get currentDocs() { - if (Doc.ActiveDashboard) { - return DocListCast(Doc.ActiveDashboard[DocData].faceDocuments); - } - return []; - } - constructor(props: FieldViewProps) { super(props); makeObservable(this); - FaceCollectionBox.Instance = this; } render() { return ( <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}> - {this.currentDocs.map(doc => ( + {FaceRecognitionHandler.FaceDocuments().map(doc => ( <FaceDocumentItem key={doc[Id]} faceDoc={doc} /> ))} </div> diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c807d99ac..5efdb3df4 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,5 +1,4 @@ /* eslint-disable no-use-before-define */ -/* eslint-disable react/jsx-props-no-spreading */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { Property } from 'csstype'; import { Howl } from 'howler'; @@ -341,7 +340,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if ((clickFunc && waitForDblClick !== 'never') || waitForDblClick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300); - // eslint-disable-next-line no-use-before-define } else if (!SnappingManager.LongPress) { this._singleClickFunc(); this._singleClickFunc = undefined; @@ -354,7 +352,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document onPointerDown = (e: React.PointerEvent): void => { if (this._props.isGroupActive?.() === GroupActive.child && !this._props.isDocumentActive?.()) return; - // eslint-disable-next-line no-use-before-define this._longPressSelector = setTimeout(() => SnappingManager.LongPress && this._props.select(false), 1000); if (!DocumentView.DownDocView) DocumentView.DownDocView = this._docView; @@ -405,7 +402,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document this._doubleTap = (this.onDoubleClickHdlr?.script || this.Document.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < ClientUtils.CLICK_TIME; if (!this.isContentActive()) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected } - // eslint-disable-next-line no-use-before-define if (SnappingManager.LongPress) e.preventDefault(); }; @@ -504,7 +500,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res; console.log(res); } catch (err) { - console.error('GPT call failed'); + console.error('GPT call failed', err); } }; @@ -1539,7 +1535,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { ) ); } - // eslint-disable-next-line default-param-last + public static FocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) { let doc = docIn; const options = optionsIn; diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index dc271fe73..3ef6f9674 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -10,7 +10,22 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; /** - * A class that handles face recognition. + * A singleton class that handles face recognition and manages face Doc collections for each face found. + * Displaying an image doc anywhere will trigger this class to test if the image contains any faces. + * If it does, each recognized face will be compared to a global set of faces (each is a face collection Doc + * that have already been found. If the face matches a face collection Doc, then it will be added to that + * collection along with the numerical representation of the face, its face descriptor. + * + * Image Doc's that are added to one or more face collection Docs will be given these metadata fields: + * <image data field>_Face<N> - a nunerical representation of the Nth face found in the image + * <image data field>_Faces - a list of all the numerical face representations found in the image (why is this needed?) + * + * Face collection Doc's are created for each person identified and are stored in the Dashboard's faceDocument's list + * + * Each Face collection Doc represents all the images found for that person. It has these fields: + * face_label - a string label for the person that was recognized (currently it's just a 'face#') + * face_descriptors - a list of all the face descriptors for different images of the person + * face_docList - a list of all image Docs that contain a face for the person */ export class FaceRecognitionHandler { static _instance: FaceRecognitionHandler; @@ -18,13 +33,90 @@ export class FaceRecognitionHandler { private _processingDocs: Set<Doc> = new Set(); private _pendingLoadDocs: Doc[] = []; - public static FaceField = (target: Doc, doc: Doc) => `${Doc.LayoutFieldKey(target)}_${doc.face_label}`; - public static FacesField = (target: Doc) => `${Doc.LayoutFieldKey(target)}_Faces`; + private static imgDocFaceField = (imgDoc: Doc, faceDoc: Doc) => `${Doc.LayoutFieldKey(imgDoc)}_${FaceRecognitionHandler.FaceDocLabel(faceDoc)}`; + /** + * initializes an image with an empty list of face descriptors + * @param imgDoc image to initialize + */ + private static initImageDocFaceDescriptors = (imgDoc: Doc) => { + imgDoc[DocData][`${Doc.LayoutFieldKey(imgDoc)}_Faces`] = new List<List<number>>(); + }; + /** + * returns the face descriptors for each face found on an image Doc + * @param imgDoc + * @returns list of face descriptors + */ + public static ImageDocFaceDescriptors = (imgDoc: Doc) => imgDoc[DocData][`${Doc.LayoutFieldKey(imgDoc)}_Faces`] as List<List<number>>; + + /** + * Adds metadata to an image Doc describing a face found in the image + * @param imgDoc image Doc containing faces + * @param faceDescriptor descriptor for the face found + * @param faceDoc face collection Doc containing the same face + */ + public static ImageDocAddFace = (imgDoc: Doc, faceDescriptor: List<number>, faceDoc: Doc) => { + const faceFieldKey = FaceRecognitionHandler.imgDocFaceField(imgDoc, faceDoc); + if (imgDoc[DocData][faceFieldKey]) { + Cast(imgDoc[DocData][faceFieldKey], listSpec('number'), null).push(faceDescriptor as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that + } else { + imgDoc[DocData][faceFieldKey] = new List<List<number>>([faceDescriptor]); + } + }; + + /** + * returns a list of all face collection Docs on the current dashboard + * @returns face collection Doc list + */ + public static FaceDocuments = () => DocListCast(Doc.ActiveDashboard?.[DocData].faceDocuments); + + public static DeleteFaceDoc = (faceDoc: Doc) => Doc.ActiveDashboard && Doc.RemoveDocFromList(Doc.ActiveDashboard[DocData], 'faceDocuments', faceDoc); + + /** + * returns the labels associated with a face collection Doc + * @param faceDoc the face collection Doc + * @returns label string + */ + public static FaceDocLabel = (faceDoc: Doc) => StrCast(faceDoc[DocData].face_label); + /** + * Returns all the face descriptors associated with a face collection Doc + * @param faceDoc a face collection Doc + * @returns face descriptors + */ + public static FaceDocDescriptors = (faceDoc: Doc) => faceDoc[DocData].face_descriptors as List<List<number>>; + + /** + * Returns a list of all face image Docs associated with the face collection + * @param faceDoc a face collection Doc + * @returns image Docs + */ + public static FaceDocFaces = (faceDoc: Doc) => DocListCast(faceDoc[DocData].face_docList); + + /** + * Adds a face image to the list of faces in a face collection Doc, and updates the face collection's list of image descriptors + * @param img - image with faces to add to a face collection Doc + * @param faceDescriptor - the face descriptor for the face in the image to add + * @param faceDoc - the face collection Doc + */ + public static FaceDocAddImageDocFace = (img: Doc, faceDescriptor: List<number>, faceDoc: Doc) => { + Doc.AddDocToList(faceDoc, 'face_docList', img); + Cast(faceDoc.face_descriptors, listSpec('number'), null).push(faceDescriptor as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that + }; + + /** + * Removes a face from a face Doc collection, and updates the face collection's list of image descriptors + * @param imgDoc - image with faces to remove from the face Doc collectoin + * @param faceDoc - the face Doc collection + */ + public static FaceDocRemoveImageDocFace = (imgDoc: Doc, faceDoc: Doc) => { + imgDoc[DocData][FaceRecognitionHandler.imgDocFaceField(imgDoc, faceDoc)] = new List<List<number>>(); + Doc.RemoveDocFromList(faceDoc[DocData], 'face_docList', imgDoc); + faceDoc[DocData].face_descriptors = new List<List<number>>(FaceRecognitionHandler.FaceDocDescriptors(faceDoc).filter(fd => !(imgDoc[DocData][FaceRecognitionHandler.imgDocFaceField(imgDoc, faceDoc)] as List<List<number>>).includes(fd))); + }; constructor() { FaceRecognitionHandler._instance = this; - this.loadModels().then(() => this._pendingLoadDocs.forEach(this.findMatches)); - DocumentManager.Instance.AddAnyViewRenderedCB(dv => FaceRecognitionHandler.Instance.findMatches(dv.Document)); + this.loadModels().then(() => this._pendingLoadDocs.forEach(this.classifyFacesInImage)); + DocumentManager.Instance.AddAnyViewRenderedCB(dv => FaceRecognitionHandler.Instance.classifyFacesInImage(dv.Document)); } @computed get examinedFaceDocs() { @@ -48,48 +140,42 @@ export class FaceRecognitionHandler { /** * When a document is added, look for matching face documents. - * @param doc The document being analyzed. + * @param imgDoc The document being analyzed. */ - public findMatches = async (doc: Doc) => { + public classifyFacesInImage = async (imgDoc: Doc) => { if (!this._loadedModels || !Doc.ActiveDashboard) { - this._pendingLoadDocs.push(doc); + this._pendingLoadDocs.push(imgDoc); return; } - if (doc.type === DocumentType.LOADING && !doc.loadingError) { - setTimeout(() => this.findMatches(doc), 1000); + if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) { + setTimeout(() => this.classifyFacesInImage(imgDoc), 1000); return; } - const imgUrl = ImageCast(doc[Doc.LayoutFieldKey(doc)]); + const imgUrl = ImageCast(imgDoc[Doc.LayoutFieldKey(imgDoc)]); // If the doc isn't an image or currently already been examined or is being processed, stop examining the document. - if (!imgUrl || this.examinedFaceDocs.includes(doc) || this._processingDocs.has(doc)) { + if (!imgUrl || this.examinedFaceDocs.includes(imgDoc) || this._processingDocs.has(imgDoc)) { return; } // Mark the document as being processed. - this._processingDocs.add(doc); + this._processingDocs.add(imgDoc); + FaceRecognitionHandler.initImageDocFaceDescriptors(imgDoc); // Get the image the document contains and analyze for faces. const [name, type] = imgUrl.url.href.split('.'); const imageURL = `${name}_o.${type}`; - const img = await this.loadImage(imageURL); - - const fullFaceDescriptions = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors(); - - doc[DocData][FaceRecognitionHandler.FacesField(doc)] = new List<List<number>>(); + const imgDocFaceDescriptions = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors(); // For each face detected, find a match. - for (const fd of fullFaceDescriptions) { - let match = this.findMatch(fd.descriptor); - const converted_list = new List<number>(Array.from(fd.descriptor)); - - if (match) { - // If a matching Face Document has been found, add the document to the Face Document's associated docs and append the face - // descriptor to the Face Document's descriptor list. - Doc.AddDocToList(match, 'face_docList', doc); - Cast(match.face_descriptors, listSpec('number'), null).push(converted_list as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that + for (const fd of imgDocFaceDescriptions) { + let faceDocMatch = this.findMatchingFaceDoc(fd.descriptor); + const faceDescriptor = new List<number>(Array.from(fd.descriptor)); + + if (faceDocMatch) { + FaceRecognitionHandler.FaceDocAddImageDocFace(imgDoc, faceDescriptor, faceDocMatch); } else { // If a matching Face Document has not been found, create a new Face Document. Doc.UserDoc().faceDocNum = NumCast(Doc.UserDoc().faceDocNum) + 1; @@ -98,53 +184,45 @@ export class FaceRecognitionHandler { newFaceDocument.title = `Face ${Doc.UserDoc().faceDocNum}`; newFaceDocument.face = ''; // just to make prettyprinting look better newFaceDocument.face_label = `Face${Doc.UserDoc().faceDocNum}`; - newFaceDocument.face_docList = new List<Doc>([doc]); - newFaceDocument.face_descriptors = new List<List<number>>([converted_list]); + newFaceDocument.face_docList = new List<Doc>([imgDoc]); + newFaceDocument.face_descriptors = new List<List<number>>([faceDescriptor]); Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'faceDocuments', newFaceDocument); - match = newFaceDocument; + faceDocMatch = newFaceDocument; } // Assign a field in the document of the matching Face Document. - const faceDescripField = FaceRecognitionHandler.FaceField(doc, match); - if (doc[DocData][faceDescripField]) { - Cast(doc[DocData][faceDescripField], listSpec('number'), null).push(converted_list as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that - } else { - doc[DocData][faceDescripField] = new List<List<number>>([converted_list]); - } - - Cast(doc[DocData][FaceRecognitionHandler.FacesField(doc)], listSpec('number'), null).push(converted_list as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that - - Doc.AddDocToList(Doc.UserDoc(), 'examinedFaceDocs', doc); + FaceRecognitionHandler.ImageDocAddFace(imgDoc, faceDescriptor, faceDocMatch); + Doc.AddDocToList(Doc.UserDoc(), 'examinedFaceDocs', imgDoc); } - this._processingDocs.delete(doc); + this._processingDocs.delete(imgDoc); }; /** - * Finds a matching Face Document given a descriptor - * @param cur_descriptor The current descriptor whose match is being searched for. - * @returns The most similar Face Document. + * Finds the most similar matching Face Document to a face descriptor + * @param faceDescriptor face descriptor number list + * @returns face Doc */ - private findMatch(cur_descriptor: Float32Array) { - if (!Doc.ActiveDashboard || DocListCast(Doc.ActiveDashboard[DocData].faceDocuments).length < 1) { - return null; + private findMatchingFaceDoc = (faceDescriptor: Float32Array) => { + if (!Doc.ActiveDashboard || FaceRecognitionHandler.FaceDocuments().length < 1) { + return undefined; } - const faceDescriptors: faceapi.LabeledFaceDescriptors[] = DocListCast(Doc.ActiveDashboard[DocData].faceDocuments).map(faceDocument => { - const float32Array = (faceDocument[DocData].face_descriptors as List<List<number>>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor))); - return new faceapi.LabeledFaceDescriptors(StrCast(faceDocument[DocData].face_label), float32Array); + const faceDescriptors = FaceRecognitionHandler.FaceDocuments().map(faceDoc => { + const float32Array = FaceRecognitionHandler.FaceDocDescriptors(faceDoc).map(fd => new Float32Array(Array.from(fd))); + return new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.FaceDocLabel(faceDoc), float32Array); }); const faceMatcher = new FaceMatcher(faceDescriptors, 0.6); - const match = faceMatcher.findBestMatch(cur_descriptor); + const match = faceMatcher.findBestMatch(faceDescriptor); if (match.label !== 'unknown') { - for (const doc of DocListCast(Doc.ActiveDashboard[DocData].faceDocuments)) { - if (doc[DocData].face_label === match.label) { - return doc; + for (const faceDoc of FaceRecognitionHandler.FaceDocuments()) { + if (FaceRecognitionHandler.FaceDocLabel(faceDoc) === match.label) { + return faceDoc; } } } - return null; - } + return undefined; + }; /** * Loads an image diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index eb6ed9757..ffb5aab79 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -/* eslint-disable default-param-last */ /* eslint-disable no-use-before-define */ import { action, computed, makeObservable, observable, ObservableMap, ObservableSet, runInAction } from 'mobx'; import { computedFn } from 'mobx-utils'; @@ -127,9 +125,7 @@ export type FieldResult<T extends FieldType = FieldType> = Opt<T> | FieldWaiting * If no default value is given, and the returned value is not undefined, it can be safely modified. */ export function DocListCastAsync(field: FieldResult): Promise<Doc[] | undefined>; -// eslint-disable-next-line no-redeclare export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>; -// eslint-disable-next-line no-redeclare export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { const list = Cast(field, listSpec(Doc)); return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue); @@ -437,7 +433,6 @@ export class Doc extends RefField { const writeMode = DocServer.getFieldWriteMode(fKey); if (fKey.startsWith('acl_') || writeMode !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; - // eslint-disable-next-line no-await-in-loop await fn(); } else { this[CachedUpdates][fKey] = fn; @@ -1576,6 +1571,7 @@ export namespace Doc { try { resolved = JSON.parse(typeof data === 'string' ? data : JSON.stringify(data)); } catch (e) { + console.error(e); return undefined; } let output: Opt<Doc>; diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index d1316d256..b3534dde7 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -394,13 +394,11 @@ export namespace RichTextUtils { for (const markName of Object.keys(schema.marks)) { // eslint-disable-next-line no-cond-assign if (ignored.includes(markName) || !(mark = markMap[markName])) { - // eslint-disable-next-line no-continue continue; } let converted = MarkToStyle.get(markName) || (markName as keyof docsV1.Schema$TextStyle); let value: unknown = true; if (!converted) { - // eslint-disable-next-line no-continue continue; } // eslint-disable-next-line @typescript-eslint/no-shadow @@ -412,10 +410,8 @@ export namespace RichTextUtils { const docDelimeter = '/doc/'; const alreadyShared = '?sharing=true'; if (new RegExp(window.location.origin + docDelimeter).test(url) && !url.endsWith(alreadyShared)) { - // eslint-disable-next-line no-await-in-loop const linkDoc = await DocServer.GetRefField(url.split(docDelimeter)[1]); if (linkDoc instanceof Doc) { - // eslint-disable-next-line no-await-in-loop let exported = (await Cast(linkDoc.link_anchor_2, Doc))!; if (!exported.customLayout) { exported = Doc.MakeEmbedding(exported); diff --git a/src/server/database.ts b/src/server/database.ts index 975b9eb80..10dc540c3 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-namespace */ import * as mongodb from 'mongodb'; import * as mongoose from 'mongoose'; import { Opt } from '../fields/Doc'; @@ -148,9 +147,7 @@ export namespace Database { } public delete(query: any, collectionName?: string): Promise<mongodb.DeleteResult>; - // eslint-disable-next-line no-dupe-class-members public delete(id: string, collectionName?: string): Promise<mongodb.DeleteResult>; - // eslint-disable-next-line no-dupe-class-members public delete(idIn: any, collectionName = DocumentsCollection) { let id = idIn; if (typeof id === 'string') { diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index 97c63a93e..0cf9a6e58 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -115,7 +115,7 @@ function registerEmbeddedBrowseRelativePathHandler(server: express.Express) { // eslint-disable-next-line @typescript-eslint/no-explicit-any function proxyServe(req: any, requrl: string, response: any) { - // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports + // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const htmlBodyMemoryStream = new (require('memorystream'))(); let wasinBrFormat = false; const sendModifiedBody = () => { |