video_monitor/templates/app/smallmodel/test.html

629 lines
27 KiB
HTML
Raw Permalink Normal View History

2026-08-30 22:22:11 +08:00
{% 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">&times;</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 %}