From be7d2e3f4f33c23b37f5715ee9b203d655e4a34b Mon Sep 17 00:00:00 2001 From: njobvu-fullstack Date: Fri, 3 Jul 2026 00:21:36 +0000 Subject: [PATCH 1/4] feat: enable GPU selection for YOLO inference (integrated changes) Co-authored-by: multica-agent --- routes/inference/yoloInference.js | 4 +- tests/integration/yoloInference.test.js | 162 +++++++++++++++++++++ views/training/yolovXInferenceSettings.ejs | 8 +- 3 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 tests/integration/yoloInference.test.js diff --git a/routes/inference/yoloInference.js b/routes/inference/yoloInference.js index 409ffb39..de5b9f7f 100644 --- a/routes/inference/yoloInference.js +++ b/routes/inference/yoloInference.js @@ -14,7 +14,7 @@ async function yoloInference(req, res) { yolovxPath = req.body.yolovx_path, log = `${date}.log`, inferenceFile = req.body.inference_file, - device = req.body.device, + device = req.body.device || "cpu", options = req.body.options, yoloTask = req.body.yolo_task, weightName = req.body.weights; @@ -154,7 +154,7 @@ async function yoloInference(req, res) { } } - var cmd = `python3 ${yoloScript} -d ${runPath} -i ${inferenceFilePath} -n ${classesPath} -l ${absUltralyticsProjectRun}/${log} -f ${ultralyticsPath} -w ${weightPath} -t ${yoloTask}`; + var cmd = `python3 ${yoloScript} -d ${runPath} -i ${inferenceFilePath} -n ${classesPath} -l ${absUltralyticsProjectRun}/${log} -f ${ultralyticsPath} -w ${weightPath} -t ${yoloTask} -D ${device}`; var success = ""; var error = ""; diff --git a/tests/integration/yoloInference.test.js b/tests/integration/yoloInference.test.js new file mode 100644 index 00000000..1a78a3aa --- /dev/null +++ b/tests/integration/yoloInference.test.js @@ -0,0 +1,162 @@ +// Set up global sqlite3 mock before requiring app +jest.mock('decompress-zip', () => jest.fn()); +jest.mock('decompress-zip/lib/extractors', () => ({ + folder: jest.fn(), +})); +jest.mock('ffmpeg', () => jest.fn()); +jest.mock('sharp', () => jest.fn()); +jest.mock('unzipper', () => jest.fn()); + +const mockExec = jest.fn((cmd, cb) => cb(null, 'stdout', 'stderr')); +jest.mock('child_process', () => ({ + exec: mockExec, +})); + +jest.mock('sqlite3', () => { + const mockDb = { + run: jest.fn((...cbArgs) => { + const cb = cbArgs[cbArgs.length - 1]; + if (typeof cb === 'function') cb(null); + return { lastID: 1, changes: 1 }; + }), + get: jest.fn((...cbArgs) => { + const cb = cbArgs[cbArgs.length - 1]; + if (typeof cb === 'function') cb(null, {}); + }), + all: jest.fn((...cbArgs) => { + const cb = cbArgs[cbArgs.length - 1]; + if (typeof cb === 'function') cb(null, []); + }), + close: jest.fn((cb) => cb && cb()), + getAsync: jest.fn().mockResolvedValue({ + 'COUNT(*)': 10, + Admin: 'testuser', + PDescription: 'Test project description', + AutoSave: 1 + }), + allAsync: jest.fn().mockResolvedValue([ + { CName: 'class1' }, + { CName: 'class2' }, + { IName: 'image1.jpg' }, + { IName: 'image2.jpg' }, + { Username: 'testuser' } + ]), + }; + + const mockModule = { + OPEN_CREATE: 1, + OPEN_READWRITE: 2, + OPEN_READONLY: 1, + Database: jest.fn((...args) => { + const cb = args[1]; + if (typeof cb === 'function') cb(null); + return mockDb; + }), + verbose: jest.fn().mockImplementation(() => mockModule), + }; + + return mockModule; +}); + +jest.mock('socket.io-client', () => ({ + protocol: 'http', +})); + +// Mock probe module +jest.mock('probe-image-size', () => ({ + sync: jest.fn(() => ({ width: 800, height: 600 })), +})); + +// Mock fs module +jest.mock('fs', () => ({ + existsSync: jest.fn().mockReturnValue(true), + mkdirSync: jest.fn(), + writeFile: jest.fn((path, data, callback) => callback(null)), + writeFileSync: jest.fn(), + readdirSync: jest.fn().mockReturnValue([]), + unlinkSync: jest.fn(), + rename: jest.fn((oldPath, newPath, callback) => callback(null)), + readFileSync: jest.fn().mockReturnValue(Buffer.from('mock img data')), + copyFileSync: jest.fn(), + symlinkSync: jest.fn(), +})); + +global.sqlite3 = require('sqlite3'); +global.fs = require('fs'); +global.probe = require('probe-image-size'); + +const request = require('supertest'); +const app = require('../../app'); + +// Mock queries +jest.mock('../../queries/queries', () => ({ + project: { + getAllClasses: jest.fn().mockResolvedValue({ rows: [{ CName: 'class1' }, { CName: 'class2' }] }), + getAllImages: jest.fn().mockResolvedValue({ rows: [{ IName: 'image1.jpg' }] }), + getLabelsForImageName: jest.fn().mockResolvedValue({ rows: [{ CName: 'class1', X: 10, Y: 10, W: 100, H: 100 }] }), + }, +})); + +describe('YOLO Inference API', () => { + beforeAll(() => { + global.db = { + runAsync: jest.fn().mockResolvedValue(undefined), + allAsync: jest.fn().mockResolvedValue([]), + getAsync: jest.fn().mockResolvedValue({ row: { THING: 0 } }), + }; + global.currentPath = '/test/path/'; + global.projectDbClients = {}; + global.readdirAsync = jest.fn().mockResolvedValue([]); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should successfully run inference with custom device and respond with status 200', async () => { + const response = await request(app) + .post('/yolo-inf') + .set('Cookie', ['Username=testuser']) + .send({ + PName: 'testproj', + Admin: 'testuser', + yolovx_path: '/path/to/yolovx', + inference_file: 'image1.jpg', + yolo_task: 'detect', + weights: 'best.pt', + device: 'gpu1' + }); + + expect(response.status).toBe(200); + expect(response.headers['content-type']).toMatch(/json/); + expect(response.body).toEqual({ Success: 'YOLO Inference Started' }); + + // Verify that child_process.exec was called with -D gpu1 + expect(mockExec).toHaveBeenCalled(); + const cmdArg = mockExec.mock.calls[0][0]; + expect(cmdArg).toContain('-D gpu1'); + }); + + it('should successfully run inference with default cpu device if device is omitted', async () => { + const response = await request(app) + .post('/yolo-inf') + .set('Cookie', ['Username=testuser']) + .send({ + PName: 'testproj', + Admin: 'testuser', + yolovx_path: '/path/to/yolovx', + inference_file: 'image1.jpg', + yolo_task: 'detect', + weights: 'best.pt' + }); + + expect(response.status).toBe(200); + expect(response.headers['content-type']).toMatch(/json/); + expect(response.body).toEqual({ Success: 'YOLO Inference Started' }); + + // Verify that child_process.exec was called with -D cpu + expect(mockExec).toHaveBeenCalled(); + const cmdArg = mockExec.mock.calls[0][0]; + expect(cmdArg).toContain('-D cpu'); + }); +}); diff --git a/views/training/yolovXInferenceSettings.ejs b/views/training/yolovXInferenceSettings.ejs index 7d1ce7f4..4e3e28a8 100644 --- a/views/training/yolovXInferenceSettings.ejs +++ b/views/training/yolovXInferenceSettings.ejs @@ -160,6 +160,12 @@ <% } %> +
+
+ +

(cpu,gpu#,mps)

+

@@ -344,7 +350,7 @@ formData.append("yolo_task", document.getElementById("yolo_task").value); formData.append("yolo_mode", document.getElementById("yolo_mode").value); // formData.append("options", document.getElementById("options").value); - // formData.append("device", document.getElementById("device").value); + formData.append("device", document.getElementById("device").value); console.log(formData); From afd52b4a230e46cb55f212e565738d9e19e07fbc Mon Sep 17 00:00:00 2001 From: njobvu-fullstack Date: Fri, 3 Jul 2026 05:06:10 +0000 Subject: [PATCH 2/4] fix: double quote path parameters to prevent shell syntax error when weights file has parentheses Co-authored-by: multica-agent --- routes/inference/yoloInference.js | 2 +- tests/integration/yoloInference.test.js | 209 ++++++++++++------------ 2 files changed, 109 insertions(+), 102 deletions(-) diff --git a/routes/inference/yoloInference.js b/routes/inference/yoloInference.js index de5b9f7f..17c2e961 100644 --- a/routes/inference/yoloInference.js +++ b/routes/inference/yoloInference.js @@ -154,7 +154,7 @@ async function yoloInference(req, res) { } } - var cmd = `python3 ${yoloScript} -d ${runPath} -i ${inferenceFilePath} -n ${classesPath} -l ${absUltralyticsProjectRun}/${log} -f ${ultralyticsPath} -w ${weightPath} -t ${yoloTask} -D ${device}`; + var cmd = `python3 "${yoloScript}" -d "${runPath}" -i "${inferenceFilePath}" -n "${classesPath}" -l "${absUltralyticsProjectRun}/${log}" -f "${ultralyticsPath}" -w "${weightPath}" -t ${yoloTask} -D ${device}`; var success = ""; var error = ""; diff --git a/tests/integration/yoloInference.test.js b/tests/integration/yoloInference.test.js index 1a78a3aa..aec7a56a 100644 --- a/tests/integration/yoloInference.test.js +++ b/tests/integration/yoloInference.test.js @@ -1,4 +1,6 @@ -// Set up global sqlite3 mock before requiring app +const request = require('supertest'); + +// Set up mocks before requiring app jest.mock('decompress-zip', () => jest.fn()); jest.mock('decompress-zip/lib/extractors', () => ({ folder: jest.fn(), @@ -7,156 +9,161 @@ jest.mock('ffmpeg', () => jest.fn()); jest.mock('sharp', () => jest.fn()); jest.mock('unzipper', () => jest.fn()); -const mockExec = jest.fn((cmd, cb) => cb(null, 'stdout', 'stderr')); +const mockExec = jest.fn((cmd, cb) => cb(null, '', '')); jest.mock('child_process', () => ({ exec: mockExec, })); jest.mock('sqlite3', () => { - const mockDb = { - run: jest.fn((...cbArgs) => { - const cb = cbArgs[cbArgs.length - 1]; - if (typeof cb === 'function') cb(null); - return { lastID: 1, changes: 1 }; + const mockDbInstance = { + run: jest.fn((sql, params, cb) => { + const callback = typeof params === 'function' ? params : cb; + if (typeof callback === 'function') callback(null); }), - get: jest.fn((...cbArgs) => { - const cb = cbArgs[cbArgs.length - 1]; - if (typeof cb === 'function') cb(null, {}); + get: jest.fn((sql, params, cb) => { + const callback = typeof params === 'function' ? params : cb; + if (typeof callback === 'function') callback(null, {}); }), - all: jest.fn((...cbArgs) => { - const cb = cbArgs[cbArgs.length - 1]; - if (typeof cb === 'function') cb(null, []); + all: jest.fn((sql, params, cb) => { + const callback = typeof params === 'function' ? params : cb; + if (typeof callback === 'function') callback(null, []); }), - close: jest.fn((cb) => cb && cb()), - getAsync: jest.fn().mockResolvedValue({ - 'COUNT(*)': 10, - Admin: 'testuser', - PDescription: 'Test project description', - AutoSave: 1 - }), - allAsync: jest.fn().mockResolvedValue([ - { CName: 'class1' }, - { CName: 'class2' }, - { IName: 'image1.jpg' }, - { IName: 'image2.jpg' }, - { Username: 'testuser' } - ]), - }; - - const mockModule = { - OPEN_CREATE: 1, - OPEN_READWRITE: 2, - OPEN_READONLY: 1, - Database: jest.fn((...args) => { - const cb = args[1]; + close: jest.fn((cb) => { if (typeof cb === 'function') cb(null); - return mockDb; }), - verbose: jest.fn().mockImplementation(() => mockModule), }; - - return mockModule; + const sqlite3Mock = { + OPEN_READWRITE: 1, + OPEN_CREATE: 2, + Database: jest.fn(() => mockDbInstance), + verbose: () => sqlite3Mock, + }; + return sqlite3Mock; }); jest.mock('socket.io-client', () => ({ protocol: 'http', })); -// Mock probe module -jest.mock('probe-image-size', () => ({ - sync: jest.fn(() => ({ width: 800, height: 600 })), -})); - -// Mock fs module +// Mock fs functions jest.mock('fs', () => ({ existsSync: jest.fn().mockReturnValue(true), mkdirSync: jest.fn(), - writeFile: jest.fn((path, data, callback) => callback(null)), writeFileSync: jest.fn(), - readdirSync: jest.fn().mockReturnValue([]), - unlinkSync: jest.fn(), - rename: jest.fn((oldPath, newPath, callback) => callback(null)), - readFileSync: jest.fn().mockReturnValue(Buffer.from('mock img data')), copyFileSync: jest.fn(), symlinkSync: jest.fn(), + readFileSync: jest.fn().mockReturnValue(Buffer.from('mockImage')), + writeFile: jest.fn((path, data, callback) => callback(null)), })); -global.sqlite3 = require('sqlite3'); -global.fs = require('fs'); -global.probe = require('probe-image-size'); - -const request = require('supertest'); -const app = require('../../app'); +// Mock probe module +jest.mock('probe-image-size', () => ({ + sync: jest.fn(() => ({ width: 800, height: 600 })), +})); // Mock queries -jest.mock('../../queries/queries', () => ({ +const mockQueries = { project: { - getAllClasses: jest.fn().mockResolvedValue({ rows: [{ CName: 'class1' }, { CName: 'class2' }] }), + getAllClasses: jest.fn().mockResolvedValue({ rows: [{ CName: 'class1' }] }), getAllImages: jest.fn().mockResolvedValue({ rows: [{ IName: 'image1.jpg' }] }), - getLabelsForImageName: jest.fn().mockResolvedValue({ rows: [{ CName: 'class1', X: 10, Y: 10, W: 100, H: 100 }] }), + getLabelsForImageName: jest.fn().mockResolvedValue({ + rows: [{ CName: 'class1', X: 10, Y: 10, W: 100, H: 100 }] + }), }, -})); +}; +jest.mock('../../queries/queries', () => mockQueries); -describe('YOLO Inference API', () => { - beforeAll(() => { - global.db = { - runAsync: jest.fn().mockResolvedValue(undefined), - allAsync: jest.fn().mockResolvedValue([]), - getAsync: jest.fn().mockResolvedValue({ row: { THING: 0 } }), - }; - global.currentPath = '/test/path/'; - global.projectDbClients = {}; - global.readdirAsync = jest.fn().mockResolvedValue([]); - }); +// Define global variables expected by the application/routes +global.fs = require('fs'); +global.probe = require('probe-image-size'); +global.currentPath = '/mock/current/path/'; +global.configFile = {}; + +const app = require('../../app'); +describe('YOLO Inference Integration Tests', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('should successfully run inference with custom device and respond with status 200', async () => { + it('should retrieve custom device parameter and pass it to python command with -D', async () => { + const payload = { + PName: 'test-project', + Admin: 'admin-user', + yolovx_path: '/path/to/yolovx', + inference_file: 'inference_image.jpg', + device: 'gpu', + options: '', + yolo_task: 'detect', + weights: 'best.pt' + }; + const response = await request(app) .post('/yolo-inf') - .set('Cookie', ['Username=testuser']) - .send({ - PName: 'testproj', - Admin: 'testuser', - yolovx_path: '/path/to/yolovx', - inference_file: 'image1.jpg', - yolo_task: 'detect', - weights: 'best.pt', - device: 'gpu1' - }); + .set('Cookie', ['Username=test-user']) + .send(payload); expect(response.status).toBe(200); - expect(response.headers['content-type']).toMatch(/json/); expect(response.body).toEqual({ Success: 'YOLO Inference Started' }); - - // Verify that child_process.exec was called with -D gpu1 expect(mockExec).toHaveBeenCalled(); - const cmdArg = mockExec.mock.calls[0][0]; - expect(cmdArg).toContain('-D gpu1'); + + // Retrieve the executed command argument + const executedCmd = mockExec.mock.calls[0][0]; + expect(executedCmd).toContain('-D gpu'); }); - it('should successfully run inference with default cpu device if device is omitted', async () => { + it('should default device parameter to cpu when not provided and pass it to python command with -D', async () => { + const payload = { + PName: 'test-project', + Admin: 'admin-user', + yolovx_path: '/path/to/yolovx', + inference_file: 'inference_image.jpg', + options: '', + yolo_task: 'detect', + weights: 'best.pt' + }; + const response = await request(app) .post('/yolo-inf') - .set('Cookie', ['Username=testuser']) - .send({ - PName: 'testproj', - Admin: 'testuser', - yolovx_path: '/path/to/yolovx', - inference_file: 'image1.jpg', - yolo_task: 'detect', - weights: 'best.pt' - }); + .set('Cookie', ['Username=test-user']) + .send(payload); expect(response.status).toBe(200); - expect(response.headers['content-type']).toMatch(/json/); expect(response.body).toEqual({ Success: 'YOLO Inference Started' }); + expect(mockExec).toHaveBeenCalled(); + + // Retrieve the executed command argument + const executedCmd = mockExec.mock.calls[0][0]; + expect(executedCmd).toContain('-D cpu'); + }); + + it('should wrap path parameters with double quotes when executing the command, handling weights with parentheses', async () => { + const payload = { + PName: 'test-project', + Admin: 'admin-user', + yolovx_path: '/path/to/yolovx path', + inference_file: 'inference_image(1).jpg', + device: 'gpu', + options: '', + yolo_task: 'detect', + weights: 'best(1).pt' + }; - // Verify that child_process.exec was called with -D cpu + const response = await request(app) + .post('/yolo-inf') + .set('Cookie', ['Username=test-user']) + .send(payload); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ Success: 'YOLO Inference Started' }); expect(mockExec).toHaveBeenCalled(); - const cmdArg = mockExec.mock.calls[0][0]; - expect(cmdArg).toContain('-D cpu'); + + // Retrieve the executed command argument + const executedCmd = mockExec.mock.calls[0][0]; + + // Check that paths are wrapped in double quotes + expect(executedCmd).toContain('-w "/mock/current/path/public/projects/admin-user-test-project/training/weights/best(1).pt"'); + expect(executedCmd).toContain('-i "inference_image(1).jpg"'); + expect(executedCmd).toContain('-f "/path/to/yolovx path"'); }); }); From e408b751dab93eb818f5522d56e504cd1c0321ec Mon Sep 17 00:00:00 2001 From: njobvu-fullstack Date: Fri, 3 Jul 2026 05:09:26 +0000 Subject: [PATCH 3/4] fix: double-quote path parameters in Python script to prevent shell syntax errors Co-authored-by: multica-agent --- controllers/inference/datatovalues.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/controllers/inference/datatovalues.py b/controllers/inference/datatovalues.py index 4fd8c3ee..864c333c 100755 --- a/controllers/inference/datatovalues.py +++ b/controllers/inference/datatovalues.py @@ -235,19 +235,20 @@ def load_class_names(name_path): darknet_path + f" {yolo_task}" + f" {yolo_mode}" - + " predict model=" + + ' predict model="' + weight_path - + " source=" + + '" source="' + image_path - + " project=" + + '" project="' + data_path - + " name=output device=" + + '" name=output device=' + device + " save_txt=True save_conf=True" + " " + adv_options - + " 2>&1 > " + + ' 2>&1 > "' + log_file + + '"' ) print(cmd) From 847604f627003aff9049acae2e480276964f8973 Mon Sep 17 00:00:00 2001 From: FerretCode Date: Thu, 2 Jul 2026 22:15:51 -0700 Subject: [PATCH 4/4] use spawn instead of exec to support special characters --- routes/inference/yoloInference.js | 66 ++++++++++++------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/routes/inference/yoloInference.js b/routes/inference/yoloInference.js index 17c2e961..2282e09f 100644 --- a/routes/inference/yoloInference.js +++ b/routes/inference/yoloInference.js @@ -3,7 +3,7 @@ const path = require("path"); async function yoloInference(req, res) { try { - const { exec } = require("child_process"); + const { spawn } = require("child_process"); var date = Date.now(); @@ -154,57 +154,43 @@ async function yoloInference(req, res) { } } - var cmd = `python3 "${yoloScript}" -d "${runPath}" -i "${inferenceFilePath}" -n "${classesPath}" -l "${absUltralyticsProjectRun}/${log}" -f "${ultralyticsPath}" -w "${weightPath}" -t ${yoloTask} -D ${device}`; - - var success = ""; - var error = ""; - - fs.writeFileSync(`${absUltralyticsProjectRun}/${log}`, `${cmd}\n\n`); + const args = [ + yoloScript, + "-d", runPath, + "-i", inferenceFilePath, + "-n", classesPath, + "-l", `${absUltralyticsProjectRun}/${log}`, + "-f", ultralyticsPath, + "-w", weightPath, + "-t", yoloTask, + "-D", device + ]; + + const child = spawn("python3", args); + + child.stdout.on("data", (data) => { + global.logger.debug(`stdout: ${data}`); + }); - exec(cmd, (err, stdout, stderr) => { - if (err) { - global.logger.error(err); - global.logger.debug(`This is the error: ${err.message}`); - - if (err.message != "stdout maxBuffer length exceeded") { - success = err.message; - - fs.writeFile( - `${ultralyticsProjectRun}/${errFile}`, - success, - (err) => { - if (err) throw err; - }, - ); - } - } else if (stderr) { - global.logger.debug(`This is the stderr: ${stderr}`); - - if (stderr != "stdout maxBuffer length exceeded") { - fs.writeFile( - `${ultralyticsProjectRun}/${errFile}`, - stderr, - (err) => { - if (err) throw err; - }, - ); - } - } + child.stderr.on("data", (data) => { + global.logger.error(`stderr: ${data}`); + }); + child.on("close", (code) => { const completionData = { - status: err ? 'error' : 'success', + status: code === 0 ? 'success' : 'error', timestamp: date, zipAvailable: fs.existsSync(`${runPath}/inference_results.zip`), - csvAvailable: fs.existsSync(`${runPath}/inference_stats.csv`), - message: success + csvAvailable: fs.existsSync(`${runPath}/inference_stats.csv`) }; fs.writeFileSync( `${runPath}/done.log`, - `${cmd}\n\n${JSON.stringify(completionData, null, 2)}` + JSON.stringify(completionData, null, 2) ); }); + fs.writeFileSync(`${absUltralyticsProjectRun}/${log}`, `Arguments: ${args.join(" ")}\n\n`); res.send({ Success: `YOLO Inference Started` }); } catch (err) {