Merge branch 'generic-face-manager' into 'dev'
Face manager as generic solution within super dashboard See merge request Shinobi-Systems/Shinobi!421face-manager-integrated
commit
b4ed7ff086
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -668,4 +668,6 @@ module.exports = function(s,config,lang,app){
|
|||
}
|
||||
},res,req)
|
||||
})
|
||||
|
||||
require('./faceManager.js')(s,config,lang,app,null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"key": "DeepStack-Face",
|
||||
"mode": "client",
|
||||
"type": "detector",
|
||||
"fullyControlledByFaceManager": false,
|
||||
"deepStack": {
|
||||
"host": "HOSTNAME OR IP",
|
||||
"port": 5000,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue