Some checks failed
test / test (push) Failing after 24s
Реализация целевого дизайна (docs/architecture/target-design.md): - src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images) - Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue) - Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов - Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history - Даунскейл sharp до 1024px перед Codex + масштабирование области референса - SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/) - Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data - Документация: README, docs/architecture/
150 lines
5.4 KiB
JavaScript
150 lines
5.4 KiB
JavaScript
"use strict";
|
|
|
|
const express = require("express");
|
|
const { parseRegion } = require("../../core/codex/prompts");
|
|
const { isHeic, convertHeicToJpeg } = require("../../core/images/heic");
|
|
const { sniffImageType } = require("../../core/images/sniff");
|
|
const { BadRequestError } = require("../../errors");
|
|
const { JOB_ID_PATTERN } = require("./history.routes");
|
|
|
|
/**
|
|
* @param {Object} deps
|
|
* @param {import('../../core/jobs/job-service').JobService} deps.jobService
|
|
* @param {import('../../core/history/history-service').HistoryService} deps.historyService
|
|
* @param {import('multer').Multer} deps.upload
|
|
* @param {import('../../logger').Logger} deps.logger
|
|
*/
|
|
function createEditRoutes({ jobService, historyService, upload, logger }) {
|
|
const router = express.Router();
|
|
|
|
router.post(
|
|
"/edit",
|
|
upload.fields([
|
|
{ name: "photo", maxCount: 1 },
|
|
{ name: "reference", maxCount: 1 },
|
|
]),
|
|
async (req, res, next) => {
|
|
try {
|
|
const photoFile = req.files?.photo?.[0];
|
|
const referenceFile = req.files?.reference?.[0];
|
|
|
|
let sourceId = req.body?.sourceId ? String(req.body.sourceId).trim() : null;
|
|
if (sourceId) {
|
|
if (!JOB_ID_PATTERN.test(sourceId)) {
|
|
throw new BadRequestError("Некорректный идентификатор исходной версии (sourceId).");
|
|
}
|
|
const version = historyService.getVersion(sourceId);
|
|
if (!version) {
|
|
throw new BadRequestError("Исходная версия не найдена в истории.");
|
|
}
|
|
} else if (!photoFile || !photoFile.buffer) {
|
|
throw new BadRequestError("Выберите фотографию или укажите sourceId из истории.");
|
|
}
|
|
|
|
const prompt = String(req.body?.prompt || "").trim();
|
|
if (!prompt) {
|
|
throw new BadRequestError("Введите промпт для редактирования фотографии.");
|
|
}
|
|
if (prompt.length > 1000) {
|
|
throw new BadRequestError("Промпт не должен превышать 1000 символов.");
|
|
}
|
|
|
|
let referenceId = req.body?.referenceId ? String(req.body.referenceId).trim() : null;
|
|
if (referenceId) {
|
|
if (!JOB_ID_PATTERN.test(referenceId)) {
|
|
throw new BadRequestError("Некорректный идентификатор референса (referenceId).");
|
|
}
|
|
const refVersion = historyService.getVersion(referenceId);
|
|
if (!refVersion) {
|
|
throw new BadRequestError("Референс не найден в истории.");
|
|
}
|
|
}
|
|
|
|
const region = parseRegion(req.body?.region);
|
|
if (region && !referenceFile && !referenceId) {
|
|
throw new BadRequestError("Область указана без изображения референса.");
|
|
}
|
|
|
|
// Process photo file buffer if uploaded
|
|
let inputBuffer = photoFile ? photoFile.buffer : undefined;
|
|
let inputMimetype = photoFile ? photoFile.mimetype : undefined;
|
|
let inputOriginalname = photoFile ? photoFile.originalname : undefined;
|
|
|
|
if (photoFile) {
|
|
if (isHeic(photoFile) || sniffImageType(inputBuffer) === "heic") {
|
|
inputBuffer = await convertHeicToJpeg(inputBuffer);
|
|
inputMimetype = "image/jpeg";
|
|
} else {
|
|
const sniffed = sniffImageType(inputBuffer);
|
|
if (!sniffed) {
|
|
throw new BadRequestError(
|
|
"Загруженная фотография не является поддерживаемым изображением."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process reference file buffer if uploaded
|
|
let referenceBuffer = referenceFile ? referenceFile.buffer : undefined;
|
|
let referenceMimetype = referenceFile ? referenceFile.mimetype : undefined;
|
|
let referenceOriginalname = referenceFile ? referenceFile.originalname : undefined;
|
|
|
|
if (referenceFile) {
|
|
if (isHeic(referenceFile) || sniffImageType(referenceBuffer) === "heic") {
|
|
referenceBuffer = await convertHeicToJpeg(referenceBuffer);
|
|
referenceMimetype = "image/jpeg";
|
|
} else {
|
|
const sniffed = sniffImageType(referenceBuffer);
|
|
if (!sniffed) {
|
|
throw new BadRequestError(
|
|
"Загруженный референс не является поддерживаемым изображением."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const payload = {
|
|
sourceId,
|
|
inputBuffer,
|
|
inputMimetype,
|
|
inputOriginalname,
|
|
prompt,
|
|
referenceId,
|
|
referenceBuffer,
|
|
referenceMimetype,
|
|
referenceOriginalname,
|
|
region,
|
|
};
|
|
|
|
const result = await jobService.createJob({
|
|
type: "edit",
|
|
payload,
|
|
requestId: req.id,
|
|
});
|
|
|
|
logger.info("Постановка задания edit в очередь", {
|
|
jobId: result.jobId,
|
|
sourceId,
|
|
prompt: prompt.slice(0, 80),
|
|
queuePosition: result.queuePosition,
|
|
});
|
|
|
|
res.json({
|
|
ok: true,
|
|
jobId: result.jobId,
|
|
status: result.status,
|
|
queuePosition: result.queuePosition,
|
|
});
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
}
|
|
);
|
|
|
|
return router;
|
|
}
|
|
|
|
module.exports = {
|
|
createEditRoutes,
|
|
};
|