Add guided detection calibration runner
This commit is contained in:
@@ -9,6 +9,7 @@ import type {
|
||||
QualityCheckRead,
|
||||
YoloPreflightResponse,
|
||||
} from '../../types'
|
||||
import type { DetectionCalibrationRunRow } from '../../hooks/useDetectionWorkflow'
|
||||
|
||||
interface CalibrationRow {
|
||||
analysisRunId: string
|
||||
@@ -42,6 +43,10 @@ interface DetectionLabProps {
|
||||
detectionRunError: string | null
|
||||
detectionRuns: DetectionRunRead[]
|
||||
qualityChecks: QualityCheckRead[]
|
||||
calibrationThresholdText: string
|
||||
runningDetectionCalibration: boolean
|
||||
detectionCalibrationRows: DetectionCalibrationRunRow[]
|
||||
detectionCalibrationError: string | null
|
||||
selectedDetectionRunId: string
|
||||
detectionItems: DetectionRead[]
|
||||
detectionClassFilter: string
|
||||
@@ -72,6 +77,8 @@ interface DetectionLabProps {
|
||||
onLoadResults: () => void
|
||||
onSelectReferenceDataset: (datasetId: string) => void
|
||||
onRunQa: () => void
|
||||
onSetCalibrationThresholdText: (value: string) => void
|
||||
onRunCalibration: () => void
|
||||
}
|
||||
|
||||
export function DetectionLab({
|
||||
@@ -90,6 +97,10 @@ export function DetectionLab({
|
||||
detectionRunError,
|
||||
detectionRuns,
|
||||
qualityChecks,
|
||||
calibrationThresholdText,
|
||||
runningDetectionCalibration,
|
||||
detectionCalibrationRows,
|
||||
detectionCalibrationError,
|
||||
selectedDetectionRunId,
|
||||
detectionItems,
|
||||
detectionClassFilter,
|
||||
@@ -120,6 +131,8 @@ export function DetectionLab({
|
||||
onLoadResults,
|
||||
onSelectReferenceDataset,
|
||||
onRunQa,
|
||||
onSetCalibrationThresholdText,
|
||||
onRunCalibration,
|
||||
}: DetectionLabProps): JSX.Element {
|
||||
const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null
|
||||
const selectedModelAsset = modelAssets.find((asset) => asset.model_asset_id === selectedModelAssetId) ?? null
|
||||
@@ -161,6 +174,7 @@ export function DetectionLab({
|
||||
: !detectionHasTileManifest
|
||||
? 'Provide a raster tile manifest for configured YOLO'
|
||||
: null
|
||||
const calibrationRunReady = detectionRunReady && detectionReferenceDatasetId.length > 0 && calibrationThresholdText.trim().length > 0
|
||||
|
||||
return (
|
||||
<section className="workspace-panel ai-lab-shell detection-lab-shell">
|
||||
@@ -501,6 +515,101 @@ export function DetectionLab({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="ai-lab-run-surface guided-calibration-surface" aria-label="Guided calibration runner">
|
||||
<div className="ai-lab-section-header">
|
||||
<div>
|
||||
<h3>Guided calibration runner</h3>
|
||||
<p>This runs real configured YOLO jobs and QA comparisons for each threshold. It does not promote or mutate model files.</p>
|
||||
</div>
|
||||
<span className={calibrationRunReady ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||
{calibrationRunReady ? 'ready' : 'needs dataset, model, manifest and reference'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="lab-form-grid">
|
||||
<label>
|
||||
Threshold set
|
||||
<input
|
||||
type="text"
|
||||
value={calibrationThresholdText}
|
||||
onChange={(event) => onSetCalibrationThresholdText(event.target.value)}
|
||||
placeholder="0.50 0.25 0.15"
|
||||
/>
|
||||
<span className="field-guidance">Use spaces, commas or semicolons. Values must be between 0 and 1.</span>
|
||||
</label>
|
||||
<label>
|
||||
Reference dataset
|
||||
<select value={detectionReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
|
||||
<option value="">Select reference dataset</option>
|
||||
{referenceDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
className="primary-action"
|
||||
type="button"
|
||||
onClick={onRunCalibration}
|
||||
disabled={runningDetectionCalibration || !calibrationRunReady}
|
||||
>
|
||||
Run calibration sweep
|
||||
</button>
|
||||
{detectionCalibrationError ? (
|
||||
<div className="result-state result-state-error">
|
||||
<strong>Calibration sweep failed.</strong>
|
||||
<p>{detectionCalibrationError}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{detectionCalibrationRows.length > 0 ? (
|
||||
<div className="calibration-progress-panel" aria-label="Calibration run progress">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<h3>Calibration run progress</h3>
|
||||
<p className="muted">Each row is backed by a persisted detection run and QA check when successful.</p>
|
||||
</div>
|
||||
<span className="count-pill">{detectionCalibrationRows.length} thresholds</span>
|
||||
</div>
|
||||
<div className="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Threshold</th>
|
||||
<th>Status</th>
|
||||
<th>Detections</th>
|
||||
<th>Precision</th>
|
||||
<th>Recall</th>
|
||||
<th>F1</th>
|
||||
<th>False positives</th>
|
||||
<th>False negatives</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detectionCalibrationRows.map((row) => (
|
||||
<tr key={row.threshold}>
|
||||
<td>{row.threshold.toFixed(2)}</td>
|
||||
<td>{row.status}</td>
|
||||
<td>{row.detection_count ?? 'n/a'}</td>
|
||||
<td>{formatNullableNumber(row.precision ?? null, 3)}</td>
|
||||
<td>{formatNullableNumber(row.recall ?? null, 3)}</td>
|
||||
<td>{formatNullableNumber(row.f1_score ?? null, 3)}</td>
|
||||
<td>{row.false_positives ?? 'n/a'}</td>
|
||||
<td>{row.false_negatives ?? 'n/a'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>No calibration sweep has been run in this session.</strong>
|
||||
<p>Choose a reference dataset and threshold set, then start the explicit sweep.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ai-lab-results-surface" aria-label="Detection results">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user