Spaces:
Running
Running
Download backend/split_annotation.py from dghadiya/t2av_eval: direct link, hf CLI and curl.
- Browser
- Download file 8.96 kB
-
https://huggingface.co/spaces/dghadiya/t2av_eval/resolve/main/backend/split_annotation.py
- Command line
-
hf download hf://spaces/dghadiya/t2av_eval/backend/split_annotation.py
-
curl -L -o split_annotation.py https://huggingface.co/spaces/dghadiya/t2av_eval/resolve/main/backend/split_annotation.py
8.96 kB
| """Helpers for the separate event/time annotation workflow. | |
| The module is deliberately independent of FastAPI and Hugging Face I/O so | |
| the partial-answer rules can be checked locally. | |
| """ | |
| from fastapi import HTTPException | |
| from datetime import datetime, timezone | |
| EVENT_ROLE = "event" | |
| TIME_ROLE = "temporal" | |
| ROLES = (EVENT_ROLE, TIME_ROLE) | |
| QUALIFICATION_RESERVATION_STATUSES = {"in_progress", "retry_ready", "qualified"} | |
| COUNTED_QUALIFICATION_STATUSES = { | |
| "in_progress", "retry_ready", "qualified", | |
| } | |
| def qualification_passed(state): | |
| """Only a successfully qualified participant may start the main task.""" | |
| return isinstance(state, dict) and state.get("status") == "qualified" | |
| def qualification_reservation_expired(state, now, ttl_seconds): | |
| """Whether an unfinished qualification has been idle beyond its allowed time.""" | |
| if (not isinstance(state, dict) | |
| or state.get("status") not in QUALIFICATION_RESERVATION_STATUSES): | |
| return False | |
| timestamp = state.get("updated_at") or state.get("created_at") | |
| if not isinstance(timestamp, str): | |
| return False | |
| try: | |
| updated_at = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) | |
| if updated_at.tzinfo is None: | |
| updated_at = updated_at.replace(tzinfo=timezone.utc) | |
| except ValueError: | |
| return False | |
| return (now - updated_at).total_seconds() >= ttl_seconds | |
| def task_assignment_expired(state, now, ttl_seconds): | |
| """Whether an incomplete split task exceeded its absolute time limit.""" | |
| if not isinstance(state, dict) or state.get("status") != "in_progress": | |
| return False | |
| assignment = state.get("assignment") | |
| timestamp = assignment.get("created_at") if isinstance(assignment, dict) else None | |
| if not isinstance(timestamp, str): | |
| timestamp = state.get("created_at") | |
| if not isinstance(timestamp, str): | |
| return False | |
| try: | |
| started_at = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) | |
| if started_at.tzinfo is None: | |
| started_at = started_at.replace(tzinfo=timezone.utc) | |
| except ValueError: | |
| return False | |
| return (now - started_at).total_seconds() >= ttl_seconds | |
| def role_occupancy(allocation, qualification_states, now, ttl_seconds): | |
| """Count task members and live qualification reservations by role.""" | |
| counts = {role: 0 for role in ROLES} | |
| assigned_ids = set() | |
| for group in (allocation or {}).get("groups", []): | |
| members = group.get("members", {}) | |
| if not isinstance(members, dict): | |
| continue | |
| for participant_id, role in members.items(): | |
| if role in counts and participant_id not in assigned_ids: | |
| counts[role] += 1 | |
| assigned_ids.add(participant_id) | |
| for participant_id, state in (qualification_states or {}).items(): | |
| if (participant_id in assigned_ids | |
| or not isinstance(state, dict) | |
| or state.get("task_timeout_released") | |
| or state.get("status") not in COUNTED_QUALIFICATION_STATUSES | |
| or qualification_reservation_expired(state, now, ttl_seconds)): | |
| continue | |
| role = state.get("annotation_role") if isinstance(state, dict) else None | |
| if role in counts: | |
| counts[role] += 1 | |
| return counts | |
| def choose_available_role(counts, capacity_per_role, rng): | |
| """Balance roles while never assigning beyond a role's remaining capacity.""" | |
| candidates = [role for role in ROLES if counts.get(role, 0) < capacity_per_role] | |
| if not candidates: | |
| return None | |
| least_occupied = min(counts.get(role, 0) for role in candidates) | |
| return rng.choice([role for role in candidates if counts.get(role, 0) == least_occupied]) | |
| def validate_role_responses(item, responses, role): | |
| """Validate only the fields assigned to this role.""" | |
| if role not in ROLES: | |
| raise HTTPException(status_code=422, detail="Invalid annotation role") | |
| if not isinstance(responses, dict): | |
| raise HTTPException(status_code=422, detail="Invalid annotation responses") | |
| events = responses.get("events") | |
| video = responses.get("video") | |
| if not isinstance(events, dict): | |
| raise HTTPException(status_code=422, detail="Invalid event responses") | |
| missing = [] | |
| valid = {"PASS", "FAIL"} | |
| for event in item.get("events", []): | |
| event_id = event.get("id") | |
| answer = events.get(event_id) | |
| if not isinstance(answer, dict): | |
| missing.append(event_id) | |
| continue | |
| if role == EVENT_ROLE: | |
| axes = ("v",) if event.get("modality") == "silent_action" else ("a", "v", "avb") | |
| for axis in axes: | |
| value = answer.get(axis) | |
| if not isinstance(value, dict) or value.get("label") not in valid: | |
| missing.append(f"{event_id}.{axis}") | |
| else: | |
| value = answer.get("tcCount") | |
| count = value.get("value") if isinstance(value, dict) else None | |
| try: | |
| number = int(count) | |
| valid_count = not isinstance(count, bool) and number >= 0 | |
| if isinstance(count, float): | |
| valid_count = valid_count and count.is_integer() | |
| elif isinstance(count, str): | |
| valid_count = valid_count and count.isdigit() | |
| except (TypeError, ValueError): | |
| valid_count = False | |
| if not valid_count: | |
| missing.append(f"{event_id}.tcCount") | |
| if role == TIME_ROLE: | |
| relation = video.get("tcRel") if isinstance(video, dict) else None | |
| if not isinstance(relation, dict) or relation.get("label") not in valid: | |
| missing.append("video.tcRel") | |
| if missing: | |
| raise HTTPException( | |
| status_code=422, | |
| detail=f"Complete all required fields before saving: {', '.join(missing)}", | |
| ) | |
| def required_event_counts(item): | |
| events = item.get("events", []) | |
| counts = {event["id"]: 1 for event in events} | |
| constraint = item.get("temporal_constraint") or {} | |
| value = constraint.get("value") or {} | |
| relation = constraint.get("relation_type") | |
| if relation == "sync_count": | |
| for event_id in value.get("events", []): | |
| counts[event_id] = value.get("count", 1) | |
| elif relation == "order_count": | |
| counts.update(value.get("event_counts") or {}) | |
| elif relation == "count" and value.get("event"): | |
| counts[value["event"]] = value.get("count", 1) | |
| return counts | |
| def aggregate_role_axes(item, responses, role): | |
| """Return the hidden-GT axes measured by a role for qualification.""" | |
| event_answers = responses["events"] | |
| def aggregate(axis, include): | |
| values = [event_answers[event["id"]][axis]["label"] | |
| for event in item.get("events", []) if include(event)] | |
| return "PASS" if values and all(value == "PASS" for value in values) else "FAIL" | |
| if role == EVENT_ROLE: | |
| has_audio = lambda event: event.get("modality") != "silent_action" | |
| return { | |
| "A": aggregate("a", has_audio), | |
| "V": aggregate("v", lambda _event: True), | |
| "AVB": aggregate("avb", has_audio), | |
| } | |
| counts = required_event_counts(item) | |
| matches = all( | |
| int(event_answers[event_id]["tcCount"]["value"]) == int(expected) | |
| for event_id, expected in counts.items() | |
| ) | |
| return { | |
| "TCREL": responses["video"]["tcRel"]["label"], | |
| "TCCNT": "PASS" if matches else "FAIL", | |
| } | |
| def score_role_answer(item, responses, expected, role): | |
| actual = aggregate_role_axes(item, responses, role) | |
| correct = sum(actual[axis] == expected[axis] for axis in actual) | |
| return correct, len(actual) | |
| def merge_role_responses(item, event_responses, temporal_responses): | |
| """Combine one event part and one temporal part, applying hierarchy rules. | |
| The input temporal part contains observed raw counts. The returned answer | |
| follows the existing full-answer shape for downstream analysis. | |
| """ | |
| events = {} | |
| event_failed = False | |
| for event in item.get("events", []): | |
| event_id = event["id"] | |
| event_part = event_responses["events"][event_id] | |
| time_part = temporal_responses["events"][event_id] | |
| combined = {key: value for key, value in event_part.items() if key in {"a", "v", "avb"}} | |
| count = time_part["tcCount"] | |
| failed = any( | |
| isinstance(value, dict) and value.get("label") == "FAIL" | |
| for key, value in combined.items() if key in {"a", "v", "avb"} | |
| ) | |
| if failed: | |
| event_failed = True | |
| count = {**count, "value": 0} | |
| combined["tcCount"] = count | |
| events[event_id] = combined | |
| tc_rel = temporal_responses["video"]["tcRel"] | |
| if event_failed: | |
| tc_rel = {**tc_rel, "label": "FAIL"} | |
| return {"events": events, "video": {"tcRel": tc_rel}} | |