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:
| Resource | What you get |
|---|---|
transcript | Time-coded transcription segments |
scenes | Detected scenes with descriptions |
shots | Shot metadata |
faces | Face detections + character clusters |
speakers | Diarized speakers |
sounds / sound-events | Non-speech audio events |
audio-segments | Audio type timeline (filter with audio_type) |
music-tracks | Identified music (Arrow does not identify songs yet: it needs a licensed fingerprint service) |
characters | Face clusters per video, named once you name them in People |
regions | Object, face and text boxes, up to 5,000 per kind (Images & documents) |
faces/{face_id}/crop | The 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.
| Member | What it holds |
|---|---|
manifest.json | video_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.npz | numpy, 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 field | Default | |
|---|---|---|
file | required: the file | |
pipeline | arrow | Leave 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 |
options | JSON index options for this upload, e.g. {"faces": false} | |
language | auto | spoken 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 takespipeline,options,languageandcontentType/content_type. It is never resent after a 5xx (the server may already hold the file): checkvideos()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.