Files
fish_monitor/Zero2YoloYard-main/templates/root.html
T
2025-12-25 17:41:18 +08:00

955 lines
52 KiB
HTML

{% extends "layout.html" %}
{% from "_macros.html" import render_modal %}
{% block content %}
<div class="container-fluid px-4 px-lg-5 content-wrapper fade-in">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-end page-header">
<div>
<h1 class="page-title mb-0">Dashboard</h1>
<p class="page-subtitle">Manage your computer vision pipeline.</p>
</div>
<ul class="nav nav-pills-fluent mt-3 mt-md-0" id="mainTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="videos-tab" data-toggle="tab" href="#videos" role="tab">
Videos
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="datasets-tab" data-toggle="tab" href="#datasets" role="tab">
Datasets
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="models-tab" data-toggle="tab" href="#models" role="tab">
Models
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="settings-tab" data-toggle="tab" href="#settings" role="tab">
Settings
</a>
</li>
</ul>
</div>
<div class="tab-content" id="mainTabContent">
<div class="tab-pane fade show active" id="videos" role="tabpanel">
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-primary shadow-sm" data-toggle="modal" data-target="#uploadVideoModal">
<i class="bi bi-plus-lg mr-2"></i>New Video
</button>
</div>
<input type="file" id="frame-import-input" webkitdirectory directory multiple style="display: none;" />
<div class="fluent-card">
<div class="table-responsive">
<table class="table table-fluent table-hover mb-0">
<thead>
<tr>
<th style="width: 25%">Description</th>
<th>Filename</th>
<th>Status</th>
<th>Progress</th>
<th>Labels</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody id="video-list"></tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade" id="datasets" role="tabpanel">
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-primary shadow-sm" data-toggle="modal" data-target="#createDatasetModal">
<i class="bi bi-plus-lg mr-2"></i>Create Dataset
</button>
</div>
<div class="fluent-card">
<div class="table-responsive">
<table class="table table-fluent table-hover mb-0">
<thead>
<tr>
<th style="width: 25%">Name</th>
<th>Status</th>
<th>Composition</th>
<th>Classes</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody id="dataset-list"></tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade" id="models" role="tabpanel">
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-primary shadow-sm" data-toggle="modal" data-target="#importModelModal">
<i class="bi bi-upload mr-2"></i>Import Model
</button>
</div>
<div class="fluent-card">
<div class="table-responsive">
<table class="table table-fluent table-hover mb-0">
<thead>
<tr>
<th>Description</th>
<th>Type</th>
<th>Label Map</th>
<th>Date</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody id="model-list"></tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade" id="settings" role="tabpanel">
<form id="settings-form">
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-9">
<div class="fluent-card settings-section">
<div class="settings-section-title">
<i class="bi bi-cpu mr-2 text-primary"></i> AI Engine & Hardware
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Computation Device</label>
<select id="setting-gpu-device" class="form-control custom-select">
<option value="auto">Auto-detect</option>
<option value="cpu">CPU Only</option>
<option value="cuda:0">NVIDIA GPU #0</option>
<option value="cuda:1">NVIDIA GPU #1</option>
</select>
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Assistance Model (SAM)</label>
<select id="setting-sam-model" class="form-control custom-select">
<option value="sam2.1_t.pt" data-name="SAM 2.1 Tiny">SAM 2.1 Tiny (Fast)</option>
<option value="sam2.1_s.pt" data-name="SAM 2.1 Small">SAM 2.1 Small (Balanced)</option>
<option value="sam2.1_b.pt" data-name="SAM 2.1 Base">SAM 2.1 Base</option>
<option value="sam2.1_l.pt" data-name="SAM 2.1 Large">SAM 2.1 Large (Precise)</option>
</select>
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Feature Extractor</label>
<select id="setting-feature-extractor-model" class="form-control custom-select">
<option value="mobilenet_v3_large">MobileNetV3-Large</option>
<option value="mobilenet_v3_small">MobileNetV3-Small</option>
</select>
</div>
</div>
</div>
<div class="fluent-card settings-section">
<div class="settings-section-title">
<i class="bi bi-sliders mr-2 text-primary"></i> Algorithm Parameters
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label class="d-flex justify-content-between small text-muted font-weight-bold">
SAM Mask Conf. <span id="sam-mask-conf-val" class="text-primary">0.35</span>
</label>
<input type="range" class="custom-range" id="setting-sam-mask-confidence" min="0.1" max="0.9" step="0.05">
</div>
<div class="form-group col-md-4">
<label class="d-flex justify-content-between small text-muted font-weight-bold">
NMS IoU <span id="nms-iou-val" class="text-primary">0.70</span>
</label>
<input type="range" class="custom-range" id="setting-nms-iou-threshold" min="0.1" max="1.0" step="0.05">
</div>
<div class="form-group col-md-4">
<label class="d-flex justify-content-between small text-muted font-weight-bold">
Proto. Temp <span id="prototype-temp-val" class="text-primary">0.07</span>
</label>
<input type="range" class="custom-range" id="setting-prototype-temperature" min="0.01" max="0.5" step="0.01">
</div>
</div>
</div>
<div class="fluent-card settings-section">
<div class="settings-section-title">
<i class="bi bi-ui-checks mr-2 text-primary"></i> Workflow
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Default Tool</label>
<select id="setting-default-annotation-mode" class="form-control custom-select">
<option value="manual">Manual Draw</option>
<option value="sam">SAM (Point)</option>
<option value="lam">LAM (Click to Label)</option>
<option value="smart_select">Smart Select</option>
</select>
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Auto-Save</label>
<div class="custom-control custom-switch mt-2">
<input type="checkbox" class="custom-control-input" id="setting-autosave-enabled">
<label class="custom-control-label" for="setting-autosave-enabled">Save on navigation</label>
</div>
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">JPEG Quality</label>
<input type="number" id="setting-jpeg-quality" class="form-control" min="10" max="100" step="5">
</div>
<div class="form-group col-md-6">
<label class="small text-muted font-weight-bold">Legacy Tracker</label>
<select id="setting-opencv-tracker" class="form-control custom-select">
{% for tracker in tracker_fns %}<option value="{{ tracker }}">{{ tracker }}</option>{% endfor %}
</select>
</div>
</div>
</div>
<div class="fluent-card settings-section">
<div class="settings-section-title">
<i class="bi bi-palette mr-2 text-primary"></i> Class Colors
</div>
<div id="class-color-manager" class="d-flex flex-wrap custom-scrollbar" style="max-height: 200px; overflow-y: auto; gap: 10px;"></div>
</div>
<div class="fluent-card settings-section" style="border-left: 4px solid var(--color-warning);">
<div class="settings-section-title text-warning">
<i class="bi bi-tools mr-2"></i> Maintenance
</div>
<div class="d-flex align-items-center justify-content-between">
<div class="form-group mb-0 mr-3 flex-grow-1">
<label class="small text-muted font-weight-bold">Cache Save Interval (s)</label>
<input type="number" class="form-control" id="setting-cache-save-interval" min="10" max="300">
</div>
<button type="button" class="btn btn-outline-warning" id="clear-cache-btn">
<i class="bi bi-eraser-fill mr-1"></i> Clear Cache
</button>
</div>
</div>
<div class="text-right pb-5">
<button type="submit" class="btn btn-primary px-4 shadow">
Apply Settings
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
{% call render_modal('uploadVideoModal', 'Upload New Video') %}
<form id="upload-video-form">
<div class="form-group">
<label class="font-weight-600">Description</label>
<input type="text" class="form-control" id="video-description" name="description" placeholder="E.g., Field Test Match 1" required maxlength="{{ limit_data.MAX_DESCRIPTION_LENGTH }}">
</div>
<div class="form-group">
<label class="font-weight-600">Video File (.mp4)</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="video-file" name="video_file" accept="video/mp4" required>
<label class="custom-file-label" for="video-file">Choose file...</label>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Upload & Process</button>
</form>
{% endcall %}
{% call render_modal('createDatasetModal', 'Create Dataset') %}
<form id="create-dataset-form">
<div class="form-group">
<label class="font-weight-600">Name</label>
<input type="text" class="form-control" id="dataset-description" placeholder="E.g., Season 2024 V1" required maxlength="{{ limit_data.MAX_DESCRIPTION_LENGTH }}">
</div>
<div class="form-group">
<label class="font-weight-600">Videos</label>
<select multiple class="form-control custom-select" id="dataset-videos" required size="5"></select>
<small class="text-muted">Ctrl/Cmd + Click to select multiple.</small>
</div>
<div class="form-row">
<div class="form-group col-6">
<label class="font-weight-600">Valid %</label>
<input type="number" class="form-control" id="eval-percent" value="20">
</div>
<div class="form-group col-6">
<label class="font-weight-600">Test %</label>
<input type="number" class="form-control" id="test-percent" value="10">
</div>
</div>
<div class="bg-light p-3 rounded mb-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 font-weight-bold">Augmentation</h6>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="aug-enabled">
<label class="custom-control-label" for="aug-enabled"></label>
</div>
</div>
<div id="augmentation-options-panel" class="mt-3" style="display: none;">
<div class="form-group">
<label class="small text-muted font-weight-bold">Copies per Image</label>
<input type="number" class="form-control" id="aug-multiply-factor" value="3">
</div>
{% include '_augmentation_controls.html' %}
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Create Dataset</button>
</form>
{% endcall %}
{% call render_modal('importModelModal', 'Import Model') %}
<form id="import-model-form">
<div class="form-group">
<label class="font-weight-600">Description</label>
<input type="text" class="form-control" id="model-description" name="description" required maxlength="{{ limit_data.MAX_DESCRIPTION_LENGTH }}">
</div>
<div class="form-group">
<label class="font-weight-600">Model (.tflite)</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="model-file" name="model_file" accept=".tflite" required>
<label class="custom-file-label" for="model-file">Select file...</label>
</div>
</div>
<div class="form-group">
<label class="font-weight-600">Label Map (.txt)</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="label-file" name="label_file" accept=".txt,.labels" required>
<label class="custom-file-label" for="label-file">Select file...</label>
</div>
</div>
<div class="form-group">
<label class="font-weight-600">Type</label>
<select class="form-control custom-select" id="model-type" name="model_type" required>
<option value="float32">Float32 (Standard)</option>
<option value="uint8">UINT8 (Edge TPU)</option>
</select>
</div>
<button type="submit" class="btn btn-primary btn-block">Import</button>
</form>
{% endcall %}
{% call render_modal('preAnnotateModal', 'Auto-Label') %}
<form id="pre-annotate-form">
<input type="hidden" id="pre-annotate-video-uuid" data-frame-count="0" data-labeled-count="0">
<div class="form-group">
<label class="font-weight-600">Model</label>
<select id="pre-annotate-model-select" class="form-control custom-select" required></select>
</div>
<div class="form-row">
<div class="form-group col-6">
<label class="font-weight-600">Start</label>
<input type="number" id="pre-annotate-start-frame" class="form-control">
</div>
<div class="form-group col-6">
<label class="font-weight-600">End</label>
<input type="number" id="pre-annotate-end-frame" class="form-control">
</div>
</div>
<div class="form-group">
<label class="d-flex justify-content-between font-weight-600">
Conf. <span id="confidence-value" class="text-primary">0.5</span>
</label>
<input type="range" class="custom-range" id="pre-annotate-confidence" min="0.1" max="0.95" step="0.05" value="0.5">
</div>
<div class="form-group">
<label class="font-weight-600 d-block mb-2">Strategy</label>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="strategy-overwrite" name="merge_strategy" value="overwrite" class="custom-control-input" checked>
<label class="custom-control-label" for="strategy-overwrite">Overwrite</label>
</div>
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="strategy-skip" name="merge_strategy" value="skip_labeled" class="custom-control-input">
<label class="custom-control-label" for="strategy-skip">Fill Gaps</label>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Start</button>
</form>
{% endcall %}
<div class="modal fade" id="manageTasksModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content border-0 shadow-lg" style="border-radius: var(--radius-lg);">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title font-weight-bold" id="manageTasksModalTitle">Tasks</h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body pt-3">
<div class="table-responsive rounded bg-light mb-4 border-0">
<table class="table table-sm table-hover mb-0 bg-transparent">
<tbody id="task-list-in-modal"></tbody>
</table>
</div>
<h6 class="font-weight-bold mb-3">New Task</h6>
<form id="create-task-form">
<input type="hidden" id="task-video-uuid">
<div class="form-row">
<div class="form-group col-md-4">
<input type="text" class="form-control form-control-sm" id="task-assigned-to" placeholder="User" required>
</div>
<div class="form-group col-md-3">
<input type="number" class="form-control form-control-sm" id="task-start-frame" placeholder="Start" required>
</div>
<div class="form-group col-md-3">
<input type="number" class="form-control form-control-sm" id="task-end-frame" placeholder="End" required>
</div>
<div class="form-group col-md-2">
<button type="submit" class="btn btn-sm btn-success btn-block">Add</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
let currentSettings = {};
$('.custom-file-input').on('change', function() {
let fileName = $(this).val().split('\\').pop();
$(this).next('.custom-file-label').addClass("selected").html(fileName);
});
function getStatusBadge(status, message) {
let badgeClass = 'secondary';
let icon = '';
if (status === 'READY') {
badgeClass = 'warning';
if(!message) badgeClass = 'success';
icon = '<i class="bi bi-check-circle-fill mr-1"></i>';
} else if (['PROCESSING', 'EXTRACTING', 'PENDING', 'PRE_ANNOTATING', 'APPLYING_PROTOTYPES'].includes(status)) {
badgeClass = 'info';
icon = '<span class="spinner-border spinner-border-sm mr-1"></span>';
} else if (status === 'CANCELLING') {
badgeClass = 'warning';
icon = '<i class="bi bi-x-circle mr-1"></i>';
} else if (status === 'FAILED') {
badgeClass = 'danger';
icon = '<i class="bi bi-exclamation-triangle-fill mr-1"></i>';
}
return `<span class="badge badge-${badgeClass} d-inline-flex align-items-center">${icon} ${status}</span>`;
}
function showSuccess(message) { alert(message); }
function showError(message) { alert('Error: ' + message); }
function refreshVideos() {
$.get('/listVideos', function(data) {
const videoList = $('#video-list');
videoList.empty();
data.all_videos.forEach(video => {
const isReady = video.status === 'READY';
let actions = '';
if (isReady) {
actions += `<button class="btn btn-sm btn-info btn-import-frames mr-1" data-uuid="${video.video_uuid}" title="Import frames"><i class="bi bi-images"></i></button>`;
actions += `<button class="btn btn-sm btn-warning btn-pre-annotate mr-1" data-uuid="${video.video_uuid}" data-frame-count="${video.frame_count}" data-labeled-count="${video.labeled_frame_count}" title="Auto-Label"><i class="bi bi-robot"></i></button>`;
actions += `<button class="btn btn-sm btn-success btn-manage-tasks mr-1" data-uuid="${video.video_uuid}" data-description="${video.description}" data-framecount="${video.frame_count}" title="Tasks"><i class="bi bi-card-checklist"></i></button>`;
} else {
actions += `<button class="btn btn-sm btn-secondary mr-1" disabled><i class="bi bi-images"></i></button>`;
}
if (['PRE_ANNOTATING', 'APPLYING_PROTOTYPES'].includes(video.status)) {
actions += `<button class="btn btn-sm btn-danger btn-cancel-task mr-1" data-uuid="${video.video_uuid}" title="Cancel Task"><i class="bi bi-stop-circle"></i></button>`;
}
actions += `<button class="btn btn-sm btn-outline-danger btn-delete-video" data-uuid="${video.video_uuid}" title="Delete"><i class="bi bi-trash"></i></button>`;
const progress = video.extracted_frame_count ? `${video.extracted_frame_count} / ${video.frame_count || '?'}` : '-';
const statusHtml = getStatusBadge(video.status, video.status_message);
const statusMsg = video.status_message ? `<div class="small text-muted mt-1">${video.status_message}</div>` : '';
const row = `
<tr>
<td class="align-middle"><strong>${video.description}</strong></td>
<td class="align-middle text-muted small">${video.video_filename || ''}</td>
<td class="align-middle">${statusHtml}${statusMsg}</td>
<td class="align-middle font-weight-bold text-muted small">${progress}</td>
<td class="align-middle"><span class="badge badge-neutral">${video.labeled_frame_count}</span></td>
<td class="align-middle text-right">${actions}</td>
</tr>`;
videoList.append(row);
});
const datasetVideoSelect = $('#dataset-videos');
datasetVideoSelect.empty();
data.ready_videos_for_dataset.forEach(video => {
datasetVideoSelect.append(`<option value="${video.video_uuid}">${video.description} (${video.labeled_frame_count} labels)</option>`);
});
});
}
function refreshDatasets() {
$.get('/listDatasets', function(data) {
const datasetList = $('#dataset-list');
datasetList.empty();
data.datasets.forEach(dataset => {
let actions = '';
if (dataset.status === 'READY') {
actions += `<a href="/downloadDataset/${dataset.dataset_uuid}" class="btn btn-sm btn-success mr-1" title="Download"><i class="bi bi-download"></i></a>`;
actions += `<a href="/datasetAnalysis/${dataset.dataset_uuid}" class="btn btn-sm btn-info mr-1" title="Analyze"><i class="bi bi-bar-chart-line"></i></a>`;
actions += `<button class="btn btn-sm btn-secondary btn-regenerate-dataset mr-1" data-uuid="${dataset.dataset_uuid}" title="Update"><i class="bi bi-arrow-repeat"></i></button>`;
} else if (dataset.status === 'FAILED') {
actions += `<button class="btn btn-sm btn-warning btn-regenerate-dataset mr-1" data-uuid="${dataset.dataset_uuid}" title="Retry"><i class="bi bi-arrow-repeat"></i></button>`;
}
actions += `<button class="btn btn-sm btn-outline-danger btn-delete-dataset" data-uuid="${dataset.dataset_uuid}" title="Delete"><i class="bi bi-trash"></i></button>`;
const videosCount = JSON.parse(dataset.video_uuids || '[]').length;
const classes = JSON.parse(dataset.sorted_label_list || '[]')
.map(c => `<span class="badge badge-neutral badge-pill mr-1 mb-1">${c}</span>`).join('');
const row = `
<tr>
<td class="align-middle"><strong>${dataset.description}</strong></td>
<td class="align-middle">${getStatusBadge(dataset.status, dataset.status_message)}</td>
<td class="align-middle">${videosCount} video(s)</td>
<td class="align-middle" style="line-height: 1.8;">${classes || '<span class="text-muted">-</span>'}</td>
<td class="align-middle text-right">${actions}</td>
</tr>`;
datasetList.append(row);
});
});
}
function refreshModels() {
$.get('/listModels', function(data) {
const modelList = $('#model-list');
modelList.empty();
data.models.forEach(model => {
const createdDate = new Date(model.create_time_ms).toLocaleDateString();
const typeBadge = model.model_type === 'uint8'
? `<span class="badge badge-info">INT8 (TPU)</span>`
: `<span class="badge badge-primary">FP32</span>`;
const row = `
<tr>
<td class="align-middle"><strong>${model.description}</strong></td>
<td class="align-middle">${typeBadge}</td>
<td class="align-middle text-muted small">${model.label_filename || 'N/A'}</td>
<td class="align-middle text-muted small">${createdDate}</td>
<td class="align-middle text-right">
<button class="btn btn-sm btn-outline-danger btn-delete-model" data-uuid="${model.model_uuid}"><i class="bi bi-trash"></i></button>
</td>
</tr>`;
modelList.append(row);
});
});
}
function loadSettings() {
$.get('/api/settings', function(data) {
if (data.success) {
currentSettings = data.settings;
const s = data.settings;
$('#setting-gpu-device').val(s.gpu_device);
$('#setting-sam-model').val(s.sam_model_checkpoint);
$('#setting-feature-extractor-model').val(s.feature_extractor_model_name);
$('#setting-sam-mask-confidence').val(s.sam_mask_confidence).trigger('input');
$('#setting-nms-iou-threshold').val(s.nms_iou_threshold).trigger('input');
$('#setting-prototype-temperature').val(s.prototype_temperature).trigger('input');
$('#setting-prototype-sample-limit').val(s.prototype_sample_limit);
$('#setting-batch-tracking-imgsz').val(s.batch_tracking_imgsz);
$('#setting-batch-tracking-conf').val(s.batch_tracking_conf).trigger('input');
$('#setting-batch-tracking-chunk-size').val(s.batch_tracking_chunk_size);
$('#setting-default-annotation-mode').val(s.default_annotation_mode);
$('#setting-autosave-enabled').prop('checked', s.autosave_enabled);
$('#setting-preannotation-conf').val(s.default_preannotation_conf).trigger('input');
$('#setting-jpeg-quality').val(s.frame_extraction_jpeg_quality);
$('#setting-opencv-tracker').val(s.default_opencv_tracker);
$('#setting-cache-save-interval').val(s.cache_save_interval_seconds);
loadClassColorManager();
}
});
}
function loadClassColorManager() {
$.get('/listClasses', function(data) {
if (data.success) {
const container = $('#class-color-manager');
container.empty();
if (data.labels.length === 0) {
container.html('<p class="text-muted small pl-2">No classes found. Add classes in the annotation view first.</p>');
return;
}
const classColors = currentSettings.class_colors || {};
data.labels.forEach(label => {
const color = classColors[label] || '#000000';
const item = `
<div class="d-flex align-items-center bg-white border rounded px-2 py-1 mr-2 mb-2 shadow-sm" style="min-width: 120px;">
<input type="color" class="border-0 p-0 mr-2 rounded-circle" value="${color}" data-class-name="${label}"
style="width: 24px; height: 24px; cursor: pointer; background: none;" title="Change color for ${label}">
<span class="small font-weight-bold text-truncate" style="max-width: 100px;">${label}</span>
</div>`;
container.append(item);
});
}
});
}
function setupAugmentationListeners(contextSelector) {
const context = $(contextSelector);
context.on('input', 'input[type="range"]', function() {
$(this).closest('div').find('.val-display').text($(this).val());
});
context.on('change', '.aug-option input[type="checkbox"]', function() {
const controlsId = $(this).closest('.aug-option').data('controls');
context.find('#' + controlsId).toggle($(this).is(':checked'));
});
context.find('.aug-option input[type="checkbox"]').each(function() {
const controlsId = $(this).closest('.aug-option').data('controls');
context.find('#' + controlsId).toggle($(this).is(':checked'));
});
}
$('#aug-enabled').on('change', function() {
$('#augmentation-options-panel').slideToggle($(this).is(':checked'));
});
setupAugmentationListeners('#createDatasetModal');
function getAugmentationOptions(contextSelector) {
const context = $(contextSelector);
const getVal = (selector, type = 'float') => type === 'int' ? parseInt(context.find(selector).val()) : parseFloat(context.find(selector).val());
const isEnabled = (selector) => context.find(selector).is(':checked');
return {
enabled: isEnabled('#aug-enabled'),
multiply_factor: getVal('#aug-multiply-factor', 'int'),
hflip: { enabled: isEnabled('#aug-hflip-enabled'), p: getVal('#aug-hflip-p') },
vflip: { enabled: isEnabled('#aug-vflip-enabled'), p: getVal('#aug-vflip-p') },
rotate90: { enabled: isEnabled('#aug-rotate90-enabled'), p: getVal('#aug-rotate90-p') },
rotate: { enabled: isEnabled('#aug-rotate-enabled'), p: getVal('#aug-rotate-p'), limit: getVal('#aug-rotate-limit', 'int') },
ssr: { enabled: isEnabled('#aug-ssr-enabled'), p: getVal('#aug-ssr-p'), rotate: getVal('#aug-ssr-rotate', 'int'), shift: getVal('#aug-ssr-shift'), scale: getVal('#aug-ssr-scale') },
affine: { enabled: isEnabled('#aug-affine-enabled'), p: getVal('#aug-affine-p'), shear: getVal('#aug-affine-shear', 'int') },
crop: { enabled: isEnabled('#aug-crop-enabled'), p: getVal('#aug-crop-p') },
grayscale: { enabled: isEnabled('#aug-grayscale-enabled'), p: getVal('#aug-grayscale-p') },
hsv: { enabled: isEnabled('#aug-hsv-enabled'), p: getVal('#aug-hsv-p'), h: getVal('#aug-hsv-h', 'int'), s: getVal('#aug-hsv-s', 'int'), v: getVal('#aug-hsv-v', 'int') },
bc: { enabled: isEnabled('#aug-bc-enabled'), p: getVal('#aug-bc-p'), b: getVal('#aug-bc-b'), c: getVal('#aug-bc-c') },
blur: { enabled: isEnabled('#aug-blur-enabled'), p: getVal('#aug-blur-p'), limit: getVal('#aug-blur-limit', 'int') },
noise: { enabled: isEnabled('#aug-noise-enabled'), p: getVal('#aug-noise-p'), limit: getVal('#aug-noise-limit') },
cutout: { enabled: isEnabled('#aug-cutout-enabled'), p: getVal('#aug-cutout-p'), holes: getVal('#aug-cutout-holes', 'int'), size: getVal('#aug-cutout-size', 'int') },
mosaic: { enabled: isEnabled('#aug-mosaic-enabled'), p: getVal('#aug-mosaic-p') }
};
}
$('#create-dataset-form').on('submit', function(e) {
e.preventDefault();
const augmentation_options = getAugmentationOptions('#createDatasetModal');
const datasetData = {
description: $('#dataset-description').val(),
video_uuids: $('#dataset-videos').val(),
eval_percent: $('#eval-percent').val(),
test_percent: $('#test-percent').val(),
augmentation_options: augmentation_options
};
$.ajax({
url: '/createDataset', type: 'POST', contentType: 'application/json',
data: JSON.stringify(datasetData),
success: function(response) {
if (response.success) {
showSuccess('Dataset creation started!');
$('#createDatasetModal').modal('hide');
refreshDatasets();
} else { showError(response.message); }
},
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
$('#upload-video-form').on('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
$.ajax({
url: '/uploadVideo', type: 'POST', data: formData, processData: false, contentType: false,
success: function(response) {
if (response.success) {
showSuccess('Upload successful! Processing started.');
$('#uploadVideoModal').modal('hide');
$(this).trigger('reset');
$('.custom-file-label').html('Choose file...');
refreshVideos();
} else { showError(response.message); }
}.bind(this),
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
$('#import-model-form').on('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
$.ajax({
url: '/importModel', type: 'POST', data: formData, processData: false, contentType: false,
success: function(response) {
if (response.success) {
showSuccess('Model imported!');
$('#importModelModal').modal('hide');
$(this).trigger('reset');
$('.custom-file-label').html('Choose file...');
refreshModels();
} else { showError(response.message); }
}.bind(this),
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
$('#settings-form').on('submit', function(e) {
e.preventDefault();
const selectedSamOption = $('#setting-sam-model').find('option:selected');
const classColors = {};
$('#class-color-manager input[type="color"]').each(function() {
classColors[$(this).data('class-name')] = $(this).val();
});
const settingsData = {
gpu_device: $('#setting-gpu-device').val(),
sam_model_checkpoint: selectedSamOption.val(),
sam_model_name: selectedSamOption.data('name'),
feature_extractor_model_name: $('#setting-feature-extractor-model').val(),
sam_mask_confidence: parseFloat($('#setting-sam-mask-confidence').val()),
nms_iou_threshold: parseFloat($('#setting-nms-iou-threshold').val()),
prototype_temperature: parseFloat($('#setting-prototype-temperature').val()),
prototype_sample_limit: parseInt($('#setting-prototype-sample-limit').val()),
batch_tracking_imgsz: parseInt($('#setting-batch-tracking-imgsz').val()),
batch_tracking_conf: parseFloat($('#setting-batch-tracking-conf') ? $('#setting-batch-tracking-conf').val() : 0.3),
batch_tracking_chunk_size: parseInt($('#setting-batch-tracking-chunk-size') ? $('#setting-batch-tracking-chunk-size').val() : 10),
default_annotation_mode: $('#setting-default-annotation-mode').val(),
autosave_enabled: $('#setting-autosave-enabled').is(':checked'),
default_preannotation_conf: parseFloat($('#setting-preannotation-conf').val() || 0.5),
frame_extraction_jpeg_quality: parseInt($('#setting-jpeg-quality').val()),
default_opencv_tracker: $('#setting-opencv-tracker').val(),
cache_save_interval_seconds: parseInt($('#setting-cache-save-interval').val()),
class_colors: classColors
};
$.ajax({
url: '/api/settings', type: 'POST', contentType: 'application/json',
data: JSON.stringify(settingsData),
success: function(response) {
if (response.success) {
alert(response.restart_required ? "Settings saved! AI model/device changes will apply to new tasks." : "Settings saved successfully!");
} else { showError(response.message); }
},
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
['sam-mask-confidence', 'nms-iou-threshold', 'prototype-temperature', 'batch-tracking-conf', 'preannotation-conf'].forEach(id => {
$(`#setting-${id}`).on('input', function() {
$(this).closest('.form-group').find('.text-primary').text(parseFloat($(this).val()).toFixed(2));
});
});
$('#clear-cache-btn').on('click', function() {
if (confirm("Clear 'Smart Select' temporary cache?")) {
const $btn = $(this);
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span> Clearing...');
$.ajax({
url: '/api/clear_cache', type: 'POST',
success: function(res) { alert(res.message); },
complete: function() { $btn.prop('disabled', false).html('<i class="bi bi-eraser-fill"></i> Clear Feature Cache'); }
});
}
});
$(document).on("click", ".btn-manage-tasks", function() {
const videoUuid = $(this).data("uuid");
const videoDesc = $(this).data("description");
const frameCount = $(this).data("framecount");
$('#manageTasksModalTitle').text('Tasks: ' + videoDesc);
$('#task-video-uuid').val(videoUuid);
$('#task-start-frame').attr('max', frameCount - 1);
$('#task-end-frame').attr('max', frameCount - 1);
refreshTasksInModal(videoUuid);
$('#manageTasksModal').modal('show');
});
function refreshTasksInModal(videoUuid) {
const taskListBody = $('#task-list-in-modal');
taskListBody.html('<tr><td colspan="4" class="text-center text-muted"><span class="spinner-border spinner-border-sm"></span> Loading...</td></tr>');
$.get(`/listTasks?video_uuid=${videoUuid}`, function(data) {
taskListBody.empty();
if (data.success && data.tasks.length > 0) {
data.tasks.forEach(task => {
const row = `
<tr>
<td class="align-middle font-weight-bold">${task.assigned_to}</td>
<td class="align-middle">${task.start_frame} - ${task.end_frame}</td>
<td class="align-middle text-muted small">${task.description || '-'}</td>
<td class="align-middle text-right">
<a href="/labelVideo?task_uuid=${task.task_uuid}" class="btn btn-sm btn-primary" title="Label"><i class="bi bi-pencil-square"></i></a>
<button class="btn btn-sm btn-outline-danger btn-delete-task" data-task-uuid="${task.task_uuid}" data-video-uuid="${videoUuid}"><i class="bi bi-trash"></i></button>
</td>
</tr>`;
taskListBody.append(row);
});
} else {
taskListBody.html('<tr><td colspan="4" class="text-center text-muted p-3">No tasks created yet.</td></tr>');
}
});
}
$('#create-task-form').on('submit', function(e) {
e.preventDefault();
const videoUuid = $('#task-video-uuid').val();
const taskData = {
video_uuid: videoUuid,
assigned_to: $('#task-assigned-to').val(),
description: $('#task-description').val(),
start_frame: $('#task-start-frame').val(),
end_frame: $('#task-end-frame').val(),
};
$.ajax({
url: '/createTask', type: 'POST', contentType: 'application/json', data: JSON.stringify(taskData),
success: function(response) {
if (response.success) {
$('#create-task-form')[0].reset();
refreshTasksInModal(videoUuid);
} else { showError(response.message); }
},
error: function() { showError('Error creating task.'); }
});
});
$(document).on("click", ".btn-pre-annotate", function() {
const videoUuid = $(this).data('uuid');
const frameCount = parseInt($(this).data('frame-count'));
const labeledCount = parseInt($(this).data('labeled-count'));
$('#pre-annotate-video-uuid').val(videoUuid).data('frame-count', frameCount).data('labeled-count', labeledCount);
const modelSelect = $('#pre-annotate-model-select');
modelSelect.html('<option>Loading models...</option>');
$('#pre-annotate-start-frame').val(0).attr('max', frameCount - 1);
$('#pre-annotate-end-frame').val(frameCount - 1).attr('max', frameCount - 1);
$.get('/listModels', function(data) {
modelSelect.empty();
if (data.models && data.models.length > 0) {
data.models.forEach(model => {
modelSelect.append(`<option value="${model.model_uuid}">${model.description} (${model.model_type})</option>`);
});
} else {
modelSelect.append('<option disabled>No models available. Import one first.</option>');
}
});
$('#preAnnotateModal').modal('show');
});
$('#pre-annotate-confidence').on('input', function() { $('#confidence-value').text($(this).val()); });
$('#pre-annotate-form').on('submit', function(e) {
e.preventDefault();
const videoUuid = $('#pre-annotate-video-uuid').val();
const modelUuid = $('#pre-annotate-model-select').val();
const labeledCount = parseInt($('#pre-annotate-video-uuid').data('labeled-count'));
if (!modelUuid) return showError("Please select a model.");
let msg = "Start auto-labeling?";
if (labeledCount > 0 && $('input[name="merge_strategy"]:checked').val() === 'overwrite') {
msg = `WARNING: This video has ${labeledCount} labels. This will OVERWRITE them in the selected range. Continue?`;
}
if (!confirm(msg)) return;
const options = {
start_frame: $('#pre-annotate-start-frame').val(),
end_frame: $('#pre-annotate-end-frame').val(),
confidence: $('#pre-annotate-confidence').val(),
merge_strategy: $('input[name="merge_strategy"]:checked').val()
};
$.ajax({
url: '/startPreAnnotation', type: 'POST', contentType: 'application/json',
data: JSON.stringify({ video_uuid: videoUuid, model_uuid: modelUuid, options: options }),
success: function(response) {
if (response.success) {
showSuccess('Auto-labeling started.');
$('#preAnnotateModal').modal('hide');
refreshVideos();
} else { showError(response.message); }
},
error: function(xhr) { showError(xhr.responseJSON ? xhr.responseJSON.message : 'Unknown error.'); }
});
});
let videoUuidForImport = null;
$(document).on("click", ".btn-import-frames", function() {
videoUuidForImport = $(this).data('uuid');
$('#frame-import-input').click();
});
$('#frame-import-input').on('change', function(e) {
if (!videoUuidForImport || e.target.files.length === 0) return;
const formData = new FormData(this);
formData.append('video_uuid', videoUuidForImport);
for (let i = 0; i < e.target.files.length; i++) formData.append('frame_files', e.target.files[i]);
alert(`Importing ${e.target.files.length} frames...`);
$.ajax({
url: '/importFrames', type: 'POST', data: formData, processData: false, contentType: false,
success: function(res) {
if(res.success) { showSuccess(`Imported ${res.imported_count} frames.`); refreshVideos(); }
else showError(res.message);
},
error: function() { showError('Import failed.'); },
complete: function() { videoUuidForImport = null; $('#frame-import-input').val(''); }
});
});
$(document).on("click", ".btn-delete-video", function() {
if (confirm('Delete this video and ALL labels?')) {
$.ajax({ url: '/deleteVideo', type: 'POST', contentType: 'application/json', data: JSON.stringify({ video_uuid: $(this).data('uuid') }), success: refreshVideos });
}
});
$(document).on("click", ".btn-delete-dataset", function() {
if (confirm('Delete this dataset?')) {
$.ajax({ url: '/deleteDataset', type: 'POST', contentType: 'application/json', data: JSON.stringify({ dataset_uuid: $(this).data('uuid') }), success: refreshDatasets });
}
});
$(document).on("click", ".btn-delete-model", function() {
if (confirm('Delete this model?')) {
$.ajax({ url: '/deleteModel', type: 'POST', contentType: 'application/json', data: JSON.stringify({ model_uuid: $(this).data('uuid') }), success: refreshModels });
}
});
$(document).on("click", ".btn-delete-task", function() {
const videoUuid = $(this).data('video-uuid');
if (confirm('Delete this task?')) {
$.ajax({ url: '/deleteTask', type: 'POST', contentType: 'application/json', data: JSON.stringify({ task_uuid: $(this).data('task-uuid') }), success: function() { refreshTasksInModal(videoUuid); } });
}
});
$(document).on("click", ".btn-regenerate-dataset", function() {
if (confirm('Update dataset? Old files will be replaced.')) {
$.ajax({ url: '/regenerateDataset', type: 'POST', contentType: 'application/json', data: JSON.stringify({ dataset_uuid: $(this).data('uuid') }), success: function(res) { if(res.success){ alert('Update started.'); refreshDatasets(); } else alert(res.message); } });
}
});
$(document).on("click", ".btn-cancel-task", function() {
if (confirm('Cancel running task?')) {
$.ajax({ url: '/cancelTask', type: 'POST', contentType: 'application/json', data: JSON.stringify({ video_uuid: $(this).data('uuid') }), success: function(res) { if(res.success){ alert('Cancellation requested.'); refreshVideos(); } else alert(res.message); } });
}
});
loadSettings();
function runAllRefreshes() { refreshVideos(); refreshDatasets(); refreshModels(); }
runAllRefreshes();
setInterval(runAllRefreshes, 5000);
});
</script>
{% endblock %}