Update llm assisted annotation
This commit is contained in:
+48
-15
@@ -27,6 +27,8 @@ from threading import Lock
|
||||
from typing import Any, Iterable
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from prepare_easycom_eot_int_review import requires_semantic_adjudication
|
||||
|
||||
|
||||
SOURCE_FPS = 20.0
|
||||
STATIC_ROOT = Path(__file__).resolve().parent / "floor_event_review_web"
|
||||
@@ -201,18 +203,22 @@ def _candidate_digest(path: Path, source_row: int, payload: dict[str, Any]) -> s
|
||||
return hashlib.sha256(encoded).hexdigest()[:20]
|
||||
|
||||
|
||||
def load_candidates(paths: Iterable[Path]) -> list[Candidate]:
|
||||
def load_candidates(
|
||||
paths: Iterable[Path], *, include_offset_controls: bool = False
|
||||
) -> list[Candidate]:
|
||||
candidates: list[Candidate] = []
|
||||
ids: set[str] = set()
|
||||
for path in paths:
|
||||
for source_row, payload in enumerate(_read_jsonl(path), 1):
|
||||
if not include_offset_controls and not requires_semantic_adjudication(payload):
|
||||
continue
|
||||
candidate_id = _candidate_digest(path, source_row, payload)
|
||||
if candidate_id in ids:
|
||||
raise ValueError(f"Duplicate candidate identity in {path}:{source_row}")
|
||||
ids.add(candidate_id)
|
||||
candidates.append(Candidate(candidate_id, path, source_row, payload))
|
||||
if not candidates:
|
||||
raise ValueError("Candidate files contain no rows")
|
||||
raise ValueError("Candidate files contain no primary review rows")
|
||||
return candidates
|
||||
|
||||
|
||||
@@ -342,12 +348,20 @@ class CandidateRepository:
|
||||
"ai_subtype": row.get("ai_subtype"),
|
||||
"participant_role": row.get("participant_role") or row.get("holder_role"),
|
||||
"event_time_seconds": event_time,
|
||||
"region_start_seconds": row.get("region_start_seconds", event_time),
|
||||
"region_end_seconds": row.get("region_end_seconds", event_time),
|
||||
"reviewed": reviewed,
|
||||
}
|
||||
|
||||
def detail(self, candidate: Candidate) -> dict[str, Any]:
|
||||
clip_stem, local_seconds, transcript_index = self._location(candidate)
|
||||
transcript_rows: list[dict[str, Any]] = []
|
||||
evidence_rows = candidate.payload.get("speaker_timeline") or candidate.payload.get("context") or []
|
||||
wearer_ids = {
|
||||
row.get("participant_id")
|
||||
for row in evidence_rows
|
||||
if isinstance(row, dict) and row.get("participant_role") == "wearer"
|
||||
}
|
||||
if clip_stem is not None:
|
||||
for index, row in enumerate(self._transcript(candidate.session, clip_stem)):
|
||||
try:
|
||||
@@ -362,10 +376,14 @@ class CandidateRepository:
|
||||
continue
|
||||
transcript_rows.append(
|
||||
{
|
||||
"uid": f"{candidate.session}/{clip_stem}/{index:04d}",
|
||||
"index": index,
|
||||
"start_seconds": start,
|
||||
"end_seconds": end,
|
||||
"participant_id": row.get("Participant_ID"),
|
||||
"participant_role": (
|
||||
"wearer" if row.get("Participant_ID") in wearer_ids else "other"
|
||||
),
|
||||
"target_of_speech": row.get("Target_of_Speech"),
|
||||
"transcription": str(row.get("Transcription", "")),
|
||||
"is_anchor": (
|
||||
@@ -478,14 +496,13 @@ class ReviewStore:
|
||||
original_event_time=candidate.get("event_time", candidate.get("anchor_dense_seconds")),
|
||||
)
|
||||
|
||||
reviewer = str(payload.get("reviewer", "")).strip()
|
||||
notes = str(payload.get("notes", "")).strip()
|
||||
if len(reviewer) > 100:
|
||||
raise ValueError("reviewer is too long")
|
||||
if not reviewer:
|
||||
raise ValueError("reviewer is required")
|
||||
if len(notes) > 4000:
|
||||
raise ValueError("notes are too long")
|
||||
# Keep an empty compatibility field in exports written by the reviewer-less UI.
|
||||
reviewer = str(payload.get("reviewer", "")).strip()
|
||||
if len(reviewer) > 100:
|
||||
raise ValueError("reviewer is too long")
|
||||
normalized["reviewer"] = reviewer
|
||||
normalized["notes"] = notes
|
||||
return normalized
|
||||
@@ -633,6 +650,20 @@ def parse_byte_range(value: str | None, size: int) -> tuple[int, int] | None:
|
||||
return start, min(end, size - 1)
|
||||
|
||||
|
||||
def stream_file_range(handle: Any, write: Any, remaining: int) -> bool:
|
||||
"""Stream a media range; return False when the browser cancels normally."""
|
||||
try:
|
||||
while remaining:
|
||||
chunk = handle.read(min(64 * 1024, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
write(chunk)
|
||||
remaining -= len(chunk)
|
||||
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class ReviewHTTPServer(ThreadingHTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
@@ -814,13 +845,7 @@ class ReviewRequestHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
with path.open("rb") as handle:
|
||||
handle.seek(start)
|
||||
remaining = end - start + 1
|
||||
while remaining:
|
||||
chunk = handle.read(min(64 * 1024, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
remaining -= len(chunk)
|
||||
stream_file_range(handle, self.wfile.write, end - start + 1)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
@@ -842,6 +867,12 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument("--port", type=int, default=8765)
|
||||
parser.add_argument("--context-before-seconds", type=float, default=3.0)
|
||||
parser.add_argument("--context-after-seconds", type=float, default=3.0)
|
||||
parser.add_argument(
|
||||
"--include-controls", "--include-offset-controls",
|
||||
dest="include_offset_controls",
|
||||
action="store_true",
|
||||
help="Include deterministic wearer-resume and non-overlap offset controls for a separate audit.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -859,7 +890,9 @@ def main() -> None:
|
||||
|
||||
output_path = args.output.expanduser().resolve()
|
||||
candidate_paths = expand_candidate_paths(args.events, output_path=output_path)
|
||||
candidates = load_candidates(candidate_paths)
|
||||
candidates = load_candidates(
|
||||
candidate_paths, include_offset_controls=args.include_offset_controls
|
||||
)
|
||||
repository = CandidateRepository(
|
||||
candidates,
|
||||
dataset_root,
|
||||
|
||||
Reference in New Issue
Block a user