629 lines
27 KiB
HTML
629 lines
27 KiB
HTML
|
|
|
|||
|
|
{% extends "app/base.html" %}
|
|||
|
|
{% load static %}
|
|||
|
|
|
|||
|
|
{% block title %}{{ T.sm_test_page|default:'小模型测试' }} - {{ settings.name|default:"Monitor" }}{% endblock %}
|
|||
|
|
{% block nav_small_models %}active{% endblock %}
|
|||
|
|
{% block page_title %}{{ T.sm_test_page|default:'小模型测试' }}{% endblock %}
|
|||
|
|
|
|||
|
|
{% block extra_head %}
|
|||
|
|
<style>
|
|||
|
|
.test-wrap{max-width:1200px;margin:0 auto;padding-bottom:20px;}
|
|||
|
|
.test-top{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:14px;flex-wrap:wrap;}
|
|||
|
|
.test-top h2{margin:0;font-size:16px;font-weight:700;color:#1e293b;display:flex;align-items:center;gap:8px;}
|
|||
|
|
.test-top h2 .badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:12px;background:#ecfdf5;color:#065f46;border:1px solid #d1fae5;}
|
|||
|
|
.test-back{display:inline-flex;align-items:center;gap:4px;padding:6px 12px;font-size:12px;border-radius:6px;border:1px solid #e2e8f0;background:#fff;color:#64748b;text-decoration:none;}
|
|||
|
|
.test-top-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
|
|||
|
|
.btn-clear{padding:6px 12px;font-size:12px;border-radius:6px;border:1px solid #fecaca;background:#fff;color:#dc2626;cursor:pointer;}
|
|||
|
|
.btn-clear:hover{background:#fef2f2;border-color:#f87171;}
|
|||
|
|
|
|||
|
|
.test-grid{display:grid;grid-template-columns:300px 1fr;gap:14px;align-items:start;}
|
|||
|
|
@media(max-width:900px){.test-grid{grid-template-columns:1fr;}}
|
|||
|
|
|
|||
|
|
.panel{background:#fff;border-radius:10px;border:1px solid #eef2f6;box-shadow:0 1px 8px rgba(15,23,42,.04);overflow:hidden;}
|
|||
|
|
.panel-head{padding:10px 14px;background:#fafbfc;border-bottom:1px solid #f1f5f9;}
|
|||
|
|
.panel-head h3{margin:0;font-size:13px;font-weight:600;color:#475569;}
|
|||
|
|
.panel-body{padding:12px 14px;}
|
|||
|
|
|
|||
|
|
.param-list{display:flex;flex-direction:column;gap:6px;}
|
|||
|
|
.param-item{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;padding:7px 10px;background:#f8fafc;border-radius:6px;font-size:11px;}
|
|||
|
|
.param-item .k{color:#94a3b8;font-weight:600;flex-shrink:0;}
|
|||
|
|
.param-item .v{color:#334155;text-align:right;word-break:break-all;font-family:Consolas,Monaco,monospace;font-size:10px;}
|
|||
|
|
.param-item .v.tags{display:flex;flex-wrap:wrap;gap:3px;justify-content:flex-end;}
|
|||
|
|
.param-tag{font-size:9px;padding:1px 6px;border-radius:8px;background:#e0f2fe;color:#0369a1;font-family:inherit;}
|
|||
|
|
|
|||
|
|
.work-zone{padding:14px;}
|
|||
|
|
|
|||
|
|
/* 紧凑上传栏 */
|
|||
|
|
.upload-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;background:#f8fafc;border:1px dashed #dbeafe;border-radius:8px;cursor:pointer;transition:.15s;}
|
|||
|
|
.upload-bar:hover,.upload-bar.drag{border-color:#169F85;background:#f0fdf9;}
|
|||
|
|
.upload-bar input{display:none;}
|
|||
|
|
.btn-upload{flex-shrink:0;padding:6px 14px;font-size:12px;font-weight:600;border-radius:6px;border:1px solid #169F85;background:#fff;color:#169F85;cursor:pointer;pointer-events:none;}
|
|||
|
|
.upload-bar:hover .btn-upload{background:#169F85;color:#fff;}
|
|||
|
|
.upload-meta{flex:1;min-width:120px;}
|
|||
|
|
.upload-meta .name{font-size:12px;color:#334155;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:320px;}
|
|||
|
|
.upload-meta .hint{font-size:11px;color:#94a3b8;margin-top:2px;}
|
|||
|
|
|
|||
|
|
/* 原图 / 渲染对比 */
|
|||
|
|
.media-compare{display:none;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px;}
|
|||
|
|
.media-compare.show{display:grid;}
|
|||
|
|
@media(max-width:640px){.media-compare{grid-template-columns:1fr;}}
|
|||
|
|
.media-pane{border:1px solid #eef2f6;border-radius:8px;overflow:hidden;background:#fff;}
|
|||
|
|
.media-pane-head{padding:6px 10px;font-size:11px;font-weight:600;color:#64748b;background:#fafbfc;border-bottom:1px solid #f1f5f9;}
|
|||
|
|
.media-pane-body{height:200px;display:flex;align-items:center;justify-content:center;background:#f1f5f9;overflow:hidden;position:relative;}
|
|||
|
|
.media-pane-body img,.media-pane-body video{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;display:block;}
|
|||
|
|
.media-pane-body img{cursor:zoom-in;}
|
|||
|
|
.media-pane-body video{cursor:zoom-in;}
|
|||
|
|
.media-pane-body .placeholder{font-size:11px;color:#94a3b8;text-align:center;padding:12px;}
|
|||
|
|
.media-zoom-hint{position:absolute;right:8px;bottom:6px;font-size:10px;color:#94a3b8;background:rgba(255,255,255,.85);padding:2px 6px;border-radius:4px;pointer-events:none;opacity:0;transition:opacity .15s;}
|
|||
|
|
.media-pane-body:hover .media-zoom-hint{opacity:1;}
|
|||
|
|
|
|||
|
|
.media-lightbox{display:none;position:fixed;inset:0;z-index:10050;background:rgba(15,23,42,.82);align-items:center;justify-content:center;padding:24px;}
|
|||
|
|
.media-lightbox.show{display:flex;}
|
|||
|
|
.media-lightbox-inner{position:relative;max-width:min(96vw,1400px);max-height:92vh;display:flex;flex-direction:column;align-items:center;}
|
|||
|
|
.media-lightbox-close{position:absolute;top:-36px;right:0;width:32px;height:32px;border:none;border-radius:50%;background:rgba(255,255,255,.15);color:#fff;font-size:22px;line-height:1;cursor:pointer;}
|
|||
|
|
.media-lightbox-close:hover{background:rgba(255,255,255,.28);}
|
|||
|
|
.media-lightbox-title{margin-bottom:8px;font-size:12px;color:#e2e8f0;text-align:center;}
|
|||
|
|
.media-lightbox-body{display:flex;align-items:center;justify-content:center;max-width:96vw;max-height:calc(92vh - 32px);overflow:auto;}
|
|||
|
|
.media-lightbox-body img,.media-lightbox-body video{max-width:96vw;max-height:calc(92vh - 32px);width:auto;height:auto;object-fit:contain;display:block;border-radius:6px;box-shadow:0 8px 32px rgba(0,0,0,.35);}
|
|||
|
|
|
|||
|
|
.run-bar{display:flex;align-items:center;gap:8px;margin-top:12px;flex-wrap:wrap;}
|
|||
|
|
.btn-run{padding:7px 18px;font-size:12px;font-weight:600;border:none;border-radius:6px;cursor:pointer;background:#169F85;color:#fff;transition:.15s;}
|
|||
|
|
.btn-run:hover:not(:disabled){background:#12836d;}
|
|||
|
|
.btn-run:disabled{opacity:.5;cursor:not-allowed;}
|
|||
|
|
.btn-ghost{padding:7px 14px;font-size:12px;border-radius:6px;border:1px solid #e2e8f0;background:#fff;color:#64748b;cursor:pointer;}
|
|||
|
|
.btn-ghost:hover{border-color:#169F85;color:#169F85;}
|
|||
|
|
.btn-ghost:disabled{opacity:.5;cursor:not-allowed;}
|
|||
|
|
|
|||
|
|
.progress-wrap{margin-top:10px;display:none;}
|
|||
|
|
.progress-wrap.show{display:block;}
|
|||
|
|
.progress-label{display:flex;justify-content:space-between;font-size:11px;color:#64748b;margin-bottom:4px;}
|
|||
|
|
.progress-track{height:4px;background:#e2e8f0;border-radius:99px;overflow:hidden;}
|
|||
|
|
.progress-fill{height:100%;width:0;border-radius:99px;background:#169F85;transition:width .25s ease;}
|
|||
|
|
.progress-msg{font-size:10px;color:#94a3b8;margin-top:4px;}
|
|||
|
|
|
|||
|
|
.result-section{margin-top:14px;display:none;}
|
|||
|
|
.result-section.show{display:block;}
|
|||
|
|
.result-card{border:1px solid #eef2f6;border-radius:8px;overflow:hidden;background:#fff;}
|
|||
|
|
.result-card-head{padding:8px 12px;background:#fafbfc;border-bottom:1px solid #f1f5f9;font-size:11px;font-weight:600;color:#475569;}
|
|||
|
|
.result-card-body{padding:10px 12px;}
|
|||
|
|
.report-table{width:100%;border-collapse:collapse;font-size:11px;}
|
|||
|
|
.report-table th,.report-table td{padding:5px 8px;text-align:left;border-bottom:1px solid #f8fafc;}
|
|||
|
|
.report-table th{color:#94a3b8;font-weight:600;width:38%;}
|
|||
|
|
.report-table td{color:#334155;font-family:Consolas,Monaco,monospace;font-size:10px;}
|
|||
|
|
.summary-chips{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;}
|
|||
|
|
.summary-chip{font-size:10px;padding:2px 8px;border-radius:10px;background:#ecfdf5;color:#065f46;border:1px solid #d1fae5;}
|
|||
|
|
|
|||
|
|
.state-running .upload-bar{pointer-events:none;opacity:.65;}
|
|||
|
|
</style>
|
|||
|
|
{% endblock %}
|
|||
|
|
|
|||
|
|
{% block content %}
|
|||
|
|
<div class="test-wrap state-idle" id="testWrap">
|
|||
|
|
<div class="test-top">
|
|||
|
|
<h2>
|
|||
|
|
<span>{{ T.sm_test_page|default:'小模型测试' }}</span>
|
|||
|
|
<span class="badge" id="algoBadge">#</span>
|
|||
|
|
</h2>
|
|||
|
|
<div class="test-top-actions">
|
|||
|
|
<button type="button" class="btn-clear" onclick="clearTempFiles()">{{ T.sm_test_clear_temp|default:'清理测试临时文件' }}</button>
|
|||
|
|
<a href="/smallmodel/index" class="test-back">← {{ T.sm_back_list|default:'返回小模型列表' }}</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="test-grid">
|
|||
|
|
<div class="panel">
|
|||
|
|
<div class="panel-head"><h3>{{ T.sm_test_params|default:'小模型参数' }}</h3></div>
|
|||
|
|
<div class="panel-body">
|
|||
|
|
<div class="param-list" id="paramList">
|
|||
|
|
<div style="text-align:center;color:#94a3b8;font-size:12px;padding:16px">{{ T.index_loading|default:'加载中...' }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="panel">
|
|||
|
|
<div class="panel-head"><h3>{{ T.sm_test_workspace|default:'小模型测试工作台' }}</h3></div>
|
|||
|
|
<div class="work-zone">
|
|||
|
|
<div id="reidDetectorPanel" style="display:none;margin-bottom:12px;padding:10px 12px;background:#f0f9ff;border:1px solid #bfdbfe;border-radius:8px;">
|
|||
|
|
<label style="display:block;font-size:12px;font-weight:600;color:#1e40af;margin-bottom:6px;">{{ T.sm_reid_detector|default:'辅助检测小模型 (YOLO)' }} *</label>
|
|||
|
|
<select id="detectorAlgoId" style="width:100%;padding:8px 10px;font-size:13px;border:1px solid #bfdbfe;border-radius:6px;" onchange="onDetectorChange()"></select>
|
|||
|
|
<div style="font-size:11px;color:#64748b;margin-top:6px;">{{ T.sm_reid_detector_hint|default:'ReID 需先用检测模型定位行人,再提取特征并关联 track_id' }}</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="upload-bar" id="dropArea">
|
|||
|
|
<input type="file" id="fileInput" accept="image/*,video/*,.mp4,.avi,.mov,.mkv,.webm">
|
|||
|
|
<span class="btn-upload">{{ T.sm_test_pick_file|default:'选择测试文件' }}</span>
|
|||
|
|
<div class="upload-meta">
|
|||
|
|
<div class="name" id="pickedName">{{ T.sm_test_upload_hint|default:'JPG / PNG / MP4,最大 100MB' }}</div>
|
|||
|
|
<div class="hint" id="pickedHint">{{ T.sm_test_upload_sub|default:'点击或拖拽到此处' }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="media-compare" id="mediaCompare">
|
|||
|
|
<div class="media-pane">
|
|||
|
|
<div class="media-pane-head">{{ T.sm_test_input_preview|default:'原始样本' }}</div>
|
|||
|
|
<div class="media-pane-body" id="inputPane">
|
|||
|
|
<div class="placeholder" id="inputEmpty">{{ T.sm_test_preview_empty|default:'选择文件后可预览' }}</div>
|
|||
|
|
<img id="previewImg" style="display:none" alt="preview">
|
|||
|
|
<video id="previewVideo" style="display:none" controls muted playsinline></video>
|
|||
|
|
<span class="media-zoom-hint">{{ T.sm_test_click_zoom|default:'点击放大' }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="media-pane">
|
|||
|
|
<div class="media-pane-head">{{ T.sm_test_output|default:'渲染结果' }}</div>
|
|||
|
|
<div class="media-pane-body" id="outputPane">
|
|||
|
|
<div class="placeholder" id="outputEmpty">{{ T.sm_test_output_wait|default:'小模型测试完成后显示' }}</div>
|
|||
|
|
<span class="media-zoom-hint">{{ T.sm_test_click_zoom|default:'点击放大' }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="run-bar">
|
|||
|
|
<button type="button" class="btn-run" id="btnRun" disabled onclick="startTest()">{{ T.sm_test_run|default:'开始小模型测试' }}</button>
|
|||
|
|
<button type="button" class="btn-ghost" id="btnReset" onclick="resetTest()">{{ T.sm_test_reset|default:'重置' }}</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="progress-wrap" id="progressWrap">
|
|||
|
|
<div class="progress-label">
|
|||
|
|
<span id="progressLabel">{{ T.sm_test_running|default:'小模型测试中...' }}</span>
|
|||
|
|
<span id="progressPct">0%</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
|
|||
|
|
<div class="progress-msg" id="progressMsg"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="result-section" id="resultSection">
|
|||
|
|
<div class="result-card">
|
|||
|
|
<div class="result-card-head">{{ T.sm_test_report|default:'小模型测试报告' }}</div>
|
|||
|
|
<div class="result-card-body" id="reportBody"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="media-lightbox" id="mediaLightbox" onclick="closeMediaLightbox(event)">
|
|||
|
|
<div class="media-lightbox-inner" onclick="event.stopPropagation()">
|
|||
|
|
<button type="button" class="media-lightbox-close" onclick="closeMediaLightbox()" aria-label="close">×</button>
|
|||
|
|
<div class="media-lightbox-title" id="lightboxTitle"></div>
|
|||
|
|
<div class="media-lightbox-body" id="lightboxBody"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
{% endblock %}
|
|||
|
|
|
|||
|
|
{% block extra_js %}
|
|||
|
|
<script>
|
|||
|
|
var g_algoId = 0;
|
|||
|
|
var g_algo = null;
|
|||
|
|
var g_file = null;
|
|||
|
|
var g_pollTimer = null;
|
|||
|
|
var g_taskId = '';
|
|||
|
|
var g_previewUrl = '';
|
|||
|
|
var g_detectorId = 0;
|
|||
|
|
var g_detectors = [];
|
|||
|
|
|
|||
|
|
function isReidAlgo(){
|
|||
|
|
return g_algo && (g_algo.task_type || '').toLowerCase() === 'reid';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateRunButtonState(){
|
|||
|
|
var needDetector = isReidAlgo();
|
|||
|
|
var detectorOk = !needDetector || g_detectorId > 0;
|
|||
|
|
document.getElementById('btnRun').disabled = !g_file || !g_algo || !g_algo.model_file || !detectorOk;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function typeLabel(t){
|
|||
|
|
return ({yolo5:'YOLOv5',yolo8:'YOLOv8',yolo11:'YOLOv11',yolo26:'YOLO26',osnet:'OSNet ReID'})[t] || t || '-';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function qsId(){
|
|||
|
|
var m = /[?&]id=(\d+)/.exec(window.location.search);
|
|||
|
|
return m ? parseInt(m[1], 10) : 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function engineLabel(name){
|
|||
|
|
if(name==='yolo_pytorch') return 'Yolo-PyTorch';
|
|||
|
|
if(name==='onnxruntime') return 'OnnxRuntime';
|
|||
|
|
if(name==='openvino') return 'OpenVINO';
|
|||
|
|
return name || '-';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderParams(a){
|
|||
|
|
var labels = a.labels || [];
|
|||
|
|
var labelsHtml = '';
|
|||
|
|
if(labels.length){
|
|||
|
|
labelsHtml = '<div class="v tags">' + labels.slice(0,8).map(function(l){
|
|||
|
|
return '<span class="param-tag">'+escapeHtml(l)+'</span>';
|
|||
|
|
}).join('') + (labels.length>8?' <span class="param-tag">+'+(labels.length-8)+'</span>':'') + '</div>';
|
|||
|
|
} else {
|
|||
|
|
labelsHtml = '<span class="v">-</span>';
|
|||
|
|
}
|
|||
|
|
var rows = [
|
|||
|
|
[_t('sm_name','小模型名称'), escapeHtml(a.name)],
|
|||
|
|
[_t('sm_type','模型类型'), typeLabel(a.algorithm_type)],
|
|||
|
|
[_t('sm_task_type','任务类型'), (a.task_type||'detect').toUpperCase()],
|
|||
|
|
[_t('sm_engine','推理引擎'), engineLabel(a.inference_engine)],
|
|||
|
|
[_t('sm_device','推理设备'), (a.device||'cpu').toUpperCase()],
|
|||
|
|
[_t('sm_resolution','分辨率'), (a.input_width||640)+'×'+(a.input_height||640)],
|
|||
|
|
];
|
|||
|
|
if((a.task_type||'detect').toLowerCase() !== 'reid'){
|
|||
|
|
rows.push([_t('sm_conf_threshold','置信度'), String(a.conf_threshold)]);
|
|||
|
|
rows.push([_t('sm_labels','类别'), labelsHtml, true]);
|
|||
|
|
}
|
|||
|
|
rows.push([_t('sm_model_file','模型'), escapeHtml(a.model_file||'-')]);
|
|||
|
|
var html = '';
|
|||
|
|
rows.forEach(function(r){
|
|||
|
|
if(r[2]) html += '<div class="param-item"><span class="k">'+r[0]+'</span>'+r[1]+'</div>';
|
|||
|
|
else html += '<div class="param-item"><span class="k">'+r[0]+'</span><span class="v">'+r[1]+'</span></div>';
|
|||
|
|
});
|
|||
|
|
document.getElementById('paramList').innerHTML = html;
|
|||
|
|
document.getElementById('algoBadge').textContent = '#'+a.id+' '+a.name;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderDetectorSelect(){
|
|||
|
|
var panel = document.getElementById('reidDetectorPanel');
|
|||
|
|
var sel = document.getElementById('detectorAlgoId');
|
|||
|
|
if(!panel || !sel) return;
|
|||
|
|
if(!isReidAlgo()){
|
|||
|
|
panel.style.display = 'none';
|
|||
|
|
g_detectorId = 0;
|
|||
|
|
updateRunButtonState();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
panel.style.display = 'block';
|
|||
|
|
sel.innerHTML = '';
|
|||
|
|
if(!g_detectors.length){
|
|||
|
|
var o = document.createElement('option');
|
|||
|
|
o.value = '';
|
|||
|
|
o.textContent = _t('sm_reid_no_detector','暂无可用检测小模型,请先在列表中添加 YOLO Detect 模型');
|
|||
|
|
sel.appendChild(o);
|
|||
|
|
g_detectorId = 0;
|
|||
|
|
} else {
|
|||
|
|
var picked = null;
|
|||
|
|
g_detectors.forEach(function(d){
|
|||
|
|
var o = document.createElement('option');
|
|||
|
|
o.value = d.id;
|
|||
|
|
var eng = engineLabel(d.inference_engine);
|
|||
|
|
o.textContent = '#'+d.id+' '+d.name+' ('+eng+')';
|
|||
|
|
sel.appendChild(o);
|
|||
|
|
if(d.is_default === 1) picked = d.id;
|
|||
|
|
});
|
|||
|
|
g_detectorId = picked || g_detectors[0].id;
|
|||
|
|
sel.value = String(g_detectorId);
|
|||
|
|
}
|
|||
|
|
updateRunButtonState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onDetectorChange(){
|
|||
|
|
var sel = document.getElementById('detectorAlgoId');
|
|||
|
|
g_detectorId = sel && sel.value ? parseInt(sel.value, 10) : 0;
|
|||
|
|
updateRunButtonState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function loadDetectors(cb){
|
|||
|
|
Api.get('/smallmodel/openDetectors').then(function(res){
|
|||
|
|
g_detectors = (res && res.code===1000 && res.data) ? res.data : [];
|
|||
|
|
renderDetectorSelect();
|
|||
|
|
if(cb) cb();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function loadAlgorithm(){
|
|||
|
|
g_algoId = qsId();
|
|||
|
|
if(!g_algoId){
|
|||
|
|
showToast(_t('sm_test_no_id','缺少小模型 ID'), 'error');
|
|||
|
|
setTimeout(function(){ window.location.href='/smallmodel/index'; }, 1200);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
Api.get('/smallmodel/openDetail', {id: g_algoId}).then(function(res){
|
|||
|
|
if(!res || res.code!==1000 || !res.data || !res.data.id){
|
|||
|
|
showToast(res?res.msg:_t('load_failed','加载失败'), 'error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
g_algo = res.data;
|
|||
|
|
renderParams(g_algo);
|
|||
|
|
if(isReidAlgo()){
|
|||
|
|
loadDetectors();
|
|||
|
|
} else {
|
|||
|
|
renderDetectorSelect();
|
|||
|
|
}
|
|||
|
|
if(!g_algo.model_file){
|
|||
|
|
showToast(_t('sm_no_model_file','请先配置模型文件'), 'warning');
|
|||
|
|
}
|
|||
|
|
updateRunButtonState();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isVideoFile(f){
|
|||
|
|
return f && (/^video\//.test(f.type) || /\.(mp4|avi|mov|mkv|webm|m4v)$/i.test(f.name));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isImageFile(f){
|
|||
|
|
return f && (/^image\//.test(f.type) || /\.(jpg|jpeg|png|bmp|webp)$/i.test(f.name));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function revokePreviewUrl(){
|
|||
|
|
if(g_previewUrl){ URL.revokeObjectURL(g_previewUrl); g_previewUrl = ''; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setFile(f){
|
|||
|
|
if(!f) return;
|
|||
|
|
if(!isVideoFile(f) && !isImageFile(f)){
|
|||
|
|
showToast(_t('sm_test_bad_type','不支持的文件类型'), 'warning');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
g_file = f;
|
|||
|
|
document.getElementById('pickedName').textContent = f.name;
|
|||
|
|
document.getElementById('pickedHint').textContent = formatBytes(f.size);
|
|||
|
|
updateRunButtonState();
|
|||
|
|
|
|||
|
|
var img = document.getElementById('previewImg');
|
|||
|
|
var vid = document.getElementById('previewVideo');
|
|||
|
|
var inputEmpty = document.getElementById('inputEmpty');
|
|||
|
|
revokePreviewUrl();
|
|||
|
|
g_previewUrl = URL.createObjectURL(f);
|
|||
|
|
img.style.display = 'none';
|
|||
|
|
vid.style.display = 'none';
|
|||
|
|
inputEmpty.style.display = 'none';
|
|||
|
|
if(isVideoFile(f)){
|
|||
|
|
vid.src = g_previewUrl;
|
|||
|
|
vid.style.display = 'block';
|
|||
|
|
} else {
|
|||
|
|
img.src = g_previewUrl;
|
|||
|
|
img.style.display = 'block';
|
|||
|
|
}
|
|||
|
|
document.getElementById('mediaCompare').classList.add('show');
|
|||
|
|
clearOutputPane(true);
|
|||
|
|
document.getElementById('resultSection').classList.remove('show');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearOutputPane(showPlaceholder){
|
|||
|
|
var pane = document.getElementById('outputPane');
|
|||
|
|
var empty = document.getElementById('outputEmpty');
|
|||
|
|
pane.querySelectorAll('img,video').forEach(function(el){ el.remove(); });
|
|||
|
|
if(showPlaceholder){
|
|||
|
|
empty.style.display = 'block';
|
|||
|
|
empty.textContent = _t('sm_test_output_wait','小模型测试完成后显示');
|
|||
|
|
empty.style.color = '';
|
|||
|
|
if(!pane.contains(empty)) pane.appendChild(empty);
|
|||
|
|
} else {
|
|||
|
|
empty.style.display = 'none';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openMediaLightbox(type, src, title){
|
|||
|
|
if(!src) return;
|
|||
|
|
var box = document.getElementById('mediaLightbox');
|
|||
|
|
var body = document.getElementById('lightboxBody');
|
|||
|
|
var titleEl = document.getElementById('lightboxTitle');
|
|||
|
|
body.innerHTML = '';
|
|||
|
|
titleEl.textContent = title || '';
|
|||
|
|
if(type === 'video'){
|
|||
|
|
var vid = document.createElement('video');
|
|||
|
|
vid.src = src;
|
|||
|
|
vid.controls = true;
|
|||
|
|
vid.autoplay = true;
|
|||
|
|
vid.playsInline = true;
|
|||
|
|
body.appendChild(vid);
|
|||
|
|
} else {
|
|||
|
|
var img = document.createElement('img');
|
|||
|
|
img.src = src;
|
|||
|
|
img.alt = title || 'preview';
|
|||
|
|
body.appendChild(img);
|
|||
|
|
}
|
|||
|
|
box.classList.add('show');
|
|||
|
|
document.body.style.overflow = 'hidden';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeMediaLightbox(e){
|
|||
|
|
if(e && e.target && e.target.id !== 'mediaLightbox' && !e.target.classList.contains('media-lightbox-close')) return;
|
|||
|
|
var box = document.getElementById('mediaLightbox');
|
|||
|
|
var body = document.getElementById('lightboxBody');
|
|||
|
|
box.classList.remove('show');
|
|||
|
|
body.innerHTML = '';
|
|||
|
|
document.body.style.overflow = '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function initMediaZoom(){
|
|||
|
|
function onPaneClick(e){
|
|||
|
|
var t = e.target;
|
|||
|
|
if(t.tagName === 'IMG' && t.src){
|
|||
|
|
e.preventDefault();
|
|||
|
|
e.stopPropagation();
|
|||
|
|
var title = t.closest('.media-pane') ? t.closest('.media-pane').querySelector('.media-pane-head').textContent : '';
|
|||
|
|
openMediaLightbox('image', t.src, title);
|
|||
|
|
} else if(t.tagName === 'VIDEO' && t.src){
|
|||
|
|
e.preventDefault();
|
|||
|
|
e.stopPropagation();
|
|||
|
|
var titleV = t.closest('.media-pane') ? t.closest('.media-pane').querySelector('.media-pane-head').textContent : '';
|
|||
|
|
openMediaLightbox('video', t.src, titleV);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
document.getElementById('inputPane').addEventListener('click', onPaneClick);
|
|||
|
|
document.getElementById('outputPane').addEventListener('click', onPaneClick);
|
|||
|
|
document.addEventListener('keydown', function(e){
|
|||
|
|
if(e.key === 'Escape') closeMediaLightbox({target: document.getElementById('mediaLightbox')});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatBytes(n){
|
|||
|
|
if(!n) return '0 B';
|
|||
|
|
if(n<1024*1024) return (n/1024).toFixed(1)+' KB';
|
|||
|
|
return (n/1024/1024).toFixed(2)+' MB';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
(function initDrop(){
|
|||
|
|
var zone = document.getElementById('dropArea');
|
|||
|
|
var input = document.getElementById('fileInput');
|
|||
|
|
zone.addEventListener('click', function(e){
|
|||
|
|
if(document.getElementById('testWrap').classList.contains('state-running')) return;
|
|||
|
|
if(e.target.tagName==='VIDEO') return;
|
|||
|
|
input.click();
|
|||
|
|
});
|
|||
|
|
input.addEventListener('change', function(e){ if(e.target.files[0]) setFile(e.target.files[0]); });
|
|||
|
|
zone.addEventListener('dragover', function(e){ e.preventDefault(); zone.classList.add('drag'); });
|
|||
|
|
zone.addEventListener('dragleave', function(){ zone.classList.remove('drag'); });
|
|||
|
|
zone.addEventListener('drop', function(e){
|
|||
|
|
e.preventDefault(); zone.classList.remove('drag');
|
|||
|
|
if(e.dataTransfer.files[0]) setFile(e.dataTransfer.files[0]);
|
|||
|
|
});
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
function setRunning(running){
|
|||
|
|
document.getElementById('testWrap').classList.toggle('state-running', running);
|
|||
|
|
updateRunButtonState();
|
|||
|
|
if(running) document.getElementById('btnRun').disabled = true;
|
|||
|
|
document.getElementById('btnReset').disabled = running;
|
|||
|
|
document.getElementById('progressWrap').classList.toggle('show', running);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateProgress(pct, msg){
|
|||
|
|
document.getElementById('progressFill').style.width = Math.min(100, pct)+'%';
|
|||
|
|
document.getElementById('progressPct').textContent = Math.round(pct)+'%';
|
|||
|
|
document.getElementById('progressMsg').textContent = msg || '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function startTest(){
|
|||
|
|
if(!g_file || !g_algoId) return;
|
|||
|
|
if(g_pollTimer){ clearInterval(g_pollTimer); g_pollTimer = null; }
|
|||
|
|
document.getElementById('resultSection').classList.remove('show');
|
|||
|
|
clearOutputPane(true);
|
|||
|
|
setRunning(true);
|
|||
|
|
updateProgress(2, _t('sm_test_uploading','上传样本...'));
|
|||
|
|
var fd = new FormData();
|
|||
|
|
fd.append('algorithm_id', g_algoId);
|
|||
|
|
fd.append('file', g_file);
|
|||
|
|
if(isReidAlgo() && g_detectorId){
|
|||
|
|
fd.append('detector_algorithm_id', g_detectorId);
|
|||
|
|
}
|
|||
|
|
Api.postForm('/smallmodel/openTestStart', fd).then(function(res){
|
|||
|
|
if(!res || res.code!==1000 || !res.data || !res.data.task_id){
|
|||
|
|
setRunning(false);
|
|||
|
|
showToast(res?res.msg:_t('msg_unknown_error','失败'), 'error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
g_taskId = res.data.task_id;
|
|||
|
|
updateProgress(8, _t('sm_test_queued','任务已排队...'));
|
|||
|
|
g_pollTimer = setInterval(pollStatus, 800);
|
|||
|
|
pollStatus();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function pollStatus(){
|
|||
|
|
if(!g_taskId) return;
|
|||
|
|
Api.get('/smallmodel/openTestStatus', {task_id: g_taskId}).then(function(res){
|
|||
|
|
if(!res || res.code!==1000 || !res.data) return;
|
|||
|
|
var d = res.data;
|
|||
|
|
updateProgress(d.progress||0, d.message||'');
|
|||
|
|
if(d.status==='done'){
|
|||
|
|
clearInterval(g_pollTimer); g_pollTimer = null;
|
|||
|
|
setRunning(false);
|
|||
|
|
showResult(d);
|
|||
|
|
showToast(_t('sm_test_done','小模型测试完成'), 'success');
|
|||
|
|
} else if(d.status==='error'){
|
|||
|
|
clearInterval(g_pollTimer); g_pollTimer = null;
|
|||
|
|
setRunning(false);
|
|||
|
|
var errMsg = d.error || d.message || _t('msg_unknown_error','失败');
|
|||
|
|
showToast(errMsg, 'error');
|
|||
|
|
clearOutputPane(true);
|
|||
|
|
document.getElementById('outputEmpty').textContent = errMsg;
|
|||
|
|
document.getElementById('outputEmpty').style.color = '#f87171';
|
|||
|
|
document.getElementById('resultSection').classList.remove('show');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showResult(d){
|
|||
|
|
var rep = d.report || {};
|
|||
|
|
var html = '<table class="report-table">';
|
|||
|
|
html += row(_t('sm_test_media','媒体类型'), rep.media_type||'-');
|
|||
|
|
html += row(_t('sm_test_input_size','输入尺寸'), (rep.input_size||[]).join('×')||'-');
|
|||
|
|
if(rep.media_type==='video'){
|
|||
|
|
html += row(_t('sm_test_frames','处理帧数'), (rep.processed_frames||0)+'/'+(rep.frame_count||'?'));
|
|||
|
|
html += row('FPS', rep.fps||'-');
|
|||
|
|
}
|
|||
|
|
html += row(_t('sm_test_det_count','检测总数'), rep.detection_count||0);
|
|||
|
|
if(rep.task_type === 'reid'){
|
|||
|
|
html += row(_t('sm_test_embedding_count','特征提取数'), rep.embedding_count||0);
|
|||
|
|
html += row(_t('sm_test_track_count','轨迹峰值'), rep.track_count||0);
|
|||
|
|
if(rep.reid_sim_mean != null) html += row(_t('sm_test_reid_sim','特征时序相似度均值'), rep.reid_sim_mean);
|
|||
|
|
if(rep.detector_name) html += row(_t('sm_reid_detector','辅助检测小模型'), escapeHtml(rep.detector_name));
|
|||
|
|
}
|
|||
|
|
html += row(_t('sm_test_infer_avg','平均推理'), (rep.inference_ms_avg||0)+' ms');
|
|||
|
|
html += row(_t('sm_test_elapsed','总耗时'), (rep.elapsed_ms||0)+' ms');
|
|||
|
|
html += '</table>';
|
|||
|
|
var summary = rep.detections_summary || [];
|
|||
|
|
if(summary.length){
|
|||
|
|
html += '<div class="summary-chips">';
|
|||
|
|
summary.slice(0,12).forEach(function(s){
|
|||
|
|
html += '<span class="summary-chip">'+escapeHtml(s.label)+' ×'+s.count+'</span>';
|
|||
|
|
});
|
|||
|
|
html += '</div>';
|
|||
|
|
}
|
|||
|
|
document.getElementById('reportBody').innerHTML = html;
|
|||
|
|
|
|||
|
|
clearOutputPane(false);
|
|||
|
|
var pane = document.getElementById('outputPane');
|
|||
|
|
if(d.output_url){
|
|||
|
|
var src = d.output_url + (d.output_url.indexOf('?') >= 0 ? '&' : '?') + 't=' + Date.now();
|
|||
|
|
if(d.output_type === 'video'){
|
|||
|
|
var vid = document.createElement('video');
|
|||
|
|
vid.src = src;
|
|||
|
|
vid.controls = true;
|
|||
|
|
vid.muted = true;
|
|||
|
|
vid.playsInline = true;
|
|||
|
|
pane.appendChild(vid);
|
|||
|
|
} else {
|
|||
|
|
var img = document.createElement('img');
|
|||
|
|
img.alt = 'result';
|
|||
|
|
img.src = src;
|
|||
|
|
img.onerror = function(){
|
|||
|
|
clearOutputPane(true);
|
|||
|
|
document.getElementById('outputEmpty').textContent = _t('sm_test_output_load_fail','渲染结果加载失败');
|
|||
|
|
document.getElementById('outputEmpty').style.color = '#f87171';
|
|||
|
|
};
|
|||
|
|
pane.appendChild(img);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
clearOutputPane(true);
|
|||
|
|
}
|
|||
|
|
document.getElementById('resultSection').classList.add('show');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function row(k,v){
|
|||
|
|
return '<tr><th>'+k+'</th><td>'+v+'</td></tr>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resetTest(){
|
|||
|
|
if(g_pollTimer){ clearInterval(g_pollTimer); g_pollTimer = null; }
|
|||
|
|
g_taskId = '';
|
|||
|
|
g_file = null;
|
|||
|
|
revokePreviewUrl();
|
|||
|
|
document.getElementById('fileInput').value = '';
|
|||
|
|
document.getElementById('pickedName').textContent = _t('sm_test_upload_hint','JPG / PNG / MP4,最大 100MB');
|
|||
|
|
document.getElementById('pickedHint').textContent = _t('sm_test_upload_sub','点击或拖拽到此处');
|
|||
|
|
document.getElementById('previewImg').style.display = 'none';
|
|||
|
|
document.getElementById('previewVideo').style.display = 'none';
|
|||
|
|
document.getElementById('inputEmpty').style.display = 'block';
|
|||
|
|
document.getElementById('mediaCompare').classList.remove('show');
|
|||
|
|
clearOutputPane(true);
|
|||
|
|
document.getElementById('resultSection').classList.remove('show');
|
|||
|
|
document.getElementById('progressWrap').classList.remove('show');
|
|||
|
|
setRunning(false);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearTempFiles(){
|
|||
|
|
if(!confirm(_t('sm_test_clear_confirm','确认清理所有小模型测试临时文件?'))) return;
|
|||
|
|
Api.post('/smallmodel/openTestClearTemp', {}).then(function(res){
|
|||
|
|
showToast(res.msg, res.code===1000?'success':'error');
|
|||
|
|
if(res.code===1000){
|
|||
|
|
resetTest();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loadAlgorithm();
|
|||
|
|
initMediaZoom();
|
|||
|
|
</script>
|
|||
|
|
{% endblock %}
|