diff --git a/.gitignore b/.gitignore
index cc19cb61..c79bb8e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,3 @@
-## Ignore Visual Studio temporary files, build results, and
-## files generated by popular Visual Studio add-ons.
-##
-## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
-
# User-specific files
*.rsuser
*.suo
@@ -33,7 +28,7 @@ bld/
[Ll]og/
[Ll]ogs/
-# Visual Studio 2015/2017 cache/options directory
+# Visual Studio cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
@@ -121,12 +116,6 @@ ipch/
# Visual Studio Trace Files
*.e2e
-# TFS 2012 Local Workspace
-$tf/
-
-# Guidance Automation Toolkit
-*.gpState
-
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
@@ -246,9 +235,6 @@ orleans.codegen.cs
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
-# RIA/Silverlight projects
-Generated_Code/
-
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
@@ -259,11 +245,6 @@ UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
-# SQL Server files
-*.mdf
-*.ldf
-*.ndf
-
# Business Intelligence projects
*.rdl.data
*.bim.layout
@@ -286,27 +267,6 @@ node_modules/
# Visual Studio 6 build log
*.plg
-# Visual Studio 6 workspace options file
-*.opt
-
-# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
-*.vbw
-
-# Visual Studio LightSwitch build output
-**/*.HTMLClient/GeneratedArtifacts
-**/*.DesktopClient/GeneratedArtifacts
-**/*.DesktopClient/ModelManifest.xml
-**/*.Server/GeneratedArtifacts
-**/*.Server/ModelManifest.xml
-_Pvt_Extensions
-
-# Paket dependency manager
-.paket/paket.exe
-paket-files/
-
-# FAKE - F# Make
-.fake/
-
# CodeRush personal settings
.cr/personal
@@ -314,25 +274,9 @@ paket-files/
__pycache__/
*.pyc
-# Cake - Uncomment if you are using it
-# tools/**
-# !tools/packages.config
-
-# Tabs Studio
-*.tss
-
# Telerik's JustMock configuration file
*.jmconfig
-# BizTalk build output
-*.btp.cs
-*.btm.cs
-*.odx.cs
-*.xsd.cs
-
-# OpenCover UI analysis results
-OpenCover/
-
# Azure Stream Analytics local run output
ASALocalRun/
@@ -342,24 +286,12 @@ ASALocalRun/
# NVidia Nsight GPU debugger configuration file
*.nvuser
-# MFractors (Xamarin productivity tool) working folder
-.mfractor/
-
# Local History for Visual Studio
.localhistory/
-# BeatPulse healthcheck temp database
-healthchecksdb
-
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
-# Ionide (cross platform F# VS Code tools) working folder
-.ionide/
-
-# Fody - auto-generated XML schema
-FodyWeavers.xsd
-
# Heat Generated Wix files.
**/*Files.wxs
@@ -382,33 +314,70 @@ FodyWeavers.xsd
/Installers/Windows/PortraitFilter.Installer/PortraitFilterInstallFiles.wxs
/Installers/Windows/CodeProjectAI.Server.Installer/DemoImangesFiles.wxs
/Installers/Windows/Python39.Installer/Python39Files.wxs
+/Installers/Windows/YoloNet.Installer/YoloNetInstallFiles.wxs
/Installers/zlib123dllx64.zip
/Installers/cudnn-windows-x86_64-8.5.0.96_cuda11-archive.zip
/src/API/Server/FrontEnd/installconfig.json
+/src/API/Server/FrontEnd/modules.json
-/src/AnalysisLayer/FaceProcessing/assets
-/src/AnalysisLayer/FaceProcessing/datastore/
-/src/AnalysisLayer/ObjectDetectionNet/assets/
-/src/AnalysisLayer/ObjectDetectionNet/custom-models
-/src/AnalysisLayer/ObjectDetectionYolo/assets
-/src/AnalysisLayer/ObjectDetectionYolo/custom-models
-
-/src/downloads/
-/src/module-downloads/
-/src/modules/downloads/
+# keep the folder structure but don't git commit the cached downloads
+!/src/downloads
+/src/downloads/*
+!/src/downloads/modules
+/src/downloads/modules/*
+!/src/downloads/modules/readme.txt
+# Downloaded assets for modules
/src/modules/ALPR/paddleocr
/src/modules/ALPR/plate.png
/src/modules/BackgroundRemover/models
+/src/modules/Cartooniser/weights
+/src/modules/FaceProcessing/assets
+/src/modules/FaceProcessing/datastore/
+/src/modules/ObjectDetectionCoral/assets
+src/modules/ObjectDetectionCoral/edgetpu_runtime
+src/modules/ObjectDetectionCoral/libedgetpu.*
+/src/modules/ObjectDetectionNet/assets/
+/src/modules/ObjectDetectionNet/custom-models
+/src/modules/ObjectDetectionNet/LocalNugets
+src/modules/ObjectDetectionYoloRKNN/assets
+src/modules/ObjectDetectionYoloRKNN/custom-models
+/src/modules/ObjectDetectionTFLite/assets
+/src/modules/ObjectDetectionYolo/assets
+/src/modules/ObjectDetectionYolo/custom-models
/src/modules/OCR/paddleocr
/src/modules/PortraitFilter/runtimeconfig.template.Designer.cs
/src/modules/SceneClassifier/assets
+/src/modules/TrainingYoloV5/assets
+/src/modules/TrainingYoloV5/datasets
+/src/modules/TrainingYoloV5/fiftyone
+/src/modules/TrainingYoloV5/training
+/src/modules/TrainingYoloV5/train
+/src/modules/TrainingYoloV5/zoo
/src/modules/YOLOv5-3.1/custom-models
/src/modules/YOLOv5-3.1/assets
/src/modules/YOLOv5-3.1/windows_packages_cpu
/src/modules/YOLOv5-3.1/windows_packages_gpu
-/src/AnalysisLayer/BackgroundRemover/models/u2net.onnx
-/src/AnalysisLayer/SceneClassifier/assets/categories_places365.txt
-/src/AnalysisLayer/SceneClassifier/assets/scene.pt
+
+# Generated module packages
+/src/modules/ALPR/ALPR-[0-9].[0-9].zip
+/src/modules/BackgroundRemover/BackgroundRemover-[0-9].[0-9].zip
+/src/modules/Cartooniser/Cartooniser-[0-9].[0-9].zip
+/src/modules/FaceProcessing/FaceProcessing-[0-9].[0-9].zip
+/src/modules/ObjectDetectionNet/ObjectDetectionNet-CPU-[0-9].[0-9].zip
+/src/modules/ObjectDetectionNet/ObjectDetectionNet-OpenVINO-[0-9].[0-9].zip
+/src/modules/ObjectDetectionNet/ObjectDetectionNet-DirectML-[0-9].[0-9].zip
+/src/modules/ObjectDetectionNet/ObjectDetectionNet-CUDA-[0-9].[0-9].zip
+/src/modules/ObjectDetectionNet/ObjectDetectionNet-CPU-[0-9].[0-9].zip
+/src/modules/ObjectDetectionTFLite/ObjectDetectionTFLite-1.0.zip
+/src/modules/ObjectDetectionYolo/ObjectDetectionYolo-[0-9].[0-9].zip
+/src/modules/OCR/OCR-[0-9].[0-9].zip
+/src/modules/PortraitFilter/PortraitFilter-[0-9].[0-9].zip
+/src/modules/SceneClassifier/SceneClassifier-[0-9].[0-9].zip
+/src/modules/SentimentAnalysis/SentimentAnalysis-[0-9].[0-9].zip
+/src/modules/SuperResolution/SuperResolution-[0-9].[0-9].zip
+/src/modules/TextSummary/TextSummary-[0-9].[0-9].zip
+/src/modules/YOLOv5-3.1/YOLOv5-3.1-[0-9].[0-9].zip
+/src/modules/ObjectDetectionNet/ObjectDetectionNet-1.1.zip
diff --git a/.vscode/launch.docker.json b/.vscode/launch.docker.json
new file mode 100644
index 00000000..8d4bd256
--- /dev/null
+++ b/.vscode/launch.docker.json
@@ -0,0 +1,335 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+
+ // Server -------------------------------------------------------------
+
+ {
+ "presentation": {
+ "group": "4 Launch",
+ "order": 1
+ },
+ "name": "Launch Server",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "/app/server/CodeProject.AI.Server.dll",
+ "args": [],
+ "cwd": "/app/server/",
+ "stopAtEntry": false,
+ "requireExactSource": false,
+ "serverReadyAction": {
+ "action": "openExternally",
+ "pattern": "\\bNow listening on:\\s+(https?://\\S+)",
+ "uriFormat": "http://localhost:%s/swagger"
+ },
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "RUNNING_IN_VSCODE": "true",
+ "DOTNET_NOLOGO": "true"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "4 Launch",
+ "order": 100
+ },
+ "name": "Stop all Processes",
+
+ "type": "python",
+ "code": "#", // dummy command
+ "console": "internalConsole",
+ "request": "launch",
+
+ // "type": "coreclr",
+ // "program": "dotnet", // dummy command
+ // "args": [ "--version" ], // dummy command
+
+ "preLaunchTask": "stop-all"
+ },
+
+ {
+ "presentation": {
+ "group": "4 Launch",
+ "order": 2
+ },
+ "name": "Launch Server without Modules",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "/app/server/CodeProject.AI.Server.dll",
+ "args": [
+ "--ModuleOptions:LaunchModules=false"
+ ],
+ "cwd": "/app/server/",
+ "stopAtEntry": false,
+ "serverReadyAction": {
+ "action": "openExternally",
+ "pattern": "\\bNow listening on:\\s+(https?://\\S+)",
+ "uriFormat": "http://localhost:%s/swagger"
+ },
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "RUNNING_IN_VSCODE": "true",
+ "DOTNET_NOLOGO": "true"
+ },
+ "logging": {
+ "engineLogging": false,
+ "moduleLoad": false,
+ "exceptions": true,
+ "browserStdOut": false
+ }
+ },
+
+ // Launch Individual --------------------------------------------------
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "ALPR",
+ "type": "python",
+ "python": "python3.8",
+ "request": "launch",
+ "program": "ALPR_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "/app/modules/ALPR",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Cartooniser",
+ "type": "python",
+ "python": "python3.9",
+ "request": "launch",
+ "program": "cartooniser_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "/app/modules/Cartooniser",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168",
+ "WEIGHTS_FOLDER": "/app/modules/Cartooniser/weights"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Face Processing",
+ "type": "python",
+ "python": "python3.8",
+ "request": "launch",
+ "program": "intelligencelayer/face.py",
+ "console": "integratedTerminal",
+ "cwd": "/app/preinstalled-modules/FaceProcessing/",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Object Detect .NET",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "/app/preinstalled-modules/ObjectDetectionNet/ObjectDetectionNet.dll",
+ "args": [],
+ "cwd": "/app/modules/ObjectDetectionNet",
+ "stopAtEntry": false,
+ "console": "internalConsole",
+ "requireExactSource": false,
+ "justMyCode": false,
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "RUNNING_IN_VSCODE": "true",
+ "DOTNET_NOLOGO": "true",
+ "CPAI_MODULE_QUEUENAME": "objectdetection_queue"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Object Detect TFLite",
+ "type": "python",
+ "request": "launch",
+ "python": "python3.9",
+ "program": "objectdetection_tflite_adapter.py",
+ "cwd": "/app/modules/ObjectDetectionTFLite",
+ "console": "integratedTerminal",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168",
+ "CPAI_MODULE_QUEUENAME": "objectdetection_queue"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Object Detect YOLO 6.2",
+ "type": "python",
+ "python": "python3.8",
+ "request": "launch",
+ "program": "detect_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "/app/preinstalled-modules/ObjectDetectionYolo",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168",
+ "CPAI_MODULE_QUEUENAME": "objectdetection_queue"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Object Detect YOLO 3.1",
+ "type": "python",
+ "python": "/app/modules/YOLOv5-3.1/bin/linux/python38/venv/bin/python",
+ "request": "launch",
+ "program": "yolo_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "/app/modules/YOLOv5-3.1",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "OCR",
+ "type": "python",
+ "python": "/app/modules/OCR/bin/linux/python38/venv/bin/python",
+ "request": "launch",
+ "program": "OCR_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "/app/modules/OCR",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Scene Classifier",
+ "type": "python",
+ "python": "/app/modules/SceneClassifier/bin/linux/python38/venv/bin/python",
+ "request": "launch",
+ "program": "scene_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "/app/modules/SceneClassifier",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Super Resolution",
+ "type": "python",
+ "python": "/app/modules/SuperResolution/bin/linux/python38/venv/bin/python",
+ "request": "launch",
+ "program": "superres_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "/app/modules/SuperResolution",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "SentimentAnalysis",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "/app/modules/SentimentAnalysis/SentimentAnalysis",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ // "cwd": "/app/modules/SentimentAnalysis", - causes an exception. WTF. See HACK.
+ "stopAtEntry": false,
+ "console": "internalConsole",
+ "requireExactSource": false,
+ "justMyCode": false,
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "RUNNING_IN_VSCODE": "true",
+ "DOTNET_NOLOGO": "true",
+ "MODELS_DIR": "/app/modules/SentimentAnalysis/sentiment_model"
+ }
+ },
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "TextSummary",
+ "type": "python",
+ "python": "/app/modules/TextSummary/bin/linux/python38/venv/bin/python",
+ "request": "launch",
+ "program": "summary_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "/app/modules/TextSummary",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 6a315c2d..d92d1a0b 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -31,14 +31,6 @@
"RUNNING_IN_VSCODE": "true",
"DOTNET_NOLOGO": "true"
}
- /*
- "logging": {
- "engineLogging": false,
- "moduleLoad": false,
- "exceptions": false,
- "browserStdOut": false
- }
- */
},
{
"presentation": {
@@ -232,7 +224,7 @@
// "program": "dotnet", // dummy command
// "args": [ "--version" ], // dummy command
- "preLaunchTask": "stop-all",
+ "preLaunchTask": "stop-all"
},
{
"presentation": {
@@ -324,6 +316,35 @@
}
},
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Background Remover",
+ "type": "python",
+ "python": "${workspaceFolder}/src/modules/BackgroundRemover/bin/linux/python39/venv/bin/python",
+ "request": "launch",
+ "program": "rembg_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "${workspaceFolder}/src/modules/BackgroundRemover",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168"
+ },
+ "windows": {
+ "python": "${workspaceFolder}/src/modules/BackgroundRemover/bin/windows/python39/venv/Scripts/python.exe",
+ },
+ "linux": {
+ "python": "${workspaceFolder}/src/modules/BackgroundRemover/bin/linux/python39/venv/bin/python",
+ },
+ "osx": {
+ "python": "${workspaceFolder}/src/modules/BackgroundRemover/bin/macos/python39/venv/bin/python",
+ }
+ },
+
{
"presentation": {
"group": "5 Launch Individual",
@@ -384,6 +405,36 @@
}
},
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Object Detect Coral",
+ "type": "python",
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionCoral/bin/linux/python39/venv/bin/python",
+ "request": "launch",
+ "program": "objectdetection_coral_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "${workspaceFolder}/src/modules/ObjectDetectionCoral",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168",
+ "CPAI_MODULE_QUEUENAME": "objectdetection_queue"
+ },
+ "windows": {
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionCoral/bin/windows/python37/venv/Scripts/python.exe",
+ },
+ "linux": {
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionCoral/bin/linux/python39/venv/bin/python",
+ },
+ "osx": {
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionCoral/bin/macos/python39/venv/bin/python",
+ }
+ },
+
{
"presentation": {
"group": "5 Launch Individual",
@@ -397,7 +448,7 @@
"linux": {
"program": "${workspaceFolder}/src/modules/ObjectDetectionNet/bin/Debug/net7.0/ObjectDetectionNet.dll",
},
- "args": [],
+ // "args": [ "--selftest" ],
"cwd": "${workspaceFolder}",
// "cwd": "${workspaceFolder}/src/modules/ObjectDetectionNet", - causes an exception. WTF. See HACK.
"stopAtEntry": false,
@@ -412,6 +463,37 @@
}
},
+
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Object Detect YOLO RKNN",
+ "type": "python",
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionYoloRKNN/bin/windows/python39/venv/Scripts/python.exe",
+ "request": "launch",
+ "program": "objectdetection_fd_rknn_adapter.py",
+ "console": "integratedTerminal",
+ "cwd": "${workspaceFolder}/src/modules/ObjectDetectionYoloRKNN",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168",
+ "CPAI_MODULE_QUEUENAME": "objectdetection_queue"
+ },
+ "windows": {
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionYoloRKNN/bin/windows/python39/venv/Scripts/python.exe",
+ },
+ "linux": {
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionYoloRKNN/bin/linux/python39/venv/bin/python",
+ },
+ "osx": {
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionYoloRKNN/bin/macos/python39/venv/bin/python",
+ }
+ },
+
{
"presentation": {
"group": "5 Launch Individual",
@@ -420,7 +502,7 @@
"name": "Object Detect TFLite",
"type": "python",
"request": "launch",
- "python": "${workspaceFolder}\\src\\modules\\ObjectDetectionTFLite\\bin\\windows\\python39\\venv\\Scripts\\python.exe",
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionTFLite/bin/windows/python39/venv/Scripts/python.exe",
"program": "objectdetection_tflite_adapter.py",
"cwd": "${workspaceFolder}/src/modules/ObjectDetectionTFLite",
"console": "integratedTerminal",
@@ -429,10 +511,11 @@
"DEBUG_IN_VSCODE": "True",
"RUNNING_IN_VSCODE": "True",
"CPAI_PORT": "32168",
- "CPAI_MODULE_QUEUENAME": "objectdetection_queue"
+ "CPAI_MODULE_QUEUENAME": "objectdetection_queue",
+ "MODEL_SIZE": "tiny"
},
"windows": {
- "python": "${workspaceFolder}\\src\\modules\\ObjectDetectionTFLite\\bin\\windows\\python39\\venv\\Scripts\\python.exe",
+ "python": "${workspaceFolder}/src/modules/ObjectDetectionTFLite/bin/windows/python39/venv/Scripts/python.exe",
},
"linux": {
"python": "${workspaceFolder}/src/modules/ObjectDetectionTFLite/bin/linux/python39/venv/bin/python",
@@ -449,12 +532,13 @@
},
"name": "Object Detect YOLO 6.2",
"type": "python",
- "python": "${workspaceFolder}/src/modules/bin/linux/python38/venv/bin/python",
+ "python": "${workspaceFolder}/src/runtimes/bin/linux/python38/venv/bin/python",
"request": "launch",
"program": "detect_adapter.py",
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/src/modules/ObjectDetectionYolo",
"justMyCode": false,
+ // "args": [ "--selftest" ],
"env": {
"DEBUG_IN_VSCODE": "True",
"RUNNING_IN_VSCODE": "True",
@@ -462,13 +546,13 @@
"CPAI_MODULE_QUEUENAME": "objectdetection_queue"
},
"windows": {
- "python": "${workspaceFolder}/src/modules/bin/windows/python37/venv/Scripts/python"
+ "python": "${workspaceFolder}/src/runtimes/bin/windows/python37/venv/Scripts/python"
},
"linux": {
- "python": "${workspaceFolder}/src/modules/bin/linux/python38/venv/bin/python",
+ "python": "${workspaceFolder}/src/runtimes/bin/linux/python38/venv/bin/python",
},
"osx": {
- "python": "${workspaceFolder}/src/modules/bin/macos/python38/venv/bin/python",
+ "python": "${workspaceFolder}/src/runtimes/bin/macos/python38/venv/bin/python",
}
},
@@ -508,7 +592,7 @@
},
"name": "OCR",
"type": "python",
- "python": "${workspaceFolder}/src/modules/OCR/bin/linux/python38/venv/bin/python",
+ "python": "${workspaceFolder}/src/modules/OCR/bin/windows/python37/venv/Scripts/python",
"request": "launch",
"program": "OCR_adapter.py",
"console": "integratedTerminal",
@@ -520,14 +604,15 @@
"CPAI_PORT": "32168"
},
"windows": {
- "python": "${workspaceFolder}/src/modules/OCR/bin/windows/python37/venv/Scripts/python"
- },
+ "python": "${workspaceFolder}/src/modules/OCR/bin/windows/python37/venv/Scripts/python",
+ }/*,
"linux": {
"python": "${workspaceFolder}/src/modules/OCR/bin/linux/python38/venv/bin/python",
},
"osx": {
"python": "${workspaceFolder}/src/modules/OCR/bin/macos/python38/venv/bin/python",
}
+ */
},
{
@@ -537,7 +622,7 @@
},
"name": "Scene Classifier",
"type": "python",
- "python": "${workspaceFolder}/src/modules/bin/linux/python38/venv/bin/python",
+ "python": "${workspaceFolder}/src/modules/SceneClassifier/bin/linux/python38/venv/bin/python",
"request": "launch",
"program": "scene_adapter.py",
"console": "integratedTerminal",
@@ -549,13 +634,13 @@
"CPAI_PORT": "32168"
},
"windows": {
- "python": "${workspaceFolder}/src/modules/bin/windows/python37/venv/Scripts/python"
+ "python": "${workspaceFolder}/src/modules/SceneClassifier/bin/windows/python37/venv/Scripts/python"
},
"linux": {
- "python": "${workspaceFolder}/src/modules/bin/linux/python38/venv/bin/python",
+ "python": "${workspaceFolder}/src/modules/SceneClassifier/bin/linux/python38/venv/bin/python",
},
"osx": {
- "python": "${workspaceFolder}/src/modules/bin/macos/python38/venv/bin/python",
+ "python": "${workspaceFolder}/src/modules/SceneClassifier/bin/macos/python38/venv/bin/python",
}
},
@@ -632,7 +717,7 @@
"CPAI_PORT": "32168"
},
"windows": {
- "python": "${workspaceFolder}\\src\\modules\\TextSummary\\bin\\windows\\python37\\venv\\Scripts\\python.exe"
+ "python": "${workspaceFolder}/src/modules/TextSummary/bin/windows/python37/venv/Scripts/python.exe"
},
"linux": {
"python": "${workspaceFolder}/src/modules/TextSummary/bin/linux/python38/venv/bin/python",
@@ -642,6 +727,64 @@
}
},
+ {
+ "presentation": {
+ "group": "5 Launch Individual",
+ "hidden": false
+ },
+ "name": "Training YOLOv5",
+ "type": "python",
+ "python": "${workspaceFolder}/src/modules/TrainingYoloV5/bin/macos/python38/venv/bin/python",
+ "request": "launch",
+ "program": "TrainingYoloV5.py",
+ "console": "integratedTerminal",
+ "cwd": "${workspaceFolder}/src/modules/TrainingYoloV5",
+ // "args": [ "--selftest" ],
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168",
+
+ "YOLOv5_AUTOINSTALL": "false",
+ "YOLOv5_VERBOSE": "false",
+
+ "CPAI_MODULE_REQUIRED_MB": "7000",
+
+ "YOLO_DATASETS_DIRNAME": "datasets",
+ "YOLO_TRAINING_DIRNAME": "training",
+ "YOLO_WEIGHTS_DIRNAME": "weights",
+ "YOLO_MODELS_DIRNAME": "assets",
+ "YOLO_DATASET_ZOO_DIRNAME": "zoo"
+ },
+ "windows": {
+ "python": "${workspaceFolder}/src/modules/TrainingYoloV5/bin/windows/python39/venv/Scripts/python.exe"
+ },
+ "linux": {
+ "python": "${workspaceFolder}/src/modules/TrainingYoloV5/bin/linux/python38/venv/bin/python",
+ },
+ "osx": {
+ "python": "${workspaceFolder}/src/modules/TrainingYoloV5/bin/macos/python38/venv/bin/python",
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168",
+
+ "CPAI_MODULE_SUPPORT_GPU": "False", // https://github.com/ultralytics/yolov5/issues/11235
+
+ "YOLOv5_AUTOINSTALL": "false",
+ "YOLOv5_VERBOSE": "false",
+
+ "YOLO_DATASETS_DIRNAME": "datasets",
+ "YOLO_TRAINING_DIRNAME": "training",
+ "YOLO_WEIGHTS_DIRNAME": "weights",
+ "YOLO_MODELS_DIRNAME": "assets",
+ "YOLO_DATASET_ZOO_DIRNAME": "zoo"
+ }
+ }
+ },
+
+
// See https://code.visualstudio.com/docs/editor/variables-reference#_settings-command-variables-and-input-variables
// For variables that can be used
{
@@ -764,6 +907,37 @@
"DEBUG_IN_VSCODE": "True",
"RUNNING_IN_VSCODE": "True"
}
+ },
+
+
+
+ {
+ "presentation": {
+ "group": "6 Launch Demo",
+ "hidden": false
+ },
+ "name": "Racoon detector",
+ "type": "python",
+ "python": "${workspaceFolder}/src/runtimes/bin/windows/python38/venv/bin/python",
+ "request": "launch",
+ "program": "racoon_detect.py",
+ "console": "integratedTerminal",
+ "cwd": "${workspaceFolder}/demos/Python/ObjectDetect",
+ "justMyCode": false,
+ "env": {
+ "DEBUG_IN_VSCODE": "True",
+ "RUNNING_IN_VSCODE": "True",
+ "CPAI_PORT": "32168"
+ },
+ "windows": {
+ "python": "${workspaceFolder}/src/runtimes/bin/windows/python39/venv/Scripts/python.exe"
+ },
+ "linux": {
+ "python": "${workspaceFolder}/src/runtimes/bin/linux/python38/venv/bin/python",
+ },
+ "osx": {
+ "python": "${workspaceFolder}/src/runtimes/bin/macos/python38/venv/bin/python",
+ }
}
]
}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..4e7bfa8f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,49 @@
+{
+ "cSpell.words": [
+ "alpr",
+ "appsettings",
+ "astype",
+ "aync",
+ "callbacktask",
+ "codeproject",
+ "CPAI",
+ "Cuda",
+ "Denoising",
+ "deskew",
+ "dtype",
+ "edgetpu",
+ "fiftyone",
+ "fouo",
+ "hostbuilder",
+ "hyps",
+ "imwrite",
+ "Initialises",
+ "installconfig",
+ "integerize",
+ "licence",
+ "logvals",
+ "modulesettings",
+ "objectdetection",
+ "Onnx",
+ "opencv",
+ "otsu",
+ "paddleocr",
+ "platenumber",
+ "pluralise",
+ "pycoral",
+ "QUEUENAME",
+ "reqid",
+ "reqtype",
+ "reso",
+ "runtimes",
+ "skia",
+ "selftest",
+ "textreader",
+ "tflite",
+ "ufeff",
+ "unclip",
+ "wwwroot",
+ "YOLO",
+ "yolov5"
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.docker.json b/.vscode/tasks.docker.json
new file mode 100644
index 00000000..1de88405
--- /dev/null
+++ b/.vscode/tasks.docker.json
@@ -0,0 +1,16 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+
+ // Launch apps ================================================================================================
+
+ {
+ "label": "stop-all", // Builds and Launches the AI server for Linux
+ "group": "none",
+ "type": "process",
+ "command": "bash",
+ "args": [ "/app/SDK/Scripts/stop_all.sh" ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/CodeProject.AI.sln b/CodeProject.AI.sln
index c7ec75a3..d6463ba8 100644
--- a/CodeProject.AI.sln
+++ b/CodeProject.AI.sln
@@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demos", "demos", "{7F18EB64
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{2379A486-0D28-4CAD-BB13-E77FBA538E0D}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Installers", "Installers", "{D885EE64-C1BD-44D6-84D8-1E46806298D9}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Javascript", "Javascript", "{3A860CDD-94B9-4002-BA08-87E8822DDE50}"
ProjectSection(SolutionItems) = preProject
demos\Javascript\Vision.html = demos\Javascript\Vision.html
@@ -43,6 +45,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frontend", "src\API\Server\
{033E966B-CBFD-4C79-AC06-78203C795EA8} = {033E966B-CBFD-4C79-AC06-78203C795EA8}
{9CBBD8B2-B7CD-4DA0-A476-C1E858CAE5D7} = {9CBBD8B2-B7CD-4DA0-A476-C1E858CAE5D7}
{C33D90E7-7570-46FB-9EB9-ED6B40A93A9B} = {C33D90E7-7570-46FB-9EB9-ED6B40A93A9B}
+ {F682C5F9-854F-4B4E-A7DE-33329F51A26B} = {F682C5F9-854F-4B4E-A7DE-33329F51A26B}
+ {F7056ECA-1C9C-4544-99CA-731C944651D6} = {F7056ECA-1C9C-4544-99CA-731C944651D6}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend", "src\API\Server\Backend\Backend.csproj", "{C93C22D7-4EB2-4EC0-A7F0-FBCFB9F6F72D}"
@@ -58,18 +62,77 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{CB26AB
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{460DB5C8-46F3-4407-A2DF-D9063D14493A}"
ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
global.json = global.json
LICENCE.md = LICENCE.md
README.md = README.md
THIRD-PARTY-NOTICES.md = THIRD-PARTY-NOTICES.md
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_PRIVATE REPO", "_PRIVATE REPO", "{78509730-6FBA-44E5-98C0-083DB7F52027}"
+ ProjectSection(SolutionItems) = preProject
+ README.txt = README.txt
+ EndProjectSection
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDK", "SDK", "{FF0C329F-41E8-4540-BCDB-97690911077D}"
ProjectSection(SolutionItems) = preProject
src\SDK\install.bat = src\SDK\install.bat
src\SDK\install.sh = src\SDK\install.sh
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windows", "Windows", "{83C828B9-2B1E-4982-B4B7-69D173DFBB27}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{5F1052CB-8586-49CB-8F46-427A5F6901C2}"
+ ProjectSection(SolutionItems) = preProject
+ Installers\Windows\CodeProjectAI-install-BG.png = Installers\Windows\CodeProjectAI-install-BG.png
+ Installers\Windows\CodeProjectAI-install-LHS.png = Installers\Windows\CodeProjectAI-install-LHS.png
+ Installers\Windows\CodeProjectAI-install-sidebar.png = Installers\Windows\CodeProjectAI-install-sidebar.png
+ Installers\Windows\CodeProjectAI-install-topbanner.png = Installers\Windows\CodeProjectAI-install-topbanner.png
+ Installers\Windows\favicon.ico = Installers\Windows\favicon.ico
+ Installers\Windows\license.rtf = Installers\Windows\license.rtf
+ Installers\Windows\logo.png = Installers\Windows\logo.png
+ Installers\Windows\logoSide.png = Installers\Windows\logoSide.png
+ Installers\Windows\Sense-install-BG.png = Installers\Windows\Sense-install-BG.png
+ Installers\Windows\Sense-install-sidebar.png = Installers\Windows\Sense-install-sidebar.png
+ Installers\Windows\Sense-install-topbanner.png = Installers\Windows\Sense-install-topbanner.png
+ Installers\Windows\SharedDirectories.wxi = Installers\Windows\SharedDirectories.wxi
+ Installers\Windows\SharedProperties.wxi = Installers\Windows\SharedProperties.wxi
+ Installers\Windows\SharedUIConfiguration.wxi = Installers\Windows\SharedUIConfiguration.wxi
+ EndProjectSection
+EndProject
+Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "CodeProject.AI.BootStrapper", "Installers\Windows\CodeProjectAI.BootStrapper\CodeProject.AI.BootStrapper.wixproj", "{C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}"
+EndProject
+Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "CodeProjectAI.Server.Installer", "Installers\Windows\CodeProjectAI.Server.Installer\CodeProjectAI.Server.Installer.wixproj", "{A1AFA75C-324E-4B79-BE13-5557E495FBBE}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".NET 3.5.1", ".NET 3.5.1", "{8A423F72-C92C-4C8E-87D8-02849FD079E2}"
+ ProjectSection(SolutionItems) = preProject
+ Installers\Windows\.NET 3.5.1\Download .NET Framework 3.5 SP1.url = Installers\Windows\.NET 3.5.1\Download .NET Framework 3.5 SP1.url
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{FB0561D3-4AF8-415A-85B4-E4E9ADDC3DB2}"
+ ProjectSection(SolutionItems) = preProject
+ .wslconfig = .wslconfig
+ Installers\Docker\BuildAndPush.bat = Installers\Docker\BuildAndPush.bat
+ Installers\Docker\BuildAndPush.sh = Installers\Docker\BuildAndPush.sh
+ Installers\Docker\BuildArm64.bat = Installers\Docker\BuildArm64.bat
+ Installers\Docker\BuildArm64.sh = Installers\Docker\BuildArm64.sh
+ Installers\Docker\BuildCPU.bat = Installers\Docker\BuildCPU.bat
+ Installers\Docker\BuildCPU.sh = Installers\Docker\BuildCPU.sh
+ Installers\Docker\BuildGPU.bat = Installers\Docker\BuildGPU.bat
+ Installers\Docker\BuildGPU.sh = Installers\Docker\BuildGPU.sh
+ Installers\Docker\BuildRPi.bat = Installers\Docker\BuildRPi.bat
+ Installers\Docker\BuildRPi.sh = Installers\Docker\BuildRPi.sh
+ Installers\Docker\Dockerfile = Installers\Docker\Dockerfile
+ Installers\Docker\Dockerfile-Arm64 = Installers\Docker\Dockerfile-Arm64
+ Installers\Docker\DockerFile-GPU = Installers\Docker\DockerFile-GPU
+ Installers\Docker\Dockerfile-GPU-cuDNN = Installers\Docker\Dockerfile-GPU-cuDNN
+ Installers\Docker\Dockerfile-RPi64 = Installers\Docker\Dockerfile-RPi64
+ Installers\Docker\DockerPush.bat = Installers\Docker\DockerPush.bat
+ Installers\Docker\README = Installers\Docker\README
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetAspNetCoreVersionAction", "Installers\Windows\GetAspNetCoreVersionAction\GetAspNetCoreVersionAction.csproj", "{C3E39164-5120-41C6-8902-2598DD0EBCD0}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "License plates", "License plates", "{D320EA6C-2388-41F7-A4D1-980192665A61}"
ProjectSection(SolutionItems) = preProject
demos\TestData\License plates\10.jpg = demos\TestData\License plates\10.jpg
@@ -138,6 +201,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Objects", "Objects", "{4ED5
demos\TestData\Objects\traffic-pexels-photo-297927.jpeg = demos\TestData\Objects\traffic-pexels-photo-297927.jpeg
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CudaVersionCustomAction", "Installers\Windows\CudaVersionCustomAction\CudaVersionCustomAction.csproj", "{214949E0-B56C-4F23-809A-07DA4DBDF925}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeProject.AI.API.Server.Backend.Tests", "tests\QueueServiceTests\CodeProject.AI.API.Server.Backend.Tests.csproj", "{031F17E0-BE84-42AF-B9FE-4F928CB03D1B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Python", "Python", "{37533562-EC4C-4FB4-8C42-FE327D1D79BD}"
@@ -154,6 +219,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{1C7E
src\modules\readme.txt = src\modules\readme.txt
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "macOS", "macOS", "{31DA8C15-C038-4667-89AB-74FED47D7B51}"
+ ProjectSection(SolutionItems) = preProject
+ Installers\macOS\create macOS installer.html = Installers\macOS\create macOS installer.html
+ EndProjectSection
+EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "ALPR", "src\modules\ALPR\ALPR.pyproj", "{6AE28B59-221B-4E3D-A66C-E255B26DAC82}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NET", "src\SDK\NET\NET.csproj", "{F7056ECA-1C9C-4544-99CA-731C944651D6}"
@@ -188,21 +258,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{95BF
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "BackgroundRemover", "src\modules\BackgroundRemover\BackgroundRemover.pyproj", "{470D3417-36A4-49A4-B719-496466FA92FC}"
EndProject
-Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "SceneClassifier", "src\modules\SceneClassifier\SceneClassifier.pyproj", "{08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}"
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Cartooniser", "src\modules\Cartooniser\Cartooniser.pyproj", "{470D3417-36A4-49A4-B719-496466FA92FE}"
EndProject
-Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "SuperResolution", "src\modules\SuperResolution\SuperResolution.pyproj", "{A472B309-3C77-4DE5-8F03-AA81938EEFB4}"
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "FaceProcessing", "src\modules\FaceProcessing\intelligencelayer\FaceProcessing.pyproj", "{E5D27495-EE4F-4AAF-8749-A6BA848111E2}"
EndProject
-Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "TextSummary", "src\modules\TextSummary\TextSummary.pyproj", "{470D3417-36A4-49A4-B719-496466FA92FB}"
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "ObjectDetectionCoral", "src\modules\ObjectDetectionCoral\ObjectDetectionCoral.pyproj", "{470D3417-36A4-49A4-B719-496477FA92FB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObjectDetectionNet", "src\modules\ObjectDetectionNet\ObjectDetectionNet.csproj", "{F682C5F9-854F-4B4E-A7DE-33329F51A26B}"
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "ObjectDetectionYolo", "src\modules\ObjectDetectionYolo\ObjectDetectionYolo.pyproj", "{B6A1D372-264E-4F66-B7FB-7FF19587476F}"
EndProject
-Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "FaceProcessing", "src\modules\FaceProcessing\intelligencelayer\FaceProcessing.pyproj", "{E5D27495-EE4F-4AAF-8749-A6BA848111E2}"
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "ObjectDetectionTFLite", "src\modules\ObjectDetectionTFLite\ObjectDetectionTFLite.pyproj", "{4C40A443-6A02-43F1-BD33-8F1A73349CDA}"
EndProject
-Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Cartooniser", "src\modules\Cartooniser\Cartooniser.pyproj", "{470D3417-36A4-49A4-B719-496466FA92FE}"
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "SceneClassifier", "src\modules\SceneClassifier\SceneClassifier.pyproj", "{08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}"
EndProject
-Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "ObjectDetectionTFLite", "src\modules\ObjectDetectionTFLite\ObjectDetectionTFLite.pyproj", "{470D3417-36A4-49A4-B719-496477FA92FB}"
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "SuperResolution", "src\modules\SuperResolution\SuperResolution.pyproj", "{A472B309-3C77-4DE5-8F03-AA81938EEFB4}"
+EndProject
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "TextSummary", "src\modules\TextSummary\TextSummary.pyproj", "{470D3417-36A4-49A4-B719-496466FA92FB}"
+EndProject
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "TrainingYoloV5", "src\modules\TrainingYoloV5\TrainingYoloV5.pyproj", "{2DFDA382-189B-45D1-94D5-3004D1AEB73C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -262,6 +336,51 @@ Global
{C93C22D7-4EB2-4EC0-A7F0-FBCFB9F6F72D}.Release|ARM64.Build.0 = Release|ARM64
{C93C22D7-4EB2-4EC0-A7F0-FBCFB9F6F72D}.Release|x86.ActiveCfg = Release|Any CPU
{C93C22D7-4EB2-4EC0-A7F0-FBCFB9F6F72D}.Release|x86.Build.0 = Release|Any CPU
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Debug|ARM64.ActiveCfg = Debug|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Debug|ARM64.Build.0 = Debug|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Debug|x86.ActiveCfg = Debug|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Debug|x86.Build.0 = Debug|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Release|Any CPU.ActiveCfg = Release|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Release|Any CPU.Build.0 = Release|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Release|ARM64.ActiveCfg = Release|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Release|ARM64.Build.0 = Release|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Release|x86.ActiveCfg = Release|x86
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79}.Release|x86.Build.0 = Release|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Debug|ARM64.ActiveCfg = Debug|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Debug|ARM64.Build.0 = Debug|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Debug|x86.ActiveCfg = Debug|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Debug|x86.Build.0 = Debug|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Release|Any CPU.ActiveCfg = Release|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Release|Any CPU.Build.0 = Release|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Release|ARM64.ActiveCfg = Release|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Release|ARM64.Build.0 = Release|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Release|x86.ActiveCfg = Release|x86
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE}.Release|x86.Build.0 = Release|x86
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Debug|Any CPU.Build.0 = Debug|x86
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Debug|ARM64.Build.0 = Debug|ARM64
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Debug|x86.ActiveCfg = Debug|x86
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Debug|x86.Build.0 = Debug|x86
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Release|Any CPU.ActiveCfg = Release|x86
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Release|Any CPU.Build.0 = Release|x86
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Release|ARM64.ActiveCfg = Release|ARM64
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Release|ARM64.Build.0 = Release|ARM64
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Release|x86.ActiveCfg = Release|x86
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0}.Release|x86.Build.0 = Release|x86
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Debug|ARM64.Build.0 = Debug|ARM64
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Debug|x86.ActiveCfg = Debug|x86
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Debug|x86.Build.0 = Debug|x86
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Release|Any CPU.ActiveCfg = Release|x86
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Release|Any CPU.Build.0 = Release|x86
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Release|ARM64.ActiveCfg = Release|ARM64
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Release|ARM64.Build.0 = Release|ARM64
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Release|x86.ActiveCfg = Release|x86
+ {214949E0-B56C-4F23-809A-07DA4DBDF925}.Release|x86.Build.0 = Release|x86
{031F17E0-BE84-42AF-B9FE-4F928CB03D1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{031F17E0-BE84-42AF-B9FE-4F928CB03D1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{031F17E0-BE84-42AF-B9FE-4F928CB03D1B}.Debug|ARM64.ActiveCfg = Debug|ARM64
@@ -310,7 +429,7 @@ Global
{0690D5F7-864F-4347-8E20-FA9903CE56EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0690D5F7-864F-4347-8E20-FA9903CE56EB}.Release|ARM64.ActiveCfg = Release|Any CPU
{0690D5F7-864F-4347-8E20-FA9903CE56EB}.Release|x86.ActiveCfg = Release|Any CPU
- {B6A1D372-264E-4F66-B7FB-7FF19587476E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B6A1D372-264E-4F66-B7FB-7FF19587476E}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{B6A1D372-264E-4F66-B7FB-7FF19587476E}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{B6A1D372-264E-4F66-B7FB-7FF19587476E}.Debug|x86.ActiveCfg = Debug|Any CPU
{B6A1D372-264E-4F66-B7FB-7FF19587476E}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -352,24 +471,24 @@ Global
{470D3417-36A4-49A4-B719-496466FA92FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{470D3417-36A4-49A4-B719-496466FA92FC}.Release|ARM64.ActiveCfg = Release|Any CPU
{470D3417-36A4-49A4-B719-496466FA92FC}.Release|x86.ActiveCfg = Release|Any CPU
- {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Debug|x86.ActiveCfg = Debug|Any CPU
- {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Release|ARM64.ActiveCfg = Release|Any CPU
- {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Release|x86.ActiveCfg = Release|Any CPU
- {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Debug|x86.ActiveCfg = Debug|Any CPU
- {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Release|ARM64.ActiveCfg = Release|Any CPU
- {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Release|x86.ActiveCfg = Release|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FB}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FB}.Debug|x86.ActiveCfg = Debug|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FB}.Release|ARM64.ActiveCfg = Release|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FB}.Release|x86.ActiveCfg = Release|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FE}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FE}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FE}.Release|x86.ActiveCfg = Release|Any CPU
+ {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Release|x86.ActiveCfg = Release|Any CPU
+ {470D3417-36A4-49A4-B719-496477FA92FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {470D3417-36A4-49A4-B719-496477FA92FB}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {470D3417-36A4-49A4-B719-496477FA92FB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {470D3417-36A4-49A4-B719-496477FA92FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {470D3417-36A4-49A4-B719-496477FA92FB}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {470D3417-36A4-49A4-B719-496477FA92FB}.Release|x86.ActiveCfg = Release|Any CPU
{F682C5F9-854F-4B4E-A7DE-33329F51A26B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F682C5F9-854F-4B4E-A7DE-33329F51A26B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F682C5F9-854F-4B4E-A7DE-33329F51A26B}.Debug|ARM64.ActiveCfg = Debug|Any CPU
@@ -388,24 +507,36 @@ Global
{B6A1D372-264E-4F66-B7FB-7FF19587476F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6A1D372-264E-4F66-B7FB-7FF19587476F}.Release|ARM64.ActiveCfg = Release|Any CPU
{B6A1D372-264E-4F66-B7FB-7FF19587476F}.Release|x86.ActiveCfg = Release|Any CPU
- {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Debug|x86.ActiveCfg = Debug|Any CPU
- {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Release|ARM64.ActiveCfg = Release|Any CPU
- {E5D27495-EE4F-4AAF-8749-A6BA848111E2}.Release|x86.ActiveCfg = Release|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FE}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FE}.Debug|x86.ActiveCfg = Debug|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FE}.Release|ARM64.ActiveCfg = Release|Any CPU
- {470D3417-36A4-49A4-B719-496466FA92FE}.Release|x86.ActiveCfg = Release|Any CPU
- {470D3417-36A4-49A4-B719-496477FA92FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {470D3417-36A4-49A4-B719-496477FA92FB}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {470D3417-36A4-49A4-B719-496477FA92FB}.Debug|x86.ActiveCfg = Debug|Any CPU
- {470D3417-36A4-49A4-B719-496477FA92FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {470D3417-36A4-49A4-B719-496477FA92FB}.Release|ARM64.ActiveCfg = Release|Any CPU
- {470D3417-36A4-49A4-B719-496477FA92FB}.Release|x86.ActiveCfg = Release|Any CPU
+ {4C40A443-6A02-43F1-BD33-8F1A73349CDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4C40A443-6A02-43F1-BD33-8F1A73349CDA}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {4C40A443-6A02-43F1-BD33-8F1A73349CDA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4C40A443-6A02-43F1-BD33-8F1A73349CDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4C40A443-6A02-43F1-BD33-8F1A73349CDA}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {4C40A443-6A02-43F1-BD33-8F1A73349CDA}.Release|x86.ActiveCfg = Release|Any CPU
+ {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {08A2DD62-D65A-47A8-AB9D-55D18DBC74D6}.Release|x86.ActiveCfg = Release|Any CPU
+ {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {A472B309-3C77-4DE5-8F03-AA81938EEFB4}.Release|x86.ActiveCfg = Release|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FB}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FB}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {470D3417-36A4-49A4-B719-496466FA92FB}.Release|x86.ActiveCfg = Release|Any CPU
+ {2DFDA382-189B-45D1-94D5-3004D1AEB73C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2DFDA382-189B-45D1-94D5-3004D1AEB73C}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {2DFDA382-189B-45D1-94D5-3004D1AEB73C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2DFDA382-189B-45D1-94D5-3004D1AEB73C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2DFDA382-189B-45D1-94D5-3004D1AEB73C}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {2DFDA382-189B-45D1-94D5-3004D1AEB73C}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -422,17 +553,26 @@ Global
{C93C22D7-4EB2-4EC0-A7F0-FBCFB9F6F72D} = {93A23681-E9E8-4381-9EB5-8D178A0EE785}
{CB26AB5B-DB85-4A59-A3AE-FA55A35D05B0} = {3A860CDD-94B9-4002-BA08-87E8822DDE50}
{FF0C329F-41E8-4540-BCDB-97690911077D} = {A8B76501-496A-4011-9C37-8308A1EBDFA7}
+ {83C828B9-2B1E-4982-B4B7-69D173DFBB27} = {D885EE64-C1BD-44D6-84D8-1E46806298D9}
+ {5F1052CB-8586-49CB-8F46-427A5F6901C2} = {83C828B9-2B1E-4982-B4B7-69D173DFBB27}
+ {C04BBD0D-FD36-4FA4-805B-106BCCD9BC79} = {83C828B9-2B1E-4982-B4B7-69D173DFBB27}
+ {A1AFA75C-324E-4B79-BE13-5557E495FBBE} = {83C828B9-2B1E-4982-B4B7-69D173DFBB27}
+ {8A423F72-C92C-4C8E-87D8-02849FD079E2} = {83C828B9-2B1E-4982-B4B7-69D173DFBB27}
+ {FB0561D3-4AF8-415A-85B4-E4E9ADDC3DB2} = {D885EE64-C1BD-44D6-84D8-1E46806298D9}
+ {C3E39164-5120-41C6-8902-2598DD0EBCD0} = {83C828B9-2B1E-4982-B4B7-69D173DFBB27}
{D320EA6C-2388-41F7-A4D1-980192665A61} = {B10B59B5-9F63-41C2-BFBB-6C7311DC4E99}
{C5CC1B6F-14B1-41C1-A2F3-164B37BDCC0C} = {B10B59B5-9F63-41C2-BFBB-6C7311DC4E99}
{49530738-22E7-4D2C-88FD-B20B68BF3A75} = {B10B59B5-9F63-41C2-BFBB-6C7311DC4E99}
{FB31D291-AB9E-43E7-B92D-DBE33F6DD65A} = {B10B59B5-9F63-41C2-BFBB-6C7311DC4E99}
{C2EFFA0A-E8EA-4AFE-8599-FC28CB7864FB} = {B10B59B5-9F63-41C2-BFBB-6C7311DC4E99}
{4ED567B5-C28D-48BB-AEDC-864E2B2C7204} = {B10B59B5-9F63-41C2-BFBB-6C7311DC4E99}
+ {214949E0-B56C-4F23-809A-07DA4DBDF925} = {83C828B9-2B1E-4982-B4B7-69D173DFBB27}
{031F17E0-BE84-42AF-B9FE-4F928CB03D1B} = {D982BD8C-2257-413B-8513-8043AB3035F3}
{37533562-EC4C-4FB4-8C42-FE327D1D79BD} = {7F18EB64-C857-49C4-9380-70D3CCE6242B}
{25750BF1-1502-4F65-8D69-CEA8C87D6446} = {37533562-EC4C-4FB4-8C42-FE327D1D79BD}
{C2500118-FD99-49EF-B726-3E2A3B30A717} = {37533562-EC4C-4FB4-8C42-FE327D1D79BD}
{1C7E0F81-1F4A-478B-80CE-4C41606DC087} = {A8B76501-496A-4011-9C37-8308A1EBDFA7}
+ {31DA8C15-C038-4667-89AB-74FED47D7B51} = {D885EE64-C1BD-44D6-84D8-1E46806298D9}
{6AE28B59-221B-4E3D-A66C-E255B26DAC82} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
{F7056ECA-1C9C-4544-99CA-731C944651D6} = {FF0C329F-41E8-4540-BCDB-97690911077D}
{0690D5F7-864F-4347-8E20-FA9903CE56EB} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
@@ -442,14 +582,16 @@ Global
{0690D5F7-864F-4347-8E20-FA9903CE56EA} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
{95BF276E-3D5E-4CB2-9FEB-A77AF0C2728C} = {FF0C329F-41E8-4540-BCDB-97690911077D}
{470D3417-36A4-49A4-B719-496466FA92FC} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
+ {470D3417-36A4-49A4-B719-496466FA92FE} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
+ {E5D27495-EE4F-4AAF-8749-A6BA848111E2} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
+ {470D3417-36A4-49A4-B719-496477FA92FB} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
+ {F682C5F9-854F-4B4E-A7DE-33329F51A26B} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
+ {B6A1D372-264E-4F66-B7FB-7FF19587476F} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
+ {4C40A443-6A02-43F1-BD33-8F1A73349CDA} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
{08A2DD62-D65A-47A8-AB9D-55D18DBC74D6} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
{A472B309-3C77-4DE5-8F03-AA81938EEFB4} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
{470D3417-36A4-49A4-B719-496466FA92FB} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
- {F682C5F9-854F-4B4E-A7DE-33329F51A26B} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
- {B6A1D372-264E-4F66-B7FB-7FF19587476F} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
- {E5D27495-EE4F-4AAF-8749-A6BA848111E2} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
- {470D3417-36A4-49A4-B719-496466FA92FE} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
- {470D3417-36A4-49A4-B719-496477FA92FB} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
+ {2DFDA382-189B-45D1-94D5-3004D1AEB73C} = {1C7E0F81-1F4A-478B-80CE-4C41606DC087}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {83740BD9-AEEF-49C7-A722-D7703D3A38CB}
diff --git a/WSL-README.md b/WSL-README.md
new file mode 100644
index 00000000..f61bbd0d
--- /dev/null
+++ b/WSL-README.md
@@ -0,0 +1,63 @@
+# Notes on developing under WSL
+
+This repository can be run under Windows or WSL at the same time. The setup.bat
+and setup.sh scripts will install runtimes and virtual environments into specific
+locations for the given OS. Virtual environments, for example, will live under
+the /bin/windows folder for a module in Windows, and /bin/linux when under Linux.
+This allows the same code and models to be worked on and tested under both
+Operating Systems.
+
+Note that this means you will need VS Code installed in Windows and in the WSL
+hosted Ubuntu (should you choose to use Ubuntu in WSL). This further requires
+that each instance of VS Code has the necessary extensions installed. The
+profile sync mechanism in VS Code makes this seamless.
+
+## Speed considerations
+
+To share the same files and code between WSL (Ubuntu) and Windows, you need to
+have one environment point to files in another. Often this would mean that the
+file system lives in Windows, and the WSL instance accesses the Windows hosted
+files through the magic of WSL.
+
+Crossing the OS boundary will result in poor disk performance for the WSL
+instance. Having a WSL instance of VS Code work on its own copy of this repo,
+separate from the Windows instance, speeds disk access (eg PIP installs)
+dramatically.
+
+## Space considerations
+
+If you choose to run separate copies of the code in WSL and Windows then it means
+you are doubling up on the code, libraries, tools, and compiled executables. If
+you have limited disk space this can be an issue.
+
+### To free up space
+
+To free up space you can use the clean.bat/clean.sh scripts under /src/SDK/Scripts.
+
+To actually realise the freed up space in WSL you will need to compact the VHD
+in which your WSL instance resides.
+
+In a Windows terminal:
+
+```cmd
+wsl --shutdown
+diskpart
+```
+
+This will shutdown WSL and open a disk partition session in a new window. Locate
+the VHD file for WSL by heading to `%LOCALAPPDATA%\Packages` and looking for a
+folder similar to `CanonicalGroupLimited.Ubuntu_79rhkp1fndgsc` that contains a
+file `ext4.vhd`.
+
+Within the session, enter the following (adjusting the `ext4.vhd` location as needed)
+
+```text
+select vdisk file="%LOCALAPPDATA%\Packages\CanonicalGroupLimited.Ubuntu_79rhkp1fndgsc\LocalState\ext4.vhdx"
+attach vdisk readonly
+compact vdisk
+detach vdisk
+exit
+```
+
+Your WSL virtual hard drive should be smaller and the space that was used
+reclaimed by Windows.
\ No newline at end of file
diff --git a/demos/Python/Face/face.py b/demos/Python/Face/face.py
index c9ff6f2a..86e5e8fb 100644
--- a/demos/Python/Face/face.py
+++ b/demos/Python/Face/face.py
@@ -2,6 +2,7 @@
import requests
from PIL import Image
from options import Options
+from .. import utils
def main():
@@ -12,7 +13,7 @@ def main():
image_data = open(filepath, "rb").read()
image = Image.open(filepath).convert("RGB")
- opts.cleanDetectedDir()
+ utils.cleanDir(opts.detectedDir)
response = requests.post(opts.endpoint("vision/face"),
files = {"image": image_data}).json()
diff --git a/demos/Python/Face/options.py b/demos/Python/Face/options.py
index 5586f5a6..1de16671 100644
--- a/demos/Python/Face/options.py
+++ b/demos/Python/Face/options.py
@@ -13,16 +13,3 @@ class Options:
def endpoint(self, route) -> str:
return self.serverUrl + route
- def cleanDetectedDir(self) -> None:
- # make sure the detected directory exists
- if not os.path.exists(self.detectedDir):
- os.mkdir(self.detectedDir)
-
- # delete all the files in the output directory
- filelist = os.listdir(self.detectedDir)
- for filename in filelist:
- try:
- filepath = os.path.join(self.detectedDir, filename)
- os.remove(filepath)
- except:
- pass
diff --git a/demos/Python/ObjectDetect/detect.py b/demos/Python/ObjectDetect/detect.py
index c8bbbdde..78b95368 100644
--- a/demos/Python/ObjectDetect/detect.py
+++ b/demos/Python/ObjectDetect/detect.py
@@ -2,6 +2,7 @@
import requests
from PIL import Image, ImageDraw
+from .. import utils
from options import Options
def main():
@@ -9,7 +10,7 @@ def main():
minConfidence = 0.4
opts = Options()
- opts.cleanDetectedDir()
+ utils.cleanDir(opts.detectedDir)
imagedir = opts.imageDir + "/Objects"
diff --git a/demos/Python/ObjectDetect/options.py b/demos/Python/ObjectDetect/options.py
index d2fbb2ae..10773f78 100644
--- a/demos/Python/ObjectDetect/options.py
+++ b/demos/Python/ObjectDetect/options.py
@@ -11,25 +11,16 @@ class Options:
# works for you
rtsp_user = os.getenv("CPAI_RTSP_DEMO_USER", "User")
rtsp_pass = os.getenv("CPAI_RTSP_DEMO_PASS", "Pass")
- rtsp_IP = os.getenv("CPAI_RTSP_DEMO_IP", "10.0.0.204")
+ rtsp_IP = os.getenv("CPAI_RTSP_DEMO_IP", "10.0.0.198")
rtsp_url = f"rtsp://{rtsp_user}:{rtsp_pass}@{rtsp_IP}/live"
+ email_server = os.getenv("CPAI_EMAIL_DEMO_SERVER", "smtp.gmail.com")
+ email_port = int(os.getenv("CPAI_EMAIL_DEMO_PORT", 587))
+ email_acct = os.getenv("CPAI_EMAIL_DEMO_FROM", "me@gmail.com")
+ email_pwd = os.getenv("CPAI_EMAIL_DEMO_PWD", "password123")
+
# names of directories of interest
detectedDir = "detected"
def endpoint(self, route) -> str:
return self.server_url + route
-
- def cleanDetectedDir(self) -> None:
- # make sure the detected directory exists
- if not os.path.exists(self.detectedDir):
- os.mkdir(self.detectedDir)
-
- # delete all the files in the output directory
- filelist = os.listdir(self.detectedDir)
- for filename in filelist:
- try:
- filepath = os.path.join(self.detectedDir, filename)
- os.remove(filepath)
- except:
- pass
diff --git a/demos/Python/ObjectDetect/racoon_detect.py b/demos/Python/ObjectDetect/racoon_detect.py
new file mode 100644
index 00000000..536952a6
--- /dev/null
+++ b/demos/Python/ObjectDetect/racoon_detect.py
@@ -0,0 +1,201 @@
+import base64
+from datetime import datetime
+import io
+from io import BytesIO
+from typing import List, Tuple
+import requests
+
+import imutils
+from imutils.video import VideoStream
+import cv2
+
+import numpy as np
+from PIL import Image, ImageDraw, ImageFont
+
+import smtplib
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+
+from options import Options
+opts = Options()
+
+recipient = "alerts@acme_security.com" # Sucker who deals with the reports
+model_name = "critters" # Model we'll use
+intruders = [ "racoon", "squirrel", "skunk" ] # Things we care about
+secs_between_checks = 5 # Min secs between sending a frame to CodeProject.AI
+secs_between_alerts = 300 # Min secs between sending alerts (don't spam!)
+
+# Set to any time that's over an hour old
+last_check_time = datetime(1999, 11, 15, 0, 0, 0)
+last_alert_time = datetime(1999, 11, 15, 0, 0, 0)
+
+def do_detection(image: Image, intruders: List[str]) -> "(Image, str)":
+
+ """
+ Performs object detection on an image and returns an image with the objects
+ that were detected outlined, as well as a de-duped list of objects detected.
+ If nothing detected, image and list of objects are both returned as None
+ """
+
+ # Convert to format suitable for a POST
+ buf = io.BytesIO()
+ image.save(buf, format='JPEG')
+ buf.seek(0)
+
+ # Better to have a session object created once at the start and closed at
+ # the end, but we keep the code simpler here for demo purposes
+ with requests.Session() as session:
+ response = session.post(opts.endpoint("vision/custom/" + model_name),
+ files={"image": ('image.png', buf, 'image/png') },
+ data={"min_confidence": 0.5}).json()
+
+ # Get the predictions (but be careful of a null return)
+ predictions = response["predictions"]
+
+ detected_list = []
+
+ if predictions:
+ # Draw each bounding box that was returned by the AI engine
+ # font = ImageFont.load_default()
+ font_size = 25
+ padding = 5
+ font = ImageFont.truetype("arial.ttf", font_size)
+ draw = ImageDraw.Draw(image)
+
+ for object in predictions:
+ label = object["label"]
+ conf = object["confidence"]
+ y_max = int(object["y_max"])
+ y_min = int(object["y_min"])
+ x_max = int(object["x_max"])
+ x_min = int(object["x_min"])
+
+ draw.rectangle([(x_min, y_min), (x_max, y_max)], outline="red", width=5)
+ draw.rectangle([(x_min, y_min - 2*padding - font_size),
+ (x_max, y_min)], fill="red", outline="red")
+ draw.text((x_min + padding, y_min - padding - font_size),
+ f"{label} {round(conf*100.0,0)}%", font=font)
+
+ # We're looking for specific objects. Build a deduped list
+ # containing only the objects we're interested in.
+ if label in intruders and not label in detected_list:
+ detected_list.append(label)
+
+ # All done. Did we find any objects we were interested in?
+ if detected_list:
+ return image, ', '.join(detected_list)
+
+ return None, None
+
+
+def report_intruder(image: Image, objects_detected: str, recipient: str) -> None:
+
+ # time since we last sent an alert
+ global last_alert_time
+ seconds_since_last_alert = (datetime.now() - last_alert_time).total_seconds()
+
+ # Only send an alert if there's been sufficient time since the last alert
+ if seconds_since_last_alert > secs_between_alerts:
+
+ # Simple console output
+ timestamp = datetime.now().strftime("%d %b %Y %I:%M:%S %p")
+ print(f"{timestamp} Intruder or intruders detected: {objects_detected}")
+
+ # Send an email alert as well
+ with BytesIO() as buffered:
+ image.save(buffered, format="JPEG")
+ img_dataB64_bytes : bytes = base64.b64encode(buffered.getvalue())
+ img_dataB64 : str = img_dataB64_bytes.decode("ascii");
+
+ message_html = "
An intruder was detected. Please review this image
" \
+ + f""
+ message_text = "A intruder was detected. We're all doomed!"
+
+ send_email(opts.email_acct, opts.email_pwd, recipient, "Intruder Alert!",
+ message_text, message_html)
+
+ # Could send an SMS or a tweet. Whatever takes your fancy...
+
+ last_alert_time = datetime.now()
+
+
+def send_email(sender, pwd, recipient, subject, message_text, message_html):
+
+ msg = MIMEMultipart('alternative')
+ msg['From'] = sender
+ msg['To'] = recipient
+ msg['Subject'] = subject
+
+ text = MIMEText(message_text, 'plain')
+ html = MIMEText(message_html, 'html')
+ msg.attach(text)
+ msg.attach(html)
+
+ try:
+ server = smtplib.SMTP(opts.email_server, opts.email_port)
+ server.ehlo()
+ server.starttls()
+ server.ehlo()
+ server.login(sender, pwd)
+ server.send_message(msg, sender, [recipient])
+ except Exception as ex:
+ print(f"Error sending email: {ex}")
+ finally:
+ server.quit()
+
+
+def main():
+
+ # Open the RTSP stream
+ vs = VideoStream(opts.rtsp_url).start()
+
+ while True:
+
+ # Grab a frame at a time
+ frame = vs.read()
+ if frame is None:
+ continue
+
+ objects_detected = ""
+
+ # Let's not send an alert *every* time we see an object, otherwise we'll
+ # get an endless stream of emails, fractions of a second apart
+ global last_check_time
+ seconds_since_last_check = (datetime.now() - last_check_time).total_seconds()
+
+ if seconds_since_last_check >= secs_between_checks:
+ # You may need to convert the colour space.
+ # image: Image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
+ image: Image = Image.fromarray(frame)
+ (image, objects_detected) = do_detection(image, intruders)
+
+ # Replace the webcam feed's frame with our image that include object
+ # bounding boxes
+ if image:
+ frame = np.asarray(image)
+
+ last_check_time = datetime.now()
+
+ # Resize and display the frame on the screen
+ if frame is not None:
+ frame = imutils.resize(frame, width = 1200)
+ cv2.imshow('WyzeCam', frame)
+
+ if objects_detected:
+ # Shrink the image to reduce email size
+ frame = imutils.resize(frame, width = 600)
+ image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
+ report_intruder(image, objects_detected, recipient)
+
+ # Wait for the user to hit 'q' for quit
+ key = cv2.waitKey(1) & 0xFF
+ if key == ord('q'):
+ break
+
+ # Clean up and we're outta here.
+ cv2.destroyAllWindows()
+ vs.stop()
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/demos/Python/utils.py b/demos/Python/utils.py
new file mode 100644
index 00000000..1c35daa2
--- /dev/null
+++ b/demos/Python/utils.py
@@ -0,0 +1,15 @@
+import os
+
+def cleanDir(dir: str) -> None:
+ # make sure the detected directory exists
+ if not os.path.exists(dir):
+ os.mkdir(dir)
+
+ # delete all the files in the output directory
+ filelist = os.listdir(dir)
+ for filename in filelist:
+ try:
+ filepath = os.path.join(dir, filename)
+ os.remove(filepath)
+ except:
+ pass
diff --git a/demos/TestData/Objects/parrot.jpg b/demos/TestData/Objects/parrot.jpg
new file mode 100644
index 00000000..1ced7b37
Binary files /dev/null and b/demos/TestData/Objects/parrot.jpg differ
diff --git a/demos/TestData/Objects/pexels-photo-414476.jpeg b/demos/TestData/Objects/pexels-photo-414476.jpeg
new file mode 100644
index 00000000..b44719a8
Binary files /dev/null and b/demos/TestData/Objects/pexels-photo-414476.jpeg differ
diff --git a/demos/TestData/Objects/quail.JPG b/demos/TestData/Objects/quail.JPG
new file mode 100644
index 00000000..4c2cf2f1
Binary files /dev/null and b/demos/TestData/Objects/quail.JPG differ
diff --git a/demos/dotNet/CodeProject.AI.Explorer/CodeProject.AI.ApiClient.cs b/demos/dotNet/CodeProject.AI.Explorer/CodeProject.AI.ApiClient.cs
index a10a7ec4..abf18a05 100644
--- a/demos/dotNet/CodeProject.AI.Explorer/CodeProject.AI.ApiClient.cs
+++ b/demos/dotNet/CodeProject.AI.Explorer/CodeProject.AI.ApiClient.cs
@@ -76,10 +76,11 @@ public async Task Ping()
try
{
var content = new MultipartFormDataContent();
- using var httpResponse = await Client.GetAsync("status/ping");
+ using var httpResponse = await Client.GetAsync("status/ping").ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- response = await httpResponse.Content.ReadFromJsonAsync();
+ response = await httpResponse.Content.ReadFromJsonAsync()
+ .ConfigureAwait(false);
response ??= new ErrorResponse("No response from the server");
}
catch (Exception ex)
@@ -112,10 +113,12 @@ public async Task DetectFaces(string image_path)
request.Add(content, "image", Path.GetFileName(image_path));
- using var httpResponse = await Client.PostAsync("vision/face", request);
+ using var httpResponse = await Client.PostAsync("vision/face", request)
+ .ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- response = await httpResponse.Content.ReadFromJsonAsync();
+ response = await httpResponse.Content.ReadFromJsonAsync()
+ .ConfigureAwait(false);
response ??= new ErrorResponse("No response from the server");
}
@@ -154,10 +157,12 @@ public async Task MatchFaces(string image1FileName, string image2F
request.Add(new StreamContent(image1_data), "image1", Path.GetFileName(image1FileName));
request.Add(new StreamContent(image2_data), "image2", Path.GetFileName(image2FileName));
- using var httpResponse = await Client.PostAsync("vision/face/match", request);
+ using var httpResponse = await Client.PostAsync("vision/face/match", request)
+ .ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- response = await httpResponse.Content.ReadFromJsonAsync();
+ response = await httpResponse.Content.ReadFromJsonAsync()
+ .ConfigureAwait(false);
response ??= new ErrorResponse("No response from the server");
}
catch (Exception ex)
@@ -188,10 +193,12 @@ public async Task DetectScene(string image_path)
request.Add(new StreamContent(image_data), "image", Path.GetFileName(image_path));
- using var httpResponse = await Client.PostAsync("vision/scene", request);
+ using var httpResponse = await Client.PostAsync("vision/scene", request)
+ .ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- response = await httpResponse.Content.ReadFromJsonAsync();
+ response = await httpResponse.Content.ReadFromJsonAsync()
+ .ConfigureAwait(false);
response ??= new ErrorResponse("No response from the server");
}
catch (Exception ex)
@@ -223,12 +230,16 @@ public async Task DetectObjects(string image_path)
request.Add(new StreamContent(image_data), "image", Path.GetFileName(image_path));
- using var httpResponse = await Client.PostAsync("vision/detection", request);
+ using var httpResponse = await Client.PostAsync("vision/detection", request)
+ .ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- response = await httpResponse.Content.ReadFromJsonAsync();
- //var json = await httpResponse.Content.ReadAsStringAsync();
- //response = System.Text.Json.JsonSerializer.Deserialize(json);
+ response = await httpResponse.Content.ReadFromJsonAsync()
+ .ConfigureAwait(false);
+
+ // var json = await httpResponse.Content.ReadAsStringAsync();
+ // response = System.Text.Json.JsonSerializer.Deserialize(json);
+
response ??= new ErrorResponse("No response from the server");
}
catch (Exception ex)
@@ -260,11 +271,12 @@ public async Task CustomDetectObjects(string modelName, string ima
request.Add(new StreamContent(image_data), "image", Path.GetFileName(image_path));
- using var httpResponse = await Client.PostAsync("vision/custom", request);
+ using var httpResponse = await Client.PostAsync("vision/custom", request)
+ .ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
// response = await httpResponse.Content.ReadFromJsonAsync();
- var json = await httpResponse.Content.ReadAsStringAsync();
+ var json = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
response = System.Text.Json.JsonSerializer.Deserialize(json);
response ??= new ErrorResponse("No response from the server");
}
@@ -309,11 +321,12 @@ public async Task RegisterFace(string userId,
Path.GetFileName(filename));
}
- using var httpResponse = await Client.PostAsync("vision/face/register", request);
+ using var httpResponse = await Client.PostAsync("vision/face/register", request)
+ .ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
// response = await httpResponse.Content.ReadFromJsonAsync();
- var json = await httpResponse.Content.ReadAsStringAsync();
+ var json = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
response = System.Text.Json.JsonSerializer.Deserialize(json);
response ??= new ErrorResponse("No response from the server");
}
@@ -351,10 +364,12 @@ public async Task RecognizeFace(string? filename,
if (minConfidence.HasValue)
request.Add(new StringContent(minConfidence.Value.ToString()), "min_confidence");
- using var httpResponse = await Client.PostAsync("vision/face/recognize", request);
+ using var httpResponse = await Client.PostAsync("vision/face/recognize", request)
+ .ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- response = await httpResponse.Content.ReadFromJsonAsync();
+ response = await httpResponse.Content.ReadFromJsonAsync()
+ .ConfigureAwait(false);
response ??= new ErrorResponse("No response from the server");
}
catch (Exception ex)
@@ -378,10 +393,12 @@ public async Task DeleteRegisteredFace(string userId)
{ new StringContent(userId), "userid" }
};
- using var httpResponse = await Client.PostAsync("vision/face/delete", request);
+ using var httpResponse = await Client.PostAsync("vision/face/delete", request)
+ .ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- response = await httpResponse.Content.ReadFromJsonAsync();
+ response = await httpResponse.Content.ReadFromJsonAsync()
+ .ConfigureAwait(false);
response ??= new ErrorResponse("No response from the server");
}
catch (Exception ex)
@@ -398,12 +415,14 @@ public async Task ListRegisteredFaces()
try
{
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
- using var httpResponse = await Client.PostAsync("vision/face/list", null);
+ using var httpResponse = await Client.PostAsync("vision/face/list", null)
+ .ConfigureAwait(false);
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
httpResponse.EnsureSuccessStatusCode();
- response = await httpResponse.Content.ReadFromJsonAsync();
+ response = await httpResponse.Content.ReadFromJsonAsync()
+ .ConfigureAwait(false);
response ??= new ErrorResponse("No response from the server");
}
catch (Exception ex)
diff --git a/demos/dotNet/CodeProject.AI.Explorer/Form1.cs b/demos/dotNet/CodeProject.AI.Explorer/Form1.cs
index 6700acf3..42680c3f 100644
--- a/demos/dotNet/CodeProject.AI.Explorer/Form1.cs
+++ b/demos/dotNet/CodeProject.AI.Explorer/Form1.cs
@@ -8,8 +8,7 @@
using System.Windows.Forms;
using CodeProject.AI.API.Common;
-
-using SkiaSharp;
+using CodeProject.AI.SDK;
using SkiaSharp.Views.Desktop;
namespace CodeProject.AI.Demo.Explorer
@@ -39,7 +38,7 @@ public Form1()
}
private async void Ping(object sender, EventArgs e)
{
- var response = await _AIService.Ping();
+ var response = await _AIService.Ping().ConfigureAwait(false);
if (_serverLive != response.success)
{
_serverLive = response.success;
@@ -153,7 +152,7 @@ private async void DetectFaceBtn_Click(object sender, EventArgs e)
return;
}
- var result = await _AIService.DetectFaces(_imageFileName);
+ var result = await _AIService.DetectFaces(_imageFileName).ConfigureAwait(false);
if (result is DetectFacesResponse detectedFaces)
{
Image? image = GetImage(_imageFileName);
@@ -210,7 +209,8 @@ private async void CompareFacesBtn_Click(object sender, EventArgs e)
return;
}
- var result = await _AIService.MatchFaces(_faceImageFileName1, _faceImageFileName2);
+ var result = await _AIService.MatchFaces(_faceImageFileName1, _faceImageFileName2)
+ .ConfigureAwait(false);
if (result is MatchFacesResponse matchedFaces)
{
detectionResult.Text = $"Similarity: {Math.Round(matchedFaces.similarity, 4)}";
@@ -233,7 +233,7 @@ private async void DetectSceneBtn_Click(object sender, EventArgs e)
return;
}
- var result = await _AIService.DetectScene(_imageFileName);
+ var result = await _AIService.DetectScene(_imageFileName).ConfigureAwait(false);
if (result is DetectSceneResponse detectedScene)
{
var image = GetImage(_imageFileName);
@@ -260,7 +260,7 @@ private async void DetectObjectsBtn_Click(object sender, EventArgs e)
}
Stopwatch stopwatch = Stopwatch.StartNew();
- var result = await _AIService.DetectObjects(_imageFileName);
+ ResponseBase result = await _AIService.DetectObjects(_imageFileName).ConfigureAwait(false);
stopwatch.Stop();
if (result is DetectObjectsResponse detectedObjects)
@@ -336,7 +336,8 @@ private async void RegisterFaceBtn_Click(object sender, EventArgs e)
return;
}
- var result = await _AIService.RegisterFace(UserIdTextbox.Text, _registerFileNames);
+ var result = await _AIService.RegisterFace(UserIdTextbox.Text, _registerFileNames)
+ .ConfigureAwait(false);
if (result is RegisterFaceResponse registeredFace)
SetStatus("Registration complete");
else
@@ -359,7 +360,7 @@ private async void RecognizeFaceBtn_Click(object sender, EventArgs e)
if(float.TryParse(MinConfidence.Text, out float parsedConfidence))
minConfidence = parsedConfidence;
- var result = await _AIService.RecognizeFace(filename, minConfidence);
+ var result = await _AIService.RecognizeFace(filename, minConfidence).ConfigureAwait(false);
if (result is RecognizeFacesResponse recognizeFace)
{
try
@@ -411,7 +412,7 @@ private async void ListFacesBtn_Click(object sender, EventArgs e)
ClearResults();
SetStatus("Listing known faces");
- var result = await _AIService.ListRegisteredFaces();
+ var result = await _AIService.ListRegisteredFaces().ConfigureAwait(false);
if (result is ListRegisteredFacesResponse registeredFaces)
{
if (result?.success ?? false)
@@ -456,7 +457,8 @@ private async void DeleteFaceBtn_Click(object sender, EventArgs e)
ClearResults();
SetStatus("Deleting registered face");
- var result = await _AIService.DeleteRegisteredFace(UserIdTextbox.Text);
+ var result = await _AIService.DeleteRegisteredFace(UserIdTextbox.Text)
+ .ConfigureAwait(false);
if (result?.success ?? false)
SetStatus("Completed Face deletion");
else
@@ -513,7 +515,7 @@ private async void RunBenchmark(bool useCustom)
: _AIService.DetectObjects(_benchmarkFileName);
taskList.Add(task);
}
- await Task.WhenAll(taskList);
+ await Task.WhenAll(taskList).ConfigureAwait(false);
sw.Stop();
BenchmarkResults.Text = $"Benchmark: {Math.Round(nIterations / (sw.ElapsedMilliseconds/ 1000.0), 2)} FPS";
@@ -562,7 +564,7 @@ private void ProcessError(ResponseBase? result)
/// SkiSharp handles more image formats than System.Drawing.
private Image? GetImage(string filename)
{
- var skiaImage = SKImage.FromEncodedData(filename);
+ var skiaImage = ImageUtils.GetImage(filename);
if (skiaImage is null)
return null;
diff --git a/global.json b/global.json
index 9a7235f0..e702da27 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,17 @@
{
"sdk": {
- //"version": "7.0.202",
+ // BE CAREFUL: If you pin the version, then
+ // 1. The SDK you pin it to must be available on every supported platform
+ // That means Windows 10+, macOS (Intel and arm64), Ubuntu 20.04+ and RPi64.
+ // 2. You have to update the docs / article to ensure it clear to developers
+ // which SDK they need
+ // 3. There should be a check in the dev setup script ensuring that users have
+ // this version, or higher, installed on their dev system.
+ //
+ // If you can't do 1 and 2 then you cannot pin the version.
+ //
+ // "version": "7.0.203",
+
//"rollForward": "latestFeature",
"allowPrerelease": false
}
diff --git a/src/API/Common/BackendRouteMap.cs b/src/API/Common/BackendRouteMap.cs
index 5e4fdefa..ff4d059e 100644
--- a/src/API/Common/BackendRouteMap.cs
+++ b/src/API/Common/BackendRouteMap.cs
@@ -148,7 +148,7 @@ public class RouteQueueInfo
public string QueueName { get; private set; }
///
- /// Gets the command identifier which distiguishes the backend operations to perform based
+ /// Gets the command identifier which distinguishes the backend operations to perform based
/// on the frontend endpoint.
///
public string Command { get; private set; }
diff --git a/src/API/Common/ModuleCollection.cs b/src/API/Common/ModuleCollection.cs
index 1883bc0c..55200bb8 100644
--- a/src/API/Common/ModuleCollection.cs
+++ b/src/API/Common/ModuleCollection.cs
@@ -25,9 +25,9 @@ public ModuleCollection() : base(StringComparer.OrdinalIgnoreCase) { }
}
///
- /// Holds information on which versions of a server a given module is compatible with.
+ /// Holds information on a given release of a module.
///
- public class VersionCompatibility
+ public class ModuleRelease
{
///
/// The version of a module
@@ -35,14 +35,24 @@ public class VersionCompatibility
public string? ModuleVersion { get; set; }
///
- /// The Inclusive range of server versions for which this module vresion can be installed on
+ /// The Inclusive range of server versions for which this module version can be installed on
///
public string[]? ServerVersionRange { get; set; }
///
/// The date this version was released
///
- public string? ReleaseDate { get; set; }
+ public string? ReleaseDate { get; set; }
+
+ ///
+ /// Any notes associated with this release
+ ///
+ public string? ReleaseNotes { get; set; }
+
+ ///
+ /// Gets or sets a string indicating how important this update is.
+ ///
+ public string? Importance { get; set; }
}
///
@@ -66,6 +76,12 @@ public class ModuleBase
///
public string[] Platforms { get; set; } = Array.Empty();
+ ///
+ /// Gets or sets the number of MB of memory needed for this module to perform operations.
+ /// If null, then no checks done.
+ ///
+ public int? RequiredMb { get; set; }
+
///
/// Gets or sets the Description for the module.
///
@@ -80,7 +96,16 @@ public class ModuleBase
/// Gets or sets the list of module versions and the server version that matches
/// each of these versions.
///
- public VersionCompatibility[] VersionCompatibililty { get; set; } = Array.Empty();
+ public ModuleRelease[] ModuleReleases { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets the legacy structure containing a list of module versions and the server
+ /// version that matches each of these versions. This name is deprecated and is only here so
+ /// we can read old modulesettings files. Once read these values will be transferred to
+ /// ModuleReleases. Deprecated not just because it was a bad name, but also becauase it was
+ /// a badly *spelled* name.
+ ///
+ public ModuleRelease[] VersionCompatibililty { get; set; } = Array.Empty();
///
/// Gets or sets the current version.
@@ -261,8 +286,16 @@ public string SettingsSummary
{
get
{
+ // Allow the module path to wrap.
+ // var path = ModulePath.Replace("\\", "\\");
+ // path = path.Replace("/", "/");
+
+ // or not...
+ var path = ModulePath;
+
var summary = new StringBuilder();
summary.AppendLine($"Module '{Name}' (ID: {ModuleId})");
+ summary.AppendLine($"Module Path: {path}");
summary.AppendLine($"AutoStart: {AutoStart}");
summary.AppendLine($"Queue: {Queue}");
summary.AppendLine($"Platforms: {string.Join(',', Platforms)}");
@@ -313,7 +346,7 @@ public static class ModuleConfigExtensions
/// 'pre-install' them in situations like a Docker image. We pre-install modules in a
/// separate folder than the downloaded and installed modules in order to avoid conflicts
/// (in Docker) when a user maps a local folder to the modules dir. Doing this to the 'pre
- /// insalled' dir would make the contents (the preinstalled modules) disappear.
+ /// installed' dir would make the contents (the preinstalled modules) disappear.
public static void Initialise(this ModuleConfig module, string moduleId, string modulesPath,
string preInstalledModulesPath)
{
@@ -333,11 +366,16 @@ public static void Initialise(this ModuleConfig module, string moduleId, string
if (module.LogVerbosity == LogVerbosity.Unknown)
module.LogVerbosity = LogVerbosity.Info;
- // Transfer old legacy value to new replacement property if it exists, and no new value was set
+ // Transfer old legacy value to new replacement property if it exists, and no new value
+ // was set
if (module.Activate is not null && module.AutoStart is null)
module.AutoStart = module.Activate;
+ if ((module.VersionCompatibililty?.Length ?? 0) > 0 && (module.ModuleReleases?.Length ?? 0) == 0)
+ module!.ModuleReleases = module!.VersionCompatibililty!;
- module.Activate = null;
+ // No longer used. These properties are still here to allow us to load legacy config files.
+ module.Activate = null;
+ module.VersionCompatibililty = Array.Empty();
}
///
@@ -358,20 +396,20 @@ public static bool Available(this ModuleConfig module, string platform, string?
bool versionOK = string.IsNullOrWhiteSpace(currentServerVersion);
if (!versionOK)
{
- if (module.VersionCompatibililty?.Any() ?? false)
+ if (module.ModuleReleases?.Any() ?? false)
{
- foreach (VersionCompatibility version in module.VersionCompatibililty)
+ foreach (ModuleRelease release in module.ModuleReleases)
{
- if (version.ServerVersionRange is null || version.ServerVersionRange.Length < 2)
+ if (release.ServerVersionRange is null || release.ServerVersionRange.Length < 2)
continue;
- string? minServerVersion = version.ServerVersionRange[0];
- string? maxServerVersion = version.ServerVersionRange[1];
+ string? minServerVersion = release.ServerVersionRange[0];
+ string? maxServerVersion = release.ServerVersionRange[1];
if (string.IsNullOrEmpty(minServerVersion)) minServerVersion = "0.0";
if (string.IsNullOrEmpty(maxServerVersion)) maxServerVersion = currentServerVersion;
- if (version.ModuleVersion == module.Version &&
+ if (release.ModuleVersion == module.Version &&
VersionInfo.Compare(minServerVersion, currentServerVersion) <= 0 &&
VersionInfo.Compare(maxServerVersion, currentServerVersion) >= 0)
{
@@ -380,8 +418,10 @@ public static bool Available(this ModuleConfig module, string platform, string?
}
}
}
- else // old modules will not have VersionCompatibility, but we are backward compatible
+ else // old modules will not have ModuleReleases, but we are backward compatible
+ {
versionOK = true;
+ }
}
// Second check: Is this module available on this platform?
@@ -456,6 +496,49 @@ public static void UpsertSetting(this ModuleConfig module, string name,
}
}
+ /* Not possible until we have this in the same project as ModuleSettings (which is in
+ FrontEnd) or we refactor this extension class into a true class that is initialised
+ with a ref to the ModuleSettings class.
+
+ ///
+ /// Gets a text summary of the settings for this module.
+ ///
+ public static string SettingsSummary(this ModuleConfig module, ModuleSettings moduleSeettings,
+ string? currentModulePath = null)
+ {
+ var summary = new StringBuilder();
+ summary.AppendLine($"Module '{module.Name}' (ID: {module.ModuleId})");
+ summary.AppendLine($"AutoStart: {module.AutoStart}");
+ summary.AppendLine($"Queue: {module.Queue}");
+ summary.AppendLine($"Platforms: {string.Join(',', module.Platforms)}");
+ summary.AppendLine($"GPU: Support {((module.SupportGPU == true)? "enabled" : "disabled")}");
+ summary.AppendLine($"Parallelism: {module.Parallelism}");
+ summary.AppendLine($"Accelerator: {module.AcceleratorDeviceName}");
+ summary.AppendLine($"Half Precis.: {module.HalfPrecision}");
+ summary.AppendLine($"Runtime: {module.Runtime}");
+ summary.AppendLine($"Runtime Loc: {module.RuntimeLocation}");
+ summary.AppendLine($"FilePath: {module.FilePath}");
+ summary.AppendLine($"Pre installed: {module.PreInstalled}");
+ //summary.AppendLine($"Module Dir: {module.ModulePath}");
+ summary.AppendLine($"Start pause: {module.PostStartPauseSecs} sec");
+ summary.AppendLine($"LogVerbosity: {module.LogVerbosity}");
+ summary.AppendLine($"Valid: {module.Valid}");
+ summary.AppendLine($"Environment Variables");
+
+ if (module.EnvironmentVariables is not null)
+ {
+ int maxLength = module.EnvironmentVariables.Max(x => x.Key.ToString().Length);
+ foreach (var envVar in module.EnvironmentVariables)
+ {
+ var value = moduleSettings.ExpandOption(envVar.Value, currentModulePath);
+ summary.AppendLine($" {envVar.Key.PadRight(maxLength)} = {envVar.Value}");
+ }
+ }
+
+ return summary.ToString().Trim();
+ }
+ */
+
///
/// Sets or updates a value in the settings Json structure.
///
@@ -469,63 +552,89 @@ public static bool UpsertSettings(JsonObject? settings, string moduleId,
if (settings is null)
return false;
- if (!settings.ContainsKey("Modules") || settings["Modules"] is null)
- settings["Modules"] = new JsonObject();
-
- JsonObject? allModules = settings["Modules"] as JsonObject;
- allModules ??= new JsonObject();
-
- if (!allModules.ContainsKey(moduleId) || allModules[moduleId] is null)
- allModules[moduleId] = new JsonObject();
-
- var moduleSettings = (JsonObject)allModules[moduleId]!;
-
- // Handle pre-defined global values first
- if (name.EqualsIgnoreCase("Activate") || name.EqualsIgnoreCase("AutoStart"))
- {
- moduleSettings["AutoStart"] = value?.ToLower() == "true";
- }
- else if (name.EqualsIgnoreCase("SupportGPU"))
- {
- moduleSettings["SupportGPU"] = value?.ToLower() == "true";
- }
- else if (name.EqualsIgnoreCase("Parallelism"))
+ // Lots of try/catch since this has been a point of issue and it's good to narrow it down
+ try
{
- if (int.TryParse(value, out int parallelism))
- moduleSettings["Parallelism"] = parallelism;
+ if (!settings.ContainsKey("Modules") || settings["Modules"] is null)
+ settings["Modules"] = new JsonObject();
}
- else if (name.EqualsIgnoreCase("UseHalfPrecision"))
+ catch (Exception e)
{
- moduleSettings["HalfPrecision"] = value;
+ Console.WriteLine($"Failed to create root modules object in settings: {e.Message}");
+ return false;
}
- else if (name.EqualsIgnoreCase("AcceleratorDeviceName"))
+
+ JsonObject? allModules = null;
+ try
{
- moduleSettings["AcceleratorDeviceName"] = value;
+ allModules = settings["Modules"] as JsonObject;
+ allModules ??= new JsonObject();
+
+ if (!allModules.ContainsKey(moduleId) || allModules[moduleId] is null)
+ allModules[moduleId] = new JsonObject();
}
- else if (name.EqualsIgnoreCase("LogVerbosity"))
+ catch (Exception e)
{
- if (Enum.TryParse(value, out LogVerbosity verbosity))
- moduleSettings["LogVerbosity"] = verbosity.ToString();
+ Console.WriteLine($"Failed to create module object in modules collection: {e.Message}");
+ return false;
}
- else if (name.EqualsIgnoreCase("PostStartPauseSecs"))
+
+ try
{
- if (int.TryParse(value, out int pauseSec))
- moduleSettings["PostStartPauseSecs"] = pauseSec;
+ var moduleSettings = (JsonObject)allModules[moduleId]!;
+
+ // Handle pre-defined global values first
+ if (name.EqualsIgnoreCase("Activate") || name.EqualsIgnoreCase("AutoStart"))
+ {
+ moduleSettings["AutoStart"] = value?.ToLower() == "true";
+ }
+ else if (name.EqualsIgnoreCase("SupportGPU"))
+ {
+ moduleSettings["SupportGPU"] = value?.ToLower() == "true";
+ }
+ else if (name.EqualsIgnoreCase("Parallelism"))
+ {
+ if (int.TryParse(value, out int parallelism))
+ moduleSettings["Parallelism"] = parallelism;
+ }
+ else if (name.EqualsIgnoreCase("UseHalfPrecision"))
+ {
+ moduleSettings["HalfPrecision"] = value;
+ }
+ else if (name.EqualsIgnoreCase("AcceleratorDeviceName"))
+ {
+ moduleSettings["AcceleratorDeviceName"] = value;
+ }
+ else if (name.EqualsIgnoreCase("LogVerbosity"))
+ {
+ if (Enum.TryParse(value, out LogVerbosity verbosity))
+ moduleSettings["LogVerbosity"] = verbosity.ToString();
+ }
+ else if (name.EqualsIgnoreCase("PostStartPauseSecs"))
+ {
+ if (int.TryParse(value, out int pauseSec))
+ moduleSettings["PostStartPauseSecs"] = pauseSec;
+ }
+ else
+ {
+ if (moduleSettings["EnvironmentVariables"] is null)
+ moduleSettings["EnvironmentVariables"] = new JsonObject();
+
+ var environmentVars = (JsonObject)moduleSettings["EnvironmentVariables"]!;
+ environmentVars[name.ToUpper()] = value;
+ }
+
+ // Clean up legacy values
+ if (moduleSettings["Activate"] is not null && moduleSettings["AutoStart"] is null)
+ moduleSettings["AutoStart"] = moduleSettings["Activate"];
+ moduleSettings.Remove("Activate");
}
- else
+ catch (Exception e)
{
- if (moduleSettings["EnvironmentVariables"] is null)
- moduleSettings["EnvironmentVariables"] = new JsonObject();
-
- var environmentVars = (JsonObject)moduleSettings["EnvironmentVariables"]!;
- environmentVars[name.ToUpper()] = value;
+ Console.WriteLine($"Failed to update module setting: {e.Message}");
+ return false;
}
- // Clean up legacy values
- if (moduleSettings["Activate"] is not null && moduleSettings["AutoStart"] is null)
- moduleSettings["AutoStart"] = moduleSettings["Activate"];
- moduleSettings.Remove("Activate");
-
return true;
}
@@ -570,7 +679,7 @@ public static void AddEnvironmentVariables(this ModuleConfig module,
if (string.IsNullOrWhiteSpace(dir))
return new JsonObject();
- string content = await File.ReadAllTextAsync(path);
+ string content = await File.ReadAllTextAsync(path).ConfigureAwait(false);
// var settings = JsonSerializer.Deserialize>(content);
var settings = JsonSerializer.Deserialize(content);
@@ -588,7 +697,7 @@ public static void AddEnvironmentVariables(this ModuleConfig module,
/// This set of module settings
/// The path to save
/// true on success; false otherwise
- public async static Task SaveSettings(JsonObject? settings, string path)
+ public async static Task SaveSettingsAsync(JsonObject? settings, string path)
{
if (settings is null || string.IsNullOrWhiteSpace(path))
return false;
@@ -605,7 +714,7 @@ public async static Task SaveSettings(JsonObject? settings, string path)
var options = new JsonSerializerOptions { WriteIndented = true };
string configJson = JsonSerializer.Serialize(settings, options);
- await File.WriteAllTextAsync(path, configJson);
+ await File.WriteAllTextAsync(path, configJson).ConfigureAwait(false);
return true;
}
@@ -659,7 +768,7 @@ public async static Task SaveAllSettings(this ModuleCollection modules, st
var options = new JsonSerializerOptions { WriteIndented = true };
string configJson = JsonSerializer.Serialize(modules, options);
- await File.WriteAllTextAsync(path, configJson);
+ await File.WriteAllTextAsync(path, configJson).ConfigureAwait(false);
return true;
}
@@ -691,22 +800,22 @@ public async static Task CreateModulesListing(this ModuleCollection module
var moduleList = modules.Values
.OrderBy(m => m.ModuleId)
.Select(m => new {
- ModuleId = m.ModuleId,
- Name = m.Name,
- Version = m.Version,
- Description = m.Description,
- Platforms = m.Platforms,
- Runtime = m.Runtime,
- VersionCompatibililty = m.VersionCompatibililty,
- License = m.License,
- LicenseUrl = m.LicenseUrl,
- Downloads = 0
+ ModuleId = m.ModuleId,
+ Name = m.Name,
+ Version = m.Version,
+ Description = m.Description,
+ Platforms = m.Platforms,
+ Runtime = m.Runtime,
+ ModuleReleases = m.ModuleReleases,
+ License = m.License,
+ LicenseUrl = m.LicenseUrl,
+ Downloads = 0
});
var options = new JsonSerializerOptions { WriteIndented = true };
string configJson = JsonSerializer.Serialize(moduleList, options);
- await File.WriteAllTextAsync(path, configJson);
+ await File.WriteAllTextAsync(path, configJson).ConfigureAwait(false);
return true;
}
diff --git a/src/API/Common/ModuleDescription.cs b/src/API/Common/ModuleDescription.cs
index 5183c621..f4dafd59 100644
--- a/src/API/Common/ModuleDescription.cs
+++ b/src/API/Common/ModuleDescription.cs
@@ -115,16 +115,11 @@ public class ModuleDescription : ModuleBase
public int Downloads { get; set; }
///
- /// Gets or sets the version of this module currently installed. This value is not
- /// deserialised, but instead must be set by the server.
+ /// Gets or sets the ModuleRelease of the latest release of this module that is compatible
+ /// with the current server. This value is not deserialised, but instead must be set by the
+ /// server.
///
- public string? CurrentInstalledVersion { get; set; }
-
- ///
- /// Gets or sets the latest version of this module that is compatible with the current
- /// server. This value is not deserialised, but instead must be set by the server.
- ///
- public string? LatestCompatibleVersion { get; set; }
+ public ModuleRelease? LatestRelease { get; set; }
///
/// Gets or sets the release date of the latest compatible version of this module
@@ -184,9 +179,9 @@ public static void Initialise(this ModuleDescription module, string currentServe
SetLatestCompatibleVersion(module, currentServerVersion);
// Set the status of all entries based on availability on this platform
- module.Status = string.IsNullOrWhiteSpace(module.LatestCompatibleVersion)
+ module.Status = string.IsNullOrWhiteSpace(module?.LatestRelease?.ModuleVersion)
|| !module.IsAvailable(SystemInfo.Platform, currentServerVersion)
- ? ModuleStatusType.NotAvailable : ModuleStatusType.Available;
+ ? ModuleStatusType.NotAvailable : ModuleStatusType.Available;
}
///
@@ -203,13 +198,14 @@ public static bool IsAvailable(this ModuleDescription module, string platform, s
return false;
// First check: Is there a version of this module that's compatible with the current server?
- if (serverVersion is not null && string.IsNullOrWhiteSpace(module.LatestCompatibleVersion))
- SetLatestCompatibleVersion(module, serverVersion);
+ if (serverVersion is not null && string.IsNullOrWhiteSpace(module?.LatestRelease?.ModuleVersion))
+ SetLatestCompatibleVersion(module!, serverVersion);
- bool versionOK = serverVersion is null || !string.IsNullOrWhiteSpace(module.LatestCompatibleVersion);
+ bool versionOK = serverVersion is null ||
+ !string.IsNullOrWhiteSpace(module?.LatestRelease?.ModuleVersion);
// Second check: Is this module available on this platform?
- return module.Valid && versionOK &&
+ return module!.Valid && versionOK &&
( module.Platforms!.Any(p => p.EqualsIgnoreCase("all")) ||
module.Platforms!.Any(p => p.EqualsIgnoreCase(platform)) );
}
@@ -219,33 +215,34 @@ private static void SetLatestCompatibleVersion(ModuleDescription module, string
// HACK: To be removed after CPAI 2.1 is released. The Versions array wasn't added to
// the downloadable list of modules until server version 2.1. All modules pre-server
// 2.1 are compatible with server 2.1+, so
- if (module.VersionCompatibililty is null || module.VersionCompatibililty.Count() == 0)
+ if ((module.ModuleReleases?.Length ?? 0) == 0)
{
- module.LatestCompatibleVersion = module.Version;
- module.CompatibleVersionReleaseDate = "2022-03-20";
+ module.LatestRelease = new ModuleRelease()
+ {
+ ModuleVersion = module.Version,
+ ReleaseDate = "2022-03-20"
+ };
+ return;
}
- else
+
+ foreach (ModuleRelease release in module!.ModuleReleases!)
{
- foreach (VersionCompatibility version in module.VersionCompatibililty)
- {
- if (version.ServerVersionRange is null || version.ServerVersionRange.Length < 2)
- continue;
+ if (release.ServerVersionRange is null || release.ServerVersionRange.Length < 2)
+ continue;
- string? minServerVersion = version.ServerVersionRange[0];
- string? maxServerVersion = version.ServerVersionRange[1];
+ string? minServerVersion = release.ServerVersionRange[0];
+ string? maxServerVersion = release.ServerVersionRange[1];
- if (string.IsNullOrEmpty(minServerVersion)) minServerVersion = "0.0";
- if (string.IsNullOrEmpty(maxServerVersion)) maxServerVersion = currentServerVersion;
+ if (string.IsNullOrEmpty(minServerVersion)) minServerVersion = "0.0";
+ if (string.IsNullOrEmpty(maxServerVersion)) maxServerVersion = currentServerVersion;
- if (VersionInfo.Compare(minServerVersion, currentServerVersion) <= 0 &&
- VersionInfo.Compare(maxServerVersion, currentServerVersion) >= 0)
+ if (VersionInfo.Compare(minServerVersion, currentServerVersion) <= 0 &&
+ VersionInfo.Compare(maxServerVersion, currentServerVersion) >= 0)
+ {
+ if (module.LatestRelease is null ||
+ VersionInfo.Compare(module.LatestRelease.ModuleVersion, release.ModuleVersion) <= 0)
{
- if (module.LatestCompatibleVersion is null ||
- VersionInfo.Compare(module.LatestCompatibleVersion, version.ModuleVersion) <= 0)
- {
- module.LatestCompatibleVersion = version.ModuleVersion;
- module.CompatibleVersionReleaseDate = version.ReleaseDate;
- }
+ module.LatestRelease = release;
}
}
}
diff --git a/src/API/Common/ProcessStatus.cs b/src/API/Common/ProcessStatus.cs
index 8ef11b45..1fd1e80b 100644
--- a/src/API/Common/ProcessStatus.cs
+++ b/src/API/Common/ProcessStatus.cs
@@ -88,6 +88,11 @@ public class ProcessStatus
///
public string? ModuleId { get; set; }
+ ///
+ /// Gets or sets the name of the queue this module is processing
+ ///
+ public string? Queue { get; set; }
+
///
/// Gets or sets the module name
///
@@ -155,8 +160,15 @@ public string Summary
StringBuilder summary = new StringBuilder();
// summary.AppendLine($"Process '{Name}' (ID: {ModuleId})");
- summary.AppendLine($"Started: {Started?.ToLocalTime().ToString("dd-MMM-yyyy h:mm:ss tt")}");
- summary.AppendLine($"LastSeen: {LastSeen?.ToLocalTime().ToString("dd-MMM-yyyy h:mm:ss tt")}");
+ string timezone = TimeZoneInfo.Local.StandardName;
+ string format = "dd MMM yyyy h:mm:ss tt";
+ string started = (Started is null) ? "Not seen"
+ : Started.Value.ToLocalTime().ToString(format) + " " + timezone;
+ string lastSeen = (LastSeen is null) ? "Not seen"
+ : LastSeen.Value.ToLocalTime().ToString(format) + " " + timezone;
+
+ summary.AppendLine($"Started: {started}");
+ summary.AppendLine($"LastSeen: {lastSeen}");
summary.AppendLine($"Status: {Status}");
summary.AppendLine($"Processed: {Processed}");
summary.AppendLine($"Provider: {ExecutionProvider}");
diff --git a/src/API/Server/Backend/CommandDispatcher.cs b/src/API/Server/Backend/CommandDispatcher.cs
index ef73c46c..84754dab 100644
--- a/src/API/Server/Backend/CommandDispatcher.cs
+++ b/src/API/Server/Backend/CommandDispatcher.cs
@@ -66,7 +66,8 @@ public async Task
/// The module to install
/// The version of the module to install
+ /// Whether or not to ignore the download cache. If true, the module
+ /// will always be freshly downloaded
/// A Tuple containing true for success; false otherwise, and a string containing
/// the error message if the operation was not successful.
- public async Task<(bool, string)> DownloadAndInstallModuleAsync(string moduleId, string version)
+ public async Task<(bool, string)> DownloadAndInstallModuleAsync(string moduleId,
+ string version,
+ bool noCache = false)
{
// if (SystemInfo.RuntimeEnvironment == RuntimeEnvironment.Development)
// return (false, $"Can't install modules when running in Development");
@@ -370,7 +375,7 @@ public async Task> GetDownloadableModules()
_logger.LogInformation($"Preparing to install module '{moduleId}'");
- ModuleDescription? moduleDownload = await GetCurrentModuleDescription(moduleId);
+ ModuleDescription? moduleDownload = await GetCurrentModuleDescription(moduleId).ConfigureAwait(false);
if (moduleDownload is null)
return (false, $"Unable to find the download info for '{moduleId}'");
@@ -391,10 +396,10 @@ public async Task> GetDownloadableModules()
if (module is not null && module.Valid)
{
if (VersionInfo.Compare(moduleDownload.Version, module.Version) <= 0)
- return (false, $"The same, or a newer version, of Module {moduleId} is already installed");
+ return (false, $"{moduleId} is already installed");
// If current module is a lower version then uninstall first
- (bool success, string uninstallError) = await UninstallModuleAsync(moduleId);
+ (bool success, string uninstallError) = await UninstallModuleAsync(moduleId).ConfigureAwait(false);
if (!success)
return (false, $"Unable to uninstall older version of {moduleId}: {uninstallError}");
}
@@ -411,15 +416,15 @@ public async Task> GetDownloadableModules()
bool downloaded = false;
string error = string.Empty;
- if (System.IO.File.Exists(downloadPath))
+ if (!noCache && System.IO.File.Exists(downloadPath))
{
_logger.LogInformation($" (using cached download for '{moduleId}')");
downloaded = true;
}
else
{
- (downloaded, error) = await _packageDownloader.DownloadFileAsync(moduleDownload.DownloadUrl!,
- downloadPath);
+ (downloaded, error) = await _packageDownloader.DownloadFileAsync(moduleDownload.DownloadUrl!, downloadPath)
+ .ConfigureAwait(false);
}
if (downloaded && !System.IO.File.Exists(downloadPath))
@@ -435,7 +440,7 @@ public async Task> GetDownloadableModules()
return (false, $"Unable to download module '{moduleId}' from {moduleDownload.DownloadUrl}. Error: {error}");
}
- return await InstallModuleAsync(downloadPath, moduleId);
+ return await InstallModuleAsync(downloadPath, moduleId).ConfigureAwait(false);
}
///
@@ -464,7 +469,7 @@ public async Task> GetDownloadableModules()
}
else
{
- moduleDownload = await GetCurrentModuleDescription(moduleId);
+ moduleDownload = await GetCurrentModuleDescription(moduleId).ConfigureAwait(false);
if (moduleDownload is not null)
{
moduleDownload.Status = ModuleStatusType.Unpacking;
@@ -505,7 +510,8 @@ to work. Doing it manually means we are assuming a modulesettings.json always
string? settingsModuleId = null;
try
{
- string content = await File.ReadAllTextAsync(Path.Combine(moduleDir, "modulesettings.json"));
+ string content = await File.ReadAllTextAsync(Path.Combine(moduleDir, "modulesettings.json"))
+ .ConfigureAwait(false);
var documentOptions = new JsonDocumentOptions
{
@@ -572,7 +578,7 @@ to work. Doing it manually means we are assuming a modulesettings.json always
moduleDownload.Status = ModuleStatusType.Installing;
ProcessStartInfo procStartInfo;
- if (SystemInfo.OperatingSystem.EqualsIgnoreCase("Windows"))
+ if (SystemInfo.IsWindows)
procStartInfo = new ProcessStartInfo(_moduleSettings.ModuleInstallerScriptPath);
else
procStartInfo = new ProcessStartInfo("bash", _moduleSettings.ModuleInstallerScriptPath);
@@ -609,18 +615,20 @@ to work. Doing it manually means we are assuming a modulesettings.json always
// Wait for the Process to complete before exiting the method or else the
// Process may be killed at some random time when the process variable is GC.
- using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
- await process.WaitForExitAsync(cts.Token);
+ using var cts = new CancellationTokenSource(_moduleOptions.ModuleInstallTimeout);
+ await process.WaitForExitAsync(cts.Token).ConfigureAwait(false);
_logger.LogInformation($"Installer exited with code {process.ExitCode}");
- await logWriter.WriteLineAsync($"Installer exited with code {process.ExitCode}");
+ await logWriter.WriteLineAsync($"Installer exited with code {process.ExitCode}")
+ .ConfigureAwait(false);
}
else
{
if (moduleDownload is not null)
moduleDownload.Status = ModuleStatusType.FailedInstall;
- await logWriter.WriteLineAsync($"Unable to start the Module installer for '{moduleId}'");
+ await logWriter.WriteLineAsync($"Unable to start the Module installer for '{moduleId}'")
+ .ConfigureAwait(false);
return (false, $"Unable to start the Module installer for '{moduleId}'");
}
}
@@ -632,7 +640,8 @@ to work. Doing it manually means we are assuming a modulesettings.json always
if (moduleDownload is not null)
moduleDownload.Status = ModuleStatusType.FailedInstall;
- await logWriter.WriteLineAsync($"Timed out attempting to install Module '{moduleId}'");
+ await logWriter.WriteLineAsync($"Timed out attempting to install Module '{moduleId}'")
+ .ConfigureAwait(false);
return (false, $"Timed out attempting to install Module '{moduleId}' (${e.Message})");
}
catch (Exception e)
@@ -640,7 +649,8 @@ to work. Doing it manually means we are assuming a modulesettings.json always
if (moduleDownload is not null)
moduleDownload.Status = ModuleStatusType.FailedInstall;
- await logWriter.WriteLineAsync($"Unable to install Module '{moduleId}' (${e.Message})");
+ await logWriter.WriteLineAsync($"Unable to install Module '{moduleId}' (${e.Message})")
+ .ConfigureAwait(false);
return (false, $"Unable to install Module '{moduleId}' (${e.Message})");
}
@@ -673,7 +683,7 @@ to work. Doing it manually means we are assuming a modulesettings.json always
if (!Directory.Exists(moduleDir))
return (false, $"Unable to find {moduleId}'s install directory {moduleDir ?? "null"}");
- ModuleDescription? moduleDownload = await GetCurrentModuleDescription(moduleId);
+ ModuleDescription? moduleDownload = await GetCurrentModuleDescription(moduleId).ConfigureAwait(false);
// If the module to be uninstalled is no longer a download, create an entry and add it
// to the download list so at least we can provide updates on it disappearing.
@@ -692,12 +702,12 @@ to work. Doing it manually means we are assuming a modulesettings.json always
// Console.WriteLine("Setting ModuleStatusType.Uninstalling");
moduleDownload.Status = ModuleStatusType.Uninstalling;
- if (!await _moduleProcessService.KillProcess(module))
+ if (!await _moduleProcessService.KillProcess(module).ConfigureAwait(false))
{
Console.WriteLine("Setting ModuleStatusType.Unknown");
RefreshDownloadableModuleList();
- moduleDownload = await GetCurrentModuleDescription(moduleId);
+ moduleDownload = await GetCurrentModuleDescription(moduleId).ConfigureAwait(false);
if (moduleDownload is not null)
moduleDownload.Status = ModuleStatusType.Unknown;
@@ -711,7 +721,7 @@ to work. Doing it manually means we are assuming a modulesettings.json always
Directory.Delete(moduleDir, true);
Console.WriteLine("Setting newly deleted module to ModuleStatusType.Available");
- moduleDownload = await GetCurrentModuleDescription(moduleId);
+ moduleDownload = await GetCurrentModuleDescription(moduleId).ConfigureAwait(false);
if (moduleDownload is not null)
moduleDownload.Status = ModuleStatusType.Available;
}
@@ -719,13 +729,13 @@ to work. Doing it manually means we are assuming a modulesettings.json always
{
_logger.LogError($"Unable to delete install folder for {moduleId} ({e.Message})");
_logger.LogInformation("Will wait a moment: sometimes a delete just needs time to complete");
- await Task.Delay(3);
+ await Task.Delay(3).ConfigureAwait(false);
}
if (Directory.Exists(moduleDir)) // shouldn't actually be possible to get here if delete failed
{
Console.WriteLine("Setting ModuleStatusType.UninstallFailed");
- moduleDownload = await GetCurrentModuleDescription(moduleId);
+ moduleDownload = await GetCurrentModuleDescription(moduleId).ConfigureAwait(false);
if (moduleDownload is not null)
moduleDownload.Status = ModuleStatusType.UninstallFailed;
@@ -735,7 +745,7 @@ to work. Doing it manually means we are assuming a modulesettings.json always
}
if (_moduleCollection.ContainsKey(moduleId) &&
- !_moduleCollection.TryRemove(moduleId, out ModuleConfig _))
+ !_moduleCollection.TryRemove(moduleId, out _))
{
if (moduleDownload is not null)
moduleDownload.Status = ModuleStatusType.UninstallFailed;
@@ -762,7 +772,7 @@ to work. Doing it manually means we are assuming a modulesettings.json always
/// A ModuleDescription object, or null if not found.
private async Task GetCurrentModuleDescription(string moduleId)
{
- List moduleList = await GetDownloadableModules();
+ List moduleList = await GetDownloadableModules().ConfigureAwait(false);
return moduleList.FirstOrDefault(m => m.ModuleId?.EqualsIgnoreCase(moduleId) == true);
}
@@ -772,7 +782,10 @@ private void SendOutputToLog(TextWriter log, object sender, DataReceivedEventArg
if (!string.IsNullOrWhiteSpace(message))
{
- log.WriteLine(Text.StripXTermColors(message));
+ message = Text.StripSpinnerChars(message);
+
+ string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss: ");
+ log.WriteLine(timestamp + Text.StripXTermColors(message));
string? moduleId = GetModuleIdFromEventSender(sender);
if (moduleId is not null)
@@ -788,7 +801,10 @@ private void SendErrorToLog(TextWriter log, object sender, DataReceivedEventArgs
if (!string.IsNullOrWhiteSpace(message))
{
- log.WriteLine(Text.StripXTermColors(message));
+ message = Text.StripSpinnerChars(message);
+
+ string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss: ");
+ log.WriteLine(timestamp + Text.StripXTermColors(message));
string? moduleId = GetModuleIdFromEventSender(sender);
if (moduleId is not null)
@@ -797,7 +813,7 @@ private void SendErrorToLog(TextWriter log, object sender, DataReceivedEventArgs
_logger.LogError(message);
}
}
-
+
///
/// Gets a module ID from an event
///
@@ -828,7 +844,7 @@ private async void ModuleInstallComplete(object? sender, EventArgs e)
return;
}
- ModuleDescription? moduleDownload = await GetCurrentModuleDescription(moduleId);
+ ModuleDescription? moduleDownload = await GetCurrentModuleDescription(moduleId).ConfigureAwait(false);
if (moduleDownload is null)
{
_logger.LogError("Unable to find recently installed module in downloadable module list");
@@ -862,7 +878,7 @@ private async void ModuleInstallComplete(object? sender, EventArgs e)
_moduleCollection.TryAdd(moduleId, moduleConfig);
// StartProcess does more than just start the module wo we need to call it
// even if the module's AutoStart is false.
- if (await _moduleProcessService.StartProcess(moduleConfig))
+ if (await _moduleProcessService.StartProcess(moduleConfig).ConfigureAwait(false))
_logger.LogInformation($"Module {moduleId} started successfully.");
else if(!(moduleConfig.AutoStart ?? false))
_logger.LogInformation($"Module {moduleId} not configured to AutoStart.");
diff --git a/src/API/Server/FrontEnd/AiModules/AiModuleRunner.cs b/src/API/Server/FrontEnd/AiModules/AiModuleRunner.cs
index 4f4c70cf..3d23e003 100644
--- a/src/API/Server/FrontEnd/AiModules/AiModuleRunner.cs
+++ b/src/API/Server/FrontEnd/AiModules/AiModuleRunner.cs
@@ -130,6 +130,7 @@ public AiModuleRunner(IOptions versionOptions,
{
ModuleId = module.ModuleId,
Name = module.Name,
+ Queue = module.Queue,
Status = status
});
}
@@ -149,7 +150,7 @@ public AiModuleRunner(IOptions versionOptions,
public async override Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogTrace("ModuleRunner Start");
- await base.StartAsync(cancellationToken);
+ await base.StartAsync(cancellationToken).ConfigureAwait(false);
}
///
@@ -161,15 +162,15 @@ public override async Task StopAsync(CancellationToken cancellationToken)
foreach (var module in _modules.Values)
tasks.Add(KillProcess(module));
- await Task.WhenAll(tasks);
+ await Task.WhenAll(tasks).ConfigureAwait(false);
- await base.StopAsync(cancellationToken);
+ await base.StopAsync(cancellationToken).ConfigureAwait(false);
}
///
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
- await Task.Delay(100); // let everything else start up as well
+ await Task.Delay(100).ConfigureAwait(false); // let everything else start up as well
if (_modules is null)
{
@@ -207,7 +208,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
else {
// Let's make sure the front end is up and running before we start the backend
// analysis services
- await Task.Delay(TimeSpan.FromSeconds(preLaunchModuleDelaySecs), stoppingToken);
+ await Task.Delay(TimeSpan.FromSeconds(preLaunchModuleDelaySecs), stoppingToken)
+ .ConfigureAwait(false);
foreach (var entry in _modules!)
{
@@ -224,19 +226,16 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
if (status == null)
continue;
- await StartProcess(module);
+ await StartProcess(module).ConfigureAwait(false);
}
}
// Install Initial Modules last so already installed modules will run
// while the installations are happening.
- if (SystemInfo.ExecutionEnvironment != ExecutionEnvironment.Docker)
- {
- _logger.LogInformation("Installing Initial Modules.");
- await _moduleInstaller.InstallInitialModules();
- }
+ if (!SystemInfo.IsDocker)
+ await _moduleInstaller.InstallInitialModules().ConfigureAwait(false);
- await Task.Delay(Timeout.Infinite, stoppingToken);
+ await Task.Delay(Timeout.Infinite, stoppingToken).ConfigureAwait(false);
_logger.LogInformation("ModuleRunner Stopped");
}
diff --git a/src/API/Server/FrontEnd/AiModules/ModuleProcessServices.cs b/src/API/Server/FrontEnd/AiModules/ModuleProcessServices.cs
index fd5c2d8d..0b152c09 100644
--- a/src/API/Server/FrontEnd/AiModules/ModuleProcessServices.cs
+++ b/src/API/Server/FrontEnd/AiModules/ModuleProcessServices.cs
@@ -118,7 +118,7 @@ public IEnumerable ListProcessStatuses()
public bool RemoveProcessStatus(string moduleId)
{
if (_processStatuses.ContainsKey(moduleId) &&
- !_processStatuses.TryRemove(moduleId, out ProcessStatus _))
+ !_processStatuses.TryRemove(moduleId, out _))
{
return false;
}
@@ -160,11 +160,15 @@ public async Task KillProcess(ModuleConfig module)
// Send a 'Quit' request but give it time to wrap things up before we step in further
var payload = new RequestPayload("Quit");
payload.SetValue("moduleId", module.ModuleId);
- await _queueServices.SendRequestAsync(module.Queue!, new BackendRequest(payload));
+ await _queueServices.SendRequestAsync(module.Queue!, new BackendRequest(payload))
+ .ConfigureAwait(false);
int shutdownServerDelaySecs = _moduleSettings.DelayAfterStoppingModulesSecs;
if (shutdownServerDelaySecs > 0)
- await Task.Delay(TimeSpan.FromSeconds(shutdownServerDelaySecs));
+ {
+ await Task.Delay(TimeSpan.FromSeconds(shutdownServerDelaySecs))
+ .ConfigureAwait(false);
+ }
try
{
@@ -172,7 +176,14 @@ public async Task KillProcess(ModuleConfig module)
{
_logger.LogInformation($"Forcing shutdown of {process.ProcessName}/{module.ModuleId}");
process.Kill(true);
- await process.WaitForExitAsync();
+
+ Stopwatch stopWatch = Stopwatch.StartNew();
+ _logger.LogDebug($"Waiting for {module.ModuleId} to end.");
+
+ await process.WaitForExitAsync().ConfigureAwait(false);
+
+ stopWatch.Stop();
+ _logger.LogDebug($"{module.ModuleId} ended after {stopWatch.ElapsedMilliseconds} ms");
}
else
_logger.LogInformation($"{module.ModuleId} went quietly");
@@ -213,6 +224,7 @@ public async Task StartProcess(ModuleConfig module)
{
ModuleId = module.ModuleId,
Name = module.Name,
+ Queue = module.Queue,
Status = ProcessStatusType.Unknown
};
_processStatuses.TryAdd(module.ModuleId, status);
@@ -235,7 +247,8 @@ public async Task StartProcess(ModuleConfig module)
{
ProcessStartInfo procStartInfo = CreateProcessStartInfo(module);
- _logger.LogInformation($"Attempting to start {module.ModuleId} with {procStartInfo.FileName} {procStartInfo.Arguments}");
+ _logger.LogDebug("");
+ _logger.LogDebug($"Attempting to start {module.ModuleId} with {procStartInfo.FileName} {procStartInfo.Arguments}");
process = new Process
{
@@ -279,7 +292,7 @@ public async Task StartProcess(ModuleConfig module)
// Trying to reduce startup CPU and instantaneous memory use for low resource
// environments such as Docker or RPi
- await Task.Delay(TimeSpan.FromSeconds(postStartPauseSecs));
+ await Task.Delay(TimeSpan.FromSeconds(postStartPauseSecs)).ConfigureAwait(false);
status.Status = ProcessStatusType.Started;
}
else
@@ -296,7 +309,7 @@ public async Task StartProcess(ModuleConfig module)
_logger.LogError(ex.StackTrace);
#if DEBUG
_logger.LogError($" *** Did you setup the Development environment?");
- if (SystemInfo.Platform == "Windows")
+ if (SystemInfo.IsWindows)
_logger.LogError($" Run \\src\\setup.bat");
else
_logger.LogError($" In /src, run 'bash setup.sh'");
@@ -308,7 +321,7 @@ public async Task StartProcess(ModuleConfig module)
}
if (process is null)
- _runningProcesses.TryRemove(module.ModuleId, out Process _);
+ _runningProcesses.TryRemove(module.ModuleId, out _);
return process != null;
}
@@ -335,7 +348,7 @@ public async Task RestartProcess(ModuleConfig module)
if (_runningProcesses.TryGetValue(module.ModuleId, out Process? process) && process != null)
{
status.Status = ProcessStatusType.Stopping;
- await KillProcess(module);
+ await KillProcess(module).ConfigureAwait(false);
status.Status = ProcessStatusType.Stopped;
}
else
@@ -345,7 +358,7 @@ public async Task RestartProcess(ModuleConfig module)
if (module.AutoStart == false || !module.Available(SystemInfo.Platform, _versionConfig.VersionInfo?.Version))
return true;
- return await StartProcess(module);
+ return await StartProcess(module).ConfigureAwait(false);
}
///
@@ -415,7 +428,7 @@ private ProcessStartInfo CreateProcessStartInfo(ModuleConfig module)
string filePath = _moduleSettings.GetFilePath(module);
string? command = _moduleSettings.GetCommandPath(module);
- _logger.LogDebug($"Command : {command}");
+ _logger.LogTrace($"Command: {command}");
// Setup the process we're going to launch
#if Windows
@@ -470,17 +483,41 @@ private void SendOutputToLog(object sender, DataReceivedEventArgs data)
if (string.IsNullOrWhiteSpace(message))
return;
- // if (string.IsNullOrEmpty(filename))
- // filename = "Process";
-
if (!string.IsNullOrEmpty(filename))
filename += ": ";
- // Force ditch the MS logging scoping headings
- if (!message.StartsWith("info: ") && !message.EndsWith("[0]"))
+ var testString = message.ToLower();
+
+ // We're picking up messages written to the console so let's provide a little help for
+ // messages that are trying to get themselves categorised properly.
+ // Optimisation: We probably should order these by info/trace/debug/warn/error/crit, but
+ // for sanity we'll keep them in order of anxiety.
+ if (testString.StartsWith("crit: "))
+ _logger.LogCritical(filename + message.Substring("crit: ".Length));
+ else if (testString.StartsWith("critical: "))
+ _logger.LogCritical(filename + message.Substring("critical: ".Length));
+ else if (testString.StartsWith("err: "))
+ _logger.LogError(filename + message.Substring("err: ".Length));
+ else if (testString.StartsWith("error: "))
+ _logger.LogError(filename + message.Substring("error: ".Length));
+ else if (testString.StartsWith("warn: "))
+ _logger.LogWarning(filename + message.Substring("warn: ".Length));
+ else if (testString.StartsWith("warning: "))
+ _logger.LogWarning(filename + message.Substring("warning: ".Length));
+ else if (testString.StartsWith("info: "))
+ _logger.LogInformation(filename + message.Substring("info: ".Length));
+ else if (testString.StartsWith("information: "))
+ _logger.LogInformation(filename + message.Substring("information: ".Length));
+ else if (testString.StartsWith("dbg: "))
+ _logger.LogDebug(filename + message.Substring("dbg: ".Length));
+ else if (testString.StartsWith("debug: "))
+ _logger.LogDebug(filename + message.Substring("debug: ".Length));
+ else if (testString.StartsWith("trc: "))
+ _logger.LogTrace(filename + message.Substring("trc: ".Length));
+ else if (testString.StartsWith("trace: "))
+ _logger.LogTrace(filename + message.Substring("trace: ".Length));
+ else
_logger.LogInformation(filename + message);
-
- // Console.WriteLine("REDIRECT STDOUT: " + filename + message);
}
private void SendErrorToLog(object sender, DataReceivedEventArgs data)
@@ -502,31 +539,13 @@ private void SendErrorToLog(object sender, DataReceivedEventArgs data)
if (string.IsNullOrWhiteSpace(error))
return;
- // if (string.IsNullOrEmpty(filename))
- // filename = "Process";
if (!string.IsNullOrEmpty(filename))
filename += ": ";
if (string.IsNullOrEmpty(error))
error = "No error provided";
- if (error.Contains("LoadLibrary failed with error 126") &&
- error.Contains("onnxruntime_providers_cuda.dll"))
- {
- error = "Attempted to load ONNX runtime CUDA provider. No luck, moving on...";
- _logger.LogInformation(filename + error);
- }
- else if (error != "info: Microsoft.Hosting.Lifetime[0]")
- {
- // TOTAL HACK. ONNX/Tensorflow output is WAY too verbose for an error
- if (error.Contains("I tensorflow/cc/saved_model/reader.cc:") ||
- error.Contains("I tensorflow/cc/saved_model/loader.cc:"))
- _logger.LogInformation(filename + error);
- else
- _logger.LogError(filename + error);
- };
-
- // Console.WriteLine("REDIRECT ERROR: " + filename + error);
+ _logger.LogError(filename + error);
}
///
@@ -546,11 +565,11 @@ private void ModuleExecutionComplete(object? sender, EventArgs e)
return;
}
- _logger.LogInformation($"Module {moduleId} has shutdown");
+ _logger.LogInformation($"** Module {moduleId} has shutdown");
// Remove this from the list of running processes
- if (_runningProcesses.TryGetValue(moduleId, out Process _))
- _runningProcesses.TryRemove(moduleId, out Process _);
+ if (_runningProcesses.TryGetValue(moduleId, out _))
+ _runningProcesses.TryRemove(moduleId, out _);
}
}
@@ -584,13 +603,27 @@ private void ModuleExecutionComplete(object? sender, EventArgs e)
processEnvironmentVars.TryAdd("CPAI_MODULE_ID", module.ModuleId);
processEnvironmentVars.TryAdd("CPAI_MODULE_NAME", module.Name);
processEnvironmentVars.TryAdd("CPAI_MODULE_PATH", _moduleSettings.GetModulePath(module));
- processEnvironmentVars.TryAdd("CPAI_MODULE_QUEUENAME", module.Queue);
processEnvironmentVars.TryAdd("CPAI_MODULE_PARALLELISM", module.Parallelism.ToString());
+ processEnvironmentVars.TryAdd("CPAI_MODULE_QUEUENAME", module.Queue);
+ if ((module.RequiredMb ?? 0) > 0)
+ processEnvironmentVars.TryAdd("CPAI_MODULE_REQUIRED_MB", module.RequiredMb?.ToString());
processEnvironmentVars.TryAdd("CPAI_MODULE_SUPPORT_GPU", (module.SupportGPU ?? false).ToString());
processEnvironmentVars.TryAdd("CPAI_ACCEL_DEVICE_NAME", module.AcceleratorDeviceName);
processEnvironmentVars.TryAdd("CPAI_HALF_PRECISION", module.HalfPrecision);
processEnvironmentVars.TryAdd("CPAI_LOG_VERBOSITY", (module.LogVerbosity ?? LogVerbosity.Info).ToString());
+ // Make sure the runtime environment variables used by the server are passed to the
+ // child process. Otherwise the NET module may start in Production mode. We *hope* the
+ // environment vars are passed down to to spawned processes, but we'll add these two
+ // just in case.
+ var aspnetEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
+ if (aspnetEnv != null)
+ processEnvironmentVars.TryAdd("ASPNETCORE_ENVIRONMENT", aspnetEnv);
+
+ var dotnetEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
+ if (dotnetEnv != null)
+ processEnvironmentVars.TryAdd("DOTNET_ENVIRONMENT", dotnetEnv);
+
return processEnvironmentVars;
}
}
diff --git a/src/API/Server/FrontEnd/AiModules/ModuleSettings.cs b/src/API/Server/FrontEnd/AiModules/ModuleSettings.cs
index 737a9c06..8ba9ac74 100644
--- a/src/API/Server/FrontEnd/AiModules/ModuleSettings.cs
+++ b/src/API/Server/FrontEnd/AiModules/ModuleSettings.cs
@@ -81,7 +81,7 @@ public string ModuleInstallerScriptPath
{
get
{
- if (SystemInfo.OperatingSystem.EqualsIgnoreCase("windows"))
+ if (SystemInfo.IsWindows)
return _moduleOptions.ModuleInstallerScriptsPath + "\\setup.bat";
return _moduleOptions.ModuleInstallerScriptsPath + "/setup.sh";
@@ -172,42 +172,63 @@ public static void LoadModuleSettings(IConfigurationBuilder config, string modul
string settingsFile = Path.Combine(modulePath, "modulesettings.json");
if (File.Exists(settingsFile))
+ {
+ Console.WriteLine($"Trace: Loading {settingsFile}");
config.AddJsonFile(settingsFile, optional: true, reloadOnChange: reloadOnChange);
+ }
if (!string.IsNullOrEmpty(runtimeEnv))
{
settingsFile = Path.Combine(modulePath, $"modulesettings.{runtimeEnv}.json");
if (File.Exists(settingsFile))
+ {
+ Console.WriteLine($"Trace: Loading {settingsFile}");
config.AddJsonFile(settingsFile, optional: true, reloadOnChange: reloadOnChange);
+ }
}
settingsFile = Path.Combine(modulePath, $"modulesettings.{os}.json");
if (File.Exists(settingsFile))
+ {
+ Console.WriteLine($"Trace: Loading {settingsFile}");
config.AddJsonFile(settingsFile, optional: true, reloadOnChange: reloadOnChange);
+ }
if (!string.IsNullOrEmpty(runtimeEnv))
{
settingsFile = Path.Combine(modulePath, $"modulesettings.{os}.{runtimeEnv}.json");
if (File.Exists(settingsFile))
+ {
+ Console.WriteLine($"Trace: Loading {settingsFile}");
config.AddJsonFile(settingsFile, optional: true, reloadOnChange: reloadOnChange);
+ }
}
settingsFile = Path.Combine(modulePath, $"modulesettings.{os}.{architecture}.json");
if (File.Exists(settingsFile))
+ {
+ Console.WriteLine($"Trace: Loading {settingsFile}");
config.AddJsonFile(settingsFile, optional: true, reloadOnChange: reloadOnChange);
+ }
if (!string.IsNullOrEmpty(runtimeEnv))
{
settingsFile = Path.Combine(modulePath, $"modulesettings.{os}.{architecture}.{runtimeEnv}.json");
if (File.Exists(settingsFile))
+ {
+ Console.WriteLine($"Trace: Loading {settingsFile}");
config.AddJsonFile(settingsFile, optional: true, reloadOnChange: reloadOnChange);
+ }
}
- if (SystemInfo.ExecutionEnvironment == ExecutionEnvironment.Docker)
+ if (SystemInfo.IsDocker)
{
settingsFile = Path.Combine(modulePath, $"modulesettings.docker.json");
if (File.Exists(settingsFile))
+ {
+ Console.WriteLine($"Trace: Loading {settingsFile}");
config.AddJsonFile(settingsFile, optional: true, reloadOnChange: reloadOnChange);
+ }
}
}
@@ -236,6 +257,7 @@ public ModuleSettings(IConfiguration config,
///
/// Returns a string that represents the current directory a module lives in. Note that a
/// module's folder is always the same name as its Id.
+ /// REVIEW: [Matthew] module.ModulePath is set safely and can be used instead of this if you wish
///
/// The module to launch
/// A string object
@@ -249,15 +271,8 @@ public string GetModulePath(ModuleBase module)
///
/// Returns a string that represents the working directory for a module.
+ /// REVIEW: [Matthew] module.WorkingDirectory is set safely and can be used instead of this if you wish
///
- ///
- /// REVIEW: [Mattew] module.WorkingDirectory is set safely and can be used instead of this if you wish
- /// The working directory isn't necessarily the dir the executed file is in. eg. .NET
- /// exes can be buried deep in /bin/Debug/net6/net6.0-windows. The working directory also
- /// isn't the Module directory, since the actual executable code for a module could be in a
- /// subdirectory of that module. So we start by assuming it's the path where the executed
- /// file is, but allow for an override (in the case of .NET development) if provided.
- ///
/// The module to launch
/// A string object
public string GetWorkingDirectory(ModuleBase module)
@@ -313,14 +328,14 @@ public string GetFilePath(ModuleConfig module)
// If it is "Python" then use our default Python location (in this case, python 3.7 or
// 3.8 if Linux/macOS)
if (runtime == "python")
- runtime = SystemInfo.OperatingSystem == "Windows" ? "python37" : "python38";
+ runtime = SystemInfo.IsWindows ? "python37" : "python38";
// HACK: In Docker, Python installs for downloaded modules can be local for downloaded
// modules, or shared for pre-installed modules. For preinstalled/shared the python
// command is in the format of python3.N because we don't install Python in the runtimes
// folder, but in the OS itself. In Docker this means we call "python3.8", rather than
// "/runtimes/bin/linux/python38/venv/bin/python3
- if (SystemInfo.ExecutionEnvironment == ExecutionEnvironment.Docker &&
+ if (SystemInfo.IsDocker &&
module.RuntimeLocation == "Shared" && runtime.StartsWith("python"))
{
if (!runtime.StartsWith("python3."))
diff --git a/src/API/Server/FrontEnd/Config/ModuleOptions.cs b/src/API/Server/FrontEnd/Config/ModuleOptions.cs
index bd4c5a98..2c1dcee9 100644
--- a/src/API/Server/FrontEnd/Config/ModuleOptions.cs
+++ b/src/API/Server/FrontEnd/Config/ModuleOptions.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
namespace CodeProject.AI.API.Server.Frontend
{
@@ -12,6 +13,11 @@ public class ModuleOptions
///
public string? ModuleListUrl { get; set; }
+ ///
+ /// Gets or sets the timeout for installing a module
+ ///
+ public TimeSpan ModuleInstallTimeout { get; set; }
+
///
/// The password that must be provided when uploading a new module for installation via
/// the API.
diff --git a/src/API/Server/FrontEnd/Config/PersistedOverrideSettings.cs b/src/API/Server/FrontEnd/Config/PersistedOverrideSettings.cs
index d386491b..934ff6b0 100644
--- a/src/API/Server/FrontEnd/Config/PersistedOverrideSettings.cs
+++ b/src/API/Server/FrontEnd/Config/PersistedOverrideSettings.cs
@@ -1,5 +1,4 @@
using System.IO;
-using System;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
@@ -44,20 +43,22 @@ public PersistedOverrideSettings(string storagePath)
else
settingsFilePath = Path.Combine(_storagePath, SettingsFilename);
- return await ModuleConfigExtensions.LoadSettings(settingsFilePath);
+ return await ModuleConfigExtensions.LoadSettings(settingsFilePath)
+ .ConfigureAwait(false);
}
///
/// Saves the persisted override settings of the current setup to file.
///
/// A JsonObject containing the settings
- public async Task SaveSettings(JsonObject? settings)
+ public async Task SaveSettingsAsync(JsonObject? settings)
{
string settingsFilePath = SystemInfo.RuntimeEnvironment == RuntimeEnvironment.Development
? Path.Combine(_storagePath, DevSettingsFilename)
: Path.Combine(_storagePath, SettingsFilename);
- return await ModuleConfigExtensions.SaveSettings(settings, settingsFilePath);
+ return await ModuleConfigExtensions.SaveSettingsAsync(settings, settingsFilePath)
+ .ConfigureAwait(false);
}
}
}
diff --git a/src/API/Server/FrontEnd/Controllers/LogController.cs b/src/API/Server/FrontEnd/Controllers/LogController.cs
index 02d2a544..3c58310f 100644
--- a/src/API/Server/FrontEnd/Controllers/LogController.cs
+++ b/src/API/Server/FrontEnd/Controllers/LogController.cs
@@ -21,7 +21,7 @@ public class LogController : ControllerBase
///
/// Constructor
///
- ///
+ /// The logger
public LogController(ILogger logger)
{
_logger = logger;
@@ -53,13 +53,6 @@ public ResponseBase AddLog([FromForm] string? entry,
if (!string.IsNullOrWhiteSpace(label))
msg += "{{" + label + "}}";
- if (entry.Contains("LoadLibrary failed with error 126") &&
- entry.Contains("onnxruntime_providers_cuda.dll"))
- {
- entry = "Attempted to load ONNX runtime CUDA provider. No luck, moving on...";
- log_level = LogLevel.Information;
- }
-
// strip out any terminal colourisation
entry = Regex.Replace(entry, "\\[\\d+(;\\d+)\\d+m", string.Empty);
diff --git a/src/API/Server/FrontEnd/Controllers/ModuleController.cs b/src/API/Server/FrontEnd/Controllers/ModuleController.cs
index 8946ac82..f84338bd 100644
--- a/src/API/Server/FrontEnd/Controllers/ModuleController.cs
+++ b/src/API/Server/FrontEnd/Controllers/ModuleController.cs
@@ -83,11 +83,12 @@ public async Task UploadModule()
using (Stream fileStream = new FileStream(downloadPath, FileMode.Create, FileAccess.Write))
{
- await uploadedFile.CopyToAsync(fileStream);
+ await uploadedFile.CopyToAsync(fileStream).ConfigureAwait(false);
fileStream.Close();
}
- (bool success, string error) = await _moduleInstaller.InstallModuleAsync(downloadPath, null);
+ (bool success, string error) = await _moduleInstaller.InstallModuleAsync(downloadPath, null)
+ .ConfigureAwait(false);
return success? new SuccessResponse() : CreateErrorResponse("Unable install module: " + error);
}
@@ -122,7 +123,8 @@ public async Task ListInstalledModules()
.ToList() ?? new List();
// Mark those modules that can't be downloaded
- List downloadables = await _moduleInstaller.GetDownloadableModules();
+ List downloadables = await _moduleInstaller.GetDownloadableModules()
+ .ConfigureAwait(false);
foreach (ModuleDescription module in modules)
{
if (!downloadables.Any(download => download.ModuleId == module.ModuleId))
@@ -145,7 +147,8 @@ public async Task ListInstalledModules()
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task ListAvailableModules()
{
- List moduleList = await _moduleInstaller.GetDownloadableModules();
+ List moduleList = await _moduleInstaller.GetDownloadableModules()
+ .ConfigureAwait(false);
return new ModuleListResponse()
{
@@ -164,7 +167,8 @@ public async Task ListAvailableModules()
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task ListAllModules()
{
- List downloadableModules = await _moduleInstaller.GetDownloadableModules()
+ List downloadableModules = await _moduleInstaller.GetDownloadableModules()
+ .ConfigureAwait(false)
?? new List();
string currentServerVersion = _versionConfig.VersionInfo!.Version;
@@ -203,14 +207,18 @@ public async Task ListAllModules()
///
/// The module to install
/// The version of the module to install
+ /// Whether or not to ignore the download cache. If true, the module
+ /// will always be freshly downloaded
/// A Response Object.
- [HttpPost("install/{moduleId}/{version}", Name = "Install Module")]
+ [HttpPost("install/{moduleId}/{version}/{nocache:bool?}", Name = "Install Module")]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task InstallModuleAsync(string moduleId, string version)
+ public async Task InstallModuleAsync(string moduleId, string version,
+ bool noCache = false)
{
- (bool success, string error) = await _moduleInstaller.DownloadAndInstallModuleAsync(moduleId, version);
+ var downloadTask = _moduleInstaller.DownloadAndInstallModuleAsync(moduleId, version, noCache);
+ (bool success, string error) = await downloadTask.ConfigureAwait(false);
return success? new SuccessResponse() : CreateErrorResponse(error);
}
@@ -225,7 +233,8 @@ public async Task InstallModuleAsync(string moduleId, string versi
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task UninstallModuleAsync(string moduleId)
{
- (bool success, string error) = await _moduleInstaller.UninstallModuleAsync(moduleId);
+ (bool success, string error) = await _moduleInstaller.UninstallModuleAsync(moduleId)
+ .ConfigureAwait(false);
return success? new SuccessResponse() : CreateErrorResponse(error);
}
diff --git a/src/API/Server/FrontEnd/Controllers/ProxyController.cs b/src/API/Server/FrontEnd/Controllers/ProxyController.cs
index 6dcfc383..5591d123 100644
--- a/src/API/Server/FrontEnd/Controllers/ProxyController.cs
+++ b/src/API/Server/FrontEnd/Controllers/ProxyController.cs
@@ -1,37 +1,38 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
-using CodeProject.AI.SDK;
using CodeProject.AI.API.Server.Backend;
using CodeProject.AI.API.Common;
-using System.Text.Json.Nodes;
-using System.Diagnostics;
+using CodeProject.AI.SDK;
+using CodeProject.AI.SDK.Common;
namespace CodeProject.AI.API.Server.Frontend.Controllers
{
// ------------------------------------------------------------------------------
- // When a backend analysis module starts it will register itself with the main CodeProject.AI
+ // When a backend analysis module starts it will register itself with the main
// Server. It does this by Posting as Register request to the Server which
// - provides the end part of url for the request
// - the name of the queue that the request will be sent to.
// - the command string that will be associated with the payload sent to the queue.
//
- // To initiate an AI operation, the client will post a payload to the
+ // To initiate an AI operation, the client will post a payload to the server
// This is accomplished by
// - getting the url ending.
// - using this to get the queue name and command name
- // - sending the above and payload to the queue
- // - await the respons
+ // - sending the above, plus a payload, to the queue
+ // - await the response
// - return the response to the caller.
// ------------------------------------------------------------------------------
@@ -47,6 +48,8 @@ public class ProxyController : ControllerBase
private readonly CommandDispatcher _dispatcher;
private readonly BackendRouteMap _routeMap;
private readonly ModuleCollection _modules;
+ private readonly TriggersConfig _triggersConfig;
+ private readonly TriggerTaskRunner _commandRunner;
///
/// Initializes a new instance of the VisionController class.
@@ -54,12 +57,19 @@ public class ProxyController : ControllerBase
/// The Command Dispatcher instance.
/// The Route Manager
/// Contains the Collection of modules
- public ProxyController(CommandDispatcher dispatcher, BackendRouteMap routeMap,
- IOptions modulesConfig)
+ /// Contains the triggers
+ /// The command runner
+ public ProxyController(CommandDispatcher dispatcher,
+ BackendRouteMap routeMap,
+ IOptions modulesConfig,
+ IOptions triggersConfig,
+ TriggerTaskRunner commandRunner)
{
- _dispatcher = dispatcher;
- _routeMap = routeMap;
- _modules = modulesConfig.Value;
+ _dispatcher = dispatcher;
+ _routeMap = routeMap;
+ _modules = modulesConfig.Value;
+ _triggersConfig = triggersConfig.Value;
+ _commandRunner = commandRunner;
}
///
@@ -75,7 +85,8 @@ public async Task Post(string path)
Stopwatch sw = Stopwatch.StartNew();
- var response = await _dispatcher.QueueRequest(routeInfo!.QueueName, payload);
+ var response = await _dispatcher.QueueRequest(routeInfo!.QueueName, payload)
+ .ConfigureAwait(false);
long analysisRoundTripMs = sw.ElapsedMilliseconds;
@@ -89,6 +100,9 @@ public async Task Post(string path)
jsonResponse ??= new JsonObject();
jsonResponse["analysisRoundTripMs"] = analysisRoundTripMs;
+ // Check for, and execute if needed, triggers
+ ProcessTriggers(routeInfo!.QueueName, jsonResponse);
+
// Wrap it back up
responseString = JsonSerializer.Serialize(jsonResponse) as string;
return new ContentResult
@@ -137,7 +151,7 @@ public IActionResult ApiSummary()
string category = index > 0 ? routeInfo.Path.Substring(0, index) : routeInfo.Path;
string route = index > 0 ? routeInfo.Path.Substring(index + 1) : string.Empty;
- // string path = $"/{version}/{category}/{route}";
+ // string path = $"/{version}/{category}/{route}";
string path = $"{version}/{routeInfo.Path}";
if (category != currentCategory)
@@ -281,5 +295,59 @@ private byte[] GetFileData(IFormFile x)
return data;
}
+
+ private void ProcessTriggers(string queueName, JsonObject response)
+ {
+ if (_triggersConfig.Triggers is null || _triggersConfig.Triggers.Length == 0)
+ return;
+
+ string platform = SystemInfo.Platform;
+
+ try
+ {
+ foreach (Trigger trigger in _triggersConfig.Triggers)
+ {
+ // If the trigger is queue specific, check
+ if (!string.IsNullOrWhiteSpace(trigger.Queue) &&
+ !trigger.Queue.EqualsIgnoreCase(queueName))
+ continue;
+
+ // Is there a task to run on this platform, and a property to look for?
+ TriggerTask? task = trigger.GetTask(platform);
+ if (string.IsNullOrEmpty(trigger.PropertyName) || task is null ||
+ string.IsNullOrEmpty(task.Command))
+ continue;
+
+ if (string.IsNullOrWhiteSpace(trigger.PredictionsCollectionName))
+ {
+ float.TryParse(response["confidence"]?.ToString(), out float confidence);
+ string? value = response[trigger.PropertyName]?.ToString();
+ if (trigger.Test(value, confidence))
+ _commandRunner.RunCommand(task);
+ }
+ else
+ {
+ var predictions = response[trigger.PredictionsCollectionName];
+ if (predictions is not null)
+ {
+ foreach (var prediction in predictions.AsArray())
+ {
+ if (prediction is null)
+ continue;
+
+ float.TryParse(prediction["confidence"]?.ToString(), out float confidence);
+ string? value = prediction[trigger.PropertyName]?.ToString();
+ if (trigger.Test(value, confidence))
+ _commandRunner.RunCommand(task);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ }
+ }
}
}
diff --git a/src/API/Server/FrontEnd/Controllers/QueueController.cs b/src/API/Server/FrontEnd/Controllers/QueueController.cs
index ee586a89..147bca4b 100644
--- a/src/API/Server/FrontEnd/Controllers/QueueController.cs
+++ b/src/API/Server/FrontEnd/Controllers/QueueController.cs
@@ -1,5 +1,7 @@
using System;
using System.IO;
+using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
@@ -14,7 +16,7 @@
namespace CodeProject.AI.API.Server.Frontend.Controllers
{
///
- /// Handles pulling requests from the Command Queue and returning reponses to the calling method.
+ /// Handles pulling requests from the Command Queue and returning responses to the calling method.
///
[Route("v1/queue")]
[ApiController]
@@ -40,7 +42,7 @@ public QueueController(QueueServices queueService,
///
/// The name of the Queue.
/// The ID of the module making the request
- /// The excution provider, typically the GPU library in use
+ /// The execution provider, typically the GPU library in use
/// The aborted request token.
/// The Request Object.
[HttpGet("{name}", Name = "GetRequestFromQueue")]
@@ -52,8 +54,8 @@ public async Task GetQueue([FromRoute] string name,
[FromQuery] string? executionProvider,
CancellationToken token)
{
-
- BackendRequestBase? request = await _queueService.DequeueRequestAsync(name, token);
+ BackendRequestBase? request = await _queueService.DequeueRequestAsync(name, token)
+ .ConfigureAwait(false);
bool shuttingDown = false;
@@ -79,26 +81,34 @@ public async Task GetQueue([FromRoute] string name,
/// the named queue if available.
///
/// The id of the request the response is for.
- /// The ID of the module making the request
- /// The hardware accelerator execution provider.
/// The Request Object.
[HttpPost("{reqid}", Name = "SetResponseInQueue")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task SetResponse(string reqid, [FromQuery] string moduleId,
- [FromQuery] string? executionProvider = null)
+ public async Task SetResponse(string reqid)
{
- string? response = null;
+ string? responseString = null;
using var bodyStream = HttpContext.Request.Body;
- if (bodyStream != null)
+ if (bodyStream != null)
{
using var textreader = new StreamReader(bodyStream);
- response = await textreader.ReadToEndAsync();
+ responseString = await textreader.ReadToEndAsync().ConfigureAwait(false);
}
- UpdateProcessStatus(moduleId, true, executionProvider: executionProvider);
+ var response = JsonSerializer.Deserialize(responseString ?? "");
+
+ string? command = response?["command"]?.ToString();
+ string? moduleId = response?["moduleId"]?.ToString();
+ string? executionProvider = response?["executionProvider"]?.ToString();
+
+ if (!string.IsNullOrWhiteSpace(moduleId))
+ {
+ bool incrementProcessCount = command is not null && !command.EqualsIgnoreCase("status");
+ UpdateProcessStatus(moduleId, incrementProcessCount: incrementProcessCount,
+ executionProvider: executionProvider);
+ }
- var success = _queueService.SetResult(reqid, response);
+ var success = _queueService.SetResult(reqid, responseString);
if (!success)
return BadRequest("failure to set response.");
@@ -115,6 +125,7 @@ private void UpdateProcessStatus(string moduleId, bool incrementProcessCount = f
{
if (status!.Status != ProcessStatusType.Stopping)
status.Status = shuttingDown? ProcessStatusType.Stopping : ProcessStatusType.Started;
+
status.Started ??= DateTime.UtcNow;
status.LastSeen = DateTime.UtcNow;
diff --git a/src/API/Server/FrontEnd/Controllers/SettingsController.cs b/src/API/Server/FrontEnd/Controllers/SettingsController.cs
index aea47e66..2d379506 100644
--- a/src/API/Server/FrontEnd/Controllers/SettingsController.cs
+++ b/src/API/Server/FrontEnd/Controllers/SettingsController.cs
@@ -123,7 +123,7 @@ public async Task UpsertSettingAsync(string moduleId, [FromForm] s
// Special case
if (settings.Name.EqualsIgnoreCase("Restart"))
{
- success = await _moduleProcessServices.RestartProcess(module);
+ success = await _moduleProcessServices.RestartProcess(module).ConfigureAwait(false);
}
else
{
@@ -131,15 +131,16 @@ public async Task UpsertSettingAsync(string moduleId, [FromForm] s
module.UpsertSetting(settings.Name, settings.Value);
// Restart the module and persist the settings
- if (await _moduleProcessServices.RestartProcess(module))
+ if (await _moduleProcessServices.RestartProcess(module).ConfigureAwait(false))
{
var settingStore = new PersistedOverrideSettings(_storagePath);
- var overrideSettings = await settingStore.LoadSettings();
+ var overrideSettings = await settingStore.LoadSettings().ConfigureAwait(false);
if (ModuleConfigExtensions.UpsertSettings(overrideSettings, module.ModuleId!,
settings.Name, settings.Value))
{
- success = await settingStore.SaveSettings(overrideSettings);
+ success = await settingStore.SaveSettingsAsync(overrideSettings)
+ .ConfigureAwait(false);
}
}
}
@@ -166,7 +167,7 @@ public async Task UpsertSettingsAsync([FromBody] SettingsDict sett
// Load up the current persisted settings so we can update and re-save them
var settingStore = new PersistedOverrideSettings(_storagePath);
- var overrideSettings = await settingStore.LoadSettings();
+ var overrideSettings = await settingStore.LoadSettings().ConfigureAwait(false);
// Keep tabs on which modules need to be restarted
List? moduleIdsToRestart = new();
@@ -212,11 +213,15 @@ public async Task UpsertSettingsAsync([FromBody] SettingsDict sett
{
ModuleConfig? module = _moduleCollection.GetModule(moduleId);
if (module is not null)
- restartSuccess = await _moduleProcessServices.RestartProcess(module) && restartSuccess;
+ {
+ var restartTask = _moduleProcessServices.RestartProcess(module);
+ restartSuccess = await restartTask.ConfigureAwait(false) && restartSuccess;
+ }
}
// Only persist these override settings if all modules restarted successfully
- bool success = restartSuccess && await settingStore.SaveSettings(overrideSettings);
+ bool success = restartSuccess && await settingStore.SaveSettingsAsync(overrideSettings)
+ .ConfigureAwait(false);
return new ResponseBase { success = success };
}
diff --git a/src/API/Server/FrontEnd/Controllers/StatusController.cs b/src/API/Server/FrontEnd/Controllers/StatusController.cs
index 6cfe2b93..598c5f6f 100644
--- a/src/API/Server/FrontEnd/Controllers/StatusController.cs
+++ b/src/API/Server/FrontEnd/Controllers/StatusController.cs
@@ -3,13 +3,13 @@
using System.Threading.Tasks;
using System.Text;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Options;
using CodeProject.AI.API.Common;
using CodeProject.AI.SDK.Common;
-using Microsoft.Extensions.Options;
namespace CodeProject.AI.API.Server.Frontend.Controllers
{
@@ -25,6 +25,7 @@ public class StatusController : ControllerBase
///
private readonly ServerVersionService _versionService;
private readonly ModuleSettings _moduleSettings;
+ private readonly ServerOptions _serverOptions;
private readonly ModuleProcessServices _moduleProcessService;
private readonly ModuleCollection _moduleCollection;
@@ -33,15 +34,18 @@ public class StatusController : ControllerBase
///
/// The Version instance.
/// The module settings instance
+ /// The server options
/// The Module Process Services.
/// The Module Collection.
public StatusController(ServerVersionService versionService,
ModuleSettings moduleSettings,
+ IOptions serverOptions,
ModuleProcessServices moduleProcessService,
IOptions moduleCollection)
{
_versionService = versionService;
_moduleSettings = moduleSettings;
+ _serverOptions = serverOptions.Value;
_moduleProcessService = moduleProcessService;
_moduleCollection = moduleCollection.Value;
}
@@ -97,12 +101,15 @@ public ResponseBase GetVersion()
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task GetSystemStatus()
{
- // run these in parallel as they have a Task.Delay(1000) in them.
- var cpuUsageTask = SystemInfo.GetCpuUsage();
- var gpuUsageTask = SystemInfo.GetGpuUsage();
- var gpuInfoTask = SystemInfo.GetGpuUsageInfo();
- var gpuVideoInfoTask = SystemInfo.GetVideoAdapterInfo();
- var serverVersion = _versionService.VersionConfig?.VersionInfo?.Version ?? string.Empty;
+ var serverVersion = _versionService.VersionConfig?.VersionInfo?.Version ?? string.Empty;
+
+ // Run these in parallel as they have a Task.Delay(1000) in them.
+ string gpuInfo = await SystemInfo.GetGpuUsageInfoAsync().ConfigureAwait(false);
+ int gpuUsage = await SystemInfo.GetGpuUsageAsync().ConfigureAwait(false);
+ string gpuVideoInfo = await SystemInfo.GetVideoAdapterInfoAsync().ConfigureAwait(false);
+ ulong gpuMemUsage = await SystemInfo.GetGpuMemoryUsageAsync().ConfigureAwait(false);
+ int cpuUsage = SystemInfo.GetCpuUsage();
+ ulong systemMemUsage = SystemInfo.GetSystemMemoryUsage();
var systemStatus = new StringBuilder();
systemStatus.AppendLine($"Server version: {serverVersion}");
@@ -110,11 +117,11 @@ public async Task GetSystemStatus()
systemStatus.AppendLine();
systemStatus.AppendLine();
- systemStatus.AppendLine(await gpuInfoTask);
+ systemStatus.AppendLine(gpuInfo);
systemStatus.AppendLine();
systemStatus.AppendLine();
- systemStatus.AppendLine(await gpuVideoInfoTask);
+ systemStatus.AppendLine(gpuVideoInfo);
systemStatus.AppendLine();
systemStatus.AppendLine();
@@ -135,10 +142,10 @@ public async Task GetSystemStatus()
var response = new
{
- CpuUsage = await cpuUsageTask,
- SystemMemUsage = await SystemInfo.GetSystemMemoryUsage(),
- GpuUsage = await gpuUsageTask,
- GpuMemUsage = await SystemInfo.GetGpuMemoryUsage(),
+ CpuUsage = cpuUsage,
+ SystemMemUsage = systemMemUsage,
+ GpuUsage = gpuUsage,
+ GpuMemUsage = gpuMemUsage,
ServerStatus = systemStatus.ToString()
};
@@ -175,7 +182,7 @@ public ObjectResult GetPaths([FromServices] IWebHostEnvironment env)
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task GetUpdateAvailable()
{
- VersionInfo? latest = await _versionService.GetLatestVersion();
+ VersionInfo? latest = await _versionService.GetLatestVersion().ConfigureAwait(false);
if (latest is null)
{
return new VersionUpdateResponse
@@ -230,12 +237,24 @@ public ResponseBase ListAnalysisStatus()
foreach (ProcessStatus process in _moduleProcessService.ListProcessStatuses())
{
- if (!string.IsNullOrEmpty(process.ModuleId))
+ ModuleConfig? module = string.IsNullOrEmpty(process.ModuleId) ? null
+ : _moduleCollection.GetModule(process.ModuleId);
+
+ if (module is not null)
{
- ModuleConfig? module = _moduleCollection.GetModule(process.ModuleId);
- process.StartupSummary = module?.SettingsSummary ?? string.Empty;
+ process.StartupSummary = module.SettingsSummary ?? string.Empty;
if (string.IsNullOrEmpty(process.StartupSummary))
+ {
Console.WriteLine($"Unable to find module for {process.ModuleId}");
+ }
+ else
+ {
+ // Expanding out the macros causes the display to be too wide
+ // process.StartupSummary = _moduleSettings.ExpandOption(process.StartupSummary,
+ // module.ModulePath);
+ string appRoot = _serverOptions.ApplicationRootPath!;
+ process.StartupSummary = process.StartupSummary.Replace(appRoot, "<root>");
+ }
}
}
diff --git a/src/API/Server/FrontEnd/Frontend.csproj b/src/API/Server/FrontEnd/Frontend.csproj
index 6bda01ec..a9ede97d 100644
--- a/src/API/Server/FrontEnd/Frontend.csproj
+++ b/src/API/Server/FrontEnd/Frontend.csproj
@@ -7,12 +7,20 @@ https://learn.microsoft.com/en-us/visualstudio/msbuild/property-functions?view=v
-
+
API Server
+ CodeProject.AI Server
+ A Service hosting the CodeProject.AI WebAPI for face detection and recognition, object detection, and scene classification, and other AI operations.
+ CodeProject
+ CodeProject
+ 2.1.12
+
+
+
+
CodeProject.AI.API.Server.Frontend
CodeProject.AI.Server
- 2.1.0
disable
enable
14515168-17dd-49db-9023-0749bb408a37
@@ -21,27 +29,22 @@ https://learn.microsoft.com/en-us/visualstudio/msbuild/property-functions?view=v
AnyCPU
net7.0
false
-
-
-
-
- Linux
- ..\..\..\..
- codeproject/ai-server
+ false
+ true
- CodeProject.AI Server
- CodeProject
- CodeProject
- true
- false
SSPL-1.0
- A Service hosting the CodeProject.AI WebAPI for face detection and recognition, object detection, and scene classification, and other AI operations.
https://www.codeproject.com/ai
codeproject125x125.png
- net7.0
+
+
+
+
+ Linux
+ ..\..\..\..
+ codeproject/ai-server
diff --git a/src/API/Server/FrontEnd/Logging/ServerLogger.cs b/src/API/Server/FrontEnd/Logging/ServerLogger.cs
index 140b1525..631e7cf5 100644
--- a/src/API/Server/FrontEnd/Logging/ServerLogger.cs
+++ b/src/API/Server/FrontEnd/Logging/ServerLogger.cs
@@ -122,18 +122,31 @@ public void Log(LogLevel logLevel, EventId eventId, TState state,
string message = formatter(state, exception);
string label = string.Empty;
- // Trim the category down a little
- if (!string.IsNullOrEmpty(_categoryName))
+ // We could create a dictionary of search/replace/new log level but then we run into
+ // issues such as "contains X AND contains Y" so just hardcode it here.
+
+ // This is more or less expected as we test for ONNXruntime. It's info, not a crash
+ if (message.Contains("LoadLibrary failed with error 126") &&
+ message.Contains("onnxruntime_providers_cuda.dll"))
+ {
+ message = "Attempted to load ONNX runtime CUDA provider. No luck, moving on...";
+ logLevel = LogLevel.Information;
+ }
+ // Annoying
+ else if (message.Contains("Failed to read environment variable [DOTNET_ROOT]"))
+ {
+ logLevel = LogLevel.Debug;
+ }
+ // Pointless
+ else if (message.Contains("Microsoft.Hosting.Lifetime[0]"))
+ {
+ return;
+ }
+ // ONNX/Tensorflow output is WAY too verbose for an error
+ else if (message.Contains("I tensorflow/cc/saved_model/reader.cc:") ||
+ message.Contains("I tensorflow/cc/saved_model/loader.cc:"))
{
- /*
- var parts = _categoryName.Split('.', StringSplitOptions.RemoveEmptyEntries);
- category = parts[^1];
- if (parts.Any(p => p == "CodeProject"))
- category = "CodeProject." + category;
- */
-
- // if (_categoryName.StartsWithIgnoreCase("CodeProject."))
- // category = "Server";
+ logLevel = LogLevel.Information;
}
// We're using the .NET logger which means we don't have a huge amount of control
diff --git a/src/API/Server/FrontEnd/Program.cs b/src/API/Server/FrontEnd/Program.cs
index 2f1a32b6..951ea955 100644
--- a/src/API/Server/FrontEnd/Program.cs
+++ b/src/API/Server/FrontEnd/Program.cs
@@ -8,9 +8,11 @@
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
+using System.Threading;
using System.Threading.Tasks;
using CodeProject.AI.API.Common;
+using CodeProject.AI.API.Server.Backend;
using CodeProject.AI.SDK.Common;
using Microsoft.AspNetCore.Hosting;
@@ -26,9 +28,13 @@ namespace CodeProject.AI.API.Server.Frontend
///
public class Program
{
+ const int defaultPort = 32168;
+ const int legacyPort = 5000;
+ const int legacyPortOsx = 5500;
+
static private ILogger? _logger = null;
- static int _port = 32168;
+ static int _port = defaultPort;
// static int _sPort = 5001; - eventually for SSL
///
@@ -37,137 +43,147 @@ public class Program
/// The command line args.
public static async Task Main(string[] args)
{
- // TODO: Pull these from the correct location
- const string company = "CodeProject";
- const string product = "AI";
-
- await SystemInfo.InitializeAsync();
+ const string productCategory = "AI";
- // lower cased as Linux has case sensitive file names
- string os = SystemInfo.OperatingSystem.ToLower();
- string architecture = SystemInfo.Architecture.ToLower();
- string? runtimeEnv = SystemInfo.RuntimeEnvironment == SDK.Common.RuntimeEnvironment.Development ||
- SystemInfo.IsDevelopmentCode ? "development" : string.Empty;
+ bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var assembly = Assembly.GetExecutingAssembly();
- var assemblyName = (assembly.GetName().Name ?? string.Empty)
- + (os == "windows" ? ".exe" : ".dll");
- var serviceName = assembly.GetCustomAttribute()?.Product
+ var assemblyName = (assembly.GetName().Name ?? string.Empty) + (isWindows? ".exe" : ".dll");
+ var companyName = assembly.GetCustomAttribute()?.Company
+ ?? "CodeProject";
+ var productName = assembly.GetCustomAttribute()?.Product
?? assemblyName.Replace(".", " ");
+
+ var serviceName = productName;
var servicePath = Path.Combine(AppContext.BaseDirectory, assemblyName);
var serviceDescription = assembly.GetCustomAttribute()?.Description
- ?? string.Empty;
+ ?? string.Empty;
- if (args.Length == 1)
+ // Prevent this app from starting more that one instance
+ using (var mutex = new Mutex(false, serviceName))
{
- if (args[0].EqualsIgnoreCase("/Install"))
+ if (!mutex.WaitOne(0))
{
- WindowsServiceInstaller.Install(servicePath, serviceName, serviceDescription);
+ Console.WriteLine("This application is already running.");
return;
}
- else if (args[0].EqualsIgnoreCase("/Uninstall"))
+
+ await SystemInfo.InitializeAsync().ConfigureAwait(false);
+
+ // lower cased as Linux has case sensitive file names
+ string os = SystemInfo.OperatingSystem.ToLower();
+ string architecture = SystemInfo.Architecture.ToLower();
+ string? runtimeEnv = SystemInfo.RuntimeEnvironment == SDK.Common.RuntimeEnvironment.Development ||
+ SystemInfo.IsDevelopmentCode ? "development" : string.Empty;
+
+
+ if (args.Length == 1)
{
- WindowsServiceInstaller.Uninstall(serviceName);
- KillOrphanedProcesses(runtimeEnv);
- return;
+ if (args[0].EqualsIgnoreCase("/Install"))
+ {
+ WindowsServiceInstaller.Install(servicePath, serviceName, serviceDescription);
+ return;
+ }
+ else if (args[0].EqualsIgnoreCase("/Uninstall"))
+ {
+ WindowsServiceInstaller.Uninstall(serviceName);
+ KillOrphanedProcesses(runtimeEnv);
+ return;
+ }
+ else if (args[0].EqualsIgnoreCase("/Start"))
+ {
+ WindowsServiceInstaller.Start(serviceName);
+ return;
+ }
+ else if (args[0].EqualsIgnoreCase("/Stop"))
+ {
+ WindowsServiceInstaller.Stop(serviceName);
+ KillOrphanedProcesses(runtimeEnv);
+ return;
+ }
}
- else if (args[0].EqualsIgnoreCase("/Start"))
+
+ // make sure any processes that didn't get killed on the Service shutdown get killed now.
+ KillOrphanedProcesses(runtimeEnv);
+
+ // GetProcessStatus a directory for the given platform that allows modules to store persisted data
+ string programDataDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
+ string applicationDataDir = $"{programDataDir}\\{companyName}\\{productCategory}".Replace('\\', Path.DirectorySeparatorChar);
+
+ // .NET's suggestion for macOS and Linux aren't great. Let's do something different.
+ if (SystemInfo.IsMacOS)
{
- WindowsServiceInstaller.Start(serviceName);
- return;
+ applicationDataDir = $"/Library/Application Support/{companyName}/{productCategory}";
}
- else if (args[0].EqualsIgnoreCase("/Stop"))
+ else if (SystemInfo.IsLinux)
{
- WindowsServiceInstaller.Stop(serviceName);
- KillOrphanedProcesses(runtimeEnv);
- return;
+ applicationDataDir = $"/etc/{companyName.ToLower()}/{productCategory.ToLower()}";
}
- }
-
- // make sure any processes that didn't get killed on the Service shutdown get killed now.
- KillOrphanedProcesses(runtimeEnv);
-
- // GetProcessStatus a directory for the given platform that allows momdules to store persisted data
- string programDataDir = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
- string applicationDataDir = $"{programDataDir}\\{company}\\{product}".Replace('\\', Path.DirectorySeparatorChar);
-
- // .NET's suggestion for macOS and Linux aren't great. Let's do something different.
- if (os == "macos")
- {
- applicationDataDir = $"/Library/Application Support/{company}/{product}";
- }
- else if (os == "linux")
- {
- applicationDataDir = $"/etc/{company.ToLower()}/{product.ToLower()}";
- }
-
- // Store this dir in the config settings so we can get to it later.
- var inMemoryConfigData = new Dictionary {
- { "ApplicationDataDir", applicationDataDir }
- };
- bool reloadConfigOnChange = SystemInfo.ExecutionEnvironment != ExecutionEnvironment.Docker;
+ // Store this dir in the config settings so we can get to it later.
+ var inMemoryConfigData = new Dictionary {
+ { "ApplicationDataDir", applicationDataDir }
+ };
- // Setup our custom Configuration Loader pipeline and build the configuration.
- IHost? host = CreateHostBuilder(args)
- .ConfigureAppConfiguration(SetupConfigurationLoaders(args, os, architecture,
- runtimeEnv, applicationDataDir,
- inMemoryConfigData,
- reloadConfigOnChange))
- .Build()
- ;
+ bool reloadConfigOnChange = !SystemInfo.IsDocker;
- _logger = host.Services.GetService>();
+ // Setup our custom Configuration Loader pipeline and build the configuration.
+ IHost? host = CreateHostBuilder(args)
+ .ConfigureAppConfiguration(SetupConfigurationLoaders(args, os, architecture,
+ runtimeEnv, applicationDataDir,
+ inMemoryConfigData,
+ reloadConfigOnChange))
+ .Build()
+ ;
- if (_logger != null)
- {
- string systemInfo = SystemInfo.GetSystemInfo();
- foreach (string line in systemInfo.Split('\n'))
- _logger.LogInformation("** " + line.TrimEnd());
+ _logger = host.Services.GetService>();
- _logger.LogInformation($"** App DataDir: {applicationDataDir}");
+ if (_logger != null)
+ {
+ string systemInfo = SystemInfo.GetSystemInfo();
+ foreach (string line in systemInfo.Split('\n'))
+ _logger.LogInformation("** " + line.TrimEnd());
- string info = await SystemInfo.GetGpuUsageInfo();
- foreach (string line in info.Split('\n'))
- _logger.LogInformation(line.TrimEnd());
+ _logger.LogInformation($"** App DataDir: {applicationDataDir}");
- info = await SystemInfo.GetVideoAdapterInfo();
- foreach (string line in info.Split('\n'))
- _logger.LogInformation(line.TrimEnd());
- }
+ string info = await SystemInfo.GetVideoAdapterInfoAsync().ConfigureAwait(false);
+ foreach (string line in info.Split('\n'))
+ _logger.LogInformation(line.TrimEnd());
+ }
- Task? hostTask;
- hostTask = host.RunAsync();
-#if DEBUG
- try
- {
- OpenBrowser($"http://localhost:{_port}/");
- }
- catch (Exception ex)
- {
- _logger?.LogError(ex, "Unable to open Dashboard on startup.");
- }
-#endif
- try
- {
- await hostTask;
+ Task? hostTask;
+ hostTask = host.RunAsync();
+ #if DEBUG
+ try
+ {
+ OpenBrowser($"http://localhost:{_port}/");
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "Unable to open Dashboard on startup.");
+ }
+ #endif
+ try
+ {
+ await hostTask.ConfigureAwait(false);
- Console.WriteLine("Shutting down");
- }
- catch (Exception ex)
- {
- // TODO: Host is gone, so no logger ??
- Console.WriteLine($"\n\nUnable to start the server: {ex.Message}.\n" +
- "Check that another instance is not running on the same port.");
- Console.Write("Press Enter to close.");
- Console.ReadLine();
+ Console.WriteLine("Shutting down");
+ }
+ catch (Exception ex)
+ {
+ // TODO: Host is gone, so no logger ??
+ Console.WriteLine($"\n\nUnable to start the server: {ex.Message}.\n" +
+ "Check that another instance is not running on the same port.");
+ Console.Write("Press Enter to close.");
+ Console.ReadLine();
+ }
}
}
private static void KillOrphanedProcesses(string? runtimeEnv)
{
- if (SystemInfo.OperatingSystem.EqualsIgnoreCase("Windows"))
+ if (SystemInfo.IsWindows)
{
try
{
@@ -216,7 +232,7 @@ private static Action SetupConfigurat
// RemoveProcessStatus the default sources and rebuild it.
config.Sources.Clear();
- // add in the default appsetting.json file and its variants
+ // add in the default appsettings.json file and its variants
// In order
// appsettings.json
// appsettings.development.json
@@ -259,7 +275,7 @@ private static Action SetupConfigurat
config.AddJsonFile(settingsFile, optional: true, reloadOnChange: reloadConfigOnChange);
}
- if (SystemInfo.ExecutionEnvironment == ExecutionEnvironment.Docker)
+ if (SystemInfo.IsDocker)
{
settingsFile = Path.Combine(baseDir, $"appsettings.docker.json");
if (File.Exists(settingsFile))
@@ -284,13 +300,17 @@ private static Action SetupConfigurat
config.AddJsonFile(Path.Combine(baseDir, VersionConfig.VersionCfgFilename),
reloadOnChange: reloadConfigOnChange, optional: true);
+ // Load the triggers.json file to load the triggers
+ config.AddJsonFile(Path.Combine(baseDir, TriggersConfig.TriggersCfgFilename),
+ reloadOnChange: reloadConfigOnChange, optional: true);
+
// Load the modulesettings.json files to get analysis module settings
LoadModulesConfiguration(config, runtimeEnv);
// Load the last saved config values as set by the user
LoadUserOverrideConfiguration(config, applicationDataDir, runtimeEnv, reloadConfigOnChange);
- // Load Envinronmnet Variables into Configuration
+ // Load Environment Variables into Configuration
config.AddEnvironmentVariables();
// Add command line back in to force it to have full override powers.
@@ -316,7 +336,7 @@ private static Action SetupConfigurat
// things. To be done at a later date.
private static void LoadModulesConfiguration(IConfigurationBuilder config, string? runtimeEnv)
{
- bool reloadOnChange = SystemInfo.ExecutionEnvironment != ExecutionEnvironment.Docker;
+ bool reloadOnChange = !SystemInfo.IsDocker;
IConfiguration configuration = config.Build();
(var modulesPath, var preInstalledModulesPath) = EnsureDirectories(configuration, runtimeEnv);
@@ -534,44 +554,49 @@ public static IHostBuilder CreateHostBuilder(string[] args)
_port = GetServerPort(hostbuilderContext);
bool foundPort = false;
- if (IsPortAvailable(_port))
+ // Listen on the port that the appsettings defines (we force the
+ // use of the default port. IsPortAvailable can sometimes be too
+ // conservative)
+ if (_port == defaultPort || IsPortAvailable(_port))
{
serverOptions.Listen(IPAddress.IPv6Any, _port);
foundPort = true;
}
- // We always want this port.
- if (_port != 32168 && IsPortAvailable(32168))
+ // If we aren't listening to the default port (32168), then listen
+ // to it! (and don't bother asking if it's available. Just try it.)
+ if (_port != defaultPort /* && IsPortAvailable(defaultPort)*/)
{
if (!foundPort)
- _port = 32168;
+ _port = defaultPort;
- serverOptions.Listen(IPAddress.IPv6Any, 32168);
+ serverOptions.Listen(IPAddress.IPv6Any, defaultPort);
foundPort = true;
}
if (!disableLegacyPort)
{
- // Add some legacy ports
+ // Add some legacy ports. First macOS (port 5500)
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
- if (_port != 5500 && IsPortAvailable(5500))
+ if (_port != legacyPortOsx && IsPortAvailable(legacyPortOsx))
{
if (!foundPort)
- _port = 5500;
+ _port = legacyPortOsx;
- serverOptions.Listen(IPAddress.IPv6Any, 5500);
+ serverOptions.Listen(IPAddress.IPv6Any, legacyPortOsx);
foundPort = true;
}
}
+ // Then everything else (port 5000)
else
{
- if (_port != 5000 && IsPortAvailable(5000))
+ if (_port != legacyPort && IsPortAvailable(legacyPort))
{
if (!foundPort)
- _port = 5000;
+ _port = legacyPort;
- serverOptions.Listen(IPAddress.IPv6Any, 5000);
+ serverOptions.Listen(IPAddress.IPv6Any, legacyPort);
foundPort = true;
}
}
@@ -614,7 +639,7 @@ public static IHostBuilder CreateHostBuilder(string[] args)
}
///
- /// Checks as to whether a given port on this machine is avaialble for use.
+ /// Checks as to whether a given port on this machine is available for use.
///
/// The port number
/// true if the port is available; false otherwise
diff --git a/src/API/Server/FrontEnd/Properties/launchSettings.json b/src/API/Server/FrontEnd/Properties/launchSettings.json
index 3017f1c8..4aa36d3c 100644
--- a/src/API/Server/FrontEnd/Properties/launchSettings.json
+++ b/src/API/Server/FrontEnd/Properties/launchSettings.json
@@ -4,7 +4,8 @@
"commandName": "Project",
"launchUrl": "http://localhost:32168",
"environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "YOLOv5_VERBOSE": "false"
},
"applicationUrl": "http://localhost:32168",
"dotnetRunMessages": true
diff --git a/src/API/Server/FrontEnd/Startup.cs b/src/API/Server/FrontEnd/Startup.cs
index c2edc887..c6c6f4a8 100644
--- a/src/API/Server/FrontEnd/Startup.cs
+++ b/src/API/Server/FrontEnd/Startup.cs
@@ -106,6 +106,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddVersionProcessRunner(Configuration);
+ services.Configure(Configuration.GetSection(TriggersConfig.TriggersCfgSection));
+
// Configure the shutdown timeout to 60s instead of 2
services.Configure(
opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(60));
@@ -115,7 +117,7 @@ public void ConfigureServices(IServiceCollection services)
/// Configures the application pipeline.
///
/// The Application Builder.
- /// The Hosting Evironment.
+ /// The Hosting Environment.
/// The logger
/// The installation instance config values.
/// The Version Configuration
@@ -150,6 +152,18 @@ public void Configure(IApplicationBuilder app,
app.UseDefaultFiles();
app.UseStaticFiles();
+ /* Should we choose to provide a folder in which we can dump items such as items
+ generated by a module, we could do:
+ app.UseStaticFiles(new StaticFileOptions
+ {
+ FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(
+ Path.Combine(builder.Environment.ContentRootPath, "/modules/moduleId/models"));
+ });
+
+ that is, if you can decipher
+ https://learn.microsoft.com/en-us/aspnet/core/fundamentals/static-files?view=aspnetcore-7.0#serve-files-from-multiple-locations
+ */
+
app.UseRouting();
app.UseCors("allowAllOrigins");
@@ -170,7 +184,7 @@ private void InitializeInstallConfig()
_installConfig.Id = Guid.NewGuid();
}
- // if this is a new install or replacing a pre V2.1.0 version
+ // if this is a new install or replacing a pre V2.1 version
if (string.IsNullOrEmpty(_installConfig.Version))
AiModuleInstaller.QueueInitialModulesInstallation();
@@ -178,11 +192,9 @@ private void InitializeInstallConfig()
try
{
- var configValues = new { install = _installConfig };
-
+ var configValues = new { install = _installConfig };
string appDataDir = Configuration["ApplicationDataDir"]
?? throw new ArgumentNullException("ApplicationDataDir is not defined in configuration");
-
string configFilePath = Path.Combine(appDataDir, InstallConfig.InstallCfgFilename);
if (!Directory.Exists(appDataDir))
diff --git a/src/API/Server/FrontEnd/Utilities/PackageDownloader.cs b/src/API/Server/FrontEnd/Utilities/PackageDownloader.cs
index 0c5bcfdd..2c7ddeb0 100644
--- a/src/API/Server/FrontEnd/Utilities/PackageDownloader.cs
+++ b/src/API/Server/FrontEnd/Utilities/PackageDownloader.cs
@@ -51,7 +51,7 @@ public async Task DownloadTextFileAsync(string uri)
{
// remove file:// and then convert /dir -> C:\dir or c:\dir -> /dir as needed
uri = uri.Substring("file://".Length);
- if (SystemInfo.OperatingSystem.EqualsIgnoreCase("Windows"))
+ if (SystemInfo.IsWindows)
{
if (uri.StartsWith("/"))
uri = "C:" + uri;
@@ -60,13 +60,13 @@ public async Task DownloadTextFileAsync(string uri)
uri = uri.Substring("c:".Length);
uri = Text.FixSlashes(uri);
- return await File.ReadAllTextAsync(uri);
+ return await File.ReadAllTextAsync(uri).ConfigureAwait(false);
}
- if (!Uri.TryCreate(uri, UriKind.Absolute, out Uri _))
+ if (!Uri.TryCreate(uri, UriKind.Absolute, out _))
throw new InvalidOperationException($"{nameof(uri)} is not a valid URI.");
- return await _httpClient.GetStringAsync(uri);
+ return await _httpClient.GetStringAsync(uri).ConfigureAwait(false);
}
///
@@ -86,7 +86,7 @@ public async Task DownloadTextFileAsync(string uri)
throw new ArgumentOutOfRangeException(error);
}
- if (!Uri.TryCreate(uri, UriKind.Absolute, out Uri _))
+ if (!Uri.TryCreate(uri, UriKind.Absolute, out _))
{
error = $"{nameof(uri)} is not a valid URI.";
throw new InvalidOperationException(error);
@@ -94,7 +94,7 @@ public async Task DownloadTextFileAsync(string uri)
try
{
- byte[] fileBytes = await _httpClient.GetByteArrayAsync(uri);
+ byte[] fileBytes = await _httpClient.GetByteArrayAsync(uri).ConfigureAwait(false);
if (fileBytes.Length > 0)
{
File.WriteAllBytes(outputPath, fileBytes);
diff --git a/src/API/Server/FrontEnd/Version/ServerVersionProcessRunner.cs b/src/API/Server/FrontEnd/Version/ServerVersionProcessRunner.cs
index aa715728..7baaef8c 100644
--- a/src/API/Server/FrontEnd/Version/ServerVersionProcessRunner.cs
+++ b/src/API/Server/FrontEnd/Version/ServerVersionProcessRunner.cs
@@ -1,5 +1,4 @@
using System;
-using System.Configuration;
using System.Threading;
using System.Threading.Tasks;
@@ -26,7 +25,7 @@ public class ServerVersionProcessRunner : BackgroundService
/// The Queue management service.
/// The logger
public ServerVersionProcessRunner(ServerVersionService versionService,
- ILogger logger)
+ ILogger logger)
{
_versionService = versionService;
_logger = logger;
@@ -36,17 +35,17 @@ public ServerVersionProcessRunner(ServerVersionService versionService,
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Let's make sure the front end is up and running before we start the version process
- await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
+ await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken).ConfigureAwait(false);
- CheckCurrentVersion();
+ await CheckCurrentVersion().ConfigureAwait(false);
}
- private async void CheckCurrentVersion()
+ private async Task CheckCurrentVersion()
{
// Grab the latest version info
if (_versionService != null)
{
- VersionInfo? latest = await _versionService.GetLatestVersion();
+ VersionInfo? latest = await _versionService.GetLatestVersion().ConfigureAwait(false);
if (latest != null && _versionService.VersionConfig?.VersionInfo != null)
{
_logger.LogDebug($"Current Version is {_versionService.VersionConfig.VersionInfo.Version}");
@@ -55,14 +54,14 @@ private async void CheckCurrentVersion()
if (compare < 0)
{
if (latest.SecurityUpdate ?? false)
- _logger.LogInformation($" *** A SECURITY UPDATE {latest.Version} is available ** ");
+ _logger.LogInformation($"*** A SECURITY UPDATE {latest.Version} is available");
else
- _logger.LogInformation($" *** A new version {latest.Version} is available ** ");
+ _logger.LogInformation($"*** A new version {latest.Version} is available");
}
else if (compare == 0)
_logger.LogInformation("Server: This is the latest version");
else
- _logger.LogInformation("Server: This is a new, unreleased version");
+ _logger.LogInformation("*** Server: This is a new, unreleased version");
}
}
}
diff --git a/src/API/Server/FrontEnd/Version/ServerVersionService.cs b/src/API/Server/FrontEnd/Version/ServerVersionService.cs
index 8276bafd..a6f0d73d 100644
--- a/src/API/Server/FrontEnd/Version/ServerVersionService.cs
+++ b/src/API/Server/FrontEnd/Version/ServerVersionService.cs
@@ -22,7 +22,7 @@ public class ServerVersionService
private readonly ServerOptions _serverOptions;
///
- /// Initializs a new instance of the Startup class.
+ /// Initializes a new instance of the Startup class.
///
/// The version Options instance.
/// The install Options instance.
@@ -85,6 +85,7 @@ public ServerVersionService(IOptions versionOptions,
// is purely things like OS / GPU.
string currentVersion = VersionConfig.VersionInfo?.Version ?? string.Empty;
_client.DefaultRequestHeaders.Add("X-CPAI-Server-Version", currentVersion);
+
var sysProperties = SystemInfo.Summary;
var systemInfoJson = JsonSerializer.Serialize(sysProperties);
_client.DefaultRequestHeaders.Add("X-CPAI-Server-SystemInfo", systemInfoJson);
@@ -95,7 +96,8 @@ public ServerVersionService(IOptions versionOptions,
try
{
- string data = await _client.GetStringAsync(_serverOptions.ServerVersionCheckUrl);
+ string data = await _client.GetStringAsync(_serverOptions.ServerVersionCheckUrl)
+ .ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(data))
{
var options = new JsonSerializerOptions
diff --git a/src/API/Server/FrontEnd/appsettings.Development.json b/src/API/Server/FrontEnd/appsettings.Development.json
index 842b57ca..527f524e 100644
--- a/src/API/Server/FrontEnd/appsettings.Development.json
+++ b/src/API/Server/FrontEnd/appsettings.Development.json
@@ -9,7 +9,7 @@
"ModuleOptions": {
// Will we be launching the backend analysis modules when the server starts? (handy to disable
// for debugging the modules separately)
- "LaunchModules": true,
+ "LaunchModules": false,
// This needs to be set to allow module uploads and installs via the API
"InstallPassword": "demo-password",
@@ -17,7 +17,7 @@
// Location of the Json list of modules that can be downloaded
// For debugging: choose either local host or local file system
- "ModuleListUrl": "file://modules.json", // From local json file
+ "ModuleListUrl": "file://modules.json", // From local json file
// "ModuleListUrl": "http://localhost:9001/ai/modules/list", // For a Local CodeProject.com install
// For testing module downloads without fear of your existing modules getting nuked
diff --git a/src/API/Server/FrontEnd/appsettings.json b/src/API/Server/FrontEnd/appsettings.json
index 5ca047eb..458c5f6f 100644
--- a/src/API/Server/FrontEnd/appsettings.json
+++ b/src/API/Server/FrontEnd/appsettings.json
@@ -73,6 +73,10 @@
// This needs to be set to allow module uploads and installs via the API
"InstallPassword": null,
+ // The time allowed for a module to be installed. 20 mins should be plenty, but for a Raspberry
+ // or Orange Pi, or slow internet, it will need longer.
+ "ModuleInstallTimeout": "00:20:00",
+
// Location of the Json list of modules that can be downloaded
"ModuleListUrl": "https://www.codeproject.com/ai/modules/list",
diff --git a/src/API/Server/FrontEnd/modules.json b/src/API/Server/FrontEnd/modules.json
index 060692f6..45dfc5b8 100644
--- a/src/API/Server/FrontEnd/modules.json
+++ b/src/API/Server/FrontEnd/modules.json
@@ -2,8 +2,8 @@
{
"ModuleId": "ALPR",
"Name": "License Plate Reader",
- "Version": "2.2",
- "Description": "Detects and readers licence plates using YOLO object detection and the PaddleOCR toolkit",
+ "Version": "2.5",
+ "Description": "Detects and readers single-line and multi-line licence plates using YOLO object detection and the PaddleOCR toolkit",
"Platforms": [
"windows",
"linux",
@@ -11,14 +11,16 @@
"macos-arm64"
],
"Runtime": "python37",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"2.0.8"
],
- "ReleaseDate": "2022-11-01"
+ "ReleaseDate": "2022-11-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "2.1",
@@ -26,7 +28,9 @@
"2.0.9",
"2.0.9"
],
- "ReleaseDate": "2022-12-01"
+ "ReleaseDate": "2022-12-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "2.2",
@@ -34,7 +38,39 @@
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "2.3",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-04-20",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
+ },
+ {
+ "ModuleVersion": "2.4",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-05-10",
+ "ReleaseNotes": "PaddlePaddle install more reliable",
+ "Importance": "Minor"
+ },
+ {
+ "ModuleVersion": "2.5",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-06-04",
+ "ReleaseNotes": "Updated PaddlePaddle",
+ "Importance": null
}
],
"License": "SSPL",
@@ -44,7 +80,7 @@
{
"ModuleId": "BackgroundRemover",
"Name": "Background Remover",
- "Version": "1.2",
+ "Version": "1.4",
"Description": "Automatically removes the background from a picture",
"Platforms": [
"windows",
@@ -52,14 +88,16 @@
"macos-arm64"
],
"Runtime": "python39",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"2.0.8"
],
- "ReleaseDate": "2022-11-01"
+ "ReleaseDate": "2022-11-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.1",
@@ -67,15 +105,39 @@
"1.6.9",
"2.0.8"
],
- "ReleaseDate": "2022-11-01"
+ "ReleaseDate": "2022-11-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.2",
+ "ServerVersionRange": [
+ "2.1.0",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
+ "ServerVersionRange": [
+ "2.1.0",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-04-20",
+ "ReleaseNotes": "Install improved for GPU enabled systems",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.4",
"ServerVersionRange": [
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-08-05",
+ "ReleaseNotes": "Bugs in error reporting corrected",
+ "Importance": "Minor"
}
],
"License": "SSPL",
@@ -85,7 +147,7 @@
{
"ModuleId": "Cartooniser",
"Name": "Cartooniser",
- "Version": "1.0",
+ "Version": "1.1",
"Description": "Convert a photo into an anime style cartoon",
"Platforms": [
"windows",
@@ -95,14 +157,26 @@
"macos-arm64"
],
"Runtime": "python39",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"2.1",
""
],
- "ReleaseDate": "2023-03-28"
+ "ReleaseDate": "2023-03-28",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.1",
+ "ServerVersionRange": [
+ "2.1.7",
+ ""
+ ],
+ "ReleaseDate": "2023-04-29",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
}
],
"License": "MIT",
@@ -112,7 +186,7 @@
{
"ModuleId": "FaceProcessing",
"Name": "Face Processing",
- "Version": "1.2",
+ "Version": "1.5",
"Description": "A number of Face image APIs including detect, recognize, and compare.",
"Platforms": [
"windows",
@@ -122,14 +196,16 @@
"macos-arm64"
],
"Runtime": "python37",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"2.0.8"
],
- "ReleaseDate": "2022-03-01"
+ "ReleaseDate": "2022-03-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.2",
@@ -137,17 +213,108 @@
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-05-17",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.4",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-08-05",
+ "ReleaseNotes": "Bugs in error reporting corrected",
+ "Importance": "Minor"
+ },
+ {
+ "ModuleVersion": "1.5",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-08-12",
+ "ReleaseNotes": "PyTorch version downgrade",
+ "Importance": null
}
],
"License": "GPL-3.0",
"LicenseUrl": "https://opensource.org/licenses/GPL-3.0",
"Downloads": 0
},
+ {
+ "ModuleId": "ObjectDetectionCoral",
+ "Name": "ObjectDetection (Coral)",
+ "Version": "1.3",
+ "Description": "The object detection module uses the Coral TPU to locate and classify the objects the models have been trained on.",
+ "Platforms": [
+ "windows",
+ "linux",
+ "linux-arm64",
+ "macos",
+ "macos-arm64"
+ ],
+ "Runtime": "python37",
+ "ModuleReleases": [
+ {
+ "ModuleVersion": "1.0",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-07-11",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.1",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-07-12",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.2",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-07-12",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-08-11",
+ "ReleaseNotes": "installer corrections, macOS/Ubuntu support improved",
+ "Importance": null
+ }
+ ],
+ "License": "Apache-2.0",
+ "LicenseUrl": "https://opensource.org/licenses/Apache-2.0",
+ "Downloads": 0
+ },
{
"ModuleId": "ObjectDetectionNet",
"Name": "Object Detection (YOLOv5 .NET)",
- "Version": "1.2",
+ "Version": "1.5",
"Description": "Provides Object Detection using YOLOv5 ONNX models with DirectML. This module is best for those on Windows and Linux without CUDA enabled GPUs",
"Platforms": [
"windows",
@@ -156,31 +323,67 @@
"macos",
"macos-arm64"
],
- "Runtime": "dotnet",
- "VersionCompatibililty": [
+ "Runtime": "execute",
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"2.0.8"
],
- "ReleaseDate": "2022-06-01"
+ "ReleaseDate": "2022-06-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.1",
"ServerVersionRange": [
- "2.1",
- "2.1"
+ "2.1.0",
+ "2.1.0"
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
},
{
"ModuleVersion": "1.2",
"ServerVersionRange": [
- "2.1",
+ "2.1.0",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-04-09",
+ "ReleaseNotes": "Corrected installer issues",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
+ "ServerVersionRange": [
+ "2.1.0",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-04-20",
+ "ReleaseNotes": "Corrected module launch command",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.4",
+ "ServerVersionRange": [
+ "2.1.8",
+ "2.1.8"
+ ],
+ "ReleaseDate": "2023-04-20",
+ "ReleaseNotes": "Minor changes in module setup",
+ "Importance": "Minor"
+ },
+ {
+ "ModuleVersion": "1.5",
+ "ServerVersionRange": [
+ "2.1.9",
""
],
- "ReleaseDate": "2023-04-09"
+ "ReleaseDate": "2023-05-04",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
}
],
"License": "MIT",
@@ -190,8 +393,8 @@
{
"ModuleId": "ObjectDetectionTFLite",
"Name": "ObjectDetection (TF-Lite)",
- "Version": "1.2",
- "Description": "The object detection module Tensorflow Lite to locate and classify the objects the models have been trained on.",
+ "Version": "1.4",
+ "Description": "The object detection module uses Tensorflow Lite to locate and classify the objects the models have been trained on.",
"Platforms": [
"windows",
"linux",
@@ -200,30 +403,56 @@
"macos-arm64"
],
"Runtime": "python39",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
- "ModuleVersion": "1.2",
+ "ModuleVersion": "1.0",
"ServerVersionRange": [
"2.1",
""
],
- "ReleaseDate": "2023-04-10"
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.1",
"ServerVersionRange": [
"2.1",
- "2.1"
+ ""
],
- "ReleaseDate": "2023-04-03"
+ "ReleaseDate": "2023-04-03",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
- "ModuleVersion": "1.0",
+ "ModuleVersion": "1.2",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-04-10",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-04-10",
+ "ReleaseNotes": "Updated Windows installer",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.4",
"ServerVersionRange": [
"2.1",
- "2.1"
+ ""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-05-17",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
}
],
"License": "Apache-2.0",
@@ -233,38 +462,97 @@
{
"ModuleId": "ObjectDetectionYolo",
"Name": "Object Detection (YOLOv5 6.2)",
- "Version": "1.2",
- "Description": "Provides Object Detection using YOLOv5 v6.2 library with support for CPUs and CUDA enabled GPUs.",
+ "Version": "1.4",
+ "Description": "Provides Object Detection using YOLOv5 6.2 targeting CUDA 11.7/Torch 1.13 for newer GPUs.",
"Platforms": [
"all"
],
"Runtime": "python37",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"2.0.8"
],
- "ReleaseDate": "2022-03-01"
+ "ReleaseDate": "2022-03-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.2",
+ "ServerVersionRange": [
+ "2.1.0",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
+ "ServerVersionRange": [
+ "2.1.7",
+ ""
+ ],
+ "ReleaseDate": "2023-04-29",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
+ },
+ {
+ "ModuleVersion": "1.4",
"ServerVersionRange": [
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-08-12",
+ "ReleaseNotes": "PyTorch version downgrade",
+ "Importance": null
}
],
"License": "GPL-3.0",
"LicenseUrl": "https://opensource.org/licenses/GPL-3.0",
"Downloads": 0
},
+ {
+ "ModuleId": "ObjectDetectionYoloRKNN",
+ "Name": "Object Detection (YOLOv5 RKNN)",
+ "Version": "1.1",
+ "Description": "Provides Object Detection using YOLOv5 RKNN models. This module only works with Rockchip RK3588/RK3588S NPUs like the Orange Pi 5/5B/5 Plus",
+ "Platforms": [
+ "linux-arm64"
+ ],
+ "Runtime": "python39",
+ "ModuleReleases": [
+ {
+ "ModuleVersion": "1.0",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-08-06",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.1",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-08-06",
+ "ReleaseNotes": "Corrected installer in docker environment",
+ "Importance": null
+ }
+ ],
+ "License": "Apache-2.0",
+ "LicenseUrl": "https://opensource.org/licenses/Apache-2.0",
+ "Downloads": 0
+ },
{
"ModuleId": "OCR",
"Name": "Optical Character Recognition",
- "Version": "1.2",
+ "Version": "1.4",
"Description": "Provides OCR support using the PaddleOCR toolkit",
"Platforms": [
"windows",
@@ -273,14 +561,16 @@
"macos-arm64"
],
"Runtime": "python37",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"2.0.8"
],
- "ReleaseDate": "2022-11-01"
+ "ReleaseDate": "2022-11-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.2",
@@ -288,7 +578,29 @@
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-05-15",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
+ },
+ {
+ "ModuleVersion": "1.4",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-05-10",
+ "ReleaseNotes": "PaddlePaddle install more reliable",
+ "Importance": "Minor"
}
],
"License": "Apache 2.0",
@@ -298,20 +610,62 @@
{
"ModuleId": "PortraitFilter",
"Name": "Portrait Filter",
- "Version": "1.1",
+ "Version": "1.4",
"Description": "Provides a depth-of-field (bokeh) effect on images. Great for selfies.",
"Platforms": [
"windows"
],
"Runtime": "execute",
- "VersionCompatibililty": [
+ "ModuleReleases": [
+ {
+ "ModuleVersion": "1.0",
+ "ServerVersionRange": [
+ "1.0",
+ "2.0.8"
+ ],
+ "ReleaseDate": "2022-06-01",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
{
"ModuleVersion": "1.1",
+ "ServerVersionRange": [
+ "2.1",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.2",
+ "ServerVersionRange": [
+ "2.1",
+ "2.1.7"
+ ],
+ "ReleaseDate": "2023-04-20",
+ "ReleaseNotes": "Updated launch command",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
+ "ServerVersionRange": [
+ "2.1",
+ "2.1.8"
+ ],
+ "ReleaseDate": "2023-05-03",
+ "ReleaseNotes": "Minor module initialisation changes",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.4",
"ServerVersionRange": [
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-05-17",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
}
],
"License": "MIT",
@@ -321,7 +675,7 @@
{
"ModuleId": "SceneClassifier",
"Name": "Scene Classification",
- "Version": "1.2",
+ "Version": "1.3",
"Description": "Classifies an image according to one of 365 pre-trained scenes",
"Platforms": [
"windows",
@@ -331,22 +685,46 @@
"macos-arm64"
],
"Runtime": "python37",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"2.0.8"
],
- "ReleaseDate": "2022-03-01"
+ "ReleaseDate": "2022-03-01",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.1",
+ "ServerVersionRange": [
+ "2.1",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
},
{
"ModuleVersion": "1.2",
+ "ServerVersionRange": [
+ "2.1",
+ "2.1.8"
+ ],
+ "ReleaseDate": "2023-05-03",
+ "ReleaseNotes": "Minor module initialisation changes",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
"ServerVersionRange": [
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-05-17",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
}
],
"License": "Apache 2.0",
@@ -356,21 +734,53 @@
{
"ModuleId": "SentimentAnalysis",
"Name": "Sentiment Analysis",
- "Version": "1.1",
- "Description": "Provides an alaysis of the sentiment of a piece of text. Positive or negative?",
+ "Version": "1.3",
+ "Description": "Provides an analysis of the sentiment of a piece of text. Positive or negative?",
"Platforms": [
"windows",
"macos"
],
"Runtime": "execute",
- "VersionCompatibililty": [
+ "ModuleReleases": [
+ {
+ "ModuleVersion": "1.0",
+ "ServerVersionRange": [
+ "1.0",
+ "2.0.8"
+ ],
+ "ReleaseDate": "2022-06-01",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
{
"ModuleVersion": "1.1",
+ "ServerVersionRange": [
+ "2.1",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.2",
+ "ServerVersionRange": [
+ "2.1",
+ "2.1.8"
+ ],
+ "ReleaseDate": "2023-05-03",
+ "ReleaseNotes": "Minor module initialisation changes",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
"ServerVersionRange": [
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-05-17",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
}
],
"License": "CC-BY-4.0",
@@ -380,7 +790,7 @@
{
"ModuleId": "SuperResolution",
"Name": "Super Resolution",
- "Version": "1.3",
+ "Version": "1.5",
"Description": "Increases the resolution of an image using AI",
"Platforms": [
"windows",
@@ -390,14 +800,16 @@
"macos-arm64"
],
"Runtime": "python39",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"2.6.8"
],
- "ReleaseDate": "2022-03-01"
+ "ReleaseDate": "2022-03-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.1",
@@ -405,15 +817,19 @@
"2.6.9",
"2.0.8"
],
- "ReleaseDate": "2022-11-01"
+ "ReleaseDate": "2022-11-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.2",
"ServerVersionRange": [
"2.1",
- "2.1"
+ ""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
},
{
"ModuleVersion": "1.3",
@@ -421,7 +837,29 @@
"2.1",
""
],
- "ReleaseDate": "2023-04-11"
+ "ReleaseDate": "2023-04-11",
+ "ReleaseNotes": "Missing assets restored",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.4",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-04-11",
+ "ReleaseNotes": "Corrected inferenceMs type",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.5",
+ "ServerVersionRange": [
+ "2.1",
+ ""
+ ],
+ "ReleaseDate": "2023-05-17",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
}
],
"License": "Apache 2.0",
@@ -431,7 +869,7 @@
{
"ModuleId": "TextSummary",
"Name": "Text Summary",
- "Version": "1.2",
+ "Version": "1.3",
"Description": "Summarizes text content by selecting a number of sentences that are most representitive of the content.",
"Platforms": [
"windows",
@@ -441,14 +879,16 @@
"macos-arm64"
],
"Runtime": "python37",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"1.6.8"
],
- "ReleaseDate": "2022-11-01"
+ "ReleaseDate": "2022-11-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.1",
@@ -456,26 +896,75 @@
"1.6.9",
"2.0.8"
],
- "ReleaseDate": "2022-11-01"
+ "ReleaseDate": "2022-11-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.2",
+ "ServerVersionRange": [
+ "2.1",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
"ServerVersionRange": [
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-05-17",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
}
],
"License": "No License",
"LicenseUrl": "https://github.com/edubey/text-summarizer",
"Downloads": 0
},
+ {
+ "ModuleId": "TrainingYoloV5",
+ "Name": "Training for YoloV5 6.2",
+ "Version": "1.1",
+ "Description": "Train custom models for YOLOv5 v6.2 with support for CPUs, CUDA enabled GPUs, and Apple Silicon.",
+ "Platforms": [
+ "all"
+ ],
+ "Runtime": "python39",
+ "ModuleReleases": [
+ {
+ "ModuleVersion": "1.0",
+ "ServerVersionRange": [
+ "2.1.10",
+ ""
+ ],
+ "ReleaseDate": "2022-08-02",
+ "ReleaseNotes": null,
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.1",
+ "ServerVersionRange": [
+ "2.1.11",
+ ""
+ ],
+ "ReleaseDate": "2023-08-12",
+ "ReleaseNotes": "Added \u0027patience\u0027, \u0027workers\u0027 as parameters",
+ "Importance": null
+ }
+ ],
+ "License": "GPL-3.0",
+ "LicenseUrl": "https://opensource.org/licenses/GPL-3.0",
+ "Downloads": 0
+ },
{
"ModuleId": "YOLOv5-3.1",
"Name": "Object Detection (YOLOv5 3.1)",
- "Version": "1.2",
- "Description": "The object detection module uses YOLO (You Only Look Once) to locate and classify the objects the models have been trained on. At this point there are 80 different types of objects that can be detected.",
+ "Version": "1.3",
+ "Description": "Provides Object Detection using YOLOv5 3.1 targeting CUDA 10.2/Torch 1.7 for older GPUs.",
"Platforms": [
"windows",
"linux",
@@ -483,22 +972,36 @@
"macos"
],
"Runtime": "python37",
- "VersionCompatibililty": [
+ "ModuleReleases": [
{
"ModuleVersion": "1.0",
"ServerVersionRange": [
"1.0",
"2.0.8"
],
- "ReleaseDate": "2022-11-01"
+ "ReleaseDate": "2022-11-01",
+ "ReleaseNotes": null,
+ "Importance": null
},
{
"ModuleVersion": "1.2",
+ "ServerVersionRange": [
+ "2.1",
+ "2.1.6"
+ ],
+ "ReleaseDate": "2023-03-20",
+ "ReleaseNotes": "Updated for CodeProject.AI Server 2.1",
+ "Importance": null
+ },
+ {
+ "ModuleVersion": "1.3",
"ServerVersionRange": [
"2.1",
""
],
- "ReleaseDate": "2023-03-20"
+ "ReleaseDate": "2023-05-17",
+ "ReleaseNotes": "Updated module settings",
+ "Importance": "Minor"
}
],
"License": "GPL-3.0",
diff --git a/src/API/Server/FrontEnd/triggers.json b/src/API/Server/FrontEnd/triggers.json
new file mode 100644
index 00000000..c0b1feff
--- /dev/null
+++ b/src/API/Server/FrontEnd/triggers.json
@@ -0,0 +1,23 @@
+{
+ "triggersSection": {
+ "triggers": [
+ /*
+ {
+ "Queue" : "objectdetection_queue",
+ "PredictionsCollectionName" : "predictions",
+ "PropertyName" : "label",
+ "PropertyValue" : "car",
+ "PropertyComparison" : "equals",
+ "Confidence" : 0.5,
+ "ConfidenceComparison" : "greaterthan",
+ "PlatformTasks" : {
+ "Windows" : { "Command": "cmd", "Args": "/c echo Hi Windows. I see a car", "Type": "Command" },
+ "Linux" : { "Command": "bash", "Args": "echo Hi Linux. I see a car", "Type": "Command" },
+ "LinuxArm64" : { "Command": "bash", "Args": "echo Hi Linux. I see a car", "Type": "Command" },
+ "macOS" : { "Command": "zsh", "Args": "echo Hi Linux. I see a car", "Type": "Command" },
+ }
+ }
+ */
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/API/Server/FrontEnd/version.json b/src/API/Server/FrontEnd/version.json
index 42d496f9..81b7e950 100644
--- a/src/API/Server/FrontEnd/version.json
+++ b/src/API/Server/FrontEnd/version.json
@@ -3,12 +3,12 @@
"versionInfo": {
"Major": 2,
"Minor": 1,
- "Patch": 0,
+ "Patch": 12,
"Build": 0,
"PreRelease": "Beta",
"SecurityUpdate": false,
- "File": "CodeProject.AI.Server-2.1.0.zip",
- "ReleaseNotes": "New and Improved module system, Performance improvements, Raspberry Pi Coral.AI support and bugs fixes."
+ "File": "CodeProject.AI.Server-2.1.12.zip",
+ "ReleaseNotes": "Improvements to module installers."
}
}
}
\ No newline at end of file
diff --git a/src/API/Server/FrontEnd/wwwroot/Index.html b/src/API/Server/FrontEnd/wwwroot/Index.html
index f2d42391..692bfc23 100644
--- a/src/API/Server/FrontEnd/wwwroot/Index.html
+++ b/src/API/Server/FrontEnd/wwwroot/Index.html
@@ -16,8 +16,8 @@
-
-
+
+
@@ -1208,7 +1544,7 @@