aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/Network.ts11
-rw-r--r--src/client/documents/DocUtils.ts2
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx88
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx97
-rw-r--r--src/client/views/nodes/FaceRectangles.tsx3
-rw-r--r--src/client/views/search/FaceRecognitionHandler.tsx64
6 files changed, 87 insertions, 178 deletions
diff --git a/src/client/Network.ts b/src/client/Network.ts
index 8876d8190..17f8a6534 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -1,3 +1,4 @@
+import formidable from 'formidable';
import * as requestPromise from 'request-promise';
import { ClientUtils } from '../ClientUtils';
import { Utils } from '../Utils';
@@ -49,15 +50,9 @@ export namespace Networking {
if (!fileguidpairs.length) {
return [];
}
- const maxFileSize = 5000000;
+ const maxFileSize = 6000000;
if (fileguidpairs.some(f => f.file.size > maxFileSize)) {
- return new Promise<Upload.FileResponse<T>[]>(res => {
- res([{
- source: { size: 0, filepath: '', mimetype: '', originalFilename: '', newFilename: '',hashAlgorithm: false,
- toJSON: () => ({ size: 0, filepath: '', mimetype: '', originalFilename: '', newFilename: '',name: '', length: 0, mtime: new Date(), type: '' }) },
- result: { name: '', message: `max file size (${maxFileSize / 1000000}MB) exceeded` }
- }]) // prettier-ignore
- });
+ return new Promise<Upload.FileResponse<T>[]>(res => res([{ source: { newFilename: '', mimetype: '' } as formidable.File, result: new Error(`max file size (${maxFileSize / 1000000}MB) exceeded`) }]));
}
formData.set('fileguids', fileguidpairs.map(pair => pair.guid).join(';'));
formData.set('filesize', fileguidpairs.reduce((sum, pair) => sum + pair.file.size, 0).toString());
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index a503d732b..35d835f1f 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -739,7 +739,7 @@ export namespace DocUtils {
const {
source: { newFilename, mimetype },
result,
- } = upfiles.lastElement() ?? { source: { newFilename: '', mimetype: '' }, result: { message: 'upload failed' } };
+ } = upfiles.lastElement() ?? { source: { newFilename: '', mimetype: '' }, result: new Error('upload failed') };
if (result instanceof Error) {
if (overwriteDoc) {
overwriteDoc.loadingError = result.message;
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 50b91e8fe..d5a2809dc 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -11,17 +11,20 @@ import { Doc, DocListCast } from '../../../../fields/Doc';
import { DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
-import { ImageCast, StrCast } from '../../../../fields/Types';
+import { listSpec } from '../../../../fields/Schema';
+import { Cast, ImageCast, StrCast } from '../../../../fields/Types';
import { DocumentType } from '../../../documents/DocumentTypes';
import { Docs } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
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 './FaceCollectionBox.scss';
import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
+import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler';
interface FaceDocumentProps {
faceDoc: Doc;
@@ -32,17 +35,15 @@ interface FaceDocumentProps {
*/
@observer
export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps> {
- private ref: React.RefObject<HTMLDivElement>;
- @observable _displayImages: boolean = true;
private _dropDisposer?: DragManager.DragDropDisposer;
- private _inputRef = React.createRef<HTMLInputElement>();
constructor(props: FaceDocumentProps) {
super(props);
makeObservable(this);
- this.ref = React.createRef();
}
+ @observable _displayImages: boolean = true;
+
protected createDropTarget = (ele: HTMLDivElement) => {
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.faceDoc));
@@ -54,20 +55,20 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps
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].faceDescriptors as List<List<number>>).length === 0 && (doc[DocData].faces as List<List<number>>).length > 1) {
+ if ((this._props.faceDoc[DocData].face_descriptors as List<List<number>>).length === 0 && (doc[DocData][FaceRecognitionHandler.FacesField(doc)] as List<List<number>>).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].faceDescriptors as List<List<number>>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor)));
- const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(StrCast(this._props.faceDoc[DocData].label), float32Array);
+ 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].faces as List<List<number>>).forEach(face => {
+ (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));
@@ -79,16 +80,15 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps
}
});
- if (doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`]) {
- doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] = new List<List<number>>([...(doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] as List<List<number>>), cur_matching_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][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] = new List<List<number>>([cur_matching_face]);
+ doc[DocData][faceFieldKey] = new List<List<number>>([cur_matching_face]);
}
- this._props.faceDoc[DocData].associatedDocs = new List<Doc>([...DocListCast(this._props.faceDoc[DocData].associatedDocs), doc]);
- this._props.faceDoc[DocData].faceDescriptors = new List<List<number>>([...(this._props.faceDoc[DocData].faceDescriptors as List<List<number>>), cur_matching_face]);
-
- //const match = faceMatcher.findBestMatch(cur_descriptor);
+ 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
}
});
return false;
@@ -107,12 +107,11 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps
/**
* Deletes a Face Document.
*/
- @action
- deleteFaceDocument = () => {
+ deleteFaceDocument = undoable(() => {
if (Doc.ActiveDashboard) {
- Doc.ActiveDashboard[DocData].faceDocuments = new List<Doc>(DocListCast(Doc.ActiveDashboard[DocData].faceDocuments).filter(doc => doc !== this._props.faceDoc));
+ Doc.RemoveDocFromList(Doc.ActiveDashboard[DocData], 'faceDocuments', this._props.faceDoc);
}
- };
+ }, 'remove face');
/**
* Deletes a document from a Face Document's associated docs list.
@@ -120,60 +119,44 @@ export class FaceDocumentItem extends ObservableReactComponent<FaceDocumentProps
*/
@action
deleteAssociatedDoc = (doc: Doc) => {
- this._props.faceDoc[DocData].faceDescriptors = new List<List<number>>(
- (this._props.faceDoc[DocData].faceDescriptors as List<List<number>>).filter(fd => !(doc[DocData][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] as List<List<number>>).includes(fd))
+ 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][`FACE DESCRIPTOR - ${this._props.faceDoc[DocData].label}`] = new List<List<number>>();
- this._props.faceDoc[DocData].associatedDocs = new List<Doc>(DocListCast(this._props.faceDoc[DocData].associatedDocs).filter(associatedDoc => associatedDoc !== doc));
+ doc[DocData][FaceRecognitionHandler.FaceField(doc, this._props.faceDoc)] = new List<List<number>>();
+ Doc.RemoveDocFromList(this._props.faceDoc[DocData], 'face_docList', doc);
};
render() {
return (
<div className="face-document-item" ref={ele => this.createDropTarget(ele!)}>
<div className="face-collection-buttons">
- <IconButton tooltip={'Delete Face From Collection'} onPointerDown={this.deleteFaceDocument} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} />
+ <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].label)}</h1>
+ <h1>{StrCast(this._props.faceDoc[DocData].face_label)}</h1>
</div>
<IconButton
- tooltip={'See image information'}
+ tooltip="See image information"
onPointerDown={() => this.onDisplayClick()}
- icon={this._displayImages ? <FontAwesomeIcon icon="caret-up" /> : <FontAwesomeIcon icon="caret-down" />}
+ icon={<FontAwesomeIcon icon={this._displayImages ? 'caret-up' : 'caret-down'} />}
color={MarqueeOptionsMenu.Instance.userColor}
style={{ width: '19px' }}
/>
{this._displayImages ? (
<div className="face-document-image-container">
- {DocListCast(this._props.faceDoc[DocData].associatedDocs).map(doc => {
+ {DocListCast(this._props.faceDoc[DocData].face_docList).map(doc => {
const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
return (
<div className="image-wrapper" key={Utils.GenerateGuid()}>
- <img
- onClick={async () => {
- await DocumentView.showDocument(doc, { willZoomCentered: true });
- }}
- style={{ maxWidth: '60px', margin: '10px' }}
- src={`${name}_o.${type}`}
- />
+ <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}
- />
+ <IconButton tooltip={'Remove Doc From Face Collection'} onPointerDown={() => this.deleteAssociatedDoc(doc)} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} />
</div>
</div>
);
})}
</div>
- ) : (
- <div></div>
- )}
+ ) : null}
</div>
);
}
@@ -190,9 +173,8 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get currentDocs() {
if (Doc.ActiveDashboard) {
return DocListCast(Doc.ActiveDashboard[DocData].faceDocuments);
- } else {
- return [];
}
+ return [];
}
constructor(props: FieldViewProps) {
@@ -204,9 +186,9 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() {
render() {
return (
<div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}>
- {this.currentDocs.map(doc => {
- return <FaceDocumentItem key={doc[Id]} faceDoc={doc} />;
- })}
+ {this.currentDocs.map(doc => (
+ <FaceDocumentItem key={doc[Id]} faceDoc={doc} />
+ ))}
</div>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index d7a41df64..6fee076ee 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,28 +1,24 @@
-/* eslint-disable jsx-a11y/no-static-element-interactions */
-import similarity from 'compute-cosine-similarity';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils';
-import { intersectRect, numberRange } from '../../../../Utils';
-import { Doc, DocListCast, NumListCast, Opt } from '../../../../fields/Doc';
+import { intersectRect } from '../../../../Utils';
+import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
-import { InkData, InkField, InkTool } from '../../../../fields/InkField';
+import { InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
-import { RichTextField } from '../../../../fields/RichTextField';
-import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../../fields/Types';
import { ImageField } from '../../../../fields/URLField';
import { GetEffectiveAcl } from '../../../../fields/util';
-import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT';
-import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
import { DocUtils } from '../../../documents/DocUtils';
-import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { DocumentType } from '../../../documents/DocumentTypes';
import { Docs, DocumentOptions } from '../../../documents/Documents';
import { SnappingManager, freeformScrollMode } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
import { UndoManager, undoBatch } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
+import { MainView } from '../../MainView';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { MarqueeViewBounds } from '../../PinFuncs';
import { PreviewCursor } from '../../PreviewCursor';
@@ -30,15 +26,10 @@ import { DocumentView } from '../../nodes/DocumentView';
import { OpenWhere } from '../../nodes/OpenWhere';
import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { CollectionCardView } from '../CollectionCardDeckView';
import { SubCollectionViewProps } from '../CollectionSubView';
-import { CollectionFreeFormView } from './CollectionFreeFormView';
-import { ImageLabelHandler } from './ImageLabelHandler';
+import { ImageLabelBoxData } from './ImageLabelBox';
import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
import './MarqueeView.scss';
-import { MainView } from '../../MainView';
-import { ImageLabelBox, ImageLabelBoxData } from './ImageLabelBox';
-import { SearchBox } from '../../search/SearchBox';
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -68,7 +59,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
static Instance: MarqueeView;
- constructor(props: any) {
+ constructor(props: SubCollectionViewProps & MarqueeViewProps) {
super(props);
makeObservable(this);
MarqueeView.Instance = this;
@@ -496,71 +487,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
});
@undoBatch
- syntaxHighlight = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
- const selected = this.marqueeSelect(false);
- if (e instanceof KeyboardEvent ? e.key === 'i' : true) {
- const inks = selected.filter(s => s.type === DocumentType.INK);
- const setDocs = selected.filter(s => s.type === DocumentType.RTF && s.color);
- const sets = setDocs.map(sd => Cast(sd.data, RichTextField)?.Text as string);
- const colors = setDocs.map(sd => FieldValue(sd.color) as string);
- const wordToColor = new Map<string, string>();
- sets.forEach((st: string, i: number) => st.split(',').forEach(word => wordToColor.set(word, colors[i])));
- const strokes: InkData[] = [];
- inks.filter(i => Cast(i.data, InkField)).forEach(i => {
- const d = Cast(i.data, InkField, null);
- const left = Math.min(...(d?.inkData.map(pd => pd.X) ?? [0]));
- const top = Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0]));
- strokes.push(d.inkData.map(pd => ({ X: pd.X + NumCast(i.x) - left, Y: pd.Y + NumCast(i.y) - top })));
- });
- CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {
- // const wordResults = results.filter((r: any) => r.category === "inkWord");
- // for (const word of wordResults) {
- // const indices: number[] = word.strokeIds;
- // indices.forEach(i => {
- // if (wordToColor.has(word.recognizedText.toLowerCase())) {
- // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase());
- // }
- // else {
- // for (const alt of word.alternates) {
- // if (wordToColor.has(alt.recognizedString.toLowerCase())) {
- // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase());
- // break;
- // }
- // }
- // }
- // })
- // }
- // const wordResults = results.filter((r: any) => r.category === "inkWord");
- // for (const word of wordResults) {
- // const indices: number[] = word.strokeIds;
- // indices.forEach(i => {
- // const otherInks: Doc[] = [];
- // indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2]));
- // inks[i].relatedInks = new List<Doc>(otherInks);
- // const uniqueColors: string[] = [];
- // Array.from(wordToColor.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c));
- // inks[i].alternativeColors = new List<string>(uniqueColors);
- // if (wordToColor.has(word.recognizedText.toLowerCase())) {
- // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase());
- // }
- // else if (word.alternates) {
- // for (const alt of word.alternates) {
- // if (wordToColor.has(alt.recognizedString.toLowerCase())) {
- // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase());
- // break;
- // }
- // }
- // }
- // });
- // }
- const lines = results.filter((r: any) => r.category === 'line');
- const text = lines.map((l: any) => l.recognizedText).join('\r\n');
- this._props.addDocument?.(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text }));
- });
- }
- });
-
- @undoBatch
summary = action(() => {
const selected = this.marqueeSelect(false).map(d => {
this._props.removeDocument?.(d);
@@ -704,8 +630,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
transform: `translate(${p[0]}px, ${p[1]}px)`,
width: Math.abs(v[0]),
height: Math.abs(v[1]),
- color: lightOrDark(this._props.Document?.backgroundColor ?? 'white'),
- borderColor: lightOrDark(this._props.Document?.backgroundColor ?? 'white'),
+ color: lightOrDark((this._props.Document?.backgroundColor as string) ?? 'white'),
+ borderColor: lightOrDark((this._props.Document?.backgroundColor as string) ?? 'white'),
zIndex: 2000,
}}>
{' '}
@@ -714,7 +640,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
<polyline //
points={this._lassoPts.reduce((s, pt) => s + pt[0] + ',' + pt[1] + ' ', '')}
fill="none"
- stroke={lightOrDark(this._props.Document?.backgroundColor ?? 'white')}
+ stroke={lightOrDark((this._props.Document?.backgroundColor as string) ?? 'white')}
strokeWidth="1"
strokeDasharray="3"
/>
@@ -753,7 +679,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
};
render() {
return (
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div
className="marqueeView"
ref={r => {
diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx
index ade4225d9..19aa90a8b 100644
--- a/src/client/views/nodes/FaceRectangles.tsx
+++ b/src/client/views/nodes/FaceRectangles.tsx
@@ -4,6 +4,7 @@ import { Doc, DocListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { Cast, NumCast } from '../../../fields/Types';
import FaceRectangle from './FaceRectangle';
+import { FaceRecognitionHandler } from '../search/FaceRecognitionHandler';
interface FaceRectanglesProps {
document: Doc;
@@ -19,7 +20,7 @@ export interface RectangleTemplate {
@observer
export class FaceRectangles extends React.Component<FaceRectanglesProps> {
render() {
- const faces = DocListCast(this.props.document.faces);
+ const faces = DocListCast(this.props.document[FaceRecognitionHandler.FacesField(this.props.document)]);
const templates: RectangleTemplate[] = faces.map(faceDoc => {
const rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc;
const style = {
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index 29ca6e797..dc271fe73 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -1,42 +1,46 @@
import * as faceapi from 'face-api.js';
import { FaceMatcher } from 'face-api.js';
+import { computed } from 'mobx';
import { Doc, DocListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { List } from '../../../fields/List';
+import { listSpec } from '../../../fields/Schema';
import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
-import { DocumentManager } from '../../util/DocumentManager';
-import { computed } from 'mobx';
import { DocumentType } from '../../documents/DocumentTypes';
-import { listSpec } from '../../../fields/Schema';
+import { DocumentManager } from '../../util/DocumentManager';
/**
* A class that handles face recognition.
*/
export class FaceRecognitionHandler {
static _instance: FaceRecognitionHandler;
- private loadedModels: boolean = false;
- @computed get examinedFaceDocs() {
- return DocListCast(Doc.UserDoc().examinedFaceDocs);
- }
- processingDocs: Set<Doc> = new Set();
- pendingLoadDocs: Doc[] = [];
+ private _loadedModels: boolean = false;
+ 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`;
constructor() {
FaceRecognitionHandler._instance = this;
- this.loadModels().then(() => this.pendingLoadDocs.forEach(this.findMatches));
+ this.loadModels().then(() => this._pendingLoadDocs.forEach(this.findMatches));
DocumentManager.Instance.AddAnyViewRenderedCB(dv => FaceRecognitionHandler.Instance.findMatches(dv.Document));
}
+ @computed get examinedFaceDocs() {
+ return DocListCast(Doc.UserDoc().examinedFaceDocs);
+ }
+
/**
* Loads the face detection models.
*/
- async loadModels() {
+ loadModels = async () => {
const MODEL_URL = `/models`;
await faceapi.loadFaceDetectionModel(MODEL_URL);
await faceapi.loadFaceLandmarkModel(MODEL_URL);
await faceapi.loadFaceRecognitionModel(MODEL_URL);
- this.loadedModels = true;
- }
+ this._loadedModels = true;
+ };
public static get Instance() {
return FaceRecognitionHandler._instance ?? new FaceRecognitionHandler();
@@ -47,8 +51,8 @@ export class FaceRecognitionHandler {
* @param doc The document being analyzed.
*/
public findMatches = async (doc: Doc) => {
- if (!this.loadedModels || !Doc.ActiveDashboard) {
- this.pendingLoadDocs.push(doc);
+ if (!this._loadedModels || !Doc.ActiveDashboard) {
+ this._pendingLoadDocs.push(doc);
return;
}
@@ -59,12 +63,12 @@ export class FaceRecognitionHandler {
const imgUrl = ImageCast(doc[Doc.LayoutFieldKey(doc)]);
// 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(doc) || this._processingDocs.has(doc)) {
return;
}
// Mark the document as being processed.
- this.processingDocs.add(doc);
+ this._processingDocs.add(doc);
// Get the image the document contains and analyze for faces.
const [name, type] = imgUrl.url.href.split('.');
@@ -74,7 +78,7 @@ export class FaceRecognitionHandler {
const fullFaceDescriptions = await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors();
- doc[DocData].faces = new List<List<number>>();
+ doc[DocData][FaceRecognitionHandler.FacesField(doc)] = new List<List<number>>();
// For each face detected, find a match.
for (const fd of fullFaceDescriptions) {
@@ -84,34 +88,36 @@ export class FaceRecognitionHandler {
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, 'associatedDocs', doc);
- Cast(match.faceDescriptors, 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(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
} else {
// If a matching Face Document has not been found, create a new Face Document.
Doc.UserDoc().faceDocNum = NumCast(Doc.UserDoc().faceDocNum) + 1;
const newFaceDocument = new Doc();
- newFaceDocument.label = `Face ${Doc.UserDoc().faceDocNum}`;
- newFaceDocument.associatedDocs = new List<Doc>([doc]);
- newFaceDocument.faceDescriptors = new List<List<number>>([converted_list]);
+ 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]);
Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'faceDocuments', newFaceDocument);
match = newFaceDocument;
}
// Assign a field in the document of the matching Face Document.
- const faceDescripField = `FACE DESCRIPTOR - ${match[DocData].label}`;
+ 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].faces, listSpec('number'), null).push(converted_list as unknown as number); // items are lists of numbers, not numbers, but type system can't handle that
+ 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);
}
- this.processingDocs.delete(doc);
+ this._processingDocs.delete(doc);
};
/**
@@ -125,14 +131,14 @@ export class FaceRecognitionHandler {
}
const faceDescriptors: faceapi.LabeledFaceDescriptors[] = DocListCast(Doc.ActiveDashboard[DocData].faceDocuments).map(faceDocument => {
- const float32Array = (faceDocument[DocData].faceDescriptors as List<List<number>>).map(faceDescriptor => new Float32Array(Array.from(faceDescriptor)));
- return new faceapi.LabeledFaceDescriptors(StrCast(faceDocument[DocData].label), float32Array);
+ 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 faceMatcher = new FaceMatcher(faceDescriptors, 0.6);
const match = faceMatcher.findBestMatch(cur_descriptor);
if (match.label !== 'unknown') {
for (const doc of DocListCast(Doc.ActiveDashboard[DocData].faceDocuments)) {
- if (doc[DocData].label === match.label) {
+ if (doc[DocData].face_label === match.label) {
return doc;
}
}