v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Some checks failed
test / test (push) Failing after 24s
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/
This commit is contained in:
156
src/core/codex/prompts.js
Normal file
156
src/core/codex/prompts.js
Normal file
@@ -0,0 +1,156 @@
|
||||
"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,
|
||||
};
|
||||
Reference in New Issue
Block a user