feat: web UI zone mode selector with preset dropdown

This commit is contained in:
jetson committed 2026-09-22 13:08:28 +07:00
1 parent 7c91081ea8
commit f0ea95dd26
3 files changed
+174

No files matched your search

+50
View File
@@ -19,6 +19,7 @@ from pathlib import Path
from src.job import JobQueue
from src.model_registry import scan_models, scan_model_groups, ModelConfig
from src.preview import extract_thumbnail, extract_sample_frames
from src.zone_config import load_zone_config, list_zone_presets, build_static_roi, get_active_zone
load_dotenv()
@@ -36,6 +37,7 @@ os.makedirs(OUTPUT_DIR, exist_ok=True)
job_queue = JobQueue(output_dir=OUTPUT_DIR)
VIDEO_EXTENSIONS = ('.mp4', '.avi', '.mkv', '.mov', '.webm')
ZONES_PATH = os.getenv("ZONES_PATH", "./zones.json")
def _parse_model_configs(form):
@@ -125,10 +127,28 @@ def upload():
n += 1
video.save(video_path)
zone_mode = request.form.get("zone_mode", "dynamic")
fixed_zone = None
zone_preset = ""
if zone_mode == "fixed" and os.path.isfile(ZONES_PATH):
preset_name = request.form.get("zone_preset", "")
zone = get_active_zone(ZONES_PATH, preset_name or None)
if zone:
import cv2
cap = cv2.VideoCapture(video_path)
vw = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
vh = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
cap.release()
fixed_zone = build_static_roi(zone, vw, vh)
zone_preset = preset_name or "default"
job = job_queue.add_job(
video_path=video_path,
model_configs=model_configs,
class_filters=class_filters,
zone_mode=zone_mode,
fixed_zone=fixed_zone,
zone_preset=zone_preset,
)
return redirect(url_for("status", job_id=job.job_id))
@@ -144,10 +164,28 @@ def upload_reuse():
if not model_configs:
return "No models selected", 400
zone_mode = request.form.get("zone_mode", "dynamic")
fixed_zone = None
zone_preset = ""
if zone_mode == "fixed" and os.path.isfile(ZONES_PATH):
preset_name = request.form.get("zone_preset", "")
zone = get_active_zone(ZONES_PATH, preset_name or None)
if zone:
import cv2
cap = cv2.VideoCapture(video_path)
vw = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
vh = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
cap.release()
fixed_zone = build_static_roi(zone, vw, vh)
zone_preset = preset_name or "default"
job = job_queue.add_job(
video_path=video_path,
model_configs=model_configs,
class_filters=class_filters,
zone_mode=zone_mode,
fixed_zone=fixed_zone,
zone_preset=zone_preset,
)
return redirect(url_for("status", job_id=job.job_id))
@@ -224,6 +262,18 @@ def api_models():
])
@app.route("/api/zones")
def api_zones():
"""Return zone presets from zones.json."""
if not os.path.isfile(ZONES_PATH):
return jsonify([])
try:
presets = list_zone_presets(ZONES_PATH)
return jsonify(presets)
except Exception:
return jsonify([])
@app.route("/api/jobs")
def api_jobs():
return jsonify([{
+48
View File
@@ -27,6 +27,10 @@
var existingSection = document.getElementById('existing-videos-section');
var videoList = document.getElementById('video-list');
var selectedExistingVideo = null;
var zoneSection = document.getElementById('zone-section');
var zoneFixedConfig = document.getElementById('zone-fixed-config');
var zonePresetSelect = document.getElementById('zone-preset-select');
var zoneRadios = document.querySelectorAll('input[name="zone_mode"]');
/* ------------------------------------------------
Upload Zone — click, drag-drop, keyboard
@@ -115,6 +119,7 @@
reader.readAsDataURL(file);
updateSubmitState();
showZoneSection();
}
function formatBytes(bytes) {
@@ -229,6 +234,7 @@
// Hide upload zone, clear file input
fileInput.value = '';
previewWrap.classList.remove('visible');
showZoneSection();
} else {
selectedExistingVideo = null;
videoPathInput.value = '';
@@ -266,6 +272,47 @@
.catch(function () {});
}
/* ------------------------------------------------
Zone Mode — show/hide, fetch presets
------------------------------------------------ */
function showZoneSection() {
if (zoneSection) zoneSection.style.display = '';
}
function initZoneMode() {
if (!zoneSection) return;
// Fetch presets from API
fetch('/api/zones')
.then(function (res) { return res.json(); })
.then(function (presets) {
if (!presets || presets.length === 0) {
var fixedRadio = zoneSection.querySelector('input[value="fixed"]');
if (fixedRadio) fixedRadio.closest('label').style.display = 'none';
return;
}
zonePresetSelect.innerHTML = '';
presets.forEach(function (p) {
var opt = document.createElement('option');
opt.value = p.name;
opt.textContent = p.name + (p.description ? ' — ' + p.description : '');
zonePresetSelect.appendChild(opt);
});
})
.catch(function () {});
zoneRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
var isFixed = zoneSection.querySelector('input[value="fixed"]').checked;
if (isFixed) {
zoneFixedConfig.classList.add('visible');
} else {
zoneFixedConfig.classList.remove('visible');
}
});
});
}
/* ------------------------------------------------
Submit — enable/disable, loading state
------------------------------------------------ */
@@ -310,6 +357,7 @@
initUploadZone();
initModelCards();
initFormSubmit();
initZoneMode();
loadExistingVideos();
// Poll active jobs
+76
View File
@@ -270,6 +270,59 @@
font-size: var(--text-sm);
}
/* Zone mode section */
.zone-section { margin-top: var(--space-8); }
.zone-section h3 {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
margin-bottom: var(--space-4);
}
.zone-radio-group {
display: flex;
gap: var(--space-4);
margin-bottom: var(--space-4);
}
.zone-radio-group label {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--color-neutral-700);
cursor: pointer;
}
.zone-radio-group input[type="radio"] {
accent-color: var(--color-primary);
}
.zone-fixed-config {
display: none;
padding: var(--space-4);
background: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: var(--radius-md);
}
.zone-fixed-config.visible { display: block; }
.zone-preset-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.zone-preset-row label {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--color-neutral-600);
white-space: nowrap;
}
.zone-preset-row select {
flex: 1;
max-width: 320px;
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
border: 1px solid var(--color-neutral-300);
border-radius: var(--radius-sm);
background: white;
color: var(--color-neutral-700);
}
/* Video delete button */
.video-delete-btn {
flex-shrink: 0;
@@ -341,6 +394,29 @@
<div class="video-list" id="video-list"></div>
</div>
{# --- Counting Zone --- #}
<div class="zone-section" id="zone-section" style="display:none">
<h3>Counting Zone</h3>
<div class="zone-radio-group">
<label>
<input type="radio" name="zone_mode" value="dynamic" checked>
Auto-detect Truck
</label>
<label>
<input type="radio" name="zone_mode" value="fixed">
Fixed Zone
</label>
</div>
<div class="zone-fixed-config" id="zone-fixed-config">
<div class="zone-preset-row">
<label for="zone-preset-select">Preset:</label>
<select name="zone_preset" id="zone-preset-select">
<option value="">Loading presets...</option>
</select>
</div>
</div>
</div>
{# --- Model Selection --- #}
<div class="model-section">
<div class="model-section-header">