497 lines
22 KiB
JavaScript
497 lines
22 KiB
JavaScript
const express = require('express');
|
|
const multer = require('multer');
|
|
const upload = multer();
|
|
const TrainingProject = require('../models/TrainingProject.js');
|
|
const LabelStudioProject = require('../models/LabelStudioProject.js')
|
|
const { seedLabelStudio, updateStatus } = require('../services/seed-label-studio.js');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const {generateTrainingJson} = require('../services/generate-json-yolox.js')
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
// Ensure JSON bodies are parsed for all routes
|
|
router.use(express.json());
|
|
|
|
router.get('/seed', async (req, res) => {
|
|
const result = await seedLabelStudio();
|
|
res.json(result);
|
|
});
|
|
|
|
|
|
|
|
// Trigger generate-json-yolox.js
|
|
|
|
router.post('/generate-yolox-json', async (req, res) => {
|
|
const { project_id } = req.body;
|
|
if (!project_id) {
|
|
return res.status(400).json({ message: 'Missing project_id in request body' });
|
|
}
|
|
try {
|
|
// Generate COCO JSONs
|
|
// Find all TrainingProjectDetails for this project
|
|
const TrainingProjectDetails = require('../models/TrainingProjectDetails.js');
|
|
const detailsRows = await TrainingProjectDetails.findAll({ where: { project_id } });
|
|
if (!detailsRows || detailsRows.length === 0) {
|
|
return res.status(404).json({ message: 'No TrainingProjectDetails found for project ' + project_id });
|
|
}
|
|
// For each details row, generate coco.jsons and exp.py in projectfolder/project_details_id
|
|
const Training = require('../models/training.js');
|
|
const { saveYoloxExp } = require('../services/generate-yolox-exp.js');
|
|
const TrainingProject = require('../models/TrainingProject.js');
|
|
const trainingProject = await TrainingProject.findByPk(project_id);
|
|
const projectName = trainingProject.name ? trainingProject.name.replace(/\s+/g, '_') : `project_${project_id}`;
|
|
for (const details of detailsRows) {
|
|
const detailsId = details.id;
|
|
await generateTrainingJson(detailsId);
|
|
const trainings = await Training.findAll({ where: { project_details_id: detailsId } });
|
|
if (trainings.length === 0) continue;
|
|
// For each training, save exp.py in projectfolder/project_details_id
|
|
const outDir = path.join(__dirname, '..', projectName, String(detailsId));
|
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
for (const training of trainings) {
|
|
const expFilePath = path.join(outDir, 'exp.py');
|
|
await saveYoloxExp(training.id, expFilePath);
|
|
}
|
|
}
|
|
|
|
// Find all trainings for this project
|
|
// ...existing code...
|
|
res.json({ message: 'YOLOX JSON and exp.py generated for project ' + project_id });
|
|
} catch (err) {
|
|
console.error('Error generating YOLOX JSON:', err);
|
|
res.status(500).json({ message: 'Failed to generate YOLOX JSON', error: err.message });
|
|
}
|
|
});
|
|
|
|
// Start YOLOX training
|
|
const { spawn } = require('child_process');
|
|
router.post('/start-yolox-training', async (req, res) => {
|
|
try {
|
|
const { project_id, training_id } = req.body;
|
|
// Get project name
|
|
const trainingProject = await TrainingProject.findByPk(project_id);
|
|
const projectName = trainingProject.name ? trainingProject.name.replace(/\s+/g, '_') : `project_${project_id}`;
|
|
// Look up training row by id or project_details_id
|
|
const Training = require('../models/training.js');
|
|
let trainingRow = await Training.findByPk(training_id);
|
|
if (!trainingRow) {
|
|
trainingRow = await Training.findOne({ where: { project_details_id: training_id } });
|
|
}
|
|
if (!trainingRow) {
|
|
return res.status(404).json({ error: `Training row not found for id or project_details_id ${training_id}` });
|
|
}
|
|
const project_details_id = trainingRow.project_details_id;
|
|
// Use the generated exp.py from the correct project folder
|
|
const outDir = path.join(__dirname, '..', projectName, String(project_details_id));
|
|
const yoloxMainDir = '/home/kitraining/Yolox/YOLOX-main';
|
|
const expSrc = path.join(outDir, 'exp.py');
|
|
if (!fs.existsSync(expSrc)) {
|
|
return res.status(500).json({ error: `exp.py not found at ${expSrc}` });
|
|
}
|
|
// Source venv and run YOLOX training in YOLOX-main folder
|
|
const yoloxVenv = '/home/kitraining/Yolox/yolox_venv/bin/activate';
|
|
// Determine model argument based on selected_model and transfer_learning
|
|
let modelArg = '';
|
|
let cmd = '';
|
|
if (
|
|
trainingRow.transfer_learning &&
|
|
typeof trainingRow.transfer_learning === 'string' &&
|
|
trainingRow.transfer_learning.toLowerCase() === 'coco'
|
|
) {
|
|
// If transfer_learning is 'coco', add -o and modelArg
|
|
modelArg = ` -c /home/kitraining/Yolox/YOLOX-main/pretrained/${trainingRow.selected_model}`;
|
|
cmd = `bash -c 'source ${yoloxVenv} && python tools/train.py -f ${expSrc} -d 1 -b 8 --fp16 -o ${modelArg}.pth --cache'`;
|
|
} else if (
|
|
trainingRow.selected_model &&
|
|
trainingRow.selected_model.toLowerCase() === 'coco' &&
|
|
(!trainingRow.transfer_learning || trainingRow.transfer_learning === false)
|
|
) {
|
|
// If selected_model is 'coco' and not transfer_learning, add modelArg only
|
|
modelArg = ` -c /pretrained/${trainingRow.selected_model}`;
|
|
cmd = `bash -c 'source ${yoloxVenv} && python tools/train.py -f ${expSrc} -d 1 -b 8 --fp16 -o ${modelArg}.pth --cache'`;
|
|
} else {
|
|
// Default: no modelArg
|
|
cmd = `bash -c 'source ${yoloxVenv} && python tools/train.py -f ${expSrc} -d 1 -b 8 --fp16' --cache`;
|
|
}
|
|
console.log(cmd)
|
|
const child = spawn(cmd, { shell: true, cwd: yoloxMainDir });
|
|
child.stdout.pipe(process.stdout);
|
|
child.stderr.pipe(process.stderr);
|
|
|
|
res.json({ message: 'Training started' });
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Failed to start training', details: err.message });
|
|
}
|
|
});
|
|
|
|
// Get YOLOX training log
|
|
router.get('/training-log', async (req, res) => {
|
|
try {
|
|
const { project_id, training_id } = req.query;
|
|
const trainingProject = await TrainingProject.findByPk(project_id);
|
|
const projectName = trainingProject.name ? trainingProject.name.replace(/\s+/g, '_') : `project_${project_id}`;
|
|
const outDir = path.join(__dirname, '..', projectName, String(training_id));
|
|
const logPath = path.join(outDir, 'training.log');
|
|
if (!fs.existsSync(logPath)) {
|
|
return res.status(404).json({ error: 'Log not found' });
|
|
}
|
|
const logData = fs.readFileSync(logPath, 'utf8');
|
|
res.json({ log: logData });
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Failed to fetch log', details: err.message });
|
|
}
|
|
});
|
|
|
|
router.post('/training-projects', upload.single('project_image'), async (req, res) => {
|
|
try {
|
|
const { title, description } = req.body;
|
|
const classes = JSON.parse(req.body.classes);
|
|
const project_image = req.file ? req.file.buffer : null;
|
|
const project_image_type = req.file ? req.file.mimetype : null;
|
|
await TrainingProject.create({
|
|
title,
|
|
description,
|
|
classes,
|
|
project_image,
|
|
project_image_type
|
|
});
|
|
res.json({ message: 'Project created!' });
|
|
} catch (error) {
|
|
console.error('Error creating project:', error);
|
|
res.status(500).json({ message: 'Failed to create project', error: error.message });
|
|
}
|
|
});
|
|
|
|
router.get('/training-projects', async (req, res) => {
|
|
try {
|
|
const projects = await TrainingProject.findAll();
|
|
// Convert BLOB to base64 data URL for each project
|
|
const serialized = projects.map(project => {
|
|
const plain = project.get({ plain: true });
|
|
if (plain.project_image) {
|
|
const base64 = Buffer.from(plain.project_image).toString('base64');
|
|
const mimeType = plain.project_image_type || 'image/png';
|
|
plain.project_image = `data:${mimeType};base64,${base64}`;
|
|
}
|
|
return plain;
|
|
});
|
|
res.json(serialized);
|
|
} catch (error) {
|
|
res.status(500).json({ message: 'Failed to fetch projects', error: error.message });
|
|
}
|
|
});
|
|
|
|
router.get('/update-status', async (req, res) => {
|
|
res.json(updateStatus)
|
|
})
|
|
|
|
router.get('/label-studio-projects', async (req, res) => {
|
|
try {
|
|
const LabelStudioProject = require('../models/LabelStudioProject.js');
|
|
const Image = require('../models/Images.js');
|
|
const Annotation = require('../models/Annotation.js');
|
|
const labelStudioProjects = await LabelStudioProject.findAll();
|
|
const projectsWithCounts = await Promise.all(labelStudioProjects.map(async project => {
|
|
const plain = project.get({ plain: true });
|
|
// Get all images for this project
|
|
const images = await Image.findAll({ where: { project_id: plain.project_id } });
|
|
let annotationCounts = {};
|
|
if (images.length > 0) {
|
|
const imageIds = images.map(img => img.image_id);
|
|
// Get all annotations for these images
|
|
const annotations = await Annotation.findAll({ where: { image_id: imageIds } });
|
|
// Count by label
|
|
for (const ann of annotations) {
|
|
const label = ann.Label;
|
|
annotationCounts[label] = (annotationCounts[label] || 0) + 1;
|
|
}
|
|
}
|
|
plain.annotationCounts = annotationCounts;
|
|
return plain;
|
|
}));
|
|
res.json(projectsWithCounts);
|
|
} catch (error) {
|
|
res.status(500).json({ message: 'Failed to fetch projects', error: error.message });
|
|
}
|
|
})
|
|
|
|
|
|
// POST endpoint to create TrainingProjectDetails with all fields
|
|
router.post('/training-project-details', async (req, res) => {
|
|
try {
|
|
const {
|
|
project_id,
|
|
annotation_projects,
|
|
class_map,
|
|
description
|
|
} = req.body;
|
|
if (!project_id || !annotation_projects) {
|
|
return res.status(400).json({ message: 'Missing required fields' });
|
|
}
|
|
const TrainingProjectDetails = require('../models/TrainingProjectDetails.js');
|
|
const created = await TrainingProjectDetails.create({
|
|
project_id,
|
|
annotation_projects,
|
|
class_map: class_map || null,
|
|
description: description || null
|
|
});
|
|
res.json({ message: 'TrainingProjectDetails created', details: created });
|
|
} catch (error) {
|
|
res.status(500).json({ message: 'Failed to create TrainingProjectDetails', error: error.message });
|
|
}
|
|
});
|
|
|
|
// GET endpoint to fetch all TrainingProjectDetails
|
|
router.get('/training-project-details', async (req, res) => {
|
|
try {
|
|
const TrainingProjectDetails = require('../models/TrainingProjectDetails.js');
|
|
const details = await TrainingProjectDetails.findAll();
|
|
res.json(details);
|
|
} catch (error) {
|
|
res.status(500).json({ message: 'Failed to fetch TrainingProjectDetails', error: error.message });
|
|
}
|
|
});
|
|
|
|
// PUT endpoint to update class_map and description in TrainingProjectDetails
|
|
router.put('/training-project-details', async (req, res) => {
|
|
try {
|
|
const { project_id, class_map, description } = req.body;
|
|
if (!project_id || !class_map || !description) {
|
|
return res.status(400).json({ message: 'Missing required fields' });
|
|
}
|
|
const TrainingProjectDetails = require('../models/TrainingProjectDetails.js');
|
|
const details = await TrainingProjectDetails.findOne({ where: { project_id } });
|
|
if (!details) {
|
|
return res.status(404).json({ message: 'TrainingProjectDetails not found' });
|
|
}
|
|
details.class_map = class_map;
|
|
details.description = description;
|
|
await details.save();
|
|
res.json({ message: 'Class map and description updated', details });
|
|
} catch (error) {
|
|
res.status(500).json({ message: 'Failed to update class map or description', error: error.message });
|
|
}
|
|
});
|
|
|
|
// POST endpoint to receive YOLOX settings and save to DB (handles multipart/form-data)
|
|
router.post('/yolox-settings', upload.any(), async (req, res) => {
|
|
try {
|
|
const settings = req.body;
|
|
// Debug: Log all received fields and types
|
|
console.log('--- YOLOX settings received ---');
|
|
console.log('settings:', settings);
|
|
if (req.files && req.files.length > 0) {
|
|
console.log('Files received:', req.files.map(f => ({ fieldname: f.fieldname, originalname: f.originalname, size: f.size })));
|
|
}
|
|
// Declare requiredFields once
|
|
const requiredFields = ['project_details_id', 'exp_name', 'max_epoch', 'depth', 'width', 'activation', 'train', 'valid', 'test', 'selected_model', 'transfer_learning'];
|
|
// Log types of required fields
|
|
requiredFields.forEach(field => {
|
|
console.log(`Field '${field}': value='${settings[field]}', type='${typeof settings[field]}'`);
|
|
});
|
|
// Map select_model to selected_model if present
|
|
if (settings && settings.select_model && !settings.selected_model) {
|
|
settings.selected_model = settings.select_model;
|
|
delete settings.select_model;
|
|
}
|
|
// Lookup project_details_id from project_id
|
|
if (!settings.project_id || isNaN(Number(settings.project_id))) {
|
|
throw new Error('Missing or invalid project_id in request. Cannot assign training to a project.');
|
|
}
|
|
const TrainingProjectDetails = require('../models/TrainingProjectDetails.js');
|
|
let details = await TrainingProjectDetails.findOne({ where: { project_id: settings.project_id } });
|
|
if (!details) {
|
|
details = await TrainingProjectDetails.create({
|
|
project_id: settings.project_id,
|
|
annotation_projects: [],
|
|
class_map: null,
|
|
description: null
|
|
});
|
|
}
|
|
settings.project_details_id = details.id;
|
|
// Map 'act' from frontend to 'activation' for DB
|
|
if (settings.act !== undefined) {
|
|
settings.activation = settings.act;
|
|
delete settings.act;
|
|
}
|
|
// Type conversion for DB compatibility
|
|
[
|
|
'max_epoch', 'depth', 'width', 'warmup_epochs', 'warmup_lr', 'no_aug_epochs', 'min_lr_ratio', 'weight_decay', 'momentum', 'print_interval', 'eval_interval', 'test_conf', 'nmsthre', 'multiscale_range', 'degrees', 'translate', 'shear', 'train', 'valid', 'test'
|
|
].forEach(f => {
|
|
if (settings[f] !== undefined) settings[f] = Number(settings[f]);
|
|
});
|
|
// Improved boolean conversion
|
|
['ema', 'enable_mixup', 'save_history_ckpt'].forEach(f => {
|
|
if (settings[f] !== undefined) {
|
|
if (typeof settings[f] === 'string') {
|
|
settings[f] = settings[f].toLowerCase() === 'true';
|
|
} else {
|
|
settings[f] = Boolean(settings[f]);
|
|
}
|
|
}
|
|
});
|
|
// Improved array conversion
|
|
['mosaic_scale', 'mixup_scale', 'scale'].forEach(f => {
|
|
if (settings[f] && typeof settings[f] === 'string') {
|
|
settings[f] = settings[f]
|
|
.split(',')
|
|
.map(s => Number(s.trim()))
|
|
.filter(n => !isNaN(n));
|
|
}
|
|
});
|
|
// Trim all string fields
|
|
Object.keys(settings).forEach(f => {
|
|
if (typeof settings[f] === 'string') settings[f] = settings[f].trim();
|
|
});
|
|
// Set default for transfer_learning if missing
|
|
if (settings.transfer_learning === undefined) settings.transfer_learning = false;
|
|
// Convert empty string seed to null
|
|
if ('seed' in settings && (settings.seed === '' || settings.seed === undefined)) {
|
|
settings.seed = null;
|
|
}
|
|
// Validate required fields for training table
|
|
for (const field of requiredFields) {
|
|
if (settings[field] === undefined || settings[field] === null || settings[field] === '') {
|
|
console.error('Missing required field:', field, 'Value:', settings[field]);
|
|
throw new Error('Missing required field: ' + field);
|
|
}
|
|
}
|
|
console.log('Received YOLOX settings:', settings);
|
|
// Handle uploaded model file (ckpt_upload)
|
|
if (req.files && req.files.length > 0) {
|
|
const ckptFile = req.files.find(f => f.fieldname === 'ckpt_upload');
|
|
if (ckptFile) {
|
|
const uploadDir = path.join(__dirname, '..', 'uploads');
|
|
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);
|
|
const filename = ckptFile.originalname || `uploaded_model_${settings.project_id}.pth`;
|
|
const filePath = path.join(uploadDir, filename);
|
|
fs.writeFileSync(filePath, ckptFile.buffer);
|
|
settings.model_upload = filePath;
|
|
}
|
|
}
|
|
// Save settings to DB only (no file)
|
|
const { pushYoloxExpToDb } = require('../services/push-yolox-exp.js');
|
|
const training = await pushYoloxExpToDb(settings);
|
|
res.json({ message: 'YOLOX settings saved to DB', training });
|
|
} catch (error) {
|
|
console.error('Error in /api/yolox-settings:', error.stack || error);
|
|
res.status(500).json({ message: 'Failed to save YOLOX settings', error: error.message });
|
|
}
|
|
});
|
|
|
|
// POST endpoint to receive binary model file and save to disk (not DB)
|
|
router.post('/yolox-settings/upload', async (req, res) => {
|
|
try {
|
|
const projectId = req.query.project_id;
|
|
if (!projectId) return res.status(400).json({ message: 'Missing project_id in query' });
|
|
// Save file to disk
|
|
const uploadDir = path.join(__dirname, '..', 'uploads');
|
|
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);
|
|
const filename = req.headers['x-upload-filename'] || `uploaded_model_${projectId}.pth`;
|
|
const filePath = path.join(uploadDir, filename);
|
|
const chunks = [];
|
|
req.on('data', chunk => chunks.push(chunk));
|
|
req.on('end', async () => {
|
|
const buffer = Buffer.concat(chunks);
|
|
fs.writeFile(filePath, buffer, async err => {
|
|
if (err) {
|
|
console.error('Error saving file:', err);
|
|
return res.status(500).json({ message: 'Failed to save model file', error: err.message });
|
|
}
|
|
// Update latest training row for this project with file path
|
|
try {
|
|
const TrainingProjectDetails = require('../models/TrainingProjectDetails.js');
|
|
const Training = require('../models/training.js');
|
|
// Find details row for this project
|
|
const details = await TrainingProjectDetails.findOne({ where: { project_id: projectId } });
|
|
if (!details) return res.status(404).json({ message: 'No TrainingProjectDetails found for project_id' });
|
|
// Find latest training for this details row
|
|
const training = await Training.findOne({ where: { project_details_id: details.id }, order: [['createdAt', 'DESC']] });
|
|
if (!training) return res.status(404).json({ message: 'No training found for project_id' });
|
|
// Save file path to model_upload field
|
|
training.model_upload = filePath;
|
|
await training.save();
|
|
res.json({ message: 'Model file uploaded and saved to disk', filename, trainingId: training.id });
|
|
} catch (dbErr) {
|
|
console.error('Error updating training with file path:', dbErr);
|
|
res.status(500).json({ message: 'File saved but failed to update training row', error: dbErr.message });
|
|
}
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error('Error in /api/yolox-settings/upload:', error.stack || error);
|
|
res.status(500).json({ message: 'Failed to upload model file', error: error.message });
|
|
}
|
|
});
|
|
|
|
// GET endpoint to fetch all trainings (optionally filtered by project_id)
|
|
router.get('/trainings', async (req, res) => {
|
|
try {
|
|
const project_id = req.query.project_id;
|
|
const TrainingProjectDetails = require('../models/TrainingProjectDetails.js');
|
|
const Training = require('../models/training.js');
|
|
if (project_id) {
|
|
// Find all details rows for this project
|
|
const detailsRows = await TrainingProjectDetails.findAll({ where: { project_id } });
|
|
if (!detailsRows || detailsRows.length === 0) return res.json([]);
|
|
// Get all trainings linked to any details row for this project
|
|
const detailsIds = detailsRows.map(d => d.id);
|
|
const trainings = await Training.findAll({ where: { project_details_id: detailsIds } });
|
|
return res.json(trainings);
|
|
} else {
|
|
// Return all trainings if no project_id is specified
|
|
const trainings = await Training.findAll();
|
|
return res.json(trainings);
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({ message: 'Failed to fetch trainings', error: error.message });
|
|
}
|
|
});
|
|
|
|
// DELETE endpoint to remove a training by id
|
|
router.delete('/trainings/:id', async (req, res) => {
|
|
try {
|
|
const Training = require('../models/training.js');
|
|
const id = req.params.id;
|
|
const deleted = await Training.destroy({ where: { id } });
|
|
if (deleted) {
|
|
res.json({ message: 'Training deleted' });
|
|
} else {
|
|
res.status(404).json({ message: 'Training not found' });
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({ message: 'Failed to delete training', error: error.message });
|
|
}
|
|
});
|
|
|
|
// DELETE endpoint to remove a training project and all related entries
|
|
router.delete('/training-projects/:id', async (req, res) => {
|
|
try {
|
|
const projectId = req.params.id;
|
|
const TrainingProject = require('../models/TrainingProject.js');
|
|
const TrainingProjectDetails = require('../models/TrainingProjectDetails.js');
|
|
const Training = require('../models/training.js');
|
|
// Find details row(s) for this project
|
|
const detailsRows = await TrainingProjectDetails.findAll({ where: { project_id: projectId } });
|
|
const detailsIds = detailsRows.map(d => d.id);
|
|
// Delete all trainings linked to these details
|
|
if (detailsIds.length > 0) {
|
|
await Training.destroy({ where: { project_details_id: detailsIds } });
|
|
await TrainingProjectDetails.destroy({ where: { project_id: projectId } });
|
|
}
|
|
// Delete the project itself
|
|
const deleted = await TrainingProject.destroy({ where: { project_id: projectId } });
|
|
if (deleted) {
|
|
res.json({ message: 'Training project and all related entries deleted' });
|
|
} else {
|
|
res.status(404).json({ message: 'Training project not found' });
|
|
}
|
|
} catch (error) {
|
|
res.status(500).json({ message: 'Failed to delete training project', error: error.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|