annotation-studio / benchmark_flora.py
pepijn223's picture
pepijn223 HF Staff
Use scalable annotation runtime, shared human-video module and optional Flora captions
fb4d72b verified
Raw History Blame Contribute Delete
7.99 kB
"""Paired native-Flora/Qwen3.8 captioning on frozen ABC reference intervals.
Run in a bounded GPU Job. Input contains interval boundaries and episode tasks,
never reference caption text. Each target is saved before continuing.
"""
import base64
import io
import json
import os
import tempfile
import time
from pathlib import Path
from types import SimpleNamespace
import av
import requests
from flora_captioner import FLORA, FloraCaptioner
from huggingface_hub import HfApi, hf_hub_download
from PIL import Image
QWEN = "Qwen/Qwen3.8-27B:novita"
class NativeFrames:
def __init__(self, entry, path):
self.entry, self.path, self.cache = entry, path, {}
def frames_at(self, record, timestamps):
fps = self.entry["fps"]
indices = [min(self.entry["length"] - 1, max(0, round(t * fps))) for t in timestamps]
with av.open(str(self.path)) as container:
stream = container.streams.video[0]
stream.codec_context.thread_count = 2
for index in sorted(set(indices) - self.cache.keys()):
target = self.entry["video_offset"] + index / fps
container.seek(int(target / stream.time_base), stream=stream, backward=True)
closest, distance = None, float("inf")
for frame in container.decode(stream):
if frame.pts is None:
continue
stamp = float(frame.pts * stream.time_base)
if abs(stamp - target) < distance:
closest, distance = frame, abs(stamp - target)
if stamp >= target:
break
if closest is None or distance > 1 / fps + 1e-4:
raise ValueError("Source frame is missing or outside its expected clock")
self.cache[index] = closest.to_image().convert("RGB")
return [self.cache[index] for index in indices]
def qwen_messages(sample, record):
content = [
{
"type": "text",
"text": (
"Write one concise, self-contained English imperative for the local operation attempted in TARGET. "
"Identify concrete objects and the working hand. Use BEFORE/AFTER only to disambiguate TARGET; "
"do not include their operations. Do not invent hidden objects, goals or success. "
"Return only the instruction, on one line. All frames use one top camera. "
f"Episode instruction: {record.episode_task}"
),
}
]
for images, block in zip(sample["frames"], sample["sample"]["blocks"], strict=True):
content.append({"type": "text", "text": block["role"] + ":"})
for frame, index in zip(images, block["original_descriptor"]["frame_indices"], strict=True):
image = (frame if isinstance(frame, Image.Image) else Image.fromarray(frame)).convert("RGB")
buffer = io.BytesIO()
image.save(buffer, format="JPEG", quality=84)
content.extend(
[
{"type": "text", "text": f"Source frame {index}"},
{
"type": "image_url",
"image_url": {
"url": "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode()
},
},
]
)
return [{"role": "user", "content": content}]
def qwen_caption(messages, state):
# Reserve a full million-token context before dispatch, reconcile reported usage.
reservation = (1_000_000 * 0.42 + 512 * 3.0) / 1e6
if state["reserved_usd"] + reservation > 3:
raise RuntimeError("Qwen inference cap reached")
state["reserved_usd"] += reservation
response = requests.post(
"https://router.huggingface.co/v1/chat/completions",
headers={"Authorization": "Bearer " + os.environ["HF_TOKEN"]},
json={
"model": QWEN,
"messages": messages,
"temperature": 0,
"max_tokens": 512,
"chat_template_kwargs": {"enable_thinking": False},
},
timeout=(20, 300),
)
if not response.ok:
raise RuntimeError(f"Qwen HTTP {response.status_code}; no automatic paid retry")
result = response.json()
usage = result.get("usage", {})
if all(type(usage.get(key)) is int for key in ("prompt_tokens", "completion_tokens")):
cost = (usage["prompt_tokens"] * 0.42 + usage["completion_tokens"] * 3) / 1e6
state["reserved_usd"] += cost - reservation
state["measured_usd"] += cost
choice = result["choices"][0]
if choice.get("finish_reason") == "length":
raise ValueError("Qwen response exceeded output limit")
caption = (choice["message"].get("content") or "").strip()
if not caption:
raise ValueError("Qwen returned an empty caption")
return caption
def main():
api = HfApi()
repo = os.environ["BENCHMARK_REPO"]
revision = os.environ["BENCHMARK_REVISION"]
inputs = json.loads(
Path(hf_hub_download(repo, "inputs.json", repo_type="dataset", revision=revision)).read_text()
)
work = Path("/workspace/results")
work.mkdir(parents=True, exist_ok=True)
state = {
"flora": FLORA,
"qwen": QWEN,
"input_revision": revision,
"reserved_usd": 0.0,
"measured_usd": 0.0,
"episodes": [],
"started_at": time.time(),
"status": "running",
}
def save():
(work / "run.json").write_text(json.dumps(state, indent=2))
api.upload_folder(
repo_id=repo,
repo_type="dataset",
folder_path=work,
path_in_repo="results",
commit_message="Checkpoint paired caption benchmark",
)
engine = FloraCaptioner()
try:
engine.load()
for entry in inputs["episodes"]:
path = hf_hub_download(
inputs["repo_id"], entry["video_path"], repo_type="dataset", revision=inputs["revision"]
)
record = SimpleNamespace(
episode_index=entry["episode_index"],
episode_task=entry["instruction"],
frame_timestamps=tuple(i / entry["fps"] for i in range(entry["length"])),
)
frames = NativeFrames(entry, path)
result = {"episode_index": entry["episode_index"], "segments": [], "status": "running"}
state["episodes"].append(result)
with tempfile.TemporaryDirectory(prefix="flora-abc-") as temporary:
for sample in engine.samples(record, entry["spans"], frames, entry["fps"], Path(temporary)):
span = entry["spans"][sample["index"]]
output = {**span, "index": sample["index"], "status": sample["status"]}
result["segments"].append(output)
if sample["status"] == "ready":
output["input_blocks"] = sample["plan"]["blocks"]
started = time.monotonic()
output["flora_caption"] = engine.infer(sample).strip()
output["flora_seconds"] = time.monotonic() - started
started = time.monotonic()
output["qwen_caption"] = qwen_caption(qwen_messages(sample, record), state)
output["qwen_seconds"] = time.monotonic() - started
output["status"] = "complete"
save()
result["status"] = "complete"
frames.cache.clear()
state["status"] = "complete"
except Exception as exc:
state.update(status="failed", error_type=type(exc).__name__, error=str(exc)[:300])
raise
finally:
state["finished_at"] = time.time()
save()
engine.close()
if __name__ == "__main__":
main()