Guides
Segmentation
Label every moment of a video, or pull out only the moments you need, with timecodes, evidence and review flags.
Segmentation turns a video into time intervals. You describe the task in plain language, and Story0 returns intervals in seconds, each with a label, the evidence behind it, and a review flag where a person should look. It costs $0.10 per minute of video in either mode.
Partition or extract
partition |
extract |
|
|---|---|---|
| Question it answers | "What is the state at every moment?" | "Where are the moments that match?" |
| Labels | 2 to 12 of yours, plus the uncertainty label | None; every returned moment is labeled match |
| Coverage | Every moment of the range is in exactly one interval | Only matching moments are returned; gaps are expected |
| When nothing matches | Not applicable: the range is always covered | An empty segments list |
| Typical uses | Visibility timelines, studio versus field footage, day versus night | Clip pulls, highlight candidates, finding one kind of action |
| Interval IDs | interval_1, interval_2, … |
moment_1, moment_2, … |
Use partition when you need to know the state of the whole video, and extract when you only care about the parts that match.
Create a segmentation
This extract request finds jogging and keeps at most five moments of at least two seconds each:
const result = await story0.segments.createAndWait(
{
input: { asset_id: "asset_01m6gwgx34hq7d2m9ycv5k8b1e" },
mode: "extract",
instruction: "Moments where a person is jogging",
constraints: { min_seconds: 2, max_segments: 5 },
billing: { max_charge: { currency: "USD", amount: "0.25" } },
metadata: { story_id: "park-0412" },
},
{ idempotencyKey: "park-0412:jogging:v1" },
);result = client.segments.create_and_wait(
input={"asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e"},
mode="extract",
instruction="Moments where a person is jogging",
constraints={"min_seconds": 2, "max_segments": 5},
billing={"max_charge": {"currency": "USD", "amount": "0.25"}},
metadata={"story_id": "park-0412"},
idempotency_key="park-0412:jogging:v1",
)curl https://api.story0.com/v1/segments \
-H "Authorization: Bearer $STORY0_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: park-0412:jogging:v1" \
-d '{
"input": { "asset_id": "asset_01m6gwgx34hq7d2m9ycv5k8b1e" },
"mode": "extract",
"instruction": "Moments where a person is jogging",
"constraints": { "min_seconds": 2, "max_segments": 5 },
"billing": { "max_charge": { "currency": "USD", "amount": "0.25" } },
"metadata": { "story_id": "park-0412" }
}'createAndWait returns the segmentation once it exists. segments.create (and the raw POST) returns the job immediately with 202 Accepted, for pipelines that wait through webhooks instead. For a complete partition example, see the Quickstart.
Request fields
| Field | Required | Description |
|---|---|---|
input |
Yes | Exactly one of asset_id or url. See Uploading media. |
mode |
Yes | partition or extract. |
instruction |
Yes | The task in plain language, 3 to 2,000 characters. |
labels |
For partition |
2 to 12 { "key", "description" } objects: the mutually exclusive states. Not allowed with extract. |
uncertainty.label |
No | The label for moments without enough evidence. Default uncertain. |
boundaries.snap |
No | Only none is available today: boundaries are never moved to shot cuts. |
sampling.fps |
No | Frames per second to inspect, from 0.1 to 4. The default depends on the length of the range. |
range |
No | { "start", "end" } in seconds: analyze only this part of the video. |
constraints |
No | For extract: min_seconds, max_seconds and max_segments. partition ignores them. |
billing.max_charge |
No | The most this request may cost, such as { "currency": "USD", "amount": "0.50" }. |
metadata |
No | Up to 20 string key-value pairs (values up to 500 characters), echoed on the job and in its webhooks. |
Send an Idempotency-Key header with every request; the SDKs add one automatically. See Idempotency and retries.
Labels
Each label is an object with a key and a description:
{ "key": "dog_visible", "description": "At least one dog is visible, even partly out of frame." }- Keys start with a lowercase letter and continue with lowercase letters, digits or underscores, up to 40 characters in all (
^[a-z][a-z0-9_]{0,39}$). They appear in results, so keep them stable. - Descriptions are 1 to 500 characters. They tell Story0 what the label means; this is where your decision rules go.
- Keys must be unique, and none may equal the uncertainty label.
- For
partition, the labels should be mutually exclusive, and together with the uncertainty label they should cover every possibility. If two things can be true at once, such as "dog visible" and "person visible", run two partitions.
The uncertainty label
Rather than forcing a guess, Story0 uses the uncertainty label where the frames don't show enough to choose: the subject is obscured, blurred, cut off or too dark, or the shot cuts away. Uncertain intervals always have review.required: true and never carry evidence. The default label is uncertain; rename it with uncertainty.label (same key format) if that word means something else in your taxonomy.
The temporal contract
Partition
- Intervals are in time order and cover the whole range: the first starts at
range.start, the last ends atrange.end, and each one starts exactly where the previous one ended. There are no gaps and no overlaps. - Each interval has exactly one label, either one of yours or the uncertainty label. Neighboring intervals always differ, because consecutive moments with the same label are merged.
- Intervals are start-inclusive and end-exclusive,
[start, end). A moment at exactly 4.5 seconds belongs to the interval that starts at 4.5. - Brief events are kept. A label seen in a single sampled frame gets its own interval, one frame interval long; nothing is smoothed away. Anything shorter than the frame interval can fall between samples and be missed, so raise
sampling.fpsif brief events matter.
Extract
- Moments are in time order. The gaps between them are moments that didn't match or couldn't be decided.
- Every moment has
label: "match"and a shorttitle, taken from its first piece of evidence (or "Matching moment" when there's none). - A single uncertain frame between two matching frames doesn't split a moment. It's bridged, and the moment is flagged
partly_uncertainwithreview.required: true. - No matches is a successful result with an empty
segmentslist, not an error.
Boundaries and resolution
Story0 samples frames at an even rate and judges each one. A boundary falls halfway between two neighboring samples whose labels differ, so boundaries sit on a grid as fine as the sampling: boundary_resolution_seconds, which equals coverage.frame_interval_seconds. At the default 2 frames per second that's 0.5 seconds. Read a boundary at 4.0 as "the change happened between the frames at 3.75 and 4.25", not as a frame-exact cut.
boundaries.snap accepts only none today. Boundaries are never moved to shot cuts, because a subject can enter or leave in the middle of an uninterrupted shot.
Sampling
By default the sampling rate depends on the length of the range you analyze:
| Range length | Default rate | Boundary resolution |
|---|---|---|
| Up to 2 minutes | 2 frames per second | 0.5 s |
| Over 2 and up to 10 minutes | 1 frame per second | 1 s |
| Over 10 minutes | About 600 frames spread across the range, for example 0.5 frames per second for 20 minutes | 1 / rate |
Set sampling.fps between 0.1 and 4 to choose your own rate. Higher rates catch briefer events and give finer boundaries; lower rates are enough for long, slow footage. The rate doesn't change the price.
Note: Story0 inspects at most 600 frames per request. If
sampling.fpsmultiplied by the length of the range would exceed that, the rate is lowered to fit, so the whole range is always covered;coverage.frame_interval_secondsreports the interval actually used. To look densely at part of a long video, combine a higher rate with arange.
coverage.sampled_frames and coverage.frame_interval_seconds report what was actually inspected. config.fps echoes the rate you asked for, or null for the default.
Analyze part of a video
"range": { "start": 120, "end": 180 }startdefaults to 0 andendto the end of the video. When you give both,endmust be greater thanstart.- An
endpast the end of the video is treated as the end of the video. - A range with less than a quarter of a second of video in it fails the job with
range_outside_media(not charged). - A range shorter than half a frame interval contains no sampled frame, so it comes back as a single uncertain interval.
- Intervals and evidence use the video's own timeline. An interval at 130–135 means 130 seconds from the start of the video, not of the range.
- The result's
rangeshows what was analyzed, and the default sampling rate depends on the range's length rather than the video's. - You're billed for the length of the range, not the whole video (still at least 30 seconds).
Shape extracted moments
constraints adjusts the moments extract returns:
| Constraint | Allowed values | Effect | Review reason added |
|---|---|---|---|
min_seconds |
Over 0, up to 600 | Moments shorter than this are padded equally on both sides, without leaving the range. | padded_to_min_seconds |
max_seconds |
Over 0, up to 3,600 | Longer moments are split into consecutive pieces of this length; the last piece can be shorter. | split_at_max_seconds |
max_segments |
1 to 500 | Keeps the longest moments, up to this many, still in time order. | None |
min_seconds can't be greater than max_seconds. Story0 applies padding first, then splitting, then the count limit, so split pieces count individually toward max_segments. A few details to plan for:
- Padding never merges moments, so two short moments close together can overlap once padded.
- Near the start or end of the range, padding is cut off at the edge, so a padded moment can end up shorter than
min_seconds. - When
max_segmentsdrops moments, the remaining ones keep their IDs, so you might seemoment_1followed bymoment_3. padded_to_min_secondsandsplit_at_max_secondsare informational. On their own they don't setreview.required.
Read the result
| Field | Description |
|---|---|
id, object |
seg_… and segmentation. |
job_id, asset_id |
The job that produced the result and the asset it analyzed. |
livemode, fixture |
livemode is false for test keys, and fixture: true appears only on test-mode results. |
mode |
partition or extract. |
time_unit |
Always seconds. |
duration_seconds |
The whole video's duration. |
range |
The range analyzed, after defaults and clamping. |
segments |
The intervals or moments: id, start, end, label, title (extract only), review and evidence. |
coverage.timeline |
Always complete; see below. |
coverage.sampled_frames |
How many frames were inspected. |
coverage.frame_interval_seconds |
Time between samples. |
coverage.uncertain_seconds |
Total time judged uncertain. |
coverage.modalities |
What was analyzed: ["visual"]. |
boundary_resolution_seconds |
The grid that boundaries fall on. |
config |
Your request as Story0 resolved it, defaults included, plus the engine version. |
engine, provenance |
The engine version (segment-2026-11), whether the analysis was reused, and the API version. |
receipt |
What the result cost. See Pricing and billing. |
retention |
When the result and the media expire. |
coverage.timeline: "complete" is a structural promise: the whole range was sampled, and in partition mode the intervals tile it exactly. It doesn't mean every frame of the video was inspected, and it says nothing about whether every label is right.
Evidence
Each evidence item is a sampled frame that supports its interval's label:
{ "id": "ev_2", "kind": "seen", "start": 4.25, "end": 4.5, "description": "Golden retriever walking on a path in the park." }startis the time of the frame andendis where that frame's share of the timeline ends. Seek a player tostartto show a reviewer what Story0 saw.kindisseen: a visual observation.descriptionis a short note about the frame.partitionintervals carry up to 2 items andextractmoments up to 3. Uncertain intervals have none, and a definite interval can also have none.- Evidence supports a label at that moment. It isn't proof that the label holds for every frame of the interval: what happens between samples is inferred.
- Evidence IDs (
ev_1,ev_2, …) are unique within a result.
Review flags
Every interval and moment has review: { "required": …, "reasons": [ … ] }:
| Reason | Mode | Sets required |
Meaning |
|---|---|---|---|
insufficient_evidence |
partition |
Yes | The interval has the uncertainty label. |
partly_uncertain |
extract |
Yes | The moment bridges an uncertain frame. |
padded_to_min_seconds |
extract |
No | The moment was padded to meet min_seconds. |
split_at_max_seconds |
extract |
No | The moment is one piece of a longer one split at max_seconds. |
review.required: false means the analysis found no reason to flag the interval. It is not a clearance for licensing, publication or safety.
Empty, uncertain and failed
| What you see | What it means |
|---|---|
A succeeded extract with an empty segments list |
Nothing matched. It's billed like any other result. |
A succeeded partition with a single uncertain interval |
Story0 sampled the video but couldn't decide anywhere. It's billed, and the interval needs review. |
A failed job |
Story0 couldn't produce a result, for example because the media was unreadable or a URL couldn't be fetched. It's not billed; job.error.code says why. |
A file that isn't readable video fails with media_unreadable when its media facts are read, before any analysis or charge; it never turns into an all-uncertain timeline. Within a readable video, frames that can't be decoded are reported as uncertain time, never as a negative label: in partition they become an uncertain interval flagged for review, and in extract a gap counted in coverage.uncertain_seconds.
Reuse
If you send the same segmentation request for the same media again, Story0 can answer from your account's earlier analysis instead of analyzing the video again. "The same" means:
- the same media, recognized by its SHA-256 hash, so an identical file uploaded again counts; and
- the same request: mode, instruction (ignoring leading and trailing spaces), labels in the same order, uncertainty label (defaults count, so omitting it equals sending
uncertain), andsampling.fps,rangeandconstraintsas you sent them.
metadata and billing.max_charge don't matter. A reused result has provenance.analysis_reused: true and the same segments, and it's charged only the $0.02-per-minute segment line instead of the full $0.10. Change anything about the request and Story0 analyzes the video again at the full price. Reuse stays within your account, and test-mode requests never use it.
Write good instructions and labels
Story0 judges each sampled frame on what it shows and then joins those judgments into intervals. Write for that.
- Describe what's visible. "At least one dog is visible" works. "The dog is barking" depends on sound, which Story0 doesn't use.
- Say what counts and what doesn't. "Count a dog that is partly out of frame. An audible bark alone does not count." Edge cases written into a description beat a guess.
- Keep partition labels exclusive.
dog_visibleandno_dog_visiblesplit every moment cleanly. "Dog" and "person" don't, because both can be on screen; run two partitions instead. - Prefer states to events. "A player is celebrating" can be seen in one frame; "a goal is scored" needs the moments before and after. For events, use
extractwith an instruction about what the frames show, such as "the ball crossing the goal line". - Ask about what a viewer would notice. Frames are inspected at a reduced size, so small on-screen text, distant faces and fine detail may not be legible.
- Change descriptions, keep keys. Keys live in your data and dashboards. Refine behavior by editing a description, and version your idempotency keys when you do.
- Check before you scale. Run a handful of representative videos, look at the uncertain intervals and the evidence, and tighten the descriptions where results surprise you.
| Instead of | Try |
|---|---|
{ "key": "dog", "description": "Dog" } |
{ "key": "dog_visible", "description": "At least one dog is visible, even partly out of frame. Toys and drawings of dogs do not count." } |
| "Find the good bits" | "Moments where a person is jogging along the path" |
One partition with dog, person, both, neither |
Two partitions: dog_visible / no_dog_visible and person_visible / no_person_visible |
Edge cases
- One label throughout: a single interval covering the whole range.
- Uncertain throughout: a single interval with the uncertainty label and
review.required: true. - Very short videos: billed as 30 seconds. At 2 frames per second, a 3-second clip gives 6 frames.
- A range that runs past the end:
endis clamped to the video's duration. Keepstartinside the video. - A tiny range: a range shorter than half a frame interval contains no sample, so it comes back as one uncertain interval.
Record reviewer feedback (preview)
When a person checks a result, you can record their verdict with POST /v1/feedback. Feedback is append-only and never changes the result it refers to.
await story0.feedback.create({
result_id: "seg_01m6gwj2c15y8n3q7w2e9t4h6b",
segment_id: "interval_3",
kind: "relabel",
label: "dog_visible",
start: 15,
end: 15.5,
actor: "editor@example.com",
comment: "The dog's tail is still in frame at the left edge.",
});client.feedback.create(
result_id="seg_01m6gwj2c15y8n3q7w2e9t4h6b",
segment_id="interval_3",
kind="relabel",
label="dog_visible",
start=15,
end=15.5,
actor="editor@example.com",
comment="The dog's tail is still in frame at the left edge.",
)kind is correct, incorrect, relabel (which needs the reviewer's label) or note. Point it at a segment_id from the result, optionally with start and end in seconds, a comment of up to 2,000 characters and an actor in your own terms. For screenings, use category instead of segment_id. Without an Idempotency-Key, a repeated call records the feedback twice; with one, the repeat returns the first record.