Documentation Index
Fetch the complete documentation index at: https://docs.sf-voice.sh/llms.txt
Use this file to discover all available pages before exploring further.
@sf-voice/media — requires a runtime with fetch, FormData, and Blob.
Current version: 0.1.1. Pin it while the API is moving.
Install
pnpm add @sf-voice/media@0.1.1
Create a client
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
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
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
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
type IngestResponse = {
asset_id: string;
task_id: string;
status: "pending";
};
Tasks
Poll until ready
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
const task = await client.getTask(task_id);
// task.status: "pending" | "indexing" | "ready" | "failed"
Task shape
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
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
const { results } = await client.search({
query: "pricing question",
asset_ids: ["call_001", "call_002"],
});
Search all indexed assets
const { results } = await client.search({
query: "AI interrupted the caller",
scope: "all",
});
scope: "all" searches across your indexed assets. Use asset_class or asset_ids for anything customer-facing.
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
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
// 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");
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
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. |