annotation-studio / flora_captioner.py
pepijn223's picture
pepijn223 HF Staff
Enable verified Flora batching: 25 ABC episodes fully exported in 15m24s
4595804 verified
Raw History Blame Contribute Delete
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