fix: AI photo enhance modal stays open with result preview; fix basicsr build
- Modal no longer auto-closes after Real-ESRGAN completes; result loads into canvas so user can review, adjust sliders, and choose to apply or discard - Backend skips swapEntryPhotoFiles until user confirms via Применить - New DELETE /api/entries/:id/photo/enhance-ai/preview for temp file cleanup - photo-ai Dockerfile: patch basicsr via find+sed instead of import (avoids torchvision.functional_tensor import crash)
This commit is contained in:
+11
-9
@@ -4,18 +4,20 @@ WORKDIR /app
|
|||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir "typing-extensions==4.12.2" "numpy<2"
|
||||||
|
|
||||||
RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu
|
RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
|
||||||
RUN pip install --no-cache-dir realesrgan==0.3.0 fastapi "uvicorn[standard]" python-multipart opencv-python-headless numpy
|
RUN pip install --no-cache-dir realesrgan==0.3.0 fastapi "uvicorn[standard]" python-multipart opencv-python-headless
|
||||||
|
|
||||||
RUN python - <<'EOF'
|
RUN BASICSR_DEG=$(python -c "import basicsr; import os; print(os.path.join(os.path.dirname(basicsr.__file__), 'data', 'degradations.py'))" 2>/dev/null) || \
|
||||||
import basicsr, os
|
BASICSR_DEG=$(find /usr/local/lib/python3.10 -path "*/basicsr/data/degradations.py" 2>/dev/null | head -1) && \
|
||||||
p = os.path.join(os.path.dirname(basicsr.__file__), 'data', 'degradations.py')
|
if [ -n "$BASICSR_DEG" ]; then \
|
||||||
s = open(p).read()
|
sed -i 's/from torchvision.transforms.functional_tensor/from torchvision.transforms.functional/g' "$BASICSR_DEG" && \
|
||||||
s = s.replace('torchvision.transforms.functional_tensor', 'torchvision.transforms.functional')
|
echo "basicsr patch applied to $BASICSR_DEG"; \
|
||||||
open(p, 'w').write(s)
|
else \
|
||||||
print('basicsr patch applied')
|
echo "basicsr degradations.py not found, skipping patch"; \
|
||||||
EOF
|
fi
|
||||||
|
|
||||||
COPY app.py .
|
COPY app.py .
|
||||||
|
|
||||||
|
|||||||
+34
-4
@@ -426,6 +426,7 @@ let enhanceEngine = 'auto';
|
|||||||
let enhanceEngineLoaded = false;
|
let enhanceEngineLoaded = false;
|
||||||
let photoAiEnabled = false;
|
let photoAiEnabled = false;
|
||||||
let photoAiPollTimer = null;
|
let photoAiPollTimer = null;
|
||||||
|
let enhanceAiResultPath = null;
|
||||||
|
|
||||||
async function loadEnhanceEngine() {
|
async function loadEnhanceEngine() {
|
||||||
if (enhanceEngineLoaded) return;
|
if (enhanceEngineLoaded) return;
|
||||||
@@ -469,9 +470,23 @@ async function runPhotoAi() {
|
|||||||
} catch (e) { clearInterval(photoAiPollTimer); reject(e); }
|
} catch (e) { clearInterval(photoAiPollTimer); reject(e); }
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
showToast('Фото восстановлено ИИ');
|
showToast('ИИ результат готов');
|
||||||
closeEnhance();
|
enhanceAiResultPath = result.photo_path;
|
||||||
refreshEntryCard(entryId);
|
const aiImg = new Image();
|
||||||
|
aiImg.onload = () => {
|
||||||
|
enhanceSourceImg = aiImg;
|
||||||
|
['Brightness', 'Contrast', 'Saturate'].forEach(k => {
|
||||||
|
document.getElementById('enhance' + k).value = 100;
|
||||||
|
document.getElementById('enhance' + k + 'Val').textContent = '100%';
|
||||||
|
});
|
||||||
|
document.getElementById('enhanceSharp').value = 0;
|
||||||
|
document.getElementById('enhanceSharpVal').textContent = '0%';
|
||||||
|
document.getElementById('enhanceDenoise').value = 0;
|
||||||
|
document.getElementById('enhanceDenoiseVal').textContent = '0%';
|
||||||
|
renderEnhancePreview();
|
||||||
|
};
|
||||||
|
aiImg.onerror = () => { showToast('Не удалось загрузить результат ИИ'); };
|
||||||
|
aiImg.src = `${API}${result.photo_path}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loading.style.display = 'none';
|
loading.style.display = 'none';
|
||||||
canvas.style.display = 'block';
|
canvas.style.display = 'block';
|
||||||
@@ -514,6 +529,7 @@ function openEnhance(id) {
|
|||||||
if (!mp) { showToast('У записи нет фото'); return; }
|
if (!mp) { showToast('У записи нет фото'); return; }
|
||||||
enhanceEntryId = id;
|
enhanceEntryId = id;
|
||||||
enhanceSourceImg = null;
|
enhanceSourceImg = null;
|
||||||
|
enhanceAiResultPath = null;
|
||||||
['Brightness', 'Contrast', 'Saturate'].forEach(k => {
|
['Brightness', 'Contrast', 'Saturate'].forEach(k => {
|
||||||
const el = document.getElementById('enhance' + k);
|
const el = document.getElementById('enhance' + k);
|
||||||
el.value = 100;
|
el.value = 100;
|
||||||
@@ -545,10 +561,24 @@ function openEnhance(id) {
|
|||||||
document.getElementById('enhanceModal').classList.add('open');
|
document.getElementById('enhanceModal').classList.add('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanupAiPreview() {
|
||||||
|
if (!enhanceAiResultPath || !enhanceEntryId) return;
|
||||||
|
const toClean = enhanceAiResultPath;
|
||||||
|
const entryId = enhanceEntryId;
|
||||||
|
fetch(`${API}/api/entries/${entryId}/photo/enhance-ai/preview`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: hdrJson(),
|
||||||
|
body: JSON.stringify({ path: toClean }),
|
||||||
|
}).catch(() => {});
|
||||||
|
enhanceAiResultPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
function closeEnhance() {
|
function closeEnhance() {
|
||||||
|
cleanupAiPreview();
|
||||||
document.getElementById('enhanceModal').classList.remove('open');
|
document.getElementById('enhanceModal').classList.remove('open');
|
||||||
enhanceEntryId = null;
|
enhanceEntryId = null;
|
||||||
enhanceSourceImg = null;
|
enhanceSourceImg = null;
|
||||||
|
enhanceAiResultPath = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function medianFilterPass(ctx, w, h) {
|
function medianFilterPass(ctx, w, h) {
|
||||||
@@ -731,7 +761,7 @@ async function saveEnhance() {
|
|||||||
try {
|
try {
|
||||||
let done = false;
|
let done = false;
|
||||||
let engineLabel = 'клиент';
|
let engineLabel = 'клиент';
|
||||||
if (enhanceEngine !== 'client') {
|
if (enhanceEngine !== 'client' && !enhanceAiResultPath) {
|
||||||
try {
|
try {
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
const timer = setTimeout(() => ctrl.abort(), 8000);
|
const timer = setTimeout(() => ctrl.abort(), 8000);
|
||||||
|
|||||||
@@ -3789,7 +3789,6 @@ async function runPhotoAiJob(job) {
|
|||||||
const out = Buffer.from(await resp.arrayBuffer());
|
const out = Buffer.from(await resp.arrayBuffer());
|
||||||
const newName = crypto.randomBytes(12).toString('hex') + '.jpg';
|
const newName = crypto.randomBytes(12).toString('hex') + '.jpg';
|
||||||
fs.writeFileSync(path.join(UPLOADS_DIR, newName), out);
|
fs.writeFileSync(path.join(UPLOADS_DIR, newName), out);
|
||||||
await swapEntryPhotoFiles(job.req, job.entryId, photoPath, `/uploads/${newName}`);
|
|
||||||
job.photo_path = `/uploads/${newName}`;
|
job.photo_path = `/uploads/${newName}`;
|
||||||
job.status = 'done';
|
job.status = 'done';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -3834,6 +3833,13 @@ app.get('/api/entries/:id/photo/enhance-ai/:jobId', requireAuth, async (req, res
|
|||||||
res.json(out);
|
res.json(out);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.delete('/api/entries/:id/photo/enhance-ai/preview', requireAuth, async (req, res) => {
|
||||||
|
const { path: p } = req.body || {};
|
||||||
|
if (!isSafeUploadPath(p)) return res.status(400).json({ error: 'Некорректный путь' });
|
||||||
|
safeUnlink(p);
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/entries/:id/photo/restore-original', requireAuth, async (req, res) => {
|
app.post('/api/entries/:id/photo/restore-original', requireAuth, async (req, res) => {
|
||||||
if (req.user.role !== 'admin') {
|
if (req.user.role !== 'admin') {
|
||||||
const acc = await entryAccessible(req.user, req.params.id);
|
const acc = await entryAccessible(req.user, req.params.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user