Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ argument for branching on `code`.
| **404** | `PROJECT_NOT_FOUND` · `SCHEMA_NOT_FOUND` · `SCHEMA_DRAFT_NOT_FOUND` · `BATCH_NOT_FOUND` · `JOB_NOT_FOUND` · `INGEST_JOB_NOT_FOUND` · `BACKGROUND_JOB_NOT_FOUND` · `ASSET_NOT_FOUND` · `SOURCE_NOT_FOUND` · `DATASET_NOT_FOUND` · `ANNOTATION_NOT_FOUND` · `RELEASE_NOT_FOUND` · `TOKEN_NOT_FOUND` · `INFERENCE_CONNECTION_NOT_FOUND` · `ASSET_NOT_IN_JOB` · `NO_SPLIT_RECIPE` · `EXPORT_FORMAT_NOT_FOUND` · `THUMBNAIL_NOT_CACHED` · `NOT_FOUND` (no such route) |
| **405** | `METHOD_NOT_ALLOWED` |
| **409** | `PROJECT_NAME_TAKEN` · `RELEASE_TAG_TAKEN` · `TOKEN_NAME_TAKEN` · `INFERENCE_CONNECTION_NAME_TAKEN` · `WORKSPACE_ALREADY_EXISTS` · `WORKSPACE_NOT_EMPTY` · `SCHEMA_VERSION_CONFLICT` · `INVALID_TRANSITION` · `STALE_WRITE` · `BATCH_NOT_EDITABLE` · `BATCH_IMMUTABLE` · `BATCH_NOT_IN_ANNOTATION` · `ASSET_NOT_WRITABLE` · `JOB_FINISHED` · `BATCH_NOT_COMPLETE` · `JOB_NOT_COMPLETE` · `EMPTY_BATCH` · `EMPTY_RELEASE` · `CONFIRMATION_REQUIRED` · `DESTRUCTIVE_SCHEMA_CHANGE` · `SCHEMA_CHANGE_WOULD_ORPHAN` · `SCHEMA_HAS_NO_DETECTABLE_CLASS` · `UNSERIALIZABLE_MANIFEST` · `LOSSY_EXPORT_NOT_CONSENTED` · `EXPORT_SOURCE_UNREADABLE` · `INFERENCE_CONNECTION_NOT_DOWNLOADABLE` · `INFERENCE_CONNECTION_NOT_CHECKABLE` · `WEIGHTS_DAMAGED` · `INFERENCE_CONNECTION_NOT_SET_UP` |
| **422** | `VALIDATION_ERROR` · `ASSET_NOT_IN_BATCH` · `ANNOTATION_NOT_FROM_MODEL` · `INVALID_NAME` · `INFERENCE_CONNECTION_INVALID` · `INVALID_SCHEMA` · `UNSUPPORTED_GEOMETRY` · `INVALID_ANNOTATION` · `LABEL_CLASS_NOT_IN_SCHEMA` · `DISALLOWED_GEOMETRY` · `DUPLICATE_CLASSIFICATION_TAG` · `MISSING_REQUIRED_ATTRIBUTE` · `UNKNOWN_ATTRIBUTE` · `INVALID_ATTRIBUTE_VALUE` · `INVALID_PARTITION` · `UNKNOWN_JOB_TYPE` · `MEDIA_ERROR` · `UNSUPPORTED_MEDIA` · `CORRUPT_MEDIA` · `UNSUPPORTED_PROMPT` · `PROMPT_POINT_OUT_OF_BOUNDS` |
| **422** | `VALIDATION_ERROR` · `ASSET_NOT_IN_BATCH` · `ANNOTATION_NOT_FROM_MODEL` · `INVALID_NAME` · `INFERENCE_CONNECTION_INVALID` · `INVALID_SCHEMA` · `UNSUPPORTED_GEOMETRY` · `INVALID_ANNOTATION` · `LABEL_CLASS_NOT_IN_SCHEMA` · `DISALLOWED_GEOMETRY` · `ANNOTATION_GEOMETRY_OUT_OF_BOUNDS` · `DUPLICATE_CLASSIFICATION_TAG` · `MISSING_REQUIRED_ATTRIBUTE` · `UNKNOWN_ATTRIBUTE` · `INVALID_ATTRIBUTE_VALUE` · `INVALID_PARTITION` · `UNKNOWN_JOB_TYPE` · `MEDIA_ERROR` · `UNSUPPORTED_MEDIA` · `CORRUPT_MEDIA` · `UNSUPPORTED_PROMPT` · `PROMPT_POINT_OUT_OF_BOUNDS` |
| **503** | `WORKSPACE_BUSY` |
| **500** | `WORKSPACE_CORRUPT` · `NOT_A_WORKSPACE` · `WORKSPACE_FORMAT_TOO_NEW` · `WORKSPACE_SCHEMA_MISMATCH` · `ENTITY_NOT_FOUND` · `ENTITY_ALREADY_EXISTS` · `CONSTRAINT_VIOLATED` · `MEDIA_TOOL_UNAVAILABLE` · `LOCAL_INFERENCE_UNAVAILABLE` · `INFERENCE_CONNECTION_NOT_RUNNABLE` · `INFERENCE_OUT_OF_MEMORY` · `INTERNAL_ERROR` |

Expand Down
11 changes: 7 additions & 4 deletions docs/batches.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,10 @@ text-prompted detector answers with text decoded from spans over that prompt rat
choice from the list, so an answer naming one of the classes, matched case-insensitively, is
written under the schema's own spelling, and an answer naming none of them - a span that
crossed the boundary between two phrases, most often - is discarded rather than guessed onto
either half; the outcome's `regions_discarded` says how many. A schema with no such class is
refused up front; see [inference.md](inference.md#what-a-connection-can-be-asked-for).
either half; the outcome's `regions_discarded` says how many. A mapped region whose geometry has
no overlap with a measured asset is discarded separately, and `regions_out_of_bounds` says how
many; unmeasured assets remain eligible. A schema with no such class is refused up front; see
[inference.md](inference.md#what-a-connection-can-be-asked-for).

**What lands enters at `pre_labeled`, never `annotated`.** Nobody judged it, so it arrives in its
own editable state rather than claiming to be somebody's work - see
Expand Down Expand Up @@ -360,8 +362,9 @@ most recent `annotation.pre_label` job naming this batch - live or settled - and
reload, a second tab or a run started at a terminal can only be shown by the batch itself saying
so. Counted in assets, the unit this handler works in, and carrying the outcome
`prelabel.py`'s `run` returns once the job has settled - `stopped_early`, `assets_labeled`,
`regions_discarded` - so a client can tell a cancelled run from an untouched batch. Derived, never
stored, and published on `BatchOut` as `pre_label_run`, `null` where none ever ran.
`regions_discarded`, `regions_out_of_bounds` - so a client can tell a cancelled run from an
untouched batch. Derived, never stored, and published on `BatchOut` as `pre_label_run`, `null`
where none ever ran.

## What approval and completion announce

Expand Down
3 changes: 2 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,8 @@ interrupted run. The batch id goes to stdout.
`pre-label BATCH_ID CONNECTION [--minimum-confidence FLOAT]` blocks and calls
`visionset.inference.pre_label` inline because a terminal has no dispatcher. Progress and the
summary are written to stderr; normal stdout contains `annotations_written`. With `--json`, the
command prints the complete outcome instead.
command prints the complete outcome instead, including `regions_discarded` for unmappable model
labels and `regions_out_of_bounds` for mapped regions without overlap with a measured asset.

`--jobs-of N` is the `BySize` partition; with no flag the batch becomes one job. There is no
`batch create` and no membership editing: a batch is born from an ingest. See
Expand Down
3 changes: 3 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ retry resumes the cache rather than starting over; a cut-off pre-labeling call h
the assets it fully entered, one commit per asset, so calling it again resumes with whatever is
still untouched.

`pre_label_batch` reports unmappable model labels as `regions_discarded` and mapped regions
without overlap with a measured asset as `regions_out_of_bounds`.

There is therefore no ingest polling, and no `resume_ingest`. If a call is cut off part way, call
`ingest` again - registration is idempotent on `(kind, path, extraction_fps)` and content
addressing means the re-run creates nothing it created before. That is the same argument that
Expand Down
13 changes: 8 additions & 5 deletions frontend/ui-core/src/generated/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3063,9 +3063,9 @@ export interface components {
* @description An axis-aligned rectangle: top-left corner plus size.
*
* ``width`` and ``height`` must be strictly positive — a zero-area box is as
* meaningless as a negative one, so neither is accepted. ``x`` and ``y`` are
* unconstrained: an annotation may legitimately start outside the asset's
* bounds when an object is clipped by the frame edge.
* meaningless as a negative one, so neither is accepted. A box may extend
* beyond an asset's frame, but cannot be wholly disjoint when that asset
* records its dimensions.
*/
BboxGeometry: {
/** Height */
Expand Down Expand Up @@ -3961,8 +3961,9 @@ export interface components {
* set before the first forward pass, so both its progress and its total are
* counted in the unit its own work is over.
*
* **The outcome, once the job has one.** `stopped_early`, `assets_labeled` and
* `regions_discarded` are the handler's own account of what a settled run did.
* **The outcome, once the job has one.** `stopped_early`, `assets_labeled`,
* `regions_discarded` and `regions_out_of_bounds` are the handler's own
* account of what a settled run did.
* They are `null` while the job is still `queued` or `running`, and `null`
* where it ended `failed` before producing one — but a `cancelled` run still
* carries them: stopping partway is a coherent outcome for a handler whose
Expand All @@ -3984,6 +3985,8 @@ export interface components {
job_id: string;
/** Regions Discarded */
regions_discarded: number | null;
/** Regions Out Of Bounds */
regions_out_of_bounds: number | null;
state: components["schemas"]["BackgroundJobState"];
/** Stopped Early */
stopped_early: boolean | null;
Expand Down
2 changes: 1 addition & 1 deletion frontend/ui-core/src/generated/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export const checkBatchState: Check<Schemas["BatchState"]> =
/*#__PURE__*/ oneOf(["draft", "approved", "in_annotation", "completed"] as const);

export const checkPreLabelRunOut: Check<Schemas["PreLabelRunOut"]> =
/*#__PURE__*/ object({ "assets_labeled": [true, either([isInteger, isNull] as const)], "assets_processed": [true, isInteger], "assets_total": [true, either([isInteger, isNull] as const)], "error": [true, either([isString, isNull] as const)], "job_id": [true, isString], "regions_discarded": [true, either([isInteger, isNull] as const)], "state": [true, checkBackgroundJobState], "stopped_early": [true, either([isBoolean, isNull] as const)] } as const);
/*#__PURE__*/ object({ "assets_labeled": [true, either([isInteger, isNull] as const)], "assets_processed": [true, isInteger], "assets_total": [true, either([isInteger, isNull] as const)], "error": [true, either([isString, isNull] as const)], "job_id": [true, isString], "regions_discarded": [true, either([isInteger, isNull] as const)], "regions_out_of_bounds": [true, either([isInteger, isNull] as const)], "state": [true, checkBackgroundJobState], "stopped_early": [true, either([isBoolean, isNull] as const)] } as const);

export const checkProgressCounts: Check<Schemas["ProgressCounts"]> =
/*#__PURE__*/ object({ "accepted": [true, isInteger], "annotated": [true, isInteger], "pre_labeled": [true, isInteger], "review_pending": [true, isInteger], "skipped": [true, isInteger], "total": [true, isInteger], "unannotated": [true, isInteger] } as const);
Expand Down
43 changes: 30 additions & 13 deletions frontend/ui-core/src/screens/PreLabelDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ interface RunView {
readonly stoppedEarly: boolean | null;
readonly assetsLabeled: number | null;
readonly regionsDiscarded: number | null;
readonly regionsOutOfBounds: number | null;
}

/**
Expand All @@ -148,6 +149,7 @@ function viewFromJob(job: BackgroundJob): RunView {
const stoppedEarly = job.result.stopped_early;
const assetsLabeled = job.result.assets_labeled;
const regionsDiscarded = job.result.regions_discarded;
const regionsOutOfBounds = job.result.regions_out_of_bounds;
return {
jobId: job.id,
state: job.state,
Expand All @@ -157,6 +159,7 @@ function viewFromJob(job: BackgroundJob): RunView {
stoppedEarly: typeof stoppedEarly === "boolean" ? stoppedEarly : null,
assetsLabeled: typeof assetsLabeled === "number" ? assetsLabeled : null,
regionsDiscarded: typeof regionsDiscarded === "number" ? regionsDiscarded : null,
regionsOutOfBounds: typeof regionsOutOfBounds === "number" ? regionsOutOfBounds : null,
};
}

Expand All @@ -171,6 +174,7 @@ function viewFromRun(run: PreLabelRun): RunView {
stoppedEarly: run.stopped_early,
assetsLabeled: run.assets_labeled,
regionsDiscarded: run.regions_discarded,
regionsOutOfBounds: run.regions_out_of_bounds,
};
}

Expand Down Expand Up @@ -223,25 +227,36 @@ function failedProgress(view: RunView): string | null {
}

/** What a settled run actually did, in words — including the one count no other UI shows. */
function DoneSummary({ result }: { readonly result: BackgroundJob["result"] }): JSX.Element {
const labeled = typeof result.assets_labeled === "number" ? result.assets_labeled : 0;
const written = typeof result.annotations_written === "number" ? result.annotations_written : 0;
const discarded = typeof result.regions_discarded === "number" ? result.regions_discarded : 0;
const skipped = typeof result.assets_skipped === "number" ? result.assets_skipped : 0;
const stoppedEarly = result.stopped_early === true;
function DoneSummary({
view,
result,
}: {
readonly view: RunView;
readonly result: BackgroundJob["result"] | null;
}): JSX.Element {
const labeled = typeof result?.assets_labeled === "number" ? result.assets_labeled : 0;
const written = typeof result?.annotations_written === "number" ? result.annotations_written : 0;
const discarded = view.regionsDiscarded ?? 0;
const outOfBounds = view.regionsOutOfBounds ?? 0;
const skipped = typeof result?.assets_skipped === "number" ? result.assets_skipped : 0;
const stoppedEarly = result?.stopped_early === true;

return (
<div className="flex flex-col gap-1" data-testid="prelabel-summary">
<p className="text-body text-foreground">
Labeled {labeled} asset{labeled === 1 ? "" : "s"}, writing {written} region
{written === 1 ? "" : "s"} for you to correct.
</p>
{result !== null && (
<p className="text-body text-foreground">
Labeled {labeled} asset{labeled === 1 ? "" : "s"}, writing {written} region
{written === 1 ? "" : "s"} for you to correct.
</p>
)}
{discarded > 0 && (
<p className="text-meta text-muted-foreground" data-testid="prelabel-discarded">
Discarded {discarded} region{discarded === 1 ? "" : "s"} for naming a class the
prompt did not ask for.
{discarded} model region{discarded === 1 ? " did" : "s did"} not match a requested class.
</p>
)}
{outOfBounds > 0 && (
<p>{`${outOfBounds} model region${outOfBounds === 1 ? " was" : "s were"} outside their asset and were skipped.`}</p>
)}
{skipped > 0 && (
<p className="text-meta text-muted-foreground">
Skipped {skipped} asset{skipped === 1 ? "" : "s"} that work had already started on
Expand Down Expand Up @@ -510,7 +525,9 @@ function PreLabelDialog({
</p>
)}

{mode === "done" && launched !== null && <DoneSummary result={launched.result} />}
{mode === "done" && view !== null && (
<DoneSummary view={view} result={launched?.result ?? null} />
)}

{mode === "stopped" && view !== null && (
<p className="text-body text-foreground" data-testid="prelabel-stopped-summary">
Expand Down
17 changes: 14 additions & 3 deletions frontend/ui-core/src/screens/preLabel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ function preLabelRunOf(overrides: Record<string, unknown> = {}): Record<string,
stopped_early: true,
assets_labeled: 9,
regions_discarded: 0,
regions_out_of_bounds: 0,
...overrides,
};
}
Expand Down Expand Up @@ -462,7 +463,7 @@ it("offers Edit these frames once a run succeeds, and sets the segment filter",
expect(screen.getByTestId("segment-pre_labeled").getAttribute("aria-pressed")).toBe("true");
});

it("says how many regions a run discarded, in words, when it discarded any", async () => {
it("separates unmappable and out-of-bounds model regions in a completed job", async () => {
on("GET", /\/background-jobs\//, {
status: 200,
body: backgroundJobOf({
Expand All @@ -471,6 +472,7 @@ it("says how many regions a run discarded, in words, when it discarded any", asy
assets_labeled: 40,
annotations_written: 90,
regions_discarded: 3,
regions_out_of_bounds: 2,
assets_skipped: 0,
stopped_early: false,
},
Expand All @@ -481,8 +483,9 @@ it("says how many regions a run discarded, in words, when it discarded any", asy
await userEvent.click(await screen.findByRole("button", { name: /pre-label/i }));
await userEvent.click(await screen.findByRole("button", { name: /start/i }));

const discarded = await screen.findByTestId("prelabel-discarded");
expect(discarded.textContent).toMatch(/3 regions/);
const summary = await screen.findByTestId("prelabel-summary");
expect(summary.textContent).toContain("3 model regions did not match a requested class");
expect(summary.textContent).toContain("2 model regions were outside their asset and were skipped");
});

it("says nothing about discarded regions when a run discarded none", async () => {
Expand All @@ -494,6 +497,7 @@ it("says nothing about discarded regions when a run discarded none", async () =>
assets_labeled: 40,
annotations_written: 90,
regions_discarded: 0,
regions_out_of_bounds: 0,
assets_skipped: 0,
stopped_early: false,
},
Expand All @@ -507,6 +511,8 @@ it("says nothing about discarded regions when a run discarded none", async () =>
await screen.findByRole("button", { name: /edit these frames/i });
expect(screen.queryByTestId("prelabel-discarded")).toBeNull();
expect(screen.queryByText(/^0$/)).toBeNull();
expect(screen.queryByText(/did not match a requested class/i)).toBeNull();
expect(screen.queryByText(/outside their asset and were skipped/i)).toBeNull();
});

/**
Expand Down Expand Up @@ -556,6 +562,7 @@ it("on reopen after a failed run, shows the handler's error and offers Try again
stopped_early: null,
assets_labeled: null,
regions_discarded: null,
regions_out_of_bounds: null,
}),
},
{ counts: { unannotated: 43, total: 48 } },
Expand Down Expand Up @@ -583,6 +590,7 @@ it("on reopen after a complete run, disables Start with its reason adjacent and
stopped_early: false,
assets_labeled: 48,
regions_discarded: 0,
regions_out_of_bounds: 2,
}),
},
{ counts: { unannotated: 0, total: 48 } },
Expand All @@ -594,6 +602,9 @@ it("on reopen after a complete run, disables Start with its reason adjacent and
expect(start.disabled).toBe(true);
const reason = await screen.findByTestId("prelabel-blocked-reason");
expect(reason.textContent).toMatch(/pre-labeled/i);
expect((await screen.findByTestId("prelabel-summary")).textContent).toContain(
"2 model regions were outside their asset and were skipped",
);

const edit = await screen.findByRole("button", { name: /edit these frames/i });
await userEvent.click(edit);
Expand Down
Loading
Loading