Skip to main content

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!,
});
OptionTypeDefault
baseUrlstringRequired.
apiKeystringRequired. Sent as X-API-Key.
timeoutMsnumber30_000Per-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

FieldRequired
asset_idYesYour ID. Returned on search results.
asset_classNoLogical group. Scopes search so results don’t leak across boundaries.
media_typeNo"audio" or "video". Auto-detected if omitted.
typesNoSurfaces to index: "audio", "video", "transcript". Defaults to all.
metadataNoFlat 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(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

FieldRequired
queryYesNatural language.
typesNo"audio", "video", "transcript". Searches all if omitted.
asset_classNoRestrict to one logical group.
asset_idsNoRestrict to a specific set of assets.
scopeNoSet to "all" to search across indexed assets.
thresholdNo0.01.0. Default 0.5. Higher = fewer, more confident results.
pageNoPage number.
limitNoResults 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
  }
}
ClassWhen
SfVoiceMediaErrorNon-2xx API response.
SfVoiceMediaRequestTimeoutErrorSingle request exceeded timeoutMs.
SfVoiceMediaPollTimeoutErrorpollTask timed out before terminal state. Carries taskId.