From 93daa686681488fe5ba64b5f0066b9fdda8d586e Mon Sep 17 00:00:00 2001 From: jetson Date: Mon, 21 Sep 2026 10:19:29 +0700 Subject: [PATCH] fix: add explicit GPU memory cleanup between models to prevent OOM --- app.py | 13 ++++++++++++ src/detection.py | 11 +++++++++++ src/job.py | 9 +++++++++ src/pipeline.py | 8 ++++++++ static/app.js | 47 +++++++++++++++++++++++++++++++++++++++----- templates/index.html | 26 ++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 511f48a..5e643d3 100644 --- a/app.py +++ b/app.py @@ -196,6 +196,19 @@ def api_videos(): return jsonify(videos) +@app.route("/api/videos/", methods=["DELETE"]) +def api_delete_video(filename): + """Delete an uploaded video.""" + safe = secure_filename(filename) + if not safe or not safe.lower().endswith(VIDEO_EXTENSIONS): + return jsonify({"error": "invalid filename"}), 400 + file_path = os.path.join(UPLOAD_DIR, safe) + if not os.path.isfile(file_path): + return jsonify({"error": "not found"}), 404 + os.remove(file_path) + return jsonify({"status": "deleted"}) + + @app.route("/api/models") def api_models(): groups = scan_model_groups(MODELS_DIR) diff --git a/src/detection.py b/src/detection.py index fa0b11e..3454f8d 100644 --- a/src/detection.py +++ b/src/detection.py @@ -28,6 +28,17 @@ class BaseDetector: self._conf = conf self._class_filter = set(class_filter) if class_filter else None + def cleanup(self) -> None: + """Release model resources and GPU memory.""" + self._model = None + try: + import gc + gc.collect() + import torch + torch.cuda.empty_cache() + except Exception: + pass + def detect(self, frame: np.ndarray) -> list[Detection]: results = self._model.predict( frame, conf=self._conf, verbose=False diff --git a/src/job.py b/src/job.py index 33136b7..ab8b94b 100644 --- a/src/job.py +++ b/src/job.py @@ -181,6 +181,15 @@ class JobQueue: ) ) + # Force release GPU memory between models + import gc + gc.collect() + try: + import torch + torch.cuda.empty_cache() + except Exception: + pass + with self._lock: if job.status != JobStatus.CANCELLED: job.status = JobStatus.COMPLETED diff --git a/src/pipeline.py b/src/pipeline.py index 9a6d8b9..95b1983 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -235,6 +235,14 @@ def run_pipeline( cap.release() if writer is not None: writer.finish() + # Force GPU memory release for TensorRT engines + import gc + gc.collect() + try: + import torch + torch.cuda.empty_cache() + except Exception: + pass duration = time.time() - start_time return PipelineResult( diff --git a/static/app.js b/static/app.js index ce2c66e..b8bec20 100644 --- a/static/app.js +++ b/static/app.js @@ -132,6 +132,23 @@ return m + ':' + (s < 10 ? '0' : '') + s; } + function formatRelativeTime(timestamp) { + var now = Math.floor(Date.now() / 1000); + var diff = now - timestamp; + if (diff < 0) diff = 0; + if (diff < 60) return 'just now'; + if (diff < 3600) { + var mins = Math.floor(diff / 60); + return mins + (mins === 1 ? ' minute ago' : ' minutes ago'); + } + if (diff < 86400) { + var hrs = Math.floor(diff / 3600); + return hrs + (hrs === 1 ? ' hour ago' : ' hours ago'); + } + var days = Math.floor(diff / 86400); + return days + (days === 1 ? ' day ago' : ' days ago'); + } + /* ------------------------------------------------ Model Cards — select/deselect, highlight ------------------------------------------------ */ @@ -205,11 +222,12 @@ var item = document.createElement('label'); item.className = 'video-item'; item.innerHTML = - '' + + '' + '
' + - '
' + escapeHtml(v.name) + '
' + - '
' + formatBytes(v.size) + '
' + - '
'; + '
' + escapeHtml(v.filename) + '
' + + '
' + formatBytes(v.size) + ' · ' + formatRelativeTime(v.mtime) + '
' + + '' + + ''; var radio = item.querySelector('input[type="radio"]'); radio.addEventListener('change', function () { // Deselect all other items @@ -221,7 +239,7 @@ selectedExistingVideo = v.path; videoPathInput.value = v.path; // Show in file-info - fileName.textContent = v.name; + fileName.textContent = v.filename; fileSize.textContent = formatBytes(v.size); fileDuration.textContent = '—'; fileInfo.classList.add('visible'); @@ -237,9 +255,28 @@ }); item.addEventListener('click', function (e) { if (e.target.tagName === 'INPUT') return; + if (e.target.classList.contains('video-delete-btn')) return; radio.checked = true; radio.dispatchEvent(new Event('change')); }); + var deleteBtn = item.querySelector('.video-delete-btn'); + if (deleteBtn) { + deleteBtn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + if (!confirm('Delete "' + v.filename + '"?')) return; + fetch('/api/videos/' + encodeURIComponent(v.filename), { method: 'DELETE' }) + .then(function (res) { return res.json(); }) + .then(function (data) { + if (data.status === 'deleted') { + item.remove(); + if (!videoList.querySelector('.video-item')) { + existingSection.style.display = 'none'; + } + } + }); + }); + } videoList.appendChild(item); }); }) diff --git a/templates/index.html b/templates/index.html index 2e33494..b29cdf9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -256,6 +256,32 @@ color: var(--color-neutral-500); } .empty-state p { margin-bottom: var(--space-2); } + + /* Video delete button */ + .video-delete-btn { + flex-shrink: 0; + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--color-neutral-200); + border-radius: var(--radius-sm); + background: var(--color-neutral-50); + color: var(--color-neutral-400); + cursor: pointer; + font-size: var(--text-sm); + transition: all var(--transition-fast); + opacity: 0; + } + .video-item:hover .video-delete-btn { + opacity: 1; + } + .video-delete-btn:hover { + background: #FEE2E2; + color: #DC2626; + border-color: #FECACA; + } {% endblock %}