Files
photo-editor/src/core/codex/prompts.js
Coder 401969cfdb
Some checks failed
test / test (push) Failing after 24s
v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Реализация целевого дизайна (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/
2026-08-27 12:52:27 +07:00

157 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
};