Merge branch 'generic-face-manager' into 'dev'

Face manager as generic solution within super dashboard

See merge request Shinobi-Systems/Shinobi!421
face-manager-integrated
Moe 2022-12-13 19:25:55 +00:00
commit b4ed7ff086
11 changed files with 1513 additions and 380 deletions

View File

@ -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."
}

View File

@ -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"}

332
libs/faceManager.js Normal file
View File

@ -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();
}

View File

@ -668,4 +668,6 @@ module.exports = function(s,config,lang,app){
}
},res,req)
})
require('./faceManager.js')(s,config,lang,app,null);
}

View File

@ -7,6 +7,7 @@
"key": "DeepStack-Face",
"mode": "client",
"type": "detector",
"fullyControlledByFaceManager": false,
"deepStack": {
"host": "HOSTNAME OR IP",
"port": 5000,

View File

@ -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();

View File

@ -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

View File

@ -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 = '<p>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. <b>The JSON below is what you are about to save.</b></p>'
html += `<pre>${newConfiguration}</pre>`
$.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 = `<p>
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.
<b>The JSON below is what you are about to save.</b>
</p>
<pre>
${newConfiguration}
</pre>`;
$.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();
}
});
});

View File

@ -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 = `<li class="nav-item" face="${name}" style="display: flex; flex-direction: row; align-items: center;">
<a class="nav-link faceNavigation" data-toggle="tab" href="#" face="${name}" style="margin-left: 5px; margin-right: 5px;">${name} <span class="badge badge-dark badge-pill" style="margin-left: 5px; margin-right: 5px;">${imageCount}</span></a>
</li>`;
return navigationItem;
};
const getFaceDelete = (name) => {
const navigationItem = `<div style="padding: 5px;">
<a class="btn btn-sm btn-danger m-0 deleteFolder" style="margin-left: 5px; margin-right: 5px;" face="${name}"><i class="fa fa-trash"></i> ${lang.deleteFace}</a>
</div>`;
return navigationItem;
};
const getImageListItem = (name, image) => {
const imageUrl = getUrl(`/${name}/image/${image}`);
const imageItem = `<div class="col-md-2 face-image" style="background-image:url('${imageUrl}'); background-size: contain; background-repeat: no-repeat; width: 150px; height: 150px; padding: 2px;" face="${name}" image="${image}">
<a class="btn btn-sm btn-danger m-0 deleteImage" face="${name}" image="${image}"><i class="fa fa-trash"></i></a>
</div>`;
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}}`, `<b style="color: red">${replacements[r]}</b>`);
});
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}<div class="mt-3"><img style="width:100%;border-radius:5px;" src="${imageUrl}"></div>`,
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();
}
});
});

View File

@ -0,0 +1,30 @@
<div class="row">
<form class="form-group-group card bg-dark red mt-1" id="faceManagerUploadForm">
<div class="card-body">
<div class="form-group">
<span class="badge badge-primary"><%- lang['Face Name'] %></span>
</div>
<div class="form-group">
<input placeholder="<%- lang['Face Name'] %>" class="form-control form-control-lg" name="faceName" id="faceNameField" style="border-radius: .3rem;" />
</div>
<div class="form-group">
<input type="file" id="fileinput" name="files" required multiple="multiple" accept="image/jpeg, image/png" />
</div>
<div>
<button type="submit" class="btn btn-round btn-primary mb-0" id="submitUpload"><i class="fa fa-upload"></i> <%- lang['Click to Upload Images'] %> (JPEG / PNG)</button>
</div>
<div class="success" id="faceManagerUploadStatus">
<span class="badge bg-light badge-success"><%- lang['Images Sent'] %></span>
</div>
</div>
</form>
<div class="card-body">
<ul class="nav nav-pills" id="faceNvigationPanel"></ul>
<div style="padding-top: 20px;">
<div class="row" id="faceImageList"></ul>
</div>
</div>
</div>
<script src="<%-window.libURL%>assets/js/super.faceManager.js" type="text/javascript"></script>

View File

@ -90,6 +90,11 @@
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#superPluginManager" role="tab"><%-lang['Plugin Manager']%></a>
</li>
<% if(config.enableFaceManager) { %>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#superFaceManager" role="tab"><%-lang['Face Manager']%></a>
</li>
<% } %>
</ul>
<div class="card-body text-white" style="background:#263343">
<!-- Tab panes -->
@ -126,6 +131,11 @@
<div class="tab-pane text-left" id="superPluginManager" role="tabpanel">
<% include blocks/superPluginManager.ejs %>
</div>
<% if(config.enableFaceManager) { %>
<div class="tab-pane text-left" id="superFaceManager" role="tabpanel">
<% include blocks/superFaceManager.ejs %>
</div>
<% } %>
</div>
</div>
</div>