# Intake Contract

Before any converter or C++ work starts, a port produces an **intake packet**:
a machine-readable `intake.json` plus a draft family note. The intake is the
single artifact that an agent, the maintainer, and future reviewers all agree
on before implementation begins.

The intake exists because STT porting has several silent-failure modes
(dtype drift, frontend config mismatch, tokenizer ID misalignment) that are
cheap to detect at the start of a port and expensive to debug later. Capturing
them in a structured artifact up front turns "I think the frontend is 16 kHz"
into "the intake says sample_rate=16000, verified against the reference's
preprocessor_config.json."

## Artifacts

- **Machine-readable:** `reports/porting/<family>/<variant>/intake.json`
- **Human-readable:** `docs/porting/families/<family>.md` (draft, pre-filled
  from `_template.md` using values from intake.json)
- **Schema:** `docs/porting/families/_intake-schema.json`

The JSON artifact is validated against the schema. The family note is the
reviewable summary.

## Scripted vs Manual Intake

`scripts/intake.py` is a bootstrapper, not the authority. It reads standard
HF metadata when available and writes a **draft** intake. For STT families,
canonical behavior may live outside HF metadata: NeMo, an author repo, or a
provider-supported code path.

A complete intake may be:

- mostly generated by `scripts/intake.py`
- generated and then manually edited
- written manually from `_intake-schema.json` when repo metadata is
  insufficient

The authority is maintainer sign-off on a schema-valid intake plus a family
note explaining the reference-framework decision. The script never chooses
the canonical reference framework by itself.

## Workflow

1. Agent or contributor runs the mechanical inspection:

   ```bash
   uv run scripts/intake.py inspect --repo Qwen/Qwen3-ASR-8B \
     --family qwen3_asr \
     --variant 8B \
     --out reports/porting/qwen3_asr/8B/intake.json
   ```

   This writes a draft intake with mechanical fields (variant, source
   metadata, dtype distribution, tokenizer summary, frontend facts,
   safetensors header dtype distribution) and leaves the human-judgment
   fields as `null`.

2. Human or agent-with-human reviews the reference modeling code
   (`modeling_<model_type>.py` for Transformers, NeMo source for NeMo) and
   fills in the null fields:
   - `reference_framework` — pick from the decision table in
     `1-reference-research.md`
   - `reference_rationale` — why this framework, not the alternatives
   - `architecture_pattern` — pick from the candidates in the intake
   - `known_risks` — anything the reference code does that isn't in the
     candidates list

3. Maintainer reviews the completed intake. Sign-off means converter and C++
   work can begin.

4. Intake runs through **preflight Gate A** (see the Preflight Gates
   section below and `scripts/preflight.py`) to catch
   declared-value-vs-reference mismatches before cascading into converter
   work.

## Field Reference

### Identity

| Field | Filled by | Notes |
|---|---|---|
| `family` | script | Stable family key (see `0-porting.md` naming) |
| `hf_repo` | script | Full HF repo id |
| `hf_revision` | script | Commit SHA at the moment of intake. Pin this — not `main` |
| `variants[]` | script + human | Records the CLI-provided variant by default. Multi-variant discovery (siblings on the Hub with their memory estimates and files) is future work — for now, add sibling variants manually to the array if the family ships multiple sizes |

### Sources

| Field | Filled by | Notes |
|---|---|---|
| `sources` | script + human | Map of source labels to `kind`, `path`, `status`, and optional `detail`. This records whether facts came from HF files, HF APIs, reference code, or manual research |

For manual intakes, fill `sources` with the same spirit: name the files,
APIs, docs, or reference-code paths used to justify the fields. If a source
is missing and the gap is accepted, mark it in `intake_gaps` and explain the
fallback in the family note.

### Config

| Field | Filled by | Notes |
|---|---|---|
| `config.architecture_candidates` | script | Heuristic matches against the four patterns (encoder-transducer, encoder-decoder, audio-LLM, encoder+CTC). Human picks one in `architecture_pattern` |
| `config.key_fields` | script | Selected config.json values that drive sizing (d_model, n_layers, etc.) |
| `config.varying_across_variants` | script | Keys whose values differ across variants in the family. Flags things the C++ implementation must handle conditionally |

### Dtype

| Field | Filled by | Notes |
|---|---|---|
| `dtype.expected` | script + human | Reviewed expected compute/storage dtype for the first accuracy GGUF. Usually from config; if absent, from the dominant floating dtype in safetensors headers. Null when unresolved |
| `dtype.source` | script + human | `"config"`, `"weights_header"`, `"manual"`, or `"unresolved"` |
| `dtype.evidence` | script + human | Human-readable basis for `dtype.expected`, such as `config.torch_dtype=bfloat16`, safetensors header counts, or a reference-code note |
| `dtype.details` | script | Optional machine-readable evidence such as `config_declared` and `header_distribution`. Preflight does not consume this |
| `dtype.expected_f32_tensors` | human | Names (or patterns) of tensors expected to remain F32 in a non-F32 model (norms, biases) |

If config declares a dtype, use that. If config does not declare one but
safetensors headers are available, the script may infer `dtype.expected` from
the dominant floating-point weight dtype. Integer and bool buffers are ignored
for this inference. This follows the MLX conversion workflow: resolve an
expected dtype, record where it came from, and use that value for downstream
checks. It is evidence, not authority — human intake sign-off may set
`source: "manual"` and explain the reference-code basis in `dtype.evidence`.

A single stray F32 weight in a BF16 model silently promotes the forward pass
to F32 via dtype promotion. Inference is 5× slower with no error. Preflight
Gate B re-checks dtype consistency between intake, converter output, and
reference to catch this before C++ work starts.

### Frontend

| Field | Filled by | Notes |
|---|---|---|
| `frontend.sample_rate` | script | From `preprocessor_config.json` |
| `frontend.n_mels` | script | Mel filterbank size |
| `frontend.hop_length` | script | Frames between windows, in samples |
| `frontend.fft_size` | script | FFT size / win_length |
| `frontend.window` | script | `hann_periodic`, `hann_symmetric`, `hamming`, or framework-specific label |
| `frontend.normalization` | script | `per_feature`, `global`, `per_utterance`, `none` |
| `frontend.preemphasis` | script | Coefficient, or `null` if not applied |
| `frontend.dither` | script | Epsilon, or `null` / `0.0` at inference |
| `frontend.center` | script | STFT `center=True` pads signal; `False` does not |
| `frontend.padding_mode` | script | `reflect`, `zero`, `constant` |
| `frontend.mel_filterbank_norm` | script | `slaney` or `htk` |

Frontend facts are the most common STT bug source. All fields get cross-checked
in preflight (intake vs GGUF KV vs reference preprocessor_config). See the
Frontend and Mel/STFT section in
[`4a-numerical-troubleshooting.md`](4a-numerical-troubleshooting.md#frontend-and-melstft)
for the specific trap list, including mel off-by-one errors.

### Tokenizer

| Field | Filled by | Notes |
|---|---|---|
| `tokenizer.type` | script | `sentencepiece`, `bpe`, `wordpiece` |
| `tokenizer.vocab_size` | script | From the tokenizer files |
| `tokenizer.special_tokens` | script | Mapping: `bos`, `eos`, `pad`, `unk`, `mask` (omit if absent) → token ID |
| `tokenizer.has_language_tokens` | script | True if the vocab contains language-task tokens (Whisper-style) |
| `tokenizer.vocab_sha256` | script | SHA of the canonical token list — detects silent vocab drift after re-export |

### Capabilities

Domain-level surface of the model — what a user can actually ask it to do.
Mirrored into the golden manifest; cross-checked by preflight against GGUF
`general.languages` and `stt.capability.*`.

| Field | Filled by | Notes |
|---|---|---|
| `capabilities.languages` | script + human | BCP-47 codes. Script auto-extracts from common config fields (`languages`, `supported_languages`, `language_list`, `text_config.languages`). Falls back to human-fill from the model card |
| `capabilities.language_detection` | human | Auto-detects input language without a hint (Whisper-style `<|detect|>` tokens, Qwen3-ASR's detection branch) |
| `capabilities.translation` | human | Produces output in a different language than the input audio. Most transducers don't; encoder-decoder + audio-LLM often do |
| `capabilities.translation_target_languages` | human | Output language codes accepted for translation. Leave empty or omit when translation is false or unknown |
| `capabilities.translation_pairs` | human | Allowed directions as `src>target`, only when support is not the simple source-language x target-language cross product |
| `capabilities.timestamps` | human | Subset of `["none", "segment", "word", "token"]`. Parakeet has word+token, Whisper has segment optional word |
| `capabilities.streaming` | human | Streaming / chunked real-time capable |
| `capabilities.speaker_diarization` | human | Multi-speaker attribution |

Any of the boolean flags may be `null` if unknown — a gap the maintainer
resolves at sign-off. The scripted `intake_gaps` list surfaces them.

### Upstream benchmarks

Publisher-claimed numbers from the model card or paper. Informational
only; our own measured numbers live in `docs/models/<family>.md` (both
the in-repo version and the HF-rendered version). Record what the
publisher reports here, not what we eventually measure.

| Field | Filled by | Notes |
|---|---|---|
| `dataset` | human | As the publisher names it. E.g. `"LibriSpeech test-clean"`, `"FLEURS fr"` |
| `language` | human | BCP-47 code, or null for multilingual aggregate |
| `metric` | human | `wer`, `cer`, `bleu`, or `other` |
| `score` | human | As reported. `null` is valid when the publisher doesn't report on this dataset — e.g. a monolingual Russian model has no entry for a LibriSpeech (English) benchmark |
| `score_unit` | human | `ratio` (0.0169) or `percent` (1.69). Omit if the metric's natural unit is unambiguous |
| `source` | human | `"model card"`, a paper URL, or blog post URL |
| `notes` | human | Optional — caveats about the reported number |

Not auto-scraped — model cards use too many formats and benchmark details
matter (split, LM used, decoding strategy). The porter reads the card and
transcribes during research.

### Human judgment fields

| Field | Notes |
|---|---|
| `reference_framework` | One of: `nemo`, `transformers`, `author_repo_<name>`. Copied into `reference.kind` in the golden manifest |
| `reference_rationale` | Why this framework over the alternatives. Cite publisher support, coverage, instrumentation feasibility |
| `architecture_pattern` | One of `encoder-transducer`, `encoder-decoder`, `audio-llm`, `encoder-ctc` |
| `known_risks` | Free-form list. Include anything the reference code does that's not already captured: novel positional encoding, custom attention masks, multimodal fusion, streaming, long-sequence degradation, per-layer dtype differences |

## When the script cannot answer

Some fields may not be mechanically extractable (preprocessor_config absent,
tokenizer files in a non-standard format, no safetensors index). When that
happens:

1. Leave the field as `null` in intake.json.
2. Add an entry to the `intake_gaps` array explaining what couldn't be
   determined and why.
3. Capture the best-known manual value in the family note.

The maintainer decides at sign-off whether a gap is acceptable to proceed or
requires resolution first.

Gaps affecting dtype, frontend config, tokenizer IDs, architecture pattern,
or reference framework must be resolved before converter/C++ work begins, or
explicitly accepted by the maintainer with the source of truth named in the
family note.

## After intake

Intake is the input to:

- **Golden manifest** — the immutable provenance fields (`hf_repo`,
  `hf_revision`, `reference_framework`, `expected_dtype`, `frontend`,
  `tokenizer_summary`) come directly from intake. In the manifest these are
  written as `source_model.hf_repo`, `source_model.hf_revision`,
  `reference.kind`, `expected_dtype`, `frontend`, and `tokenizer_summary`.
- **Preflight Gate A** — cross-checks declared intake values against the
  reference framework's config/preprocessor/tokenizer files. Does not
  enforce that the human judgment fields (`reference_framework`,
  `architecture_pattern`, `known_risks`) are non-null; that's a separate
  human sign-off gate before converter work starts.
- **Converter** — reads intake to know what dtype, frontend, and tokenizer
  fields to emit into the GGUF.
- **C++ loader** — the declared frontend config is what the loader
  cross-checks against GGUF KV at load time.

Intake is not regenerated during the port. If it turns out to be wrong,
update it in place and re-run preflight.
