Local annotation

Annotates a Signal Dataset snapshot on the machine you run RFGen from, publishing the annotation set beside it. execution_mode: local is the default; Annotate an existing dataset is the task page, and this one covers the run in detail — subsetting, failures, and resume.

Prepare

uv pip install -e '.[gemini]'   # omitting this fails with BackendUnavailableError
export GEMINI_API_KEY=...       # omitting this fails in the provider SDK: ValueError: No API key was provided

The run is synchronous: it publishes the annotation set or reports an error in your shell. No polling, no Spark session, no Java runtime.

Configure

To annotate the whole store, omit requests. That is the ordinary case at any size:

execution_mode: local
annotation_type: caption
template_id: caption.declared.v1
run_id: caption-v1
model: gemini-3.1-flash-lite
backend: local_concurrent
signal_dataset:
  dataset_uri: /absolute/path/rfgen-output   # the snapshot root holding root.json

Naming records is for annotating a subset on purpose. Sample IDs are 64-character SHA-256 hex strings, so list real ones rather than inventing them:

rfgen inspect /absolute/path/rfgen-output --sample-size 3 --sample-ids
signal_dataset:
  dataset_uri: /absolute/path/rfgen-output
  requests:
    - {sample_id: 9cd73142fc0a..., annotation_type: caption, template_id: caption.declared.v1, run_id: caption-v1}

For a dataset rather than a smoke test, switch the backend, for many records per provider job, roughly half the price:

backend: gemini_batch

For a local model server or a lower-cost hosted endpoint, switch the provider. Both settings below matter: the default 60-second timeout expires while a large model loads, and one GPU serves one request at a time.

backend: local_concurrent
model: gemma4
inference:
  provider: openai_compatible
  timeout_s: 600
  concurrency: 1
  endpoints: [{base_url: "http://localhost:11434/v1", model: gemma4}]

Run

rfgen annotate submit --config-dir ./annotation-config --config-name local-caption

Exit

Meaning

0

complete, every record the run attempted succeeded. A subset run’s skipped rows do not affect this.

2

complete_with_errors, some records failed; the rest are written

1

Stopped before any terminal result; a JSON error_code on stderr

Read it back

The run publishes to <dataset_uri>/annotations/sets/<run_id>/. Read it with the signal_dataset package against the same root:

import signal_dataset as sd

annotations = sd.open("/absolute/path/rfgen-output").annotations["caption-v1"]
print(annotations[0].status, annotations[0].output)

The set is named by your run_id, not by the template, and is dense: one row per record in the snapshot, carrying success, failed, or skipped.

Resume

Re-submit the same run_id. Records that already have a complete row are skipped without calling the model again:

rfgen annotate submit --config-dir ./annotation-config --config-name local-caption

A row written by a different provider is refused rather than adopted: treating it as this run’s work would turn a retry into an overwrite.

When records fail

A complete_with_errors run lists what it could not annotate under result.failures, each with sample_id, error_code, and a message. That message is the provider’s own only when the failure came from outside the inference client; a failure the client raises itself, transport or schema alike, repeats its code instead. The list stops after 20; terminal_counts always carries the exact total, so a wholesale failure is visible even when the sample is truncated.

Two codes account for nearly every local failure:

error_code

Means

Do

provider_error

The call returned no usable reply: refused, unreachable, or retries exhausted against rate limiting. The submission is fine.

Re-run, subject to the publication rule below. Whole run failing: check GEMINI_API_KEY is exported and model is one your key can reach. A fraction failing: lower inference.concurrency, or switch to backend: gemini_batch, which makes far fewer calls for the same dataset.

annotation_schema_invalid

The model replied and the reply did not match the template’s output schema after the client’s retries. A model-capability signal, not a transport one.

Try a more capable model. Re-submitting unchanged usually reproduces it.

vertex_<rpc_code>

An SDK exception normalized to its gRPC status, lowercased: vertex_unavailable, vertex_resource_exhausted, vertex_deadline_exceeded, and the rest of the standard set, plus vertex_<number> for a numeric status. The vertex_ prefix names the normalizer, not the provider, so a Gemini call produces these too.

vertex_unavailable, vertex_resource_exhausted, and vertex_deadline_exceeded are retried inside the run; seeing them on a terminal row means the retries were exhausted. Re-run, subject to the publication rule below.

vertex_unknown

The catch-all. An exception carrying no gRPC status, or one whose status is outside the standard set, normalizes to UNKNOWN rather than to anything descriptive. It is not a claim that the provider said “unknown”.

The code carries no diagnostic information, so read the driver logs rather than the code. It is not retried in-run, so a transient fault reaches you as terminal. Re-run, subject to the publication rule below.

A published set cannot be repaired

Re-running the same run_id does not retry a failed row on the Signal Dataset route. A published annotation set is immutable, so an unchanged configuration is the idempotent no-op: it returns the same counts in seconds, having made no provider call. That is deliberate, and it is the rule stated under Annotate an existing dataset.

Same run_id resumes only a killed attempt, whose work was staged and never published. Once the set exists, the choice is:

  • Give the new run a new run_id. It re-annotates every record, including the ones that succeeded, and publishes a second set beside the first.

  • Leave the failures in place and filter them out downstream on row.status == "success".

There is no way to repair individual rows inside a published set. Fix the cause first, so the second run does not reproduce the failure: a provider_error fraction usually means inference.concurrency is too high for the key.

A failed row carries its code in detail_status and its values are empty, so the code is all you get from the row itself. Find the affected records by re-opening the published set and filtering on row.status == "failed". Do that rather than reading result.failures on a remote run: the dataproc_serverless route reports failures: null and returns counts only. result.failures is populated on the local route.