# ADR 0001: Primary Batch Connection Mode

## Status

Accepted

## Context

The SDK currently centers on realtime transcription. Realtime sessions open a
provider transport, emit live transcription events, and resolve
`stopTranscription()` when the realtime session has closed. Whisper providers
also support `batchReprocess`, which reprocesses retained durable audio after a
realtime session has stopped.

We need a primary batch mode for developers who want to capture audio with the
same connection properties as realtime mode, but without live transcription
events. In this mode, `startTranscription()` starts local capture and
`stopTranscription()` returns the batch transcription result.

This mode must stay distinct from the existing `batchReprocess` capability.
`batchReprocess` means "run an additional batch request after a realtime
session"; primary batch mode means "the session itself is batch."

## Decision

Add a top-level connection discriminator:

```ts
mode?: "realtime" | "batch";
```

When omitted, `mode` defaults to `"realtime"` for source compatibility.

Primary batch mode is initially supported only for explicit Whisper provider
connections:

- `sofya_as_service`
- `sofya_whisper_flow`
- `stt_wvad`

Batch mode is not supported for `apiKey` provider discovery until the discovery
API exposes an explicit batch HTTP endpoint. Batch mode is also not supported
for `oracle` or `sofya_compliance`.

In batch mode, the top-level `endpoint` is the direct HTTP batch endpoint, not a
realtime websocket endpoint and not a base URL used for derivation.

```ts
const transcriber = createTranscriber<MyBatchPayload>({
  provider: "sofya_as_service",
  mode: "batch",
  endpoint: "https://api.example.com/api/transcriber",
  config: {
    language: "pt-BR",
    token: "...",
    external_id: "consultation-123",
    headers: {
      "x-client": "web",
    },
    batch: {
      parseResponse: async (response) =>
        response.json() as Promise<MyBatchPayload>,
    },
  },
});

transcriber.startTranscription(stream);
const result = await transcriber.stopTranscription();
```

`startTranscription(mediaStream)` captures and stores local audio only.
`stopTranscription()` freezes local capture, exports one WAV file, uploads it as
`multipart/form-data`, parses the response, and resolves with the typed batch
payload.

Default request behavior:

- HTTP method: `POST`
- Body: `FormData`
- File field: `file`
- File name: `consultation.wav`
- File content: WAV built from captured PCM
- `Content-Type` is not set manually so the browser can set the multipart
  boundary

Default response behavior:

- Parse with `response.json()`
- Allow `config.batch.parseResponse(response)` to override parsing

Batch mode inherits the same request context as realtime where applicable:

- `config.token` becomes `Authorization: Bearer ...` unless
  `config.headers.Authorization` is already set
- `config.headers` are included, except `Content-Type` is removed
- `config.external_id` becomes the `x-external-id` query parameter
- `config.language` becomes `transcription_language` using the existing
  language selector
- `config.translation_lang` becomes `translation_language` using the existing
  language selector

Batch mode must not create or touch a websocket. It must not emit transcript
content events:

- `recognizing`
- `recognized`
- `recognized_diarization`
- `nomatch`

Subscribing to those events remains allowed at runtime, but they are never
emitted in batch mode.

Lifecycle, debug, telemetry, and warning events may still exist where they are
meaningful:

- `ready`
- `stopped`
- `error`
- `telemetry`
- `telemetry_row`
- `stt_audit_ingestion_warning`

`pauseTranscription()` and `resumeTranscription()` remain available in batch
mode and control local capture only. The final uploaded WAV contains only audio
captured while not paused.

`stopTranscription()` rejects with a typed SDK error when the batch request,
audio export, or response parsing fails. Audit ingestion failures must not mask
the primary batch result or primary batch error.

After successful batch stop, retained IndexedDB audio follows the same 30 minute
TTL used by realtime batch reprocess.

Implement primary batch mode with a separate `BatchTranscriptionAdapter`.
Shared helpers may be extracted from `WhisperTranscriptionAdapter` when the
duplication is immediately useful, but the batch adapter should not inherit the
realtime websocket/reconnect state machine.

## Consequences

The public return type of `stopTranscription()` becomes mode-specific:

- realtime mode: `Promise<Blob | null>`
- batch mode: `Promise<TBatchPayload>`

TypeScript should omit `reprocessAudio()` from the batch transcriber type.
Runtime calls to `reprocessAudio()` in batch mode should throw a clear
unsupported-mode error.

`getResilienceStatus()` has no realtime transport meaning in batch mode. The
first implementation should return `null` for batch mode rather than inventing a
batch-specific resilience snapshot.

The existing `batchReprocess` configuration remains scoped to realtime sessions
and should not be reused to configure primary batch mode.

## Non-Goals

- No API-key batch discovery until provider discovery returns an explicit batch
  endpoint.
- No Oracle or Sofya Compliance batch mode in the first version.
- No continuous upload or streaming batch request during recording.
- No duplicate batch operation through `reprocessAudio()` on batch-mode
  transcribers.
