> ## Documentation Index
> Fetch the complete documentation index at: https://docs.icelab.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript

> @sf-voice/media TypeScript SDK reference: create a client, ingest assets, poll indexing tasks, and search video, audio, and transcripts.

`@sf-voice/media` — requires a runtime with `fetch`, `FormData`, and `Blob`.

Current version: `0.1.1`. Pin it while the API is moving.

## Install

<CodeGroup>
  ```sh pnpm theme={null}
  pnpm add @sf-voice/media@0.1.1
  ```

  ```sh npm theme={null}
  npm install @sf-voice/media@0.1.1
  ```

  ```sh bun theme={null}
  bun add @sf-voice/media@0.1.1
  ```
</CodeGroup>

## Create a client

```ts theme={null}
import { SfVoiceMedia } from "@sf-voice/media";

const client = new SfVoiceMedia({
  baseUrl: "https://api.sf-voice.com",
  apiKey: process.env.SF_VOICE_API_KEY!,
});
```

| Option      | Type     | Default  |                                |
| ----------- | -------- | -------- | ------------------------------ |
| `baseUrl`   | `string` | —        | Required.                      |
| `apiKey`    | `string` | —        | Required. Sent as `X-API-Key`. |
| `timeoutMs` | `number` | `30_000` | Per-request abort timeout.     |

## Ingest

`ingest(request)` submits a recording and returns immediately with a `task_id`.

### From a URL

```ts theme={null}
const { task_id } = await client.ingest({
  source: "url",
  asset_id: "call_001",
  asset_class: "customer_acme",
  url: "https://storage.example.com/calls/001.mp3",
  media_type: "audio",
  types: ["audio", "transcript"],
  metadata: { caller: "+14155550199" },
});
```

### From S3

```ts theme={null}
const { task_id } = await client.ingest({
  source: "s3",
  asset_id: "call_001",
  asset_class: "customer_acme",
  s3_key: "calls/customer_acme/001.mp3",
  media_type: "audio",
  types: ["audio", "transcript"],
});
```

### From a file

```ts theme={null}
const buffer = await file.arrayBuffer();

const { task_id } = await client.ingest({
  source: "file",
  asset_id: "call_001",
  asset_class: "customer_acme",
  file: buffer,
  filename: "001.mp3",
  content_type: "audio/mpeg",
  media_type: "audio",
  types: ["audio", "transcript"],
});
```

`file` accepts `Blob`, `ArrayBuffer`, or `Uint8Array`.

### Common fields

| Field         | Required |                                                                           |
| ------------- | -------- | ------------------------------------------------------------------------- |
| `asset_id`    | Yes      | Your ID. Returned on search results.                                      |
| `asset_class` | No       | Logical group. Scopes search so results don't leak across boundaries.     |
| `media_type`  | No       | `"audio"` or `"video"`. Auto-detected if omitted.                         |
| `types`       | No       | Surfaces to index: `"audio"`, `"video"`, `"transcript"`. Defaults to all. |
| `metadata`    | No       | Flat string/number/boolean pairs for your own use.                        |

### Response

```ts theme={null}
type IngestResponse = {
  asset_id: string;
  task_id: string;
  status: "pending";
};
```

## Tasks

### Poll until ready

```ts theme={null}
const task = await client.pollTask(task_id, {
  intervalMs: 2_000,   // how often to check (default 1_500)
  timeoutMs: 120_000,  // give up after this long (default 120_000)
});

if (task.status === "failed") {
  throw new Error(task.error ?? "indexing failed");
}
```

`pollTask` throws `SfVoiceMediaPollTimeoutError` if the task doesn't reach `ready` or `failed` within the timeout.

### Check once

```ts theme={null}
const task = await client.getTask(task_id);
// task.status: "pending" | "indexing" | "ready" | "failed"
```

### Task shape

```ts theme={null}
type Task = {
  task_id: string;
  asset_id: string;
  asset_class?: string;
  types: Array<"audio" | "video" | "transcript">;
  status: "pending" | "indexing" | "ready" | "failed";
  error?: string;
  created_at: string;
  completed_at?: string;
};
```

## Search

`search(request)` returns timestamped matches across indexed recordings.

### By asset class

```ts theme={null}
const { results } = await client.search({
  query: "customer asks about cancelling their subscription",
  asset_class: "customer_acme",
  types: ["transcript"],
  threshold: 0.7,
  limit: 10,
});
```

### By specific assets

```ts theme={null}
const { results } = await client.search({
  query: "pricing question",
  asset_ids: ["call_001", "call_002"],
});
```

### Search all indexed assets

```ts theme={null}
const { results } = await client.search({
  query: "AI interrupted the caller",
  scope: "all",
});
```

<Warning>
  `scope: "all"` searches across your indexed assets. Use `asset_class` or `asset_ids` for anything customer-facing.
</Warning>

### Search fields

| Field         | Required |                                                                     |
| ------------- | -------- | ------------------------------------------------------------------- |
| `query`       | Yes      | Natural language.                                                   |
| `types`       | No       | `"audio"`, `"video"`, `"transcript"`. Searches all if omitted.      |
| `asset_class` | No       | Restrict to one logical group.                                      |
| `asset_ids`   | No       | Restrict to a specific set of assets.                               |
| `scope`       | No       | Set to `"all"` to search across indexed assets.                     |
| `threshold`   | No       | `0.0`–`1.0`. Default `0.5`. Higher = fewer, more confident results. |
| `page`        | No       | Page number.                                                        |
| `limit`       | No       | Results per page. Max 50.                                           |

### Result shape

```ts theme={null}
type SearchResult = {
  asset_id: string;
  score: number;      // 0.0–1.0
  start_ms: number;   // offset into the recording
  end_ms: number;
  match_type: "audio" | "video" | "transcript";
  thumbnail_url?: string;
};
```

`start_ms` and `end_ms` are milliseconds into the source recording. Use them to seek a player to the matching segment.

## Assets

```ts theme={null}
// list (paginated)
const { items, page_info } = await client.listAssets({ page: 1, limit: 20 });

// single
const asset = await client.getAsset("call_001");

// delete
await client.deleteAsset("call_001");
```

```ts theme={null}
type Asset = {
  asset_id: string;
  asset_class?: string;
  media_type: "audio" | "video";
  source_type: "url" | "s3" | "file";
  types: Array<"audio" | "video" | "transcript">;
  status: "pending" | "indexing" | "ready" | "failed";
  metadata?: Record<string, string | number | boolean>;
  duration_ms?: number;
  created_at: string;
  updated_at: string;
};
```

## Errors

```ts theme={null}
import { SfVoiceMediaError } from "@sf-voice/media";

try {
  await client.search({ query: "...", asset_class: "customer_acme" });
} catch (err) {
  if (err instanceof SfVoiceMediaError) {
    console.error(err.code);    // machine-readable code
    console.error(err.status);  // HTTP status
    console.error(err.message); // human-readable
  }
}
```

| Class                             | When                                                          |
| --------------------------------- | ------------------------------------------------------------- |
| `SfVoiceMediaError`               | Non-2xx API response.                                         |
| `SfVoiceMediaRequestTimeoutError` | Single request exceeded `timeoutMs`.                          |
| `SfVoiceMediaPollTimeoutError`    | `pollTask` timed out before terminal state. Carries `taskId`. |
