Download flora_captioner.py from finerobotics/annotation-studio: direct link, hf CLI and curl.
- Browser
- Download file 16.3 kB
-
https://huggingface.co/spaces/finerobotics/annotation-studio/resolve/main/flora_captioner.py
- Command line
-
hf download hf://spaces/finerobotics/annotation-studio/flora_captioner.py
-
curl -L -o flora_captioner.py https://huggingface.co/spaces/finerobotics/annotation-studio/resolve/main/flora_captioner.py
16.3 kB
| """Independent native Flora TempSeg → Captioner workflow on a CUDA Job. | |
| The default profile uses NVIDIA's reference decoder. The opt-in accelerated | |
| profile batches independent requests and requires fused GDN kernels; both use | |
| the same planner, processor, mixed precision loader and greedy decoding policy. | |
| """ | |
| import asyncio | |
| import hashlib | |
| import os | |
| import sys | |
| import tempfile | |
| import threading | |
| import time | |
| from concurrent.futures import ThreadPoolExecutor | |
| from fractions import Fraction | |
| from itertools import islice | |
| from pathlib import Path | |
| FLORA = { | |
| "model_id": "nvidia/Flora-Captioner", | |
| "code_revision": "f736ca36925ae55d2f7915993ed1bc9f87288bb3", | |
| "weights_revision": "fef0bbfe11829ca7f3539d58da7a6a2c2f109dc6", | |
| "segmenter_id": "nvidia/Flora-TempSeg", | |
| "segmenter_revision": "114a1076f70eb53f4a58ac045505db3087e79347", | |
| "segmenter_sha256": "1d80fe544e4cd1a7ff6b0ea4be71e988972408dab81d4c8af72e3a628739d6c3", | |
| "segmenter_base_id": "Qwen/Qwen3.5-9B", | |
| "segmenter_base_revision": "c202236235762e1c871ad0ccb60c8ee5ba337b9a", | |
| "temporal_profile": "yam", | |
| "hardware": "h200", | |
| "resident_models": True, | |
| "license": "NVIDIA OneWay Noncommercial License", | |
| "role": "TempSeg predicts and stitches intervals; Captioner writes their labels independently", | |
| } | |
| def reference_runtime(): | |
| from huggingface_hub import snapshot_download | |
| # Reference modules resolve __file__; Hub cache symlinks would resolve into | |
| # blobs/ and lose sibling config/vendor files. Materialize a real code tree. | |
| directory = str( | |
| Path(os.environ.get("FLORA_CODE_CACHE", Path.home() / ".cache/flora/code")) / FLORA["code_revision"] | |
| ) | |
| snapshot_download( | |
| FLORA["model_id"], | |
| revision=FLORA["code_revision"], | |
| allow_patterns=["batch_utils/**"], | |
| local_dir=directory, | |
| ) | |
| sys.path.insert(0, directory) | |
| from batch_utils.bootstrap import production | |
| from batch_utils.config import MODELS | |
| from batch_utils.models import Session, generate | |
| from batch_utils.preprocessing import instruction_suffix | |
| from flora_unified.unified_input import load_frames, prompt | |
| expected = { | |
| "tempseg": (FLORA["segmenter_id"], FLORA["segmenter_revision"]), | |
| "tempseg_base": (FLORA["segmenter_base_id"], FLORA["segmenter_base_revision"]), | |
| "captioner": (FLORA["model_id"], FLORA["weights_revision"]), | |
| } | |
| if any((MODELS[k]["repo"], MODELS[k]["revision"]) != value for k, value in expected.items()): | |
| raise ValueError("Native Flora model revisions differ from the reviewed configuration") | |
| if MODELS["tempseg"]["sha256"] != FLORA["segmenter_sha256"]: | |
| raise ValueError("Native Flora adapter checksum differs from the reviewed configuration") | |
| return production, Session, generate, instruction_suffix, load_frames, prompt | |
| class TemporalPredictor: | |
| """Adapt LeRobot frame access to NVIDIA's unchanged window/stitch engine.""" | |
| def __init__(self, engine, source, binding, record, frames): | |
| self.engine, self.source, self.binding = engine, source, binding | |
| self.record, self.frames = record, frames | |
| def predict(self, request): | |
| from batch_utils.preprocessing import temporal_inputs | |
| from flora_runtime.video_segmentation.fine_subtask import FINE_RESPONSE_SCHEMA | |
| if ( | |
| request["source"]["source_id"] != self.source.source_id | |
| or request["model_binding"] != self.binding | |
| ): | |
| raise ValueError("Native Flora temporal request binding changed") | |
| production = self.engine.reference[0] | |
| pts_us = [Fraction(str(t)) * 1_000_000 for t in self.record.frame_timestamps] | |
| predictions, payloads = [], iter(request["windows"]) | |
| while batch := list(islice(payloads, self.engine.batch_size)): | |
| windows, requests = [], [] | |
| for payload in batch: | |
| self.engine.check_deadline() | |
| window = production.window_from_payload(payload, self.source) | |
| plan = production.temporal_frame_plan(window, pts_us) | |
| images = self.frames.frames_at( | |
| self.record, [self.record.frame_timestamps[i] for i in plan["source_frame_indices"]] | |
| ) | |
| inputs, _ = temporal_inputs( | |
| images, | |
| window, | |
| FLORA["temporal_profile"], | |
| self.engine.processor, | |
| ["main"], | |
| self.record.episode_task or "", | |
| ) | |
| windows.append(window) | |
| requests.append(inputs) | |
| for window, prediction in zip(windows, self.engine.generate(requests, 1024), strict=True): | |
| predictions.append({"scope_id": window.scope_id, "prediction_json": prediction}) | |
| return { | |
| "schema_version": FINE_RESPONSE_SCHEMA, | |
| "source_id": self.source.source_id, | |
| "model_io_contract": self.binding["model_io_contract"], | |
| "model_binding": self.binding, | |
| "predictions": predictions, | |
| } | |
| class FloraCaptioner: | |
| def __init__( | |
| self, | |
| deadline=None, | |
| cache_models=False, | |
| execution="reference", | |
| batch_size=4, | |
| vision_frame_batching=False, | |
| ): | |
| if execution not in {"reference", "accelerated"}: | |
| raise ValueError("Unknown Flora execution profile") | |
| if not isinstance(batch_size, int) or isinstance(batch_size, bool) or not 1 <= batch_size <= 8: | |
| raise ValueError("Flora batch size must be between 1 and 8") | |
| self.lock = threading.RLock() | |
| self.deadline = deadline | |
| self.execution = execution | |
| if type(vision_frame_batching) is not bool or vision_frame_batching and execution != "accelerated": | |
| raise ValueError("Vision frame batching requires the accelerated Flora profile") | |
| self.vision_frame_batching = vision_frame_batching | |
| self.batch_size = batch_size if execution == "accelerated" else 1 | |
| self.kernel_versions = {} | |
| if execution == "accelerated": | |
| from flora_acceleration import require_fast_kernels | |
| self.kernel_versions = require_fast_kernels() | |
| self.reference = reference_runtime() | |
| self.cache_models = cache_models or execution == "accelerated" | |
| self.session = None if self.cache_models else self.reference[1]() | |
| self.sessions = {} | |
| self.cached_models = {} | |
| self.model = self.processor = None | |
| self.loaded_kind = None | |
| self.prefetch_pool = None | |
| self.prefetch = None | |
| if execution == "accelerated": | |
| from batch_utils.models import snapshot, validate_manifest | |
| def prepare_captioner(): | |
| path = snapshot("captioner") | |
| # Session.load still invokes the native validator and checks | |
| # every fingerprint before reusing this full checksum pass. | |
| validate_manifest(path) | |
| return path | |
| self.prefetch_pool = ThreadPoolExecutor(max_workers=1) | |
| self.prefetch = self.prefetch_pool.submit(prepare_captioner) | |
| def close(self): | |
| if self.prefetch_pool is not None: | |
| self.prefetch_pool.shutdown(wait=True, cancel_futures=True) | |
| self.model = self.processor = None | |
| self.cached_models.clear() | |
| for session in self.sessions.values(): | |
| session.close() | |
| self.sessions.clear() | |
| if self.session is not None: | |
| self.session.close() | |
| self.loaded_kind = None | |
| def check_resident_capacity(self): | |
| import torch | |
| # The native models use ~18 + 51 GiB before activations. This mode | |
| # targets the H200, leaving room for both models and their KV caches. | |
| if torch.cuda.get_device_properties(0).total_memory < 120 * 1024**3: | |
| raise ValueError("Keeping both Flora models loaded requires an H200-class GPU with 120 GiB") | |
| def check_deadline(self): | |
| if self.deadline and time.time() >= self.deadline: | |
| from provider import DeadlineExceededError | |
| raise DeadlineExceededError("Flora Job time limit reached; completed episodes are saved.") | |
| def annotate(self, record, frames, fps): | |
| """Predict boundaries with TempSeg, then caption them without a provider call.""" | |
| with self.lock: | |
| spans = self.segment(record, frames, fps) | |
| return self.relabel(record, spans, frames, fps) | |
| def segment(self, record, frames, fps): | |
| production = self.reference[0] | |
| if fps < 4 or not record.frame_timestamps: | |
| raise ValueError("Flora temporal segmentation needs at least 4 source FPS") | |
| if any(abs(t - i / fps) > 1e-4 for i, t in enumerate(record.frame_timestamps)): | |
| raise ValueError("The Studio Flora robot profile needs an episode-relative constant-rate clock") | |
| self.load("tempseg") | |
| source = production.AnnotationSource( | |
| source_id=f"studio_episode_{record.episode_index}", | |
| location=f"lerobot:episode/{record.episode_index}", | |
| # NVIDIA's scheduler accepts integral microseconds, matching its | |
| # native media probe. Frame counts / 30 often repeat in decimal. | |
| duration_s=round(len(record.frame_timestamps) / fps * 1_000_000) / 1_000_000, | |
| fps=float(fps), | |
| ) | |
| pool = FLORA["temporal_profile"] | |
| binding = { | |
| "candidate_id": "flora_final", | |
| "checkpoint_sha256": FLORA["segmenter_sha256"], | |
| "model_io_contract": production.fine_window_profile(production.PROFILES[pool]).model_io_contract, | |
| } | |
| predictor = TemporalPredictor(self, source, binding, record, frames) | |
| native = production.production_segmenter(predictor, binding, pool) | |
| segments = asyncio.run(native.segment_async(source)) | |
| if not segments: | |
| raise ValueError("Flora TempSeg did not return any subtask intervals") | |
| return [ | |
| { | |
| "start": segment.span.start_s, | |
| "end": segment.span.end_s, | |
| "metadata": dict(segment.metadata), | |
| } | |
| for segment in segments | |
| ] | |
| def samples(self, record, spans, frames, fps, directory): | |
| """Native duration-adaptive BEFORE/TARGET/AFTER plans without reference text.""" | |
| production, _, _, suffix, load_frames, prompt = self.reference | |
| rate = Fraction(str(fps)) | |
| plans, dispositions = production.caption_plans( | |
| [ | |
| {"start_s": span["start"], "end_s": span["end"], "metadata": span.get("metadata", {})} | |
| for span in spans | |
| ], | |
| { | |
| "fps_num": rate.numerator, | |
| "fps_den": rate.denominator, | |
| "frame_count": len(record.frame_timestamps), | |
| }, | |
| ) | |
| wanted = sorted({i for plan in plans for block in plan["blocks"] for i in block["frame_indices"]}) | |
| images = frames.frames_at(record, [record.frame_timestamps[i] for i in wanted]) | |
| receipts = {} | |
| for index, image in zip(wanted, images, strict=True): | |
| path = directory / f"{index:012d}.png" | |
| image.convert("RGB").save(path, format="PNG") | |
| receipts[index] = { | |
| "path": path.name, | |
| "png_sha256": hashlib.sha256(path.read_bytes()).hexdigest(), | |
| "size": list(image.size), | |
| } | |
| by_event = {plan["event_id"]: plan for plan in plans} | |
| for disposition in dispositions: | |
| plan = by_event.get(disposition["event_id"]) | |
| if plan is None: | |
| yield {"status": disposition["status"], "index": disposition["event_index"]} | |
| continue | |
| sample = production.caption_sample(plan, receipts, ["main"]) | |
| blocks, _ = load_frames(sample, directory) | |
| content = prompt(sample, self.processor.video_token) + suffix(record.episode_task) | |
| yield { | |
| "status": "ready", | |
| "index": plan["event_index"], | |
| "plan": plan, | |
| "sample": sample, | |
| "frames": blocks, | |
| "content": content, | |
| } | |
| def caption_inputs(self, sample): | |
| from transformers.video_utils import VideoMetadata | |
| self.check_deadline() | |
| proc = self.processor | |
| text = proc.apply_chat_template( | |
| [{"role": "user", "content": sample["content"]}], | |
| tokenize=False, | |
| add_generation_prompt=True, | |
| enable_thinking=False, | |
| ) | |
| metadata = [ | |
| VideoMetadata( | |
| total_num_frames=len(frames), | |
| fps=sample["plan"]["source_fps_num"] / sample["plan"]["source_fps_den"], | |
| frames_indices=list(block["block_local_timestamp_indices"]), | |
| ) | |
| for frames, block in zip(sample["frames"], sample["sample"]["blocks"], strict=True) | |
| ] | |
| inputs = proc( | |
| text=[text], | |
| videos=sample["frames"], | |
| video_metadata=metadata, | |
| do_sample_frames=False, | |
| return_metadata=True, | |
| return_tensors="pt", | |
| ) | |
| return inputs | |
| def generate(self, requests, output_limit): | |
| """Retain reference batch-one behavior unless acceleration is explicitly selected.""" | |
| if self.execution == "accelerated": | |
| from flora_acceleration import generate_many | |
| return generate_many( | |
| self.model, | |
| self.processor, | |
| requests, | |
| output_limit, | |
| batch_size=self.batch_size, | |
| check_deadline=self.check_deadline, | |
| ) | |
| return [self.reference[2](self.model, self.processor, inputs, output_limit) for inputs in requests] | |
| def infer(self, sample): | |
| return self.generate([self.caption_inputs(sample)], 512)[0] | |
| def load(self, kind="captioner"): | |
| self.check_deadline() | |
| if kind == "captioner" and self.prefetch is not None: | |
| self.prefetch.result() | |
| if self.cache_models: | |
| if kind not in self.cached_models: | |
| self.check_resident_capacity() | |
| # Session.load closes its own previous model. A separate native | |
| # Session per kind preserves both verified model instances. | |
| session = self.reference[1]() | |
| model, processor = session.load(kind) | |
| if self.vision_frame_batching: | |
| from batch_utils.models import runtime_event | |
| from flora_vision import configure_vision_batching | |
| layers = configure_vision_batching(model) | |
| runtime_event("vision_frames_batched", kind=kind, layers=layers, backend="sdpa") | |
| self.cached_models[kind] = model, processor | |
| self.sessions[kind] = session | |
| self.model, self.processor = self.cached_models[kind] | |
| self.loaded_kind = kind | |
| return | |
| if self.model is None or self.loaded_kind != kind: | |
| # Release our references before Session closes the preceding model. | |
| self.model = self.processor = None | |
| self.model, self.processor = self.session.load(kind) | |
| self.loaded_kind = kind | |
| def relabel(self, record, spans, frames, fps): | |
| with self.lock, tempfile.TemporaryDirectory(prefix="flora-caption-") as temporary: | |
| self.load() | |
| result = [dict(span) for span in spans] | |
| samples = iter(self.samples(record, spans, frames, fps, Path(temporary))) | |
| while batch := list(islice(samples, self.batch_size)): | |
| if any(sample["status"] != "ready" for sample in batch): | |
| raise ValueError("Flora cannot caption a source-limited target interval") | |
| captions = self.generate([self.caption_inputs(sample) for sample in batch], 512) | |
| for sample, caption in zip(batch, captions, strict=True): | |
| if not caption.strip(): | |
| raise ValueError("Flora returned an empty caption") | |
| result[sample["index"]]["text"] = caption.strip() | |
| return result | |