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
+7
View File
@@ -7,6 +7,13 @@
# Changelog # Changelog
## Sprint 155 Detection operator profiles (2026-07-09)
- Added explicit Detection Lab operator profiles for the inactive `geointel-building-yolov8s-aoi1024bg512r3e50-pt` local model asset.
- Added a balanced review profile at confidence threshold `0.15` and a conservative review profile at `0.35`, with persisted gate metrics shown in the UI.
- Kept both profiles clearly marked as candidate-only and not default-approved because the promotion recommendation remains `none` and background false-positive pressure still blocks automatic activation.
- No model download behavior, API contract, migration, provider fetching, fake detection output or active runtime default changed.
## Sprint 154 Background-aware AOI1024 YOLOv8s candidate gate (2026-07-09) ## Sprint 154 Background-aware AOI1024 YOLOv8s candidate gate (2026-07-09)
- Exported and audited background-aware AOI1024 training dataset `/app/storage/operator-data/yolo-building-aoi1024-bgaware512r3`; the audit passed with 162 tiles, 117 positive tiles, 45 negative tiles, 21,530 labels and no warnings. - Exported and audited background-aware AOI1024 training dataset `/app/storage/operator-data/yolo-building-aoi1024-bgaware512r3`; the audit passed with 162 tiles, 117 positive tiles, 45 negative tiles, 21,530 labels and no warnings.
@@ -20,10 +20,9 @@ def test_detection_lab_explains_explicit_model_asset_and_threshold_selection() -
assert "Explicit model asset" in lab assert "Explicit model asset" in lab
assert "No model file is selected automatically" in lab assert "No model file is selected automatically" in lab
assert "Current benchmark candidate" in lab assert "Operator profiles" in lab
assert "geointel-building-yolov8s-hardneg160r4e50-pt" in lab assert "DETECTION_OPERATOR_PROFILES" in lab
assert "Recommended starting threshold" in lab assert "Candidate only - not default-approved" in lab
assert "0.25" in lab
assert "will_download_models" in lab assert "will_download_models" in lab
@@ -0,0 +1,46 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_detection_operator_profiles_define_explicit_non_default_yolo_candidates() -> None:
profiles = ROOT / "frontend" / "src" / "components" / "detection" / "detectionProfiles.ts"
source = profiles.read_text(encoding="utf-8")
assert "DETECTION_OPERATOR_PROFILES" in source
assert "geointel-building-yolov8s-aoi1024bg512r3e50-pt" in source
assert "balanced-review" in source
assert "conservative-review" in source
assert "confidenceThreshold: 0.15" in source
assert "confidenceThreshold: 0.35" in source
assert "defaultApproved: false" in source
assert "promotionRecommendation: 'none'" in source
assert "false-positive pressure" in source
def test_detection_lab_surfaces_profiles_as_deliberate_operator_actions() -> None:
lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(
encoding="utf-8"
)
assert "DETECTION_OPERATOR_PROFILES" in lab
assert "Operator profiles" in lab
assert "profile.displayName" in lab
assert "profile.confidenceThreshold" in lab
assert "Candidate only - not default-approved" in lab
assert "Apply profile" in lab
assert "onApplyOperatorProfile(profile)" in lab
assert "Recommended starting threshold: 0.25" not in lab
def test_detection_workflow_applies_profiles_without_auto_selecting_assets() -> None:
hook = (ROOT / "frontend" / "src" / "hooks" / "useDetectionWorkflow.ts").read_text(encoding="utf-8")
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
assert "applyDetectionOperatorProfile" in hook
assert "setSelectedDetectionModelId('yolo-configured')" in hook
assert "setSelectedModelAssetId(profile.modelAssetId)" in hook
assert "setDetectionConfidenceThreshold(profile.confidenceThreshold)" in hook
assert "setSelectedModelAssetId(assetResponse.items[0]" not in hook
assert "onApplyOperatorProfile={applyDetectionOperatorProfile}" in app
+8
View File
@@ -260,6 +260,14 @@ as false-positive pressure. It does not run QA/QC or invent reference metrics
for empty/sparse background AOIs. The first expanded local model improved dense for empty/sparse background AOIs. The first expanded local model improved dense
AOI F1, but Kasterlee-bos false positives block default promotion. AOI F1, but Kasterlee-bos false positives block default promotion.
The current inactive AOI1024 background-aware local model asset,
`geointel-building-yolov8s-aoi1024bg512r3e50-pt`, is exposed in Detection Lab
only through deliberate operator profiles. `balanced-review` applies threshold
`0.15` for the strongest positive-AOI F1 observed so far; `conservative-review`
applies threshold `0.35` for higher precision review. Both profiles remain
candidate-only, not default-approved, because the promotion recommendation is
still `none` and background false-positive pressure has not passed the gate.
To compare the same model/tile/threshold grid across all prepared operator To compare the same model/tile/threshold grid across all prepared operator
samples, use: samples, use:
+29
View File
@@ -6168,3 +6168,32 @@ Open:
- Build a V1 operator detection profile layer in the UI/docs: expose `balanced` (`threshold=0.15`) and `conservative review` (`threshold=0.35`) as explicit choices for local model assets, with clear warning that the model is not a default-approved detector. - Build a V1 operator detection profile layer in the UI/docs: expose `balanced` (`threshold=0.15`) and `conservative review` (`threshold=0.35`) as explicit choices for local model assets, with clear warning that the model is not a default-approved detector.
- Clean the background corpus classification: separate pure-empty AOIs from sparse-building contextual AOIs, then retrain or recalibrate against that cleaner gate. - Clean the background corpus classification: separate pure-empty AOIs from sparse-building contextual AOIs, then retrain or recalibrate against that cleaner gate.
# Sprint 155 - Detection operator profiles
## What changed
- Added `frontend/src/components/detection/detectionProfiles.ts` with explicit operator profiles for the inactive `geointel-building-yolov8s-aoi1024bg512r3e50-pt` local model asset.
- Exposed two deliberate Detection Lab actions:
- `balanced-review`: confidence threshold `0.15`, positive-AOI F1 `0.5074022485589402`, precision `0.636639`, recall `0.424258`, max background detections `103`.
- `conservative-review`: confidence threshold `0.35`, positive-AOI F1 `0.32086574003576274`, precision `0.840006`, recall `0.202135`, max background detections `55`.
- Applying a profile selects `yolo-configured`, the local model asset id and the profile threshold. It does not auto-select assets on model catalog load and does not promote the candidate as a default detector.
- Detection Lab now marks both profiles as `Candidate only - not default-approved` because the promotion recommendation remains `none`.
- Updated frontend, AI pipeline and TODO documentation.
## What was tested
- Added regression coverage in `backend/tests/test_sprint155_detection_operator_profiles.py`.
- Ran `python -m pytest tests/test_sprint155_detection_operator_profiles.py tests/test_sprint122_model_asset_activation_guardrails.py -q`.
- Ran `python -m pytest` in `backend`: 435 passed.
- Ran `python -m compileall backend/app`.
- Ran `cd frontend && npm run typecheck`.
- Ran `cd frontend && npm run build`.
- Ran `bash scripts/run_readiness_check.sh`.
- Ran `cd backend && python -m alembic heads` and `cd backend && python -m alembic upgrade head --sql`.
- Ran `bash -n scripts/live_migration_smoke.sh`.
## Known limitations
- The profiles are review/demo aids only. The background corpus still needs to be split into pure-empty negatives and sparse-building contextual AOIs before retraining or recalibrating for a default detector decision.
- No backend API contract, migration, provider fetching, fake detection output, model download behavior or active runtime default changed.
+1 -1
View File
@@ -120,7 +120,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Export and audit AOI1024 clean-label variants; select `yolo-building-aoi1024-visible050-minpx8` as the first audit-passing 512px training candidate. - [x] Export and audit AOI1024 clean-label variants; select `yolo-building-aoi1024-visible050-minpx8` as the first audit-passing 512px training candidate.
- [x] Train and gate `geointel-building-yolov8s-aoi1024clean512e50-pt` through seven positive AOIs and nine hard-negative/background AOIs. - [x] Train and gate `geointel-building-yolov8s-aoi1024clean512e50-pt` through seven positive AOIs and nine hard-negative/background AOIs.
- [x] Train and gate background-aware `geointel-building-yolov8s-aoi1024bg512r3e50-pt`; it is the strongest positive-AOI candidate so far but remains inactive because full background-candidate false-positive pressure still blocks default promotion. - [x] Train and gate background-aware `geointel-building-yolov8s-aoi1024bg512r3e50-pt`; it is the strongest positive-AOI candidate so far but remains inactive because full background-candidate false-positive pressure still blocks default promotion.
- [ ] Add explicit operator detection profiles for local model assets: balanced review around threshold `0.15` and conservative high-precision review around threshold `0.35`, both clearly marked as non-default-approved until promotion gates pass. - [x] Add explicit operator detection profiles for local model assets: balanced review around threshold `0.15` and conservative high-precision review around threshold `0.35`, both clearly marked as non-default-approved until promotion gates pass.
- [ ] Split the background corpus into pure-empty negatives and sparse-building contextual AOIs, then retrain or recalibrate against the cleaner gate. - [ ] Split the background corpus into pure-empty negatives and sparse-building contextual AOIs, then retrain or recalibrate against the cleaner gate.
- [ ] Promote a V1 default building detector only after it passes seven positive AOIs, clean hard-negative/background gates and persisted QA/QC evidence without fake detections or model downloads. - [ ] Promote a V1 default building detector only after it passes seven positive AOIs, clean hard-negative/background gates and persisted QA/QC evidence without fake detections or model downloads.
+1
View File
@@ -122,6 +122,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
- Detection Lab now exposes the `yolo-configured` capability reported by the backend. - Detection Lab now exposes the `yolo-configured` capability reported by the backend.
- When `yolo-configured` is selected, users can provide an existing raster tile manifest path. - When `yolo-configured` is selected, users can provide an existing raster tile manifest path.
- Detection Lab lists local model assets from `GET /api/v1/detection/model-assets` so operators can choose an existing mounted model file instead of editing only one hidden `YOLO_MODEL_PATH` slot. - Detection Lab lists local model assets from `GET /api/v1/detection/model-assets` so operators can choose an existing mounted model file instead of editing only one hidden `YOLO_MODEL_PATH` slot.
- Detection Lab exposes explicit operator profiles for the current inactive local AOI1024 building detector: balanced review at threshold `0.15` and conservative review at threshold `0.35`. Applying a profile deliberately selects the local model asset and threshold; it does not approve or promote a default model.
- Detection Lab includes a read-only YOLO runtime preflight panel with backend status, dependency visibility, local model configuration, `torch`/`ultralytics` versions, CUDA state and `YOLO_CONFIG_DIR`. - Detection Lab includes a read-only YOLO runtime preflight panel with backend status, dependency visibility, local model configuration, `torch`/`ultralytics` versions, CUDA state and `YOLO_CONFIG_DIR`.
- The UI still does not download models or create fake detections; backend status and error codes remain the source of truth. - The UI still does not download models or create fake detections; backend status and error codes remain the source of truth.
+2
View File
@@ -260,6 +260,7 @@ function App(): JSX.Element {
runDetection, runDetection,
runDetectionQa, runDetectionQa,
runDetectionCalibration, runDetectionCalibration,
applyDetectionOperatorProfile,
resetDetectionForProject, resetDetectionForProject,
setSelectedDetectionDatasetId, setSelectedDetectionDatasetId,
setSelectedDetectionModelId, setSelectedDetectionModelId,
@@ -994,6 +995,7 @@ function App(): JSX.Element {
onSetCalibrationThresholdText={setCalibrationThresholdText} onSetCalibrationThresholdText={setCalibrationThresholdText}
onRunCalibration={runDetectionCalibration} onRunCalibration={runDetectionCalibration}
onOpenCalibrationEvidence={openQualityEvidenceOnMap} onOpenCalibrationEvidence={openQualityEvidenceOnMap}
onApplyOperatorProfile={applyDetectionOperatorProfile}
/> />
<SegmentationLab <SegmentationLab
@@ -10,6 +10,7 @@ import type {
YoloPreflightResponse, YoloPreflightResponse,
} from '../../types' } from '../../types'
import type { DetectionCalibrationRunRow } from '../../hooks/useDetectionWorkflow' import type { DetectionCalibrationRunRow } from '../../hooks/useDetectionWorkflow'
import { DETECTION_OPERATOR_PROFILES, type DetectionOperatorProfile } from './detectionProfiles'
interface CalibrationRow { interface CalibrationRow {
analysisRunId: string analysisRunId: string
@@ -80,6 +81,7 @@ interface DetectionLabProps {
onSetCalibrationThresholdText: (value: string) => void onSetCalibrationThresholdText: (value: string) => void
onRunCalibration: () => void onRunCalibration: () => void
onOpenCalibrationEvidence: (qualityCheckId: string) => void onOpenCalibrationEvidence: (qualityCheckId: string) => void
onApplyOperatorProfile: (profile: DetectionOperatorProfile) => void
} }
export function DetectionLab({ export function DetectionLab({
@@ -135,6 +137,7 @@ export function DetectionLab({
onSetCalibrationThresholdText, onSetCalibrationThresholdText,
onRunCalibration, onRunCalibration,
onOpenCalibrationEvidence, onOpenCalibrationEvidence,
onApplyOperatorProfile,
}: DetectionLabProps): JSX.Element { }: DetectionLabProps): JSX.Element {
const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null
const selectedModelAsset = modelAssets.find((asset) => asset.model_asset_id === selectedModelAssetId) ?? 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 detectionModelUiRunnable = detectionModelReady && selectedDetectionModelId !== 'manual-fixture-detector'
const detectionHasExplicitModelAsset = const detectionHasExplicitModelAsset =
selectedDetectionModelId !== 'yolo-configured' || modelAssets.length === 0 || selectedModelAssetId.length > 0 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 calibrationRows = buildCalibrationRows(detectionRuns, qualityChecks)
const bestF1Candidate = bestCalibrationRow(calibrationRows, 'f1') const bestF1Candidate = bestCalibrationRow(calibrationRows, 'f1')
const bestPrecisionCandidate = bestCalibrationRow(calibrationRows, 'precision') const bestPrecisionCandidate = bestCalibrationRow(calibrationRows, 'precision')
@@ -251,12 +251,62 @@ export function DetectionLab({
{selectedModelAsset ? 'asset selected' : 'no explicit asset'} {selectedModelAsset ? 'asset selected' : 'no explicit asset'}
</span> </span>
</div> </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"> <div className="model-asset-guidance">
<strong>Current benchmark candidate</strong> <strong>Selected model asset status</strong>
<p> <p>
{benchmarkCandidateAsset.display_name} is available for deliberate evaluation. Recommended starting threshold: 0.25. {selectedModelAsset.display_name} is operator-selected. Keep local candidates inactive until persisted
Keep it operator-selected until hard-negative false positives are reduced. promotion evidence explicitly recommends default activation.
</p> </p>
</div> </div>
) : null} ) : null}
@@ -469,7 +519,7 @@ export function DetectionLab({
/> />
{selectedDetectionModelId === 'yolo-configured' ? ( {selectedDetectionModelId === 'yolo-configured' ? (
<span className="field-guidance"> <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> </span>
) : null} ) : null}
</label> </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> loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
} }
interface DetectionOperatorProfileSelection {
modelAssetId: string
confidenceThreshold: number
}
export interface DetectionCalibrationRunRow { export interface DetectionCalibrationRunRow {
threshold: number threshold: number
status: 'queued' | 'running' | 'success' | 'failed' 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 = () => { const resetDetectionForProject = () => {
setSelectedDetectionDatasetId('') setSelectedDetectionDatasetId('')
setDetectionRuns([]) setDetectionRuns([])
@@ -383,6 +394,7 @@ export function useDetectionWorkflow({
runDetection, runDetection,
runDetectionQa, runDetectionQa,
runDetectionCalibration, runDetectionCalibration,
applyDetectionOperatorProfile,
resetDetectionForProject, resetDetectionForProject,
setSelectedDetectionDatasetId, setSelectedDetectionDatasetId,
setSelectedDetectionModelId, setSelectedDetectionModelId,
+61
View File
@@ -3473,6 +3473,67 @@ button.entity-card {
line-height: 1.35; 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 { .field-guidance {
display: block; display: block;
margin-top: 0.24rem; margin-top: 0.24rem;