Add detection operator profiles
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-09 20:19:10 +02:00
parent 60a7e822db
commit 90048ffb4a
12 changed files with 275 additions and 13 deletions
+2
View File
@@ -260,6 +260,7 @@ function App(): JSX.Element {
runDetection,
runDetectionQa,
runDetectionCalibration,
applyDetectionOperatorProfile,
resetDetectionForProject,
setSelectedDetectionDatasetId,
setSelectedDetectionModelId,
@@ -994,6 +995,7 @@ function App(): JSX.Element {
onSetCalibrationThresholdText={setCalibrationThresholdText}
onRunCalibration={runDetectionCalibration}
onOpenCalibrationEvidence={openQualityEvidenceOnMap}
onApplyOperatorProfile={applyDetectionOperatorProfile}
/>
<SegmentationLab
@@ -10,6 +10,7 @@ import type {
YoloPreflightResponse,
} from '../../types'
import type { DetectionCalibrationRunRow } from '../../hooks/useDetectionWorkflow'
import { DETECTION_OPERATOR_PROFILES, type DetectionOperatorProfile } from './detectionProfiles'
interface CalibrationRow {
analysisRunId: string
@@ -80,6 +81,7 @@ interface DetectionLabProps {
onSetCalibrationThresholdText: (value: string) => void
onRunCalibration: () => void
onOpenCalibrationEvidence: (qualityCheckId: string) => void
onApplyOperatorProfile: (profile: DetectionOperatorProfile) => void
}
export function DetectionLab({
@@ -135,6 +137,7 @@ export function DetectionLab({
onSetCalibrationThresholdText,
onRunCalibration,
onOpenCalibrationEvidence,
onApplyOperatorProfile,
}: DetectionLabProps): JSX.Element {
const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null
const selectedModelAsset = modelAssets.find((asset) => asset.model_asset_id === selectedModelAssetId) ?? null
@@ -145,9 +148,6 @@ export function DetectionLab({
const detectionModelUiRunnable = detectionModelReady && selectedDetectionModelId !== 'manual-fixture-detector'
const detectionHasExplicitModelAsset =
selectedDetectionModelId !== 'yolo-configured' || modelAssets.length === 0 || selectedModelAssetId.length > 0
const benchmarkCandidateAsset = modelAssets.find(
(asset) => asset.model_asset_id === 'geointel-building-yolov8s-hardneg160r4e50-pt',
)
const calibrationRows = buildCalibrationRows(detectionRuns, qualityChecks)
const bestF1Candidate = bestCalibrationRow(calibrationRows, 'f1')
const bestPrecisionCandidate = bestCalibrationRow(calibrationRows, 'precision')
@@ -251,12 +251,62 @@ export function DetectionLab({
{selectedModelAsset ? 'asset selected' : 'no explicit asset'}
</span>
</div>
{benchmarkCandidateAsset ? (
<div className="model-asset-guidance">
<strong>Operator profiles</strong>
<p>
Candidate profiles apply a local model asset and confidence threshold only after an explicit click.
Candidate only - not default-approved while the promotion recommendation remains none.
</p>
</div>
<div className="operator-profile-grid" aria-label="Configured YOLO operator profiles">
{DETECTION_OPERATOR_PROFILES.map((profile) => {
const profileAsset = modelAssets.find((asset) => asset.model_asset_id === profile.modelAssetId)
const profileSelected =
selectedModelAssetId === profile.modelAssetId &&
Math.abs(detectionConfidenceThreshold - profile.confidenceThreshold) < 0.0001
return (
<div
className={profileSelected ? 'operator-profile-card operator-profile-card-selected' : 'operator-profile-card'}
key={profile.id}
>
<div className="operator-profile-card-header">
<strong>{profile.displayName}</strong>
<span className={profile.defaultApproved ? 'status-badge status-badge-ready' : 'status-badge'}>
{profile.defaultApproved ? 'default-approved' : 'Candidate only - not default-approved'}
</span>
</div>
<p>{profile.description}</p>
<div className="operator-profile-metrics">
<span>threshold {profile.confidenceThreshold.toFixed(2)}</span>
<span>precision {profile.precision.toFixed(3)}</span>
<span>recall {profile.recall.toFixed(3)}</span>
<span>F1 {profile.f1.toFixed(3)}</span>
<span>max background FP {profile.maxBackgroundDetections}</span>
</div>
<div className="entity-meta">
<span>asset: {profile.modelAssetId}</span>
<span>promotionRecommendation: {profile.promotionRecommendation}</span>
<span>available: {profileAsset ? 'yes' : 'not mounted'}</span>
</div>
<p className="field-guidance">{profile.limitationMessage}</p>
<button
className="secondary-action"
type="button"
onClick={() => onApplyOperatorProfile(profile)}
disabled={!profileAsset}
>
Apply profile
</button>
</div>
)
})}
</div>
{selectedModelAsset ? (
<div className="model-asset-guidance">
<strong>Current benchmark candidate</strong>
<strong>Selected model asset status</strong>
<p>
{benchmarkCandidateAsset.display_name} is available for deliberate evaluation. Recommended starting threshold: 0.25.
Keep it operator-selected until hard-negative false positives are reduced.
{selectedModelAsset.display_name} is operator-selected. Keep local candidates inactive until persisted
promotion evidence explicitly recommends default activation.
</p>
</div>
) : null}
@@ -469,7 +519,7 @@ export function DetectionLab({
/>
{selectedDetectionModelId === 'yolo-configured' ? (
<span className="field-guidance">
Recommended starting threshold: 0.25 for the current local YOLOv8s benchmark candidate.
Use an operator profile for the current local YOLOv8s candidate, or enter a threshold manually for calibration.
</span>
) : null}
</label>
@@ -0,0 +1,47 @@
export interface DetectionOperatorProfile {
id: string
displayName: string
modelAssetId: string
confidenceThreshold: number
defaultApproved: boolean
promotionRecommendation: 'none' | 'promote_candidate'
precision: number
recall: number
f1: number
maxBackgroundDetections: number
description: string
limitationMessage: string
}
export const DETECTION_OPERATOR_PROFILES: DetectionOperatorProfile[] = [
{
id: 'balanced-review',
displayName: 'Balanced review',
modelAssetId: 'geointel-building-yolov8s-aoi1024bg512r3e50-pt',
confidenceThreshold: 0.15,
defaultApproved: false,
promotionRecommendation: 'none',
precision: 0.636639,
recall: 0.424258,
f1: 0.5074022485589402,
maxBackgroundDetections: 103,
description: 'Best positive-AOI F1 profile for deliberate operator review of the inactive AOI1024 model asset.',
limitationMessage:
'Candidate only because false-positive pressure still blocks default promotion on the background/hard-negative gate.',
},
{
id: 'conservative-review',
displayName: 'Conservative review',
modelAssetId: 'geointel-building-yolov8s-aoi1024bg512r3e50-pt',
confidenceThreshold: 0.35,
defaultApproved: false,
promotionRecommendation: 'none',
precision: 0.840006,
recall: 0.202135,
f1: 0.32086574003576274,
maxBackgroundDetections: 55,
description: 'Higher-precision profile for demos or review sessions where fewer false positives matter more than recall.',
limitationMessage:
'Candidate only because false-positive pressure remains visible; use it deliberately and inspect persisted QA evidence.',
},
]
@@ -21,6 +21,11 @@ interface DetectionWorkflowOptions {
loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
}
interface DetectionOperatorProfileSelection {
modelAssetId: string
confidenceThreshold: number
}
export interface DetectionCalibrationRunRow {
threshold: number
status: 'queued' | 'running' | 'success' | 'failed'
@@ -333,6 +338,12 @@ export function useDetectionWorkflow({
}
}
const applyDetectionOperatorProfile = (profile: DetectionOperatorProfileSelection) => {
setSelectedDetectionModelId('yolo-configured')
setSelectedModelAssetId(profile.modelAssetId)
setDetectionConfidenceThreshold(profile.confidenceThreshold)
}
const resetDetectionForProject = () => {
setSelectedDetectionDatasetId('')
setDetectionRuns([])
@@ -383,6 +394,7 @@ export function useDetectionWorkflow({
runDetection,
runDetectionQa,
runDetectionCalibration,
applyDetectionOperatorProfile,
resetDetectionForProject,
setSelectedDetectionDatasetId,
setSelectedDetectionModelId,
+61
View File
@@ -3473,6 +3473,67 @@ button.entity-card {
line-height: 1.35;
}
.operator-profile-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 0.62rem;
}
.operator-profile-card {
display: grid;
gap: 0.48rem;
min-width: 0;
border: 1px solid #d8e3de;
border-radius: 8px;
padding: 0.72rem;
background: #ffffff;
}
.operator-profile-card-selected {
border-color: var(--accent);
background: #f7fffc;
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.1);
}
.operator-profile-card-header {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: 0.6rem;
}
.operator-profile-card-header strong {
min-width: 0;
color: var(--text);
font-size: 0.94rem;
line-height: 1.25;
}
.operator-profile-card p {
margin: 0;
color: var(--muted);
font-size: 0.8rem;
line-height: 1.35;
}
.operator-profile-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(6.4rem, 1fr));
gap: 0.36rem;
}
.operator-profile-metrics span {
border: 1px solid #e0e9e4;
border-radius: 6px;
padding: 0.34rem 0.42rem;
background: #f9fbfa;
color: var(--text);
font-size: 0.76rem;
font-weight: 700;
overflow-wrap: anywhere;
}
.field-guidance {
display: block;
margin-top: 0.24rem;