Annotations

An annotation is text supervision attached to a record that already exists. It never rewrites I/Q samples, structured labels, or source metadata. It writes a new layer beside them.

Labels come from the generator and are exact. Annotations come from a language model and are prose. The concept below is how rfgen keeps the two apart – not how it keeps the prose grounded, which it cannot.

The flow

The flow below is the Signal Dataset route, which is the default. The two older sources, local HDF5 and remote WebDataset, share every step except the last.

        flowchart LR
    subgraph local["Your machine, or the Dataproc driver"]
        A["Record metadata<br/>declared by the generator"] --> B["Evidence report<br/>bounded, deterministic"]
        B --> C["Prompt"]
        B --> J["Claims<br/>projected from the same evidence"]
        E["Caption"] --> F["Parse + declared schema"]
        F -->|valid| G["Row: caption + claims"]
        F -->|invalid| H["Failed row<br/>error code, no prose"]
        J --> G
        G --> I["Annotation output<br/>dense set, overlay, or sidecars"]
        H --> I
    end
    subgraph provider["Provider"]
        D(["Language model"])
    end
    C -->|evidence JSON only| D
    D --> E
    

Four properties of that flow are the concept:

  1. The model never sees the record. It sees an evidence report: a bounded, deterministic projection built locally. Raw I/Q, spectrograms, seeds and dataset names do not cross the boundary, and neither does the generator’s own vocabulary – the internal type a value came from, the function that derived it, the words “declared by generator”.

    No record identifier crosses either. A staged batch does need a key to pair each reply back to its request, and that key is the record’s position in the batch — r00000000, r00000001 — not its id. Using the id was tried and was wrong twice over: a record id is a content digest, so identical records shared one and replies landed on the wrong rows, and refusing such a corpus meant the production path could not annotate it at all.

    The non-numeric prefix is load-bearing. Vertex Batch reads a purely numeric key as a number and echoes it back normalized — "00000000" returns as "0" — so a bare ordinal cannot be paired against what was sent. That is visible only through a real provider; every local double echoes faithfully.

    What does cross on that leg is the job’s own deployment configuration: the staging bucket URI, the run id in that path, and the GCP project and location. A batch job cannot exist without them, and none names a record, a dataset, or a generator.

  2. Machine-readable claims are written locally, beside the prose. The row carries a claims object projected from the same evidence, in this process, before the request was sent – so no value in it was authored by a model. It is not the evidence report: the bookkeeping saying how this generator knew each fact is dropped, because a deployed model has no such generator to name.

  3. The prose is not checked, and cannot be. rfgen checks that the reply parses and matches the schema the annotator declared. It does not check that a sentence is true, because no mechanical check over text can: a model may restate a value in another unit, name a different quantity, invert a relation, or invent a property. A caption may contradict the claims in its own row. Every row says so, in provenance.prose_verification. Judge the prose with a stronger model, ad hoc, outside the pipeline – the row carries the truth beside it, so that judgement is cheap.

  4. On a snapshot, every record gets a row. A failed record produces a failed row and a record outside the requested subset a skipped one, so a consumer never has to distinguish “not annotated” from “annotated and failed”. The two retained sources write only the records they annotated.

Where the annotation goes

source_kind selects what rfgen reads, and defaults to the snapshot rfgen generate writes:

source_kind: signal_dataset   # the default; omit it and you get this

One annotation product ships: a scene report, from scene_report.declared.v1.

Source

Output

Shape

signal_dataset (default)

A dense annotation set under the snapshot root

One row per source ordinal, success / failed / skipped

Density is the property that matters downstream: a reader never has to distinguish “not annotated” from “annotated and failed”. Read a set back shows the row counts a published set reports.

An annotation set name is permanent. Republishing identical bytes is idempotent; different bytes under the same name stop the run rather than overwrite it. Choose a new run_id instead.

The complete runnable configuration is on the task page: Annotation.

Evidence comes from the generator, never from the waveform

scene_report.declared.v1 builds its evidence from generator-declared metadata. The annotation source contract exposes read_metadata and no payload accessor, so no annotation path can reach a waveform.

That makes the evidence exact by construction: component count equals declared emitter count, up to the configured component_cap.

An earlier template measured its evidence out of the I/Q with an STFT. It was removed: a thresholded spectrogram merges co-channel emitters into one component, fragments a hopping emitter into several, and under-reports SNR for anything wider than one FFT bin. Every one of those is a wrong training label, and the generator already knows what it placed.

Provider neutrality

The extension contract is provider-neutral. A snapshot or local HDF5 run can select any client registered under rfgen.inference_clients through the local_concurrent backend, including a local model server:

inference:
  provider: openai_compatible
  endpoints: [{base_url: "http://localhost:11434/v1", model: gemma4}]

Hosted-model output is not assumed byte-identical across calls or over time. Provider work is manually gated, and every bounded qualification is recorded under Evidence. Those runs establish structural behaviour for the combinations they name, not semantic quality and not fleet reliability.

Warning

To understand

See

Where the loop runs and how the model is called

Annotation execution

Exact evidence from generator metadata

Declared-metadata evidence

How to run one

Annotation

Row and schema contracts

Annotation templates