Action Recognition: new task, model ABC, and workflow block - #2854
Action Recognition: new task, model ABC, and workflow block#2854leeclemnet wants to merge 66 commits into
Conversation
|
👋 Thanks for the pull request! Here is how automated Claude review works here, so you spend credits (and reviewer time) wisely. 🚧 Right now this is a draft, so automated Claude review is paused — nothing is being spent yet. Mark it Ready for review to trigger it. Warning 💸 The Claude reviewer bills in credits, not vibesAutomated 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:
However you get there, arrive prepared:
Reviews are not free. A draft costs nothing to review; a Ready PR is a promise that it is worth reviewing.
|
9ef7ef4 to
2f3768f
Compare
…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>
This reverts commit b2a0a8f.
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>
Same-class ranges unify only when no sampled frame lies in the gap. ceil(sampling_stride) is that rule exactly on the sampling grid, not an approximation: consecutive samples sit floor/ceil(stride) apart, and the next possible sampled gap is about twice the stride. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cookbook comment refers to the localization prompt, so that prompt now sits directly under it, ahead of the mapping templates. Comments added during this branch shrink to the constraint they carry; the experiment history lives in the PR and commit messages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
class_filter no longer forms a prompt vocabulary — detected events are classified into it. The window floor now appears on the window_seconds field, the finite-clip geometry (a window at or beyond the clip length never fires) is documented, and error_status gets a sentence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Other architectures, possibly non-VLM, will serve this task, so the prose names model properties (pixel budget, temporal floor) instead of cosmos-3-edge. The 2 s default window sat below the prototype model's floor, so a fresh block returned empty timelines out of the box. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pixel budgets and temporal floors are properties of the prototype model, not the task. The block describes only what it controls: frames per call, temporal context per classification, and result frequency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
to_legacy_classification_prediction assumed every input is a tensor- native prediction object and crashed on the legacy dict shape that Video Segment Classification's window_classes emits. Input already in the target shape passes through unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under ENABLE_TENSOR_DATA_REPRESENTATION every classification producer emits a tensor-native prediction; the tensor sibling emitted the legacy dict and crashed the visualization's converter. window_classes now builds a MultiLabelClassificationPrediction per the multi-label block convention: dense confidence over the vocabulary with detected ids, or a per-prediction dynamic class map for open vocabulary. The numpy block keeps the legacy dict. This reverts the converter's dict passthrough — contract violations fail loudly again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The WebRTC output path imported the numpy wildcard serialiser statically, so under ENABLE_TENSOR_DATA_REPRESENTATION tensor-native outputs (window_classes, native detections) reached orjson unserialised. Its default encoder then returned unknown objects unchanged, which orjson retries forever — the run died with "default serializer exceeds recursion limit" after one frame. The import now mirrors the loader's flag swap, and the default encoder raises a TypeError so future gaps fail with a clear message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No-think mapping misattributes subjects in multi-object captions: on the vehicles captions it assigned the overtake to the wrong caption across several vocabulary phrasings, and open-vocab condensing swapped subjects the same way. With thinking, attribution is correct at both tested budgets — but think plus one JSON entry per caption outgrows 512 tokens at nine captions, so the mapping shares the 4096 localization budget. Measured stage-2 cost: 12-50 s per fire by caption count. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sandbox enabler: single-call no-think experiments need the switch on prompt_video, which only prompt_text carried. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Monotone timelines: the display equals the stored union; the open-range provisional extension and the openness band are gone. window_classes derives from range mergeability — a class stays listed while a future positive fire could still merge with its range (one window plus the sample tolerance) and clears when the range closes. Defaults become the sliding operating point (2 s window, 0.5 s stride) and stride is always explicit; boundary precision equals the stride. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…contract Zero-shot now makes one no-think call per window: lettered choice over the vocabulary, or a short-phrase label open-vocabulary. Timelines emerge from the block merging per-window verdicts; the two-stage localization pipeline and its prompts are removed. Models fine-tuned on the roboflow-train#880 contract (class tokens in the tokenizer) speak that contract instead: its system message and prompt, no-think at 256 tokens, span-line answers parsed with the trainer's grammar, and constrained decoding via a prefix_allowed_tokens_fn state machine so answers parse by construction and labels stay in vocabulary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-shot Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The trainer ships adapter layouts under the dash-less cosmos3-edge slug with task vlm: base weights under base/, the class-token tokenizer and adapter at the top, and no resized embedding table. The loader now registers that slug, grows the embeddings to the tokenizer before the adapter attaches, and merges. Class names derive from the added <|cls:...|> tokens in token-id order; video_pre_processing from inference_config.json sets the fine-tune frame cap. The parser strips the chat end token that skip_special_tokens=False keeps on the final span line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Window length and sample rate are training decisions, not deployment settings: the ABC gains a VideoSampling contract (16 s / 4 fps / 4-frame floor by default), the Cosmos3 wrapper fills it from the package's video_pre_processing, and the block reads it from the loaded model instead of exposing window_seconds. Stride stays a block parameter. Fires wait for the model's minimum frame count. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A clip shorter than one window never reached a scheduled call, and longer clips lost the frames after the final call. The block now joins the stream pipeline at depth 0 and classifies the pending tail on end of stream, which matches the trainer: it classifies a shorter-than-window clip whole, down to the model's minimum frame count. The pipeline gated draining on the RF-DETR depth flag; it now detects any handler that exposes flush(), so a depth-0 block reaches the sinks. The handler keeps the last frame batch to carry that flush result. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
roboflow-train#884 ships the platform's class_names.txt convention, in class-token order. Prefer it, and keep deriving names from the class tokens for checkpoints trained before it. The model_config.json lookup is gone: that file is the loader's own offline manifest, so it never carried a class list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
roboflow-train#884 records mode: a whole_video checkpoint reads a clip as one unit, with its frame budget spread over the full length, so fixed windows misrepresent it. These models now skip the stride schedule: the buffer holds a thinned set of frames spanning the stream, capped at the trained frame budget and keeping the first and newest samples, and the end-of-stream flush classifies it. Thinning lowers the real frame rate, so the call reports the rate the buffer carries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The trainer emits window_frames next to window_seconds, and derives the seconds from frames over fps. In whole_video mode the length has no window meaning at all: frames is the budget drawn from the entire clip. Read window_frames when the package declares it, and keep deriving it from the window length otherwise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The team settled on Action Recognition for the task, and on cosmos3-edge with the vlm task type for fine-tune packages. Renames the workflows block to roboflow_core/action_recognition_model@v1, the kind to action_recognition_prediction, the registry task to action-recognition, and the model layer to ActionRecognitionModel / ActionRecognitionPrediction / Cosmos3EdgeActionRecognition, with the modules and tests moved to match. The dash-carrying cosmos-3-edge id keeps resolving to the plain reasoner under the vlm task, because the Cosmos3 VLM block and the legacy adapter both load it that way; the block wraps that reasoner for action recognition at load time. Fine-tune packages ship as cosmos3-edge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every block under core_steps/models/roboflow uses roboflow_core/roboflow_<task>_model@vN. Display names carry no prefix, so only the type changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only fine-tune packages ever carry the action-recognition task, and they ship under the dash-less cosmos3-edge slug. The hosted cosmos-3-edge base is registered as vlm and resolves to the plain reasoner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9d3e835 to
1b80acc
Compare
roboflow-train now ships class_names.txt in every package, so deriving the class list from the tokenizer is dead. The wrapper also held the package sampling twice, as a raw dict and as the typed contract; it now keeps only VideoSampling, which gained the trained frame side. Also fixes the WebRTC worker, which read the handler result as a list. A workflow holding a stream-pipelined block runs behind a handler that returns an InferenceHandlerResult instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A package whose tokenizer carries class tokens but ships no class_names.txt used to load as a zero-shot model, which serves the wrong prompt and decoder against fine-tuned weights. It now raises CorruptedModelPackageError. A base model without class tokens still loads as zero-shot. The WebRTC worker also drove blocks to the end of a stream without ever flushing them, so a clip shorter than the model's window produced nothing. Every path there ends by announcing completion, so the flush runs at that point and sends what the block still holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What does this PR do?
Adds Action Recognition: a video task that assigns class labels to frame ranges, and the workflow block that runs it over a stream. Output is
(start_frame_idx, end_frame_idx, class)tuples, which can overlap. Net diff: 30 files, +4506/−23.One output contract covers both kinds of checkpoint, so the block never knows which it holds. A zero-shot model returns a verdict for the whole window. A fine-tuned model returns spans inside the window.
Model layer (
inference_models)action-recognition, theActionRecognitionModelABC, and the frozenActionRecognitionPredictionresult. AVideoSamplingcontract travels with the model and carries window length, sample rate, minimum frames, sampling mode, frame budget, and trained frame side.Cosmos3EdgeActionRecognitionimplements the ABC over the pure-VLM reasoner and picks its mode from the package. A tokenizer with<|cls:...|>tokens is a fine-tune; anything else runs zero-shot.<|cls:name|> <S.SS> <E.EE>span lines. Decoding runs through aprefix_allowed_tokens_fnstate machine that admits only tokens continuing a valid line, so the answer always parses. Frames are capped at the trained side. Classes come fromclass_names.txt, and sampling frominference_config.json. A package with class tokens but no class list raisesCorruptedModelPackageErrorrather than loading as zero-shot.base/, the class-token tokenizer on top, and an embedding resize before the adapter attaches. It also gainedenable_thinkingand constrained-decoding support on the video path, and realVideoMetadataso the model's frame timestamps match the clip.Workflows layer (
inference)roboflow_core/roboflow_action_recognition_model@v1and a tensor sibling.model_idis the standard model field;class_filterrestricts a fine-tune's classes or forms the zero-shot vocabulary.stride_secondsis the only temporal setting, and it defaults to the window, which tiles the stream without overlap.window_classeslists classes that a later call can still merge with, anderror_statusreports a failed call without ending the stream.action_recognition_predictionwith its serializer and deserializer, andffmpegin the dev GPU image, without which upload FPS detection failed silently.Known limit. Zero-shot classifies a whole window, so its boundaries land on window edges. In-window localization is the fine-tune path.
Related Issue(s): consumes roboflow-train#880 and #884.
Type of Change
Testing
Test details:
Unit tests, all green in the CUDA staging container: 51 in the model layer (mode routing, lettered and phrase parsing, the fine-tune prompt and span format, per-call class subsets, the constrained-decoding automaton, the package readers, the malformed-package error, per-mode frame resolution) and 76 on the block (manifest, fire cadence, float sampling grid, FPS pinning, monotone merging, keep-alive
window_classes, whole-video mode, end-of-stream flush, resets, tensor paths). The stream handler suite adds 23, and the visualization suite 14.Live validation on an L4, through the workflow path with real weights:
out0–8 s,in8.5–15.5 s) returnsoutandinwith boundaries inside half a second of the labels, at the 8 s window its package declares. A 6 s clip, shorter than that window, is classified whole by the end-of-stream flush; before the flush it returned nothing.window_classesstays on for the event and clears when the range closes.ENABLE_TENSOR_DATA_REPRESENTATIONon and off.Checklist
Additional Context
The prompt and decode settings come from measurements against NVIDIA's own demo assets, not preference. Zero-shot cannot localize inside a window under a cheap decode: the SFT span contract, the cookbook dense-caption prompt, and the guide's timestamp query all collapse to positional templates without a think block, and neither a larger token budget, nor thinking, nor NVIDIA's sampling parameters, nor lower resolution recovers it. Thinking-on dense captioning does localize, at 20–90 s per call, which is why an earlier two-stage design was dropped. The fine-tune trains that ability into the cheap path, so the shipped zero-shot mode buys classification and the fine-tune buys boundaries.
Testing ran on an L4 staging build. Implementation used Codex agents for two of the larger changes, with review and live validation here.
🤖 Generated with Claude Code