Niravi Docs
npm ↗ Get a key ↗

Videos & processing

List your library, drill into a single video’s analysis, upload, delete, and export. Photos and documents use the same endpoints: see Images & documents.

List & fetch

GET /api/v1/videos is paginated (limit, offset → pagination.has_more). GET /api/v1/videos/{id} returns one video’s full detail.

import requests  # the Python SDK is not on PyPI yet: plain HTTP works

BASE = "https://api.niravi.io/api/v1"
H = {"X-API-Key": "nv_..."}

for v in requests.get(f"{BASE}/videos", headers=H, params={"limit": 200}).json()["data"]:
    print(v["video_id"], v["filename"])

video = requests.get(f"{BASE}/videos/{video_id}", headers=H).json()["data"]["video"]
const videos = await niravi.videos({ allPages: true });
const video = await niravi.getVideo(videoId);

Analysis sub-resources

Each video exposes its derived analysis as separate endpoints (GET /api/v1/videos/{id}/<resource>), grouped under Analysis in the reference:

ResourceWhat you get
transcriptTime-coded transcription segments
scenesDetected scenes with descriptions
shotsShot metadata
facesFace detections + character clusters
speakersDiarized speakers
sounds / sound-eventsNon-speech audio events
audio-segmentsAudio type timeline (filter with audio_type)
music-tracksIdentified music (Arrow does not identify songs yet: it needs a licensed fingerprint service)
charactersFace clusters per video, named once you name them in People
regionsObject, face and text boxes, up to 5,000 per kind (Images & documents)
faces/{face_id}/cropThe face crop as image/jpeg (every crop_url in a response points here)
segments = requests.get(f"{BASE}/videos/{video_id}/transcript", headers=H).json()["data"]["segments"]
scenes = requests.get(f"{BASE}/videos/{video_id}/scenes", headers=H).json()["data"]["scenes"]
const v = await niravi.getVideo(videoId);
await v.transcript(); await v.scenes(); await v.shots(); await v.faces(); await v.speakers();
await v.soundEvents(); await v.audioSegments(); await v.characters(); await v.regions();

Frames

GET /api/v1/videos/{id}/frames/{timestamp} returns the stored keyframe nearest to timestamp (seconds). Keyframes are stored once per second, so the answer says which one you got: timestamp (the keyframe’s), requested_timestamp and approx_within_s.

{ "success": true, "data": { "video_id": "…", "timestamp": 10.0, "requested_timestamp": 10.4, "approx_within_s": 0.4,
  "source": "nearest-keyframe", "mime": "image/jpeg", "bytes": 28103, "image_b64": "/9j/4AAQ…" } }

For the image itself, add ?format=jpeg (or send Accept: image/jpeg): the response is the JPEG. Every search hit’s frame_url already points there.

curl -o frame.jpg "https://api.niravi.io/api/v1/videos/$VIDEO_ID/frames/10?format=jpeg" -H "X-API-Key: $NIRAVI_API_KEY"
const jpeg = await v.frame(10, { jpeg: true });   // Uint8Array

Exports

Render a video’s analysis into deliverables (under Exports in the reference): screenplay, subtitles.srt, subtitles.vtt, storyboard, and the .nmap archive.

Subtitles

subtitles.srt and subtitles.vtt answer JSON by default (data.content holds the file’s text). Add ?format=file, or send Accept: application/x-subrip (SRT) or Accept: text/vtt (VTT), for the raw file as a download (<video_id>.srt / .vtt).

curl -OJ "https://api.niravi.io/api/v1/videos/$VIDEO_ID/subtitles.srt?format=file" -H "X-API-Key: $NIRAVI_API_KEY"
screenplay = requests.get(f"{BASE}/videos/{video_id}/screenplay", headers=H).json()["data"]
srt = requests.get(f"{BASE}/videos/{video_id}/subtitles.srt", headers=H).json()["data"]["content"]
vtt = requests.get(f"{BASE}/videos/{video_id}/subtitles.vtt", headers={**H, "Accept": "text/vtt"}).text
const srt = await v.subtitles("srt", { raw: true });   // the file's text

The .nmap archive

GET /api/v1/videos/{id}/nmap downloads everything the index holds for one asset as one portable file: a ZIP (application/zip) named <video_id>.nmap.

MemberWhat it holds
manifest.jsonvideo_id, schema_ver (1), duration_s, pipeline, recipe, kind, page_count, frame_count, row_counts, content_stamp, and model_registry: each vector table’s model and dimension (Arrow: frame_vec so400m16-256/webli 1152-d in frames_arrow, region_vec 1152-d in regions_arrow, text_vec embeddinggemma 512-d in the scene, summary and transcript tables; faces sface, sounds AST, captions, speakers pyannote, transcript whisper). central.omitted lists the tables this asset has no source for (never fabricated), central.lance_errors any vector table that could not be read
raw.json{table: [rows]} without vectors: videos, frames, scenes, transcriptions, speakers, face_clusters, faces, sound_events, audio_segments, music_tracks, content_classification, asset_passages (documents) and more
vectors.npznumpy, no pickles: <table>__vecs float32 N × D matrices with their <table>__ids (or <table>__rowidx for one-row-per-video tables)
import io, json, zipfile, numpy as np, requests

r = requests.get(f"{BASE}/videos/{video_id}/nmap", headers=H)
r.raise_for_status()                                    # 409 not_ready while processing
z = zipfile.ZipFile(io.BytesIO(r.content))
manifest = json.loads(z.read("manifest.json"))
raw = json.loads(z.read("raw.json"))
vecs = np.load(io.BytesIO(z.read("vectors.npz")), allow_pickle=False)
frames = vecs["frames_arrow__vecs"]                     # (frames, 1152) float32
await niravi.downloadNmap(videoId, "./clip.nmap");      // Node: streamed to disk
const bytes = await v.nmap();                           // or the bytes

The archive is the base indexed data, the same for every workspace that can read the asset: your workspace’s own edits (a rename, tags, corrected transcript lines) are not in it. It answers 409 not_ready while the asset is processing. The ETag is the content stamp, so send If-None-Match to get a 304 until the asset is reindexed.

Upload (write scope)

POST /api/v1/videos (multipart) ingests a video, photo or document. Requires the write scope.

Form fieldDefault
filerequired: the file
pipelinearrowLeave it out: every upload runs on Arrow, whichever way it arrives (HTTP, the SDKs, the dashboard, link imports). The NiNo tiers (lite, pro, max, nino_*) still run when named, for videos only, and are deprecated
optionsJSON index options for this upload, e.g. {"faces": false}
languageautospoken language hint
curl https://api.niravi.io/api/v1/videos \
  -H "X-API-Key: $NIRAVI_API_KEY" \
  -F "file=@clip.mp4"

The acknowledgement (201) carries video_id, kind, the effective index_options and eta_seconds, an estimate that includes the work queued ahead of it. Processing is asynchronous: poll GET /api/v1/videos/{id} until video.status is completed (or failed: a job that cannot finish marks its video failed rather than leaving it processing); index.report then says what every stage did.

Long videos are processed with bounded memory (audio is transcribed and diarized in windows): a 54-minute 1080p episode (671 MB) indexed in about 22 minutes. Each GPU worker indexes one upload at a time, so a batch of long episodes queues; poll each video’s status.

const created = await niravi.upload("/path/to/clip.mp4", { options: { faces: false }, language: "en" });  // Node

The SDKs’ upload() streams the file from disk with no upload timeout and takes pipeline, options, language and contentType / content_type. It is never resent after a 5xx (the server may already hold the file): check videos() before uploading again.

Delete (write scope)

DELETE /api/v1/videos/{id} removes a video, photo or document. It leaves every list at once and is purged in the background: search indexes, pages, regions, faces and stored files, and the People it appeared in are refreshed.

{ "success": true, "data": { "video_id": "…", "deleted": true, "purging": true } }

Only the workspace that owns the asset can delete it; one shared into your workspace answers 403. After a delete, GET /videos/{id} returns 404.