feat: AI photo enhancement (Real-ESRGAN container) + restore original
- new photo-ai service: FastAPI + Real-ESRGAN x2plus on CPU, internal only - async job queue in server (POST enhance-ai / GET status), 5min timeout, sequential processing - keep original photo backup (entries.photo_original_path, uploads/.originals), restore-original endpoint - UI: AI button and restore-original button in enhance modal - db: photo_original_path column (init.sql, migration.sql, runtime ensure)
This commit is contained in:
@@ -187,6 +187,8 @@
|
||||
<div class="settings-field"><label>Резкость <span id="enhanceSharpVal">0%</span></label><input type="range" id="enhanceSharp" min="0" max="100" value="0"></div>
|
||||
</div>
|
||||
<div class="card-foot" style="justify-content:flex-end;gap:8px;margin-top:6px">
|
||||
<button type="button" class="btn-primary ghost" id="enhanceRestoreBtn" style="display:none" title="Вернуть исходную фотографию">↩ Вернуть оригинал</button>
|
||||
<button type="button" class="btn-primary ghost" id="enhanceAiBtn" style="display:none" title="ИИ-восстановление (Real-ESRGAN)">🤖 ИИ</button>
|
||||
<button type="button" class="btn-primary ghost" id="enhanceAutoBtn" title="Подобрать настройки автоматически">✨ Авто</button>
|
||||
<button type="button" class="btn-primary ghost" id="enhanceCancelBtn">Оставить как есть</button>
|
||||
<button type="button" class="btn-primary" id="enhanceSaveBtn">Применить</button>
|
||||
|
||||
@@ -424,6 +424,8 @@ let enhanceTimer = null;
|
||||
let enhanceSaving = false;
|
||||
let enhanceEngine = 'auto';
|
||||
let enhanceEngineLoaded = false;
|
||||
let photoAiEnabled = false;
|
||||
let photoAiPollTimer = null;
|
||||
|
||||
async function loadEnhanceEngine() {
|
||||
if (enhanceEngineLoaded) return;
|
||||
@@ -432,9 +434,79 @@ async function loadEnhanceEngine() {
|
||||
const r = await fetch(`${API}/api/public-settings`);
|
||||
const f = await r.json();
|
||||
if (['auto', 'server', 'client'].includes(f.photo_enhance_engine)) enhanceEngine = f.photo_enhance_engine;
|
||||
photoAiEnabled = f.photo_ai_enabled === 'true';
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function runPhotoAi() {
|
||||
if (!enhanceEntryId || enhanceSaving) return;
|
||||
const entryId = enhanceEntryId;
|
||||
if (!confirm('ИИ-восстановление фото (Real-ESRGAN). Может занять до минуты на CPU. Продолжить?')) return;
|
||||
enhanceSaving = true;
|
||||
const btn = document.getElementById('enhanceAiBtn');
|
||||
btn.disabled = true;
|
||||
const loading = document.getElementById('enhanceLoading');
|
||||
const canvas = document.getElementById('enhanceCanvas');
|
||||
try {
|
||||
const res = await fetch(`${API}/api/entries/${enhanceEntryId}/photo/enhance-ai`, { method: 'POST', headers: hdr() });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Ошибка запуска ИИ');
|
||||
}
|
||||
const { jobId } = await res.json();
|
||||
canvas.style.display = 'none';
|
||||
loading.style.display = '';
|
||||
loading.textContent = 'ИИ обрабатывает фото... (может занять до минуты)';
|
||||
btn.textContent = '⏳ ИИ...';
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
photoAiPollTimer = setInterval(async () => {
|
||||
try {
|
||||
const r = await fetch(`${API}/api/entries/${enhanceEntryId}/photo/enhance-ai/${jobId}`, { headers: hdr() });
|
||||
if (!r.ok) throw new Error('Ошибка опроса статуса');
|
||||
const s = await r.json();
|
||||
if (s.status === 'done') { clearInterval(photoAiPollTimer); resolve(s); }
|
||||
else if (s.status === 'error') { clearInterval(photoAiPollTimer); reject(new Error(s.error || 'Ошибка ИИ')); }
|
||||
} catch (e) { clearInterval(photoAiPollTimer); reject(e); }
|
||||
}, 2000);
|
||||
});
|
||||
showToast('Фото восстановлено ИИ');
|
||||
closeEnhance();
|
||||
refreshEntryCard(entryId);
|
||||
} catch (e) {
|
||||
loading.style.display = 'none';
|
||||
canvas.style.display = 'block';
|
||||
alert(e.message);
|
||||
} finally {
|
||||
enhanceSaving = false;
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🤖 ИИ';
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreOriginal() {
|
||||
if (!enhanceEntryId || enhanceSaving) return;
|
||||
if (!confirm('Вернуть исходную фотографию? Текущая улучшенная версия будет удалена.')) return;
|
||||
enhanceSaving = true;
|
||||
const btn = document.getElementById('enhanceRestoreBtn');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(`${API}/api/entries/${enhanceEntryId}/photo/restore-original`, { method: 'POST', headers: hdr() });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Ошибка восстановления');
|
||||
}
|
||||
const savedId = enhanceEntryId;
|
||||
showToast('Оригинал возвращён');
|
||||
closeEnhance();
|
||||
refreshEntryCard(savedId);
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
} finally {
|
||||
enhanceSaving = false;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openEnhance(id) {
|
||||
const e = currentEntries.find(x => x.id === id);
|
||||
if (!e) return;
|
||||
@@ -452,6 +524,10 @@ function openEnhance(id) {
|
||||
document.getElementById('enhanceSharpVal').textContent = '0%';
|
||||
const orig = document.getElementById('enhanceOrig');
|
||||
orig.src = `${API}${mp}`;
|
||||
document.getElementById('enhanceRestoreBtn').style.display = e.photo_original_path ? '' : 'none';
|
||||
loadEnhanceEngine().then(() => {
|
||||
document.getElementById('enhanceAiBtn').style.display = photoAiEnabled ? '' : 'none';
|
||||
});
|
||||
const canvas = document.getElementById('enhanceCanvas');
|
||||
canvas.style.display = 'none';
|
||||
const loading = document.getElementById('enhanceLoading');
|
||||
@@ -1052,6 +1128,8 @@ document.getElementById('editPhotoPreview').addEventListener('click', e => {
|
||||
document.getElementById('aiCorrectBtn').addEventListener('click', aiCorrect);
|
||||
document.getElementById('enhanceCancelBtn').addEventListener('click', closeEnhance);
|
||||
document.getElementById('enhanceAutoBtn').addEventListener('click', autoEnhance);
|
||||
document.getElementById('enhanceAiBtn').addEventListener('click', runPhotoAi);
|
||||
document.getElementById('enhanceRestoreBtn').addEventListener('click', restoreOriginal);
|
||||
document.getElementById('enhanceSaveBtn').addEventListener('click', saveEnhance);
|
||||
document.getElementById('enhanceModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEnhance(); });
|
||||
document.getElementById('enhanceBrightness').addEventListener('input', scheduleEnhancePreview);
|
||||
|
||||
Reference in New Issue
Block a user