diff --git a/languages/en_CA.json b/languages/en_CA.json index cfd44653..a533b689 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -1739,5 +1739,15 @@ "Last Updated": "Last Updated", "Z-Wave Manager": "Z-Wave Manager", "Z-Wave": "Z-Wave", - "Primary":"Primary" + "Primary":"Primary", + "Upload Images": "Upload Images", + "Images Sent": "Images Sent", + "Click to Upload Images": "Click to Upload Images", + "Face Name": "Face Name", + "faceManager": "Face Manager", + "Face Manager": "Face Manager", + "deleteFace": "Delete Face", + "deleteFaceText": "Are you sure you want to delete ALL the images for this face {face}? they will not be recoverable.", + "deleteImage": "Delete Image", + "deleteImageText": "Are you sure you want to delete this image {image} of {face}? it will not be recoverable." } diff --git a/libs/config.js b/libs/config.js index adc0fdd2..23238875 100644 --- a/libs/config.js +++ b/libs/config.js @@ -36,6 +36,7 @@ module.exports = function(s){ if(config.cron.interval === undefined)config.cron.interval=1; if(config.databaseType === undefined){config.databaseType='mysql'} if(config.pluginKeys === undefined)config.pluginKeys={}; + if(config.enableFaceManager === undefined)config.enableFaceManager=false; if(config.databaseLogs === undefined){config.databaseLogs=false} if(config.useUTC === undefined){config.useUTC=false} if(config.iconURL === undefined){config.iconURL = "https://shinobi.video/libs/assets/icon/apple-touch-icon-152x152.png"} diff --git a/libs/faceManager.js b/libs/faceManager.js new file mode 100644 index 00000000..ad7374e0 --- /dev/null +++ b/libs/faceManager.js @@ -0,0 +1,332 @@ +const fs = require('fs'); +const fsExtra = require("fs-extra"); +const fileUpload = require('express-fileupload'); +const { execPath } = require('process'); + +const MODULE_NAME = "face"; + +module.exports = (s, config, lang, app, io) => { + const data = { + faces: {} + }; + + const sendDataToConnectedSuperUsers = (message) => { + return s.tx(message, '$'); + }; + + const reloadFacesData = (notify = false) => { + data.faces = {}; + + fs.readdir(config.facesFolder, (err, folders) => { + folders.forEach((name) => { + const faceDirectory = `${config.facesFolder}${name}`; + const stats = fs.statSync(faceDirectory); + + if(stats.isDirectory()) { + try { + data.faces[name] = fs.readdirSync(faceDirectory); + + } catch (ex) { + console.error(`Failed to load faces and images, Error: ${ex}`); + } + } + }); + + const message = { + f: 'recompileFaceDescriptors', + faces: data.faces, + path: config.facesFolder + }; + + sendDataToConnectedSuperUsers(message); + + s.sendToAllDetectors(message); + }); + }; + + const notifyImageUploaded = (name, image, authUser) => { + const endpoint = `${name}/image/${image}`; + const fileLink = getUrl(endpoint, authUser); + + sendDataToConnectedSuperUsers({ + f:`${MODULE_NAME}ImageUploaded`, + faceName: name, + fileName: image, + url: fileLink + }); + }; + + const notifyContentDeleted = (name, image = null) => { + const isImageDeleted = image !== undefined && image !== null; + const deleteDescription = isImageDeleted ? "Image" : "Folder"; + + const message = { + f: `${MODULE_NAME}${deleteDescription}Deleted`, + faceName: name + } + + if(isImageDeleted) { + message["fileName"] = image + } + + sendDataToConnectedSuperUsers(message); + }; + + const getUrl = (endpoint, authUser = ":auth") => { + const url = `${config.webPaths.superApiPrefix}${authUser}/${MODULE_NAME}${endpoint}`; + + return url; + }; + + const getImagePath = (name, image) => { + const path = `${getFacePath(name)}/${image}`; + + return path; + }; + + const getFacePath = (name) => { + const path = `${config.facesFolder}${name}`; + + return path; + }; + + const checkFile = (file, name, authUser) => { + const fileName = file.name; + const fileParts = fileName.split("."); + const fileExt = fileParts[fileParts.length - 1]; + const allowedExtensions = ["jpeg", "jpg", "png"]; + const canUpload = allowedExtensions.includes(fileExt); + const result = canUpload ? fileName : null; + + if(canUpload) { + const facePath = getFacePath(name); + const imagePath = getImagePath(name, fileName); + + if(!fs.existsSync(facePath)){ + fs.mkdirSync(facePath); + } + + file.mv(imagePath, function(err) { + if(err) { + console.error(`Failed to store image in ${imagePath}, Error: ${err}`); + } else { + notifyImageUploaded(name, fileName, authUser); + } + }); + } + + return result; + }; + + const registerDeleteEndpoint = (endpoint, handler) => { + const url = getUrl(endpoint); + + app.delete(url, (req, res) => { + s.superAuth(req.params, superAuthResponse => { + handler(req, res, superAuthResponse); + }, res, req); + }); + }; + + const registerGetEndpoint = (endpoint, handler) => { + const url = getUrl(endpoint); + + app.get(url, (req, res) => { + s.superAuth(req.params, superAuthResponse => { + handler(req, res, superAuthResponse); + }, res, req); + }); + }; + + const registerPostEndpoint = (endpoint, handler, isFileUpload = false) => { + const url = getUrl(endpoint); + + if(isFileUpload) { + app.post(url, fileUpload(), (req, res) => { + s.superAuth(req.params, superAuthResponse => { + handler(req, res, superAuthResponse); + }, res, req); + }); + } else { + app.post(url, (req, res) => { + s.superAuth(req.params, superAuthResponse => { + handler(req, res, superAuthResponse); + }, res, req); + }); + } + }; + + const handleGetFaces = (req, res, superAuthResponse) => { + res.json({ + ok: true, + faces: data.faces + }); + }; + + const handleGetImage = (req, res, superAuthResponse) => { + const imagePath = getImagePath(req.params.name, req.params.image); + + if(fs.existsSync(imagePath)) { + res.setHeader('Content-Type', 'image/jpeg'); + fs.createReadStream(imagePath).pipe(res); + + } else { + res.json({ + ok: false, + msg: lang['File Not Found'] + }); + } + }; + + const handleDeleteImage = (req, res, superAuthResponse) => { + const name = req.params.name; + const image = req.params.image; + + const imagePath = getImagePath(name, image); + + if(fs.existsSync(imagePath)) { + fs.rm(imagePath,() => { + reloadFacesData(true); + + console.info(`Delete image '${image}' of face '${name}' completed successfuly`); + + notifyContentDeleted(name, image); + + res.json({ + ok: true, + }); + }); + + } else { + res.json({ + ok: false, + }); + } + }; + + const handleDeleteFace = (req, res, superAuthResponse) => { + const name = req.params.name; + const facePath = getFacePath(name); + + if(fs.existsSync(facePath)) { + fsExtra.emptyDirSync(facePath); + + fs.rmdir(facePath, () => { + reloadFacesData(true); + + console.info(`Delete face '${name}' completed successfuly`); + + notifyContentDeleted(name); + + res.json({ + ok: true, + }); + }); + + } else { + res.json({ + ok: false, + }); + } + }; + + const handleMoveImage = (req, res, superAuthResponse) => { + const oldImagePath = getImagePath(req.params.name, req.params.image); + const newImagePath = getImagePath(req.params.newName, req.params.image); + const fileExists = fs.existsSync(oldImagePath); + + if(fileExists) { + fs.rename(oldImagePath, newImagePath, (err, content) => { + notifyContentDeleted(req.params.name, req.params.image); + + notifyImageUploaded(req.params.newName, req.params.newImage, req.params.auth); + + if(err) { + console.error(`Failed to move file from ${oldImagePath} to ${newImagePath}, Error: ${err}`); + } else { + reloadFacesData(true); + + console.info(`Handle image move completed successfuly, From: ${oldImagePath}, To: ${newImagePath}`); + } + }); + } else { + console.error(`Handle image move failed file '${oldImagePath}' does not exists`); + } + + res.json({ + ok: fileExists, + }); + }; + + const handleImageUpload = (req, res, superAuthResponse) => { + const fileKeys = Object.keys(req.files || {}); + + if(fileKeys.length == 0){ + return res.status(400).send('No files were uploaded.'); + } + + fileKeys.forEach(key => { + const file = req.files[key]; + + try { + const files = file instanceof Array ? [...file] : [file]; + + const uploaded = files.map(f => checkFile(f, req.params.name, req.params.auth)); + + reloadFacesData(true); + + const responseData = { + ok: true, + filesUploaded: uploaded + }; + + console.info(`Handle image uploading completed successfuly, Data: ${responseData}`); + + res.json(responseData); + } catch(err) { + console.error(`Failed to upload file ${file}, Error: ${err}`); + + res.json({ + ok: false + }); + } + }); + } + + const createDirectory = () => { + if(!config.facesFolder) { + config.facesFolder = `${s.mainDirectory}/faces/`; + } + + config.facesFolder = s.checkCorrectPathEnding(config.facesFolder); + + if(!fs.existsSync(config.facesFolder)){ + fs.mkdirSync(config.facesFolder) + } + }; + + const onProcessReady = (d) => { + reloadFacesData(); + }; + + const initialize = () => { + if(!config.enableFaceManager) { + return; + } + + createDirectory(); + + registerGetEndpoint('s', handleGetFaces); + registerGetEndpoint('/:name/image/:image', handleGetImage); + + registerDeleteEndpoint('/:name', handleDeleteFace); + registerDeleteEndpoint('/:name/image/:image', handleDeleteImage); + + registerPostEndpoint('/:name', handleImageUpload, () => fileUpload()); + registerPostEndpoint('/:name/image/:image/move/:newName', handleMoveImage); + + s.onProcessReadyExtensions.push(onProcessReady); + }; + + initialize(); +} diff --git a/libs/webServerAdminPaths.js b/libs/webServerAdminPaths.js index 6612ea9e..8205c7a8 100644 --- a/libs/webServerAdminPaths.js +++ b/libs/webServerAdminPaths.js @@ -668,4 +668,6 @@ module.exports = function(s,config,lang,app){ } },res,req) }) + + require('./faceManager.js')(s,config,lang,app,null); } diff --git a/plugins/deepstack-face/conf.sample.json b/plugins/deepstack-face/conf.sample.json index 14338e0b..a78241fe 100644 --- a/plugins/deepstack-face/conf.sample.json +++ b/plugins/deepstack-face/conf.sample.json @@ -7,6 +7,7 @@ "key": "DeepStack-Face", "mode": "client", "type": "detector", + "fullyControlledByFaceManager": false, "deepStack": { "host": "HOSTNAME OR IP", "port": 5000, diff --git a/plugins/deepstack-face/shinobi-deepstack-face.js b/plugins/deepstack-face/shinobi-deepstack-face.js index 63da2f92..fb1608c1 100644 --- a/plugins/deepstack-face/shinobi-deepstack-face.js +++ b/plugins/deepstack-face/shinobi-deepstack-face.js @@ -1,3 +1,4 @@ +// // Shinobi - DeepStack Face Recognition Plugin // Copyright (C) 2021 Elad Bar // @@ -50,6 +51,8 @@ if(s === null) { let detectorSettings = null; +const DELIMITER = "___"; + const DETECTOR_TYPE_FACE = 'face'; const DETECTOR_TYPE_OBJECT = 'object'; @@ -60,10 +63,13 @@ const DETECTOR_CONFIGUTATION = { face: { detectEndpoint: '/vision/face/recognize', startupEndpoint: '/vision/face/list', + registerEndpoint: '/vision/face/register', + deleteEndpoint: '/vision/face/delete', key: 'userid' }, object: { detectEndpoint: '/vision/detection', + startupEndpoint: '/vision/detection', key: 'label' } } @@ -102,74 +108,200 @@ const postMessage = (data) => { const initialize = () => { const deepStackProtocol = PROTOCOLS[config.deepStack.isSSL]; - baseUrl = `${deepStackProtocol}://${config.deepStack.host}:${config.deepStack.port}/v1`; + const baseUrl = `${deepStackProtocol}://${config.deepStack.host}:${config.deepStack.port}/v1`; const detectionType = config.plug.split("-")[1].toLowerCase(); const detectorConfig = DETECTOR_CONFIGUTATION[detectionType]; const detectorConfigKeys = Object.keys(detectorConfig); - detectorSettings = { type: detectionType, active: false, baseUrl: baseUrl, - apiKey: config.deepStack.apiKey + apiKey: config.deepStack.apiKey, + fullyControlledByFaceManager: config.fullyControlledByFaceManager || false, + faces: { + shinobi: null, + server: null, + legacy: null + }, + facesPath: null, + eventMapping: { + "recompileFaceDescriptors": onRecompileFaceDescriptors + } }; - - if(detectionType === DETECTOR_TYPE_FACE) { - detectorSettings["registeredPersons"] = config.persons === undefined ? [] : config.persons; - } - + detectorConfigKeys.forEach(k => detectorSettings[k] = detectorConfig[k]); - const testRequestData = getFormData(detectorSettings.detectEndpoint); + const startupRequestData = getFormData(detectorSettings.startupEndpoint); - request.post(testRequestData, (err, res, body) => { - try { - if(err) { - throw err; - } - + request.post(startupRequestData, handleStartupResponse); +}; + +const getJobKey = (groupId, monitorId) => { + const jobKey = `${groupId}_${monitorId}`; + + return jobKey; +} + +const addJob = (groupId, monitorId) => { + const jobKey = getJobKey(groupId, monitorId); + const jobExists = detectorSettings.jobs.includes(jobKey); + + if(!jobExists) { + detectorSettings.jobs.push(jobKey); + } + + return !jobExists; +} + +const removeJob = (groupId, monitorId) => { + const jobKey = getJobKey(groupId, monitorId); + + detectorSettings.jobs = detectorSettings.jobs.filter(j => j !== jobKey); +} + +const handleStartupResponse = (err, res, body) => { + try { + if(err) { + logError(`Failed to initialize ${config.plug} plugin, Error: ${err}`); + + } else { const response = JSON.parse(body); - + if(response.error) { detectorSettings.active = !response.error.endsWith('endpoint not activated'); } else { detectorSettings.active = response.success; } - const detectorSettingsKeys = Object.keys(detectorSettings); - - const pluginMessageHeader = []; - pluginMessageHeader.push(`${config.plug} loaded`); - - const configMessage = detectorSettingsKeys.map(k => `${k}: ${detectorSettings[k]}`); - - const fullPluginMessage = pluginMessageHeader.concat(configMessage); - - const pluginMessage = fullPluginMessage.join(", "); - - logInfo(pluginMessage); + logInfo(`${config.plug} loaded, Configuration: ${JSON.stringify(detectorSettings)}`); if (detectorSettings.active) { s.detectObject = detectObject; - if(detectionType === DETECTOR_TYPE_FACE) { - const requestData = getFormData(detectorSettings.startupEndpoint); - const requestTime = getCurrentTimestamp(); - - request.post(requestData, (errStartup, resStartup, bodyStartup) => { - if (!!resStartup) { - resStartup.duration = getDuration(requestTime); - } - - onFaceListResult(errStartup, resStartup, bodyStartup); - }); + if(detectorSettings.type === DETECTOR_TYPE_FACE) { + onFaceListResult(err, res, body); } - } - } catch(ex) { - logError(`Failed to initialize ${config.plug} plugin, Error: ${ex}`) + } } - }); + + + } catch(ex) { + logError(`Failed to initialize ${config.plug} plugin, Error: ${ex}`); + } +}; + +const registerFace = (serverFileName) => { + const facesPath = detectorSettings.facesPath; + const shinobiFileParts = serverFileName.split(DELIMITER); + const faceName = shinobiFileParts[0]; + const image = shinobiFileParts[1]; + + const frameLocation = `${facesPath}${faceName}/${image}`; + + const imageStream = fs.createReadStream(frameLocation); + + const form = { + image: imageStream, + userid: serverFileName + }; + + const requestData = getFormData(detectorSettings.registerEndpoint, form); + + request.post(requestData, (err, res, body) => { + if (err) { + logError(`Failed to register face, Face: ${faceName}, Image: ${image}, Error: ${err}`); + } else { + logInfo(`Register face, Face: ${faceName}, Image: ${image}`); + } + }); +}; + +const unregisterFace = (serverFileName) => { + if (serverFileName === null) { + return; + } + + const form = { + userid: serverFileName + }; + + const requestData = getFormData(detectorSettings.deleteEndpoint, form); + + request.post(requestData, (err, res, body) => { + if (err) { + logError(`Failed to delete face, UserID: ${serverFileName}, Error: ${err}`); + } else { + logInfo(`Deleted face, UserID: ${serverFileName}`); + } + }); +}; + +const getServerFileNameByShinobi = (name, image) => { + const fileName = `${name}${DELIMITER}${image}`; + + return fileName; +} + +const compareShinobiVSServer = () => { + const allFaces = detectorSettings.faces; + const shinobiFaces = allFaces.shinobi; + const serverFaces = allFaces.server; + const compareShinobiVSServerDelayID = detectorSettings.compareShinobiVSServerDelayID || null; + + if (compareShinobiVSServerDelayID !== null) { + clearTimeout(compareShinobiVSServerDelayID) + } + + if(serverFaces === null || shinobiFaces === null) { + detectorSettings.compareShinobiVSServerDelayID = setTimeout(compareShinobiVSServer, 5000); + logWarn("AI Server not ready yet, will retry in 5 seconds"); + + return; + } + + const shinobiFaceKeys = Object.keys(shinobiFaces); + + const shinobiFiles = shinobiFaceKeys.length === 0 ? + [] : + shinobiFaceKeys + .map(faceName => { + const value = shinobiFaces[faceName].map(image => getServerFileNameByShinobi(faceName, image)); + + return value; + }) + .reduce((acc, item) => { + const result = [...acc, ...item]; + + return result; + }); + + const facesToRegister = shinobiFiles.filter(f => !serverFaces.includes(f)); + const facesToUnregister = serverFaces.filter(f => !shinobiFiles.includes(f)); + const allowUnregister = detectorSettings.fullyControlledByFaceManager || false; + + if(facesToRegister.length > 0) { + logInfo(`Registering the following faces: ${facesToRegister}`); + facesToRegister.forEach(f => registerFace(f)); + } + + if(facesToUnregister.length > 0) { + if(allowUnregister) { + logInfo(`Unregister the following faces: ${facesToUnregister}`); + + facesToUnregister.forEach(f => unregisterFace(f)); + } else { + logInfo(`Skip unregistering the following faces: ${facesToUnregister}`); + + detectorSettings.faces.legacy = facesToUnregister; + } + } + + if(facesToRegister.length > 0 || (facesToUnregister.length > 0 && allowUnregister)) { + const startupRequestData = getFormData(detectorSettings.startupEndpoint); + + request.post(startupRequestData, onFaceListResult); + } }; const processImage = (frameBuffer, d, tx, frameLocation, callback) => { @@ -197,8 +329,12 @@ const processImage = (frameBuffer, d, tx, frameLocation, callback) => { onImageProcessed(d, tx, err, res, body, frameBuffer); fs.unlinkSync(frameLocation); + + removeJob(d.ke, d.id); }); - }catch(ex){ + } catch(ex) { + removeJob(d.ke, d.id); + logError(`Failed to process image, Error: ${ex}`); if(fs.existsSync(frameLocation)) { @@ -214,27 +350,37 @@ const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { return; } + const jobCreated = addJob(d.ke, d.id); + + if(!jobCreated) { + return; + } + const dirCreationOptions = { recursive: true }; d.dir = `${s.dir.streams}${d.ke}/${d.id}/`; - const filePath = `${d.dir}${s.gid(5)}.jpg`; + const path = `${d.dir}${s.gid(5)}.jpg`; if(!fs.existsSync(d.dir)) { fs.mkdirSync(d.dir, dirCreationOptions); } - fs.writeFile(filePath, frameBuffer, function(err) { + fs.writeFile(path, frameBuffer, function(err) { if(err) { + removeJob(d.ke, d.id); + return s.systemLog(err); } try { - processImage(frameBuffer, d, tx, filePath, callback); + processImage(frameBuffer, d, tx, path, callback); } catch(ex) { + removeJob(d.ke, d.id); + logError(`Detector failed to parse frame, Error: ${ex}`); } }); @@ -257,8 +403,6 @@ const getDuration = (requestTime) => { }; const onFaceListResult = (err, res, body) => { - const duration = !!res ? res.duration : 0; - try { const response = JSON.parse(body); @@ -266,20 +410,22 @@ const onFaceListResult = (err, res, body) => { const facesArr = response.faces; const faceStr = facesArr.join(","); + detectorSettings.faces.server = facesArr; + if(success) { - logInfo(`DeepStack loaded with the following faces: ${faceStr}, Response time: ${duration} ms`); + logInfo(`DeepStack loaded with the following faces: ${faceStr}`); } else { - logWarn(`Failed to connect to DeepStack server, Error: ${err}, Response time: ${duration} ms`); + logWarn(`Failed to connect to DeepStack server, Error: ${err}`); } } catch(ex) { - logError(`Error while connecting to DeepStack server, Error: ${ex} | ${err}, Response time: ${duration} ms`); + logError(`Error while connecting to DeepStack server, Error: ${ex} | ${err}`); } }; const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { const duration = !!res ? res.duration : 0; - let objects = []; + const result = []; try { if(err) { @@ -294,11 +440,13 @@ const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { const predictions = response.predictions; if(predictions !== null && predictions.length > 0) { - objects = predictions.map(p => getDeepStackObject(p)).filter(p => !!p); + const predictionDescriptons = predictions.map(p => getPredictionDescripton(p)).filter(p => !!p); - if(objects.length > 0) { - const identified = objects.filter(p => p.tag !== FACE_UNKNOWN); - const unknownCount = objects.length - identified.length; + result.push(...predictionDescriptons); + + if(predictionDescriptons.length > 0) { + const identified = predictionDescriptons.filter(p => p.tag !== FACE_UNKNOWN); + const unknownCount = predictionDescriptons.length - identified.length; if(unknownCount > 0) { logInfo(`${d.id} detected ${unknownCount} unknown ${detectorSettings.type}s, Response time: ${duration} ms`); @@ -341,12 +489,12 @@ const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { tx(eventData); } } - } + } } catch(ex) { logError(`Error while processing image, Error: ${ex} | ${err},, Response time: ${duration} ms, Body: ${body}`); } - return objects + return result }; const getFormData = (endpoint, additionalParameters) => { @@ -371,7 +519,7 @@ const getFormData = (endpoint, additionalParameters) => { return requestData; }; -const getDeepStackObject = (prediction) => { +const getPredictionDescripton = (prediction) => { if(prediction === undefined) { return null; } @@ -396,14 +544,40 @@ const getDeepStackObject = (prediction) => { }; if (detectorSettings.type === DETECTOR_TYPE_FACE) { - const matchingPersons = detectorSettings.registeredPersons.filter(p => tag.startsWith(p)) - const person = matchingPersons.length > 0 ? matchingPersons[0] : null; + const legacyFaces = detectorSettings.faces.legacy || []; - obj["person"] = person; - } - + if (legacyFaces.includes(tag)) { + const matchingPersons = detectorSettings.registeredPersons.filter(p => tag.startsWith(p)) + obj.person = matchingPersons.length > 0 ? matchingPersons[0] : null; + + } else { + const shinobiFileParts = tag.split(DELIMITER); + obj.person = shinobiFileParts[0]; + } + } return obj; }; +const onRecompileFaceDescriptors = (d) => { + if(detectorSettings.faces.shinobi !== d.faces) { + detectorSettings.faces.shinobi = d.faces; + detectorSettings.facesPath = d.path; + compareShinobiVSServer(); + + } +}; + +s.MainEventController = (d,cn,tx) => { + const handler = detectorSettings.eventMapping[d.f]; + + if (handler !== undefined) { + try { + handler(d); + } catch (error) { + logError(`Failed to handle event ${d.f}, Error: ${error}`); + } + } +} + initialize(); diff --git a/plugins/deepstack-object/shinobi-deepstack-object.js b/plugins/deepstack-object/shinobi-deepstack-object.js index 62474268..f79a6318 100644 --- a/plugins/deepstack-object/shinobi-deepstack-object.js +++ b/plugins/deepstack-object/shinobi-deepstack-object.js @@ -1,5 +1,5 @@ // -// Shinobi - DeepStack Object Detection Plugin +// Shinobi - DeepStack Face Recognition Plugin // Copyright (C) 2021 Elad Bar // // Base Init >> @@ -113,7 +113,8 @@ const initialize = () => { type: detectionType, active: false, baseUrl: baseUrl, - apiKey: config.deepStack.apiKey + apiKey: config.deepStack.apiKey, + jobs: [] }; if(detectionType === DETECTOR_TYPE_FACE) { @@ -173,7 +174,7 @@ const initialize = () => { }); }; -const processImage = (frameBuffer, d, tx, frameLocation, callback) => { +const processImage = (imageB64, d, tx, frameLocation, callback) => { if(!detectorSettings.active) { return; } @@ -195,11 +196,15 @@ const processImage = (frameBuffer, d, tx, frameLocation, callback) => { res.duration = getDuration(requestTime); } - onImageProcessed(d, tx, err, res, body, frameBuffer); + onImageProcessed(d, tx, err, res, body, imageB64); fs.unlinkSync(frameLocation); + + removeJob(d.ke, d.id); }); }catch(ex){ + removeJob(d.ke, d.id); + logError(`Failed to process image, Error: ${ex}`); if(fs.existsSync(frameLocation)) { @@ -209,10 +214,38 @@ const processImage = (frameBuffer, d, tx, frameLocation, callback) => { callback(); }; +const getJobKey = (groupId, monitorId) => { + const jobKey = `${groupId}_${monitorId}`; + + return jobKey; +} + +const addJob = (groupId, monitorId) => { + const jobKey = getJobKey(groupId, monitorId); + const jobExists = detectorSettings.jobs.includes(jobKey); + + if(!jobExists) { + detectorSettings.jobs.push(jobKey); + } + + return !jobExists; +} + +const removeJob = (groupId, monitorId) => { + const jobKey = getJobKey(groupId, monitorId); + + detectorSettings.jobs = detectorSettings.jobs.filter(j => j !== jobKey); +} const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { if(!detectorSettings.active) { return; + } + + const jobCreated = addJob(d.ke, d.id); + + if(!jobCreated) { + return; } const dirCreationOptions = { @@ -221,21 +254,27 @@ const detectObject = (frameBuffer, d, tx, frameLocation, callback) => { d.dir = `${s.dir.streams}${d.ke}/${d.id}/`; - const filePath = `${d.dir}${s.gid(5)}.jpg`; + frameLocation = `${d.dir}${s.gid(5)}.jpg`; if(!fs.existsSync(d.dir)) { fs.mkdirSync(d.dir, dirCreationOptions); } - fs.writeFile(filePath, frameBuffer, function(err) { + fs.writeFile(frameLocation, frameBuffer, function(err) { if(err) { + removeJob(d.ke, d.id); + return s.systemLog(err); } try { - processImage(frameBuffer, d, tx, filePath, callback); + const imageB64 = frameBuffer.toString('base64'); + + processImage(imageB64, d, tx, frameLocation, callback); } catch(ex) { + removeJob(d.ke, d.id); + logError(`Detector failed to parse frame, Error: ${ex}`); } }); @@ -277,7 +316,7 @@ const onFaceListResult = (err, res, body) => { } }; -const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { +const onImageProcessed = (d, tx, err, res, body, imageStream) => { const duration = !!res ? res.duration : 0; let objects = []; @@ -297,12 +336,14 @@ const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { if(predictions !== null && predictions.length > 0) { objects = predictions.map(p => getDeepStackObject(p)).filter(p => !!p); - if(objects.length > 0) { + if(objects.length === 0) { + logInfo(`Processed image for ${detectorSettings.type} on monitor ${d.id} returned no results, Response time: ${duration} ms`); + } else { const identified = objects.filter(p => p.tag !== FACE_UNKNOWN); const unknownCount = objects.length - identified.length; if(unknownCount > 0) { - logInfo(`${d.id} detected ${unknownCount} unknown ${detectorSettings.type}s, Response time: ${duration} ms`); + logInfo(`{d.id}$ detected ${unknownCount} unknown ${detectorSettings.type}s, Response time: ${duration} ms`); } if(identified.length > 0) { @@ -334,17 +375,19 @@ const onImageProcessed = (d, tx, err, res, body, frameBuffer) => { matrices: objects, imgHeight: width, imgWidth: height, - time: duration - }, - frame: frameBuffer + time: duration, + imageStream: imageStream + } }; tx(eventData); } } + } else { + logWarn(`Processed image for ${detectorSettings.type} on monitor ${d.id} failed, Reason: ${response.error}, Response time: ${duration} ms`); } } catch(ex) { - logError(`Error while processing image, Error: ${ex} | ${err},, Response time: ${duration} ms, Body: ${body}`); + logError(`Error while processing image, Error: ${ex} | ${err}, Response time: ${duration} ms, Body: ${body}`); } return objects diff --git a/web/assets/js/super.configEditor.js b/web/assets/js/super.configEditor.js index f63bc7b1..4c13e3c2 100644 --- a/web/assets/js/super.configEditor.js +++ b/web/assets/js/super.configEditor.js @@ -1,321 +1,380 @@ -$(document).ready(function(){ - var schema = { - "title": "Main Configuration", - "type": "object", - "properties": { - "debugLog": { - "type": "boolean", - "default": false - }, - "subscriptionId": { - "type": "string", - }, - "port": { - "type": "integer", - "default": 8080 - }, - "passwordType": { - "type": "string", - "enum": [ - "sha256", - "sha512", - "md5" - ], - "default": "sha256" - }, - "addStorage": { - "type": "array", - "format": "table", - "title": "Additional Storage", - "description": "Separate storage locations that can be set for different monitors.", - "uniqueItems": true, - "items": { - "type": "object", - "title": "Storage Array", - "properties": { - "name": { - "type": "string", - }, - "path": { - "type": "string", - "default": "__DIR__/videos2" - } - } - }, - "default": [ - { - "name": "second", - "path": "__DIR__/videos2" - } - ] - }, - "plugins": { - "type": "array", - "format": "table", - "title": "Plugins", - "descripton": "Elaborate Plugin connection settings.", - "uniqueItems": true, - "items": { - "type": "object", - "title": "Plugin", - "properties": { - "plug": { - "type": "string", - "default": "pluginName" - }, - "key": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "host", - "client" - ], - "default": "client" - }, - "https": { - "type": "boolean", - "descripton": "Only for Host mode.", - "default": false - }, - "host": { - "type": "string", - "descripton": "Only for Host mode.", - "default": "localhost" - }, - "port": { - "type": "integer", - "descripton": "Only for Host mode.", - "default": 8082 - }, - "type": { - "type": "string", - "default": "detector" - } - } - }, - "default": [ - { - "name": "second", - "path": "__DIR__/videos2" - } - ] - }, - "pluginKeys": { +$(document).ready(function () { + const schema = { + "title": "Main Configuration", + "type": "object", + "properties": { + "debugLog": { + "type": "boolean", + "default": false + }, + "subscriptionId": { + "type": "string" + }, + "port": { + "type": "integer", + "default": 8080 + }, + "passwordType": { + "type": "string", + "enum": [ + "sha256", + "sha512", + "md5" + ], + "default": "sha256" + }, + "addStorage": { + "type": "array", + "format": "table", + "title": "Additional Storage", + "description": "Separate storage locations that can be set for different monitors.", + "uniqueItems": true, + "items": { "type": "object", - "format": "table", - "title": "Plugin Keys", - "description": "Quick client connection setup for plugins. Just add the plugin key to make it ready for incoming connections.", - "uniqueItems": true, - "items": { - "type": "object", - "title": "Plugin Key", - "properties": {} + "title": "Storage Array", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string", + "default": "__DIR__/videos2" + } } }, - "db": { - "type": "object", - "format": "table", - "title": "Database Options", - "description": "Credentials to connect to where detailed information is stored.", - "properties": { - "host": { - "type": "string", - "default": "127.0.0.1" - }, - "user": { - "type": "string", - "default": "majesticflame" - }, - "password": { - "type": "string", - "default": "" - }, - "database": { - "type": "string", - "default": "ccio" - }, - "port": { - "type": "integer", - "default": 3306 - } + "default": [ + { + "name": "second", + "path": "__DIR__/videos2" + } + ] + }, + "plugins": { + "type": "array", + "format": "table", + "title": "Plugins", + "descripton": "Elaborate Plugin connection settings.", + "uniqueItems": true, + "items": { + "type": "object", + "title": "Plugin", + "properties": { + "plug": { + "type": "string", + "default": "pluginName" }, - "default": { - "host": "127.0.0.1", - "user": "majesticflame", - "password": "", - "database": "ccio", - "port":3306 + "key": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "host", + "client" + ], + "default": "client" + }, + "https": { + "type": "boolean", + "descripton": "Only for Host mode.", + "default": false + }, + "host": { + "type": "string", + "descripton": "Only for Host mode.", + "default": "localhost" + }, + "port": { + "type": "integer", + "descripton": "Only for Host mode.", + "default": 8082 + }, + "type": { + "type": "string", + "default": "detector" } + } }, - "cron": { - "type": "object", - "format": "table", - "title": "CRON Options", - "properties": { - "key": { - "type": "string", - }, - "deleteOld": { - "type": "boolean", - "description": "cron will delete videos older than Max Number of Days per account.", - "default": true - }, - "deleteNoVideo": { - "type": "boolean", - "description": "cron will delete SQL rows that it thinks have no video files.", - "default": true - }, - "deleteOverMax": { - "type": "boolean", - "description": "cron will delete files that are over the set maximum storage per account.", - "default": true - }, - } + "default": null + }, + "pluginKeys": { + "type": "object", + "format": "table", + "title": "Plugin Keys", + "description": "Quick client connection setup for plugins. Just add the plugin key to make it ready for incoming connections.", + "uniqueItems": true, + "items": { + "type": "object", + "title": "Plugin Key", + "properties": {} + } + }, + "enableFaceManager": { + "type": "boolean", + "title": "Enable Face Manager", + "description": "Super dashboard and API to register and unregister faces with images for Face Recognition plugins", + "default": false + }, + "db": { + "type": "object", + "format": "table", + "title": "Database Options", + "description": "Credentials to connect to where detailed information is stored.", + "properties": { + "host": { + "type": "string", + "default": "127.0.0.1" + }, + "user": { + "type": "string", + "default": "majesticflame" + }, + "password": { + "type": "string", + "default": "" + }, + "database": { + "type": "string", + "default": "ccio" + }, + "port": { + "type": "integer", + "default": 3306 + } }, - "mail": { + "default": { + "host": "127.0.0.1", + "user": "majesticflame", + "password": "", + "database": "ccio", + "port": 3306 + } + }, + "cron": { + "type": "object", + "format": "table", + "title": "CRON Options", + "properties": { + "key": { + "type": "string" + }, + "deleteOld": { + "type": "boolean", + "description": "cron will delete videos older than Max Number of Days per account.", + "default": true + }, + "deleteNoVideo": { + "type": "boolean", + "description": "cron will delete SQL rows that it thinks have no video files.", + "default": true + }, + "deleteOverMax": { + "type": "boolean", + "description": "cron will delete files that are over the set maximum storage per account.", + "default": true + } + } + }, + "mail": { + "type": "object", + "format": "table", + "title": "Email Options", + "properties": { + "service": { + "type": "string" + }, + "host": { + "type": "string" + }, + "auth": { "type": "object", - "format": "table", - "title": "Email Options", "properties": { - "service": { - "type": "string", + "user": { + "type": "string" }, - "host": { - "type": "string", - }, - "auth": { - "type": "object", - "properties": { - "user": { - "type": "string", - }, - "pass": { - "type": "string", - }, - }, - }, - "secure": { - "type": "boolean", - "default": false - }, - "ignoreTLS": { - "type": "boolean", - }, - "requireTLS": { - "type": "boolean", - }, - "port": { - "type": "integer", + "pass": { + "type": "string" } } - }, - "detectorMergePamRegionTriggers": { - "type": "boolean", - "default": true - }, - "doSnapshot": { - "type": "boolean", - "default": true - }, - "discordBot": { - "type": "boolean", - "default": false - }, - "dropInEventServer": { - "type": "boolean", - "default": false - }, - "ftpServer": { - "type": "boolean", - "default": false - }, - "oldPowerVideo": { - "type": "boolean", - "default": false - }, - "wallClockTimestampAsDefault": { - "type": "boolean", - "default": true - }, - "defaultMjpeg": { - "type": "string", - }, - "streamDir": { - "type": "string", - }, - "videosDir": { - "type": "string", - }, - "windowsTempDir": { - "type": "string", + }, + "secure": { + "type": "boolean", + "default": false, + "format": "checkbox" + }, + "ignoreTLS": { + "type": "boolean" + }, + "requireTLS": { + "type": "boolean" + }, + "port": { + "type": "integer" + } } + }, + "detectorMergePamRegionTriggers": { + "type": "boolean", + "default": true + }, + "doSnapshot": { + "type": "boolean", + "default": true + }, + "discordBot": { + "type": "boolean", + "default": false + }, + "dropInEventServer": { + "type": "boolean", + "default": false + }, + "ftpServer": { + "type": "boolean", + "default": false + }, + "oldPowerVideo": { + "type": "boolean", + "default": false + }, + "wallClockTimestampAsDefault": { + "type": "boolean", + "default": true + }, + "defaultMjpeg": { + "type": "string" + }, + "streamDir": { + "type": "string" + }, + "videosDir": { + "type": "string" + }, + "windowsTempDir": { + "type": "string" } - }; + } + }; - var configurationTab = $('#config') - var configurationForm = configurationTab.find('form') + const configurationTab = $("#config"); + const configurationForm = configurationTab.find("form"); - // Set default options - JSONEditor.defaults.options.theme = 'bootstrap3'; - JSONEditor.defaults.options.iconlib = 'fontawesome4'; + const moduleData = { + endpoint: null, + configurationEditor: null + } - // Initialize the editor - var configurationEditor = new JSONEditor(document.getElementById("configForHumans"),{ - theme: 'bootstrap3', - schema: schema + const handleGetConfigurationData = data => { + const dataConfig = data.config; + const dataConfigKeys = Object.keys(dataConfig); + const schemaItemsKeys = Object.keys(schema.properties); + + const schemaWithoutData = schemaItemsKeys.filter( + (sk) => !dataConfigKeys.includes(sk) + ); + const dataWithoutSchema = dataConfigKeys.filter( + (dk) => !schemaItemsKeys.includes(dk) + ); + + schemaWithoutData.forEach((sk) => { + const schemaItem = schema.properties[sk]; + const defaultConfig = schemaItem.default; + + data.config[sk] = defaultConfig; }); - function loadConfiguationIntoEditor(){ - $.get(superApiPrefix + $user.sessionKey + '/system/configure',function(data){ - configurationEditor.setValue(data.config); - }) + if (dataWithoutSchema.length > 0) { + dataWithoutSchema.forEach((dk) => { + const schemaItem = { + title: dk, + options: { + hidden: true, + }, + }; + + schema.properties[dk] = schemaItem; + }); + + // Set default options + JSONEditor.defaults.options.theme = "bootstrap3"; + JSONEditor.defaults.options.iconlib = "fontawesome4"; } - // configurationEditor.on("change", function() { - // // Do something... - // }); - var submitConfiguration = function(){ - var errors = configurationEditor.validate(); - console.log(errors.length) - console.log(errors) - if(errors.length === 0) { - var newConfiguration = JSON.stringify(configurationEditor.getValue(),null,3) - var html = '

This is a change being applied to the configuration file (conf.json). Are you sure you want to do this? You must restart Shinobi for these changes to take effect. The JSON below is what you are about to save.

' - html += `
${newConfiguration}
` - $.confirm.create({ - title: 'Save Configuration', - body: html, - clickOptions: { - class: 'btn-success', - title: lang.Save, - }, - clickCallback: function(){ - $.post(superApiPrefix + $user.sessionKey + '/system/configure',{ - data: newConfiguration - },function(data){ - // console.log(data) - }) - } - }) - }else{ - new PNotify({text:'Invalid JSON Syntax, Cannot Save.',type:'error'}) - } + + const configurationEditor = new JSONEditor( + document.getElementById("configForHumans"), { + schema: schema, + } + ); + + configurationEditor.setValue(data.config); + + moduleData.configurationEditor = configurationEditor; + window.configurationEditor = configurationEditor; + }; + + const handlePostConfigurationData = data => { + // console.log(data); + } + + function loadConfiguationIntoEditor(d) { + moduleData.endpoint = `${superApiPrefix}${$user.sessionKey}/system/configure`; + + $.get(moduleData.endpoint, handleGetConfigurationData); + } + + // configurationEditor.on("change", function() { + // // Do something... + // }); + const submitConfiguration = function () { + const configurationEditor = moduleData.configurationEditor; + const errors = configurationEditor.validate(); + + if (errors.length === 0) { + const newConfiguration = JSON.stringify(configurationEditor.getValue(), null, 3); + + const html = `

+ This is a change being applied to the configuration file (conf.json). + Are you sure you want to do this? You must restart Shinobi for these changes to take effect. + + The JSON below is what you are about to save. +

+
+                      ${newConfiguration}
+                    
`; + + $.confirm.create({ + title: "Save Configuration", + body: html, + clickOptions: { + class: "btn-success", + title: lang.Save, + }, + clickCallback: () => { + const requestData = { + data: newConfiguration + }; + + $.post(moduleData.endpoint, requestData, handlePostConfigurationData); + }, + }); + } else { + new PNotify({ text: "Invalid JSON Syntax, Cannot Save.", type: "error" }); } - configurationTab.find('.submit').click(function(){ - submitConfiguration() - }) - configurationForm.submit(function(e){ - e.preventDefault() - submitConfiguration() - return false; - }) - $.ccio.ws.on('f',function(d){ - switch(d.f){ - case'init_success': - loadConfiguationIntoEditor() - break; - } - }) - window.configurationEditor = configurationEditor -}) + }; + + configurationTab.find(".submit").click(function () { + submitConfiguration(); + }); + + configurationForm.submit(function (e) { + e.preventDefault(); + submitConfiguration(); + return false; + }); + + $.ccio.ws.on("f", d => { + if (d.f === "init_success") { + loadConfiguationIntoEditor(); + } + }); +}); diff --git a/web/assets/js/super.faceManager.js b/web/assets/js/super.faceManager.js new file mode 100644 index 00000000..a06484f9 --- /dev/null +++ b/web/assets/js/super.faceManager.js @@ -0,0 +1,471 @@ +$(document).ready(() => { + //const faceManagerModal = $("#faceManager"); + const faceImageList = $("#faceImageList"); + const faceNvigationPanel = $("#faceNvigationPanel"); + + const faceManagerForm = $("#faceManagerUploadForm"); + const faceNameField = $("#faceNameField"); + const fileinput = $("#fileinput"); + const faceManagerUploadStatus = $("#faceManagerUploadStatus"); + + const getUrl = (endpoint) => { + const url = `${moduleData.baseUrl}${endpoint}`; + + return url; + }; + + const getFaceImages = () => { + const url = getUrl("s"); + + $.get(url, response => { + moduleData.faces = response.faces || {}; + + onFaceImagesReterived(); + }); + }; + + const sendAJAXRequest = (requestData) => { + try { + requestData.success = result => { + console.info(`Succesfully sent ${requestData.type} request to ${requestData.url}, Response: ${JSON.stringify(result)}`); + }; + + requestData.error = result => { + console.error(`Failed to ${requestData.type} request to ${requestData.url}, Response: ${JSON.stringify(result)}`); + }; + + $.ajax(requestData); + } catch (error) { + console.error(`Failed to ${requestData.type} request to ${requestData.url}, Error: ${error}`); + } + }; + + const deleteFaceImage = (name, image) => { + try { + const url = getUrl(`/${name}/image/${image}`); + + const requestData = { + url: url, + type: "DELETE" + }; + + sendAJAXRequest(requestData); + } catch (error) { + console.error(`Failed to delete image ${image} of face ${face}, Error: ${error}`); + } + }; + + const deleteFaceFolder = (name) => { + try { + const url = getUrl(`/${name}`); + + const requestData = { + url: url, + type: "DELETE" + }; + + sendAJAXRequest(requestData); + } catch (error) { + console.error(`Failed to delete face ${face}, Error: ${error}`); + } + }; + + const moveFaceImage = (name, image, newFaceName) => { + try { + const url = getUrl(`/${name}/image/${image}/move/${newFaceName}`); + + const requestData = { + url: url, + type: "POST", + data: new FormData(faceManagerForm[0]), + cache: false, + contentType: false, + processData: false, + }; + + sendAJAXRequest(requestData); + } catch (error) { + console.error(`Failed to move image ${image} of face ${face} to ${newFaceName}, Error: ${error}`); + } + }; + + const onFormSubmitted = () => { + try { + const name = faceNameField.val(); + const url = getUrl(`/${name}`); + + const requestData = { + url: url, + type: "POST", + data: new FormData(faceManagerForm[0]), + cache: false, + contentType: false, + processData: false + }; + + faceManagerUploadStatus.show(); + + sendAJAXRequest(requestData); + + faceNameField.val(""); + fileinput.val(""); + } catch (error) { + console.error(`Failed to handle form submit event, Error: ${error}`); + } + }; + + const getFaceNavigationItem = (name) => { + const images = moduleData.faces[name]; + const imageCount = images.length; + + const navigationItem = ``; + + return navigationItem; + }; + + const getFaceDelete = (name) => { + const navigationItem = `
+ ${lang.deleteFace} +
`; + + return navigationItem; + }; + + const getImageListItem = (name, image) => { + const imageUrl = getUrl(`/${name}/image/${image}`); + const imageItem = `
+ +
`; + + return imageItem; + }; + + const onSelectedFaceChanged = () => { + try { + $(`#faceNvigationPanel li[face="${moduleData.selectedFace}"] a`).tab('show'); + + faceImageList.empty(); + const images = moduleData.faces[moduleData.selectedFace]; + const items = images.map(image => getImageListItem(moduleData.selectedFace, image)); + + const deleteFace = getFaceDelete(moduleData.selectedFace); + items.push(deleteFace); + + const html = items.join(""); + faceImageList.html(html); + + activateDraggableImages(); + prettySizeFaceImages(); + } catch (error) { + console.error(`Failed to handle face [${moduleData.selectedFace}] selection changed event, Error: ${error}`); + } + }; + + const onFaceImagesReterived = () => { + try { + faceNvigationPanel.empty(); + faceImageList.empty(); + faceManagerUploadStatus.hide(); + + const faceKeys = Object.keys(moduleData.faces); + + if(faceKeys.length > 0) { + if(moduleData.selectedFace === null) { + moduleData.selectedFace = faceKeys[0]; + } + + const items = faceKeys.map(name => getFaceNavigationItem(name)); + + const html = items.join(""); + + faceNvigationPanel.html(html); + + faceKeys.forEach(name => activateDroppableContainer(name)); + + onSelectedFaceChanged(); + } + + } catch (error) { + console.error(`Failed to handle images reterived event, Error: ${error}`); + } + }; + + const drawFaceImages = (selectedFace = null) => { + try { + moduleData.selectedFace = selectedFace; + + onFormDataChanged(); + + getFaceImages(); + } catch (error) { + console.error(`Failed to draw face images, Error: ${error}`); + } + }; + + const prettySizeFaceImages = () => { + try { + const faceImagesRendered = faceImageList.find(".face-image"); + const faceHeight = faceImagesRendered.first().width(); + faceImagesRendered.css("height", faceHeight); + } catch (error) { + console.error(`Failed to resize images, Error: ${error}`); + } + }; + + const activateDroppableContainer = (name) => { + const navigationItem = faceNvigationPanel.find(`.nav-item[face="${name}"]`); + + try { + navigationItem.droppable("destroy"); + } catch (err) {} + + try { + navigationItem.droppable({ + tolerance: "intersect", + accept: ".face-image", + activeClass: "ui-state-default", + hoverClass: "ui-state-hover", + drop: (eventData, ui) => { + const newFace = getEventItemAttribute(eventData, "face", "target"); + const oldFace = getEventItemAttribute(eventData.originalEvent, "face", "target"); + const fileName = getEventItemAttribute(eventData.originalEvent, "image", "target"); + const faceImageElement = $(ui.draggable); + + if (oldFace !== newFace) { + moveFaceImage(oldFace, fileName, newFace); + } else { + faceImageElement.css({ + top: 0, + left: 0, + }); + } + }, + }); + } catch (error) { + console.error(`Failed to activate draggable images, Face: ${name}, Error: ${error}`); + } + }; + + const activateDraggableImages = (name) => { + const imageEls = faceImageList.find(`.face-image`); + try { + imageEls.draggable("destroy"); + } catch (err) {} + + try { + imageEls.draggable({ + appendTo: "body", + cursor: "move", + revert: "invalid", + }); + } catch (error) { + console.error(`Failed to activate draggable images, Face: ${name}, Error: ${error}`); + } + }; + + $(window).resize(() => { + try { + prettySizeFaceImages(); + } catch (error) { + console.error(`Failed to resize event, Error: ${error}`); + } + + }); + + const getEventItemAttribute = (eventData, key, targetKey = null) => { + if(targetKey === null) { + targetKey = "currentTarget"; + } + + window.eventItems = { + eventData: eventData, + targetKey: targetKey, + key: key, + targetElement: eventData[targetKey] + }; + + const targetElement = eventData[targetKey]; + + const value = targetElement.attributes[key].value; + + return value; + }; + + const getFixedText = (text, replacements = {}) => { + const replacementKeys = Object.keys(replacements); + replacementKeys.forEach(r => { + text = text.replace(`{${r}}`, `${replacements[r]}`); + }); + + return text; + }; + + const onDeleteImageRequest = (e) => { + try { + e.preventDefault(); + const faceName = getEventItemAttribute(e, "face"); + const faceImage = getEventItemAttribute(e, "image"); + + const imageUrl = getUrl(`/${faceName}/image/${faceImage}`); + const textReplacements = { + "face": faceName, + "image": faceImage + }; + + const text = getFixedText(lang.deleteImageText, textReplacements); + + const data = { + title: lang.deleteImage, + body: `${text}
`, + clickOptions: { + class: "btn-danger", + title: lang.Delete, + }, + clickCallback: () => deleteFaceImage(faceName, faceImage) + }; + + $.confirm.create(data); + } catch (error) { + console.error(`Failed to handle delete image request event, Error: ${error}`); + } + + return false; + }; + + const onDeleteFaceRequest = (e) => { + try { + e.preventDefault(); + + const faceName = getEventItemAttribute(e, "face"); + + const textReplacements = { + "face": faceName + }; + + const text = getFixedText(lang.deleteFaceText, textReplacements); + + const data = { + title: lang.deleteFace, + body: text, + clickOptions: { + class: "btn-danger", + title: lang.Delete, + }, + clickCallback: () => deleteFaceFolder(faceName) + }; + + $.confirm.create(data); + } catch (error) { + console.error(`Failed to handle delete face request event, Error: ${error}`); + } + + return false; + }; + + const onFormDataChanged = () => { + try { + const name = faceNameField.val(); + const image = fileinput.val(); + + const enableSubmit = name !== undefined && name !== null && name.length > 0 && image !== undefined && image !== null && image.length > 0; + + $("#submitUpload").prop('disabled', !enableSubmit); + } catch (error) { + console.error(`Failed to handle image browsing, Error: ${error}`); + } + }; + + const onInitSuccess = (d) => { + try { + moduleData.baseUrl = `${superApiPrefix}${d.superSessionKey}/face`; + + drawFaceImages(); + } catch (error) { + console.error(`Failed to handle init success event, Error: ${error}`); + } + }; + + const onRecompileFaceDescriptors = (d) => { + moduleData.faces = d.faces; + + onFaceImagesReterived(); + }; + + const onFaceFolderDeleted = (d) => { + drawFaceImages(); + }; + + const onFaceImageDeleted = (d) => { + drawFaceImages(moduleData.selectedFace); + }; + + const onFaceImageUploaded = (d) => { + drawFaceImages(d.faceName); + }; + + const moduleData = { + baseUrl: null, + faces: {}, + selectedFace: null, + eventMapper: { + "init_success": onInitSuccess, + "recompileFaceDescriptors": onRecompileFaceDescriptors, + "faceFolderDeleted": onFaceFolderDeleted, + "faceImageDeleted": onFaceImageDeleted, + "faceImageUploaded": onFaceImageUploaded + } + }; + + $.ccio.ws.on("f", (d) => { + const handler = moduleData.eventMapper[d.f]; + + if (handler === undefined) { + console.info(`No handler found, Data: ${JSON.stringify(d)}`); + } else { + try { + setTimeout(() => { + handler(d); + }, 100); + } catch (error) { + console.error(`Failed to handle event ${d.f}, Error: ${error}`); + } + } + }); + + // Upload image + faceManagerForm.submit(() => { + onFormSubmitted(); + + return false; + }); + + $("div").on("click", ".deleteFolder", e => { + onDeleteFaceRequest(e); + }); + + faceImageList.on("click", ".deleteImage", e => { + onDeleteImageRequest(e); + }); + + faceNameField.change(() => { + onFormDataChanged(); + }); + + fileinput.change(() => { + onFormDataChanged(); + }); + + faceNvigationPanel.on('click', ".faceNavigation", e => { + const faceName = getEventItemAttribute(e, "face"); + + if(faceName !== moduleData.selectedFace) { + moduleData.selectedFace = faceName; + + onSelectedFaceChanged(); + } + + + }); +}); diff --git a/web/pages/blocks/superFaceManager.ejs b/web/pages/blocks/superFaceManager.ejs new file mode 100644 index 00000000..edb2a7b0 --- /dev/null +++ b/web/pages/blocks/superFaceManager.ejs @@ -0,0 +1,30 @@ +
+
+
+
+ <%- lang['Face Name'] %> +
+
+ +
+
+ +
+
+ +
+
+ <%- lang['Images Sent'] %> +
+
+
+
+ + +
+
+
+
+
+ + diff --git a/web/pages/super.ejs b/web/pages/super.ejs index 75d1201e..6ff9051f 100644 --- a/web/pages/super.ejs +++ b/web/pages/super.ejs @@ -90,6 +90,11 @@ + <% if(config.enableFaceManager) { %> + + <% } %>
@@ -126,6 +131,11 @@
<% include blocks/superPluginManager.ejs %>
+ <% if(config.enableFaceManager) { %> +
+ <% include blocks/superFaceManager.ejs %> +
+ <% } %>