from __future__ import annotations from collections import defaultdict from collections.abc import Callable from pathlib import Path from typing import Any from .json_io import read_json from .feedback_boxes import valid_feedback_box Log = Callable[[str], None] def apply_checkbox_actions( labels_data: dict[str, dict[str, Any]], actions: list[dict[str, Any]], log: Log, ) -> set[str]: actions_by_label: defaultdict[str, list[dict[str, Any]]] = defaultdict(list) for action in actions: actions_by_label[str(action.get("label", ""))].append(action) dirty_labels: set[str] = set() for label, label_actions in actions_by_label.items(): if label not in labels_data: continue result = labels_data[label]["result"] feedbacks = result.get("feedback", []) # Match the renderer's fallback for invalid boxes so checkbox indices # still address the right comment when returned annotations are read. global_feedbacks = [item for item in feedbacks if not valid_feedback_box(item.get("box_2d"))] local_feedbacks = [item for item in feedbacks if valid_feedback_box(item.get("box_2d"))] local_feedbacks.sort(key=lambda item: item["box_2d"][0]) for action in label_actions: action_type = action.get("type") if action_type == "score": result["score"] = action.get("value") dirty_labels.add(label) log(f" > Updated score for {label} to {action.get('value')}") elif action_type == "clear_all": for feedback in feedbacks: feedback["to_delete"] = True if feedback.get("box_2d"): feedback["norectangle"] = True dirty_labels.add(label) log(f" > Cleared all feedbacks in {label}") elif action_type == "del_global": index = int(action.get("index", -1)) if 0 <= index < len(global_feedbacks): global_feedbacks[index]["to_delete"] = True dirty_labels.add(label) log(f" > Deleted global feedback in {label}") elif action_type in {"del_local", "del_local_rect"}: index = int(action.get("index", -1)) if 0 <= index < len(local_feedbacks): target = local_feedbacks[index] if action_type == "del_local": target["to_delete"] = True log(f" > Deleted local feedback in {label}") else: target["norectangle"] = True log(f" > Deleted rectangle in {label}") dirty_labels.add(label) return dirty_labels def apply_score_overrides( labels_data: dict[str, dict[str, Any]], score_path: Path, log: Log, ) -> set[str]: if not score_path.exists(): return set() loaded = read_json(score_path) if not isinstance(loaded, dict): raise TypeError(f"Expected a JSON object in {score_path}") dirty: set[str] = set() for label, score in loaded.items(): if label not in labels_data: continue current = str(labels_data[label]["result"].get("score", 0)) if current != str(score): labels_data[label]["result"]["score"] = score dirty.add(label) log(f" > Overrode score for {label} to {score} from score.json") return dirty