Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial faces work #132

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
.cache
.idea
.vscode
.nyc_output
.tap
bin
build
bundle-debug.log
data
Expand Down
7 changes: 5 additions & 2 deletions packages/database/src/media/faces.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const getFaces = (entry, minScore) => {
}

const { width, height, data } = faces;

return data
.filter(face => face.alignedRect.score >= minScore)
.map(face => {
Expand All @@ -18,9 +19,11 @@ const getFaces = (entry, minScore) => {
.map(v => v.expression)
.slice(0, 2)

const gender = face.genderProbability > 0.7 ? face.gender : 'unknown';
const age = +face.age.toFixed(1);

return {
age: +face.age.toFixed(1),
gender: face.genderProbability > 0.7 ? face.gender : 'unknown',
faceTag: Boolean(face.faceTag) ? face.faceTag : `${gender} (${age}y)`,
expressions,
x: +(box.x / width).toFixed(3),
y: +(box.y / height).toFixed(3),
Expand Down
55 changes: 49 additions & 6 deletions packages/events/src/apply-events.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import { Event, EventAction } from './models';
import { random } from 'lodash';
import { Event, EventAction, FaceTag, Rect } from './models';

import { Taggable } from './taggable';

const defaultRect: Rect = {
x: -1,
y: -1,
width: -1,
height: -1
}

const findFace = (faces: any[], rect: Rect) => {
return faces.findIndex((face) =>
face.x == rect.x
&& face.y == rect.y
&& face.width == rect.width
&& face.height == rect.height
)
}

const applyEventAction = <T extends Taggable>(data: T, action: EventAction): boolean => {
let changed = false;
switch (action.action) {
case 'addTag': {
if (!data.tags) {
data.tags = [];
}
if (data.tags.indexOf(action.value) < 0) {
data.tags.push(action.value);
if (data.tags.indexOf(action.value as string) < 0) {
data.tags.push(action.value as string);
changed = true;
}
break;
Expand All @@ -19,19 +36,45 @@ const applyEventAction = <T extends Taggable>(data: T, action: EventAction): boo
if (!data.tags || !data.tags.length) {
return false;
}
const index = data.tags.indexOf(action.value);
const index = data.tags.indexOf(action.value as string);
if (index >= 0) {
data.tags.splice(index, 1);
changed = true;
}
break;
}

case 'addFaceTag': {
if (!data.faces || !data.faces.length) {
return false;
}

const faceIdx = findFace(data.faces, (action.value as FaceTag).rect)
if (faceIdx >= 0) {
data.faces[faceIdx].faceTag = (action.value as FaceTag).name;
changed = true;
}
break;
}
case 'removeFaceTag': {
if (!data.faces || !data.faces.length) {
return false;
}
const faceIdx = findFace(data.faces, (action.value as FaceTag).rect)
if (faceIdx >= 0) {
data.faces[faceIdx].faceTag = `unknown (${random(0, 1000)})`;
changed = true;
}
break;
}
}
return changed;
}

const isValidEvent = (event: Event) => {
return event.type == 'userAction' && event.targetIds?.length && event.actions?.length
return event.type == 'userAction'
&& event.targetIds?.length
&& event.actions?.length
}

const applyEventDate = (entry: Taggable, event: Event) => {
Expand All @@ -42,7 +85,7 @@ const applyEventDate = (entry: Taggable, event: Event) => {
}
}

type EntryIdMap = {[key: string]: Taggable[]}
type EntryIdMap = { [key: string]: Taggable[] }

const idMapReducer = (result: EntryIdMap, entry: Taggable) => {
const id = entry.id
Expand Down
14 changes: 13 additions & 1 deletion packages/events/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@ export interface Event {

export interface EventAction {
action: string;
value: string;
value: string|FaceTag;
}

export interface FaceTag {
name: string;
rect: Rect;
}

export interface Rect {
x: number;
y: number;
width: number;
height: number;
}

export type EventListener = (event: Event) => void;
1 change: 1 addition & 0 deletions packages/events/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { readEvents } from './read-events';
export { mergeEvents } from './merge-events';
export { appendEvent, appendEvents, writeEvents } from './append-event';
export { applyEvents } from './apply-events'
export * from './models'
1 change: 1 addition & 0 deletions packages/events/src/taggable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export interface Taggable {
id: string;
updated?: string;
tags?: string[];
faces?: any[];
appliedEventIds?: string[];
}
1 change: 0 additions & 1 deletion packages/webapp/src/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { Map } from './map';
import { MediaView } from './single/MediaView';
import { useAppConfig } from './utils/useAppConfig'
import { loadDatabase, OfflineDatabase } from './offline'
import { applyEvents } from "@home-gallery/events";

export const Root = () => {
return (
Expand Down
21 changes: 20 additions & 1 deletion packages/webapp/src/api/ApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
import { Event, EventAction } from '@home-gallery/events'
import { pushEvent as pushEventApi, eventStream as eventStreamApi, ServerEventListener, getTree, mapEntriesForBrowser } from './api';
import { UnsavedEventHandler } from './UnsavedEventHandler';
import { Tag } from './models';
import { FaceTag, Tag } from './models';
import { Entry } from "../store/entry";
import { OfflineDatabase } from '../offline'
import { EventBus } from './EventBus';
Expand All @@ -18,12 +18,31 @@ const tagToAction = (tag: Tag): EventAction => {
}
}

const faceTagToAction = (tag: FaceTag): EventAction => {
if (tag.remove) {
return {action: 'removeFaceTag', value: {name:tag.name, rect:tag.rect}}
} else {
return {action: 'addFaceTag', value: {name:tag.name, rect:tag.rect}}
}
}

export const addTags = async (entryIds: string[], tags: Tag[]) => {
const actions = tags.map(tagToAction);
const event: Event = {type: 'userAction', id: uuidv4(), targetIds: entryIds, actions };
return pushEvent(event);
}

export const addFaceTags = async (entryIds: string[], faceTags: FaceTag[]) => {
const actions = faceTags.map(faceTagToAction);
const event: Event = {
type: 'userAction',
id: uuidv4(),
targetIds: entryIds,
actions
};
return pushEvent(event);
}

let eventStreamSubscribed = false;

const unsavedEventHandler = new UnsavedEventHandler();
Expand Down
6 changes: 6 additions & 0 deletions packages/webapp/src/api/models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Rect } from '@home-gallery/events'

export interface Tag {
name: string;
remove: boolean;
}

export interface FaceTag extends Tag {
rect: Rect;
}
132 changes: 132 additions & 0 deletions packages/webapp/src/dialog/dialog-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as React from "react";

import { Tag, FaceTag } from "../api/models";

import { MultiTagDialog, SingleTagDialog } from './tag-dialog'
import { MultiFaceTagDialog, SingleFaceTagDialog } from './face-tag-dialog'

export type TagsConfig = {
initialTags?: Tag[];
onTagsSubmit: ({ tags }: { tags: Tag[] }) => void;
}

export type FaceTagsConfig = {
initialFaceTags?: FaceTag[];
onFaceTagsSubmit: ({ faceTags }: { faceTags: FaceTag[] }) => void;
}

const initialTagsConfig: TagsConfig = {
initialTags: [],
onTagsSubmit: () => false,
}

const initialFaceTagsConfig: FaceTagsConfig = {
initialFaceTags: [],
onFaceTagsSubmit: () => false
}

export type DialogContextType = {
setTagsDialogVisible: (visible: boolean) => void;
openTagsDialog: ({ initialTags, onTagsSubmit }: TagsConfig) => void

setFaceTagsDialogVisible: (visible: boolean) => void;
openFaceTagsDialog: ({ initialFaceTags, onFaceTagsSubmit }: FaceTagsConfig) => void
}

const initialDialogContextValue: DialogContextType = {
setTagsDialogVisible: () => false,
openTagsDialog: () => false,

setFaceTagsDialogVisible: () => false,
openFaceTagsDialog: () => false
}

export const DialogContext = React.createContext<DialogContextType>(initialDialogContextValue)

export const MultiTagDialogProvider = ({ children }) => {
const [tagsDialogVisible, setTagsDialogVisible] = React.useState(false);
const [facesDialogVisible, setFaceTagsDialogVisible] = React.useState(false);

const [tagsConfig, setTagsConfig] = React.useState<TagsConfig>(initialTagsConfig);
const [faceTagsConfig, setFaceTagsConfig] = React.useState<FaceTagsConfig>(initialFaceTagsConfig);

const openTagsDialog = (tagsConfig: TagsConfig) => {
setTagsDialogVisible(true);
setFaceTagsDialogVisible(false);

setTagsConfig((prev) => ({ ...prev, ...tagsConfig }));
};

const onTagsSubmit = ({ tags }) => {
tagsConfig.onTagsSubmit({ tags })
}

const openFaceTagsDialog = (faceTagsConfig: FaceTagsConfig) => {
setTagsDialogVisible(true);
setFaceTagsDialogVisible(false);

setFaceTagsConfig((prev) => ({ ...prev, ...faceTagsConfig }));
};

const onFaceTagsSubmit = ({ faceTags }) => {
faceTagsConfig.onFaceTagsSubmit({ faceTags })
}

return (
<DialogContext.Provider value={{ setTagsDialogVisible, openTagsDialog, setFaceTagsDialogVisible, openFaceTagsDialog }}>
{children}
{tagsDialogVisible && (
<MultiTagDialog onSubmit={onTagsSubmit} onCancel={() => setTagsDialogVisible(false)}></MultiTagDialog>
)}
{ facesDialogVisible && (
<MultiFaceTagDialog onSubmit={onFaceTagsSubmit} onCancel={() => setFaceTagsDialogVisible(false)}></MultiFaceTagDialog>
)}
</DialogContext.Provider>
)
}

export const SingleTagDialogProvider = ({ children }) => {
const [tagsDialogVisible, setTagsDialogVisible] = React.useState(false);
const [facesDialogVisible, setFaceTagsDialogVisible] = React.useState(false);

const [tagsConfig, setTagsConfig] = React.useState<TagsConfig>(initialTagsConfig);
const [faceTagsConfig, setFaceTagsConfig] = React.useState<FaceTagsConfig>(initialFaceTagsConfig);


const openTagsDialog = ({ initialTags, onTagsSubmit }: TagsConfig) => {
setTagsDialogVisible(true);
setFaceTagsDialogVisible(false);

setTagsConfig({ initialTags, onTagsSubmit });
};

const openFaceTagsDialog = ({ initialFaceTags, onFaceTagsSubmit }: FaceTagsConfig) => {
setTagsDialogVisible(false);
setFaceTagsDialogVisible(true);

setFaceTagsConfig({ initialFaceTags, onFaceTagsSubmit });
};

const onTagsSubmit = ({ tags }) => {
tagsConfig.onTagsSubmit({ tags })
}

const onFaceTagsSubmit = ({ faceTags }) => {
faceTagsConfig.onFaceTagsSubmit({ faceTags })
}


return (
<DialogContext.Provider value={{ setTagsDialogVisible, openTagsDialog, setFaceTagsDialogVisible, openFaceTagsDialog }}>
{children}
{tagsDialogVisible && (
<SingleTagDialog tags={tagsConfig.initialTags || []} onSubmit={onTagsSubmit} onCancel={() => setTagsDialogVisible(false)}></SingleTagDialog>
)}
{facesDialogVisible && (
<SingleFaceTagDialog faceTags={faceTagsConfig.initialFaceTags || []} onSubmit={onFaceTagsSubmit} onCancel={() => setFaceTagsDialogVisible(false)}></SingleFaceTagDialog>
)}
</DialogContext.Provider>
)
}


29 changes: 29 additions & 0 deletions packages/webapp/src/dialog/face-tag-dialog-help.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import * as icons from '@fortawesome/free-solid-svg-icons'

export const MultiTagHelp = ({show, setShow}) => {
return (
<>
{show &&
<div className="relative p-4 border rounded bg-info-800 border-info-900">
<button className="absolute flex items-center justify-center w-8 h-8 rounded top-2 right-4 hover:bg-gray-800/50 active:bg-gray-800/70" onClick={() => setShow(false)}><FontAwesomeIcon icon={icons.faTimes} className="text-info-950" /></button>
<p className="text-gray-400">Add single face tags with <i className="text-gray-200">Enter key</i> or <i className="text-gray-200">comma sign</i>. Prefix tag with <i className="text-gray-200">minus sign</i> to remove tag from the media. E.g. <i className="text-gray-200">newTag, -removeTag</i>. Click on the tag to toggle between <i className="text-gray-200">add</i> and <i className="text-gray-200">remove</i> action.</p>
</div>
}
</>
)
}

export const SingleTagHelp = ({show, setShow}) => {
return (
<>
{show &&
<div className="relative p-4 border rounded bg-info-800 border-info-900">
<button className="absolute flex items-center justify-center w-8 h-8 rounded top-2 right-4 hover:bg-gray-800/50 active:bg-gray-800/70" onClick={() => setShow(false)}><FontAwesomeIcon icon={icons.faTimes} className="text-info-950" /></button>
<p className="text-gray-400">Add single face tags with <i className="text-gray-200">Enter key</i> or <i className="text-gray-200">comma sign</i>. E.g. <i className="text-gray-200">newTag, otherTag</i>.</p>
</div>
}
</>
)
}
Loading