diff --git a/photo-ai/Dockerfile b/photo-ai/Dockerfile index 168748b..157efc5 100644 --- a/photo-ai/Dockerfile +++ b/photo-ai/Dockerfile @@ -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 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 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' -import basicsr, os -p = os.path.join(os.path.dirname(basicsr.__file__), 'data', 'degradations.py') -s = open(p).read() -s = s.replace('torchvision.transforms.functional_tensor', 'torchvision.transforms.functional') -open(p, 'w').write(s) -print('basicsr patch applied') -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) || \ + BASICSR_DEG=$(find /usr/local/lib/python3.10 -path "*/basicsr/data/degradations.py" 2>/dev/null | head -1) && \ + if [ -n "$BASICSR_DEG" ]; then \ + sed -i 's/from torchvision.transforms.functional_tensor/from torchvision.transforms.functional/g' "$BASICSR_DEG" && \ + echo "basicsr patch applied to $BASICSR_DEG"; \ + else \ + echo "basicsr degradations.py not found, skipping patch"; \ + fi COPY app.py . diff --git a/public/js/journal.js b/public/js/journal.js index fd53ec4..b06c283 100644 --- a/public/js/journal.js +++ b/public/js/journal.js @@ -426,6 +426,7 @@ let enhanceEngine = 'auto'; let enhanceEngineLoaded = false; let photoAiEnabled = false; let photoAiPollTimer = null; +let enhanceAiResultPath = null; async function loadEnhanceEngine() { if (enhanceEngineLoaded) return; @@ -469,9 +470,23 @@ async function runPhotoAi() { } catch (e) { clearInterval(photoAiPollTimer); reject(e); } }, 2000); }); - showToast('Фото восстановлено ИИ'); - closeEnhance(); - refreshEntryCard(entryId); + showToast('ИИ результат готов'); + enhanceAiResultPath = result.photo_path; + 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) { loading.style.display = 'none'; canvas.style.display = 'block'; @@ -514,6 +529,7 @@ function openEnhance(id) { if (!mp) { showToast('У записи нет фото'); return; } enhanceEntryId = id; enhanceSourceImg = null; + enhanceAiResultPath = null; ['Brightness', 'Contrast', 'Saturate'].forEach(k => { const el = document.getElementById('enhance' + k); el.value = 100; @@ -545,10 +561,24 @@ function openEnhance(id) { 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() { + cleanupAiPreview(); document.getElementById('enhanceModal').classList.remove('open'); enhanceEntryId = null; enhanceSourceImg = null; + enhanceAiResultPath = null; } function medianFilterPass(ctx, w, h) { @@ -731,7 +761,7 @@ async function saveEnhance() { try { let done = false; let engineLabel = 'клиент'; - if (enhanceEngine !== 'client') { + if (enhanceEngine !== 'client' && !enhanceAiResultPath) { try { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 8000); diff --git a/server.js b/server.js index 690fe70..0ec626e 100644 --- a/server.js +++ b/server.js @@ -3789,7 +3789,6 @@ async function runPhotoAiJob(job) { const out = Buffer.from(await resp.arrayBuffer()); const newName = crypto.randomBytes(12).toString('hex') + '.jpg'; fs.writeFileSync(path.join(UPLOADS_DIR, newName), out); - await swapEntryPhotoFiles(job.req, job.entryId, photoPath, `/uploads/${newName}`); job.photo_path = `/uploads/${newName}`; job.status = 'done'; } catch (e) { @@ -3834,6 +3833,13 @@ app.get('/api/entries/:id/photo/enhance-ai/:jobId', requireAuth, async (req, res 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) => { if (req.user.role !== 'admin') { const acc = await entryAccessible(req.user, req.params.id);