# Documents, audio and everything else

> PDFs, spreadsheets, voice notes, zips — what the pipeline does with each, and what it deliberately does not.

Not everything you upload is a photo. Four paths, decided by MIME type, and the
last one is the one people assume wrongly.

| MIME | path | what you get |
|---|---|---|
| `image/*` | ladder | WebP variants + palette + dimensions |
| `video/*` | background job | poster, MP4, HLS, optional AI proxy |
| `audio/*` | inline transcode | an MP3 variant **plus** the untouched original |
| **anything else** | **passthrough** | the bytes, stored and served as-is |

## Audio

Voice notes, clips, tracks. Two things are written:

1. **The original**, untouched, under the `original` variant.
2. **An `mp3` variant** — mono, ~96 kbps, tuned for speech.

The transcode exists because of a real interoperability problem: `MediaRecorder`
emits `audio/webm;opus` on Chrome/Android and `audio/mp4`/AAC on iOS, and each
one chokes somewhere the other works. MP3 is the one container and codec every
target browser decodes.

```ts
// Give raw bytes an explicit audio MIME, or they classify as `other`
// and you get no mp3 variant.
await nt.upload(bytes, {
  contentType: "audio/mpeg",
  fileName: "voice-note.mp3",
});
```

:::caution[The MIME is what decides, not the extension you hoped for]
`upload(bytes)` with no `contentType` and no extension on `fileName` resolves to
`application/octet-stream` → `kind: "other"` → passthrough, no MP3 variant, and
`regenerate()` is unsupported. The resolution order is
`Blob.type` → `contentType` → the extension in `fileName` → octet-stream.
:::

Legacy `.m4a`/`.wav` with no matching pipeline still fall back to `original`, so
the bytes are safe either way.

## PDFs, spreadsheets, zips — the passthrough path

A PDF, an `.xlsx`, a `.csv`, a `.zip`, a font, a `.glb` — anything that is not
image, video or audio takes the same branch:

- stored **raw**, byte-for-byte, under the `original` variant key
- served from the CDN with its real content type
- the row is `ready` **immediately** — there is no job to wait for
- classified as `kind: "document"` (PDF) or `kind: "other"` (everything else)

```ts
const res = await nt.upload(pdfFile, { fileName: "contract.pdf" });
// → { ok: true, passthrough: true, assetId, url }
```

### What you do NOT get, and should not wait for

This is the part worth being blunt about:

- **No thumbnail.** No first-page render of a PDF, no spreadsheet preview.
- **No text extraction.** No search, no OCR, no page count.
- **No transforms.** A `/t/` URL on a document is meaningless.
- **No virus scanning.** The bytes are stored as received.

If you need any of that, you need a document pipeline, and this is not one. What
this *is* good at is being the single addressed, deduplicated, CDN-backed place
your files live — so a contract and the photos attached to it are the same kind
of thing to your code.

### Getting the extension right matters more here

For images the stored extension rarely bites. For documents it always does,
because `original` is the only variant there is, and its extension comes from
**the uploaded filename**, not the MIME:

```ts
// Right: the URL will end in .pdf
await nt.upload(bytes, { fileName: "contract.pdf", contentType: "application/pdf" });

// Wrong: the URL ends in .bin and the browser will not know what to do with it
await nt.upload(bytes);
```

### Archiving a video as bytes

A useful trick, since video presets silently drop `original`: upload it under a
**non-video** filename and it takes the passthrough branch instead.

```ts
// Stored verbatim, no transcode, no HLS — a real backup of the source file.
await nt.upload(clipBytes, { fileName: "master.mp4.bin" });
```

You lose poster, playback variant and streaming — which is the point. Do this
*in addition to* a normal video upload, not instead of it.

## Deduplication applies to all of it

Every path is content-addressed. Uploading the same PDF twice returns the same
asset, and the second upload transfers nothing. That is also true across
different filenames — the SHA is of the *bytes*, not the name.