Warning: Alpha Release: The API may change in future versions.
Python SDK for Overshoot real-time video analysis API.
- Python >= 3.10
aiohttp >= 3.9, < 4livekit >= 1.0.0- FFmpeg and ffprobe on PATH (download) — required for
FileSource,RTSPSource,HLSSource,RTMPSource, andCameraSource
pip install git+https://github.com/Overshoot-ai/overshoot-python.gitSee the
examples/directory for complete runnable scripts.
import asyncio
import overshoot
async def main():
models = await overshoot.get_models(api_key="ovs_...")
ready = [m for m in models if m.ready]
for m in ready:
print(f"{m.model}: {m.status}")
asyncio.run(main())import asyncio
import overshoot
async def main():
# Pick a ready model
models = await overshoot.get_models(api_key="ovs_...")
model = next(m.model for m in models if m.ready)
client = overshoot.Overshoot(api_key="ovs_...")
stream = await client.streams.create(
source=overshoot.CameraSource(),
prompt="Describe what you see",
model=model,
on_result=lambda r: print(r.result),
)
await asyncio.sleep(30)
await stream.close()
await client.close()
asyncio.run(main())stream = await client.streams.create(
source=overshoot.FileSource(path="/path/to/video.mp4", loop=True),
prompt="Detect all objects",
model="Qwen/Qwen3.5-9B",
on_result=lambda r: print(r.result),
)stream = await client.streams.create(
source=overshoot.RTSPSource(url="rtsp://user:pass@192.168.1.10/stream"),
prompt="Alert if someone enters the room",
model="Qwen/Qwen3.5-9B",
on_result=lambda r: print(r.result),
)Uses TCP transport for reliability.
stream = await client.streams.create(
source=overshoot.HLSSource(url="https://example.com/live.m3u8"),
prompt="Describe the scene",
model="Qwen/Qwen3.5-9B",
on_result=lambda r: print(r.result),
)stream = await client.streams.create(
source=overshoot.RTMPSource(url="rtmp://example.com/live/stream"),
prompt="Describe the scene",
model="Qwen/Qwen3.5-9B",
on_result=lambda r: print(r.result),
)Push frames from any pipeline — OpenCV, PIL, a robotics stack, etc.
import cv2
import numpy as np
import overshoot
source = overshoot.FrameSource(width=640, height=480)
stream = await client.streams.create(
source=source,
prompt="Count the people",
model="Qwen/Qwen3.5-9B",
on_result=lambda r: print(r.result),
)
cap = cv2.VideoCapture(0)
while True:
ret, bgr = cap.read()
if not ret:
break
rgba = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGBA)
rgba = cv2.resize(rgba, (640, 480))
source.push_frame(rgba) # accepts numpy arrays or raw bytesIf you manage your own LiveKit room:
stream = await client.streams.create(
source=overshoot.LiveKitSource(
url="wss://your-livekit-server.com",
token="your-livekit-token",
),
prompt="Describe what you see",
model="Qwen/Qwen3.5-9B",
on_result=lambda r: print(r.result),
)| Source | Input | Requires FFmpeg |
|---|---|---|
FileSource(path, loop=False) |
Local video file | Yes |
RTSPSource(url) |
RTSP camera/server | Yes |
HLSSource(url) |
HLS live stream | Yes |
RTMPSource(url) |
RTMP stream | Yes |
CameraSource(device, width, height) |
Local camera | Yes |
FrameSource(width, height) |
Programmatic (push frames) | No |
LiveKitSource(url, token) |
User-managed LiveKit room | No |
The Overshoot class manages streams with automatic keepalive, WebSocket result delivery, and resource cleanup.
client = overshoot.Overshoot(
api_key="ovs_...",
base_url="https://api.overshoot.ai/v0.2", # default
timeout=30.0,
)Always close when done:
await client.close()For direct HTTP control without background tasks or media pipelines:
api = overshoot.ApiClient(api_key="ovs_...")
# Create stream (only accepts LiveKitSource or None for native transport)
response = await api.create_stream(
source=overshoot.LiveKitSource(url="wss://...", token="..."),
processing=overshoot.ClipProcessingConfig(target_fps=6),
inference=overshoot.InferenceConfig(
prompt="Describe what you see",
model="Qwen/Qwen3.5-9B",
),
mode="clip",
)
# Manage stream lifecycle manually
keepalive = await api.keepalive(response.stream_id)
config = await api.update_prompt(response.stream_id, "New prompt")
status = await api.close_stream(response.stream_id)
# Utilities
models = await api.get_models()
health = await api.health_check()
await api.close()| Parameter | Type | Default | Description |
|---|---|---|---|
source |
SourceConfig |
required | Video source |
prompt |
str |
required | Analysis prompt |
model |
str |
required | Model name (use get_models()) |
on_result |
Callable |
required | Callback for each result |
on_error |
Callable |
None |
Callback for errors |
mode |
"clip" or "frame" |
"frame" |
Stream mode |
output_schema |
dict |
None |
JSON schema for structured output |
max_output_tokens |
int |
None |
Cap tokens per inference request |
target_fps |
int |
6 |
Clip mode: target frame sampling rate (1-30) |
clip_length_seconds |
float |
0.5 |
Clip mode: clip duration |
delay_seconds |
float |
0.5 |
Clip mode: delay between clips |
interval_seconds |
float |
0.5 |
Frame mode: capture interval |
The list of available models is dynamic. Always use get_models() at runtime:
models = await overshoot.get_models(api_key="ovs_...")
ready_models = [m for m in models if m.ready]| Status | ready |
Meaning |
|---|---|---|
"ready" |
True |
Healthy, performing well |
"degraded" |
True |
Near capacity, expect higher latency |
"saturated" |
False |
At capacity, will reject new streams |
"unavailable" |
False |
Endpoint not reachable |
Processes short video clips with multiple frames — ideal for motion and temporal analysis.
stream = await client.streams.create(
source=source,
prompt="What is happening?",
model="Qwen/Qwen3.5-9B",
on_result=handle_result,
mode="clip",
target_fps=6,
clip_length_seconds=0.5,
delay_seconds=0.5,
)Processes individual frames at regular intervals — ideal for static analysis, OCR.
stream = await client.streams.create(
source=source,
prompt="Read all visible text",
model="Qwen/Qwen3.5-9B",
on_result=handle_result,
mode="frame",
interval_seconds=0.5,
)Caps the maximum tokens per inference request. The server enforces 128 effective output tokens per second per stream:
effective_tokens_per_second = max_output_tokens * requests_per_second
If omitted, the server auto-calculates: floor(128 * interval).
stream.stream_id # server-assigned ID
stream.is_active # True if runningconfig = await stream.update_prompt("Now count the people")await stream.close()schema = {
"type": "object",
"properties": {
"people_count": {"type": "integer"},
"description": {"type": "string"},
},
"required": ["people_count", "description"],
}
def handle_result(result: overshoot.StreamInferenceResult):
data = result.result_json()
print(f"People: {data['people_count']}")
stream = await client.streams.create(
source=source,
prompt="Count people and describe the scene",
model="Qwen/Qwen3.5-9B",
on_result=handle_result,
output_schema=schema,
)| Field | Type | Description |
|---|---|---|
id |
str |
Result ID |
stream_id |
str |
Stream ID |
mode |
"clip" or "frame" |
Processing mode |
model_name |
str |
Model used |
prompt |
str |
Task that was run |
result |
str |
Model output |
inference_latency_ms |
float |
Model inference time |
total_latency_ms |
float |
End-to-end latency |
ok |
bool |
Success status |
error |
str | None |
Error message if failed |
finish_reason |
str | None |
Why the model stopped ("stop", "length", "content_filter") |
def handle_error(error: Exception):
print(f"Stream error: {error}")
stream = await client.streams.create(
source=source,
prompt="Describe what you see",
model="Qwen/Qwen3.5-9B",
on_result=handle_result,
on_error=handle_error,
)| Exception | HTTP Status | Description |
|---|---|---|
AuthenticationError |
401 | Invalid or revoked API key |
InsufficientCreditsError |
402 | Not enough credits |
ValidationError |
400/422 | Invalid request parameters |
NotFoundError |
404 | Stream or resource not found |
ServerError |
5xx | Server-side error |
NetworkError |
— | Connection/timeout failure |
StreamClosedError |
— | Operation on a closed stream |
WebSocketError |
— | WebSocket connection/protocol error |
- Concurrent streams: Maximum 5 per API key (429 error if exceeded).
- Output token rate: 128 effective tokens per second per stream.
- Billing: By stream duration, not inference count.
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("overshoot").setLevel(logging.DEBUG)MIT