Skip to main content
A camera on a bird feeder, a front door, or a street records hours of footage. Jev can help you analyse it. Send Jev the frames of a video one at a time and ask questions about each frame. With footage from a bird feeder camera, the answers become a visit log per species. A snowy bird feeder with a male cardinal and a downy woodpecker, each in its own
labeled box with a species probability. A status bar shows the time, the bird_present
probability, and the visits open so far. We send Jev one frame a second, with five questions about each frame per request:
  • bird_present, a Noul: is there a bird?
  • how_many, a Choice of none, one, or several
  • bird, a Box: where is the most prominent bird?
  • species_left and species_right, two Choice questions over a list of species: what are the leftmost and rightmost birds?
A Box question is used to find where the bird is in the image. It returns one box, so when two species share the feeder, a second request asks for a box around each species by name. This cookbook boxes up to two species at a time. Plain code turns the answers into visits. On the 101-second video clip we used, Jev agreed with our hand labels on whether a bird was there in all 101 frames, and named the correct species in 132 of 133 labeled bird sightings.
Note - Images and Box questions are experimental (“dreamer-ready”). Every request that carries an image or a Box question needs the TypeSafe-Danger-Zone-Images: true header. See Images and Box.

Setup

Requires Python 3.10 or later. Choose a package manager to install the dependencies:
Create a virtual environment and add the dependencies:
Set your TypeSafe API key:
Create a new file named feeder_watch.py. As you work through this cookbook, copy each Python code block into that file, in order. This command runs it whether you installed with pip or uv:
A full run sends about 180 requests. Pillow draws the boxes and matplotlib lays out the figures. Every response is saved in json_cache.json, so a second run makes no requests. To replay the published numbers without an API key, download json_cache.json into the folder you run the script from. Numbers below came from jev-1.14.0 on 2026-10-15.

Download the frames

The footage is “Busy Bird Feeder”, a 101-second clip filmed by Courtney Celley of the U.S. Fish and Wildlife Service and released into the public domain. For the first minute, a male cardinal and a downy woodpecker share the feeder, so most frames hold two birds of different species. After that, the male cardinal leaves and a female cardinal feeds from behind the feeder, mostly out of sight. We cut one frame a second at 1024 × 576 pixels, and labeled each frame by hand with the species of every bird in full view. Twenty-two frames are marked partial, because the female cardinal in them is cut off at the edge or mostly hidden behind the feeder. The frames are in usfws_frames.zip and the labels are in usfws_labels.json, both in typesafe-public-examples. The source and the changes made are in DATA_LICENSE.md. The code below downloads both, unzips the frames into usfws_frames/, and skips anything that is already on disk.

Write the questions

Each request sends one frame as the state, under the key frame, and asks five questions about it. All five share the one request, so the image is sent once.
  • The bird_present question is a Noul. Below 0.5, the frame counts as empty and the code ignores the other answers. A Box question always returns a box, even when there is nothing to find, so this answer is what tells the code whether to use the box.
  • The how_many question is a Choice over none, one, and several. The code asks for a second box only when the answer is several.
  • The bird question is a Box around the most prominent bird. Frames with one bird use this box.
  • The species_left and species_right questions are the same Choice, over ten species plus other, asked about the leftmost and the rightmost bird. With one bird in the frame, both answers describe it. With two, each answer describes one bird.
Each species option describes what the bird looks like on a feeder camera, rather than what a field guide says. For example, the grackle option says the bird can fill the frame so that only its head shows.

Ask about one frame

Each request sends one frame and a set of questions, with the model version and the images header set above. The three frames below have two birds, one bird, and no bird.
The two-bird frame came back as several, with the cardinal on the left, the woodpecker on the right, and both species answers near 1.00. On the one-bird frame, the left and right answers both name the woodpecker. On the empty frame, bird_present is low, so the code ignores the box and the species answers. Those answers still name an option, here other. On the two-bird frame, the bird box covers only the cardinal. One Box question returns one box, so the woodpecker needs a question of its own.

Box each bird by its species

We need two Box questions, one for “the leftmost bird” and one for “the rightmost bird”. On this feeder, both boxes landed on the cardinal.
Both boxes cover the same part of the frame. We need to ask for each bird by its species. The first request already said which two species are there, so the second request asks for “the male northern cardinal” and “the downy woodpecker”. The phrase for each species is in BOX_TARGETS. Where the name alone could also fit another bird, the phrase adds what the bird looks like.
With named boxes, the woodpecker gets its own box, to the right of the cardinal’s. Its box confidence, 0.36, is lower than the cardinal’s 0.84. The figure below shows both pairs of boxes on the frame.
output A Box question always returns a box. If the first request names a species that is barely in the frame, such as a bird hidden behind the feeder, the second request still returns a box for that species, and the box can land on the other bird. Two named boxes that overlap by 0.6 or more are treated as the same bird found twice, and only the box with the higher confidence is kept. Overlap here is intersection over union: the area the two boxes share, divided by the total area they cover.

Ask about every frame

For each frame:
  1. Send the five questions. Stop there if bird_present is below 0.5, or if neither species answer reaches 0.5.
  2. If how_many is several and the two species answers name different species, send the second request for one box per species, then run dedupe() on the boxes. Do the same when only one side’s answer reaches 0.5, because the bird box might be around the other bird.
  3. Otherwise, keep the most likely species and the bird box from the first request.
The result for a frame is each bird’s species, species probability, box and box confidence, plus the number of requests and the seconds the frame took.
The frames don’t depend on each other, so four threads ask about them in parallel. The results still come back in frame order, because pool.map keeps the order of its input. On a live camera, send one frame at a time instead, each as soon as the previous answer is back.
Some frames sent the second request and still ended with one box. On those frames, either only one species answer reached 0.5, or the two named boxes overlapped and dedupe() kept one.

Turn answers into visits

Visits are built from the results in frame order, one species at a time, so two birds at the feeder at once make two overlapping visits. Three settings decide what counts as a visit:
  • A frame counts toward a species when that species’ probability is at least species_at.
  • A visit stays open through gaps of up to max_gap frames, such as when the bird hops out of view for a moment.
  • A visit needs at least min_frames frames that name its species. With the default of 2, a wrong answer on a single frame doesn’t become a visit.
These settings belong to your application, and Jev never sees them. To try other values, rerun visits() on the saved results. No new requests are needed.
As a timeline, each row of the visit log is a species and each bar is one visit, with the total time at the feeder beside the row.
output The two long visits match the visit log built the same way from our hand labels, with the same start and end seconds. In every run we made of this clip, the two long visits came out the same, but the short visits changed from run to run. The short visit here is wrong: the male cardinal visit from 1:12 to 1:15 is a female cardinal behind the feeder, misnamed when only her red beak showed. Lowering min_frames to 1 shows the single-frame sightings that the default of 2 keeps out of the log:
Some of these are real birds, such as the female cardinal at 1:04 and 1:08, and some are misreads, such as the male cardinal at 1:34. A higher min_frames keeps more wrong answers out of the log, and it also drops more birds that only show up for a second or two.

Check the answers against the labels

Two checks compare the answers with the hand labels:
  • whether bird_present agrees with the label on whether the frame has a bird in it
  • whether every labeled bird’s species is among the species that ask_frame() returned
A frame marked partial counts as having a bird for the first check. It is left out of the second, because its label doesn’t name every bird in it.
The hardest frames for bird_present are 1:33 and 1:34, where only part of a cardinal shows; the left half of the figure below is 1:33. Jev scored both at about 0.8 and counted the bird, and the labels mark both partial. The one labeled bird that Jev missed is the woodpecker at 1:02, which shares the frame with the male cardinal. That frame came back with only the male cardinal. The right half of the figure shows a failure that dedupe() doesn’t catch. At 1:15, the female cardinal’s beak shows at the right edge of the cage, and Jev named a male cardinal. The box for the male cardinal landed on the woodpecker. It overlaps the woodpecker’s own box by less than 0.6, so both boxes stay. A check on each box would catch this: crop to the box and ask a Noul question whether the bird in the crop is a male cardinal.
output

See it running live

The same questions work on live footage. The video below shows a small web app built on them. It takes a frame from a webcam or a video file, asks Jev, draws the boxes, and keeps a running visit log. In the video, it plays a stretch of the Ostdrossel feeder cam that the questions weren’t tuned on, at about two answers a second.
Bird footage: Ostdrossel feeder cam by Ostdrossel, licensed CC BY, shown with boxes and labels added by the app.

Before you point it at your own camera

  • One Box question returns one box. This code boxes at most two birds per frame, and only when they are different species. For more species, ask one Noul question per species, such as “Is there a blue jay in frame?”, then a named Box for each species whose answer comes back high.
  • Two birds of the same species can’t be told apart by name. For a flock, a zero-shot detector such as Meta’s SAM 3 can find every bird from the text prompt “bird”, with no training. Jev can then name the species of each cropped bird.
  • Small frames are rejected. The Images page allows 280 to 1568 pixels on each side, with at least 78,400 pixels in total (280 × 280).
  • Adjust the thresholds as needed. The values of PRESENT_AT, SPECIES_AT, OVERLAP_AT, and the visits() defaults worked here. Label a few minutes of your own footage and check them against it.
  • Write the species list for your camera. Describe what your camera shows of each bird, close-ups included. Keep the other option, so that a bird your list doesn’t name isn’t forced into one of the names it does have.