Harden raster detection manifest handoff
This commit is contained in:
@@ -83,6 +83,7 @@ function App(): JSX.Element {
|
||||
jobs,
|
||||
rasterPreview,
|
||||
latestRasterTileManifestPath,
|
||||
latestRasterTileManifest,
|
||||
selectedIntersectTargetId,
|
||||
selectedClipAreaId,
|
||||
rasterTileSize,
|
||||
@@ -279,6 +280,8 @@ function App(): JSX.Element {
|
||||
setDetectionTileManifestPath(manifestPath)
|
||||
setSelectedDetectionDatasetId(selectedDataset.id)
|
||||
setSelectedDetectionModelId('yolo-configured')
|
||||
setDetectionConfidenceThreshold(0.25)
|
||||
loadYoloPreflight(manifestPath).catch(() => null)
|
||||
setActiveWorkspace('ai')
|
||||
}
|
||||
const {
|
||||
@@ -1088,6 +1091,7 @@ function App(): JSX.Element {
|
||||
selectedRasterStats,
|
||||
rasterPreview,
|
||||
latestRasterTileManifestPath,
|
||||
latestRasterTileManifest,
|
||||
rasterUnavailableMessage,
|
||||
selectedClipAreaId,
|
||||
selectedIntersectTargetId,
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
RasterMetadataResponse,
|
||||
RasterPreviewResponse,
|
||||
RasterStatsResponse,
|
||||
RasterTileHandoff,
|
||||
VectorSummary,
|
||||
} from '../../types'
|
||||
import { RasterControls } from './RasterControls'
|
||||
@@ -20,6 +21,7 @@ export interface DatasetDetailPanelProps {
|
||||
selectedRasterStats: RasterStatsResponse | null
|
||||
rasterPreview: RasterPreviewResponse | null
|
||||
latestRasterTileManifestPath: string
|
||||
latestRasterTileManifest: RasterTileHandoff | null
|
||||
rasterUnavailableMessage: string | null
|
||||
selectedClipAreaId: string
|
||||
selectedIntersectTargetId: string
|
||||
@@ -104,6 +106,7 @@ export function DatasetDetailPanel({
|
||||
selectedRasterStats,
|
||||
rasterPreview,
|
||||
latestRasterTileManifestPath,
|
||||
latestRasterTileManifest,
|
||||
rasterUnavailableMessage,
|
||||
selectedClipAreaId,
|
||||
selectedIntersectTargetId,
|
||||
@@ -178,6 +181,7 @@ export function DatasetDetailPanel({
|
||||
selectedRasterStats={selectedRasterStats}
|
||||
rasterPreview={rasterPreview}
|
||||
latestRasterTileManifestPath={latestRasterTileManifestPath}
|
||||
latestRasterTileManifest={latestRasterTileManifest}
|
||||
rasterUnavailableMessage={rasterUnavailableMessage}
|
||||
selectedClipAreaId={selectedClipAreaId}
|
||||
rasterTileSize={rasterTileSize}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRead, RasterMetadataResponse, RasterPreviewResponse, RasterStatsResponse } from '../../types'
|
||||
import type { AreaRead, RasterMetadataResponse, RasterPreviewResponse, RasterStatsResponse, RasterTileHandoff } from '../../types'
|
||||
|
||||
interface RasterControlsProps {
|
||||
areas: AreaRead[]
|
||||
@@ -7,6 +7,7 @@ interface RasterControlsProps {
|
||||
selectedRasterStats: RasterStatsResponse | null
|
||||
rasterPreview: RasterPreviewResponse | null
|
||||
latestRasterTileManifestPath: string
|
||||
latestRasterTileManifest: RasterTileHandoff | null
|
||||
rasterUnavailableMessage: string | null
|
||||
selectedClipAreaId: string
|
||||
rasterTileSize: number
|
||||
@@ -61,6 +62,7 @@ export function RasterControls({
|
||||
selectedRasterStats,
|
||||
rasterPreview,
|
||||
latestRasterTileManifestPath,
|
||||
latestRasterTileManifest,
|
||||
rasterUnavailableMessage,
|
||||
selectedClipAreaId,
|
||||
rasterTileSize,
|
||||
@@ -167,11 +169,41 @@ export function RasterControls({
|
||||
{latestRasterTileManifestPath ||
|
||||
'No tile manifest generated yet. Generate tiles before handing this raster to Detection Lab.'}
|
||||
</p>
|
||||
<button type="button" onClick={onUseTileManifestForDetection} disabled={!latestRasterTileManifestPath}>
|
||||
Use in Detection Lab
|
||||
{latestRasterTileManifest ? (
|
||||
<dl className="raster-manifest-details">
|
||||
<div>
|
||||
<dt>Tile count</dt>
|
||||
<dd>{latestRasterTileManifest.count ?? 'n/a'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Tile size</dt>
|
||||
<dd>{latestRasterTileManifest.tile_size ?? 'n/a'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Overlap</dt>
|
||||
<dd>{latestRasterTileManifest.overlap ?? 'n/a'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Tile set</dt>
|
||||
<dd>{latestRasterTileManifest.tile_set_id ?? 'n/a'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Use in Detection Lab with current manifest"
|
||||
onClick={onUseTileManifestForDetection}
|
||||
disabled={!latestRasterTileManifestPath}
|
||||
>
|
||||
Use manifest in Detection Lab
|
||||
</button>
|
||||
<button type="button" onClick={onUseTileManifestForSegmentation} disabled={!latestRasterTileManifestPath}>
|
||||
Use in Segmentation Lab
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Use in Segmentation Lab with current manifest"
|
||||
onClick={onUseTileManifestForSegmentation}
|
||||
disabled={!latestRasterTileManifestPath}
|
||||
>
|
||||
Use manifest in Segmentation Lab
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -311,11 +311,20 @@ export function DetectionLab({
|
||||
<span>CUDA</span>
|
||||
<strong>{yoloPreflight.runtime.cuda_available === true ? 'available' : yoloPreflight.runtime.cuda_available === false ? 'not available' : 'not checked'}</strong>
|
||||
</div>
|
||||
<div className={yoloPreflight.checks.manifest_valid ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
|
||||
<span>Tile manifest validation</span>
|
||||
<strong>{yoloPreflight.checks.manifest_valid === true ? 'valid' : yoloPreflight.checks.manifest_path_set ? 'not valid' : 'not provided'}</strong>
|
||||
</div>
|
||||
<div className={yoloPreflight.tile_count > 0 ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
|
||||
<span>Tile count</span>
|
||||
<strong>{yoloPreflight.tile_count} / {yoloPreflight.max_tiles}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="entity-meta">
|
||||
<span>torch_version: {yoloPreflight.runtime.torch_version ?? 'n/a'}</span>
|
||||
<span>ultralytics_version: {yoloPreflight.runtime.ultralytics_version ?? 'n/a'}</span>
|
||||
<span>cuda_available: {String(yoloPreflight.runtime.cuda_available ?? 'unknown')}</span>
|
||||
<span>will_run_inference: {String(yoloPreflight.will_run_inference)}</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>
|
||||
@@ -437,6 +446,13 @@ export function DetectionLab({
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? (
|
||||
<div className="linked-manifest-card">
|
||||
<strong>Linked tile manifest</strong>
|
||||
<p>{detectionTileManifestPath}</p>
|
||||
<span>Refresh preflight after changing model asset or manifest path.</span>
|
||||
</div>
|
||||
) : null}
|
||||
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
|
||||
Run detection
|
||||
</button>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
AreaRead,
|
||||
DatasetCreateResponse,
|
||||
JobRead,
|
||||
RasterTileHandoff,
|
||||
RasterMetadataResponse,
|
||||
RasterPreviewResponse,
|
||||
RasterStatsResponse,
|
||||
@@ -28,12 +29,39 @@ function toRasterMetadata(metadata: Record<string, unknown> | null | undefined):
|
||||
}
|
||||
|
||||
function extractRasterTileManifestPath(job: JobRead | null | undefined): string {
|
||||
const manifest = toRasterTileHandoff(job)
|
||||
return manifest?.manifest_path ?? ''
|
||||
}
|
||||
|
||||
function optionalString(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value : null
|
||||
}
|
||||
|
||||
function optionalNumber(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||
}
|
||||
|
||||
function toRasterTileHandoff(job: JobRead | null | undefined): RasterTileHandoff | null {
|
||||
const result = job?.result_json
|
||||
if (!result) {
|
||||
return ''
|
||||
return null
|
||||
}
|
||||
const manifestPath = result.manifest_path
|
||||
return typeof manifestPath === 'string' ? manifestPath : ''
|
||||
if (typeof manifestPath !== 'string' || manifestPath.trim().length === 0) {
|
||||
return null
|
||||
}
|
||||
const manifest = typeof result.manifest === 'object' && result.manifest !== null
|
||||
? result.manifest as Record<string, unknown>
|
||||
: {}
|
||||
return {
|
||||
manifest_path: manifestPath,
|
||||
tile_set_id: optionalString(result.tile_set_id) ?? optionalString(manifest.tile_set_id),
|
||||
tile_size: optionalNumber(result.tile_size) ?? optionalNumber(manifest.tile_size),
|
||||
overlap: optionalNumber(result.overlap) ?? optionalNumber(manifest.overlap),
|
||||
count: optionalNumber(result.count) ?? optionalNumber(manifest.count),
|
||||
source_dataset_id: optionalString(result.source_dataset_id) ?? optionalString(manifest.source_dataset_id),
|
||||
created_at: optionalString(manifest.created_at),
|
||||
}
|
||||
}
|
||||
|
||||
export function useDatasetWorkflow({
|
||||
@@ -53,6 +81,7 @@ export function useDatasetWorkflow({
|
||||
const [jobs, setJobs] = useState<JobRead[]>([])
|
||||
const [rasterPreview, setRasterPreview] = useState<RasterPreviewResponse | null>(null)
|
||||
const [latestRasterTileManifestPath, setLatestRasterTileManifestPath] = useState('')
|
||||
const [latestRasterTileManifest, setLatestRasterTileManifest] = useState<RasterTileHandoff | null>(null)
|
||||
const [selectedIntersectTargetId, setSelectedIntersectTargetId] = useState('')
|
||||
const [selectedClipAreaId, setSelectedClipAreaId] = useState('')
|
||||
const [rasterTileSize, setRasterTileSize] = useState(512)
|
||||
@@ -113,7 +142,10 @@ export function useDatasetWorkflow({
|
||||
const latestRasterTileJob = response.items.find(
|
||||
(job) => job.job_type === 'raster.tile' && extractRasterTileManifestPath(job),
|
||||
)
|
||||
setLatestRasterTileManifestPath(extractRasterTileManifestPath(latestRasterTileJob))
|
||||
const manifest = toRasterTileHandoff(latestRasterTileJob)
|
||||
const manifestPath = manifest ? manifest.manifest_path : ''
|
||||
setLatestRasterTileManifest(manifest)
|
||||
setLatestRasterTileManifestPath(manifestPath)
|
||||
}
|
||||
|
||||
const loadDatasetDetails = async (projectId: string, dataset: DatasetCreateResponse) => {
|
||||
@@ -126,6 +158,7 @@ export function useDatasetWorkflow({
|
||||
setDatasetContent(null)
|
||||
setRasterPreview(null)
|
||||
setLatestRasterTileManifestPath('')
|
||||
setLatestRasterTileManifest(null)
|
||||
setSelectedDatasetId(dataset.id)
|
||||
setJobs([])
|
||||
try {
|
||||
@@ -372,10 +405,13 @@ export function useDatasetWorkflow({
|
||||
overlap: rasterTileOverlap,
|
||||
output_name: rasterTileOutputName || undefined,
|
||||
})
|
||||
const manifestPath = extractRasterTileManifestPath(job)
|
||||
const manifest = toRasterTileHandoff(job)
|
||||
const manifestPath = manifest?.manifest_path ?? ''
|
||||
setLatestRasterTileManifest(manifest)
|
||||
setLatestRasterTileManifestPath(manifestPath)
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
if (manifestPath) {
|
||||
setLatestRasterTileManifest(manifest)
|
||||
setLatestRasterTileManifestPath(manifestPath)
|
||||
}
|
||||
setDatasetDetailError(null)
|
||||
@@ -465,6 +501,7 @@ export function useDatasetWorkflow({
|
||||
setDatasetContent(null)
|
||||
setRasterPreview(null)
|
||||
setLatestRasterTileManifestPath('')
|
||||
setLatestRasterTileManifest(null)
|
||||
setJobs([])
|
||||
}
|
||||
|
||||
@@ -478,6 +515,7 @@ export function useDatasetWorkflow({
|
||||
jobs,
|
||||
rasterPreview,
|
||||
latestRasterTileManifestPath,
|
||||
latestRasterTileManifest,
|
||||
selectedIntersectTargetId,
|
||||
selectedClipAreaId,
|
||||
rasterTileSize,
|
||||
|
||||
@@ -3702,6 +3702,63 @@ button.entity-card {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.raster-manifest-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
|
||||
gap: 0.42rem;
|
||||
min-width: 0;
|
||||
margin: 0.2rem 0 0.35rem;
|
||||
}
|
||||
|
||||
.raster-manifest-details div {
|
||||
min-width: 0;
|
||||
border: 1px solid #d8e3de;
|
||||
border-radius: 6px;
|
||||
padding: 0.42rem 0.48rem;
|
||||
background: #f8fbf9;
|
||||
}
|
||||
|
||||
.raster-manifest-details dt {
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 850;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.raster-manifest-details dd {
|
||||
min-width: 0;
|
||||
margin: 0.12rem 0 0;
|
||||
color: var(--text);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 750;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.linked-manifest-card {
|
||||
display: grid;
|
||||
gap: 0.22rem;
|
||||
min-width: 0;
|
||||
border: 1px solid rgba(47, 125, 86, 0.24);
|
||||
border-radius: 7px;
|
||||
padding: 0.6rem 0.65rem;
|
||||
background: #f5fbf6;
|
||||
}
|
||||
|
||||
.linked-manifest-card strong {
|
||||
color: var(--text);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.linked-manifest-card p,
|
||||
.linked-manifest-card span {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.raster-readiness-item span,
|
||||
.raster-manifest-handoff span {
|
||||
color: var(--muted);
|
||||
|
||||
@@ -259,6 +259,16 @@ export interface RasterTileResponse {
|
||||
manifest: RasterTileManifest
|
||||
}
|
||||
|
||||
export interface RasterTileHandoff {
|
||||
manifest_path: string
|
||||
tile_set_id?: string | null
|
||||
tile_size?: number | null
|
||||
overlap?: number | null
|
||||
count?: number | null
|
||||
source_dataset_id?: string | null
|
||||
created_at?: string | null
|
||||
}
|
||||
|
||||
export interface VectorBBoxResponse {
|
||||
dataset_id: string
|
||||
bounds_json: Record<string, number> | null
|
||||
|
||||
Reference in New Issue
Block a user