supli6669 commited on
Commit ·
45ffbdc
1
Parent(s): 21dc5ba
fix: resolve all 9 codebase bugs (B1-B9) & add Rule 9 to AGENTS.md
Browse files- .agents/AGENTS.md +8 -0
- app.py +41 -13
- handover.md +32 -1
- pipeline.py +16 -11
.agents/AGENTS.md
CHANGED
|
@@ -52,3 +52,11 @@ All AI agents working on this codebase must adhere strictly to these rules:
|
|
| 52 |
- Mark tasks `[x]` (done) when verified complete.
|
| 53 |
- Add notes under each task if you discover important findings (e.g., actual iteration count, loss values, timing).
|
| 54 |
- This ensures seamless handover between sessions and agents.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
- Mark tasks `[x]` (done) when verified complete.
|
| 53 |
- Add notes under each task if you discover important findings (e.g., actual iteration count, loss values, timing).
|
| 54 |
- This ensures seamless handover between sessions and agents.
|
| 55 |
+
|
| 56 |
+
9. **UI & Pipeline Threading & State Guidelines**:
|
| 57 |
+
- **Cache `get_training_status()`**: Always use `@st.cache_data(ttl=5)` for log reading functions so UI polling loops do not cause disk I/O flooding.
|
| 58 |
+
- **`FaceRestoreHelper` Cache Key**: Do **NOT** include `upscale` in `_face_helper_cache` keys. `upscale` factor only changes `warpAffine` matrix scaling, not the detector model weights.
|
| 59 |
+
- **Thread Pool Guard**: Only use `ThreadPoolExecutor` in `pipeline.py` when `len(face_helper.cropped_faces) > 1`. Single-face processing must avoid thread pool overhead.
|
| 60 |
+
- **Thread Parameter Snapshots**: Always snapshot Streamlit sidebar values into local variables (e.g., `_w`, `_detector`) before spawning background threads to prevent UI state re-binding issues.
|
| 61 |
+
- **Unified ONNX Caching**: Always use `_get_onnx_session()` for creating or retrieving ONNX inference sessions instead of custom `hasattr` checks.
|
| 62 |
+
|
app.py
CHANGED
|
@@ -326,8 +326,11 @@ document.addEventListener('keydown', function(e) {
|
|
| 326 |
// Ctrl+S: Save settings (show toast)
|
| 327 |
if (e.ctrlKey && e.key === 's') {
|
| 328 |
e.preventDefault();
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
| 331 |
}
|
| 332 |
// Esc: Cancel processing
|
| 333 |
if (e.key === 'Escape') {
|
|
@@ -340,6 +343,7 @@ document.addEventListener('keydown', function(e) {
|
|
| 340 |
</script>
|
| 341 |
""", unsafe_allow_html=True)
|
| 342 |
|
|
|
|
| 343 |
def get_training_status():
|
| 344 |
import re
|
| 345 |
import datetime
|
|
@@ -358,7 +362,8 @@ def get_training_status():
|
|
| 358 |
if not log_files:
|
| 359 |
return None
|
| 360 |
|
| 361 |
-
|
|
|
|
| 362 |
try:
|
| 363 |
with open(latest_log, "r", encoding="utf-8") as f:
|
| 364 |
lines = f.readlines()[-30:]
|
|
@@ -377,7 +382,12 @@ def get_training_status():
|
|
| 377 |
if match:
|
| 378 |
epoch = int(match.group(1))
|
| 379 |
iteration = int(match.group(2).replace(",", ""))
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
loss = float(loss_match.group(1)) if loss_match else 0.0
|
| 382 |
|
| 383 |
# Extract timestamp at start of line: "2026-07-15 13:25:52"
|
|
@@ -942,6 +952,13 @@ with tab_batch:
|
|
| 942 |
key="batch_uploader"
|
| 943 |
)
|
| 944 |
if uploaded_files:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 945 |
# If not processing and no zip data, display start button
|
| 946 |
if not st.session_state.get('batch_processing') and st.session_state.get('batch_zip_data') is None and st.session_state.get('batch_error') is None:
|
| 947 |
if st.button("🚀 Process Batch", key="batch_button"):
|
|
@@ -974,6 +991,17 @@ with tab_batch:
|
|
| 974 |
st.session_state.batch_processing = False
|
| 975 |
st.stop()
|
| 976 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 977 |
# Queue IPC
|
| 978 |
batch_queue = queue.Queue()
|
| 979 |
st.session_state._batch_queue = batch_queue
|
|
@@ -998,17 +1026,17 @@ with tab_batch:
|
|
| 998 |
|
| 999 |
result = pipeline.process_image(
|
| 1000 |
img_b,
|
| 1001 |
-
w=
|
| 1002 |
-
detection_model=
|
| 1003 |
-
upscale=
|
| 1004 |
-
blend_softness=
|
| 1005 |
-
bg_upsampler=
|
| 1006 |
-
det_threshold=
|
| 1007 |
-
sharpen_amount=
|
| 1008 |
-
face_upsample=
|
| 1009 |
parallel=True,
|
| 1010 |
batch_size=4,
|
| 1011 |
-
face_restore=
|
| 1012 |
)
|
| 1013 |
|
| 1014 |
ok, buf = cv2.imencode(".png", result)
|
|
|
|
| 326 |
// Ctrl+S: Save settings (show toast)
|
| 327 |
if (e.ctrlKey && e.key === 's') {
|
| 328 |
e.preventDefault();
|
| 329 |
+
const toast = document.createElement('div');
|
| 330 |
+
toast.textContent = '✨ Settings saved!';
|
| 331 |
+
toast.style.cssText = 'position:fixed;bottom:24px;right:24px;background:#8b5cf6;color:white;padding:10px 18px;border-radius:8px;font-weight:600;z-index:999999;box-shadow:0 4px 12px rgba(0,0,0,0.3);';
|
| 332 |
+
document.body.appendChild(toast);
|
| 333 |
+
setTimeout(() => toast.remove(), 2500);
|
| 334 |
}
|
| 335 |
// Esc: Cancel processing
|
| 336 |
if (e.key === 'Escape') {
|
|
|
|
| 343 |
</script>
|
| 344 |
""", unsafe_allow_html=True)
|
| 345 |
|
| 346 |
+
@st.cache_data(ttl=5, show_spinner=False)
|
| 347 |
def get_training_status():
|
| 348 |
import re
|
| 349 |
import datetime
|
|
|
|
| 362 |
if not log_files:
|
| 363 |
return None
|
| 364 |
|
| 365 |
+
# B9 FIX: sort log files to get the newest log file
|
| 366 |
+
latest_log = sorted(log_files)[-1]
|
| 367 |
try:
|
| 368 |
with open(latest_log, "r", encoding="utf-8") as f:
|
| 369 |
lines = f.readlines()[-30:]
|
|
|
|
| 382 |
if match:
|
| 383 |
epoch = int(match.group(1))
|
| 384 |
iteration = int(match.group(2).replace(",", ""))
|
| 385 |
+
eta_raw = eta_match.group(1) if eta_match else "Unknown"
|
| 386 |
+
# B6 FIX: Clean up negative ETA strings
|
| 387 |
+
if "day" in eta_raw and "-" in eta_raw:
|
| 388 |
+
eta = "Finishing..."
|
| 389 |
+
else:
|
| 390 |
+
eta = eta_raw
|
| 391 |
loss = float(loss_match.group(1)) if loss_match else 0.0
|
| 392 |
|
| 393 |
# Extract timestamp at start of line: "2026-07-15 13:25:52"
|
|
|
|
| 952 |
key="batch_uploader"
|
| 953 |
)
|
| 954 |
if uploaded_files:
|
| 955 |
+
# B5 FIX: Reset stale batch state if the uploaded file list changed
|
| 956 |
+
current_file_signature = [f"{f.name}_{f.size}" for f in uploaded_files]
|
| 957 |
+
if st.session_state.get('_last_batch_file_signature') != current_file_signature:
|
| 958 |
+
st.session_state._last_batch_file_signature = current_file_signature
|
| 959 |
+
st.session_state.batch_zip_data = None
|
| 960 |
+
st.session_state.batch_error = None
|
| 961 |
+
|
| 962 |
# If not processing and no zip data, display start button
|
| 963 |
if not st.session_state.get('batch_processing') and st.session_state.get('batch_zip_data') is None and st.session_state.get('batch_error') is None:
|
| 964 |
if st.button("🚀 Process Batch", key="batch_button"):
|
|
|
|
| 991 |
st.session_state.batch_processing = False
|
| 992 |
st.stop()
|
| 993 |
|
| 994 |
+
# B4 FIX: Snapshot all sidebar parameter values before passing to thread
|
| 995 |
+
_w = fidelity_weight
|
| 996 |
+
_detector = face_detector
|
| 997 |
+
_upscale = upscale_factor
|
| 998 |
+
_blend = blend_softness
|
| 999 |
+
_bg_upsampler = 'realesrgan' if bg_upscale_toggle else None
|
| 1000 |
+
_det_thresh = det_threshold
|
| 1001 |
+
_sharpen = sharpen_amount
|
| 1002 |
+
_face_upsample = face_upscale_toggle
|
| 1003 |
+
_face_restore = enable_face_restoration
|
| 1004 |
+
|
| 1005 |
# Queue IPC
|
| 1006 |
batch_queue = queue.Queue()
|
| 1007 |
st.session_state._batch_queue = batch_queue
|
|
|
|
| 1026 |
|
| 1027 |
result = pipeline.process_image(
|
| 1028 |
img_b,
|
| 1029 |
+
w=_w,
|
| 1030 |
+
detection_model=_detector,
|
| 1031 |
+
upscale=_upscale,
|
| 1032 |
+
blend_softness=_blend,
|
| 1033 |
+
bg_upsampler=_bg_upsampler,
|
| 1034 |
+
det_threshold=_det_thresh,
|
| 1035 |
+
sharpen_amount=_sharpen,
|
| 1036 |
+
face_upsample=_face_upsample,
|
| 1037 |
parallel=True,
|
| 1038 |
batch_size=4,
|
| 1039 |
+
face_restore=_face_restore
|
| 1040 |
)
|
| 1041 |
|
| 1042 |
ok, buf = cv2.imencode(".png", result)
|
handover.md
CHANGED
|
@@ -766,4 +766,35 @@ Changes to [train_custom.py](file:///d:/.gemini-scratch/custom-ai-enhancer/train
|
|
| 766 |
|
| 767 |
### Git Commit & Push Status
|
| 768 |
- **Commit:** `7d99559` — "feat: add sequential model improvement roadmap (Phase 1 complete)"
|
| 769 |
-
- **Status:**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
|
| 767 |
### Git Commit & Push Status
|
| 768 |
- **Commit:** `7d99559` — "feat: add sequential model improvement roadmap (Phase 1 complete)"
|
| 769 |
+
- **Status:** Committed.
|
| 770 |
+
|
| 771 |
+
---
|
| 772 |
+
|
| 773 |
+
## Task 14: Codebase Bug Audit & Full Remediation (B1–B9)
|
| 774 |
+
|
| 775 |
+
**Date:** 2026-07-20
|
| 776 |
+
**Status:** ✅ Completed
|
| 777 |
+
|
| 778 |
+
### Overview
|
| 779 |
+
Audited `app.py` (1110 lines) and `pipeline.py` (635 lines) during background model training. Identified 9 bugs across state management, caching, threading, and UI performance, and fully remediated all 9. Formulated Rule 9 in `AGENTS.md` to prevent regression.
|
| 780 |
+
|
| 781 |
+
### Remediation Details
|
| 782 |
+
|
| 783 |
+
| Bug ID | Severity | File | Problem Description | Fix Applied |
|
| 784 |
+
|--------|----------|------|---------------------|-------------|
|
| 785 |
+
| **B1** | 🔴 Critical | `app.py` | `get_training_status()` ran on every 100ms UI rerun during processing, causing log file I/O flooding. | Added `@st.cache_data(ttl=5, show_spinner=False)` decorator to throttle log parsing. |
|
| 786 |
+
| **B2** | 🔴 Critical | `pipeline.py` | `parallel=True` spawned `ThreadPoolExecutor` even for single-face images, adding ~20ms overhead. | Guarded thread pool execution with `len(face_helper.cropped_faces) > 1`. |
|
| 787 |
+
| **B3** | 🟠 High | `pipeline.py` | `_face_helper_cache` included `upscale` factor in key, forcing full 3-5s model re-inits on upscale change. | Simplified cache key to `detection_model` only (`upscale` is handled in affine warp stage). |
|
| 788 |
+
| **B4** | 🟠 High | `app.py` | Batch processing worker thread captured outer scope sidebar variables, leading to state mutation during execution. | Snapshotted all parameter variables (`_w`, `_detector`, etc.) before starting background thread. |
|
| 789 |
+
| **B5** | 🟠 High | `app.py` | Uploading a new file batch retained previous batch's `batch_zip_data` in session state. | Added file signature tracking (`_last_batch_file_signature`) to reset zip state on input change. |
|
| 790 |
+
| **B6** | 🟠 High | `app.py` | Negative ETA string (e.g. `-1 day, 23:59:26`) rendered directly in training dashboard. | Formatted negative ETA strings to display `"Finishing..."`. |
|
| 791 |
+
| **B7** | 🟡 Medium | `app.py` | `Ctrl+S` shortcut triggered blocking `alert()` browser dialog. | Replaced `alert()` with a non-blocking floating toast notification DOM element. |
|
| 792 |
+
| **B8** | 🟡 Medium | `pipeline.py` | Real-ESRGAN ONNX session used ad-hoc `hasattr` check instead of central cache. | Unified session loading via `_get_onnx_session()`. |
|
| 793 |
+
| **B9** | 🟡 Medium | `app.py` | `get_training_status()` read `log_files[0]`, which was not guaranteed to be the newest log file. | Sorted `log_files` alphabetically by timestamp and selected `[-1]`. |
|
| 794 |
+
|
| 795 |
+
### Rule Enforced
|
| 796 |
+
Added **Rule 9** to `AGENTS.md` and synced with Obsidian Vault `D:\AgentBrain\`.
|
| 797 |
+
|
| 798 |
+
### Git Commit & Push Status
|
| 799 |
+
- **Status:** Pending commit.
|
| 800 |
+
|
pipeline.py
CHANGED
|
@@ -140,15 +140,13 @@ class LocalAIEnhancerPipeline:
|
|
| 140 |
img_rgb = img_rgb.astype(np.float32) / 255.0
|
| 141 |
img_input = np.transpose(img_rgb, (2, 0, 1))
|
| 142 |
img_input = np.expand_dims(img_input, axis=0)
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
ort_inputs = {self.ort_session_re.get_inputs()[0].name: img_input}
|
| 151 |
-
ort_outs = self.ort_session_re.run(None, ort_inputs)
|
| 152 |
output_tensor = ort_outs[0]
|
| 153 |
|
| 154 |
output = np.squeeze(output_tensor, axis=0)
|
|
@@ -312,7 +310,11 @@ class LocalAIEnhancerPipeline:
|
|
| 312 |
|
| 313 |
# Set up FaceRestoreHelper for face processing
|
| 314 |
os.environ['FACE_DETECTOR_PATH'] = os.path.join(project_dir, "weights", "facelib")
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
if cache_key not in self._face_helper_cache:
|
| 317 |
print(f"[Pipeline] Creating new FaceRestoreHelper for {detection_model} (upscale={upscale})...")
|
| 318 |
face_helper = FaceRestoreHelper(
|
|
@@ -397,7 +399,10 @@ class LocalAIEnhancerPipeline:
|
|
| 397 |
|
| 398 |
self._report_progress("restoration", 0.8, "Face restoration complete")
|
| 399 |
else:
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
| 401 |
def _process_face(idx, cropped_face):
|
| 402 |
if self.use_onnx:
|
| 403 |
try:
|
|
|
|
| 140 |
img_rgb = img_rgb.astype(np.float32) / 255.0
|
| 141 |
img_input = np.transpose(img_rgb, (2, 0, 1))
|
| 142 |
img_input = np.expand_dims(img_input, axis=0)
|
| 143 |
+
|
| 144 |
+
# B8 FIX: Use unified _get_onnx_session() cache instead of ad-hoc
|
| 145 |
+
# hasattr/None check, which would not survive garbage collection.
|
| 146 |
+
session = self._get_onnx_session(self.realesrgan_onnx_path)
|
| 147 |
+
|
| 148 |
+
ort_inputs = {session.get_inputs()[0].name: img_input}
|
| 149 |
+
ort_outs = session.run(None, ort_inputs)
|
|
|
|
|
|
|
| 150 |
output_tensor = ort_outs[0]
|
| 151 |
|
| 152 |
output = np.squeeze(output_tensor, axis=0)
|
|
|
|
| 310 |
|
| 311 |
# Set up FaceRestoreHelper for face processing
|
| 312 |
os.environ['FACE_DETECTOR_PATH'] = os.path.join(project_dir, "weights", "facelib")
|
| 313 |
+
# B3 FIX: upscale is NOT part of FaceRestoreHelper initialisation — it only
|
| 314 |
+
# affects warpAffine geometry in paste_faces_custom_blend. Including upscale
|
| 315 |
+
# in the cache key caused a full model re-init (3-5 s) on every upscale
|
| 316 |
+
# factor change. Only the detection model matters for the helper instance.
|
| 317 |
+
cache_key = detection_model
|
| 318 |
if cache_key not in self._face_helper_cache:
|
| 319 |
print(f"[Pipeline] Creating new FaceRestoreHelper for {detection_model} (upscale={upscale})...")
|
| 320 |
face_helper = FaceRestoreHelper(
|
|
|
|
| 399 |
|
| 400 |
self._report_progress("restoration", 0.8, "Face restoration complete")
|
| 401 |
else:
|
| 402 |
+
# B2 FIX: Only use ThreadPoolExecutor when there are multiple faces.
|
| 403 |
+
# For single-face images (the common case), spawning a thread pool
|
| 404 |
+
# adds ~20 ms of overhead with zero parallelism benefit.
|
| 405 |
+
if parallel and len(face_helper.cropped_faces) > 1:
|
| 406 |
def _process_face(idx, cropped_face):
|
| 407 |
if self.use_onnx:
|
| 408 |
try:
|