Add local model asset catalog
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-06 20:59:03 +02:00
parent 9e20cc82ae
commit 6e2a8cbdc4
33 changed files with 660 additions and 7 deletions
+2
View File
@@ -107,6 +107,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
## Sprint 7B additions
- Added a lightweight Provider Capabilities panel.
- The panel lists GRB, OSM, manual and fixture provider status, configured state, authority level, supported layers, supported geometry types, query modes and limitation messages.
- Provider Capabilities now labels GRB/OSM/manual/fixture as reference data source capabilities, not AI model choices.
- GRB and OSM are shown as `not_configured`; the UI does not expose a live import/download action for them.
- Existing dataset, reference and QA/QC UI remains unchanged.
@@ -120,6 +121,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
## Sprint 8B additions
- 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.
- 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 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.
+8
View File
@@ -222,10 +222,13 @@ function App(): JSX.Element {
})
const {
detectionModels,
modelAssets,
loadingDetectionModels,
detectionModelError,
modelAssetError,
selectedDetectionDatasetId,
selectedDetectionModelId,
selectedModelAssetId,
detectionTileManifestPath,
detectionConfidenceThreshold,
runningDetection,
@@ -254,6 +257,7 @@ function App(): JSX.Element {
resetDetectionForProject,
setSelectedDetectionDatasetId,
setSelectedDetectionModelId,
setSelectedModelAssetId,
setDetectionTileManifestPath,
setDetectionConfidenceThreshold,
setSelectedDetectionRunId,
@@ -930,10 +934,13 @@ function App(): JSX.Element {
<div className="workspace-grid workspace-grid-ai">
<DetectionLab
detectionModels={detectionModels}
modelAssets={modelAssets}
loadingDetectionModels={loadingDetectionModels}
detectionModelError={detectionModelError}
modelAssetError={modelAssetError}
selectedDetectionDatasetId={selectedDetectionDatasetId}
selectedDetectionModelId={selectedDetectionModelId}
selectedModelAssetId={selectedModelAssetId}
detectionTileManifestPath={detectionTileManifestPath}
detectionConfidenceThreshold={detectionConfidenceThreshold}
runningDetection={runningDetection}
@@ -959,6 +966,7 @@ function App(): JSX.Element {
onRefreshYoloPreflight={() => loadYoloPreflight()}
onSelectDataset={setSelectedDetectionDatasetId}
onSelectModel={setSelectedDetectionModelId}
onSelectModelAsset={setSelectedModelAssetId}
onSetConfidenceThreshold={setDetectionConfidenceThreshold}
onSetTileManifestPath={setDetectionTileManifestPath}
onRunDetection={runDetection}
@@ -5,15 +5,19 @@ import type {
DetectionRead,
DetectionRunRead,
DetectionRunResponse,
ModelAssetRead,
YoloPreflightResponse,
} from '../../types'
interface DetectionLabProps {
detectionModels: DetectionModelCapability[]
modelAssets: ModelAssetRead[]
loadingDetectionModels: boolean
detectionModelError: string | null
modelAssetError: string | null
selectedDetectionDatasetId: string
selectedDetectionModelId: string
selectedModelAssetId: string
detectionTileManifestPath: string
detectionConfidenceThreshold: number
runningDetection: boolean
@@ -39,6 +43,7 @@ interface DetectionLabProps {
onRefreshYoloPreflight: () => void
onSelectDataset: (datasetId: string) => void
onSelectModel: (modelId: string) => void
onSelectModelAsset: (modelAssetId: string) => void
onSetConfidenceThreshold: (value: number) => void
onSetTileManifestPath: (value: string) => void
onRunDetection: () => void
@@ -53,10 +58,13 @@ interface DetectionLabProps {
export function DetectionLab({
detectionModels,
modelAssets,
loadingDetectionModels,
detectionModelError,
modelAssetError,
selectedDetectionDatasetId,
selectedDetectionModelId,
selectedModelAssetId,
detectionTileManifestPath,
detectionConfidenceThreshold,
runningDetection,
@@ -82,6 +90,7 @@ export function DetectionLab({
onRefreshYoloPreflight,
onSelectDataset,
onSelectModel,
onSelectModelAsset,
onSetConfidenceThreshold,
onSetTileManifestPath,
onRunDetection,
@@ -94,6 +103,7 @@ export function DetectionLab({
onRunQa,
}: DetectionLabProps): JSX.Element {
const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null
const selectedModelAsset = modelAssets.find((asset) => asset.model_asset_id === selectedModelAssetId) ?? null
const detectionRequiresTileManifest = selectedDetectionModelId === 'yolo-configured'
const detectionHasDataset = selectedDetectionDatasetId.length > 0
const detectionHasModel = selectedDetectionModel !== null
@@ -149,6 +159,12 @@ export function DetectionLab({
<p>{detectionModelError}</p>
</div>
) : null}
{modelAssetError ? (
<div className="result-state result-state-error">
<strong>Local model assets unavailable.</strong>
<p>{modelAssetError}</p>
</div>
) : null}
{detectionModels.length === 0 && !loadingDetectionModels ? (
<div className="result-state result-state-empty">
<strong>No detection models reported by backend.</strong>
@@ -173,6 +189,47 @@ export function DetectionLab({
</ul>
</div>
{selectedDetectionModelId === 'yolo-configured' ? (
<div className="ai-lab-model-surface" aria-label="Local model asset selection">
<div className="ai-lab-section-header">
<div>
<h3>Local model assets</h3>
<p>Select an existing model file mounted into the backend runtime. GeoIntel does not download model weights.</p>
</div>
<span className={selectedModelAsset ? 'status-badge status-badge-ready' : 'status-badge'}>
{selectedModelAsset ? 'asset selected' : 'using configured path'}
</span>
</div>
<label>
Local model file
<select value={selectedModelAssetId} onChange={(event) => onSelectModelAsset(event.target.value)}>
<option value="">Use configured YOLO_MODEL_PATH</option>
{modelAssets.map((asset) => (
<option key={asset.model_asset_id} value={asset.model_asset_id}>
{asset.display_name} {asset.active ? '(active)' : ''}
</option>
))}
</select>
</label>
{modelAssets.length === 0 && !loadingDetectionModels ? (
<div className="result-state result-state-empty">
<strong>No local model assets found.</strong>
<p>Mount model files into the backend model directory or continue with the configured YOLO_MODEL_PATH.</p>
</div>
) : null}
{selectedModelAsset ? (
<div className="result-summary-card">
<p>File: {selectedModelAsset.filename}</p>
<p>Status: {selectedModelAsset.status}</p>
<p>Size: {formatModelAssetSize(selectedModelAsset.size_bytes)}</p>
<p>SHA-256: {selectedModelAsset.sha256.slice(0, 12)}</p>
<p>Path: {selectedModelAsset.model_path}</p>
<p>{selectedModelAsset.limitation_message}</p>
</div>
) : null}
</div>
) : null}
<div className="ai-lab-model-surface" aria-label="YOLO runtime preflight">
<div className="ai-lab-section-header">
<div>
@@ -238,6 +295,7 @@ export function DetectionLab({
<span>cuda_available: {String(yoloPreflight.runtime.cuda_available ?? 'unknown')}</span>
<span>YOLO_CONFIG_DIR: {yoloPreflight.runtime.yolo_config_dir ?? 'n/a'}</span>
<span>model directory: {yoloPreflight.runtime.model_directory ?? 'n/a'}</span>
<span>model_asset_id: {yoloPreflight.model_asset_id ?? 'n/a'}</span>
</div>
</div>
) : null}
@@ -486,3 +544,13 @@ export function DetectionLab({
</section>
)
}
function formatModelAssetSize(sizeBytes: number): string {
if (sizeBytes >= 1024 * 1024) {
return `${(sizeBytes / (1024 * 1024)).toFixed(1)} MB`
}
if (sizeBytes >= 1024) {
return `${(sizeBytes / 1024).toFixed(1)} KB`
}
return `${sizeBytes} B`
}
@@ -48,6 +48,12 @@ export function ProviderPanel({
</div>
<div className="system-provider-capability-surface" aria-label="Provider capability registry">
<div className="provider-detail-stack">
<strong>Official reference sources</strong>
<div>
GRB and OSM are reference-data provider capabilities, not AI model choices. Manual upload is the configured path for real reference datasets today; fixtures remain test/demo only.
</div>
</div>
<ul className="system-provider-list">
{providers.map((provider) => (
<li className="system-provider-card" key={provider.provider_name}>
@@ -7,6 +7,7 @@ import type {
DetectionRead,
DetectionRunRead,
DetectionRunResponse,
ModelAssetRead,
QualityCheckRead,
YoloPreflightResponse,
} from '../types'
@@ -28,10 +29,13 @@ export function useDetectionWorkflow({
loadQualityChecks,
}: DetectionWorkflowOptions) {
const [detectionModels, setDetectionModels] = useState<DetectionModelCapability[]>([])
const [modelAssets, setModelAssets] = useState<ModelAssetRead[]>([])
const [loadingDetectionModels, setLoadingDetectionModels] = useState(false)
const [detectionModelError, setDetectionModelError] = useState<string | null>(null)
const [modelAssetError, setModelAssetError] = useState<string | null>(null)
const [selectedDetectionDatasetId, setSelectedDetectionDatasetId] = useState('')
const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-placeholder')
const [selectedModelAssetId, setSelectedModelAssetId] = useState('')
const [detectionTileManifestPath, setDetectionTileManifestPath] = useState('')
const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.5)
const [runningDetection, setRunningDetection] = useState(false)
@@ -55,6 +59,7 @@ export function useDetectionWorkflow({
const loadDetectionModels = async () => {
setLoadingDetectionModels(true)
setDetectionModelError(null)
setModelAssetError(null)
try {
const response = await detectionApi.listModels()
setDetectionModels(response.models)
@@ -63,6 +68,17 @@ export function useDetectionWorkflow({
}
} catch (error) {
setDetectionModelError(formatError(error, 'Failed to load detection models'))
}
try {
const assetResponse = await detectionApi.listModelAssets()
setModelAssets(assetResponse.items)
const activeAsset = assetResponse.items.find((asset) => asset.active) ?? assetResponse.items[0] ?? null
if (!assetResponse.items.some((asset) => asset.model_asset_id === selectedModelAssetId)) {
setSelectedModelAssetId(activeAsset?.model_asset_id ?? '')
}
} catch (error) {
setModelAssets([])
setModelAssetError(formatError(error, 'Failed to load local model assets'))
} finally {
setLoadingDetectionModels(false)
}
@@ -74,6 +90,7 @@ export function useDetectionWorkflow({
try {
const response = await detectionApi.getYoloPreflight({
tile_manifest_path: tileManifestPath.trim() || null,
model_asset_id: selectedModelAssetId || null,
})
setYoloPreflight(response)
} catch (error) {
@@ -143,6 +160,7 @@ export function useDetectionWorkflow({
project_id: selectedProjectId,
dataset_id: datasetId,
model_id: selectedDetectionModelId,
model_asset_id: selectedModelAssetId || null,
confidence_threshold: detectionConfidenceThreshold,
tile_manifest_path: detectionTileManifestPath.trim() || null,
parameters_json: {},
@@ -198,10 +216,13 @@ export function useDetectionWorkflow({
return {
detectionModels,
modelAssets,
loadingDetectionModels,
detectionModelError,
modelAssetError,
selectedDetectionDatasetId,
selectedDetectionModelId,
selectedModelAssetId,
detectionTileManifestPath,
detectionConfidenceThreshold,
runningDetection,
@@ -230,6 +251,7 @@ export function useDetectionWorkflow({
resetDetectionForProject,
setSelectedDetectionDatasetId,
setSelectedDetectionModelId,
setSelectedModelAssetId,
setDetectionTileManifestPath,
setDetectionConfidenceThreshold,
setSelectedDetectionRunId,
+3 -1
View File
@@ -8,6 +8,7 @@ import type {
DetectionRunRead,
DetectionRunRequest,
DetectionRunResponse,
ModelAssetListResponse,
YoloPreflightResponse,
} from '../../types'
@@ -24,7 +25,8 @@ function queryString(params: Record<string, string | number | boolean | null | u
export const detectionApi = {
listModels: (): Promise<DetectionModelsResponse> => apiGet<DetectionModelsResponse>('/api/v1/detection/models'),
getYoloPreflight: (params: { tile_manifest_path?: string | null; check_model_load?: boolean | null } = {}): Promise<YoloPreflightResponse> =>
listModelAssets: (): Promise<ModelAssetListResponse> => apiGet<ModelAssetListResponse>('/api/v1/detection/model-assets'),
getYoloPreflight: (params: { tile_manifest_path?: string | null; check_model_load?: boolean | null; model_asset_id?: string | null } = {}): Promise<YoloPreflightResponse> =>
apiGet<YoloPreflightResponse>(`/api/v1/detection/yolo/preflight${queryString(params)}`),
run: (payload: DetectionRunRequest): Promise<DetectionRunResponse> =>
apiPost<DetectionRunResponse>('/api/v1/detection/run', payload),
+24
View File
@@ -404,6 +404,28 @@ export interface DetectionModelsResponse {
models: DetectionModelCapability[]
}
export interface ModelAssetRead {
model_asset_id: string
filename: string
display_name: string
model_path: string
suffix: string
framework: string
task_type: string
size_bytes: number
sha256: string
active: boolean
status: string
limitation_message: string
will_download_models: boolean
}
export interface ModelAssetListResponse {
items: ModelAssetRead[]
total: number
model_directory: string
}
export interface YoloPreflightChecks {
enabled: boolean
dependencies_available?: boolean | null
@@ -428,6 +450,7 @@ export interface YoloPreflightRuntime {
export interface YoloPreflightResponse {
model_id: string
model_asset_id?: string | null
model_path?: string | null
tile_manifest_path?: string | null
status: string
@@ -445,6 +468,7 @@ export interface DetectionRunRequest {
project_id: string
dataset_id: string
model_id: string
model_asset_id?: string | null
confidence_threshold: number
class_filter?: string[] | null
tile_manifest_path?: string | null