Skip to content

Action Recognition: new task, model ABC, and workflow block - #2854

Open
leeclemnet wants to merge 87 commits into
mainfrom
feat/cosmos3-edge-action-recognition
Open

Action Recognition: new task, model ABC, and workflow block#2854
leeclemnet wants to merge 87 commits into
mainfrom
feat/cosmos3-edge-action-recognition

Conversation

@leeclemnet

@leeclemnet leeclemnet commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds Action Recognition: a video task that labels frame ranges. Output is (start_frame_idx, end_frame_idx, class) tuples, which can overlap.

The temporal contract travels with the model. VideoSampling states the window, the sample rate, and the limits training set, so a caller sends a clip and nothing else. No caller picks a window length or a frame rate, because those are training decisions. This is the idea the rest of the diff follows from.

Two kinds of checkpoint share one output type, so nothing downstream tells them apart:

  • Fine-tune (from https://github.com/roboflow/roboflow-train/pull/880 / https://github.com/roboflow/roboflow-train/pull/884). Sliding windows, class-token spans, and a prefix_allowed_tokens_fn state machine that admits only tokens continuing a valid line, so the answer always parses. Runs on a stream and over HTTP.
  • Zero-shot. Reads a whole clip in one call and names the events it finds in its own words. Endpoint only: a stream has no end to span, so the block refuses a whole-video package and says where to send it.

Four surfaces reach the task: the block roboflow_core/roboflow_action_recognition_model@v1, both HTTP routes (/infer/action_recognition, and the legacy /{model}/{version} that serverless serves today), and client.infer_action_recognition in the SDK. The server samples, so a request carries a clip and the response covers that clip.

image

Type of Change

  • New feature (non-breaking change that adds functionality)

Testing

  • I have tested this change locally
  • I have added/updated tests for this change

Test details:

CI

  • New unit tests

Local inference server build on L4 VM

v0  POST /{model}/{version}          [v0]  108.8s (warm start)

v1  POST /infer/action_recognition   [v1]  108.8s (warm start)

output in details below (same for both).
Details { "timeline": [ { "start_frame_idx": 0, "end_frame_idx": 15, "class": "The robot's arm scoops popcorn from the machine and begins lifting it toward the cup.", "class_id": -1 }, { "start_frame_idx": 15, "end_frame_idx": 45, "class": "The robot continues to lift the popcorn scoop, moving it closer to the cup while maintaining a steady grip.", "class_id": -1 }, { "start_frame_idx": 45, "end_frame_idx": 75, "class": "The robot's arm positions the popcorn scoop directly above the cup, preparing to release the popcorn.", "class_id": -1 }, { "start_frame_idx": 75, "end_frame_idx": 105, "class": "The robot releases popcorn into the cup, with the arm slightly adjusting its angle for precision.", "class_id": -1 }, { "start_frame_idx": 105, "end_frame_idx": 135, "class": "The robot's arm retracts slightly after releasing popcorn, readying for the next scoop.", "class_id": -1 }, { "start_frame_idx": 135, "end_frame_idx": 165, "class": "The robot scoops another portion of popcorn from the machine, lifting it toward the cup.", "class_id": -1 }, { "start_frame_idx": 165, "end_frame_idx": 195, "class": "The robot positions the popcorn scoop above the cup again, ensuring accurate placement.", "class_id": -1 }, { "start_frame_idx": 195, "end_frame_idx": 225, "class": "The robot releases popcorn into the cup, with the arm making minor adjustments for consistency.", "class_id": -1 }, { "start_frame_idx": 225, "end_frame_idx": 255, "class": "The robot's arm retracts slightly, preparing for the next cycle of scooping.", "class_id": -1 }, { "start_frame_idx": 255, "end_frame_idx": 285, "class": "The robot scoops another portion of popcorn, lifting it toward the cup.", "class_id": -1 }, { "start_frame_idx": 285, "end_frame_idx": 315, "class": "The robot positions the popcorn scoop above the cup, ensuring alignment for release.", "class_id": -1 }, { "start_frame_idx": 315, "end_frame_idx": 345, "class": "The robot releases popcorn into the cup, with the arm making slight adjustments for precision.", "class_id": -1 }, { "start_frame_idx": 345, "end_frame_idx": 375, "class": "The robot's arm retracts slightly, readying for the next scoop.", "class_id": -1 }, { "start_frame_idx": 375, "end_frame_idx": 405, "class": "The robot scoops another portion of popcorn, lifting it toward the cup.", "class_id": -1 }, { "start_frame_idx": 405, "end_frame_idx": 435, "class": "The robot positions the popcorn scoop above the cup, ensuring alignment for release.", "class_id": -1 }, { "start_frame_idx": 435, "end_frame_idx": 465, "class": "The robot releases popcorn into the cup, with the arm making minor adjustments for consistency.", "class_id": -1 }, { "start_frame_idx": 465, "end_frame_idx": 495, "class": "The robot's arm retracts slightly, preparing for the next cycle of scooping.", "class_id": -1 }, { "start_frame_idx": 495, "end_frame_idx": 525, "class": "The robot scoops another portion of popcorn, lifting it toward the cup.", "class_id": -1 }, { "start_frame_idx": 525, "end_frame_idx": 555, "class": "The robot positions the popcorn scoop above the cup, ensuring alignment for release.", "class_id": -1 } ], "source_fps": 30.0, "frame_count": 563, "windows_classified": 1 }
  • Reproduce NVIDIA's zero-shot temporal localization demo via v1 API

  • Fine-tuned model test via v0+v1 API peter-robicheaux/video-7-cosmos3-edge-t6 (lee-2.mov, in the train split)

v0   5.4s (warm start)
v1   5.7s (warm start) 

[same output both ways]
out  id=2  frames   0-436  =  0.00s -  7.78s     (labelled 0-8s)
in   id=0  frames 436-857  =  7.78s - 15.28s     (labelled 8.5-15.5s)
  • Fine-tuned model test via numpy+torch workflow block peter-robicheaux/video-7-cosmos3-edge-t6 (lee-2.mov again, stride=15.5s to ensure inference triggers before the 16s stream ends)
{
  "model_output": {
    "timeline": [
      {
        "start_frame_idx": 1,
        "end_frame_idx": 450,
        "class": "in",
        "class_id": 0
      },
      {
        "start_frame_idx": 464,
        "end_frame_idx": 857,
        "class": "out",
        "class_id": 2
      }
    ],
    "error_status": ""
  }
}

note the frame ranges are similar but the class assignments flip vs the HTTP endpoint. I could not find a bug that explains this, so I believe the fine-tuned model most likely is not confident in the in vs out class.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code where necessary, particularly in hard-to-understand areas
  • My changes generate no new warnings or errors
  • I have updated the documentation accordingly (if applicable)

Additional Context

CI stays red until an inference-models release carries the new modules and requirements/ pins it. The branch was developed against a source overlay.

ACTION_RECOGNITION_ENABLED gates the v1 route. It currently defaults to on; flip the default before merge if this needs to land dark.

The prompt and decode settings come from measurement against NVIDIA's own demo assets. Zero-shot needs its reasoning pass to localize, which costs 12–80 s per clip depending on length. That is affordable for a clip and not for a stream, which is why the two modes divide the way they do.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

👋 Thanks for the pull request! Here is how automated Claude review works here, so you spend credits (and reviewer time) wisely.

🚦 This PR is marked Ready for review, so automated Claude review will run — and every pass spends real credits.

Warning

💸 The Claude reviewer bills in credits, not vibes

Automated review spins up a real agent that reads real code and spends real credits on every pass. It is glad to help — but it is not a rubber duck, a linter you poke in a loop, or a substitute for reading the contributing guide. Treat it like an expensive senior reviewer whose time you booked, and show up prepared.

Draft when unsure, Ready when you mean it:

  • 🌱 Not sure the PR is in good shape yet? Keep it (or set it back) as a draft — drafts pause review, so you can push and iterate without burning credits on a moving target.
  • 💪 Feel strong about the contents? Mark it Ready for review and the reviewer will take a look.

However you get there, arrive prepared:

  • 🧱 Bring a SOLID, thorough PR. Point your local agent at our skills/ to tune it to our guidelines first — or, if you are one of those fabled carbon-based contributors, read them yourself. A half-baked diff costs exactly the same to review as a finished one.
  • Resolve every comment before you re-request review. Re-requesting with threads still open means paying twice for the same conversation.
  • 🔁 Do not use CI review as an inner loop for a local agent. The reviewer is not a step-by-step debugger — do the unfolding locally and arrive with the answer, not the search.
  • 🙋 If something looks off, ask a human. One question to a maintainer is cheaper and faster than three rounds of agent re-review chasing a misread.

Reviews are not free. A draft costs nothing to review; a Ready PR is a promise that it is worth reviewing.

  • Prefer to skip automated review entirely? Add the skip-claude-review label.

@leeclemnet leeclemnet changed the title Cosmos3 video classification: new task, temporal localization, workflow block [WIP] Cosmos3 video classification: new task, temporal localization, workflow block Aug 25, 2026
@leeclemnet
leeclemnet force-pushed the feat/cosmos3-edge-action-recognition branch from 9ef7ef4 to 2f3768f Compare August 26, 2026 12:21
@leeclemnet leeclemnet changed the title [WIP] Cosmos3 video classification: new task, temporal localization, workflow block Cosmos3 video segment classification: new task, model ABC, workflow block Aug 26, 2026
leeclemnet and others added 26 commits August 28, 2026 18:18
…zation

Register cosmos-3-edge under the new video-multi-label-classification
task and add Cosmos3EdgeReasoner.temporal_localization: one prompt per
clip requesting JSON events in seconds, parsed into sampled-frame
index ranges (floor start, ceil end). The parser accepts numeric
seconds, mm:ss.ff strings, and the prompt guide's caption key. An
env-gated integration test is the Phase 0 prompt-format gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the model-agnostic streaming video classification block
(roboflow_core/video_classification_model@v1, Cosmos3 by default) with
its tensor sibling, the video_multi_label_classification_prediction
kind backed by the typed VideoIntervalClassification entity, and
serializer/deserializer/loader wiring. The block samples tumbling
windows (seconds x fps, tracker-style fps fallback), calls
temporal_localization per window, and emits a cumulative timeline with
provisional open ranges plus per-frame active classes. Tail-window
classification at stream end needs an execution-engine end-of-stream
signal and ships separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prompt for integer frame indices instead of seconds, per review: the
platform contract is frame-indexed and unit conversion in the parser
adds failure modes before any observed need. An entry survives only
when both boundaries are plain JSON integers inside the sampled range;
timestamps, floats, numeric strings, and the caption key all drop the
entry. The Phase 0 gate tests this format first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Match the prior action_recogntion_block v2 conventions per Peter:
rename window_size_seconds/sampling_fps to window_seconds/sample_fps,
replace tumbling windows with a rolling buffer classified from the
first frame onward at a configurable stride_seconds (default
window_seconds / 2 for 50 percent overlap), and surface model-call
failures through a new error_status output instead of raising, so a
stream survives transient failures. Fire time is stamped before the
call so a failed call does not retry on every frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Future video classification models will be purpose-built, not VLM
wrappers, so nothing outside Cosmos3-specific code may depend on
temporal_localization. Add the VideoClassificationModel ABC and the
frozen VideoSegmentClassification result dataclass under models/base
(single abstract infer; the backend pre/forward/post split does not fit
API-wrapping models), and Cosmos3VideoSegmentClassification, which
implements infer by wrapping the reasoner. The registry's
video-multi-label-classification entry points at the wrapper; the block
types against the ABC and wraps a bare reasoner only for the hosted
cosmos-3-edge id, whose platform task type is vlm. Naming follows the
annotation tooling: Segment, not Interval, throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cosmos3EdgeReasoner is a pure VLM again (prompt / prompt_video only).
Cosmos3VideoSegmentClassification owns the temporal-localization prompt
template and the strict parser, which now returns
VideoSegmentClassification dataclasses directly, and calls the
reasoner's generic prompt_video. The Phase 0 integration gate targets
the segment classifier's infer().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apply the Segment nomenclature to the base module and ABC:
models/base/video_segment_classification.py defines
VideoSegmentClassificationModel. The workflow block class name is
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
By analogy with ClassificationPrediction: the model-layer dataclass and
the workflows entity are both named
VideoSegmentClassificationPrediction. The ABC and the Cosmos3 class
names are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Complete the Segment nomenclature sweep: the registry task becomes
video-segment-classification, and the kind and its serializers follow
(video_segment_classification_prediction). The pre-existing
multi-label-classification image task is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
serialize_/deserialize_video_segment_classification_prediction_kind now
match the video_segment_classification_prediction kind, following the
kind-named serializer convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation

Match the Cosmos3Edge* family naming (Cosmos3EdgeReasoner,
Cosmos3EdgeWorldModel) and the segment nomenclature in the module name:
cosmos3_video_segment_classification.py defines
Cosmos3EdgeVideoSegmentClassification. Test module renamed to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The block completes the Segment nomenclature sweep:
roboflow_core/video_segment_classification_model@v1, display name
"Video Segment Classification Model", class
VideoSegmentClassificationModelBlockV1, package
models/roboflow/video_segment_classification/. The type string is free
to change while unreleased.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A naming audit found stragglers the class renames missed: the block's
model_id description (which named Cosmos3's mechanism, not the task),
the GPU restriction note, the unsupported-model error, the tensor
sibling docstring, the changelog feature name, and the integration test
name. Search keywords keep "video classification" for discoverability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per Lee's staging UI test: model_id becomes the generic-model-block
required RoboflowModelField; class_names becomes optional and moves to
Additional Properties, with vocabulary resolved block param ->
model-provided (fine-tunes read class_names from the package's
model_config.json) -> fully open (new open-vocabulary prompt,
class_id -1); active_classes now emits the standard multi-label
classification prediction shape so Classification Label Visualization
accepts it (and no longer accepts the timeline kind). Both prompt
templates cite NVIDIA's temporal-localization cookbook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow the sibling base-class convention: VideoSegmentClassificationModel
declares class_names as an abstract property; implementations provide
it (Cosmos3 returns its stored vocabulary, None when zero-shot).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
class_names becomes class_filter, matching the core model blocks: for
fine-tuned models it restricts output to a subset of the model's
classes; for zero-shot models it forms the prompt vocabulary; empty
accepts all classes (open vocabulary). class_id indexes the model's
own class list when one exists (stable ids under filtering, as with
detection class filters), else the filter list, else -1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Found via instrumented WebRTC UI runs: the reset signature included the
resolved source fps, and live streams re-estimate measured_fps
continuously, so every frame looked like a config change — state wiped
per frame, single-frame windows, empty timelines, and one ~7 s model
call per frame. Resolve fps once per video and store it in the
bookkeeping; the reset signature carries user configuration only. The
sam3 video blocks are immune because they never consume fps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Observed live: a fractional sampling stride (7.5 at 30 fps / 4 sample
fps) places samples alternately 7 and 8 frames apart, so adjacent
windows' reports sit ceil(stride) apart and the float tolerance missed
the merge by under one frame, fragmenting a continuous event into
window-aligned pieces. Regression test reproduces the observed
geometry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Give Cosmos3 exactly what it was trained on (per the technical report
and the cookbook processor sources): prompt_video accepts video_fps and
passes VideoMetadata with do_sample_frames=False, so the processor's
injected per-frame '<X.X seconds>' markers carry the real clock instead
of the assumed 24 fps; the segment classifier prompts for decimal
seconds at 0.1 s granularity (the trained localization format) and
converts to sampled-frame indices in the parser (floor start, ceil
end). The public frame-index contract is unchanged at every boundary.
The block documents the pixel/temporal trade: the model spreads a
fixed pixel budget across a call's frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
do_sample_frames=False leaves the Edge processor's sampled-frame
indices unset and its per-frame timestamp step crashes on None.
Passing num_frames equal to the frame count makes the sampler an
identity linspace, keeping indices populated and timestamps correct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The checkpoint's processor config merges fps=2 into every call, which
collides with the identity num_frames; an explicit fps=None wins the
merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live finding on the full vehicles clip: window-spanning assertions
smear boundaries and bridge two distinct same-class events into one
range. Both prompt templates now instruct precise boundaries (start at
visibility, end when the event stops, not at clip edges) and separate
events per distinct occurrence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two live-path fixes from full-clip testing on the WebRTC UI runs:

The blind 30 fps fallback on a 25 fps stream skews the model's whole
time axis by 20 percent (windows, strides, and every reported second).
When the stream provides no fps, the block now pins a one-shot estimate
from the median of the first eight frame-timestamp deltas (clamped
1-120; the median shrugs off delivery stalls during model calls;
degenerate timestamps keep the 30 fallback). Sticky like the existing
pin; resets clear it.

A segment counted as open only when its end exactly touched the
window's last sampled frame, but the model rounds boundaries by roughly
10-20 percent of its window - ongoing events kept closing and
active_classes flickered. Openness is now a band:
max(0.15 x window frames, sampling stride). Recorded endpoints are
unchanged; absent classes still close immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
measured_fps reports the pipeline's arrival rate. WebRTC ACK windows pace
delivery to processing, so after the first-frame model call it tracks model
latency (~0.05 fps observed). Pinning it collapsed stride_frames to 1 and
fired the model on every frame, which kept arrival slow — a self-
reinforcing loop. Only declared fps pins directly; otherwise the frame-
timestamp median estimates the source rate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every supported source declares its FPS: cv2-backed files, RTSP, and
webcams via CAP_PROP_FPS, and WebRTC uploads via the worker's ffprobe
detection. The only fps-less path observed was a dev image without
ffprobe, where frames arrive ACK-paced and timestamps carry no source
clock — there the estimator measured the delivery pipe (77.62 on a
25 fps clip). With no valid consumer left, declared fps pins and the
30 FPS warning fallback covers the rest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
leeclemnet and others added 10 commits August 31, 2026 12:16
The lettered prompt is gone, so every description that told a caller to
pass a vocabulary for zero-shot was wrong. class_filter now reads as what
it is: a fine-tune's class subset, which zero-shot ignores.

The changelog entry only named the task. It now states the contract that
travels with a model, what each mode answers with, and when a package is
rejected outright.

The vocabulary warning test asserted through caplog, which passed in the
container and failed locally because that logger does not propagate the
same way in both. It now asserts on the call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base64 grows a clip by a third and the request is held whole in memory
before the clip reaches disk, so a gateway rejects a large one. Nothing
said so, and base64 reads as the equal of a URL in every description.

The SDK already defaults to url; the docstring now says why.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wrapper carried the answer grammar inline: fourteen state constants, a
transition function, the constrained decoder and the parser, none of which
concern prompting or calling a model. prefix_allowed_tokens_fn now meets a
named class across a module boundary.

Grammar and parser move together because they describe one format in two
directions. A change to what training emits lands in one file, and drift
between what we constrain and what we read back stays visible.

Pure refactor: the same 68 tests pass, split to mirror the modules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The class was named for a task, InferenceModelsActionRecognitionAdapter,
but sat in the cosmos3 family directory, where adapters are named for a
family instead. Its body is task-generic: it serves any model meeting the
ActionRecognitionModel contract. It now sits beside the five other task
adapters, and only the two cosmos3-shaped helpers name that family.

It also skipped resolve_roboflow_model_alias, which every sibling calls.
No alias targets an action recognition model today, so nothing was broken,
but the first one registered would not have resolved. The call runs before
the task suffix is stripped, so it sees the id the caller gave.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The branch added it for the two-stage zero-shot design, where a second
text-only call mapped captions onto a vocabulary or condensed them into
labels. Both stages are gone, and nothing calls it. prompt() does not
route through it, so the vlm path is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zero-shot answered every clip with one summary segment. Three trained
values were reaching it, and each one alone is enough to cause that.

The frame budget: whole-video planning capped at window_seconds x
sample_fps, which is a fine-tune's trained budget. On an 18.8 s clip it
cut 75 frames to 64 and dragged the rate to 3.4 fps. max_frames is now
declared rather than derived, and stays unset for a model that never
trained on one.

The rate: the window reported count / duration instead of the rate it
sampled at, so 4.0 arrived as 3.9964. The model builds a timestamp per
frame from that number, and off-rate stamps are values training never
produced. 4.0 gives 19 segments on NVIDIA's demo clip; 3.9964 gives 1,
on identical frames, greedy, five runs each.

The frame side: the adapter resized to 360, which is what the SFT package
records. 360 and 480 both give one segment where native gives 19, so
max_frame_side is optional too and absent means native.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
max_frame_side fell back to 360 when a package left it out. That is the
guess this branch has been removing: a value the model never declared,
applied as though it had. roboflow-train always writes the key, so the
fallback only ever guessed for a package shape that does not exist.

Both limits now read the same way, and absent means absent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three planning tests duplicated others: two ran the same inputs for a
subset of the same assertions, and one asserted a rate the neighbouring
test now asserts alongside the step it checks.

Three wrapper tests went for a different reason. One repeated an outcome
stated more generally elsewhere. The other two pinned implementation
rather than behaviour: mode caching against a tokenizer mutated mid-run,
which nothing does, and a property returning its constructor argument.

Also renamed a block test that said 'default stride' while passing one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They ran three times the length of every neighbouring entry, describing
mechanism that belongs in the code. What each one now states is the
public surface, the contract, and what a caller has to know.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@leeclemnet
leeclemnet force-pushed the feat/cosmos3-edge-action-recognition branch from 72af91b to 30ae1e4 Compare August 31, 2026 16:39
The entry said class tokens make a package a fine-tune. The class list
has to resolve to those tokens, which is a different claim: the list
names the classes and the tokens say the model can emit the span format.
A package carrying one without the other is rejected, not routed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@leeclemnet
leeclemnet force-pushed the feat/cosmos3-edge-action-recognition branch from 30ae1e4 to abb2cf5 Compare August 31, 2026 16:39
leeclemnet and others added 6 commits August 31, 2026 14:51
Eight cleanups from the review, none changing behaviour.

read_frame_windows walks a clip once for every window. Reading each
window on its own capture re-decoded everything before it, so a 30 minute
clip decoded 3,036,656 frames to deliver 7,168.

load_action_recognition_model is now the one load path. The block skipped
resolve_roboflow_model_alias and the trust flags the adapter passes, so a
model id resolved differently by entry point.

The constrained decoder sliced the prompt off after crossing to host. It
now slices the tensor, so a 10k token prompt stops being copied once per
generated token.

One pydantic segment model, declared in the entities layer and imported
by the workflow kind. merge_window_segments replaces the window-to-clip
mapping each caller had written out, which had already drifted on stride.

Also: the backend helper that keeps ordering hash-seed independent, a
disjunct the check above already covered, and the URL error translation
video fetching had dropped, so an SSRF rejection surfaces as
InputImageLoadError rather than raw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The method hard-coded the v1 route, so select_api_v0 was silently
ignored. Serverless serves v0, and model-eval reaches the task through
this client, so v0 was the version that mattered most.

It now dispatches on client mode like infer() does: a URL rides in the
image query parameter, base64 rides as the body, and the class list goes
on the query string. Both versions answer with the same shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six fixes from the bot review, five of them verified defects.

The grid was the real one. Windows tiled in frames where training slices
in time, positions rounded where training takes the first frame at or
after each timestamp, short clips floored where training rounds, and
clips under the frame floor were refused where training clamps up to it.
At 29.97 fps that started a window (0, 7, 15, 22, 30) against training's
(0, 8, 15, 23, 30), and a 4.2 s clip read 16 frames at 4.0 against 17 at
4.0157. All three now match.

Two grids exist because two trainings do. A model that recorded a frame
budget was trained by the platform and reads count / duration. A model
that recorded nothing has only its pretraining, which used a plain rate,
and reading it at anything else collapses the answer to a summary.

Also: a span the model places outside the clip no longer clamps into a
one-frame event on the nearest edge; windows_classified counts calls
rather than plans; merge_segment builds a replacement rather than
assigning to a frozen dataclass; a sampling value we cannot honour is
rejected instead of quietly becoming a default; and the slow integration
test no longer asserts a vocabulary that zero-shot ignores by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It read as a per-frame classification and is not one. Users expecting a
standard classification output would have found a keep-alive list of
classes whose ranges can still merge, which answers a question about the
timeline rather than about the frame.

The state that only fed it goes with it: the keep-alive window, the
bookkeeping field, and both builders. The tensor sibling now overrides
frame extraction alone, which is all a tensor block has to change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two divergences remained after the last pass, both reproduced.

A source slower than the recorded rate was capped to the source. Training
picks the count from the recorded rate alone and repeats frames the
source cannot supply distinctly, so 20 frames at 2 fps became 20 samples
at 2 fps here against 40 at 4 fps there. The cap now applies only to a
model that recorded nothing, where there is no training to reproduce and
a repeat buys nothing.

Timestamps were built by multiplying floats. (0.1 * 3) * 30 lands just
above 9.0, so a fourth frame read 10 where training read 9. Windows and
stamps are whole microseconds now, and a frame comes from one
multiplication rather than a chain.

Verified against the trainer's algorithm on six shapes, including a
source slower than the contract, the drift case, 29.97 fps, and the
lee-2 clip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A window holds tens of frames, and each one crossed on its own with its
own synchronisation. They now stack and cross together. Ragged input,
which cannot stack, keeps the per-frame path.

Same bytes and same values either way, which the new test asserts
against the per-frame conversion directly. The live fine-tune answer and
its timings are unchanged.

The frames still cross at source resolution and are resized on the host
afterwards. Resizing on the device first would move far less, but
interpolate is not bit-identical to INTER_AREA, and this model has
already shown it answers differently on smaller input changes than that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@leeclemnet
leeclemnet marked this pull request as ready for review August 31, 2026 20:30
@leeclemnet leeclemnet added the claude-review Use to trigger AI review manually label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Claude review started at commit 10768549ddff9c55c52aaeab0d5e3e88e55a4346.

New commits are not auto-reviewed. Add the claude-review label to request a re-review — the label is consumed when the review starts, so just add it again next time.

@github-actions github-actions Bot removed the claude-review Use to trigger AI review manually label Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant