feat(photo-ai): Stage 5 — фронтенд фото-ИИ (режимы, статус, метаданные)
Только фронтенд: server.js, worker.js, photo-ai/, compose и схема БД не тронуты.
Контракт /enhance-ai, /api/photo-ai/health и service в /api/photo-jobs/status
взят из Stage 4 как есть (I6 — новых колонок нет, метаданные из photo_jobs.params
и audit_log.target).
journal.html + journal.js:
- селект режима рядом с «🤖 ИИ»: Универсально (x2) / Быстро (x4) / Лица /
Лица + фон; PHOTO_AI_MODES — единственная таблица режим→{model, face}
- значение уходит в POST .../enhance-ai телом {model, face, face_model};
face_model подставляется только когда face !== 'off'
- дефолт селекта из photo_ai_face_mode (face=all → facesbg, face=face → faces)
- подсказка про медленный CPU: loadPhotoAiDevice() читает
GET /api/photo-ai/health (только для админа)
- текст подтверждения и надпись «ИИ обрабатывает…» различают апскейл и face-режим
- кнопка и селект скрываются вместе при photo_ai_enabled === 'false' (I3)
settings.html + settings.js:
- новый раздел «Фото-ИИ»: режим по умолчанию, модель лиц, желаемое устройство
- read-only статус из /api/photo-jobs/status: фактическое устройство,
device_name, VRAM, модели/модели лиц/загруженные; жёлтым — расхождение с
желаемым устройством, красным — недоступность и незаданный PHOTO_AI_URL
- renderStackInfo() рисует карточку «Фото-ИИ» из stack.photo_ai
- новые id в DIRTY_FIELDS и в payload PUT /api/settings
- photo_ai_device_pref документирующий: фактическое устройство задаёт
PHOTO_AI_DEVICE в контейнере
worker.html + worker.js:
- PHOTO_ACTION_LABELS: ai_face «ИИ + лица», ai_upscale «ИИ-апскейл»
- в модалке сравнения — чипы с моделью, режимом лиц, найденными лицами,
устройством, временем обработки и предупреждениями
- строка состояния учитывает service.reachable === false («задания ждут»)
и дописывает «· расчёт на <device>»
audit.js: 10 меток для кодов, которые раньше показывались сырыми (профиль и
фото ученика, главное фото, фото модуля, удаление/очистка уведомлений, логотип).
Проверено: node --check для всех четырёх js. Приёмка на живом стеке в этом
изменении не гонялась — чекбокс приёмки Stage 5 в TODO_PHOTO_FACE_AI.md
оставлен пустым, там же журнал раздела 5 с замечаниями для Stage 6.
This commit is contained in:
+52
-3
@@ -19,6 +19,7 @@ const STATUS_BADGE = {
|
||||
let state = null;
|
||||
let photoState = null;
|
||||
let photoJobRow = null;
|
||||
let photoJobMeta = {};
|
||||
let refreshTimer = null;
|
||||
let busy = false;
|
||||
let photoBusy = false;
|
||||
@@ -29,6 +30,8 @@ let photoTotal = 0;
|
||||
|
||||
const PHOTO_ACTION_LABELS = {
|
||||
ai: 'ИИ-обработка',
|
||||
ai_face: 'ИИ + лица',
|
||||
ai_upscale: 'ИИ-апскейл',
|
||||
enhance: 'Улучшение',
|
||||
restore: 'Возврат оригинала',
|
||||
rollback: 'Откат',
|
||||
@@ -224,9 +227,15 @@ function render() {
|
||||
function renderPhoto() {
|
||||
if (!photoState) return;
|
||||
const w = photoState.worker || {};
|
||||
document.getElementById('photoStateHint').textContent = !photoState.enabled
|
||||
? 'Обработка фотографий выключена'
|
||||
: (!photoState.ai_configured ? 'PHOTO_AI_URL не настроен' : (w.processing ? `Обрабатывается задание #${w.current_id ?? ''}` : 'Ожидание очереди'));
|
||||
const svc = photoState.service || {};
|
||||
let hint;
|
||||
if (!photoState.enabled) hint = 'Обработка фотографий выключена';
|
||||
else if (!photoState.ai_configured) hint = 'PHOTO_AI_URL не настроен';
|
||||
else if (svc.reachable === false) hint = 'Сервис фото-ИИ недоступен — задания ждут';
|
||||
else if (w.processing) hint = `Обрабатывается задание #${w.current_id ?? ''}`;
|
||||
else hint = 'Ожидание очереди';
|
||||
if (photoState.enabled && photoState.ai_configured && svc.reachable && svc.device) hint += ` · расчёт на ${svc.device}`;
|
||||
document.getElementById('photoStateHint').textContent = hint;
|
||||
document.getElementById('photoToggleBtn').textContent = photoState.enabled ? '📷 Выключить фото-воркер' : '📷 Включить фото-воркер';
|
||||
const c = photoState.counts || {};
|
||||
document.getElementById('photoCounts').innerHTML = [
|
||||
@@ -297,12 +306,52 @@ function setPhotoJobClip(p) {
|
||||
document.getElementById('photoJobDivider').style.left = v + '%';
|
||||
}
|
||||
|
||||
async function loadPhotoJobMeta() {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/audit?action=photo.job.preview&limit=200`, { headers: hdr() });
|
||||
if (!res.ok) return;
|
||||
const rows = await res.json();
|
||||
const map = {};
|
||||
rows.forEach(r => {
|
||||
const t = r.target;
|
||||
if (t && t.job_id && !map[t.job_id]) map[t.job_id] = t;
|
||||
});
|
||||
photoJobMeta = map;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
function renderPhotoJobMeta(r) {
|
||||
const el = document.getElementById('photoJobMeta');
|
||||
if (!el) return;
|
||||
const p = r.params && typeof r.params === 'object' ? r.params : {};
|
||||
const m = photoJobMeta[r.id] || {};
|
||||
const chips = [];
|
||||
const model = m.model || p.model;
|
||||
if (model) chips.push(['модель', esc(String(model))]);
|
||||
const face = m.face || p.face;
|
||||
if (face && face !== 'off') {
|
||||
const fm = m.face_model || p.face_model;
|
||||
const title = face === 'all' ? 'лица + фон' : 'только лица';
|
||||
chips.push(['режим', esc(title) + (fm ? ` (${esc(String(fm))})` : '')]);
|
||||
}
|
||||
if (m.faces_found !== null && m.faces_found !== undefined) chips.push(['найдено лиц', String(m.faces_found)]);
|
||||
if (m.device) chips.push(['устройство', esc(String(m.device))]);
|
||||
if (m.elapsed_ms !== null && m.elapsed_ms !== undefined) chips.push(['обработка', esc(fmtDur(m.elapsed_ms))]);
|
||||
if (Array.isArray(m.warnings) && m.warnings.length) chips.push(['предупреждения', esc(m.warnings.join('; '))]);
|
||||
el.innerHTML = chips.length
|
||||
? chips.map(([k, v]) => `<span class="chip">${esc(k)}: <b>${v}</b></span>`).join('')
|
||||
: '<span class="hint">Метаданные обработки недоступны</span>';
|
||||
}
|
||||
|
||||
function openPhotoJob(r) {
|
||||
photoJobRow = r;
|
||||
document.getElementById('photoJobTitle').textContent = `Обработка фото — запись #${r.entry_id} ${r.student_name || ''}`;
|
||||
document.getElementById('photoJobBefore').src = thumbUrl(r.before_path);
|
||||
document.getElementById('photoJobAfter').src = r.after_path ? `${API}${r.after_path}` : '';
|
||||
document.getElementById('photoJobRollbackBtn').style.display = (r.before_path && r.action !== 'rollback') ? '' : 'none';
|
||||
renderPhotoJobMeta(r);
|
||||
loadPhotoJobMeta().then(() => { if (photoJobRow === r) renderPhotoJobMeta(r); });
|
||||
setPhotoJobClip(50);
|
||||
document.getElementById('photoJobModal').classList.add('open');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user