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/
157 lines
5.8 KiB
JavaScript
157 lines
5.8 KiB
JavaScript
"use strict";
|
||
|
||
const { AppError } = require("../../errors");
|
||
|
||
/**
|
||
* Парсит и валидирует координаты прямоугольной области референса.
|
||
* @param {string|object} value
|
||
* @returns {{ x: number, y: number, w: number, h: number } | null}
|
||
*/
|
||
function parseRegion(value) {
|
||
if (value === undefined || value === null || String(value).trim() === "") {
|
||
return null;
|
||
}
|
||
|
||
let parsed = value;
|
||
if (typeof value === "string") {
|
||
try {
|
||
parsed = JSON.parse(value);
|
||
} catch {
|
||
throw new AppError(400, "Область референса передана в неверном формате.");
|
||
}
|
||
}
|
||
|
||
const nums = ["x", "y", "w", "h"].map((key) => Number(parsed?.[key]));
|
||
|
||
if (nums.some((num) => !Number.isFinite(num) || num < 0)) {
|
||
throw new AppError(
|
||
400,
|
||
"Область референса должна содержать неотрицательные числовые поля x, y, w, h."
|
||
);
|
||
}
|
||
|
||
const [x, y, w, h] = nums;
|
||
if (w <= 0 || h <= 0) {
|
||
throw new AppError(400, "Ширина и высота области должны быть больше 0.");
|
||
}
|
||
|
||
return {
|
||
x: Math.round(x),
|
||
y: Math.round(y),
|
||
w: Math.round(w),
|
||
h: Math.round(h),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Масштабирует координаты области при даунскейле референса.
|
||
* @param {{ x: number, y: number, w: number, h: number }} region
|
||
* @param {number} factor
|
||
* @returns {{ x: number, y: number, w: number, h: number }}
|
||
*/
|
||
function scaleRegion(region, factor = 1) {
|
||
if (!region || factor === 1 || !Number.isFinite(factor) || factor <= 0) {
|
||
return region;
|
||
}
|
||
|
||
return {
|
||
x: Math.max(0, Math.round(region.x * factor)),
|
||
y: Math.max(0, Math.round(region.y * factor)),
|
||
w: Math.max(1, Math.round(region.w * factor)),
|
||
h: Math.max(1, Math.round(region.h * factor)),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Собирает текст системного промпта для редактирования через Codex.
|
||
* @param {Object} options
|
||
* @param {string} options.prompt
|
||
* @param {{ width?: number, height?: number }|null} [options.dimensions]
|
||
* @param {string} [options.inputName='input.jpg']
|
||
* @param {string|null} [options.referenceName=null]
|
||
* @param {{ x: number, y: number, w: number, h: number }|null} [options.region=null]
|
||
* @param {boolean} [options.downscaled=false]
|
||
* @returns {string}
|
||
*/
|
||
function buildEditPrompt({
|
||
prompt,
|
||
dimensions = null,
|
||
inputName = "input.jpg",
|
||
referenceName = null,
|
||
region = null,
|
||
downscaled = false,
|
||
}) {
|
||
const size =
|
||
dimensions && dimensions.width && dimensions.height
|
||
? `${dimensions.width}×${dimensions.height}`
|
||
: "";
|
||
|
||
const lines = [
|
||
`Перед тобой фотография ${inputName}. Примени к ней следующее редактирование: «${prompt}».`,
|
||
];
|
||
|
||
if (referenceName) {
|
||
lines.push(
|
||
`Также дано вспомогательное изображение ${referenceName} — тот же сюжет и ракурс, что и ${inputName}. Красным прямоугольником на ${referenceName} выделена область, в которой нужно внести правку.`
|
||
);
|
||
}
|
||
|
||
if (referenceName && region) {
|
||
lines.push(
|
||
`Координаты области на ${referenceName}: x=${region.x}, y=${region.y}, ширина=${region.w}, высота=${region.h} (в пикселях).`
|
||
);
|
||
lines.push(
|
||
`Примени редактирование к ${inputName} именно в области, соответствующей прямоугольнику на ${referenceName}. Остальную часть ${inputName} не меняй.`
|
||
);
|
||
}
|
||
|
||
if (referenceName && !region) {
|
||
lines.push(
|
||
`Используй ${referenceName} как ориентир по сюжету и ракурсу при внесении правки в ${inputName}.`
|
||
);
|
||
}
|
||
|
||
if (downscaled) {
|
||
lines.push("Исходное фото уменьшено для скорости обработки.");
|
||
}
|
||
|
||
lines.push(
|
||
"Сохрани отредактированное фото в файл output.jpg в текущей директории.",
|
||
"Не перерисовывай фото с нуля и не заменяй его на полностью новое изображение — редактируй исходное фото, сохраняя его сюжет, объекты, лица и пропорции.",
|
||
size ? `Сохрани исходный размер ${size}.` : "Сохрани исходные пропорции.",
|
||
"Файл output.jpg должен быть валидным JPEG. По завершении кратко опиши, что сделал."
|
||
);
|
||
|
||
return lines.join(" ");
|
||
}
|
||
|
||
/**
|
||
* Собирает промпт для подсказок вариантов редактирования.
|
||
* @param {string} [inputName='input.jpg']
|
||
* @returns {string}
|
||
*/
|
||
function buildSuggestPrompt(inputName = "input.jpg") {
|
||
return `Перед тобой фотография ${inputName}. Предложи 3 коротких варианта промпта (по одному на строку, без нумерации) для её редактирования. Только варианты промптов, ничего больше.`;
|
||
}
|
||
|
||
/**
|
||
* Парсит предложения Codex в массив строк.
|
||
* @param {string} text
|
||
* @returns {string[]}
|
||
*/
|
||
function parseSuggestions(text) {
|
||
return String(text || "")
|
||
.split(/\r?\n/)
|
||
.map((line) => line.replace(/^\s*(?:[-*•]|\d+[.)])\s*/, "").trim())
|
||
.filter((line) => line.length > 0 && line.length <= 200)
|
||
.slice(0, 3);
|
||
}
|
||
|
||
module.exports = {
|
||
parseRegion,
|
||
scaleRegion,
|
||
buildEditPrompt,
|
||
buildSuggestPrompt,
|
||
parseSuggestions,
|
||
};
|