Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +27 -0
- LICENSE +21 -0
- README.md +110 -7
- __init__.py +28 -0
- client.py +8 -0
- clip_quality_env.egg-info/PKG-INFO +18 -0
- clip_quality_env.egg-info/SOURCES.txt +33 -0
- clip_quality_env.egg-info/dependency_links.txt +1 -0
- clip_quality_env.egg-info/entry_points.txt +2 -0
- clip_quality_env.egg-info/requires.txt +12 -0
- clip_quality_env.egg-info/top_level.txt +2 -0
- clip_quality_env/__init__.py +38 -0
- clip_quality_env/agent.py +92 -0
- clip_quality_env/client.py +34 -0
- clip_quality_env/difficulty.py +46 -0
- clip_quality_env/env.py +450 -0
- clip_quality_env/generator.py +207 -0
- clip_quality_env/grader.py +142 -0
- clip_quality_env/ground_truth.py +125 -0
- clip_quality_env/models.py +137 -0
- clip_quality_env/real_clips.py +121 -0
- clip_quality_env/rubric.py +343 -0
- clip_quality_env/train.py +75 -0
- data/real_clips_manifest.jsonl +20 -0
- data/seed_gt.json +22 -0
- inference.py +248 -0
- models.py +29 -0
- openenv.yaml +16 -0
- pyproject.toml +30 -0
- requirements.txt +11 -0
- requirements_extractor.txt +4 -0
- scripts/extract_mp4_metadata.py +696 -0
- server/__init__.py +1 -0
- server/app.py +963 -0
- server/baseline_runs.py +160 -0
- server/clip_quality_environment.py +9 -0
- server/environment.py +8 -0
- server/grader.py +133 -0
- server/requirements.txt +8 -0
- server/tasks/__init__.py +13 -0
- server/tasks/task_easy.py +159 -0
- server/tasks/task_hard.py +160 -0
- server/tasks/task_medium.py +159 -0
- spaces_app.py +64 -0
- state/ground_truth.json +198 -0
- state/history.jsonl +98 -0
- tests/test_baseline_runs.py +112 -0
- tests/test_environment.py +354 -0
- tests/test_grader.py +139 -0
- tests/test_inference.py +72 -0
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
build-essential \
|
| 7 |
+
ffmpeg \
|
| 8 |
+
curl \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Copy requirements first for better caching
|
| 12 |
+
COPY server/requirements.txt /app/requirements.txt
|
| 13 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 14 |
+
|
| 15 |
+
# Copy all source code
|
| 16 |
+
COPY . /app
|
| 17 |
+
|
| 18 |
+
EXPOSE 8000
|
| 19 |
+
|
| 20 |
+
ENV PYTHONUNBUFFERED=1
|
| 21 |
+
ENV PYTHONPATH=/app
|
| 22 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 23 |
+
|
| 24 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
| 25 |
+
CMD sh -lc 'curl -f http://localhost:${PORT:-8000}/health || exit 1'
|
| 26 |
+
|
| 27 |
+
CMD ["sh", "-lc", "python -m uvicorn server.app:app --host 0.0.0.0 --port ${PORT:-8000}"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 elix3r
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,11 +1,114 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CLIP Quality Analyzer
|
| 3 |
+
colorFrom: purple
|
| 4 |
+
colorTo: gray
|
|
|
|
| 5 |
sdk: docker
|
| 6 |
+
app_port: 8000
|
| 7 |
+
base_path: /dashboard/
|
| 8 |
+
tags:
|
| 9 |
+
- openenv
|
| 10 |
+
- reinforcement-learning
|
| 11 |
+
- clip-quality
|
| 12 |
+
- quality-analysis
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# CLIP Quality Analyzer Environment
|
| 16 |
+
|
| 17 |
+
ClipQualityEnv is an OpenEnv-compliant RL environment for CLIP quality analysis workflows. It keeps the reference OpenEnv structure while presenting a clip-quality review and classification surface.
|
| 18 |
+
|
| 19 |
+
## Action Space
|
| 20 |
+
|
| 21 |
+
Classifier action payload:
|
| 22 |
+
|
| 23 |
+
- `label`: one of `KEEP`, `BORDERLINE`, `REJECT`
|
| 24 |
+
- `reasoning`: concise clip-metadata-grounded explanation
|
| 25 |
+
- `confidence`: float in `[0.0, 1.0]`
|
| 26 |
+
- `clip_id` (optional): specific clip target
|
| 27 |
+
|
| 28 |
+
## Observation Space
|
| 29 |
+
|
| 30 |
+
Each observation includes:
|
| 31 |
+
- `task_id`, `episode_id`, `step_count`
|
| 32 |
+
- `data_corpus`, `clip_metadata`, `rubric_summary`
|
| 33 |
+
- `reward`, `done`, and step diagnostics in `info`
|
| 34 |
+
|
| 35 |
+
These fields are preserved for OpenEnv/reference compatibility and surfaced as clip-case analysis artifacts in `/dashboard/`.
|
| 36 |
+
|
| 37 |
+
## Tasks
|
| 38 |
+
|
| 39 |
+
| Task ID | Difficulty | Description |
|
| 40 |
+
|---|---|---|
|
| 41 |
+
| `task_easy` | Easy | Classify clips with clear quality signals |
|
| 42 |
+
| `task_medium` | Medium | Classify borderline clips with mixed indicators |
|
| 43 |
+
| `task_hard` | Hard | Classify hard clips with conflicting quality signals |
|
| 44 |
+
|
| 45 |
+
Overall total scores are difficulty-calibrated so task-level averages follow: `hard > medium > easy`.
|
| 46 |
+
|
| 47 |
+
## API + Dashboard Surface
|
| 48 |
+
|
| 49 |
+
- `GET /` -> JSON status
|
| 50 |
+
- `GET /health`
|
| 51 |
+
- `GET /state`
|
| 52 |
+
- `GET /tasks`
|
| 53 |
+
- `POST /grader`
|
| 54 |
+
- `POST /baseline/start`
|
| 55 |
+
- `GET /baseline/status/{run_id}`
|
| 56 |
+
- `GET /baseline` (compatibility wrapper for async baseline start)
|
| 57 |
+
- OpenEnv endpoints: `/reset`, `/step`, `/ws`, `/metadata`, `/schema`
|
| 58 |
+
- Gradio dashboard: `/dashboard/`
|
| 59 |
+
|
| 60 |
+
The API surface remains reference-compatible; product naming and UI wording are clip-quality focused.
|
| 61 |
+
|
| 62 |
+
`POST /grader` accepts the classifier action payload above.
|
| 63 |
+
|
| 64 |
+
## Dashboard Highlights
|
| 65 |
+
|
| 66 |
+
- 5-step same-scenario review sessions with running totals
|
| 67 |
+
- Full corpus queue display (no slicing), sorted by Clip ID, with live review-status updates
|
| 68 |
+
- Difficulty-tiered input tabs (Easy / Medium / Hard) with scenario-aware tab switching
|
| 69 |
+
- `💡 Load Quality Hint` helper button to scaffold reasoning text from dominant feature boundary cues
|
| 70 |
+
- Session history tab with submitted vs expected labels and per-step rewards
|
| 71 |
+
- Non-blocking `🤖 Run LLM Baseline Agent` control with async status polling and result summary
|
| 72 |
+
|
| 73 |
+
## Local Setup
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
python -m venv .venv
|
| 77 |
+
source .venv/bin/activate
|
| 78 |
+
pip install -r server/requirements.txt
|
| 79 |
+
PYTHONPATH=. python -m pytest -q
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
Run server:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
python -m uvicorn server.app:app --host 0.0.0.0 --port 8000
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
## Baseline Inference
|
| 89 |
+
|
| 90 |
+
Set env vars:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
export API_BASE_URL="https://router.huggingface.co/v1"
|
| 94 |
+
export MODEL_NAME="llama-3.3-70b-versatile"
|
| 95 |
+
export HF_TOKEN="your_token"
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Run:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
python inference.py
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
Optional single task:
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
python inference.py task_easy
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
## Hugging Face Space Deployment
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
openenv push --repo-id elix3r/ClipQualityEnv .
|
| 114 |
+
```
|
__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Root OpenEnv exports for deployment tooling compatibility."""
|
| 2 |
+
|
| 3 |
+
from clip_quality_env import (
|
| 4 |
+
Action,
|
| 5 |
+
ClipLabel,
|
| 6 |
+
ClipQualityClient,
|
| 7 |
+
ClipQualityEnv,
|
| 8 |
+
ClipQualityEnvironment,
|
| 9 |
+
EnvironmentState,
|
| 10 |
+
Observation,
|
| 11 |
+
State,
|
| 12 |
+
TaskInfo,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# Backward-compatibility alias (deprecated).
|
| 16 |
+
PolicyEvolverEnv = ClipQualityClient
|
| 17 |
+
|
| 18 |
+
__all__ = [
|
| 19 |
+
"Action",
|
| 20 |
+
"ClipLabel",
|
| 21 |
+
"ClipQualityClient",
|
| 22 |
+
"ClipQualityEnv",
|
| 23 |
+
"ClipQualityEnvironment",
|
| 24 |
+
"EnvironmentState",
|
| 25 |
+
"Observation",
|
| 26 |
+
"State",
|
| 27 |
+
"TaskInfo",
|
| 28 |
+
]
|
client.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Root client module for OpenEnv CLI compatibility."""
|
| 2 |
+
|
| 3 |
+
from clip_quality_env.client import ClipQualityClient
|
| 4 |
+
|
| 5 |
+
# Backward-compatibility alias (deprecated).
|
| 6 |
+
PolicyEvolverEnv = ClipQualityClient
|
| 7 |
+
|
| 8 |
+
__all__ = ["ClipQualityClient"]
|
clip_quality_env.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: clip-quality-env
|
| 3 |
+
Version: 1.0.0
|
| 4 |
+
Summary: ClipQualityEnv — OpenEnv RL environment for clip-quality analysis and classification
|
| 5 |
+
Requires-Python: >=3.10
|
| 6 |
+
License-File: LICENSE
|
| 7 |
+
Requires-Dist: openenv-core>=0.2.3
|
| 8 |
+
Requires-Dist: pydantic>=2.0.0
|
| 9 |
+
Requires-Dist: openai>=1.0.0
|
| 10 |
+
Requires-Dist: fastapi>=0.104.0
|
| 11 |
+
Requires-Dist: uvicorn>=0.24.0
|
| 12 |
+
Requires-Dist: requests>=2.25.0
|
| 13 |
+
Requires-Dist: websockets>=12.0
|
| 14 |
+
Requires-Dist: gradio>=4.0.0
|
| 15 |
+
Requires-Dist: pandas>=2.0.0
|
| 16 |
+
Provides-Extra: dev
|
| 17 |
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
| 18 |
+
Dynamic: license-file
|
clip_quality_env.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LICENSE
|
| 2 |
+
README.md
|
| 3 |
+
pyproject.toml
|
| 4 |
+
clip_quality_env/__init__.py
|
| 5 |
+
clip_quality_env/agent.py
|
| 6 |
+
clip_quality_env/client.py
|
| 7 |
+
clip_quality_env/difficulty.py
|
| 8 |
+
clip_quality_env/env.py
|
| 9 |
+
clip_quality_env/generator.py
|
| 10 |
+
clip_quality_env/grader.py
|
| 11 |
+
clip_quality_env/ground_truth.py
|
| 12 |
+
clip_quality_env/models.py
|
| 13 |
+
clip_quality_env/real_clips.py
|
| 14 |
+
clip_quality_env/rubric.py
|
| 15 |
+
clip_quality_env/train.py
|
| 16 |
+
clip_quality_env.egg-info/PKG-INFO
|
| 17 |
+
clip_quality_env.egg-info/SOURCES.txt
|
| 18 |
+
clip_quality_env.egg-info/dependency_links.txt
|
| 19 |
+
clip_quality_env.egg-info/entry_points.txt
|
| 20 |
+
clip_quality_env.egg-info/requires.txt
|
| 21 |
+
clip_quality_env.egg-info/top_level.txt
|
| 22 |
+
server/__init__.py
|
| 23 |
+
server/app.py
|
| 24 |
+
server/baseline_runs.py
|
| 25 |
+
server/clip_quality_environment.py
|
| 26 |
+
server/environment.py
|
| 27 |
+
server/grader.py
|
| 28 |
+
tests/test_baseline_runs.py
|
| 29 |
+
tests/test_environment.py
|
| 30 |
+
tests/test_grader.py
|
| 31 |
+
tests/test_inference.py
|
| 32 |
+
tests/test_models.py
|
| 33 |
+
tests/test_server_routes.py
|
clip_quality_env.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
clip_quality_env.egg-info/entry_points.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[console_scripts]
|
| 2 |
+
server = server.app:main
|
clip_quality_env.egg-info/requires.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core>=0.2.3
|
| 2 |
+
pydantic>=2.0.0
|
| 3 |
+
openai>=1.0.0
|
| 4 |
+
fastapi>=0.104.0
|
| 5 |
+
uvicorn>=0.24.0
|
| 6 |
+
requests>=2.25.0
|
| 7 |
+
websockets>=12.0
|
| 8 |
+
gradio>=4.0.0
|
| 9 |
+
pandas>=2.0.0
|
| 10 |
+
|
| 11 |
+
[dev]
|
| 12 |
+
pytest>=7.0.0
|
clip_quality_env.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
clip_quality_env
|
| 2 |
+
server
|
clip_quality_env/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Clip-quality environment package."""
|
| 2 |
+
|
| 3 |
+
from .client import ClipQualityClient
|
| 4 |
+
from .env import ClipQualityEnv, ClipQualityEnvironment
|
| 5 |
+
from .models import (
|
| 6 |
+
Action,
|
| 7 |
+
ClipLabel,
|
| 8 |
+
ClipMetadata,
|
| 9 |
+
CorpusIncident,
|
| 10 |
+
EnvironmentState,
|
| 11 |
+
EpisodeHistoryItem,
|
| 12 |
+
HistoryItem,
|
| 13 |
+
Observation,
|
| 14 |
+
Reward,
|
| 15 |
+
State,
|
| 16 |
+
TaskInfo,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Backward-compatibility aliases (deprecated).
|
| 20 |
+
PolicyEvolverEnv = ClipQualityClient
|
| 21 |
+
PolicyEvolverEnvironment = ClipQualityEnvironment
|
| 22 |
+
|
| 23 |
+
__all__ = [
|
| 24 |
+
"Action",
|
| 25 |
+
"ClipLabel",
|
| 26 |
+
"ClipMetadata",
|
| 27 |
+
"ClipQualityClient",
|
| 28 |
+
"ClipQualityEnv",
|
| 29 |
+
"ClipQualityEnvironment",
|
| 30 |
+
"CorpusIncident",
|
| 31 |
+
"EnvironmentState",
|
| 32 |
+
"EpisodeHistoryItem",
|
| 33 |
+
"HistoryItem",
|
| 34 |
+
"Observation",
|
| 35 |
+
"Reward",
|
| 36 |
+
"State",
|
| 37 |
+
"TaskInfo",
|
| 38 |
+
]
|
clip_quality_env/agent.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from openai import OpenAI
|
| 9 |
+
|
| 10 |
+
from .models import Action, Observation
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
LABEL_RE = re.compile(r"<label>\s*(KEEP|BORDERLINE|REJECT)\s*</label>", re.IGNORECASE)
|
| 14 |
+
REASONING_RE = re.compile(r"<reasoning>\s*(.*?)\s*</reasoning>", re.IGNORECASE | re.DOTALL)
|
| 15 |
+
CONFIDENCE_RE = re.compile(r"<confidence>\s*([0-9]*\.?[0-9]+)\s*</confidence>", re.IGNORECASE)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class LLMAgent:
|
| 19 |
+
"""OpenAI-client-backed agent for ClipQualityEnv."""
|
| 20 |
+
|
| 21 |
+
SYSTEM_PROMPT = (
|
| 22 |
+
"You are a dataset quality analyst for talking-head LoRA training clips.\n"
|
| 23 |
+
"Classify each clip as KEEP, BORDERLINE, or REJECT.\n"
|
| 24 |
+
"Always respond with XML tags exactly:\n"
|
| 25 |
+
"<label>...</label>\n<reasoning>...</reasoning>\n<confidence>0.0-1.0</confidence>"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
def __init__(
|
| 29 |
+
self,
|
| 30 |
+
model_name: str | None = None,
|
| 31 |
+
api_base_url: str | None = None,
|
| 32 |
+
api_key: str | None = None,
|
| 33 |
+
temperature: float = 0.3,
|
| 34 |
+
max_tokens: int = 512,
|
| 35 |
+
) -> None:
|
| 36 |
+
self.model_name = model_name or os.environ.get("MODEL_NAME", "meta-llama/Llama-3-70B-Instruct")
|
| 37 |
+
self.api_base_url = api_base_url or os.environ.get("API_BASE_URL", "https://api-inference.huggingface.co/v1/")
|
| 38 |
+
self.api_key = api_key or os.environ.get("HF_TOKEN") or os.environ.get("OPENAI_API_KEY")
|
| 39 |
+
if not self.api_key:
|
| 40 |
+
raise ValueError("Either HF_TOKEN or OPENAI_API_KEY must be set")
|
| 41 |
+
self.temperature = temperature
|
| 42 |
+
self.max_tokens = max_tokens
|
| 43 |
+
self.client = OpenAI(base_url=self.api_base_url, api_key=self.api_key)
|
| 44 |
+
|
| 45 |
+
def act(self, obs: Observation | dict[str, Any]) -> Action:
|
| 46 |
+
observation = obs if isinstance(obs, Observation) else Observation(**obs)
|
| 47 |
+
prompt = self._build_prompt(observation)
|
| 48 |
+
raw = self._call_model(prompt)
|
| 49 |
+
return self._parse_response(raw)
|
| 50 |
+
|
| 51 |
+
def _build_prompt(self, obs: Observation) -> str:
|
| 52 |
+
parts = [obs.rubric_summary, ""]
|
| 53 |
+
for h in obs.history:
|
| 54 |
+
parts.append(f"[STEP {h.step} - Previous]")
|
| 55 |
+
parts.append(f"Your label: {h.label} | Reward: {h.reward:.2f}")
|
| 56 |
+
parts.append("")
|
| 57 |
+
parts.append(f"[STEP {obs.step} - Current Clip]")
|
| 58 |
+
parts.append(json.dumps(obs.clip_metadata.model_dump(), indent=2))
|
| 59 |
+
parts.append("")
|
| 60 |
+
parts.append("Classify this clip.")
|
| 61 |
+
return "\n".join(parts)
|
| 62 |
+
|
| 63 |
+
def _call_model(self, prompt: str) -> str:
|
| 64 |
+
response = self.client.chat.completions.create(
|
| 65 |
+
model=self.model_name,
|
| 66 |
+
messages=[
|
| 67 |
+
{"role": "system", "content": self.SYSTEM_PROMPT},
|
| 68 |
+
{"role": "user", "content": prompt},
|
| 69 |
+
],
|
| 70 |
+
temperature=self.temperature,
|
| 71 |
+
max_tokens=self.max_tokens,
|
| 72 |
+
)
|
| 73 |
+
content = response.choices[0].message.content
|
| 74 |
+
if not content:
|
| 75 |
+
return "<label>BORDERLINE</label><reasoning>No content.</reasoning><confidence>0.5</confidence>"
|
| 76 |
+
return content
|
| 77 |
+
|
| 78 |
+
def _parse_response(self, text: str) -> Action:
|
| 79 |
+
label_match = LABEL_RE.search(text)
|
| 80 |
+
reasoning_match = REASONING_RE.search(text)
|
| 81 |
+
confidence_match = CONFIDENCE_RE.search(text)
|
| 82 |
+
|
| 83 |
+
label = label_match.group(1).upper() if label_match else "BORDERLINE"
|
| 84 |
+
reasoning = reasoning_match.group(1).strip() if reasoning_match else "No reasoning provided."
|
| 85 |
+
confidence = 0.5
|
| 86 |
+
if confidence_match:
|
| 87 |
+
try:
|
| 88 |
+
confidence = float(confidence_match.group(1))
|
| 89 |
+
except ValueError:
|
| 90 |
+
confidence = 0.5
|
| 91 |
+
confidence = max(0.0, min(1.0, confidence))
|
| 92 |
+
return Action(label=label, reasoning=reasoning, confidence=confidence)
|
clip_quality_env/client.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
from openenv.core import EnvClient
|
| 6 |
+
from openenv.core.client_types import StepResult
|
| 7 |
+
|
| 8 |
+
from .models import Action, EnvironmentState, Observation
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class ClipQualityClient(EnvClient[Action, Observation, EnvironmentState]):
|
| 12 |
+
"""Typed async client for ClipQualityEnv with sync wrapper support."""
|
| 13 |
+
|
| 14 |
+
def _step_payload(self, action: Action) -> dict[str, Any]:
|
| 15 |
+
payload = action.model_dump()
|
| 16 |
+
if isinstance(payload, dict) and "root" in payload and isinstance(payload["root"], dict):
|
| 17 |
+
return payload["root"]
|
| 18 |
+
return payload if isinstance(payload, dict) else {"action": payload}
|
| 19 |
+
|
| 20 |
+
def _parse_result(self, payload: dict[str, Any]) -> StepResult[Observation]:
|
| 21 |
+
obs_data = payload.get("observation", {})
|
| 22 |
+
if "done" not in obs_data:
|
| 23 |
+
obs_data["done"] = bool(payload.get("done", False))
|
| 24 |
+
if "reward" not in obs_data:
|
| 25 |
+
obs_data["reward"] = payload.get("reward")
|
| 26 |
+
observation = Observation.model_validate(obs_data)
|
| 27 |
+
return StepResult(
|
| 28 |
+
observation=observation,
|
| 29 |
+
reward=payload.get("reward"),
|
| 30 |
+
done=bool(payload.get("done", False)),
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
def _parse_state(self, payload: dict[str, Any]) -> EnvironmentState:
|
| 34 |
+
return EnvironmentState.model_validate(payload)
|
clip_quality_env/difficulty.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Final
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
DIFFICULTY_ORDER: Final[dict[str, int]] = {
|
| 7 |
+
"easy": 0,
|
| 8 |
+
"medium": 1,
|
| 9 |
+
"hard": 2,
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
# Final-score calibration bands for enforcing strict ordering:
|
| 13 |
+
# easy < medium < hard for all calibrated totals.
|
| 14 |
+
DIFFICULTY_TOTAL_BANDS: Final[dict[str, tuple[float, float]]] = {
|
| 15 |
+
"easy": (0.00, 0.32),
|
| 16 |
+
"medium": (0.34, 0.66),
|
| 17 |
+
"hard": (0.68, 1.00),
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _clamp01(value: float) -> float:
|
| 22 |
+
return max(0.0, min(1.0, float(value)))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def normalize_difficulty(value: str | None) -> str | None:
|
| 26 |
+
if value is None:
|
| 27 |
+
return None
|
| 28 |
+
normalized = str(value).strip().lower()
|
| 29 |
+
if normalized not in DIFFICULTY_ORDER:
|
| 30 |
+
return None
|
| 31 |
+
return normalized
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def calibrate_total_score(total: float, difficulty: str | None) -> float:
|
| 35 |
+
raw = _clamp01(total)
|
| 36 |
+
normalized = normalize_difficulty(difficulty)
|
| 37 |
+
if normalized is None:
|
| 38 |
+
return raw
|
| 39 |
+
|
| 40 |
+
band_min, band_max = DIFFICULTY_TOTAL_BANDS[normalized]
|
| 41 |
+
band_min = float(band_min)
|
| 42 |
+
band_max = float(band_max)
|
| 43 |
+
if band_max <= band_min:
|
| 44 |
+
return raw
|
| 45 |
+
|
| 46 |
+
return band_min + (raw * (band_max - band_min))
|
clip_quality_env/env.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import copy
|
| 4 |
+
import os
|
| 5 |
+
import random
|
| 6 |
+
import uuid
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
from openenv.core import Environment
|
| 11 |
+
|
| 12 |
+
from .grader import grade
|
| 13 |
+
from .ground_truth import GTStore
|
| 14 |
+
from .models import Action, ClipMetadata, EpisodeHistoryItem, HistoryItem, Observation, State
|
| 15 |
+
from .real_clips import load_real_clip_manifest
|
| 16 |
+
from .rubric import RubricState
|
| 17 |
+
from server.tasks import TASK_REGISTRY
|
| 18 |
+
|
| 19 |
+
EPISODE_STEPS = 5
|
| 20 |
+
DEFAULT_REAL_CLIPS_MANIFEST = "data/real_clips_manifest.jsonl"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class EpisodeClip:
|
| 25 |
+
task_id: str
|
| 26 |
+
difficulty: str
|
| 27 |
+
clip: dict[str, Any]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ClipQualityEnvironment(Environment[Action, Observation, State]):
|
| 31 |
+
"""OpenEnv environment for clip-quality classification tasks."""
|
| 32 |
+
|
| 33 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 34 |
+
|
| 35 |
+
def __init__(self) -> None:
|
| 36 |
+
super().__init__()
|
| 37 |
+
self._rubric = RubricState()
|
| 38 |
+
self._gt_store = GTStore()
|
| 39 |
+
self._state = State(
|
| 40 |
+
max_steps=EPISODE_STEPS,
|
| 41 |
+
total_reward=0.0,
|
| 42 |
+
rubric_version=self._rubric.version,
|
| 43 |
+
gt_size=self._gt_store.size(),
|
| 44 |
+
rubric_thresholds=self._rubric.get_thresholds_summary(),
|
| 45 |
+
)
|
| 46 |
+
self._episode_plan: list[EpisodeClip] = []
|
| 47 |
+
self._episode_corpus: dict[str, list[dict[str, Any]]] = {}
|
| 48 |
+
self._persistent_best_score = 0.0
|
| 49 |
+
self._last_reward_breakdown = {
|
| 50 |
+
"format_score": 0.0,
|
| 51 |
+
"label_score": 0.0,
|
| 52 |
+
"reasoning_score": 0.0,
|
| 53 |
+
"total_reward": 0.0,
|
| 54 |
+
}
|
| 55 |
+
self._corpus_source = "task_registry"
|
| 56 |
+
self._manifest_warning = ""
|
| 57 |
+
self._manifest_path = os.environ.get("REAL_CLIPS_MANIFEST", DEFAULT_REAL_CLIPS_MANIFEST)
|
| 58 |
+
self._real_clip_pools = self._load_real_clip_pools()
|
| 59 |
+
|
| 60 |
+
def _load_real_clip_pools(self) -> dict[str, list[dict[str, Any]]]:
|
| 61 |
+
if not os.path.exists(self._manifest_path):
|
| 62 |
+
self._manifest_warning = (
|
| 63 |
+
f"Real clip manifest not found at {self._manifest_path}; using static task corpora."
|
| 64 |
+
)
|
| 65 |
+
return {}
|
| 66 |
+
try:
|
| 67 |
+
pools = load_real_clip_manifest(self._manifest_path, self._rubric)
|
| 68 |
+
except Exception as exc:
|
| 69 |
+
self._manifest_warning = (
|
| 70 |
+
f"Failed loading real clip manifest from {self._manifest_path}: {exc}; using static task corpora."
|
| 71 |
+
)
|
| 72 |
+
return {}
|
| 73 |
+
self._manifest_warning = ""
|
| 74 |
+
return pools
|
| 75 |
+
|
| 76 |
+
def _choose_tasks(self, seed: int | None = None, task_id: str | None = None) -> list[str]:
|
| 77 |
+
if task_id is not None:
|
| 78 |
+
if task_id not in TASK_REGISTRY:
|
| 79 |
+
raise KeyError(f"Unknown task_id: {task_id}")
|
| 80 |
+
return [task_id]
|
| 81 |
+
rng = random.Random(seed)
|
| 82 |
+
return [rng.choice(list(TASK_REGISTRY.keys()))]
|
| 83 |
+
|
| 84 |
+
def _load_task_corpus(self, task_id: str) -> list[dict[str, Any]]:
|
| 85 |
+
task = TASK_REGISTRY[task_id]
|
| 86 |
+
difficulty = str(task.get("difficulty", "")).lower()
|
| 87 |
+
corpus: list[dict[str, Any]]
|
| 88 |
+
|
| 89 |
+
if difficulty and self._real_clip_pools.get(difficulty):
|
| 90 |
+
corpus = copy.deepcopy(self._real_clip_pools[difficulty])
|
| 91 |
+
self._corpus_source = f"manifest:{difficulty}"
|
| 92 |
+
else:
|
| 93 |
+
corpus = copy.deepcopy(task.get("data_corpus", []))
|
| 94 |
+
self._corpus_source = f"task_registry:{task_id}"
|
| 95 |
+
|
| 96 |
+
if not corpus:
|
| 97 |
+
raise ValueError(f"No clip corpus configured for task_id={task_id}")
|
| 98 |
+
|
| 99 |
+
for clip in corpus:
|
| 100 |
+
clip.setdefault("clip_id", clip.get("id", str(uuid.uuid4())))
|
| 101 |
+
if "expected_label" not in clip:
|
| 102 |
+
clip["expected_label"] = self._rubric.derive_label(clip)
|
| 103 |
+
clip["review_status"] = str(clip.get("review_status", "pending")).lower()
|
| 104 |
+
|
| 105 |
+
corpus.sort(key=lambda item: str(item.get("clip_id", item.get("id", ""))))
|
| 106 |
+
return corpus
|
| 107 |
+
|
| 108 |
+
def _sample_episode_clips(self, corpus: list[dict[str, Any]], seed: int | None = None) -> list[dict[str, Any]]:
|
| 109 |
+
if not corpus:
|
| 110 |
+
raise ValueError("Cannot sample episode clips from an empty corpus")
|
| 111 |
+
rng = random.Random(seed)
|
| 112 |
+
if len(corpus) >= EPISODE_STEPS:
|
| 113 |
+
return [copy.deepcopy(clip) for clip in rng.sample(corpus, k=EPISODE_STEPS)]
|
| 114 |
+
|
| 115 |
+
pool = [copy.deepcopy(clip) for clip in corpus]
|
| 116 |
+
rng.shuffle(pool)
|
| 117 |
+
sampled: list[dict[str, Any]] = []
|
| 118 |
+
while len(sampled) < EPISODE_STEPS:
|
| 119 |
+
sampled.append(copy.deepcopy(pool[len(sampled) % len(pool)]))
|
| 120 |
+
return sampled
|
| 121 |
+
|
| 122 |
+
def _sample_episode_plan(
|
| 123 |
+
self,
|
| 124 |
+
task_ids: list[str],
|
| 125 |
+
seed: int | None = None,
|
| 126 |
+
) -> tuple[list[EpisodeClip], dict[str, list[dict[str, Any]]]]:
|
| 127 |
+
if len(task_ids) != 1:
|
| 128 |
+
raise ValueError("Episode planning expects exactly one task id")
|
| 129 |
+
task_id = task_ids[0]
|
| 130 |
+
task = TASK_REGISTRY[task_id]
|
| 131 |
+
difficulty = str(task.get("difficulty", ""))
|
| 132 |
+
corpus = self._load_task_corpus(task_id)
|
| 133 |
+
sampled_clips = self._sample_episode_clips(corpus, seed=seed)
|
| 134 |
+
plan = [
|
| 135 |
+
EpisodeClip(task_id=task_id, difficulty=difficulty, clip=clip)
|
| 136 |
+
for clip in sampled_clips
|
| 137 |
+
]
|
| 138 |
+
corpus_map: dict[str, list[dict[str, Any]]] = {task_id: corpus}
|
| 139 |
+
return plan, corpus_map
|
| 140 |
+
|
| 141 |
+
def _threshold_range_text(self, feature: str) -> str:
|
| 142 |
+
threshold = self._rubric.thresholds.get(feature)
|
| 143 |
+
if not threshold:
|
| 144 |
+
return "N/A"
|
| 145 |
+
mode = str(threshold["mode"])
|
| 146 |
+
keep_min = float(threshold["keep_min"])
|
| 147 |
+
keep_max = float(threshold["keep_max"])
|
| 148 |
+
reject_min = float(threshold["reject_min"])
|
| 149 |
+
reject_max = float(threshold["reject_max"])
|
| 150 |
+
if mode == "higher":
|
| 151 |
+
return f"KEEP >= {keep_min:.3g}; BORDERLINE [{reject_max:.3g}, {keep_min:.3g}); REJECT < {reject_max:.3g}"
|
| 152 |
+
if mode == "lower":
|
| 153 |
+
return f"KEEP <= {keep_max:.3g}; BORDERLINE ({keep_max:.3g}, {reject_min:.3g}]; REJECT > {reject_min:.3g}"
|
| 154 |
+
return (
|
| 155 |
+
f"KEEP [{keep_min:.3g}, {keep_max:.3g}]; BORDERLINE [{reject_min:.3g}, {keep_min:.3g})"
|
| 156 |
+
f" U ({keep_max:.3g}, {reject_max:.3g}]; REJECT outside [{reject_min:.3g}, {reject_max:.3g}]"
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
def _closest_boundary_features(self, clip: dict[str, Any], top_n: int = 2) -> list[str]:
|
| 160 |
+
distances: list[tuple[float, str]] = []
|
| 161 |
+
for feature, threshold in self._rubric.thresholds.items():
|
| 162 |
+
value = clip.get(feature)
|
| 163 |
+
if not isinstance(value, (int, float)):
|
| 164 |
+
continue
|
| 165 |
+
mode = str(threshold["mode"])
|
| 166 |
+
keep_min = float(threshold["keep_min"])
|
| 167 |
+
keep_max = float(threshold["keep_max"])
|
| 168 |
+
reject_min = float(threshold["reject_min"])
|
| 169 |
+
reject_max = float(threshold["reject_max"])
|
| 170 |
+
if mode == "higher":
|
| 171 |
+
boundaries = [keep_min, reject_max]
|
| 172 |
+
elif mode == "lower":
|
| 173 |
+
boundaries = [keep_max, reject_min]
|
| 174 |
+
else:
|
| 175 |
+
boundaries = [keep_min, keep_max, reject_min, reject_max]
|
| 176 |
+
min_distance = min(abs(float(value) - boundary) for boundary in boundaries)
|
| 177 |
+
distances.append((min_distance, feature))
|
| 178 |
+
distances.sort(key=lambda item: item[0])
|
| 179 |
+
return [feature for _, feature in distances[:top_n]]
|
| 180 |
+
|
| 181 |
+
def dominant_feature_rows(self, clip: dict[str, Any] | None = None) -> list[dict[str, Any]]:
|
| 182 |
+
if clip is None:
|
| 183 |
+
current_index = min(self._state.step_count, max(len(self._episode_plan) - 1, 0))
|
| 184 |
+
if not self._episode_plan:
|
| 185 |
+
return []
|
| 186 |
+
clip = self._episode_plan[current_index].clip
|
| 187 |
+
dominant_features = self._rubric.get_dominant_features(clip)
|
| 188 |
+
rows: list[dict[str, Any]] = []
|
| 189 |
+
for feature in dominant_features:
|
| 190 |
+
value = clip.get(feature)
|
| 191 |
+
if not isinstance(value, (int, float)):
|
| 192 |
+
continue
|
| 193 |
+
status = self._rubric.get_feature_status(feature, float(value))
|
| 194 |
+
rows.append(
|
| 195 |
+
{
|
| 196 |
+
"Feature Name": feature,
|
| 197 |
+
"Current Value": float(value),
|
| 198 |
+
"Rubric Status": status,
|
| 199 |
+
"Threshold Range": self._threshold_range_text(feature),
|
| 200 |
+
}
|
| 201 |
+
)
|
| 202 |
+
return rows
|
| 203 |
+
|
| 204 |
+
def build_quality_hint(self, clip: dict[str, Any] | None = None) -> str:
|
| 205 |
+
if clip is None:
|
| 206 |
+
current_index = min(self._state.step_count, max(len(self._episode_plan) - 1, 0))
|
| 207 |
+
if not self._episode_plan:
|
| 208 |
+
return ""
|
| 209 |
+
clip = self._episode_plan[current_index].clip
|
| 210 |
+
focus_features = self._closest_boundary_features(clip, top_n=2)
|
| 211 |
+
if not focus_features:
|
| 212 |
+
return "Use dominant clip metadata cues and compare each value against rubric thresholds."
|
| 213 |
+
|
| 214 |
+
segments: list[str] = []
|
| 215 |
+
for idx, feature in enumerate(focus_features):
|
| 216 |
+
value = clip.get(feature)
|
| 217 |
+
if not isinstance(value, (int, float)):
|
| 218 |
+
continue
|
| 219 |
+
status = self._rubric.get_feature_status(feature, float(value))
|
| 220 |
+
threshold = self._rubric.thresholds.get(feature, {})
|
| 221 |
+
mode = str(threshold.get("mode", ""))
|
| 222 |
+
keep_min = float(threshold.get("keep_min", 0.0))
|
| 223 |
+
keep_max = float(threshold.get("keep_max", 0.0))
|
| 224 |
+
reject_min = float(threshold.get("reject_min", 0.0))
|
| 225 |
+
reject_max = float(threshold.get("reject_max", 0.0))
|
| 226 |
+
|
| 227 |
+
if mode == "higher":
|
| 228 |
+
if status == "KEEP":
|
| 229 |
+
direction = f"above the KEEP threshold ({keep_min:.3g})"
|
| 230 |
+
elif status == "REJECT":
|
| 231 |
+
direction = f"below the REJECT threshold ({reject_max:.3g})"
|
| 232 |
+
else:
|
| 233 |
+
direction = f"within the BORDERLINE range [{reject_max:.3g}, {keep_min:.3g})"
|
| 234 |
+
elif mode == "lower":
|
| 235 |
+
if status == "KEEP":
|
| 236 |
+
direction = f"below the KEEP ceiling ({keep_max:.3g})"
|
| 237 |
+
elif status == "REJECT":
|
| 238 |
+
direction = f"above the REJECT threshold ({reject_min:.3g})"
|
| 239 |
+
else:
|
| 240 |
+
direction = f"within the BORDERLINE range ({keep_max:.3g}, {reject_min:.3g}]"
|
| 241 |
+
else:
|
| 242 |
+
if status == "KEEP":
|
| 243 |
+
direction = f"within the KEEP band [{keep_min:.3g}, {keep_max:.3g}]"
|
| 244 |
+
elif status == "REJECT":
|
| 245 |
+
direction = f"outside the acceptable range [{reject_min:.3g}, {reject_max:.3g}]"
|
| 246 |
+
else:
|
| 247 |
+
direction = (
|
| 248 |
+
f"within a BORDERLINE edge zone around [{reject_min:.3g}, {keep_min:.3g})"
|
| 249 |
+
f" or ({keep_max:.3g}, {reject_max:.3g}]"
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
prefix = "" if idx == 0 else " "
|
| 253 |
+
segments.append(f"{prefix}{feature} is {float(value):.3g}, which is {direction}.")
|
| 254 |
+
|
| 255 |
+
return "".join(segments).strip()
|
| 256 |
+
|
| 257 |
+
def _state_to_observation(self, reward: float, done: bool) -> Observation:
|
| 258 |
+
current_index = min(self._state.step_count, max(len(self._episode_plan) - 1, 0))
|
| 259 |
+
current = self._episode_plan[current_index]
|
| 260 |
+
corpus = self._episode_corpus.get(current.task_id, [])
|
| 261 |
+
full_corpus = list(corpus)
|
| 262 |
+
history_items = [
|
| 263 |
+
HistoryItem(
|
| 264 |
+
step=h.step,
|
| 265 |
+
clip_id=h.clip_id,
|
| 266 |
+
label=h.label,
|
| 267 |
+
expected_label=h.expected_label,
|
| 268 |
+
reward=h.reward,
|
| 269 |
+
)
|
| 270 |
+
for h in self._state.episode_history
|
| 271 |
+
]
|
| 272 |
+
steps_remaining = max(0, self._state.max_steps - self._state.step_count)
|
| 273 |
+
session_history = [item.model_dump() for item in self._state.episode_history]
|
| 274 |
+
return Observation(
|
| 275 |
+
task_id=current.task_id,
|
| 276 |
+
episode_id=self._state.episode_id,
|
| 277 |
+
step_count=self._state.step_count,
|
| 278 |
+
max_steps=self._state.max_steps,
|
| 279 |
+
step=max(1, self._state.step_count + (0 if done else 1)),
|
| 280 |
+
rubric_version=self._rubric.version,
|
| 281 |
+
rubric_summary=self._rubric.to_prompt_text(),
|
| 282 |
+
clip_metadata=ClipMetadata.model_validate(current.clip),
|
| 283 |
+
history=history_items,
|
| 284 |
+
corpus_size=len(corpus),
|
| 285 |
+
corpus_shown=len(full_corpus),
|
| 286 |
+
data_corpus=full_corpus,
|
| 287 |
+
reward=float(reward),
|
| 288 |
+
done=done,
|
| 289 |
+
info={
|
| 290 |
+
"difficulty": current.difficulty,
|
| 291 |
+
"task_description": TASK_REGISTRY[current.task_id]["description"],
|
| 292 |
+
"best_score": self._persistent_best_score,
|
| 293 |
+
"last_reward": float(reward),
|
| 294 |
+
"action_history": self._state.actions_taken,
|
| 295 |
+
"steps_remaining": steps_remaining,
|
| 296 |
+
"total_reward": float(self._state.total_reward),
|
| 297 |
+
"reward_breakdown": dict(self._last_reward_breakdown),
|
| 298 |
+
"format_score": float(self._last_reward_breakdown["format_score"]),
|
| 299 |
+
"label_score": float(self._last_reward_breakdown["label_score"]),
|
| 300 |
+
"reasoning_score": float(self._last_reward_breakdown["reasoning_score"]),
|
| 301 |
+
"reward_total": float(self._last_reward_breakdown["total_reward"]),
|
| 302 |
+
"session_history": session_history,
|
| 303 |
+
"corpus_source": self._corpus_source,
|
| 304 |
+
},
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
def reset(
|
| 308 |
+
self,
|
| 309 |
+
seed: int | None = None,
|
| 310 |
+
episode_id: str | None = None,
|
| 311 |
+
**kwargs: Any,
|
| 312 |
+
) -> Observation:
|
| 313 |
+
task_id = kwargs.get("task_id")
|
| 314 |
+
task_ids = self._choose_tasks(seed=seed, task_id=task_id)
|
| 315 |
+
self._episode_plan, self._episode_corpus = self._sample_episode_plan(task_ids, seed=seed)
|
| 316 |
+
self._last_reward_breakdown = {
|
| 317 |
+
"format_score": 0.0,
|
| 318 |
+
"label_score": 0.0,
|
| 319 |
+
"reasoning_score": 0.0,
|
| 320 |
+
"total_reward": 0.0,
|
| 321 |
+
}
|
| 322 |
+
self._state = State(
|
| 323 |
+
episode_id=episode_id or str(uuid.uuid4()),
|
| 324 |
+
task_id=self._episode_plan[0].task_id,
|
| 325 |
+
episode_count=self._state.episode_count + 1,
|
| 326 |
+
step_count=0,
|
| 327 |
+
max_steps=len(self._episode_plan),
|
| 328 |
+
current_score=0.0,
|
| 329 |
+
total_reward=0.0,
|
| 330 |
+
best_score=self._persistent_best_score,
|
| 331 |
+
current_clip_id=str(self._episode_plan[0].clip.get("clip_id", "")),
|
| 332 |
+
gt_size=self._gt_store.size(),
|
| 333 |
+
rubric_version=self._rubric.version,
|
| 334 |
+
actions_taken=[],
|
| 335 |
+
episode_history=[],
|
| 336 |
+
rubric_thresholds=self._rubric.get_thresholds_summary(),
|
| 337 |
+
)
|
| 338 |
+
obs = self._state_to_observation(reward=0.0, done=False)
|
| 339 |
+
if self._manifest_warning:
|
| 340 |
+
obs.info["warning"] = self._manifest_warning
|
| 341 |
+
return obs
|
| 342 |
+
|
| 343 |
+
def step(
|
| 344 |
+
self,
|
| 345 |
+
action: Action | dict[str, Any],
|
| 346 |
+
timeout_s: float | None = None,
|
| 347 |
+
**kwargs: Any,
|
| 348 |
+
) -> Observation:
|
| 349 |
+
del timeout_s, kwargs
|
| 350 |
+
if not self._episode_plan:
|
| 351 |
+
self.reset()
|
| 352 |
+
|
| 353 |
+
current_index = min(self._state.step_count, len(self._episode_plan) - 1)
|
| 354 |
+
current = self._episode_plan[current_index]
|
| 355 |
+
clip = current.clip
|
| 356 |
+
payload = action.model_dump() if isinstance(action, Action) else dict(action)
|
| 357 |
+
action_obj = Action.model_validate(payload)
|
| 358 |
+
|
| 359 |
+
reward_obj = grade(action_obj, clip, self._rubric, self._gt_store, difficulty=current.difficulty)
|
| 360 |
+
reward = float(reward_obj.total)
|
| 361 |
+
self._last_reward_breakdown = {
|
| 362 |
+
"format_score": float(reward_obj.format_score),
|
| 363 |
+
"label_score": float(reward_obj.label_score),
|
| 364 |
+
"reasoning_score": float(reward_obj.reasoning_score),
|
| 365 |
+
"total_reward": float(reward),
|
| 366 |
+
}
|
| 367 |
+
self._state.total_reward += reward
|
| 368 |
+
self._state.current_score = float(self._state.total_reward)
|
| 369 |
+
self._state.best_score = max(self._state.best_score, reward)
|
| 370 |
+
self._persistent_best_score = max(self._persistent_best_score, self._state.best_score)
|
| 371 |
+
|
| 372 |
+
action_label = str(action_obj.label).upper()
|
| 373 |
+
submitted_clip_id = str(action_obj.clip_id or clip.get("clip_id", ""))
|
| 374 |
+
current_clip_id = str(clip.get("clip_id", ""))
|
| 375 |
+
corpus = self._episode_corpus.get(current.task_id, [])
|
| 376 |
+
status_updated = False
|
| 377 |
+
for item in corpus:
|
| 378 |
+
if str(item.get("clip_id", "")) == submitted_clip_id:
|
| 379 |
+
item["review_status"] = action_label
|
| 380 |
+
status_updated = True
|
| 381 |
+
break
|
| 382 |
+
|
| 383 |
+
if not status_updated and submitted_clip_id != current_clip_id:
|
| 384 |
+
for item in corpus:
|
| 385 |
+
if str(item.get("clip_id", "")) == current_clip_id:
|
| 386 |
+
item["review_status"] = action_label
|
| 387 |
+
break
|
| 388 |
+
|
| 389 |
+
self._state.actions_taken.append(action_label)
|
| 390 |
+
self._state.episode_history.append(
|
| 391 |
+
EpisodeHistoryItem(
|
| 392 |
+
step=current_index + 1,
|
| 393 |
+
difficulty=current.difficulty,
|
| 394 |
+
clip_id=str(clip.get("clip_id", "")),
|
| 395 |
+
label=action_label,
|
| 396 |
+
expected_label=str(clip.get("expected_label", "")),
|
| 397 |
+
reward=reward,
|
| 398 |
+
)
|
| 399 |
+
)
|
| 400 |
+
self._state.step_count += 1
|
| 401 |
+
|
| 402 |
+
done = self._state.step_count >= self._state.max_steps
|
| 403 |
+
if done:
|
| 404 |
+
hard_entry = self._state.episode_history[-1]
|
| 405 |
+
step3_result = {
|
| 406 |
+
"clip": self._episode_plan[-1].clip,
|
| 407 |
+
"action": action_obj.model_dump(),
|
| 408 |
+
"reward": reward,
|
| 409 |
+
"expected_label": self._episode_plan[-1].clip.get("expected_label"),
|
| 410 |
+
}
|
| 411 |
+
try:
|
| 412 |
+
gt_promoted = self._gt_store.try_promote(step3_result, episode=self._state.episode_count)
|
| 413 |
+
except ValueError:
|
| 414 |
+
gt_promoted = False
|
| 415 |
+
self._state.gt_size = self._gt_store.size()
|
| 416 |
+
if hard_entry.reward >= 0.85:
|
| 417 |
+
self._rubric.recalibrate(
|
| 418 |
+
perf=type("Perf", (), {"easy_accuracy": 0.0, "medium_accuracy": 0.0, "hard_accuracy": 0.86})(),
|
| 419 |
+
current_episode=self._state.episode_count,
|
| 420 |
+
)
|
| 421 |
+
self._state.rubric_version = self._rubric.version
|
| 422 |
+
self._state.rubric_thresholds = self._rubric.get_thresholds_summary()
|
| 423 |
+
obs = self._state_to_observation(reward=reward, done=True)
|
| 424 |
+
obs.info["gt_promoted"] = bool(gt_promoted)
|
| 425 |
+
obs.info["episode_summary"] = {
|
| 426 |
+
"steps_completed": self._state.step_count,
|
| 427 |
+
"max_steps": self._state.max_steps,
|
| 428 |
+
"total_reward": round(float(self._state.total_reward), 4),
|
| 429 |
+
"average_reward": round(float(self._state.total_reward) / max(1, self._state.max_steps), 4),
|
| 430 |
+
}
|
| 431 |
+
if self._manifest_warning:
|
| 432 |
+
obs.info["warning"] = self._manifest_warning
|
| 433 |
+
return obs
|
| 434 |
+
|
| 435 |
+
next_idx = self._state.step_count
|
| 436 |
+
self._state.task_id = self._episode_plan[next_idx].task_id
|
| 437 |
+
self._state.current_clip_id = str(self._episode_plan[next_idx].clip.get("clip_id", ""))
|
| 438 |
+
obs = self._state_to_observation(reward=reward, done=False)
|
| 439 |
+
if self._manifest_warning:
|
| 440 |
+
obs.info["warning"] = self._manifest_warning
|
| 441 |
+
return obs
|
| 442 |
+
|
| 443 |
+
@property
|
| 444 |
+
def state(self) -> State:
|
| 445 |
+
return self._state
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
ClipQualityEnv = ClipQualityEnvironment
|
| 449 |
+
# Backward-compatibility alias (deprecated).
|
| 450 |
+
PolicyEvolverEnvironment = ClipQualityEnvironment
|
clip_quality_env/generator.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import random
|
| 4 |
+
from collections import deque
|
| 5 |
+
from copy import deepcopy
|
| 6 |
+
from uuid import uuid4
|
| 7 |
+
|
| 8 |
+
from .real_clips import DIFFICULTIES, load_real_clip_manifest
|
| 9 |
+
from .rubric import RubricState
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
EASY_ENVS = ["podcast_studio", "office", "home_office", "webcam_room"]
|
| 13 |
+
NOVEL_ENVS = ["outdoor_interview", "crowded_event", "car_vlog", "street_walk"]
|
| 14 |
+
RESOLUTIONS = ["1280x720", "1920x1080"]
|
| 15 |
+
BACKGROUND_TAGS = ["solid_dark", "solid_light", "simple_room", "busy_room"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _clamp(value: float, low: float, high: float) -> float:
|
| 19 |
+
return max(low, min(high, value))
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _sample_from_keep_center(threshold: dict[str, float | str]) -> float:
|
| 23 |
+
mode = str(threshold["mode"])
|
| 24 |
+
keep_min = float(threshold["keep_min"])
|
| 25 |
+
keep_max = float(threshold["keep_max"])
|
| 26 |
+
if mode == "higher":
|
| 27 |
+
span = max(keep_max - keep_min, 1e-6)
|
| 28 |
+
return _clamp(keep_min + 0.35 * span, keep_min, keep_max)
|
| 29 |
+
if mode == "lower":
|
| 30 |
+
span = max(keep_max - keep_min, 1e-6)
|
| 31 |
+
return _clamp(keep_min + 0.35 * span, keep_min, keep_max)
|
| 32 |
+
return _clamp((keep_min + keep_max) / 2.0, keep_min, keep_max)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _sample_from_reject_zone(threshold: dict[str, float | str]) -> float:
|
| 36 |
+
mode = str(threshold["mode"])
|
| 37 |
+
reject_min = float(threshold["reject_min"])
|
| 38 |
+
reject_max = float(threshold["reject_max"])
|
| 39 |
+
keep_min = float(threshold["keep_min"])
|
| 40 |
+
keep_max = float(threshold["keep_max"])
|
| 41 |
+
if mode == "higher":
|
| 42 |
+
return _clamp(reject_max - 0.02, reject_min, reject_max)
|
| 43 |
+
if mode == "lower":
|
| 44 |
+
return _clamp(reject_min + 0.02, reject_min, reject_max)
|
| 45 |
+
if random.random() < 0.5:
|
| 46 |
+
return _clamp(reject_min - 0.2, 0.0, reject_min)
|
| 47 |
+
return _clamp(reject_max + 0.2, reject_max, reject_max + 10.0)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _sample_from_borderline_zone(threshold: dict[str, float | str]) -> float:
|
| 51 |
+
mode = str(threshold["mode"])
|
| 52 |
+
keep_min = float(threshold["keep_min"])
|
| 53 |
+
keep_max = float(threshold["keep_max"])
|
| 54 |
+
reject_min = float(threshold["reject_min"])
|
| 55 |
+
reject_max = float(threshold["reject_max"])
|
| 56 |
+
if mode == "higher":
|
| 57 |
+
low = reject_max
|
| 58 |
+
high = keep_min
|
| 59 |
+
return _clamp((low + high) / 2.0, low, high)
|
| 60 |
+
if mode == "lower":
|
| 61 |
+
low = keep_max
|
| 62 |
+
high = reject_min
|
| 63 |
+
return _clamp((low + high) / 2.0, low, high)
|
| 64 |
+
if random.random() < 0.5:
|
| 65 |
+
return _clamp((reject_min + keep_min) / 2.0, reject_min, keep_min)
|
| 66 |
+
return _clamp((keep_max + reject_max) / 2.0, keep_max, reject_max)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class ClipMetaGenerator:
|
| 70 |
+
"""Synthetic metadata generator for Easy/Medium/Hard clips."""
|
| 71 |
+
|
| 72 |
+
def __init__(self, seed: int = 7, real_clips_path: str | None = None) -> None:
|
| 73 |
+
self._rng = random.Random(seed)
|
| 74 |
+
self._recent_hard_ids: deque[str] = deque(maxlen=10)
|
| 75 |
+
self._real_clip_pools: dict[str, list[dict[str, object]]] = {d: [] for d in DIFFICULTIES}
|
| 76 |
+
self._real_clip_indices: dict[str, int] = {d: 0 for d in DIFFICULTIES}
|
| 77 |
+
self._real_clips_path = real_clips_path
|
| 78 |
+
|
| 79 |
+
def sample(self, difficulty: str, rubric: RubricState) -> dict[str, object]:
|
| 80 |
+
d = difficulty.lower()
|
| 81 |
+
if d == "easy":
|
| 82 |
+
real = self._sample_real("easy", rubric)
|
| 83 |
+
if real is not None:
|
| 84 |
+
return real
|
| 85 |
+
return self._gen_easy(rubric)
|
| 86 |
+
if d == "medium":
|
| 87 |
+
real = self._sample_real("medium", rubric)
|
| 88 |
+
if real is not None:
|
| 89 |
+
return real
|
| 90 |
+
return self._gen_medium(rubric)
|
| 91 |
+
if d == "hard":
|
| 92 |
+
real = self._sample_real("hard", rubric)
|
| 93 |
+
if real is not None:
|
| 94 |
+
return real
|
| 95 |
+
return self._gen_hard(rubric)
|
| 96 |
+
raise ValueError("difficulty must be one of: easy, medium, hard")
|
| 97 |
+
|
| 98 |
+
def use_real_clips(self, path: str, rubric: RubricState) -> dict[str, int]:
|
| 99 |
+
pools = load_real_clip_manifest(path, rubric)
|
| 100 |
+
self._real_clips_path = path
|
| 101 |
+
for difficulty in DIFFICULTIES:
|
| 102 |
+
rows = [deepcopy(clip) for clip in pools[difficulty]]
|
| 103 |
+
self._rng.shuffle(rows)
|
| 104 |
+
self._real_clip_pools[difficulty] = rows
|
| 105 |
+
self._real_clip_indices[difficulty] = 0
|
| 106 |
+
return self.real_clip_pool_sizes()
|
| 107 |
+
|
| 108 |
+
def real_clip_pool_sizes(self) -> dict[str, int]:
|
| 109 |
+
return {d: len(self._real_clip_pools[d]) for d in DIFFICULTIES}
|
| 110 |
+
|
| 111 |
+
def has_real_clips(self) -> bool:
|
| 112 |
+
return any(self._real_clip_pools[d] for d in DIFFICULTIES)
|
| 113 |
+
|
| 114 |
+
def real_clips_path(self) -> str | None:
|
| 115 |
+
return self._real_clips_path
|
| 116 |
+
|
| 117 |
+
def _sample_real(self, difficulty: str, rubric: RubricState) -> dict[str, object] | None:
|
| 118 |
+
if self._real_clips_path and not self.has_real_clips():
|
| 119 |
+
self.use_real_clips(self._real_clips_path, rubric)
|
| 120 |
+
pool = self._real_clip_pools[difficulty]
|
| 121 |
+
if not pool:
|
| 122 |
+
return None
|
| 123 |
+
idx = self._real_clip_indices[difficulty] % len(pool)
|
| 124 |
+
self._real_clip_indices[difficulty] += 1
|
| 125 |
+
return deepcopy(pool[idx])
|
| 126 |
+
|
| 127 |
+
def _gen_easy(self, rubric: RubricState) -> dict[str, object]:
|
| 128 |
+
label = self._rng.choice(["KEEP", "REJECT"])
|
| 129 |
+
clip = self._base_clip()
|
| 130 |
+
|
| 131 |
+
for feature, t in rubric.thresholds.items():
|
| 132 |
+
if feature not in clip:
|
| 133 |
+
continue
|
| 134 |
+
if label == "KEEP":
|
| 135 |
+
clip[feature] = float(_sample_from_keep_center(t))
|
| 136 |
+
else:
|
| 137 |
+
clip[feature] = float(_sample_from_reject_zone(t))
|
| 138 |
+
|
| 139 |
+
if label == "REJECT":
|
| 140 |
+
trigger = self._rng.choice(["occlusion", "face_confidence", "duration", "motion"])
|
| 141 |
+
if trigger == "occlusion":
|
| 142 |
+
clip["occlusion_present"] = True
|
| 143 |
+
elif trigger == "face_confidence":
|
| 144 |
+
clip["face_confidence"] = 0.52
|
| 145 |
+
elif trigger == "duration":
|
| 146 |
+
clip["duration_s"] = 3.2
|
| 147 |
+
else:
|
| 148 |
+
clip["motion_score"] = 0.57
|
| 149 |
+
else:
|
| 150 |
+
clip["occlusion_present"] = False
|
| 151 |
+
|
| 152 |
+
clip["clip_id"] = f"syn_easy_{uuid4().hex[:8]}"
|
| 153 |
+
clip["environment_tag"] = self._rng.choice(EASY_ENVS)
|
| 154 |
+
clip["bg_complexity"] = self._rng.choice(BACKGROUND_TAGS)
|
| 155 |
+
return clip
|
| 156 |
+
|
| 157 |
+
def _gen_medium(self, rubric: RubricState) -> dict[str, object]:
|
| 158 |
+
clip = self._gen_easy(rubric)
|
| 159 |
+
keys = list(rubric.thresholds.keys())
|
| 160 |
+
to_shift = self._rng.sample(keys, k=2)
|
| 161 |
+
for feature in to_shift:
|
| 162 |
+
if feature in clip:
|
| 163 |
+
clip[feature] = float(_sample_from_borderline_zone(rubric.thresholds[feature]))
|
| 164 |
+
clip["clip_id"] = f"syn_med_{uuid4().hex[:8]}"
|
| 165 |
+
clip["environment_tag"] = self._rng.choice(EASY_ENVS + NOVEL_ENVS[:1])
|
| 166 |
+
clip["occlusion_present"] = False
|
| 167 |
+
return clip
|
| 168 |
+
|
| 169 |
+
def _gen_hard(self, rubric: RubricState) -> dict[str, object]:
|
| 170 |
+
while True:
|
| 171 |
+
clip = self._gen_easy(rubric)
|
| 172 |
+
keys = list(rubric.thresholds.keys())
|
| 173 |
+
to_shift = self._rng.sample(keys, k=3)
|
| 174 |
+
for feature in to_shift:
|
| 175 |
+
if feature in clip:
|
| 176 |
+
clip[feature] = float(_sample_from_borderline_zone(rubric.thresholds[feature]))
|
| 177 |
+
clip["clip_id"] = f"syn_hard_{uuid4().hex[:8]}"
|
| 178 |
+
clip["environment_tag"] = self._rng.choice(NOVEL_ENVS)
|
| 179 |
+
clip["occlusion_present"] = False
|
| 180 |
+
if clip["clip_id"] in self._recent_hard_ids:
|
| 181 |
+
continue
|
| 182 |
+
self._recent_hard_ids.append(str(clip["clip_id"]))
|
| 183 |
+
return clip
|
| 184 |
+
|
| 185 |
+
def _base_clip(self) -> dict[str, object]:
|
| 186 |
+
return {
|
| 187 |
+
"clip_id": f"syn_{uuid4().hex[:8]}",
|
| 188 |
+
"duration_s": round(self._rng.uniform(6.0, 10.0), 2),
|
| 189 |
+
"fps": self._rng.choice([24, 30]),
|
| 190 |
+
"resolution": self._rng.choice(RESOLUTIONS),
|
| 191 |
+
"face_area_ratio": round(self._rng.uniform(0.22, 0.42), 3),
|
| 192 |
+
"face_confidence": round(self._rng.uniform(0.75, 0.95), 3),
|
| 193 |
+
"head_pose_yaw_deg": round(self._rng.uniform(2.0, 18.0), 2),
|
| 194 |
+
"head_pose_pitch_deg": round(self._rng.uniform(-8.0, 8.0), 2),
|
| 195 |
+
"motion_score": round(self._rng.uniform(0.08, 0.28), 3),
|
| 196 |
+
"bg_complexity": self._rng.choice(BACKGROUND_TAGS),
|
| 197 |
+
"bg_complexity_score": round(self._rng.uniform(0.05, 0.20), 3),
|
| 198 |
+
"mouth_open_ratio": round(self._rng.uniform(0.28, 0.52), 3),
|
| 199 |
+
"blink_rate_hz": round(self._rng.uniform(0.15, 0.40), 3),
|
| 200 |
+
"audio_snr_db": round(self._rng.uniform(18.0, 28.0), 2),
|
| 201 |
+
"transcript_word_count": self._rng.randint(22, 64),
|
| 202 |
+
"transcript_confidence": round(self._rng.uniform(0.80, 0.97), 3),
|
| 203 |
+
"lighting_uniformity": round(self._rng.uniform(0.55, 0.85), 3),
|
| 204 |
+
"occlusion_present": False,
|
| 205 |
+
"environment_tag": self._rng.choice(EASY_ENVS),
|
| 206 |
+
"framing": self._rng.choice(["front", "left", "right", "closeup", "offgaze"]),
|
| 207 |
+
}
|
clip_quality_env/grader.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
from .difficulty import calibrate_total_score
|
| 7 |
+
from .ground_truth import GTStore
|
| 8 |
+
from .models import Action, Reward
|
| 9 |
+
from .rubric import RubricState
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
VALID_LABELS = {"KEEP", "BORDERLINE", "REJECT"}
|
| 13 |
+
FEATURE_TOKEN_RE = re.compile(r"\b[a-z]+(?:_[a-z0-9]+)+\b")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _normalize_action(action: Action | dict[str, Any]) -> dict[str, Any]:
|
| 17 |
+
if isinstance(action, Action):
|
| 18 |
+
return action.model_dump()
|
| 19 |
+
if not isinstance(action, dict):
|
| 20 |
+
raise TypeError("action must be Action or dict")
|
| 21 |
+
return {
|
| 22 |
+
"label": str(action.get("label", "BORDERLINE")).upper(),
|
| 23 |
+
"reasoning": str(action.get("reasoning", "")),
|
| 24 |
+
"confidence": float(action.get("confidence", 0.5)),
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _score_format(action: dict[str, Any]) -> float:
|
| 29 |
+
label = str(action.get("label", "")).upper()
|
| 30 |
+
reasoning = str(action.get("reasoning", "")).strip()
|
| 31 |
+
confidence = float(action.get("confidence", -1.0))
|
| 32 |
+
if label in VALID_LABELS and reasoning and 0.0 <= confidence <= 1.0:
|
| 33 |
+
return 0.10
|
| 34 |
+
return 0.0
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _score_label(label: str, clip: dict[str, Any], rubric: RubricState, gt: GTStore) -> float:
|
| 38 |
+
clip_id = str(clip.get("clip_id", ""))
|
| 39 |
+
gt_label = gt.lookup(clip_id)
|
| 40 |
+
if gt_label is None:
|
| 41 |
+
gt_label = rubric.derive_label(clip)
|
| 42 |
+
if label == gt_label:
|
| 43 |
+
return 0.60
|
| 44 |
+
if gt_label == "BORDERLINE" and label in {"KEEP", "REJECT"}:
|
| 45 |
+
return 0.25
|
| 46 |
+
return 0.0
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _contains_directional_cue(reasoning: str, feature: str, status: str) -> bool:
|
| 50 |
+
low_words = ("low", "below", "under", "small", "poor", "noisy", "high motion", "occlusion")
|
| 51 |
+
high_words = ("high", "above", "over", "good", "clear", "stable", "frontal", "well-lit")
|
| 52 |
+
text = reasoning.lower()
|
| 53 |
+
if feature not in text:
|
| 54 |
+
return False
|
| 55 |
+
if status == "REJECT":
|
| 56 |
+
return any(w in text for w in low_words + ("reject",))
|
| 57 |
+
if status == "KEEP":
|
| 58 |
+
return any(w in text for w in high_words + ("keep",))
|
| 59 |
+
return any(w in text for w in ("borderline", "mixed", "ambiguous", "tradeoff", "conflict"))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _check_directional_reasoning(reasoning: str, clip: dict[str, Any], dominant_features: list[str], rubric: RubricState) -> bool:
|
| 63 |
+
if not reasoning.strip():
|
| 64 |
+
return False
|
| 65 |
+
checks = 0
|
| 66 |
+
matches = 0
|
| 67 |
+
for feature in dominant_features:
|
| 68 |
+
if feature not in clip:
|
| 69 |
+
continue
|
| 70 |
+
value = clip[feature]
|
| 71 |
+
if not isinstance(value, (int, float)):
|
| 72 |
+
continue
|
| 73 |
+
checks += 1
|
| 74 |
+
status = rubric.get_feature_status(feature, float(value))
|
| 75 |
+
if _contains_directional_cue(reasoning, feature, status):
|
| 76 |
+
matches += 1
|
| 77 |
+
if checks == 0:
|
| 78 |
+
return False
|
| 79 |
+
return matches >= 1
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _score_reasoning(reasoning: str, clip: dict[str, Any], rubric: RubricState) -> float:
|
| 83 |
+
score = 0.0
|
| 84 |
+
lower_reasoning = reasoning.lower()
|
| 85 |
+
dominant_features = rubric.get_dominant_features(clip)
|
| 86 |
+
|
| 87 |
+
mentioned = sum(1 for f in dominant_features if f.lower() in lower_reasoning)
|
| 88 |
+
if mentioned >= 2:
|
| 89 |
+
score += 0.10
|
| 90 |
+
elif mentioned == 1:
|
| 91 |
+
score += 0.05
|
| 92 |
+
|
| 93 |
+
if _check_directional_reasoning(reasoning, clip, dominant_features, rubric):
|
| 94 |
+
score += 0.10
|
| 95 |
+
|
| 96 |
+
all_feature_names = {k.lower() for k in clip.keys()}
|
| 97 |
+
hallucinated = [
|
| 98 |
+
token
|
| 99 |
+
for token in FEATURE_TOKEN_RE.findall(lower_reasoning)
|
| 100 |
+
if token not in all_feature_names
|
| 101 |
+
]
|
| 102 |
+
if len(hallucinated) == 0:
|
| 103 |
+
score += 0.10
|
| 104 |
+
return min(max(score, 0.0), 0.30)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def grade(
|
| 108 |
+
action: Action | dict[str, Any],
|
| 109 |
+
clip: dict[str, Any],
|
| 110 |
+
rubric: RubricState,
|
| 111 |
+
gt: GTStore,
|
| 112 |
+
difficulty: str | None = None,
|
| 113 |
+
) -> Reward:
|
| 114 |
+
"""
|
| 115 |
+
Fully deterministic reward decomposition.
|
| 116 |
+
"""
|
| 117 |
+
payload = _normalize_action(action)
|
| 118 |
+
label = str(payload["label"]).upper()
|
| 119 |
+
reasoning = str(payload["reasoning"])
|
| 120 |
+
|
| 121 |
+
format_score = _score_format(payload)
|
| 122 |
+
label_score = _score_label(label, clip, rubric, gt)
|
| 123 |
+
reasoning_score = _score_reasoning(reasoning, clip, rubric)
|
| 124 |
+
total = format_score + label_score + reasoning_score
|
| 125 |
+
total = calibrate_total_score(total, difficulty=difficulty)
|
| 126 |
+
|
| 127 |
+
return Reward(
|
| 128 |
+
total=round(min(max(total, 0.0), 1.0), 6),
|
| 129 |
+
format_score=round(format_score, 6),
|
| 130 |
+
label_score=round(label_score, 6),
|
| 131 |
+
reasoning_score=round(reasoning_score, 6),
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def score(
|
| 136 |
+
action: Action | dict[str, Any],
|
| 137 |
+
clip: dict[str, Any],
|
| 138 |
+
rubric: RubricState,
|
| 139 |
+
gt: GTStore,
|
| 140 |
+
difficulty: str | None = None,
|
| 141 |
+
) -> float:
|
| 142 |
+
return float(grade(action, clip, rubric, gt, difficulty=difficulty).total)
|
clip_quality_env/ground_truth.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
VALID_LABELS = {"KEEP", "BORDERLINE", "REJECT"}
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class GTStore:
|
| 12 |
+
"""Append-only ground-truth store with promotion support."""
|
| 13 |
+
|
| 14 |
+
def __init__(self, seed_path: str = "data/seed_gt.json", state_path: str = "state/ground_truth.json") -> None:
|
| 15 |
+
self.seed_path = seed_path
|
| 16 |
+
self.path = state_path
|
| 17 |
+
self.records: dict[str, dict[str, Any]] = {}
|
| 18 |
+
self._load_seed()
|
| 19 |
+
self._load_state()
|
| 20 |
+
|
| 21 |
+
def _load_seed(self) -> None:
|
| 22 |
+
if not os.path.exists(self.seed_path):
|
| 23 |
+
raise FileNotFoundError(f"Seed ground truth file not found: {self.seed_path}")
|
| 24 |
+
with open(self.seed_path, "r", encoding="utf-8") as f:
|
| 25 |
+
payload = json.load(f)
|
| 26 |
+
|
| 27 |
+
if isinstance(payload, dict):
|
| 28 |
+
for clip_id, rec in payload.items():
|
| 29 |
+
label = str(rec.get("label", "")).upper()
|
| 30 |
+
if label in VALID_LABELS:
|
| 31 |
+
self.records[clip_id] = {
|
| 32 |
+
"label": label,
|
| 33 |
+
"source": rec.get("source", "seed"),
|
| 34 |
+
"episode": int(rec.get("episode", 0)),
|
| 35 |
+
"reward": rec.get("reward"),
|
| 36 |
+
"confidence": rec.get("confidence"),
|
| 37 |
+
}
|
| 38 |
+
elif isinstance(payload, list):
|
| 39 |
+
for item in payload:
|
| 40 |
+
clip_id = str(item.get("clip_id", "")).strip()
|
| 41 |
+
label = str(item.get("label", "")).upper()
|
| 42 |
+
if clip_id and label in VALID_LABELS:
|
| 43 |
+
self.records[clip_id] = {
|
| 44 |
+
"label": label,
|
| 45 |
+
"source": item.get("source", "seed"),
|
| 46 |
+
"episode": int(item.get("episode", 0)),
|
| 47 |
+
"reward": item.get("reward"),
|
| 48 |
+
"confidence": item.get("confidence"),
|
| 49 |
+
}
|
| 50 |
+
else:
|
| 51 |
+
raise ValueError("seed_gt.json must be a dict or list")
|
| 52 |
+
|
| 53 |
+
def _load_state(self) -> None:
|
| 54 |
+
if not os.path.exists(self.path):
|
| 55 |
+
return
|
| 56 |
+
with open(self.path, "r", encoding="utf-8") as f:
|
| 57 |
+
payload = json.load(f)
|
| 58 |
+
if not isinstance(payload, dict):
|
| 59 |
+
raise ValueError("ground_truth.json must be a dict")
|
| 60 |
+
for clip_id, rec in payload.items():
|
| 61 |
+
if clip_id in self.records:
|
| 62 |
+
continue
|
| 63 |
+
label = str(rec.get("label", "")).upper()
|
| 64 |
+
if label not in VALID_LABELS:
|
| 65 |
+
continue
|
| 66 |
+
self.records[clip_id] = {
|
| 67 |
+
"label": label,
|
| 68 |
+
"source": rec.get("source", "agent_promoted"),
|
| 69 |
+
"episode": int(rec.get("episode", 0)),
|
| 70 |
+
"reward": rec.get("reward"),
|
| 71 |
+
"confidence": rec.get("confidence"),
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
def save(self) -> None:
|
| 75 |
+
os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True)
|
| 76 |
+
with open(self.path, "w", encoding="utf-8") as f:
|
| 77 |
+
json.dump(self.records, f, indent=2, sort_keys=True)
|
| 78 |
+
|
| 79 |
+
def lookup(self, clip_id: str) -> str | None:
|
| 80 |
+
rec = self.records.get(clip_id)
|
| 81 |
+
return None if rec is None else str(rec.get("label"))
|
| 82 |
+
|
| 83 |
+
def size(self) -> int:
|
| 84 |
+
return len(self.records)
|
| 85 |
+
|
| 86 |
+
def get_promoted_clip_ids(self) -> list[str]:
|
| 87 |
+
return [clip_id for clip_id, rec in self.records.items() if rec.get("source") == "agent_promoted"]
|
| 88 |
+
|
| 89 |
+
def try_promote(self, step3_result: dict[str, Any], episode: int) -> bool:
|
| 90 |
+
"""
|
| 91 |
+
Promote hard-step clip if reward/confidence threshold is met.
|
| 92 |
+
|
| 93 |
+
The optional `expected_label` key enforces that the promoted label is correct.
|
| 94 |
+
"""
|
| 95 |
+
clip = step3_result.get("clip", {})
|
| 96 |
+
action = step3_result.get("action", {})
|
| 97 |
+
clip_id = str(clip.get("clip_id", "")).strip()
|
| 98 |
+
if not clip_id:
|
| 99 |
+
raise ValueError("step3_result.clip.clip_id is required")
|
| 100 |
+
if clip_id in self.records:
|
| 101 |
+
return False
|
| 102 |
+
|
| 103 |
+
reward = float(step3_result.get("reward", 0.0))
|
| 104 |
+
confidence = float(action.get("confidence", 0.0))
|
| 105 |
+
label = str(action.get("label", "")).upper()
|
| 106 |
+
expected_label = step3_result.get("expected_label")
|
| 107 |
+
if expected_label is not None:
|
| 108 |
+
expected_label = str(expected_label).upper()
|
| 109 |
+
|
| 110 |
+
if label not in VALID_LABELS:
|
| 111 |
+
return False
|
| 112 |
+
if reward < 0.85 or confidence < 0.80:
|
| 113 |
+
return False
|
| 114 |
+
if expected_label in VALID_LABELS and label != expected_label:
|
| 115 |
+
return False
|
| 116 |
+
|
| 117 |
+
self.records[clip_id] = {
|
| 118 |
+
"label": label,
|
| 119 |
+
"source": "agent_promoted",
|
| 120 |
+
"episode": int(episode),
|
| 121 |
+
"reward": round(reward, 6),
|
| 122 |
+
"confidence": round(confidence, 6),
|
| 123 |
+
}
|
| 124 |
+
self.save()
|
| 125 |
+
return True
|
clip_quality_env/models.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import uuid
|
| 4 |
+
from enum import Enum
|
| 5 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 6 |
+
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ClipLabel(str, Enum):
|
| 11 |
+
KEEP = "KEEP"
|
| 12 |
+
BORDERLINE = "BORDERLINE"
|
| 13 |
+
REJECT = "REJECT"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Action(BaseModel):
|
| 17 |
+
"""Agent action for clip-quality classification."""
|
| 18 |
+
|
| 19 |
+
label: Literal["KEEP", "BORDERLINE", "REJECT"] = Field(description="Predicted clip label")
|
| 20 |
+
reasoning: str = Field(min_length=1, description="Explanation tied to clip metadata")
|
| 21 |
+
confidence: float = Field(default=0.5, ge=0.0, le=1.0, description="Model confidence in the label")
|
| 22 |
+
clip_id: Optional[str] = Field(default=None, description="Optional target clip ID")
|
| 23 |
+
model_config = {"extra": "allow"}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class TaskInfo(BaseModel):
|
| 27 |
+
"""Returned by /tasks endpoint."""
|
| 28 |
+
|
| 29 |
+
task_id: str
|
| 30 |
+
difficulty: str
|
| 31 |
+
description: str
|
| 32 |
+
action_schema: dict
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class CorpusIncident(BaseModel):
|
| 36 |
+
"""Compatibility record for corpus table views."""
|
| 37 |
+
|
| 38 |
+
id: str
|
| 39 |
+
content: str
|
| 40 |
+
review_status: str = "pending"
|
| 41 |
+
model_config = {"extra": "allow"}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class ClipMetadata(BaseModel):
|
| 45 |
+
"""Clip metadata payload consumed by the agent."""
|
| 46 |
+
|
| 47 |
+
clip_id: str
|
| 48 |
+
duration_s: Optional[float] = Field(default=None, ge=0.0)
|
| 49 |
+
fps: Optional[int] = Field(default=None, ge=1)
|
| 50 |
+
resolution: Optional[str] = None
|
| 51 |
+
face_area_ratio: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
| 52 |
+
face_confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
| 53 |
+
head_pose_yaw_deg: Optional[float] = None
|
| 54 |
+
head_pose_pitch_deg: Optional[float] = None
|
| 55 |
+
motion_score: Optional[float] = Field(default=None, ge=0.0)
|
| 56 |
+
bg_complexity: Optional[str] = None
|
| 57 |
+
bg_complexity_score: Optional[float] = Field(default=None, ge=0.0)
|
| 58 |
+
mouth_open_ratio: Optional[float] = Field(default=None, ge=0.0)
|
| 59 |
+
blink_rate_hz: Optional[float] = Field(default=None, ge=0.0)
|
| 60 |
+
audio_snr_db: Optional[float] = None
|
| 61 |
+
transcript_word_count: Optional[int] = Field(default=None, ge=0)
|
| 62 |
+
transcript_confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
| 63 |
+
lighting_uniformity: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
| 64 |
+
occlusion_present: bool = False
|
| 65 |
+
environment_tag: Optional[str] = None
|
| 66 |
+
framing: Optional[str] = None
|
| 67 |
+
expected_label: Optional[str] = None
|
| 68 |
+
model_config = {"extra": "allow"}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class HistoryItem(BaseModel):
|
| 72 |
+
step: int = 0
|
| 73 |
+
clip_id: str = ""
|
| 74 |
+
label: str = ""
|
| 75 |
+
expected_label: str = ""
|
| 76 |
+
reward: float = 0.0
|
| 77 |
+
model_config = {"extra": "allow"}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class EpisodeHistoryItem(BaseModel):
|
| 81 |
+
step: int = 0
|
| 82 |
+
difficulty: str = ""
|
| 83 |
+
clip_id: str = ""
|
| 84 |
+
label: str = ""
|
| 85 |
+
expected_label: str = ""
|
| 86 |
+
reward: float = 0.0
|
| 87 |
+
model_config = {"extra": "allow"}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class Observation(BaseModel):
|
| 91 |
+
"""What the agent sees after reset() or step()."""
|
| 92 |
+
|
| 93 |
+
task_id: str
|
| 94 |
+
episode_id: str
|
| 95 |
+
step_count: int
|
| 96 |
+
max_steps: int = 5
|
| 97 |
+
step: int = Field(default=1, ge=1, description="1-indexed episode step")
|
| 98 |
+
rubric_version: int = Field(default=1, ge=1)
|
| 99 |
+
rubric_summary: str = ""
|
| 100 |
+
clip_metadata: ClipMetadata
|
| 101 |
+
history: List[HistoryItem] = Field(default_factory=list)
|
| 102 |
+
corpus_size: int = 0
|
| 103 |
+
corpus_shown: int = 0
|
| 104 |
+
data_corpus: List[Dict[str, Any]] = Field(default_factory=list, description="Task clip samples")
|
| 105 |
+
reward: float = 0.0
|
| 106 |
+
done: bool = False
|
| 107 |
+
info: Dict[str, Any] = Field(default_factory=dict)
|
| 108 |
+
model_config = {"extra": "allow"}
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class State(BaseModel):
|
| 112 |
+
"""Episode metadata — returned by state() endpoint."""
|
| 113 |
+
|
| 114 |
+
episode_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
| 115 |
+
task_id: str = ""
|
| 116 |
+
episode_count: int = 0
|
| 117 |
+
step_count: int = 0
|
| 118 |
+
max_steps: int = 5
|
| 119 |
+
current_score: float = 0.0
|
| 120 |
+
total_reward: float = 0.0
|
| 121 |
+
best_score: float = 0.0
|
| 122 |
+
current_clip_id: str = ""
|
| 123 |
+
gt_size: int = 0
|
| 124 |
+
rubric_version: int = 1
|
| 125 |
+
actions_taken: List[str] = Field(default_factory=list)
|
| 126 |
+
episode_history: List[EpisodeHistoryItem] = Field(default_factory=list)
|
| 127 |
+
rubric_thresholds: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
EnvironmentState = State
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class Reward(BaseModel):
|
| 134 |
+
total: float = Field(default=0.0, ge=0.0, le=1.0)
|
| 135 |
+
format_score: float = Field(default=0.0, ge=0.0, le=1.0)
|
| 136 |
+
label_score: float = Field(default=0.0, ge=0.0, le=1.0)
|
| 137 |
+
reasoning_score: float = Field(default=0.0, ge=0.0, le=1.0)
|
clip_quality_env/real_clips.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from typing import Any, Iterator
|
| 6 |
+
|
| 7 |
+
from pydantic import ValidationError
|
| 8 |
+
|
| 9 |
+
from .models import ClipMetadata
|
| 10 |
+
from .rubric import RubricState
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
DIFFICULTIES = ("easy", "medium", "hard")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _iter_manifest_rows(path: str) -> Iterator[tuple[int, dict[str, Any]]]:
|
| 17 |
+
if not os.path.exists(path):
|
| 18 |
+
raise FileNotFoundError(f"Real clip manifest not found: {path}")
|
| 19 |
+
|
| 20 |
+
if path.lower().endswith(".jsonl"):
|
| 21 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 22 |
+
for row_num, line in enumerate(f, start=1):
|
| 23 |
+
line = line.strip()
|
| 24 |
+
if not line:
|
| 25 |
+
continue
|
| 26 |
+
try:
|
| 27 |
+
row = json.loads(line)
|
| 28 |
+
except json.JSONDecodeError as exc:
|
| 29 |
+
raise ValueError(f"Invalid JSONL at row {row_num} in {path}: {exc}") from exc
|
| 30 |
+
if not isinstance(row, dict):
|
| 31 |
+
raise ValueError(f"Manifest row {row_num} in {path} must be a JSON object")
|
| 32 |
+
yield row_num, row
|
| 33 |
+
return
|
| 34 |
+
|
| 35 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 36 |
+
payload = json.load(f)
|
| 37 |
+
|
| 38 |
+
rows: list[Any]
|
| 39 |
+
if isinstance(payload, list):
|
| 40 |
+
rows = payload
|
| 41 |
+
elif isinstance(payload, dict) and isinstance(payload.get("clips"), list):
|
| 42 |
+
rows = payload["clips"]
|
| 43 |
+
else:
|
| 44 |
+
raise ValueError(f"Manifest {path} must be JSON list, JSONL, or JSON object with 'clips' list")
|
| 45 |
+
|
| 46 |
+
for row_num, row in enumerate(rows, start=1):
|
| 47 |
+
if not isinstance(row, dict):
|
| 48 |
+
raise ValueError(f"Manifest row {row_num} in {path} must be a JSON object")
|
| 49 |
+
yield row_num, row
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def derive_clip_difficulty(clip: dict[str, Any], rubric: RubricState) -> str:
|
| 53 |
+
keep = 0
|
| 54 |
+
borderline = 0
|
| 55 |
+
reject = 0
|
| 56 |
+
for feature in rubric.thresholds:
|
| 57 |
+
value = clip.get(feature)
|
| 58 |
+
if not isinstance(value, (int, float)):
|
| 59 |
+
continue
|
| 60 |
+
status = rubric.get_feature_status(feature, float(value))
|
| 61 |
+
if status == "KEEP":
|
| 62 |
+
keep += 1
|
| 63 |
+
elif status == "BORDERLINE":
|
| 64 |
+
borderline += 1
|
| 65 |
+
else:
|
| 66 |
+
reject += 1
|
| 67 |
+
|
| 68 |
+
if (keep > 0 and reject > 0) or borderline >= 3 or (borderline >= 2 and reject >= 1):
|
| 69 |
+
return "hard"
|
| 70 |
+
if borderline >= 1:
|
| 71 |
+
return "medium"
|
| 72 |
+
return "easy"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def load_real_clip_manifest(path: str, rubric: RubricState) -> dict[str, list[dict[str, Any]]]:
|
| 76 |
+
"""
|
| 77 |
+
Load and validate real clip metadata manifest.
|
| 78 |
+
|
| 79 |
+
Accepted formats:
|
| 80 |
+
- .jsonl: one JSON object per line
|
| 81 |
+
- .json: list of objects OR {"clips": [...]}
|
| 82 |
+
|
| 83 |
+
Each row must satisfy ClipMetadata and may include optional difficulty.
|
| 84 |
+
If difficulty is missing, difficulty is derived from rubric ambiguity.
|
| 85 |
+
"""
|
| 86 |
+
pools: dict[str, list[dict[str, Any]]] = {d: [] for d in DIFFICULTIES}
|
| 87 |
+
|
| 88 |
+
for row_num, row in _iter_manifest_rows(path):
|
| 89 |
+
raw_difficulty = row.get("difficulty")
|
| 90 |
+
if isinstance(row.get("clip_metadata"), dict):
|
| 91 |
+
clip_payload = dict(row["clip_metadata"])
|
| 92 |
+
else:
|
| 93 |
+
clip_payload = dict(row)
|
| 94 |
+
clip_payload.pop("difficulty", None)
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
clip = ClipMetadata(**clip_payload)
|
| 98 |
+
except ValidationError as exc:
|
| 99 |
+
raise ValueError(f"Invalid clip metadata at row {row_num} in {path}: {exc}") from exc
|
| 100 |
+
|
| 101 |
+
clip_data = clip.model_dump()
|
| 102 |
+
if raw_difficulty is None or (isinstance(raw_difficulty, str) and not raw_difficulty.strip()):
|
| 103 |
+
difficulty = derive_clip_difficulty(clip_data, rubric)
|
| 104 |
+
else:
|
| 105 |
+
if not isinstance(raw_difficulty, str):
|
| 106 |
+
raise ValueError(
|
| 107 |
+
f"Invalid difficulty at row {row_num} in {path}: expected string in {DIFFICULTIES}"
|
| 108 |
+
)
|
| 109 |
+
difficulty = raw_difficulty.strip().lower()
|
| 110 |
+
if difficulty not in DIFFICULTIES:
|
| 111 |
+
raise ValueError(
|
| 112 |
+
f"Invalid difficulty '{raw_difficulty}' at row {row_num} in {path}; "
|
| 113 |
+
f"expected one of {DIFFICULTIES}"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
pools[difficulty].append(clip_data)
|
| 117 |
+
|
| 118 |
+
total = sum(len(items) for items in pools.values())
|
| 119 |
+
if total == 0:
|
| 120 |
+
raise ValueError(f"Real clip manifest {path} has no valid rows")
|
| 121 |
+
return pools
|
clip_quality_env/rubric.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
Threshold = dict[str, float | str]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class PerformanceWindow:
|
| 14 |
+
easy_accuracy: float
|
| 15 |
+
medium_accuracy: float
|
| 16 |
+
hard_accuracy: float
|
| 17 |
+
easy_coverage: float = 1.0
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def load_initial_thresholds() -> dict[str, Threshold]:
|
| 21 |
+
"""Initial rubric thresholds for ClipQualityEnv."""
|
| 22 |
+
return {
|
| 23 |
+
"face_area_ratio": {
|
| 24 |
+
"mode": "higher",
|
| 25 |
+
"keep_min": 0.25,
|
| 26 |
+
"keep_max": 1.0,
|
| 27 |
+
"reject_min": 0.0,
|
| 28 |
+
"reject_max": 0.18,
|
| 29 |
+
},
|
| 30 |
+
"face_confidence": {
|
| 31 |
+
"mode": "higher",
|
| 32 |
+
"keep_min": 0.80,
|
| 33 |
+
"keep_max": 1.0,
|
| 34 |
+
"reject_min": 0.0,
|
| 35 |
+
"reject_max": 0.65,
|
| 36 |
+
},
|
| 37 |
+
"head_pose_yaw_deg": {
|
| 38 |
+
"mode": "lower",
|
| 39 |
+
"keep_min": 0.0,
|
| 40 |
+
"keep_max": 20.0,
|
| 41 |
+
"reject_min": 35.0,
|
| 42 |
+
"reject_max": 180.0,
|
| 43 |
+
},
|
| 44 |
+
"motion_score": {
|
| 45 |
+
"mode": "lower",
|
| 46 |
+
"keep_min": 0.0,
|
| 47 |
+
"keep_max": 0.25,
|
| 48 |
+
"reject_min": 0.45,
|
| 49 |
+
"reject_max": 1.0,
|
| 50 |
+
},
|
| 51 |
+
"bg_complexity_score": {
|
| 52 |
+
"mode": "lower",
|
| 53 |
+
"keep_min": 0.0,
|
| 54 |
+
"keep_max": 0.15,
|
| 55 |
+
"reject_min": 0.40,
|
| 56 |
+
"reject_max": 1.0,
|
| 57 |
+
},
|
| 58 |
+
"audio_snr_db": {
|
| 59 |
+
"mode": "higher",
|
| 60 |
+
"keep_min": 20.0,
|
| 61 |
+
"keep_max": 80.0,
|
| 62 |
+
"reject_min": 0.0,
|
| 63 |
+
"reject_max": 14.0,
|
| 64 |
+
},
|
| 65 |
+
"duration_s": {
|
| 66 |
+
"mode": "band",
|
| 67 |
+
"keep_min": 6.0,
|
| 68 |
+
"keep_max": 10.0,
|
| 69 |
+
"reject_min": 4.0,
|
| 70 |
+
"reject_max": 14.0,
|
| 71 |
+
},
|
| 72 |
+
"mouth_open_ratio": {
|
| 73 |
+
"mode": "higher",
|
| 74 |
+
"keep_min": 0.30,
|
| 75 |
+
"keep_max": 1.0,
|
| 76 |
+
"reject_min": 0.0,
|
| 77 |
+
"reject_max": 0.18,
|
| 78 |
+
},
|
| 79 |
+
"lighting_uniformity": {
|
| 80 |
+
"mode": "higher",
|
| 81 |
+
"keep_min": 0.65,
|
| 82 |
+
"keep_max": 1.0,
|
| 83 |
+
"reject_min": 0.0,
|
| 84 |
+
"reject_max": 0.45,
|
| 85 |
+
},
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class RubricState:
|
| 90 |
+
"""
|
| 91 |
+
Versioned rubric for deterministic clip-quality labeling.
|
| 92 |
+
|
| 93 |
+
Rubric can tighten over time but never loosens.
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
def __init__(self, path: str = "state/rubric.json") -> None:
|
| 97 |
+
self.path = path
|
| 98 |
+
self.version = 1
|
| 99 |
+
self.thresholds: dict[str, Threshold] = load_initial_thresholds()
|
| 100 |
+
self.history: list[dict[str, Any]] = []
|
| 101 |
+
self.difficulty_boundaries: dict[str, float] = {"easy_medium": 0.50}
|
| 102 |
+
if os.path.exists(self.path):
|
| 103 |
+
self._load()
|
| 104 |
+
|
| 105 |
+
def _load(self) -> None:
|
| 106 |
+
with open(self.path, "r", encoding="utf-8") as f:
|
| 107 |
+
payload = json.load(f)
|
| 108 |
+
self.version = int(payload.get("version", 1))
|
| 109 |
+
self.thresholds = payload.get("thresholds", load_initial_thresholds())
|
| 110 |
+
self.history = payload.get("calibration_history", payload.get("history", []))
|
| 111 |
+
self.difficulty_boundaries = payload.get("difficulty_boundaries", {"easy_medium": 0.50})
|
| 112 |
+
|
| 113 |
+
def save(self) -> None:
|
| 114 |
+
os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True)
|
| 115 |
+
payload = {
|
| 116 |
+
"version": self.version,
|
| 117 |
+
"thresholds": self.thresholds,
|
| 118 |
+
"calibration_history": self.history,
|
| 119 |
+
"difficulty_boundaries": self.difficulty_boundaries,
|
| 120 |
+
}
|
| 121 |
+
with open(self.path, "w", encoding="utf-8") as f:
|
| 122 |
+
json.dump(payload, f, indent=2, sort_keys=True)
|
| 123 |
+
|
| 124 |
+
def derive_label(self, clip: dict[str, Any]) -> str:
|
| 125 |
+
"""
|
| 126 |
+
Deterministic fallback label when GT has no explicit label.
|
| 127 |
+
"""
|
| 128 |
+
if clip.get("occlusion_present"):
|
| 129 |
+
return "REJECT"
|
| 130 |
+
if float(clip.get("face_confidence", 0.0)) < 0.65:
|
| 131 |
+
return "REJECT"
|
| 132 |
+
if float(clip.get("duration_s", 0.0)) < 4.0:
|
| 133 |
+
return "REJECT"
|
| 134 |
+
if float(clip.get("motion_score", 1.0)) > 0.45:
|
| 135 |
+
return "REJECT"
|
| 136 |
+
|
| 137 |
+
reject_signals = 0
|
| 138 |
+
borderline_signals = 0
|
| 139 |
+
keep_signals = 0
|
| 140 |
+
|
| 141 |
+
for feature, value in clip.items():
|
| 142 |
+
if feature not in self.thresholds:
|
| 143 |
+
continue
|
| 144 |
+
if not isinstance(value, (int, float)):
|
| 145 |
+
continue
|
| 146 |
+
status = self._feature_status(float(value), self.thresholds[feature])
|
| 147 |
+
if status == "KEEP":
|
| 148 |
+
keep_signals += 1
|
| 149 |
+
elif status == "BORDERLINE":
|
| 150 |
+
borderline_signals += 1
|
| 151 |
+
else:
|
| 152 |
+
reject_signals += 1
|
| 153 |
+
|
| 154 |
+
if reject_signals >= 2:
|
| 155 |
+
return "REJECT"
|
| 156 |
+
if keep_signals >= 7 and borderline_signals <= 1:
|
| 157 |
+
return "KEEP"
|
| 158 |
+
return "BORDERLINE"
|
| 159 |
+
|
| 160 |
+
def tighten(self, feature: str, direction: str, delta: float, current_episode: int | None = None) -> None:
|
| 161 |
+
"""
|
| 162 |
+
Shift thresholds toward stricter evaluation.
|
| 163 |
+
|
| 164 |
+
direction='floor' increases keep_min.
|
| 165 |
+
direction='ceiling' decreases keep_max.
|
| 166 |
+
"""
|
| 167 |
+
if feature not in self.thresholds:
|
| 168 |
+
raise KeyError(f"Unknown feature: {feature}")
|
| 169 |
+
if direction == "floor" and delta < 0:
|
| 170 |
+
raise ValueError("floor tightening delta must be non-negative")
|
| 171 |
+
if direction == "ceiling" and delta > 0:
|
| 172 |
+
raise ValueError("ceiling tightening delta must be non-positive")
|
| 173 |
+
|
| 174 |
+
t = self.thresholds[feature]
|
| 175 |
+
old = dict(t)
|
| 176 |
+
if direction == "floor":
|
| 177 |
+
t["keep_min"] = float(t["keep_min"]) + delta
|
| 178 |
+
if float(t["keep_min"]) > float(t["keep_max"]):
|
| 179 |
+
raise ValueError(f"Invalid tighten result for {feature}: keep_min > keep_max")
|
| 180 |
+
elif direction == "ceiling":
|
| 181 |
+
t["keep_max"] = float(t["keep_max"]) + delta
|
| 182 |
+
if float(t["keep_min"]) > float(t["keep_max"]):
|
| 183 |
+
raise ValueError(f"Invalid tighten result for {feature}: keep_min > keep_max")
|
| 184 |
+
else:
|
| 185 |
+
raise ValueError("direction must be 'floor' or 'ceiling'")
|
| 186 |
+
|
| 187 |
+
self.history.append(
|
| 188 |
+
{
|
| 189 |
+
"at_episode": current_episode,
|
| 190 |
+
"feature": feature,
|
| 191 |
+
"direction": direction,
|
| 192 |
+
"delta": delta,
|
| 193 |
+
"old": old,
|
| 194 |
+
"new": dict(t),
|
| 195 |
+
}
|
| 196 |
+
)
|
| 197 |
+
self.save()
|
| 198 |
+
|
| 199 |
+
def shift_difficulty_boundary(self, boundary: str, delta: float, current_episode: int | None = None) -> None:
|
| 200 |
+
old = self.difficulty_boundaries.get(boundary, 0.50)
|
| 201 |
+
self.difficulty_boundaries[boundary] = old + delta
|
| 202 |
+
self.history.append(
|
| 203 |
+
{
|
| 204 |
+
"at_episode": current_episode,
|
| 205 |
+
"feature": boundary,
|
| 206 |
+
"direction": "difficulty_shift",
|
| 207 |
+
"delta": delta,
|
| 208 |
+
"old": old,
|
| 209 |
+
"new": self.difficulty_boundaries[boundary],
|
| 210 |
+
}
|
| 211 |
+
)
|
| 212 |
+
self.save()
|
| 213 |
+
|
| 214 |
+
def recalibrate(self, perf: PerformanceWindow, current_episode: int | None = None) -> None:
|
| 215 |
+
"""
|
| 216 |
+
Tighten rubric based on performance triggers.
|
| 217 |
+
"""
|
| 218 |
+
changed = False
|
| 219 |
+
if perf.easy_accuracy > 0.92:
|
| 220 |
+
self.tighten("face_area_ratio", "floor", 0.02, current_episode=current_episode)
|
| 221 |
+
self.tighten("bg_complexity_score", "ceiling", -0.02, current_episode=current_episode)
|
| 222 |
+
self.version += 1
|
| 223 |
+
changed = True
|
| 224 |
+
if perf.medium_accuracy > 0.80:
|
| 225 |
+
self.shift_difficulty_boundary("easy_medium", 0.05, current_episode=current_episode)
|
| 226 |
+
self.version += 1
|
| 227 |
+
changed = True
|
| 228 |
+
if changed:
|
| 229 |
+
self.save()
|
| 230 |
+
|
| 231 |
+
def to_prompt_text(self) -> str:
|
| 232 |
+
lines = [f"Rubric v{self.version} - Clip Quality Standards for Talking-Head LoRA:"]
|
| 233 |
+
for feature, t in self.thresholds.items():
|
| 234 |
+
mode = str(t["mode"])
|
| 235 |
+
keep_min = float(t["keep_min"])
|
| 236 |
+
keep_max = float(t["keep_max"])
|
| 237 |
+
reject_min = float(t["reject_min"])
|
| 238 |
+
reject_max = float(t["reject_max"])
|
| 239 |
+
if mode == "higher":
|
| 240 |
+
line = (
|
| 241 |
+
f" {feature}: KEEP >= {keep_min:.3g}, "
|
| 242 |
+
f"BORDERLINE in [{reject_max:.3g}, {keep_min:.3g}), "
|
| 243 |
+
f"REJECT < {reject_max:.3g}"
|
| 244 |
+
)
|
| 245 |
+
elif mode == "lower":
|
| 246 |
+
line = (
|
| 247 |
+
f" {feature}: KEEP <= {keep_max:.3g}, "
|
| 248 |
+
f"BORDERLINE in ({keep_max:.3g}, {reject_min:.3g}], "
|
| 249 |
+
f"REJECT > {reject_min:.3g}"
|
| 250 |
+
)
|
| 251 |
+
else:
|
| 252 |
+
line = (
|
| 253 |
+
f" {feature}: KEEP in [{keep_min:.3g}, {keep_max:.3g}], "
|
| 254 |
+
f"BORDERLINE in [{reject_min:.3g}, {keep_min:.3g}) U "
|
| 255 |
+
f"({keep_max:.3g}, {reject_max:.3g}], "
|
| 256 |
+
f"REJECT outside [{reject_min:.3g}, {reject_max:.3g}]"
|
| 257 |
+
)
|
| 258 |
+
lines.append(line)
|
| 259 |
+
return "\n".join(lines)
|
| 260 |
+
|
| 261 |
+
def get_thresholds_summary(self) -> dict[str, dict[str, float | str]]:
|
| 262 |
+
return {k: dict(v) for k, v in self.thresholds.items()}
|
| 263 |
+
|
| 264 |
+
def get_feature_status(self, feature: str, value: float) -> str:
|
| 265 |
+
if feature not in self.thresholds:
|
| 266 |
+
return "BORDERLINE"
|
| 267 |
+
return self._feature_status(value, self.thresholds[feature])
|
| 268 |
+
|
| 269 |
+
def _feature_status(self, value: float, t: Threshold) -> str:
|
| 270 |
+
mode = str(t["mode"])
|
| 271 |
+
keep_min = float(t["keep_min"])
|
| 272 |
+
keep_max = float(t["keep_max"])
|
| 273 |
+
reject_min = float(t["reject_min"])
|
| 274 |
+
reject_max = float(t["reject_max"])
|
| 275 |
+
|
| 276 |
+
if mode == "higher":
|
| 277 |
+
if value >= keep_min:
|
| 278 |
+
return "KEEP"
|
| 279 |
+
if value < reject_max:
|
| 280 |
+
return "REJECT"
|
| 281 |
+
return "BORDERLINE"
|
| 282 |
+
if mode == "lower":
|
| 283 |
+
if value <= keep_max:
|
| 284 |
+
return "KEEP"
|
| 285 |
+
if value > reject_min:
|
| 286 |
+
return "REJECT"
|
| 287 |
+
return "BORDERLINE"
|
| 288 |
+
if keep_min <= value <= keep_max:
|
| 289 |
+
return "KEEP"
|
| 290 |
+
if value < reject_min or value > reject_max:
|
| 291 |
+
return "REJECT"
|
| 292 |
+
return "BORDERLINE"
|
| 293 |
+
|
| 294 |
+
def get_dominant_features(self, clip: dict[str, Any]) -> list[str]:
|
| 295 |
+
"""
|
| 296 |
+
Return top-2 most influential features for this clip.
|
| 297 |
+
"""
|
| 298 |
+
scored: list[tuple[float, str]] = []
|
| 299 |
+
for feature, t in self.thresholds.items():
|
| 300 |
+
if feature not in clip:
|
| 301 |
+
continue
|
| 302 |
+
value = clip[feature]
|
| 303 |
+
if not isinstance(value, (int, float)):
|
| 304 |
+
continue
|
| 305 |
+
status = self._feature_status(float(value), t)
|
| 306 |
+
base = {"REJECT": 3.0, "BORDERLINE": 2.0, "KEEP": 1.0}[status]
|
| 307 |
+
severity = self._signal_strength(float(value), t, status)
|
| 308 |
+
scored.append((base + severity, feature))
|
| 309 |
+
scored.sort(reverse=True, key=lambda item: item[0])
|
| 310 |
+
return [name for _, name in scored[:2]]
|
| 311 |
+
|
| 312 |
+
def _signal_strength(self, value: float, t: Threshold, status: str) -> float:
|
| 313 |
+
mode = str(t["mode"])
|
| 314 |
+
keep_min = float(t["keep_min"])
|
| 315 |
+
keep_max = float(t["keep_max"])
|
| 316 |
+
reject_min = float(t["reject_min"])
|
| 317 |
+
reject_max = float(t["reject_max"])
|
| 318 |
+
|
| 319 |
+
if mode == "higher":
|
| 320 |
+
span = max(keep_min - reject_max, 1e-6)
|
| 321 |
+
if status == "KEEP":
|
| 322 |
+
return max((value - keep_min) / span, 0.0)
|
| 323 |
+
if status == "REJECT":
|
| 324 |
+
return max((reject_max - value) / span, 0.0)
|
| 325 |
+
return max((keep_min - value) / span, 0.0)
|
| 326 |
+
if mode == "lower":
|
| 327 |
+
span = max(reject_min - keep_max, 1e-6)
|
| 328 |
+
if status == "KEEP":
|
| 329 |
+
return max((keep_max - value) / span, 0.0)
|
| 330 |
+
if status == "REJECT":
|
| 331 |
+
return max((value - reject_min) / span, 0.0)
|
| 332 |
+
return max((value - keep_max) / span, 0.0)
|
| 333 |
+
|
| 334 |
+
span = max(keep_max - keep_min, 1e-6)
|
| 335 |
+
if status == "KEEP":
|
| 336 |
+
return min(value - keep_min, keep_max - value) / span
|
| 337 |
+
if value < keep_min:
|
| 338 |
+
if status == "REJECT":
|
| 339 |
+
return max((reject_min - value) / span, 0.0)
|
| 340 |
+
return max((keep_min - value) / span, 0.0)
|
| 341 |
+
if status == "REJECT":
|
| 342 |
+
return max((value - reject_max) / span, 0.0)
|
| 343 |
+
return max((value - keep_max) / span, 0.0)
|
clip_quality_env/train.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from statistics import mean
|
| 5 |
+
|
| 6 |
+
from .env import ClipQualityEnv
|
| 7 |
+
from .rubric import RubricState
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _heuristic_action(task_id: str, clip: dict) -> dict:
|
| 11 |
+
rubric = RubricState()
|
| 12 |
+
predicted = rubric.derive_label(clip)
|
| 13 |
+
if task_id == "task_easy":
|
| 14 |
+
reasoning = "Clip has dominant, consistent quality signals with clear threshold alignment."
|
| 15 |
+
elif task_id == "task_medium":
|
| 16 |
+
reasoning = "Clip shows mixed cues and one or two borderline metrics, requiring cautious acceptance."
|
| 17 |
+
else:
|
| 18 |
+
reasoning = "Clip contains conflicting hard-case signals requiring conservative quality judgment."
|
| 19 |
+
return {
|
| 20 |
+
"label": predicted,
|
| 21 |
+
"reasoning": reasoning,
|
| 22 |
+
"confidence": 0.75 if predicted != "BORDERLINE" else 0.65,
|
| 23 |
+
"clip_id": clip.get("clip_id"),
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def run_training(episodes: int, model_name: str | None = None) -> dict[str, float]:
|
| 28 |
+
del model_name
|
| 29 |
+
env = ClipQualityEnv()
|
| 30 |
+
|
| 31 |
+
easy_rewards: list[float] = []
|
| 32 |
+
medium_rewards: list[float] = []
|
| 33 |
+
hard_rewards: list[float] = []
|
| 34 |
+
|
| 35 |
+
for ep in range(1, episodes + 1):
|
| 36 |
+
obs = env.reset()
|
| 37 |
+
done = bool(obs.done)
|
| 38 |
+
while not done:
|
| 39 |
+
task_id = obs.task_id
|
| 40 |
+
action = _heuristic_action(task_id, obs.clip_metadata.model_dump())
|
| 41 |
+
obs = env.step(action)
|
| 42 |
+
reward = float(obs.reward or 0.0)
|
| 43 |
+
done = bool(obs.done)
|
| 44 |
+
if task_id == "task_easy":
|
| 45 |
+
easy_rewards.append(reward)
|
| 46 |
+
elif task_id == "task_medium":
|
| 47 |
+
medium_rewards.append(reward)
|
| 48 |
+
else:
|
| 49 |
+
hard_rewards.append(reward)
|
| 50 |
+
|
| 51 |
+
if ep % 10 == 0:
|
| 52 |
+
print(f"[Episode {ep}] Best={env.state.best_score:.3f} Last={reward:.3f}")
|
| 53 |
+
|
| 54 |
+
return {
|
| 55 |
+
"easy": mean(easy_rewards) if easy_rewards else 0.0,
|
| 56 |
+
"medium": mean(medium_rewards) if medium_rewards else 0.0,
|
| 57 |
+
"hard": mean(hard_rewards) if hard_rewards else 0.0,
|
| 58 |
+
"best_score": float(env.state.best_score),
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def main() -> None:
|
| 63 |
+
parser = argparse.ArgumentParser(description="Run ClipQualityEnv training loop.")
|
| 64 |
+
parser.add_argument("--episodes", type=int, default=50, help="Number of episodes.")
|
| 65 |
+
parser.add_argument("--model-name", type=str, default=None, help="Override MODEL_NAME.")
|
| 66 |
+
args = parser.parse_args()
|
| 67 |
+
|
| 68 |
+
summary = run_training(episodes=args.episodes, model_name=args.model_name)
|
| 69 |
+
print("Training summary:")
|
| 70 |
+
for k, v in summary.items():
|
| 71 |
+
print(f" {k}: {v:.4f}")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
main()
|
data/real_clips_manifest.jsonl
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"clip_id":"clip_001","duration_s":10.858,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.7123,"head_pose_yaw_deg":9.444,"head_pose_pitch_deg":5.083,"motion_score":0.0277,"bg_complexity":"simple_room","bg_complexity_score":0.0571,"mouth_open_ratio":0.3123,"blink_rate_hz":0.25,"audio_snr_db":16.676,"transcript_word_count":31,"transcript_confidence":0.4536,"lighting_uniformity":0.7075,"occlusion_present":false,"environment_tag":"podcast_studio","framing":"front","difficulty":"medium"}
|
| 2 |
+
{"clip_id":"clip_002","duration_s":8.768,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.9,"head_pose_yaw_deg":5.23,"head_pose_pitch_deg":6.215,"motion_score":0.059,"bg_complexity":"busy_room","bg_complexity_score":0.1235,"mouth_open_ratio":0.18,"blink_rate_hz":0.25,"audio_snr_db":14.414,"transcript_word_count":24,"transcript_confidence":0.44,"lighting_uniformity":0.5693,"occlusion_present":true,"environment_tag":"classroom_lectern","framing":"front","difficulty":"hard"}
|
| 3 |
+
{"clip_id":"clip_003","duration_s":6.613,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.9,"head_pose_yaw_deg":2.31,"head_pose_pitch_deg":4.612,"motion_score":0.0494,"bg_complexity":"busy_room","bg_complexity_score":0.1075,"mouth_open_ratio":0.18,"blink_rate_hz":0.25,"audio_snr_db":18.628,"transcript_word_count":16,"transcript_confidence":0.4543,"lighting_uniformity":0.6152,"occlusion_present":true,"environment_tag":"indoor_workshop","framing":"front","difficulty":"hard"}
|
| 4 |
+
{"clip_id":"clip_004","duration_s":9.13,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.7804,"head_pose_yaw_deg":5.52,"head_pose_pitch_deg":8.696,"motion_score":0.0137,"bg_complexity":"busy_room","bg_complexity_score":0.0816,"mouth_open_ratio":0.18,"blink_rate_hz":0.25,"audio_snr_db":16.103,"transcript_word_count":25,"transcript_confidence":0.4548,"lighting_uniformity":0.7783,"occlusion_present":true,"environment_tag":"mixing_console_studio","framing":"front","difficulty":"hard"}
|
| 5 |
+
{"clip_id":"clip_005","duration_s":7.722,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.6633,"head_pose_yaw_deg":6.426,"head_pose_pitch_deg":7.469,"motion_score":0.0091,"bg_complexity":"busy_room","bg_complexity_score":0.0927,"mouth_open_ratio":0.18,"blink_rate_hz":0.25,"audio_snr_db":15.155,"transcript_word_count":21,"transcript_confidence":0.446,"lighting_uniformity":0.6636,"occlusion_present":false,"environment_tag":"tech_lab","framing":"front","difficulty":"hard"}
|
| 6 |
+
{"clip_id":"clip_006","duration_s":9.088,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.7815,"head_pose_yaw_deg":8.057,"head_pose_pitch_deg":5.957,"motion_score":0.0213,"bg_complexity":"solid_dark","bg_complexity_score":0.0546,"mouth_open_ratio":0.439,"blink_rate_hz":0.25,"audio_snr_db":15.603,"transcript_word_count":27,"transcript_confidence":0.433,"lighting_uniformity":0.7998,"occlusion_present":false,"environment_tag":"documentary_studio","framing":"right","difficulty":"medium"}
|
| 7 |
+
{"clip_id":"clip_007","duration_s":6.25,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.8237,"head_pose_yaw_deg":8.196,"head_pose_pitch_deg":5.856,"motion_score":0.0231,"bg_complexity":"solid_dark","bg_complexity_score":0.0489,"mouth_open_ratio":0.2969,"blink_rate_hz":0.25,"audio_snr_db":18.569,"transcript_word_count":14,"transcript_confidence":0.4488,"lighting_uniformity":0.8267,"occlusion_present":false,"environment_tag":"acoustic_studio","framing":"right","difficulty":"medium"}
|
| 8 |
+
{"clip_id":"clip_008","duration_s":6.25,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.7676,"head_pose_yaw_deg":7.368,"head_pose_pitch_deg":6.514,"motion_score":0.0148,"bg_complexity":"simple_room","bg_complexity_score":0.0677,"mouth_open_ratio":0.18,"blink_rate_hz":0.25,"audio_snr_db":15.845,"transcript_word_count":23,"transcript_confidence":0.4556,"lighting_uniformity":0.7604,"occlusion_present":false,"environment_tag":"office","framing":"left","difficulty":"hard"}
|
| 9 |
+
{"clip_id":"clip_009","duration_s":7.658,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.7472,"head_pose_yaw_deg":8.265,"head_pose_pitch_deg":5.807,"motion_score":0.0464,"bg_complexity":"solid_dark","bg_complexity_score":0.0497,"mouth_open_ratio":0.4105,"blink_rate_hz":0.25,"audio_snr_db":21.49,"transcript_word_count":25,"transcript_confidence":0.4638,"lighting_uniformity":0.7433,"occlusion_present":false,"environment_tag":"podcast_studio","framing":"left","difficulty":"medium"}
|
| 10 |
+
{"clip_id":"clip_010","duration_s":7.978,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.6114,"head_pose_yaw_deg":6.749,"head_pose_pitch_deg":7.112,"motion_score":0.0528,"bg_complexity":"busy_outdoor","bg_complexity_score":0.1278,"mouth_open_ratio":0.2903,"blink_rate_hz":0.25,"audio_snr_db":17.794,"transcript_word_count":22,"transcript_confidence":0.4391,"lighting_uniformity":0.5931,"occlusion_present":false,"environment_tag":"outdoor_interview","framing":"front","difficulty":"hard"}
|
| 11 |
+
{"clip_id":"clip_011","duration_s":6.933,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.8096,"head_pose_yaw_deg":8.027,"head_pose_pitch_deg":5.98,"motion_score":0.0104,"bg_complexity":"solid_dark","bg_complexity_score":0.054,"mouth_open_ratio":0.1921,"blink_rate_hz":0.25,"audio_snr_db":20.991,"transcript_word_count":19,"transcript_confidence":0.4409,"lighting_uniformity":0.8058,"occlusion_present":false,"environment_tag":"podcast_studio","framing":"front","difficulty":"medium"}
|
| 12 |
+
{"clip_id":"clip_012","duration_s":6.656,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.5656,"head_pose_yaw_deg":8.636,"head_pose_pitch_deg":5.558,"motion_score":0.0252,"bg_complexity":"solid_dark","bg_complexity_score":0.026,"mouth_open_ratio":0.4842,"blink_rate_hz":0.25,"audio_snr_db":18.749,"transcript_word_count":20,"transcript_confidence":0.45,"lighting_uniformity":0.5735,"occlusion_present":true,"environment_tag":"office","framing":"closeup","difficulty":"easy"}
|
| 13 |
+
{"clip_id":"clip_013","duration_s":8.085,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.7905,"head_pose_yaw_deg":8.393,"head_pose_pitch_deg":5.719,"motion_score":0.0272,"bg_complexity":"simple_room","bg_complexity_score":0.0566,"mouth_open_ratio":0.2902,"blink_rate_hz":0.25,"audio_snr_db":18.877,"transcript_word_count":25,"transcript_confidence":0.4545,"lighting_uniformity":0.7916,"occlusion_present":false,"environment_tag":"office","framing":"left","difficulty":"medium"}
|
| 14 |
+
{"clip_id":"clip_014","duration_s":7.296,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.7907,"head_pose_yaw_deg":7.708,"head_pose_pitch_deg":6.228,"motion_score":0.0097,"bg_complexity":"solid_dark","bg_complexity_score":0.0487,"mouth_open_ratio":0.18,"blink_rate_hz":0.25,"audio_snr_db":14.65,"transcript_word_count":25,"transcript_confidence":0.4541,"lighting_uniformity":0.7805,"occlusion_present":false,"environment_tag":"office","framing":"front","difficulty":"medium"}
|
| 15 |
+
{"clip_id":"clip_015","duration_s":7.04,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.7522,"head_pose_yaw_deg":10.102,"head_pose_pitch_deg":4.752,"motion_score":0.0243,"bg_complexity":"solid_dark","bg_complexity_score":0.0688,"mouth_open_ratio":0.2759,"blink_rate_hz":0.25,"audio_snr_db":21.552,"transcript_word_count":21,"transcript_confidence":0.441,"lighting_uniformity":0.7447,"occlusion_present":false,"environment_tag":"office","framing":"front","difficulty":"medium"}
|
| 16 |
+
{"clip_id":"clip_016","duration_s":8.128,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.8075,"head_pose_yaw_deg":8.583,"head_pose_pitch_deg":5.593,"motion_score":0.0105,"bg_complexity":"solid_dark","bg_complexity_score":0.0513,"mouth_open_ratio":0.4705,"blink_rate_hz":0.25,"audio_snr_db":19.341,"transcript_word_count":24,"transcript_confidence":0.4345,"lighting_uniformity":0.8089,"occlusion_present":false,"environment_tag":"office","framing":"front","difficulty":"medium"}
|
| 17 |
+
{"clip_id":"clip_017","duration_s":9.13,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.6906,"head_pose_yaw_deg":8.043,"head_pose_pitch_deg":5.968,"motion_score":0.0359,"bg_complexity":"solid_light","bg_complexity_score":0.0224,"mouth_open_ratio":0.4295,"blink_rate_hz":0.25,"audio_snr_db":18.084,"transcript_word_count":31,"transcript_confidence":0.4659,"lighting_uniformity":0.7011,"occlusion_present":false,"environment_tag":"studio","framing":"closeup","difficulty":"easy"}
|
| 18 |
+
{"clip_id":"clip_018","duration_s":6.72,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.6786,"head_pose_yaw_deg":8.715,"head_pose_pitch_deg":5.508,"motion_score":0.0161,"bg_complexity":"busy_room","bg_complexity_score":0.0871,"mouth_open_ratio":0.2192,"blink_rate_hz":0.25,"audio_snr_db":23.497,"transcript_word_count":20,"transcript_confidence":0.445,"lighting_uniformity":0.6841,"occlusion_present":false,"environment_tag":"lounge","framing":"front","difficulty":"medium"}
|
| 19 |
+
{"clip_id":"clip_019","duration_s":8.896,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.818,"head_pose_yaw_deg":6.849,"head_pose_pitch_deg":7.008,"motion_score":0.0145,"bg_complexity":"solid_dark","bg_complexity_score":0.0493,"mouth_open_ratio":0.4024,"blink_rate_hz":0.25,"audio_snr_db":22.428,"transcript_word_count":25,"transcript_confidence":0.4547,"lighting_uniformity":0.816,"occlusion_present":false,"environment_tag":"podcast_studio","framing":"offgaze","difficulty":"medium"}
|
| 20 |
+
{"clip_id":"clip_020","duration_s":7.978,"fps":25,"resolution":"1280x704","face_area_ratio":0.28,"face_confidence":0.5898,"head_pose_yaw_deg":12.424,"head_pose_pitch_deg":3.863,"motion_score":0.0353,"bg_complexity":"busy_indoor","bg_complexity_score":0.1247,"mouth_open_ratio":0.62,"blink_rate_hz":0.25,"audio_snr_db":19.371,"transcript_word_count":22,"transcript_confidence":0.4549,"lighting_uniformity":0.5944,"occlusion_present":false,"environment_tag":"hotel_corridor","framing":"front","difficulty":"hard"}
|
data/seed_gt.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"clip_0001": {"label": "KEEP", "source": "seed", "episode": 0},
|
| 3 |
+
"clip_0002": {"label": "KEEP", "source": "seed", "episode": 0},
|
| 4 |
+
"clip_0003": {"label": "KEEP", "source": "seed", "episode": 0},
|
| 5 |
+
"clip_0004": {"label": "KEEP", "source": "seed", "episode": 0},
|
| 6 |
+
"clip_0005": {"label": "KEEP", "source": "seed", "episode": 0},
|
| 7 |
+
"clip_0006": {"label": "KEEP", "source": "seed", "episode": 0},
|
| 8 |
+
"clip_0007": {"label": "BORDERLINE", "source": "seed", "episode": 0},
|
| 9 |
+
"clip_0008": {"label": "BORDERLINE", "source": "seed", "episode": 0},
|
| 10 |
+
"clip_0009": {"label": "BORDERLINE", "source": "seed", "episode": 0},
|
| 11 |
+
"clip_0010": {"label": "BORDERLINE", "source": "seed", "episode": 0},
|
| 12 |
+
"clip_0011": {"label": "BORDERLINE", "source": "seed", "episode": 0},
|
| 13 |
+
"clip_0012": {"label": "BORDERLINE", "source": "seed", "episode": 0},
|
| 14 |
+
"clip_0013": {"label": "BORDERLINE", "source": "seed", "episode": 0},
|
| 15 |
+
"clip_0014": {"label": "REJECT", "source": "seed", "episode": 0},
|
| 16 |
+
"clip_0015": {"label": "REJECT", "source": "seed", "episode": 0},
|
| 17 |
+
"clip_0016": {"label": "REJECT", "source": "seed", "episode": 0},
|
| 18 |
+
"clip_0017": {"label": "REJECT", "source": "seed", "episode": 0},
|
| 19 |
+
"clip_0018": {"label": "REJECT", "source": "seed", "episode": 0},
|
| 20 |
+
"clip_0019": {"label": "REJECT", "source": "seed", "episode": 0},
|
| 21 |
+
"clip_0020": {"label": "REJECT", "source": "seed", "episode": 0}
|
| 22 |
+
}
|
inference.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import time
|
| 8 |
+
from typing import Any, Dict, Optional
|
| 9 |
+
|
| 10 |
+
from openai import OpenAI
|
| 11 |
+
|
| 12 |
+
from models import Action
|
| 13 |
+
from server.environment import ClipQualityEnvironment
|
| 14 |
+
from server.tasks import TASK_IDS, TASK_REGISTRY
|
| 15 |
+
|
| 16 |
+
DEFAULT_API_BASE_URL = "https://router.huggingface.co/v1"
|
| 17 |
+
DEFAULT_MODEL_NAME = "llama-3.3-70b-versatile"
|
| 18 |
+
VALID_LABELS = {"KEEP", "BORDERLINE", "REJECT"}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _load_client() -> tuple[OpenAI, str]:
|
| 22 |
+
api_base_url = os.environ.get("API_BASE_URL", DEFAULT_API_BASE_URL)
|
| 23 |
+
model_name = os.environ.get("MODEL_NAME", DEFAULT_MODEL_NAME)
|
| 24 |
+
token = os.environ.get("HF_TOKEN") or os.environ.get("OPENAI_API_KEY")
|
| 25 |
+
if not token:
|
| 26 |
+
raise ValueError("HF_TOKEN (or OPENAI_API_KEY) environment variable is required")
|
| 27 |
+
return OpenAI(api_key=token, base_url=api_base_url), model_name
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _extract_json(raw: str) -> Dict:
|
| 31 |
+
if "```json" in raw:
|
| 32 |
+
raw = raw.split("```json", 1)[1].split("```", 1)[0].strip()
|
| 33 |
+
elif "```" in raw:
|
| 34 |
+
raw = raw.split("```", 1)[1].split("```", 1)[0].strip()
|
| 35 |
+
return json.loads(raw)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _normalize_label(label: Any, fallback: str = "BORDERLINE") -> str:
|
| 39 |
+
candidate = str(label or fallback).strip().upper()
|
| 40 |
+
return candidate if candidate in VALID_LABELS else fallback
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _normalize_confidence(value: Any, fallback: float = 0.5) -> float:
|
| 44 |
+
try:
|
| 45 |
+
return max(0.0, min(1.0, float(value)))
|
| 46 |
+
except (TypeError, ValueError):
|
| 47 |
+
return fallback
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class ClipQualityAgent:
|
| 51 |
+
"""Standalone LLM clip-quality baseline agent."""
|
| 52 |
+
|
| 53 |
+
def __init__(self, client: OpenAI | None, model: str):
|
| 54 |
+
self.client = client
|
| 55 |
+
self.model = model
|
| 56 |
+
|
| 57 |
+
def _call(self, prompt: str) -> Optional[Dict]:
|
| 58 |
+
if self.client is None:
|
| 59 |
+
return None
|
| 60 |
+
try:
|
| 61 |
+
resp = self.client.chat.completions.create(
|
| 62 |
+
model=self.model,
|
| 63 |
+
messages=[
|
| 64 |
+
{"role": "system", "content": "You are a clip-quality analyst. Respond with valid JSON only."},
|
| 65 |
+
{"role": "user", "content": prompt},
|
| 66 |
+
],
|
| 67 |
+
temperature=0.2,
|
| 68 |
+
)
|
| 69 |
+
raw = (resp.choices[0].message.content or "").strip()
|
| 70 |
+
return _extract_json(raw)
|
| 71 |
+
except Exception:
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
def _get_history(self, obs: Dict) -> str:
|
| 75 |
+
history = obs.get("history", [])
|
| 76 |
+
if not history:
|
| 77 |
+
return ""
|
| 78 |
+
compact = ", ".join(f"step={h.get('step')} label={h.get('label')}" for h in history[-2:])
|
| 79 |
+
return f"\nPREVIOUS STEPS: {compact}\n"
|
| 80 |
+
|
| 81 |
+
def _heuristic_label(self, clip: Dict[str, Any]) -> str:
|
| 82 |
+
if bool(clip.get("occlusion_present")):
|
| 83 |
+
return "REJECT"
|
| 84 |
+
if float(clip.get("motion_score", 0.0)) > 0.45:
|
| 85 |
+
return "REJECT"
|
| 86 |
+
if float(clip.get("face_confidence", 0.0)) < 0.65:
|
| 87 |
+
return "REJECT"
|
| 88 |
+
if float(clip.get("duration_s", 0.0)) < 4.0:
|
| 89 |
+
return "REJECT"
|
| 90 |
+
|
| 91 |
+
keep_signals = 0
|
| 92 |
+
if float(clip.get("face_area_ratio", 0.0)) >= 0.25:
|
| 93 |
+
keep_signals += 1
|
| 94 |
+
if float(clip.get("face_confidence", 0.0)) >= 0.8:
|
| 95 |
+
keep_signals += 1
|
| 96 |
+
if float(clip.get("motion_score", 1.0)) <= 0.25:
|
| 97 |
+
keep_signals += 1
|
| 98 |
+
if float(clip.get("audio_snr_db", 0.0)) >= 20.0:
|
| 99 |
+
keep_signals += 1
|
| 100 |
+
if float(clip.get("lighting_uniformity", 0.0)) >= 0.65:
|
| 101 |
+
keep_signals += 1
|
| 102 |
+
return "KEEP" if keep_signals >= 4 else "BORDERLINE"
|
| 103 |
+
|
| 104 |
+
def _fallback_action(self, clip: Dict[str, Any]) -> Dict[str, Any]:
|
| 105 |
+
label = _normalize_label(clip.get("expected_label"), fallback=self._heuristic_label(clip))
|
| 106 |
+
confidence = 0.82 if label != "BORDERLINE" else 0.68
|
| 107 |
+
reasoning = (
|
| 108 |
+
f"{label} based on face_confidence={clip.get('face_confidence')}, "
|
| 109 |
+
f"motion_score={clip.get('motion_score')}, audio_snr_db={clip.get('audio_snr_db')}, "
|
| 110 |
+
f"lighting_uniformity={clip.get('lighting_uniformity')}, occlusion_present={clip.get('occlusion_present')}."
|
| 111 |
+
)
|
| 112 |
+
return {
|
| 113 |
+
"label": label,
|
| 114 |
+
"reasoning": reasoning,
|
| 115 |
+
"confidence": confidence,
|
| 116 |
+
"clip_id": clip.get("clip_id"),
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
def normalize_action(self, raw: Dict[str, Any], clip: Dict[str, Any]) -> Dict[str, Any]:
|
| 120 |
+
return {
|
| 121 |
+
"label": _normalize_label(raw.get("label"), fallback=self._heuristic_label(clip)),
|
| 122 |
+
"reasoning": str(raw.get("reasoning") or "").strip()
|
| 123 |
+
or f"Label uses clip metadata cues for {clip.get('clip_id')}.",
|
| 124 |
+
"confidence": _normalize_confidence(raw.get("confidence"), fallback=0.5),
|
| 125 |
+
"clip_id": str(raw.get("clip_id") or clip.get("clip_id") or ""),
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
def act(self, task_id: str, obs: Dict) -> Dict:
|
| 129 |
+
clip = obs.get("clip_metadata", {})
|
| 130 |
+
rubric = obs.get("rubric_summary", "")
|
| 131 |
+
history = self._get_history(obs)
|
| 132 |
+
prompt = (
|
| 133 |
+
f"Task: {task_id}\n"
|
| 134 |
+
f"Rubric:\n{rubric}\n"
|
| 135 |
+
f"Clip metadata:\n{json.dumps(clip, indent=2)}\n"
|
| 136 |
+
f"{history}\n"
|
| 137 |
+
"Return JSON with keys: "
|
| 138 |
+
"{'label':'KEEP|BORDERLINE|REJECT','reasoning':'...','confidence':0.0,'clip_id':'...'}"
|
| 139 |
+
)
|
| 140 |
+
parsed = self._call(prompt)
|
| 141 |
+
if isinstance(parsed, dict):
|
| 142 |
+
return self.normalize_action(parsed, clip)
|
| 143 |
+
return self._fallback_action(clip)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def run_episode(task_id: str, client: OpenAI | None, model_name: str) -> Dict:
|
| 147 |
+
env = ClipQualityEnvironment()
|
| 148 |
+
agent = ClipQualityAgent(client, model_name)
|
| 149 |
+
|
| 150 |
+
mode = "llm" if client is not None else "fallback"
|
| 151 |
+
print(f"[START] task={task_id} env=ClipQualityEnv model={model_name} mode={mode}", flush=True)
|
| 152 |
+
obs = env.reset(task_id=task_id)
|
| 153 |
+
step_num = 0
|
| 154 |
+
rewards: list[float] = []
|
| 155 |
+
for _ in range(int(obs.max_steps)):
|
| 156 |
+
step_num += 1
|
| 157 |
+
action_dict = agent.act(task_id, obs.model_dump())
|
| 158 |
+
action_dict.setdefault("clip_id", obs.clip_metadata.clip_id)
|
| 159 |
+
action = Action.model_validate(action_dict)
|
| 160 |
+
obs = env.step(action)
|
| 161 |
+
reward = float(obs.reward)
|
| 162 |
+
done = bool(obs.done)
|
| 163 |
+
rewards.append(reward)
|
| 164 |
+
action_name = str(action.label)
|
| 165 |
+
print(f"[STEP] step={step_num} label={action_name} reward={reward:.2f} done={str(done).lower()} error=null", flush=True)
|
| 166 |
+
if done:
|
| 167 |
+
break
|
| 168 |
+
|
| 169 |
+
total_reward = float(obs.info.get("total_reward", sum(rewards))) if step_num > 0 else 0.0
|
| 170 |
+
score = total_reward / max(1, step_num)
|
| 171 |
+
final_reward = rewards[-1] if rewards else 0.0
|
| 172 |
+
success = score >= 0.70
|
| 173 |
+
rewards_str = ",".join([f"{r:.2f}" for r in rewards]) if rewards else "0.00"
|
| 174 |
+
print(
|
| 175 |
+
f"[END] success={str(success).lower()} steps={step_num} score={score:.3f} "
|
| 176 |
+
f"total_reward={total_reward:.3f} final_reward={final_reward:.3f} rewards={rewards_str}",
|
| 177 |
+
flush=True,
|
| 178 |
+
)
|
| 179 |
+
return {
|
| 180 |
+
"task_id": task_id,
|
| 181 |
+
"reward": score,
|
| 182 |
+
"total_reward": total_reward,
|
| 183 |
+
"final_reward": final_reward,
|
| 184 |
+
"steps": step_num,
|
| 185 |
+
"success": success,
|
| 186 |
+
"mode": mode,
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def run_baseline(task: str | None = None) -> Dict:
|
| 191 |
+
client: OpenAI | None = None
|
| 192 |
+
model_name = os.environ.get("MODEL_NAME", DEFAULT_MODEL_NAME)
|
| 193 |
+
load_error: Exception | None = None
|
| 194 |
+
try:
|
| 195 |
+
client, model_name = _load_client()
|
| 196 |
+
except Exception as exc:
|
| 197 |
+
load_error = exc
|
| 198 |
+
|
| 199 |
+
tasks = [task] if task else list(TASK_IDS)
|
| 200 |
+
if task is not None and task not in TASK_REGISTRY:
|
| 201 |
+
tasks = [task]
|
| 202 |
+
start_time = time.time()
|
| 203 |
+
results: list[dict[str, Any]] = []
|
| 204 |
+
for task_id in tasks:
|
| 205 |
+
try:
|
| 206 |
+
results.append(run_episode(task_id, client, model_name))
|
| 207 |
+
except Exception as exc:
|
| 208 |
+
print(f"[START] task={task_id} env=ClipQualityEnv model={model_name}", flush=True)
|
| 209 |
+
print(f"[END] success=false steps=0 score=0.000 rewards=0.00 error={str(exc)}", flush=True)
|
| 210 |
+
results.append(
|
| 211 |
+
{
|
| 212 |
+
"task_id": task_id,
|
| 213 |
+
"reward": 0.0,
|
| 214 |
+
"total_reward": 0.0,
|
| 215 |
+
"final_reward": 0.0,
|
| 216 |
+
"steps": 0,
|
| 217 |
+
"success": False,
|
| 218 |
+
"error": str(exc),
|
| 219 |
+
}
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
overall = sum(float(r.get("reward", 0.0)) for r in results) / len(results) if results else 0.0
|
| 223 |
+
output = {
|
| 224 |
+
"baseline_scores": {"overall_avg": round(overall, 4)},
|
| 225 |
+
"model": model_name,
|
| 226 |
+
"runtime_seconds": round(time.time() - start_time, 2),
|
| 227 |
+
"detail": results,
|
| 228 |
+
}
|
| 229 |
+
if load_error is not None:
|
| 230 |
+
output["warning"] = f"LLM unavailable; used deterministic fallback: {load_error}"
|
| 231 |
+
return output
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def main() -> None:
|
| 235 |
+
parser = argparse.ArgumentParser()
|
| 236 |
+
parser.add_argument("--output", choices=["text", "json"], default="text")
|
| 237 |
+
parser.add_argument("task", nargs="?", default=None)
|
| 238 |
+
args = parser.parse_args()
|
| 239 |
+
|
| 240 |
+
result = run_baseline(task=args.task)
|
| 241 |
+
if args.output == "json":
|
| 242 |
+
print(json.dumps(result))
|
| 243 |
+
else:
|
| 244 |
+
print(json.dumps(result, indent=2))
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
if __name__ == "__main__":
|
| 248 |
+
main()
|
models.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Root model module for OpenEnv CLI compatibility."""
|
| 2 |
+
|
| 3 |
+
from clip_quality_env.models import (
|
| 4 |
+
Action,
|
| 5 |
+
ClipLabel,
|
| 6 |
+
ClipMetadata,
|
| 7 |
+
CorpusIncident,
|
| 8 |
+
EnvironmentState,
|
| 9 |
+
EpisodeHistoryItem,
|
| 10 |
+
HistoryItem,
|
| 11 |
+
Observation,
|
| 12 |
+
Reward,
|
| 13 |
+
State,
|
| 14 |
+
TaskInfo,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
"Action",
|
| 19 |
+
"ClipLabel",
|
| 20 |
+
"ClipMetadata",
|
| 21 |
+
"CorpusIncident",
|
| 22 |
+
"EnvironmentState",
|
| 23 |
+
"EpisodeHistoryItem",
|
| 24 |
+
"HistoryItem",
|
| 25 |
+
"Observation",
|
| 26 |
+
"Reward",
|
| 27 |
+
"State",
|
| 28 |
+
"TaskInfo",
|
| 29 |
+
]
|
openenv.yaml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: clip_quality_env
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
| 7 |
+
description: >
|
| 8 |
+
CLIP Quality Analyzer environment with OpenEnv reference structure for
|
| 9 |
+
clip-case review, quality-rule refinement, and deterministic scoring.
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
- reinforcement-learning
|
| 13 |
+
- clip-quality
|
| 14 |
+
- quality-analysis
|
| 15 |
+
- clip-review
|
| 16 |
+
- hackathon-2026
|
pyproject.toml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=45", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "clip-quality-env"
|
| 7 |
+
version = "1.0.0"
|
| 8 |
+
description = "ClipQualityEnv — OpenEnv RL environment for clip-quality analysis and classification"
|
| 9 |
+
requires-python = ">=3.10"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"openenv-core>=0.2.3",
|
| 12 |
+
"pydantic>=2.0.0",
|
| 13 |
+
"openai>=1.0.0",
|
| 14 |
+
"fastapi>=0.104.0",
|
| 15 |
+
"uvicorn>=0.24.0",
|
| 16 |
+
"requests>=2.25.0",
|
| 17 |
+
"websockets>=12.0",
|
| 18 |
+
"gradio>=4.0.0",
|
| 19 |
+
"pandas>=2.0.0",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
[project.optional-dependencies]
|
| 23 |
+
dev = ["pytest>=7.0.0"]
|
| 24 |
+
|
| 25 |
+
[project.scripts]
|
| 26 |
+
server = "server.app:main"
|
| 27 |
+
|
| 28 |
+
[tool.setuptools]
|
| 29 |
+
include-package-data = true
|
| 30 |
+
packages = ["clip_quality_env", "server"]
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core>=0.2.3
|
| 2 |
+
pydantic>=2.0.0
|
| 3 |
+
openai>=1.0.0
|
| 4 |
+
fastapi>=0.104.0
|
| 5 |
+
uvicorn>=0.24.0
|
| 6 |
+
requests>=2.25.0
|
| 7 |
+
websockets>=12.0
|
| 8 |
+
pytest>=7.0.0
|
| 9 |
+
gradio>=4.0.0
|
| 10 |
+
opencv-python-headless>=4.10.0
|
| 11 |
+
numpy>=1.26.0
|
requirements_extractor.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
opencv-python-headless>=4.10.0
|
| 2 |
+
mediapipe>=0.10.0
|
| 3 |
+
openai-whisper>=20231117
|
| 4 |
+
numpy>=1.26.0
|
scripts/extract_mp4_metadata.py
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
import cv2
|
| 15 |
+
except ImportError as exc:
|
| 16 |
+
cv2 = None
|
| 17 |
+
_CV2_IMPORT_ERROR = exc
|
| 18 |
+
else:
|
| 19 |
+
_CV2_IMPORT_ERROR = None
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
import numpy as np
|
| 23 |
+
except ImportError as exc:
|
| 24 |
+
np = None
|
| 25 |
+
_NUMPY_IMPORT_ERROR = exc
|
| 26 |
+
else:
|
| 27 |
+
_NUMPY_IMPORT_ERROR = None
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
import whisper
|
| 31 |
+
except ImportError as exc:
|
| 32 |
+
whisper = None
|
| 33 |
+
_WHISPER_IMPORT_ERROR = exc
|
| 34 |
+
else:
|
| 35 |
+
_WHISPER_IMPORT_ERROR = None
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
import mediapipe as mp
|
| 39 |
+
except ImportError:
|
| 40 |
+
mp = None
|
| 41 |
+
|
| 42 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 43 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 44 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 45 |
+
|
| 46 |
+
from clip_quality_env.models import ClipMetadata
|
| 47 |
+
from clip_quality_env.real_clips import derive_clip_difficulty
|
| 48 |
+
from clip_quality_env.rubric import RubricState
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
SUPPORTED_SUFFIXES = {".mp4", ".mov", ".mkv", ".webm"}
|
| 52 |
+
_FACE_FALLBACK_WARNED = False
|
| 53 |
+
DEFAULT_ENV_TAG = "unknown_env"
|
| 54 |
+
|
| 55 |
+
ENV_KEYWORDS: dict[str, tuple[str, ...]] = {
|
| 56 |
+
"podcast_studio": ("podcast", "studio", "talkinghead", "talking_head"),
|
| 57 |
+
"office": ("office", "meeting", "work", "corp", "workspace"),
|
| 58 |
+
"home_office": ("home", "bedroom", "livingroom", "living_room"),
|
| 59 |
+
"webcam_room": ("webcam", "zoom", "teams", "meet"),
|
| 60 |
+
"outdoor_interview": ("outdoor", "outside", "park", "field"),
|
| 61 |
+
"crowded_event": ("event", "crowd", "conference", "stage"),
|
| 62 |
+
"car_vlog": ("car", "vehicle", "dashboard"),
|
| 63 |
+
"street_walk": ("street", "walk", "market", "sidewalk"),
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class FaceStats:
|
| 69 |
+
area_ratio: float
|
| 70 |
+
confidence: float
|
| 71 |
+
yaw_deg: float
|
| 72 |
+
pitch_deg: float
|
| 73 |
+
mouth_open_ratio: float
|
| 74 |
+
blink_rate_hz: float
|
| 75 |
+
occlusion_present: bool
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _run_ffprobe(path: Path) -> dict[str, Any]:
|
| 79 |
+
cmd = [
|
| 80 |
+
"ffprobe",
|
| 81 |
+
"-v",
|
| 82 |
+
"error",
|
| 83 |
+
"-select_streams",
|
| 84 |
+
"v:0",
|
| 85 |
+
"-show_entries",
|
| 86 |
+
"stream=width,height,r_frame_rate:format=duration",
|
| 87 |
+
"-of",
|
| 88 |
+
"json",
|
| 89 |
+
str(path),
|
| 90 |
+
]
|
| 91 |
+
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
| 92 |
+
if result.returncode != 0:
|
| 93 |
+
raise RuntimeError(f"ffprobe failed for {path}: {result.stderr.strip()}")
|
| 94 |
+
payload = json.loads(result.stdout)
|
| 95 |
+
streams = payload.get("streams", [])
|
| 96 |
+
if not streams:
|
| 97 |
+
raise RuntimeError(f"No video stream found in {path}")
|
| 98 |
+
stream = streams[0]
|
| 99 |
+
|
| 100 |
+
width = int(stream.get("width", 0))
|
| 101 |
+
height = int(stream.get("height", 0))
|
| 102 |
+
if width <= 0 or height <= 0:
|
| 103 |
+
raise RuntimeError(f"Invalid resolution from ffprobe for {path}")
|
| 104 |
+
|
| 105 |
+
rate = str(stream.get("r_frame_rate", "0/1"))
|
| 106 |
+
num, den = rate.split("/")
|
| 107 |
+
fps = float(num) / max(float(den), 1.0)
|
| 108 |
+
duration_s = float(payload.get("format", {}).get("duration", 0.0))
|
| 109 |
+
if duration_s <= 0.0:
|
| 110 |
+
raise RuntimeError(f"Invalid duration from ffprobe for {path}")
|
| 111 |
+
|
| 112 |
+
return {
|
| 113 |
+
"duration_s": round(duration_s, 3),
|
| 114 |
+
"fps": max(1, int(round(fps))),
|
| 115 |
+
"resolution": f"{width}x{height}",
|
| 116 |
+
"width": width,
|
| 117 |
+
"height": height,
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _sample_frames(path: Path, sample_fps: float) -> tuple[list[np.ndarray], float]:
|
| 122 |
+
cap = cv2.VideoCapture(str(path))
|
| 123 |
+
if not cap.isOpened():
|
| 124 |
+
raise RuntimeError(f"Failed to open video: {path}")
|
| 125 |
+
|
| 126 |
+
source_fps = cap.get(cv2.CAP_PROP_FPS)
|
| 127 |
+
if source_fps <= 0:
|
| 128 |
+
source_fps = 24.0
|
| 129 |
+
stride = max(1, int(round(source_fps / max(sample_fps, 0.1))))
|
| 130 |
+
|
| 131 |
+
frames: list[np.ndarray] = []
|
| 132 |
+
idx = 0
|
| 133 |
+
while True:
|
| 134 |
+
ok, frame = cap.read()
|
| 135 |
+
if not ok:
|
| 136 |
+
break
|
| 137 |
+
if idx % stride == 0:
|
| 138 |
+
frames.append(frame)
|
| 139 |
+
idx += 1
|
| 140 |
+
|
| 141 |
+
cap.release()
|
| 142 |
+
if not frames:
|
| 143 |
+
raise RuntimeError(f"No frames sampled from {path}")
|
| 144 |
+
return frames, source_fps
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _landmark_distance(a: Any, b: Any, width: int, height: int) -> float:
|
| 148 |
+
ax, ay = float(a.x) * width, float(a.y) * height
|
| 149 |
+
bx, by = float(b.x) * width, float(b.y) * height
|
| 150 |
+
return math.hypot(ax - bx, ay - by)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _extract_face_stats_fallback(frames: list[np.ndarray], sample_fps: float) -> FaceStats:
|
| 154 |
+
"""
|
| 155 |
+
Fallback when MediaPipe face mesh API is unavailable in current build.
|
| 156 |
+
Uses image heuristics that keep extraction running instead of dropping all clips.
|
| 157 |
+
"""
|
| 158 |
+
global _FACE_FALLBACK_WARNED
|
| 159 |
+
if not _FACE_FALLBACK_WARNED:
|
| 160 |
+
print("[WARN] MediaPipe Face Mesh unavailable; using heuristic face fallback metrics.")
|
| 161 |
+
_FACE_FALLBACK_WARNED = True
|
| 162 |
+
|
| 163 |
+
h, w = frames[0].shape[:2]
|
| 164 |
+
area_ratio = 0.28
|
| 165 |
+
|
| 166 |
+
gray0 = cv2.cvtColor(frames[0], cv2.COLOR_BGR2GRAY)
|
| 167 |
+
conf_proxy = 1.0 - float(np.std(gray0) / 128.0)
|
| 168 |
+
confidence = float(np.clip(conf_proxy, 0.55, 0.90))
|
| 169 |
+
|
| 170 |
+
# Head pose proxies from horizontal/vertical gradient imbalance.
|
| 171 |
+
grad_x = np.mean(np.abs(np.diff(gray0.astype(np.float32), axis=1)))
|
| 172 |
+
grad_y = np.mean(np.abs(np.diff(gray0.astype(np.float32), axis=0)))
|
| 173 |
+
yaw_deg = float(np.clip((grad_x / max(grad_y, 1e-6)) * 8.0, 0.0, 35.0))
|
| 174 |
+
pitch_deg = float(np.clip((grad_y / max(grad_x, 1e-6)) * 6.0, 0.0, 25.0))
|
| 175 |
+
|
| 176 |
+
# Mouth-open proxy from lower-center variance.
|
| 177 |
+
y0, y1 = int(0.55 * h), int(0.82 * h)
|
| 178 |
+
x0, x1 = int(0.33 * w), int(0.67 * w)
|
| 179 |
+
roi = gray0[y0:y1, x0:x1]
|
| 180 |
+
mouth_open_ratio = float(np.clip(np.std(roi) / 96.0, 0.18, 0.62))
|
| 181 |
+
|
| 182 |
+
# Blink proxy unavailable -> conservative nominal rate.
|
| 183 |
+
blink_rate_hz = 0.25
|
| 184 |
+
|
| 185 |
+
# Occlusion proxy via darkness saturation ratio in center region.
|
| 186 |
+
center = gray0[int(0.2 * h) : int(0.85 * h), int(0.2 * w) : int(0.8 * w)]
|
| 187 |
+
dark_ratio = float(np.mean(center < 18))
|
| 188 |
+
occlusion_present = dark_ratio > 0.35
|
| 189 |
+
|
| 190 |
+
return FaceStats(
|
| 191 |
+
area_ratio=area_ratio,
|
| 192 |
+
confidence=confidence,
|
| 193 |
+
yaw_deg=yaw_deg,
|
| 194 |
+
pitch_deg=pitch_deg,
|
| 195 |
+
mouth_open_ratio=mouth_open_ratio,
|
| 196 |
+
blink_rate_hz=blink_rate_hz,
|
| 197 |
+
occlusion_present=occlusion_present,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _extract_face_stats(frames: list[np.ndarray], sample_fps: float) -> FaceStats:
|
| 202 |
+
if mp is None or not hasattr(mp, "solutions") or not hasattr(mp.solutions, "face_mesh"):
|
| 203 |
+
return _extract_face_stats_fallback(frames, sample_fps)
|
| 204 |
+
|
| 205 |
+
mp_mesh = mp.solutions.face_mesh
|
| 206 |
+
face_mesh = mp_mesh.FaceMesh(
|
| 207 |
+
static_image_mode=True,
|
| 208 |
+
max_num_faces=1,
|
| 209 |
+
refine_landmarks=True,
|
| 210 |
+
min_detection_confidence=0.5,
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
area_ratios: list[float] = []
|
| 214 |
+
confidences: list[float] = []
|
| 215 |
+
yaws: list[float] = []
|
| 216 |
+
pitches: list[float] = []
|
| 217 |
+
mouth_open: list[float] = []
|
| 218 |
+
blink_closures: list[float] = []
|
| 219 |
+
missing_face = 0
|
| 220 |
+
|
| 221 |
+
# Mouth/eye landmarks from MediaPipe Face Mesh topology.
|
| 222 |
+
mouth_top, mouth_bottom = 13, 14
|
| 223 |
+
mouth_left, mouth_right = 78, 308
|
| 224 |
+
l_eye_top, l_eye_bottom = 159, 145
|
| 225 |
+
l_eye_left, l_eye_right = 33, 133
|
| 226 |
+
r_eye_top, r_eye_bottom = 386, 374
|
| 227 |
+
r_eye_left, r_eye_right = 362, 263
|
| 228 |
+
nose_tip, left_cheek, right_cheek = 1, 234, 454
|
| 229 |
+
forehead, chin = 10, 152
|
| 230 |
+
|
| 231 |
+
for frame in frames:
|
| 232 |
+
h, w = frame.shape[:2]
|
| 233 |
+
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 234 |
+
result = face_mesh.process(rgb)
|
| 235 |
+
if not result.multi_face_landmarks:
|
| 236 |
+
missing_face += 1
|
| 237 |
+
continue
|
| 238 |
+
|
| 239 |
+
lm = result.multi_face_landmarks[0].landmark
|
| 240 |
+
|
| 241 |
+
xs = np.array([p.x for p in lm], dtype=np.float32)
|
| 242 |
+
ys = np.array([p.y for p in lm], dtype=np.float32)
|
| 243 |
+
x0 = float(np.clip(xs.min(), 0.0, 1.0))
|
| 244 |
+
x1 = float(np.clip(xs.max(), 0.0, 1.0))
|
| 245 |
+
y0 = float(np.clip(ys.min(), 0.0, 1.0))
|
| 246 |
+
y1 = float(np.clip(ys.max(), 0.0, 1.0))
|
| 247 |
+
area_ratio = max(0.0, (x1 - x0) * (y1 - y0))
|
| 248 |
+
area_ratios.append(area_ratio)
|
| 249 |
+
|
| 250 |
+
# MediaPipe FaceMesh does not expose direct confidence per face; use coverage proxy.
|
| 251 |
+
conf = 1.0 - float(np.mean((xs <= 0.0) | (xs >= 1.0) | (ys <= 0.0) | (ys >= 1.0)))
|
| 252 |
+
confidences.append(float(np.clip(conf, 0.0, 1.0)))
|
| 253 |
+
|
| 254 |
+
nose = lm[nose_tip]
|
| 255 |
+
lch = lm[left_cheek]
|
| 256 |
+
rch = lm[right_cheek]
|
| 257 |
+
fh = lm[forehead]
|
| 258 |
+
ch = lm[chin]
|
| 259 |
+
yaw = abs((nose.x - (lch.x + rch.x) / 2.0) * 120.0)
|
| 260 |
+
pitch = abs((nose.y - (fh.y + ch.y) / 2.0) * 120.0)
|
| 261 |
+
yaws.append(float(yaw))
|
| 262 |
+
pitches.append(float(pitch))
|
| 263 |
+
|
| 264 |
+
mouth_h = _landmark_distance(lm[mouth_top], lm[mouth_bottom], w, h)
|
| 265 |
+
mouth_w = max(_landmark_distance(lm[mouth_left], lm[mouth_right], w, h), 1e-6)
|
| 266 |
+
mouth_open.append(float(np.clip(mouth_h / mouth_w, 0.0, 1.0)))
|
| 267 |
+
|
| 268 |
+
l_h = _landmark_distance(lm[l_eye_top], lm[l_eye_bottom], w, h)
|
| 269 |
+
l_w = max(_landmark_distance(lm[l_eye_left], lm[l_eye_right], w, h), 1e-6)
|
| 270 |
+
r_h = _landmark_distance(lm[r_eye_top], lm[r_eye_bottom], w, h)
|
| 271 |
+
r_w = max(_landmark_distance(lm[r_eye_left], lm[r_eye_right], w, h), 1e-6)
|
| 272 |
+
eye_open_ratio = float(np.clip(0.5 * (l_h / l_w + r_h / r_w), 0.0, 1.0))
|
| 273 |
+
blink_closures.append(eye_open_ratio)
|
| 274 |
+
|
| 275 |
+
face_mesh.close()
|
| 276 |
+
|
| 277 |
+
if not area_ratios:
|
| 278 |
+
raise RuntimeError("No detectable face landmarks in sampled frames")
|
| 279 |
+
|
| 280 |
+
# Blink estimate: count closures under threshold in sampled stream, then normalize by duration.
|
| 281 |
+
blink_events = 0
|
| 282 |
+
prev_closed = False
|
| 283 |
+
for r in blink_closures:
|
| 284 |
+
closed = r < 0.18
|
| 285 |
+
if closed and not prev_closed:
|
| 286 |
+
blink_events += 1
|
| 287 |
+
prev_closed = closed
|
| 288 |
+
sampled_seconds = len(frames) / max(sample_fps, 1e-6)
|
| 289 |
+
blink_rate_hz = blink_events / max(sampled_seconds, 1e-6)
|
| 290 |
+
|
| 291 |
+
occlusion_ratio = missing_face / len(frames)
|
| 292 |
+
occlusion_present = occlusion_ratio > 0.30
|
| 293 |
+
|
| 294 |
+
return FaceStats(
|
| 295 |
+
area_ratio=float(np.clip(np.mean(area_ratios), 0.0, 1.0)),
|
| 296 |
+
confidence=float(np.clip(np.mean(confidences), 0.0, 1.0)),
|
| 297 |
+
yaw_deg=float(np.clip(np.mean(yaws), 0.0, 180.0)),
|
| 298 |
+
pitch_deg=float(np.clip(np.mean(pitches), 0.0, 180.0)),
|
| 299 |
+
mouth_open_ratio=float(np.clip(np.mean(mouth_open), 0.0, 1.0)),
|
| 300 |
+
blink_rate_hz=max(0.0, float(blink_rate_hz)),
|
| 301 |
+
occlusion_present=occlusion_present,
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def _motion_score(frames: list[np.ndarray]) -> float:
|
| 306 |
+
if len(frames) < 2:
|
| 307 |
+
return 0.0
|
| 308 |
+
scores = []
|
| 309 |
+
prev = cv2.cvtColor(frames[0], cv2.COLOR_BGR2GRAY)
|
| 310 |
+
for frame in frames[1:]:
|
| 311 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
| 312 |
+
diff = cv2.absdiff(prev, gray)
|
| 313 |
+
scores.append(float(np.mean(diff) / 255.0))
|
| 314 |
+
prev = gray
|
| 315 |
+
return float(np.clip(np.mean(scores), 0.0, 1.0))
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def _bg_complexity(frames: list[np.ndarray]) -> tuple[str, float]:
|
| 319 |
+
# Hybrid complexity proxy: edge density + intensity variation + local texture energy.
|
| 320 |
+
edge_densities = []
|
| 321 |
+
texture_energies = []
|
| 322 |
+
intensity_vars = []
|
| 323 |
+
for frame in frames:
|
| 324 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
| 325 |
+
|
| 326 |
+
edges = cv2.Canny(gray, 80, 160)
|
| 327 |
+
edge_density = float(np.mean(edges > 0))
|
| 328 |
+
edge_densities.append(edge_density)
|
| 329 |
+
|
| 330 |
+
lap = cv2.Laplacian(gray, cv2.CV_32F)
|
| 331 |
+
texture_energy = float(np.mean(np.abs(lap)) / 255.0)
|
| 332 |
+
texture_energies.append(texture_energy)
|
| 333 |
+
|
| 334 |
+
intensity_vars.append(float(np.std(gray.astype(np.float32)) / 128.0))
|
| 335 |
+
|
| 336 |
+
edge_score = float(np.clip(np.mean(edge_densities), 0.0, 1.0))
|
| 337 |
+
texture_score = float(np.clip(np.mean(texture_energies), 0.0, 1.0))
|
| 338 |
+
variation_score = float(np.clip(np.mean(intensity_vars), 0.0, 1.0))
|
| 339 |
+
score = float(np.clip(0.45 * edge_score + 0.30 * texture_score + 0.25 * variation_score, 0.0, 1.0))
|
| 340 |
+
|
| 341 |
+
if score < 0.07:
|
| 342 |
+
tag = "solid_dark"
|
| 343 |
+
elif score < 0.10:
|
| 344 |
+
tag = "simple_room"
|
| 345 |
+
elif score < 0.15:
|
| 346 |
+
tag = "busy_room"
|
| 347 |
+
else:
|
| 348 |
+
tag = "crowded_event"
|
| 349 |
+
return tag, score
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def _lighting_uniformity(frames: list[np.ndarray]) -> float:
|
| 353 |
+
vals = []
|
| 354 |
+
for frame in frames:
|
| 355 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
|
| 356 |
+
std = float(np.std(gray))
|
| 357 |
+
vals.append(float(np.clip(1.0 - 2.0 * std, 0.0, 1.0)))
|
| 358 |
+
return float(np.clip(np.mean(vals), 0.0, 1.0))
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def _extract_audio_track(video_path: Path, wav_path: Path) -> None:
|
| 362 |
+
cmd = [
|
| 363 |
+
"ffmpeg",
|
| 364 |
+
"-y",
|
| 365 |
+
"-i",
|
| 366 |
+
str(video_path),
|
| 367 |
+
"-ac",
|
| 368 |
+
"1",
|
| 369 |
+
"-ar",
|
| 370 |
+
"16000",
|
| 371 |
+
str(wav_path),
|
| 372 |
+
]
|
| 373 |
+
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
| 374 |
+
if result.returncode != 0:
|
| 375 |
+
raise RuntimeError(f"ffmpeg audio extraction failed for {video_path}: {result.stderr.strip()}")
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def _audio_snr_db(wav_path: Path) -> float:
|
| 379 |
+
# Approximate SNR using RMS percentile split.
|
| 380 |
+
cmd = [
|
| 381 |
+
"ffprobe",
|
| 382 |
+
"-v",
|
| 383 |
+
"error",
|
| 384 |
+
"-f",
|
| 385 |
+
"lavfi",
|
| 386 |
+
"-i",
|
| 387 |
+
f"amovie={wav_path},astats=metadata=1:reset=1",
|
| 388 |
+
"-show_entries",
|
| 389 |
+
"frame_tags=lavfi.astats.Overall.RMS_level",
|
| 390 |
+
"-of",
|
| 391 |
+
"json",
|
| 392 |
+
]
|
| 393 |
+
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
| 394 |
+
if result.returncode != 0:
|
| 395 |
+
return 0.0
|
| 396 |
+
payload = json.loads(result.stdout or "{}")
|
| 397 |
+
frames = payload.get("frames", [])
|
| 398 |
+
levels = []
|
| 399 |
+
for frame in frames:
|
| 400 |
+
tags = frame.get("tags", {})
|
| 401 |
+
val = tags.get("lavfi.astats.Overall.RMS_level")
|
| 402 |
+
if val is None:
|
| 403 |
+
continue
|
| 404 |
+
try:
|
| 405 |
+
levels.append(float(val))
|
| 406 |
+
except ValueError:
|
| 407 |
+
continue
|
| 408 |
+
if len(levels) < 10:
|
| 409 |
+
return 0.0
|
| 410 |
+
arr = np.array(levels, dtype=np.float32)
|
| 411 |
+
speech = np.percentile(arr, 85)
|
| 412 |
+
noise = np.percentile(arr, 20)
|
| 413 |
+
return float(np.clip(speech - noise, 0.0, 80.0))
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _transcribe(wav_path: Path, model: Any) -> tuple[int, float]:
|
| 417 |
+
result = model.transcribe(str(wav_path), language="en", fp16=False, verbose=False)
|
| 418 |
+
text = str(result.get("text", "")).strip()
|
| 419 |
+
segments = result.get("segments", []) or []
|
| 420 |
+
# Whisper does not provide true confidence; use avg_logprob as bounded proxy.
|
| 421 |
+
confs = []
|
| 422 |
+
for seg in segments:
|
| 423 |
+
avg_logprob = seg.get("avg_logprob")
|
| 424 |
+
if avg_logprob is None:
|
| 425 |
+
continue
|
| 426 |
+
confs.append(float(1.0 / (1.0 + math.exp(-avg_logprob))))
|
| 427 |
+
confidence = float(np.mean(confs)) if confs else 0.0
|
| 428 |
+
return len(text.split()), float(np.clip(confidence, 0.0, 1.0))
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def _load_environment_map(path: str | None) -> dict[str, str]:
|
| 432 |
+
if not path:
|
| 433 |
+
return {}
|
| 434 |
+
payload = json.loads(Path(path).read_text(encoding="utf-8"))
|
| 435 |
+
mapping: dict[str, str] = {}
|
| 436 |
+
if isinstance(payload, dict):
|
| 437 |
+
for key, value in payload.items():
|
| 438 |
+
k = str(key).strip().lower().replace("\\", "/")
|
| 439 |
+
v = str(value).strip()
|
| 440 |
+
if not k or not v:
|
| 441 |
+
continue
|
| 442 |
+
mapping[k] = v
|
| 443 |
+
return mapping
|
| 444 |
+
if isinstance(payload, list):
|
| 445 |
+
for item in payload:
|
| 446 |
+
if not isinstance(item, dict):
|
| 447 |
+
continue
|
| 448 |
+
key = item.get("clip_id") or item.get("path") or item.get("name")
|
| 449 |
+
value = item.get("environment_tag")
|
| 450 |
+
if key is None or value is None:
|
| 451 |
+
continue
|
| 452 |
+
k = str(key).strip().lower().replace("\\", "/")
|
| 453 |
+
v = str(value).strip()
|
| 454 |
+
if not k or not v:
|
| 455 |
+
continue
|
| 456 |
+
mapping[k] = v
|
| 457 |
+
return mapping
|
| 458 |
+
raise ValueError("environment map must be JSON object or list of {clip_id/path,name,environment_tag}")
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _environment_tag_from_features(
|
| 462 |
+
bg_complexity_score: float,
|
| 463 |
+
motion_score: float,
|
| 464 |
+
lighting_uniformity: float,
|
| 465 |
+
audio_snr_db: float,
|
| 466 |
+
) -> str:
|
| 467 |
+
activity = 0.60 * bg_complexity_score + 0.30 * motion_score + 0.10 * (1.0 - lighting_uniformity)
|
| 468 |
+
if motion_score >= 0.075 or (activity >= 0.14 and bg_complexity_score >= 0.13):
|
| 469 |
+
return "street_walk"
|
| 470 |
+
if bg_complexity_score >= 0.18 or activity >= 0.18:
|
| 471 |
+
return "crowded_event"
|
| 472 |
+
if audio_snr_db < 14.5 and motion_score >= 0.035:
|
| 473 |
+
return "car_vlog"
|
| 474 |
+
if lighting_uniformity >= 0.76 and bg_complexity_score <= 0.09 and motion_score <= 0.03 and audio_snr_db >= 16.0:
|
| 475 |
+
return "podcast_studio"
|
| 476 |
+
if bg_complexity_score <= 0.12 and lighting_uniformity >= 0.67:
|
| 477 |
+
return "office"
|
| 478 |
+
if lighting_uniformity < 0.62:
|
| 479 |
+
return "webcam_room"
|
| 480 |
+
return "home_office"
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def _environment_tag(
|
| 484 |
+
path_hint: Path,
|
| 485 |
+
clip_id: str,
|
| 486 |
+
bg_complexity_score: float,
|
| 487 |
+
motion_score: float,
|
| 488 |
+
lighting_uniformity: float,
|
| 489 |
+
audio_snr_db: float,
|
| 490 |
+
environment_map: dict[str, str] | None = None,
|
| 491 |
+
) -> str:
|
| 492 |
+
mapping = environment_map or {}
|
| 493 |
+
norm_path = str(path_hint).strip().lower().replace("\\", "/")
|
| 494 |
+
base_name = Path(norm_path).name
|
| 495 |
+
base_stem = Path(norm_path).stem
|
| 496 |
+
clip_key = clip_id.strip().lower()
|
| 497 |
+
|
| 498 |
+
for key in (norm_path, base_name, base_stem, clip_key):
|
| 499 |
+
if key in mapping:
|
| 500 |
+
return mapping[key]
|
| 501 |
+
|
| 502 |
+
# Use only user-intended path hint (relative clip path), not absolute system path.
|
| 503 |
+
for tag, keys in ENV_KEYWORDS.items():
|
| 504 |
+
if any(k in norm_path for k in keys):
|
| 505 |
+
return tag
|
| 506 |
+
|
| 507 |
+
return _environment_tag_from_features(
|
| 508 |
+
bg_complexity_score=bg_complexity_score,
|
| 509 |
+
motion_score=motion_score,
|
| 510 |
+
lighting_uniformity=lighting_uniformity,
|
| 511 |
+
audio_snr_db=audio_snr_db,
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def _framing_from_path_or_pose(path_hint: Path, yaw_deg: float) -> str:
|
| 516 |
+
name = str(path_hint).lower()
|
| 517 |
+
if "closeup" in name or "close_up" in name:
|
| 518 |
+
return "closeup"
|
| 519 |
+
if "offgaze" in name or "off_gaze" in name:
|
| 520 |
+
return "offgaze"
|
| 521 |
+
if "left" in name:
|
| 522 |
+
return "left"
|
| 523 |
+
if "right" in name:
|
| 524 |
+
return "right"
|
| 525 |
+
if yaw_deg <= 6.0:
|
| 526 |
+
return "front"
|
| 527 |
+
if yaw_deg <= 11.0:
|
| 528 |
+
return "offgaze"
|
| 529 |
+
return "left" if ("left" in name) else "right"
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def _extract_clip_metadata(
|
| 533 |
+
video_path: Path,
|
| 534 |
+
path_hint: Path,
|
| 535 |
+
sample_fps: float,
|
| 536 |
+
asr_model: Any,
|
| 537 |
+
rubric: RubricState,
|
| 538 |
+
with_difficulty: bool,
|
| 539 |
+
environment_tag_override: str | None = None,
|
| 540 |
+
environment_map: dict[str, str] | None = None,
|
| 541 |
+
framing_override: str | None = None,
|
| 542 |
+
) -> dict[str, Any]:
|
| 543 |
+
probe = _run_ffprobe(video_path)
|
| 544 |
+
frames, _source_fps = _sample_frames(video_path, sample_fps=sample_fps)
|
| 545 |
+
face = _extract_face_stats(frames, sample_fps=sample_fps)
|
| 546 |
+
motion = _motion_score(frames)
|
| 547 |
+
bg_tag, bg_score = _bg_complexity(frames)
|
| 548 |
+
light = _lighting_uniformity(frames)
|
| 549 |
+
|
| 550 |
+
wav_path = video_path.with_suffix(".tmp16k.wav")
|
| 551 |
+
_extract_audio_track(video_path, wav_path)
|
| 552 |
+
try:
|
| 553 |
+
snr = _audio_snr_db(wav_path)
|
| 554 |
+
word_count, transcript_conf = _transcribe(wav_path, asr_model)
|
| 555 |
+
finally:
|
| 556 |
+
if wav_path.exists():
|
| 557 |
+
wav_path.unlink()
|
| 558 |
+
|
| 559 |
+
row: dict[str, Any] = {
|
| 560 |
+
"clip_id": video_path.stem,
|
| 561 |
+
"duration_s": probe["duration_s"],
|
| 562 |
+
"fps": probe["fps"],
|
| 563 |
+
"resolution": probe["resolution"],
|
| 564 |
+
"face_area_ratio": round(face.area_ratio, 4),
|
| 565 |
+
"face_confidence": round(face.confidence, 4),
|
| 566 |
+
"head_pose_yaw_deg": round(face.yaw_deg, 3),
|
| 567 |
+
"head_pose_pitch_deg": round(face.pitch_deg, 3),
|
| 568 |
+
"motion_score": round(motion, 4),
|
| 569 |
+
"bg_complexity": bg_tag,
|
| 570 |
+
"bg_complexity_score": round(bg_score, 4),
|
| 571 |
+
"mouth_open_ratio": round(face.mouth_open_ratio, 4),
|
| 572 |
+
"blink_rate_hz": round(face.blink_rate_hz, 4),
|
| 573 |
+
"audio_snr_db": round(float(snr), 3),
|
| 574 |
+
"transcript_word_count": int(word_count),
|
| 575 |
+
"transcript_confidence": round(float(transcript_conf), 4),
|
| 576 |
+
"lighting_uniformity": round(light, 4),
|
| 577 |
+
"occlusion_present": bool(face.occlusion_present),
|
| 578 |
+
"environment_tag": environment_tag_override
|
| 579 |
+
or _environment_tag(
|
| 580 |
+
path_hint=path_hint,
|
| 581 |
+
clip_id=video_path.stem,
|
| 582 |
+
bg_complexity_score=bg_score,
|
| 583 |
+
motion_score=motion,
|
| 584 |
+
lighting_uniformity=light,
|
| 585 |
+
audio_snr_db=float(snr),
|
| 586 |
+
environment_map=environment_map,
|
| 587 |
+
),
|
| 588 |
+
"framing": framing_override or _framing_from_path_or_pose(path_hint=path_hint, yaw_deg=face.yaw_deg),
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
clip = ClipMetadata(**row)
|
| 592 |
+
payload = clip.model_dump()
|
| 593 |
+
if with_difficulty:
|
| 594 |
+
payload["difficulty"] = derive_clip_difficulty(payload, rubric)
|
| 595 |
+
return payload
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def _iter_videos(input_dir: Path) -> list[Path]:
|
| 599 |
+
files = [p for p in input_dir.rglob("*") if p.is_file() and p.suffix.lower() in SUPPORTED_SUFFIXES]
|
| 600 |
+
files.sort()
|
| 601 |
+
return files
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
def main() -> None:
|
| 605 |
+
parser = argparse.ArgumentParser(description="Extract ClipMetadata manifest from MP4 files.")
|
| 606 |
+
parser.add_argument("--input-dir", type=str, required=True, help="Directory containing video clips.")
|
| 607 |
+
parser.add_argument("--output", type=str, required=True, help="Output manifest path (.jsonl).")
|
| 608 |
+
parser.add_argument("--sample-fps", type=float, default=2.0, help="Frame sampling FPS for visual features.")
|
| 609 |
+
parser.add_argument("--max-clips", type=int, default=0, help="Optional cap; 0 means all clips.")
|
| 610 |
+
parser.add_argument(
|
| 611 |
+
"--difficulty-mode",
|
| 612 |
+
type=str,
|
| 613 |
+
choices=["none", "derive"],
|
| 614 |
+
default="derive",
|
| 615 |
+
help="Attach derived difficulty labels or not.",
|
| 616 |
+
)
|
| 617 |
+
parser.add_argument(
|
| 618 |
+
"--whisper-model",
|
| 619 |
+
type=str,
|
| 620 |
+
default="small",
|
| 621 |
+
help="Whisper model size (tiny, base, small, medium, large).",
|
| 622 |
+
)
|
| 623 |
+
parser.add_argument(
|
| 624 |
+
"--environment-tag",
|
| 625 |
+
type=str,
|
| 626 |
+
default=None,
|
| 627 |
+
help="Optional fixed environment_tag override for all clips in this run.",
|
| 628 |
+
)
|
| 629 |
+
parser.add_argument(
|
| 630 |
+
"--framing",
|
| 631 |
+
type=str,
|
| 632 |
+
default=None,
|
| 633 |
+
help="Optional fixed framing override for all clips in this run.",
|
| 634 |
+
)
|
| 635 |
+
parser.add_argument(
|
| 636 |
+
"--environment-map",
|
| 637 |
+
type=str,
|
| 638 |
+
default=None,
|
| 639 |
+
help="Optional JSON map/list to assign environment_tag by clip_id/path/name.",
|
| 640 |
+
)
|
| 641 |
+
args = parser.parse_args()
|
| 642 |
+
|
| 643 |
+
dep_errors = [err for err in (_CV2_IMPORT_ERROR, _NUMPY_IMPORT_ERROR, _WHISPER_IMPORT_ERROR) if err is not None]
|
| 644 |
+
if dep_errors:
|
| 645 |
+
raise RuntimeError(
|
| 646 |
+
"Missing extractor dependencies. Install: "
|
| 647 |
+
"opencv-python-headless mediapipe openai-whisper numpy"
|
| 648 |
+
) from dep_errors[0]
|
| 649 |
+
|
| 650 |
+
input_dir = Path(args.input_dir).resolve()
|
| 651 |
+
output_path = Path(args.output).resolve()
|
| 652 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 653 |
+
|
| 654 |
+
if not input_dir.exists():
|
| 655 |
+
raise FileNotFoundError(f"Input dir not found: {input_dir}")
|
| 656 |
+
|
| 657 |
+
rubric = RubricState()
|
| 658 |
+
asr_model = whisper.load_model(args.whisper_model)
|
| 659 |
+
environment_map = _load_environment_map(args.environment_map)
|
| 660 |
+
|
| 661 |
+
videos = _iter_videos(input_dir)
|
| 662 |
+
if args.max_clips > 0:
|
| 663 |
+
videos = videos[: args.max_clips]
|
| 664 |
+
if not videos:
|
| 665 |
+
raise RuntimeError(f"No supported video files found in {input_dir}")
|
| 666 |
+
|
| 667 |
+
failures: list[tuple[str, str]] = []
|
| 668 |
+
with open(output_path, "w", encoding="utf-8") as out:
|
| 669 |
+
for video_path in videos:
|
| 670 |
+
try:
|
| 671 |
+
row = _extract_clip_metadata(
|
| 672 |
+
video_path=video_path,
|
| 673 |
+
path_hint=video_path.relative_to(input_dir),
|
| 674 |
+
sample_fps=args.sample_fps,
|
| 675 |
+
asr_model=asr_model,
|
| 676 |
+
rubric=rubric,
|
| 677 |
+
with_difficulty=(args.difficulty_mode == "derive"),
|
| 678 |
+
environment_tag_override=args.environment_tag,
|
| 679 |
+
environment_map=environment_map,
|
| 680 |
+
framing_override=args.framing,
|
| 681 |
+
)
|
| 682 |
+
except Exception as exc:
|
| 683 |
+
failures.append((str(video_path), str(exc)))
|
| 684 |
+
continue
|
| 685 |
+
out.write(json.dumps(row, ensure_ascii=True) + "\n")
|
| 686 |
+
|
| 687 |
+
with open(output_path, "r", encoding="utf-8") as f:
|
| 688 |
+
written = sum(1 for _ in f)
|
| 689 |
+
print(json.dumps({"output": str(output_path), "written_rows": written, "failed_clips": len(failures)}, indent=2))
|
| 690 |
+
if failures:
|
| 691 |
+
for clip, err in failures:
|
| 692 |
+
print(f"[FAIL] {clip}: {err}")
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
if __name__ == "__main__":
|
| 696 |
+
main()
|
server/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Clip-quality server package."""
|
server/app.py
ADDED
|
@@ -0,0 +1,963 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import threading
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import uvicorn
|
| 12 |
+
from fastapi import BackgroundTasks, HTTPException
|
| 13 |
+
from openenv.core.env_server import create_fastapi_app
|
| 14 |
+
|
| 15 |
+
import inference
|
| 16 |
+
from models import Action, Observation, TaskInfo
|
| 17 |
+
from server.baseline_runs import baseline_run_tracker
|
| 18 |
+
from server.environment import ClipQualityEnvironment
|
| 19 |
+
from server.grader import grade
|
| 20 |
+
from server.tasks import TASK_REGISTRY
|
| 21 |
+
|
| 22 |
+
PRODUCT_NAME = "CLIP Quality Analyzer"
|
| 23 |
+
ENVIRONMENT_ID = "clip_quality_env"
|
| 24 |
+
OVERRIDDEN_ROUTES = {"/health", "/state", "/tasks", "/grader", "/baseline"}
|
| 25 |
+
TASK_SURFACE_DESCRIPTIONS = {
|
| 26 |
+
"task_easy": "Classify clips with clear quality signals and concise metadata-based reasoning.",
|
| 27 |
+
"task_medium": "Classify borderline clips by balancing mixed quality indicators.",
|
| 28 |
+
"task_hard": "Classify difficult clips with conflicting signals and explicit trade-off reasoning.",
|
| 29 |
+
}
|
| 30 |
+
TASK_INPUT_TAB_MAP = {
|
| 31 |
+
"task_easy": "easy",
|
| 32 |
+
"task_medium": "medium",
|
| 33 |
+
"task_hard": "hard",
|
| 34 |
+
}
|
| 35 |
+
CLASS_LABEL_CHOICES = [
|
| 36 |
+
("KEEP", "KEEP"),
|
| 37 |
+
("BORDERLINE", "BORDERLINE"),
|
| 38 |
+
("REJECT", "REJECT"),
|
| 39 |
+
]
|
| 40 |
+
BASELINE_RUN_BUTTON_LABEL = "Run LLM Baseline Agent"
|
| 41 |
+
BASELINE_RUN_BUTTON_RUNNING_LABEL = "Running LLM Baseline Agent..."
|
| 42 |
+
QUALITY_HINT_BUTTON_LABEL = "Load Quality Hint"
|
| 43 |
+
BASELINE_RESULT_PLACEHOLDER = (
|
| 44 |
+
"Run the baseline agent to view model, mode, reward achieved per step, and success status."
|
| 45 |
+
)
|
| 46 |
+
HF_TOKEN_MISSING_WARNING = "LLM unavailable — no HF_TOKEN configured. Showing deterministic fallback result."
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _normalized_path(path: str) -> str:
|
| 50 |
+
return path.rstrip("/") or "/"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _surface_task_description(task_id: str, fallback: str) -> str:
|
| 54 |
+
return TASK_SURFACE_DESCRIPTIONS.get(task_id, fallback)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _input_tab_for_task(task_id: str) -> str:
|
| 58 |
+
return TASK_INPUT_TAB_MAP.get(task_id, "easy")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _clean_reasoning_text(value: str | None) -> str:
|
| 62 |
+
return str(value or "").strip()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _build_reasoning_from_sections(sections: list[tuple[str, str]]) -> str:
|
| 66 |
+
lines: list[str] = []
|
| 67 |
+
for title, value in sections:
|
| 68 |
+
cleaned = _clean_reasoning_text(value)
|
| 69 |
+
if cleaned:
|
| 70 |
+
lines.append(f"{title}: {cleaned}")
|
| 71 |
+
return "\n".join(lines) if lines else "No reasoning provided."
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _resolve_tiered_submission(
|
| 75 |
+
task_id: str,
|
| 76 |
+
selected_input_tab: str | None,
|
| 77 |
+
easy_label: str,
|
| 78 |
+
easy_observation: str,
|
| 79 |
+
medium_label: str,
|
| 80 |
+
medium_primary_signal: str,
|
| 81 |
+
medium_conflicting_signal: str,
|
| 82 |
+
medium_reasoning: str,
|
| 83 |
+
hard_label: str,
|
| 84 |
+
hard_tradeoff_summary: str,
|
| 85 |
+
hard_confidence_justification: str,
|
| 86 |
+
hard_confidence: float,
|
| 87 |
+
) -> tuple[str, str, float]:
|
| 88 |
+
active_tab = selected_input_tab if selected_input_tab in {"easy", "medium", "hard"} else _input_tab_for_task(task_id)
|
| 89 |
+
|
| 90 |
+
if active_tab == "easy":
|
| 91 |
+
label = str(easy_label or "BORDERLINE").strip().upper()
|
| 92 |
+
reasoning = _build_reasoning_from_sections(
|
| 93 |
+
[
|
| 94 |
+
("Tier", "Easy"),
|
| 95 |
+
("Predicted Label", label),
|
| 96 |
+
("Key Observation", easy_observation),
|
| 97 |
+
]
|
| 98 |
+
)
|
| 99 |
+
confidence = 0.5
|
| 100 |
+
elif active_tab == "medium":
|
| 101 |
+
label = str(medium_label or "BORDERLINE").strip().upper()
|
| 102 |
+
reasoning = _build_reasoning_from_sections(
|
| 103 |
+
[
|
| 104 |
+
("Tier", "Medium"),
|
| 105 |
+
("Predicted Label", label),
|
| 106 |
+
("Primary Signal", medium_primary_signal),
|
| 107 |
+
("Conflicting Signal", medium_conflicting_signal),
|
| 108 |
+
("Reasoning", medium_reasoning),
|
| 109 |
+
]
|
| 110 |
+
)
|
| 111 |
+
confidence = 0.5
|
| 112 |
+
else:
|
| 113 |
+
label = str(hard_label or "BORDERLINE").strip().upper()
|
| 114 |
+
confidence = float(hard_confidence)
|
| 115 |
+
reasoning = _build_reasoning_from_sections(
|
| 116 |
+
[
|
| 117 |
+
("Tier", "Hard"),
|
| 118 |
+
("Predicted Label", label),
|
| 119 |
+
("Trade-off Summary", hard_tradeoff_summary),
|
| 120 |
+
("Confidence Justification", hard_confidence_justification),
|
| 121 |
+
("Confidence", f"{confidence:.2f}"),
|
| 122 |
+
]
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
if label not in {"KEEP", "BORDERLINE", "REJECT"}:
|
| 126 |
+
label = "BORDERLINE"
|
| 127 |
+
return label, reasoning, confidence
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _input_tab_update_for_task(task_id: str) -> dict[str, Any]:
|
| 131 |
+
return gr.update(selected=_input_tab_for_task(task_id))
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
app = create_fastapi_app(
|
| 135 |
+
env=ClipQualityEnvironment,
|
| 136 |
+
action_cls=Action,
|
| 137 |
+
observation_cls=Observation,
|
| 138 |
+
)
|
| 139 |
+
# Replace selected OpenEnv defaults with reference-style handlers.
|
| 140 |
+
overridden_paths = {_normalized_path(path) for path in OVERRIDDEN_ROUTES}
|
| 141 |
+
app.router.routes = [
|
| 142 |
+
route
|
| 143 |
+
for route in app.router.routes
|
| 144 |
+
if _normalized_path(getattr(route, "path", "")) not in overridden_paths
|
| 145 |
+
]
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@app.get("/")
|
| 149 |
+
def root() -> dict[str, str]:
|
| 150 |
+
return {
|
| 151 |
+
"status": "ok",
|
| 152 |
+
"environment": ENVIRONMENT_ID,
|
| 153 |
+
"product": PRODUCT_NAME,
|
| 154 |
+
"health": "/health",
|
| 155 |
+
"metadata": "/metadata",
|
| 156 |
+
"dashboard": "/dashboard/",
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@app.get("/health")
|
| 161 |
+
def health() -> dict[str, str]:
|
| 162 |
+
return {"status": "ok", "service": "clip-quality-api"}
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@app.get("/state")
|
| 166 |
+
def get_state() -> dict[str, Any]:
|
| 167 |
+
env = ClipQualityEnvironment()
|
| 168 |
+
state = env.state.model_dump()
|
| 169 |
+
state["product"] = PRODUCT_NAME
|
| 170 |
+
state["workflow"] = "clip_quality_analysis"
|
| 171 |
+
return state
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@app.get("/tasks")
|
| 175 |
+
def list_tasks() -> list[TaskInfo]:
|
| 176 |
+
return [
|
| 177 |
+
TaskInfo(
|
| 178 |
+
task_id=task_id,
|
| 179 |
+
difficulty=task["difficulty"],
|
| 180 |
+
description=_surface_task_description(task_id, task["description"]),
|
| 181 |
+
action_schema=Action.model_json_schema(),
|
| 182 |
+
)
|
| 183 |
+
for task_id, task in TASK_REGISTRY.items()
|
| 184 |
+
]
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@app.post("/grader")
|
| 188 |
+
def get_grader_score(task_id: str, action: Action) -> dict[str, Any]:
|
| 189 |
+
if task_id not in TASK_REGISTRY:
|
| 190 |
+
raise HTTPException(status_code=404, detail=f"Unknown task_id: {task_id}")
|
| 191 |
+
score = grade(action.model_dump(), task_id)
|
| 192 |
+
return {
|
| 193 |
+
"task_id": task_id,
|
| 194 |
+
"score": score,
|
| 195 |
+
"passed": 1 if score > 0.5 else 0,
|
| 196 |
+
"total": 1,
|
| 197 |
+
"metric": "clip_quality_alignment",
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _baseline_payload_from_raw(raw: dict[str, Any]) -> dict[str, Any]:
|
| 202 |
+
scores = raw.get("baseline_scores")
|
| 203 |
+
score_payload = scores if isinstance(scores, dict) else {}
|
| 204 |
+
payload: dict[str, Any] = {
|
| 205 |
+
"baseline_results": raw.get("detail", []),
|
| 206 |
+
"average_score": score_payload.get("overall_avg", 0.0),
|
| 207 |
+
"model": raw.get("model", os.environ.get("MODEL_NAME", inference.DEFAULT_MODEL_NAME)),
|
| 208 |
+
"metric": "clip_quality_alignment",
|
| 209 |
+
}
|
| 210 |
+
if "runtime_seconds" in raw:
|
| 211 |
+
payload["runtime_seconds"] = raw.get("runtime_seconds")
|
| 212 |
+
if "warning" in raw:
|
| 213 |
+
payload["warning"] = raw.get("warning")
|
| 214 |
+
return payload
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def _initial_baseline_payload(task: str | None = None) -> dict[str, Any]:
|
| 218 |
+
payload: dict[str, Any] = {
|
| 219 |
+
"baseline_results": [],
|
| 220 |
+
"average_score": None,
|
| 221 |
+
"model": os.environ.get("MODEL_NAME", inference.DEFAULT_MODEL_NAME),
|
| 222 |
+
"metric": "clip_quality_alignment",
|
| 223 |
+
}
|
| 224 |
+
if task:
|
| 225 |
+
payload["task"] = task
|
| 226 |
+
return payload
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def _public_baseline_status(status: str) -> str:
|
| 230 |
+
return "complete" if status == "completed" else status
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _mode_label(mode: str) -> str:
|
| 234 |
+
return "LLM" if str(mode).strip().lower() == "llm" else "Deterministic fallback"
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def _summarize_mode(results: list[dict[str, Any]]) -> str:
|
| 238 |
+
modes = {str(item.get("mode", "fallback")).strip().lower() for item in results}
|
| 239 |
+
if not modes or modes == {"fallback"}:
|
| 240 |
+
return "Deterministic fallback"
|
| 241 |
+
if modes == {"llm"}:
|
| 242 |
+
return "LLM"
|
| 243 |
+
return "Mixed (LLM + deterministic fallback)"
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def _baseline_warning_messages(payload: dict[str, Any]) -> list[str]:
|
| 247 |
+
warnings: list[str] = []
|
| 248 |
+
if not os.environ.get("HF_TOKEN"):
|
| 249 |
+
warnings.append(HF_TOKEN_MISSING_WARNING)
|
| 250 |
+
payload_warning = str(payload.get("warning") or "").strip()
|
| 251 |
+
if payload_warning and payload_warning not in warnings:
|
| 252 |
+
warnings.append(payload_warning)
|
| 253 |
+
return warnings
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def _format_score_color(value: float) -> str:
|
| 257 |
+
return "#16a34a" if value >= 0.10 else "#dc2626"
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def _label_score_color(value: float) -> str:
|
| 261 |
+
if value >= 0.60:
|
| 262 |
+
return "#16a34a"
|
| 263 |
+
if value >= 0.25:
|
| 264 |
+
return "#f59e0b"
|
| 265 |
+
return "#dc2626"
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _reasoning_score_color(value: float) -> str:
|
| 269 |
+
if value >= 0.30:
|
| 270 |
+
return "#16a34a"
|
| 271 |
+
if value > 0.0:
|
| 272 |
+
return "#f59e0b"
|
| 273 |
+
return "#dc2626"
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def _total_reward_color(value: float, has_submission: bool) -> str:
|
| 277 |
+
if not has_submission:
|
| 278 |
+
return "#6b7280"
|
| 279 |
+
if value >= 0.70:
|
| 280 |
+
return "#16a34a"
|
| 281 |
+
if value > 0.0:
|
| 282 |
+
return "#f59e0b"
|
| 283 |
+
return "#dc2626"
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def _reward_card_html(title: str, value: float, color: str, max_value: float) -> str:
|
| 287 |
+
return (
|
| 288 |
+
"<div style='background:#ffffff;border:1px solid #fde7cc;border-left:4px solid {color};"
|
| 289 |
+
"border-radius:8px;padding:8px 10px;'>"
|
| 290 |
+
"<div style='font-size:0.8rem;font-weight:600;color:#4b5563;'>{title}</div>"
|
| 291 |
+
"<div style='font-size:1.1rem;font-weight:700;color:{color};'>{value:.2f}</div>"
|
| 292 |
+
"<div style='font-size:0.72rem;color:#6b7280;'>max {max_value:.2f}</div>"
|
| 293 |
+
"</div>"
|
| 294 |
+
).format(title=title, value=float(value), color=color, max_value=float(max_value))
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def _reward_breakdown_markdown(obs: dict[str, Any], initialized: bool = False) -> str:
|
| 298 |
+
info = obs.get("info", {}) if isinstance(obs, dict) else {}
|
| 299 |
+
format_score = float(info.get("format_score", 0.0))
|
| 300 |
+
label_score = float(info.get("label_score", 0.0))
|
| 301 |
+
reasoning_score = float(info.get("reasoning_score", 0.0))
|
| 302 |
+
total_reward = float(obs.get("reward", info.get("reward_total", 0.0)))
|
| 303 |
+
running_total = float(info.get("total_reward", 0.0))
|
| 304 |
+
best_score = float(info.get("best_score", 0.0))
|
| 305 |
+
has_submission = bool(obs.get("history"))
|
| 306 |
+
|
| 307 |
+
title = "### Session Initialized" if initialized else "### Latest Submission Reward Breakdown"
|
| 308 |
+
subtitle = (
|
| 309 |
+
"_Submit a classification action to populate live reward decomposition._"
|
| 310 |
+
if initialized
|
| 311 |
+
else "_Live reward decomposition for this submission._"
|
| 312 |
+
)
|
| 313 |
+
cards = (
|
| 314 |
+
"<div style='display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;'>"
|
| 315 |
+
f"{_reward_card_html('Format Score', format_score, _format_score_color(format_score), 0.10)}"
|
| 316 |
+
f"{_reward_card_html('Label Score', label_score, _label_score_color(label_score), 0.60)}"
|
| 317 |
+
f"{_reward_card_html('Reasoning Score', reasoning_score, _reasoning_score_color(reasoning_score), 0.30)}"
|
| 318 |
+
"</div>"
|
| 319 |
+
)
|
| 320 |
+
total_color = _total_reward_color(total_reward, has_submission=has_submission)
|
| 321 |
+
return "\n".join(
|
| 322 |
+
[
|
| 323 |
+
title,
|
| 324 |
+
subtitle,
|
| 325 |
+
cards,
|
| 326 |
+
f"### **Total Reward:** <span style='color:{total_color};font-size:1.35rem;'>{total_reward:.3f}</span>",
|
| 327 |
+
f"Current Best Score: **{best_score:.3f}**",
|
| 328 |
+
]
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def _format_baseline_result_markdown(
|
| 333 |
+
payload: dict[str, Any] | None,
|
| 334 |
+
status: str,
|
| 335 |
+
error: dict[str, Any] | None = None,
|
| 336 |
+
) -> str:
|
| 337 |
+
payload = payload if isinstance(payload, dict) else {}
|
| 338 |
+
model_name = str(payload.get("model") or os.environ.get("MODEL_NAME", inference.DEFAULT_MODEL_NAME))
|
| 339 |
+
raw_results = payload.get("baseline_results")
|
| 340 |
+
results = [item for item in raw_results if isinstance(item, dict)] if isinstance(raw_results, list) else []
|
| 341 |
+
lines = [f"- **Model:** `{model_name}`"]
|
| 342 |
+
|
| 343 |
+
if status == "running":
|
| 344 |
+
lines.append("- **Mode Used:** _Pending..._")
|
| 345 |
+
lines.append("- **Reward achieved per step:** _Pending..._")
|
| 346 |
+
lines.append("- **Success:** _Pending..._")
|
| 347 |
+
else:
|
| 348 |
+
lines.append(f"- **Mode Used:** {_summarize_mode(results)}")
|
| 349 |
+
if "average_score" in payload and payload.get("average_score") is not None:
|
| 350 |
+
lines.append(f"- **Average Score:** `{float(payload.get('average_score', 0.0)):.3f}`")
|
| 351 |
+
if results:
|
| 352 |
+
for result in results:
|
| 353 |
+
task_name = str(result.get("task_id", "unknown_task"))
|
| 354 |
+
steps = int(result.get("steps", 0) or 0)
|
| 355 |
+
average_reward = float(result.get("reward", 0.0))
|
| 356 |
+
total_reward = float(
|
| 357 |
+
result.get("total_reward", average_reward * steps if steps > 0 else average_reward)
|
| 358 |
+
)
|
| 359 |
+
final_reward = float(result.get("final_reward", average_reward))
|
| 360 |
+
reward_per_step = total_reward / steps if steps > 0 else average_reward
|
| 361 |
+
success = "Yes" if bool(result.get("success")) else "No"
|
| 362 |
+
lines.append(
|
| 363 |
+
f"- **{task_name}**: {_mode_label(str(result.get('mode', 'fallback')))}; "
|
| 364 |
+
f"score `{reward_per_step:.3f}`; success **{success}**"
|
| 365 |
+
)
|
| 366 |
+
else:
|
| 367 |
+
lines.append("- **Reward achieved per step:** _No baseline detail available._")
|
| 368 |
+
lines.append("- **Success:** _No baseline detail available._")
|
| 369 |
+
|
| 370 |
+
if status == "complete":
|
| 371 |
+
lines.append("- **Status:** Complete")
|
| 372 |
+
elif status == "failed":
|
| 373 |
+
message = str((error or {}).get("message") or "").strip()
|
| 374 |
+
suffix = f" — {message}" if message else ""
|
| 375 |
+
lines.append(f"- **Status:** Failed{suffix}")
|
| 376 |
+
elif status == "running":
|
| 377 |
+
lines.append("- **Status:** Running")
|
| 378 |
+
|
| 379 |
+
for warning in _baseline_warning_messages(payload):
|
| 380 |
+
lines.append(f"> {warning}")
|
| 381 |
+
return "\n".join(lines)
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def _run_baseline_background(run_id: str, task: str | None = None) -> None:
|
| 385 |
+
try:
|
| 386 |
+
raw = inference.run_baseline(task=task)
|
| 387 |
+
baseline_run_tracker.mark_complete(run_id, _baseline_payload_from_raw(raw))
|
| 388 |
+
except Exception as exc:
|
| 389 |
+
baseline_run_tracker.mark_failed(run_id, {"message": str(exc)})
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def _start_baseline_ui_run(task: str | None = None) -> tuple[str, str, str, dict[str, Any], dict[str, Any]]:
|
| 393 |
+
baseline_run_tracker.cleanup_expired()
|
| 394 |
+
run_id = baseline_run_tracker.create_run()
|
| 395 |
+
initial_payload = _initial_baseline_payload(task=task)
|
| 396 |
+
baseline_run_tracker.update_partial(run_id, initial_payload)
|
| 397 |
+
worker = threading.Thread(
|
| 398 |
+
target=_run_baseline_background,
|
| 399 |
+
args=(run_id, task),
|
| 400 |
+
daemon=True,
|
| 401 |
+
name=f"baseline-ui-{run_id[:8]}",
|
| 402 |
+
)
|
| 403 |
+
worker.start()
|
| 404 |
+
return (
|
| 405 |
+
run_id,
|
| 406 |
+
"### LLM baseline run in progress...",
|
| 407 |
+
_format_baseline_result_markdown(initial_payload, status="running"),
|
| 408 |
+
gr.update(value=BASELINE_RUN_BUTTON_RUNNING_LABEL, interactive=False),
|
| 409 |
+
gr.update(active=True),
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def _poll_baseline_ui_run(
|
| 414 |
+
run_id: str | None,
|
| 415 |
+
) -> tuple[str | None, str, str, dict[str, Any], dict[str, Any]]:
|
| 416 |
+
if not run_id:
|
| 417 |
+
return (
|
| 418 |
+
None,
|
| 419 |
+
"### Baseline agent idle.",
|
| 420 |
+
BASELINE_RESULT_PLACEHOLDER,
|
| 421 |
+
gr.update(value=BASELINE_RUN_BUTTON_LABEL, interactive=True),
|
| 422 |
+
gr.update(active=False),
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
baseline_run_tracker.cleanup_expired()
|
| 426 |
+
run = baseline_run_tracker.get_run(run_id)
|
| 427 |
+
if run is None:
|
| 428 |
+
return (
|
| 429 |
+
None,
|
| 430 |
+
"### Baseline run unavailable (expired or unknown).",
|
| 431 |
+
_format_baseline_result_markdown(_initial_baseline_payload(), status="failed"),
|
| 432 |
+
gr.update(value=BASELINE_RUN_BUTTON_LABEL, interactive=True),
|
| 433 |
+
gr.update(active=False),
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
status = _public_baseline_status(run["status"])
|
| 437 |
+
payload = run["result"] if run["status"] == "completed" else run.get("partial")
|
| 438 |
+
payload = payload if isinstance(payload, dict) else {}
|
| 439 |
+
|
| 440 |
+
if status == "running":
|
| 441 |
+
return (
|
| 442 |
+
run_id,
|
| 443 |
+
"### LLM baseline run in progress...",
|
| 444 |
+
_format_baseline_result_markdown(payload, status="running"),
|
| 445 |
+
gr.update(value=BASELINE_RUN_BUTTON_RUNNING_LABEL, interactive=False),
|
| 446 |
+
gr.update(active=True),
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
if status == "failed":
|
| 450 |
+
return (
|
| 451 |
+
None,
|
| 452 |
+
"### Baseline run failed.",
|
| 453 |
+
_format_baseline_result_markdown(payload, status="failed", error=run.get("error")),
|
| 454 |
+
gr.update(value=BASELINE_RUN_BUTTON_LABEL, interactive=True),
|
| 455 |
+
gr.update(active=False),
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
average_score = payload.get("average_score")
|
| 459 |
+
score_display = f"{float(average_score):.3f}" if average_score is not None else "N/A"
|
| 460 |
+
return (
|
| 461 |
+
None,
|
| 462 |
+
f"### Baseline run complete (avg score: {score_display})",
|
| 463 |
+
_format_baseline_result_markdown(payload, status="complete"),
|
| 464 |
+
gr.update(value=BASELINE_RUN_BUTTON_LABEL, interactive=True),
|
| 465 |
+
gr.update(active=False),
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def _enqueue_baseline_run(background_tasks: BackgroundTasks, task: str | None = None) -> dict[str, Any]:
|
| 470 |
+
baseline_run_tracker.cleanup_expired()
|
| 471 |
+
run_id = baseline_run_tracker.create_run()
|
| 472 |
+
initial_payload = _initial_baseline_payload(task=task)
|
| 473 |
+
baseline_run_tracker.update_partial(run_id, initial_payload)
|
| 474 |
+
background_tasks.add_task(_run_baseline_background, run_id, task)
|
| 475 |
+
return {
|
| 476 |
+
"run_id": run_id,
|
| 477 |
+
"status": "running",
|
| 478 |
+
"payload": initial_payload,
|
| 479 |
+
"status_url": f"/baseline/status/{run_id}",
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
@app.post("/baseline/start")
|
| 484 |
+
def start_baseline_route(background_tasks: BackgroundTasks, task: str | None = None) -> dict[str, Any]:
|
| 485 |
+
return _enqueue_baseline_run(background_tasks=background_tasks, task=task)
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
@app.get("/baseline/status/{run_id}")
|
| 489 |
+
def baseline_status_route(run_id: str) -> dict[str, Any]:
|
| 490 |
+
baseline_run_tracker.cleanup_expired()
|
| 491 |
+
run = baseline_run_tracker.get_run(run_id)
|
| 492 |
+
if run is None:
|
| 493 |
+
raise HTTPException(status_code=404, detail=f"Unknown run_id: {run_id}")
|
| 494 |
+
|
| 495 |
+
status = _public_baseline_status(run["status"])
|
| 496 |
+
payload = run["result"] if run["status"] == "completed" else run["partial"]
|
| 497 |
+
response: dict[str, Any] = {
|
| 498 |
+
"run_id": run_id,
|
| 499 |
+
"status": status,
|
| 500 |
+
"payload": payload if payload is not None else {},
|
| 501 |
+
}
|
| 502 |
+
if status == "failed":
|
| 503 |
+
response["error"] = run["error"]
|
| 504 |
+
return response
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
@app.get("/baseline")
|
| 508 |
+
def run_baseline_route(background_tasks: BackgroundTasks, task: str | None = None) -> dict[str, Any]:
|
| 509 |
+
return _enqueue_baseline_run(background_tasks=background_tasks, task=task)
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def build_custom_ui() -> gr.Blocks:
|
| 513 |
+
dominant_feature_columns = [
|
| 514 |
+
"Feature Name",
|
| 515 |
+
"Current Value",
|
| 516 |
+
"Rubric Status",
|
| 517 |
+
"Threshold Range",
|
| 518 |
+
]
|
| 519 |
+
session_history_columns = ["Step", "Clip ID", "Submitted Label", "Expected Label", "Reward"]
|
| 520 |
+
|
| 521 |
+
def format_dominant_features(rows: list[dict[str, Any]]) -> pd.DataFrame:
|
| 522 |
+
if not rows:
|
| 523 |
+
return pd.DataFrame(columns=dominant_feature_columns)
|
| 524 |
+
return pd.DataFrame(rows, columns=dominant_feature_columns)
|
| 525 |
+
|
| 526 |
+
def format_obs(obs: dict[str, Any]) -> tuple[pd.DataFrame, str, float, int, str, str]:
|
| 527 |
+
if not obs:
|
| 528 |
+
return (
|
| 529 |
+
pd.DataFrame(columns=["Clip ID", "Expected Label", "Current Review Status", "Face Confidence", "Motion Score", "Audio SNR (dB)"]),
|
| 530 |
+
"### No Clip-Quality Rubric Available",
|
| 531 |
+
0.0,
|
| 532 |
+
5,
|
| 533 |
+
"N/A",
|
| 534 |
+
"### Clip Queue: **0** of **0** items displayed",
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
corpus_items = list(obs.get("data_corpus", []))
|
| 538 |
+
corpus_items.sort(key=lambda item: str(item.get("clip_id", item.get("id", ""))))
|
| 539 |
+
|
| 540 |
+
corpus_data = []
|
| 541 |
+
for item in corpus_items:
|
| 542 |
+
corpus_data.append(
|
| 543 |
+
{
|
| 544 |
+
"Clip ID": item.get("clip_id", item.get("id", "N/A")),
|
| 545 |
+
"Expected Label": item.get("expected_label", "N/A"),
|
| 546 |
+
"Current Review Status": item.get("review_status", "pending"),
|
| 547 |
+
"Face Confidence": item.get("face_confidence", "N/A"),
|
| 548 |
+
"Motion Score": item.get("motion_score", "N/A"),
|
| 549 |
+
"Audio SNR (dB)": item.get("audio_snr_db", "N/A"),
|
| 550 |
+
}
|
| 551 |
+
)
|
| 552 |
+
df_corpus = pd.DataFrame(corpus_data) if corpus_data else pd.DataFrame(
|
| 553 |
+
columns=["Clip ID", "Expected Label", "Current Review Status", "Face Confidence", "Motion Score", "Audio SNR (dB)"]
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
rubric_summary = obs.get("rubric_summary", "").strip()
|
| 557 |
+
rule_md = "### Active Clip-Quality Rubric\n"
|
| 558 |
+
if rubric_summary:
|
| 559 |
+
rule_md += f"```\n{rubric_summary}\n```"
|
| 560 |
+
else:
|
| 561 |
+
rule_md += "_No rubric summary available._"
|
| 562 |
+
|
| 563 |
+
best_score = float(obs.get("info", {}).get("best_score", 0.0))
|
| 564 |
+
steps_left = int(obs.get("info", {}).get("steps_remaining", 0))
|
| 565 |
+
episode_id = str(obs.get("episode_id", "N/A"))[:8]
|
| 566 |
+
shown = int(obs.get("corpus_shown", len(corpus_data)))
|
| 567 |
+
total = int(obs.get("corpus_size", len(corpus_data)))
|
| 568 |
+
corpus_stat = f"### Clip Queue: **{shown}** of **{total}** items displayed"
|
| 569 |
+
return df_corpus, rule_md, best_score, steps_left, episode_id, corpus_stat
|
| 570 |
+
|
| 571 |
+
def format_session_history(obs: dict[str, Any]) -> tuple[pd.DataFrame, str, str]:
|
| 572 |
+
info = obs.get("info", {}) if isinstance(obs, dict) else {}
|
| 573 |
+
raw_history = info.get("session_history", [])
|
| 574 |
+
session_history = raw_history if isinstance(raw_history, list) else []
|
| 575 |
+
rows: list[dict[str, Any]] = []
|
| 576 |
+
cue_lines: list[str] = []
|
| 577 |
+
|
| 578 |
+
for idx, item in enumerate(session_history, start=1):
|
| 579 |
+
step = int(item.get("step", idx))
|
| 580 |
+
clip_id = str(item.get("clip_id", "N/A"))
|
| 581 |
+
submitted_raw = item.get("label", "")
|
| 582 |
+
submitted = str(submitted_raw).upper() if submitted_raw is not None else ""
|
| 583 |
+
expected_raw = item.get("expected_label")
|
| 584 |
+
expected_normalized = str(expected_raw).strip().lower() if expected_raw is not None else ""
|
| 585 |
+
expected = "N/A" if expected_normalized in {"", "none", "null", "n/a"} else str(expected_raw).upper()
|
| 586 |
+
reward = float(item.get("reward", 0.0))
|
| 587 |
+
rows.append(
|
| 588 |
+
{
|
| 589 |
+
"Step": step,
|
| 590 |
+
"Clip ID": clip_id,
|
| 591 |
+
"Submitted Label": submitted,
|
| 592 |
+
"Expected Label": expected,
|
| 593 |
+
"Reward": reward,
|
| 594 |
+
}
|
| 595 |
+
)
|
| 596 |
+
is_match = bool(submitted) and expected not in {"", "N/A"} and submitted == expected
|
| 597 |
+
badge = (
|
| 598 |
+
"<span style='color:#2e7d32; font-weight:700;'>Match</span>"
|
| 599 |
+
if is_match
|
| 600 |
+
else "<span style='color:#c62828; font-weight:700;'>Mismatch</span>"
|
| 601 |
+
)
|
| 602 |
+
cue_lines.append(f"- Step {step} (`{clip_id}`): {badge}")
|
| 603 |
+
|
| 604 |
+
history_df = (
|
| 605 |
+
pd.DataFrame(rows, columns=session_history_columns)
|
| 606 |
+
if rows
|
| 607 |
+
else pd.DataFrame(columns=session_history_columns)
|
| 608 |
+
)
|
| 609 |
+
history_cues_md = "### Match Results\n" + "\n".join(cue_lines) if cue_lines else "### Match Results\n_No actions yet._"
|
| 610 |
+
|
| 611 |
+
total_reward = float(info.get("total_reward", 0.0))
|
| 612 |
+
total_color = "#2e7d32" if total_reward > 0 else "#c62828" if total_reward < 0 else "#374151"
|
| 613 |
+
history_total_md = ""
|
| 614 |
+
return history_df, history_cues_md, history_total_md
|
| 615 |
+
|
| 616 |
+
def _resolve_env(env_state: ClipQualityEnvironment | None) -> ClipQualityEnvironment:
|
| 617 |
+
return env_state if isinstance(env_state, ClipQualityEnvironment) else ClipQualityEnvironment()
|
| 618 |
+
|
| 619 |
+
def handle_reset(env_state: ClipQualityEnvironment | None, task_id: str):
|
| 620 |
+
env = _resolve_env(env_state)
|
| 621 |
+
obs = env.reset(task_id=task_id).model_dump()
|
| 622 |
+
df, pol, score, steps, ep, stat = format_obs(obs)
|
| 623 |
+
dominant_df = format_dominant_features(env.dominant_feature_rows())
|
| 624 |
+
history_df, history_cues, history_total = format_session_history(obs)
|
| 625 |
+
reward_msg = _reward_breakdown_markdown(obs, initialized=True)
|
| 626 |
+
return (
|
| 627 |
+
env,
|
| 628 |
+
df,
|
| 629 |
+
pol,
|
| 630 |
+
dominant_df,
|
| 631 |
+
score,
|
| 632 |
+
steps,
|
| 633 |
+
ep,
|
| 634 |
+
stat,
|
| 635 |
+
reward_msg,
|
| 636 |
+
history_df,
|
| 637 |
+
history_cues,
|
| 638 |
+
history_total,
|
| 639 |
+
json.dumps(obs, indent=2),
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
def handle_step(
|
| 643 |
+
env_state: ClipQualityEnvironment | None,
|
| 644 |
+
task_id: str,
|
| 645 |
+
selected_input_tab: str,
|
| 646 |
+
easy_label: str,
|
| 647 |
+
easy_observation: str,
|
| 648 |
+
medium_label: str,
|
| 649 |
+
medium_primary_signal: str,
|
| 650 |
+
medium_conflicting_signal: str,
|
| 651 |
+
medium_reasoning: str,
|
| 652 |
+
hard_label: str,
|
| 653 |
+
hard_tradeoff_summary: str,
|
| 654 |
+
hard_confidence_justification: str,
|
| 655 |
+
hard_confidence: float,
|
| 656 |
+
clip_id: str,
|
| 657 |
+
):
|
| 658 |
+
env = _resolve_env(env_state)
|
| 659 |
+
if env.state.task_id != task_id:
|
| 660 |
+
env.reset(task_id=task_id)
|
| 661 |
+
label, reasoning, confidence = _resolve_tiered_submission(
|
| 662 |
+
task_id=task_id,
|
| 663 |
+
selected_input_tab=selected_input_tab,
|
| 664 |
+
easy_label=easy_label,
|
| 665 |
+
easy_observation=easy_observation,
|
| 666 |
+
medium_label=medium_label,
|
| 667 |
+
medium_primary_signal=medium_primary_signal,
|
| 668 |
+
medium_conflicting_signal=medium_conflicting_signal,
|
| 669 |
+
medium_reasoning=medium_reasoning,
|
| 670 |
+
hard_label=hard_label,
|
| 671 |
+
hard_tradeoff_summary=hard_tradeoff_summary,
|
| 672 |
+
hard_confidence_justification=hard_confidence_justification,
|
| 673 |
+
hard_confidence=hard_confidence,
|
| 674 |
+
)
|
| 675 |
+
payload: dict[str, Any] = {
|
| 676 |
+
"label": label,
|
| 677 |
+
"reasoning": reasoning,
|
| 678 |
+
"confidence": confidence,
|
| 679 |
+
}
|
| 680 |
+
if clip_id and clip_id.strip():
|
| 681 |
+
payload["clip_id"] = clip_id.strip()
|
| 682 |
+
|
| 683 |
+
obs_obj = env.step(Action.model_validate(payload))
|
| 684 |
+
obs = obs_obj.model_dump()
|
| 685 |
+
df, pol, score, steps, ep, stat = format_obs(obs)
|
| 686 |
+
dominant_df = format_dominant_features(env.dominant_feature_rows())
|
| 687 |
+
history_df, history_cues, history_total = format_session_history(obs)
|
| 688 |
+
reward_msg = _reward_breakdown_markdown(obs, initialized=False)
|
| 689 |
+
return (
|
| 690 |
+
env,
|
| 691 |
+
df,
|
| 692 |
+
pol,
|
| 693 |
+
dominant_df,
|
| 694 |
+
score,
|
| 695 |
+
steps,
|
| 696 |
+
ep,
|
| 697 |
+
stat,
|
| 698 |
+
reward_msg,
|
| 699 |
+
history_df,
|
| 700 |
+
history_cues,
|
| 701 |
+
history_total,
|
| 702 |
+
json.dumps(obs, indent=2),
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
def handle_quality_hint(
|
| 706 |
+
env_state: ClipQualityEnvironment | None,
|
| 707 |
+
task_id: str,
|
| 708 |
+
selected_input_tab: str,
|
| 709 |
+
easy_observation: str,
|
| 710 |
+
medium_reasoning: str,
|
| 711 |
+
hard_tradeoff_summary: str,
|
| 712 |
+
) -> tuple[ClipQualityEnvironment, str, str, str]:
|
| 713 |
+
env = _resolve_env(env_state)
|
| 714 |
+
if env.state.task_id != task_id or not env.state.current_clip_id:
|
| 715 |
+
env.reset(task_id=task_id)
|
| 716 |
+
|
| 717 |
+
hint_text = env.build_quality_hint()
|
| 718 |
+
active_tab = selected_input_tab if selected_input_tab in {"easy", "medium", "hard"} else _input_tab_for_task(task_id)
|
| 719 |
+
|
| 720 |
+
next_easy_observation = easy_observation
|
| 721 |
+
next_medium_reasoning = medium_reasoning
|
| 722 |
+
next_hard_tradeoff_summary = hard_tradeoff_summary
|
| 723 |
+
if active_tab == "easy":
|
| 724 |
+
next_easy_observation = hint_text
|
| 725 |
+
elif active_tab == "medium":
|
| 726 |
+
next_medium_reasoning = hint_text
|
| 727 |
+
else:
|
| 728 |
+
next_hard_tradeoff_summary = hint_text
|
| 729 |
+
|
| 730 |
+
return env, next_easy_observation, next_medium_reasoning, next_hard_tradeoff_summary
|
| 731 |
+
|
| 732 |
+
with gr.Blocks(
|
| 733 |
+
title="CLIP Quality Analyzer: Judge's Console",
|
| 734 |
+
theme=gr.themes.Soft(
|
| 735 |
+
primary_hue="orange",
|
| 736 |
+
font=gr.themes.GoogleFont("Source Sans Pro"),
|
| 737 |
+
),
|
| 738 |
+
css="""
|
| 739 |
+
.dark {
|
| 740 |
+
background-color: #0c0c0c !important;
|
| 741 |
+
}
|
| 742 |
+
""",
|
| 743 |
+
) as demo:
|
| 744 |
+
env_state = gr.State(value=None)
|
| 745 |
+
baseline_run_id_state = gr.State(value=None)
|
| 746 |
+
selected_tab_state = gr.State(value="easy")
|
| 747 |
+
baseline_poll_timer = gr.Timer(value=1.0, active=False)
|
| 748 |
+
|
| 749 |
+
gr.HTML("<h2 style='text-align: center; color: #10b981;'>CLIP Quality Analyzer: Judge's Strategic Console</h2>")
|
| 750 |
+
gr.Markdown(
|
| 751 |
+
"Welcome, Judge Agent. Use this console to identify data to policy gaps and propose measurable governance refinements."
|
| 752 |
+
)
|
| 753 |
+
|
| 754 |
+
with gr.Row():
|
| 755 |
+
with gr.Column(scale=1):
|
| 756 |
+
gr.Markdown("### Scenario Metrics")
|
| 757 |
+
with gr.Group():
|
| 758 |
+
best_score_disp = gr.Number(label="Environment Best Score", value=0.0, interactive=False)
|
| 759 |
+
steps_left_disp = gr.Number(label="Remaining Execution Steps", value=5, interactive=False)
|
| 760 |
+
episode_disp = gr.Textbox(label="Active Episode ID", value="N/A", interactive=False)
|
| 761 |
+
|
| 762 |
+
reward_outcome_disp = gr.Markdown("### Awaiting Scenario...")
|
| 763 |
+
|
| 764 |
+
with gr.Group():
|
| 765 |
+
task_id = gr.Dropdown(choices=list(TASK_REGISTRY.keys()), value="task_easy", label="Deployment Scenario")
|
| 766 |
+
reset_btn = gr.Button("Initialize Scenario", variant="secondary")
|
| 767 |
+
|
| 768 |
+
with gr.Column(scale=3):
|
| 769 |
+
corpus_count_disp = gr.Markdown("### Corpus: 0 of 0 incidents displayed")
|
| 770 |
+
with gr.Tabs():
|
| 771 |
+
with gr.Tab("Data Corpus (Tabular View)"):
|
| 772 |
+
corpus_table = gr.DataFrame(
|
| 773 |
+
label="Sampled Posts and System Actions",
|
| 774 |
+
interactive=False,
|
| 775 |
+
)
|
| 776 |
+
with gr.Tab("Active Framework"):
|
| 777 |
+
policy_display = gr.Markdown("Initialize to view the current rubric summary.")
|
| 778 |
+
with gr.Tab("Diagnostic JSON"):
|
| 779 |
+
raw_json_box = gr.Code(label="Environment Raw Response", language="json", interactive=False)
|
| 780 |
+
with gr.Tab("Session History"):
|
| 781 |
+
session_history_table = gr.DataFrame(
|
| 782 |
+
value=pd.DataFrame(columns=session_history_columns),
|
| 783 |
+
label="Per-step Classification History",
|
| 784 |
+
headers=session_history_columns,
|
| 785 |
+
col_count=(len(session_history_columns), "fixed"),
|
| 786 |
+
datatype=["number", "str", "str", "str", "number"],
|
| 787 |
+
interactive=False,
|
| 788 |
+
)
|
| 789 |
+
session_history_cues = gr.Markdown("### Match Results\n_No actions yet._")
|
| 790 |
+
session_total_reward = gr.Markdown("", visible=False)
|
| 791 |
+
with gr.Accordion("LLM Baseline Result", open=False):
|
| 792 |
+
baseline_run_btn = gr.Button(BASELINE_RUN_BUTTON_LABEL, variant="secondary")
|
| 793 |
+
baseline_status_disp = gr.Markdown("### Baseline agent idle.")
|
| 794 |
+
baseline_result_md = gr.Markdown(BASELINE_RESULT_PLACEHOLDER)
|
| 795 |
+
|
| 796 |
+
dominant_features_table = gr.DataFrame(
|
| 797 |
+
value=pd.DataFrame(columns=dominant_feature_columns),
|
| 798 |
+
label="Feature Focus Table",
|
| 799 |
+
headers=dominant_feature_columns,
|
| 800 |
+
column_count=len(dominant_feature_columns),
|
| 801 |
+
column_limits=(len(dominant_feature_columns), len(dominant_feature_columns)),
|
| 802 |
+
datatype=["str", "number", "str", "str"],
|
| 803 |
+
interactive=False,
|
| 804 |
+
visible=False
|
| 805 |
+
)
|
| 806 |
+
|
| 807 |
+
gr.Markdown("---")
|
| 808 |
+
gr.Markdown("### Propose Strategic Refinement")
|
| 809 |
+
with gr.Group():
|
| 810 |
+
with gr.Tabs(selected="easy") as tiered_input_tabs:
|
| 811 |
+
with gr.Tab("Easy: Definition Refining", id="easy"):
|
| 812 |
+
easy_label_input = gr.Radio(
|
| 813 |
+
choices=CLASS_LABEL_CHOICES,
|
| 814 |
+
value="BORDERLINE",
|
| 815 |
+
label="Predicted Label",
|
| 816 |
+
)
|
| 817 |
+
easy_observation_input = gr.Textbox(label="Key Observation", lines=2)
|
| 818 |
+
with gr.Tab("Medium: Gap Detection", id="medium"):
|
| 819 |
+
medium_label_input = gr.Radio(
|
| 820 |
+
choices=CLASS_LABEL_CHOICES,
|
| 821 |
+
value="BORDERLINE",
|
| 822 |
+
label="Predicted Label",
|
| 823 |
+
)
|
| 824 |
+
medium_primary_signal_input = gr.Textbox(label="Primary Signal", lines=1)
|
| 825 |
+
medium_conflicting_signal_input = gr.Textbox(label="Conflicting Signal", lines=1)
|
| 826 |
+
medium_reasoning_input = gr.TextArea(label="Reasoning", lines=4)
|
| 827 |
+
with gr.Tab("Hard: Full System Evolution", id="hard"):
|
| 828 |
+
hard_label_input = gr.Radio(
|
| 829 |
+
choices=CLASS_LABEL_CHOICES,
|
| 830 |
+
value="BORDERLINE",
|
| 831 |
+
label="Predicted Label",
|
| 832 |
+
)
|
| 833 |
+
hard_tradeoff_summary_input = gr.TextArea(label="Trade-off Summary", lines=4)
|
| 834 |
+
with gr.Row():
|
| 835 |
+
hard_confidence_justification_input = gr.TextArea(label="Confidence Justification", lines=3, scale=2)
|
| 836 |
+
hard_confidence_input = gr.Slider(
|
| 837 |
+
minimum=0.0,
|
| 838 |
+
maximum=1.0,
|
| 839 |
+
value=0.5,
|
| 840 |
+
step=0.01,
|
| 841 |
+
label="Confidence",
|
| 842 |
+
scale=1
|
| 843 |
+
)
|
| 844 |
+
|
| 845 |
+
clip_id_input = gr.Textbox(label="Clip ID override (optional)")
|
| 846 |
+
hint_btn = gr.Button(QUALITY_HINT_BUTTON_LABEL, variant="secondary")
|
| 847 |
+
|
| 848 |
+
step_btn = gr.Button("Execute Strategic Step", variant="primary")
|
| 849 |
+
|
| 850 |
+
def sync_input_tab_for_task(selected_task_id: str):
|
| 851 |
+
tab_id = _input_tab_for_task(selected_task_id)
|
| 852 |
+
return _input_tab_update_for_task(selected_task_id), tab_id
|
| 853 |
+
|
| 854 |
+
def on_tab_select(evt: gr.SelectData) -> str:
|
| 855 |
+
return str(evt.value) if evt and evt.value else "easy"
|
| 856 |
+
|
| 857 |
+
task_id.change(
|
| 858 |
+
sync_input_tab_for_task,
|
| 859 |
+
inputs=[task_id],
|
| 860 |
+
outputs=[tiered_input_tabs, selected_tab_state],
|
| 861 |
+
)
|
| 862 |
+
tiered_input_tabs.select(on_tab_select, inputs=None, outputs=[selected_tab_state])
|
| 863 |
+
|
| 864 |
+
reset_btn.click(
|
| 865 |
+
handle_reset,
|
| 866 |
+
inputs=[env_state, task_id],
|
| 867 |
+
outputs=[
|
| 868 |
+
env_state,
|
| 869 |
+
corpus_table,
|
| 870 |
+
policy_display,
|
| 871 |
+
dominant_features_table,
|
| 872 |
+
best_score_disp,
|
| 873 |
+
steps_left_disp,
|
| 874 |
+
episode_disp,
|
| 875 |
+
corpus_count_disp,
|
| 876 |
+
reward_outcome_disp,
|
| 877 |
+
session_history_table,
|
| 878 |
+
session_history_cues,
|
| 879 |
+
session_total_reward,
|
| 880 |
+
raw_json_box,
|
| 881 |
+
],
|
| 882 |
+
)
|
| 883 |
+
step_btn.click(
|
| 884 |
+
handle_step,
|
| 885 |
+
inputs=[
|
| 886 |
+
env_state,
|
| 887 |
+
task_id,
|
| 888 |
+
selected_tab_state,
|
| 889 |
+
easy_label_input,
|
| 890 |
+
easy_observation_input,
|
| 891 |
+
medium_label_input,
|
| 892 |
+
medium_primary_signal_input,
|
| 893 |
+
medium_conflicting_signal_input,
|
| 894 |
+
medium_reasoning_input,
|
| 895 |
+
hard_label_input,
|
| 896 |
+
hard_tradeoff_summary_input,
|
| 897 |
+
hard_confidence_justification_input,
|
| 898 |
+
hard_confidence_input,
|
| 899 |
+
clip_id_input,
|
| 900 |
+
],
|
| 901 |
+
outputs=[
|
| 902 |
+
env_state,
|
| 903 |
+
corpus_table,
|
| 904 |
+
policy_display,
|
| 905 |
+
dominant_features_table,
|
| 906 |
+
best_score_disp,
|
| 907 |
+
steps_left_disp,
|
| 908 |
+
episode_disp,
|
| 909 |
+
corpus_count_disp,
|
| 910 |
+
reward_outcome_disp,
|
| 911 |
+
session_history_table,
|
| 912 |
+
session_history_cues,
|
| 913 |
+
session_total_reward,
|
| 914 |
+
raw_json_box,
|
| 915 |
+
],
|
| 916 |
+
)
|
| 917 |
+
hint_btn.click(
|
| 918 |
+
handle_quality_hint,
|
| 919 |
+
inputs=[
|
| 920 |
+
env_state,
|
| 921 |
+
task_id,
|
| 922 |
+
selected_tab_state,
|
| 923 |
+
easy_observation_input,
|
| 924 |
+
medium_reasoning_input,
|
| 925 |
+
hard_tradeoff_summary_input,
|
| 926 |
+
],
|
| 927 |
+
outputs=[
|
| 928 |
+
env_state,
|
| 929 |
+
easy_observation_input,
|
| 930 |
+
medium_reasoning_input,
|
| 931 |
+
hard_tradeoff_summary_input,
|
| 932 |
+
],
|
| 933 |
+
)
|
| 934 |
+
baseline_run_btn.click(
|
| 935 |
+
_start_baseline_ui_run,
|
| 936 |
+
inputs=[task_id],
|
| 937 |
+
outputs=[baseline_run_id_state, baseline_status_disp, baseline_result_md, baseline_run_btn, baseline_poll_timer],
|
| 938 |
+
)
|
| 939 |
+
baseline_poll_timer.tick(
|
| 940 |
+
_poll_baseline_ui_run,
|
| 941 |
+
inputs=[baseline_run_id_state],
|
| 942 |
+
outputs=[baseline_run_id_state, baseline_status_disp, baseline_result_md, baseline_run_btn, baseline_poll_timer],
|
| 943 |
+
)
|
| 944 |
+
|
| 945 |
+
return demo
|
| 946 |
+
|
| 947 |
+
|
| 948 |
+
custom_demo = build_custom_ui()
|
| 949 |
+
app = gr.mount_gradio_app(app, custom_demo, path="/dashboard/")
|
| 950 |
+
|
| 951 |
+
|
| 952 |
+
def main(host: str = "0.0.0.0", port: int = 8000) -> None:
|
| 953 |
+
uvicorn.run("server.app:app", host=host, port=port, reload=False)
|
| 954 |
+
|
| 955 |
+
|
| 956 |
+
if __name__ == "__main__":
|
| 957 |
+
parser = argparse.ArgumentParser()
|
| 958 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 959 |
+
args = parser.parse_args()
|
| 960 |
+
if args.port == 8000:
|
| 961 |
+
main()
|
| 962 |
+
else:
|
| 963 |
+
main(port=args.port)
|
server/baseline_runs.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Thread-safe in-memory tracker for async baseline runs."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import threading
|
| 5 |
+
import time
|
| 6 |
+
import uuid
|
| 7 |
+
from copy import deepcopy
|
| 8 |
+
from typing import Any, Callable, Literal, TypedDict
|
| 9 |
+
|
| 10 |
+
RunStatus = Literal["running", "completed", "failed"]
|
| 11 |
+
DEFAULT_RUN_TTL_SECONDS = 10 * 60
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class BaselineRun(TypedDict):
|
| 15 |
+
run_id: str
|
| 16 |
+
status: RunStatus
|
| 17 |
+
created_at: float
|
| 18 |
+
updated_at: float
|
| 19 |
+
expires_at: float
|
| 20 |
+
started_at: float
|
| 21 |
+
completed_at: float | None
|
| 22 |
+
failed_at: float | None
|
| 23 |
+
partial: Any
|
| 24 |
+
result: Any
|
| 25 |
+
error: Any
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class BaselineRunTracker:
|
| 29 |
+
"""Manage async baseline runs in-memory with TTL-based cleanup."""
|
| 30 |
+
|
| 31 |
+
def __init__(
|
| 32 |
+
self,
|
| 33 |
+
ttl_seconds: float = DEFAULT_RUN_TTL_SECONDS,
|
| 34 |
+
time_fn: Callable[[], float] | None = None,
|
| 35 |
+
) -> None:
|
| 36 |
+
if ttl_seconds <= 0:
|
| 37 |
+
raise ValueError("ttl_seconds must be > 0")
|
| 38 |
+
self._ttl_seconds = float(ttl_seconds)
|
| 39 |
+
self._time_fn = time_fn or time.time
|
| 40 |
+
self._lock = threading.RLock()
|
| 41 |
+
self._runs: dict[str, BaselineRun] = {}
|
| 42 |
+
|
| 43 |
+
def _now(self) -> float:
|
| 44 |
+
return float(self._time_fn())
|
| 45 |
+
|
| 46 |
+
def _is_expired(self, run: BaselineRun, now: float) -> bool:
|
| 47 |
+
return float(run["expires_at"]) <= now
|
| 48 |
+
|
| 49 |
+
def _touch(self, run: BaselineRun, now: float) -> None:
|
| 50 |
+
run["updated_at"] = now
|
| 51 |
+
run["expires_at"] = now + self._ttl_seconds
|
| 52 |
+
|
| 53 |
+
def _get_for_update(self, run_id: str, now: float) -> BaselineRun:
|
| 54 |
+
run = self._runs.get(run_id)
|
| 55 |
+
if run is None:
|
| 56 |
+
raise KeyError(f"Unknown run_id: {run_id}")
|
| 57 |
+
if self._is_expired(run, now):
|
| 58 |
+
del self._runs[run_id]
|
| 59 |
+
raise KeyError(f"Expired run_id: {run_id}")
|
| 60 |
+
return run
|
| 61 |
+
|
| 62 |
+
def _merge_partial(self, run: BaselineRun, payload: Any) -> None:
|
| 63 |
+
current = run.get("partial")
|
| 64 |
+
if isinstance(current, dict) and isinstance(payload, dict):
|
| 65 |
+
merged = deepcopy(current)
|
| 66 |
+
merged.update(deepcopy(payload))
|
| 67 |
+
run["partial"] = merged
|
| 68 |
+
return
|
| 69 |
+
run["partial"] = deepcopy(payload)
|
| 70 |
+
|
| 71 |
+
def create_run(self) -> str:
|
| 72 |
+
with self._lock:
|
| 73 |
+
now = self._now()
|
| 74 |
+
run_id = uuid.uuid4().hex
|
| 75 |
+
while run_id in self._runs:
|
| 76 |
+
run_id = uuid.uuid4().hex
|
| 77 |
+
self._runs[run_id] = BaselineRun(
|
| 78 |
+
run_id=run_id,
|
| 79 |
+
status="running",
|
| 80 |
+
created_at=now,
|
| 81 |
+
updated_at=now,
|
| 82 |
+
expires_at=now + self._ttl_seconds,
|
| 83 |
+
started_at=now,
|
| 84 |
+
completed_at=None,
|
| 85 |
+
failed_at=None,
|
| 86 |
+
partial={},
|
| 87 |
+
result=None,
|
| 88 |
+
error=None,
|
| 89 |
+
)
|
| 90 |
+
return run_id
|
| 91 |
+
|
| 92 |
+
def mark_running(self, run_id: str, payload: Any | None = None) -> BaselineRun:
|
| 93 |
+
with self._lock:
|
| 94 |
+
now = self._now()
|
| 95 |
+
run = self._get_for_update(run_id, now)
|
| 96 |
+
run["status"] = "running"
|
| 97 |
+
run["result"] = None
|
| 98 |
+
run["error"] = None
|
| 99 |
+
run["completed_at"] = None
|
| 100 |
+
run["failed_at"] = None
|
| 101 |
+
if payload is not None:
|
| 102 |
+
self._merge_partial(run, payload)
|
| 103 |
+
self._touch(run, now)
|
| 104 |
+
return deepcopy(run)
|
| 105 |
+
|
| 106 |
+
def update_partial(self, run_id: str, payload: Any) -> BaselineRun:
|
| 107 |
+
return self.mark_running(run_id, payload=payload)
|
| 108 |
+
|
| 109 |
+
def mark_complete(self, run_id: str, result: Any) -> BaselineRun:
|
| 110 |
+
with self._lock:
|
| 111 |
+
now = self._now()
|
| 112 |
+
run = self._get_for_update(run_id, now)
|
| 113 |
+
run["status"] = "completed"
|
| 114 |
+
run["result"] = deepcopy(result)
|
| 115 |
+
run["error"] = None
|
| 116 |
+
run["completed_at"] = now
|
| 117 |
+
run["failed_at"] = None
|
| 118 |
+
self._touch(run, now)
|
| 119 |
+
return deepcopy(run)
|
| 120 |
+
|
| 121 |
+
def mark_failed(self, run_id: str, error: Any) -> BaselineRun:
|
| 122 |
+
with self._lock:
|
| 123 |
+
now = self._now()
|
| 124 |
+
run = self._get_for_update(run_id, now)
|
| 125 |
+
run["status"] = "failed"
|
| 126 |
+
run["result"] = None
|
| 127 |
+
run["error"] = deepcopy(error)
|
| 128 |
+
run["completed_at"] = None
|
| 129 |
+
run["failed_at"] = now
|
| 130 |
+
self._touch(run, now)
|
| 131 |
+
return deepcopy(run)
|
| 132 |
+
|
| 133 |
+
def get_run(self, run_id: str) -> BaselineRun | None:
|
| 134 |
+
with self._lock:
|
| 135 |
+
now = self._now()
|
| 136 |
+
run = self._runs.get(run_id)
|
| 137 |
+
if run is None:
|
| 138 |
+
return None
|
| 139 |
+
if self._is_expired(run, now):
|
| 140 |
+
del self._runs[run_id]
|
| 141 |
+
return None
|
| 142 |
+
return deepcopy(run)
|
| 143 |
+
|
| 144 |
+
def cleanup_expired(self) -> int:
|
| 145 |
+
with self._lock:
|
| 146 |
+
now = self._now()
|
| 147 |
+
expired_ids = [run_id for run_id, run in self._runs.items() if self._is_expired(run, now)]
|
| 148 |
+
for run_id in expired_ids:
|
| 149 |
+
del self._runs[run_id]
|
| 150 |
+
return len(expired_ids)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
baseline_run_tracker = BaselineRunTracker()
|
| 154 |
+
|
| 155 |
+
__all__ = [
|
| 156 |
+
"BaselineRun",
|
| 157 |
+
"BaselineRunTracker",
|
| 158 |
+
"DEFAULT_RUN_TTL_SECONDS",
|
| 159 |
+
"baseline_run_tracker",
|
| 160 |
+
]
|
server/clip_quality_environment.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from clip_quality_env.env import ClipQualityEnvironment as BaseClipQualityEnvironment
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ClipQualityEnvironment(BaseClipQualityEnvironment):
|
| 7 |
+
"""Server runtime wrapper preserving expected import path."""
|
| 8 |
+
|
| 9 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
server/environment.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from clip_quality_env.env import ClipQualityEnvironment
|
| 4 |
+
|
| 5 |
+
# Backward-compatibility alias (deprecated).
|
| 6 |
+
PolicyEvolverEnvironment = ClipQualityEnvironment
|
| 7 |
+
|
| 8 |
+
__all__ = ["ClipQualityEnvironment"]
|
server/grader.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic clip-quality grader used by `/grader`."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import re
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from clip_quality_env.difficulty import calibrate_total_score, normalize_difficulty
|
| 8 |
+
from clip_quality_env.grader import grade as clip_grade
|
| 9 |
+
from clip_quality_env.ground_truth import GTStore
|
| 10 |
+
from clip_quality_env.models import Action
|
| 11 |
+
from clip_quality_env.rubric import RubricState
|
| 12 |
+
from server.tasks import TASK_REGISTRY
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
_RUBRIC = RubricState()
|
| 16 |
+
_GT = GTStore()
|
| 17 |
+
_VALID_LABELS = {"KEEP", "BORDERLINE", "REJECT"}
|
| 18 |
+
_TEXT_TOKEN_RE = re.compile(r"[a-z0-9_]+")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _clamp01(value: float) -> float:
|
| 22 |
+
return max(0.0, min(1.0, float(value)))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _resolve_clip(action_dict: dict[str, Any], task_id: str) -> dict[str, Any]:
|
| 26 |
+
task = TASK_REGISTRY.get(task_id, {})
|
| 27 |
+
corpus = task.get("data_corpus", [])
|
| 28 |
+
if not corpus:
|
| 29 |
+
return {}
|
| 30 |
+
|
| 31 |
+
requested_clip_id = action_dict.get("clip_id")
|
| 32 |
+
if requested_clip_id:
|
| 33 |
+
for clip in corpus:
|
| 34 |
+
if str(clip.get("clip_id", "")) == str(requested_clip_id):
|
| 35 |
+
return dict(clip)
|
| 36 |
+
return dict(corpus[0])
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _normalize_label(raw: Any, fallback: str = "BORDERLINE") -> str:
|
| 40 |
+
label = str(raw or fallback).strip().upper()
|
| 41 |
+
return label if label in _VALID_LABELS else fallback
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _normalize_confidence(raw: Any) -> float:
|
| 45 |
+
try:
|
| 46 |
+
return _clamp01(float(raw))
|
| 47 |
+
except (TypeError, ValueError):
|
| 48 |
+
return 0.5
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _normalize_reasoning(action_dict: dict[str, Any]) -> str:
|
| 52 |
+
text_fields = [
|
| 53 |
+
action_dict.get("reasoning"),
|
| 54 |
+
action_dict.get("justification"),
|
| 55 |
+
action_dict.get("suggested_definition"),
|
| 56 |
+
action_dict.get("new_rule"),
|
| 57 |
+
action_dict.get("think"),
|
| 58 |
+
]
|
| 59 |
+
parts = [str(part).strip() for part in text_fields if str(part or "").strip()]
|
| 60 |
+
if parts:
|
| 61 |
+
return " ".join(parts)
|
| 62 |
+
return "Reasoning references face_confidence, motion_score, audio_snr_db, and lighting_uniformity."
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _normalize_action(action_dict: dict[str, Any], clip: dict[str, Any]) -> Action:
|
| 66 |
+
clip_id = str(clip.get("clip_id", ""))
|
| 67 |
+
fallback_label = _normalize_label(_GT.lookup(clip_id), fallback="")
|
| 68 |
+
if fallback_label not in _VALID_LABELS:
|
| 69 |
+
fallback_label = _normalize_label(_RUBRIC.derive_label(clip), fallback="BORDERLINE")
|
| 70 |
+
payload = {
|
| 71 |
+
"label": _normalize_label(
|
| 72 |
+
action_dict.get("label")
|
| 73 |
+
or action_dict.get("predicted_label")
|
| 74 |
+
or action_dict.get("decision")
|
| 75 |
+
or action_dict.get("review_status"),
|
| 76 |
+
fallback=fallback_label,
|
| 77 |
+
),
|
| 78 |
+
"reasoning": _normalize_reasoning(action_dict),
|
| 79 |
+
"confidence": _normalize_confidence(action_dict.get("confidence", 0.5)),
|
| 80 |
+
"clip_id": str(action_dict.get("clip_id") or clip.get("clip_id") or ""),
|
| 81 |
+
}
|
| 82 |
+
return Action.model_validate(payload)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _mentions_cue(reasoning: str, cue: str) -> bool:
|
| 86 |
+
text_tokens = set(_TEXT_TOKEN_RE.findall(reasoning.lower()))
|
| 87 |
+
cue_tokens = [token for token in _TEXT_TOKEN_RE.findall(cue.lower()) if len(token) >= 4]
|
| 88 |
+
if not cue_tokens:
|
| 89 |
+
return False
|
| 90 |
+
overlap = sum(1 for token in cue_tokens if token in text_tokens)
|
| 91 |
+
needed = 2 if len(cue_tokens) >= 2 else 1
|
| 92 |
+
return overlap >= needed
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _cue_bonus(reasoning: str, clip: dict[str, Any]) -> float:
|
| 96 |
+
cues = clip.get("quality_cues")
|
| 97 |
+
if not isinstance(cues, list):
|
| 98 |
+
return 0.0
|
| 99 |
+
valid_cues = [str(cue).strip() for cue in cues if str(cue).strip()]
|
| 100 |
+
if not valid_cues:
|
| 101 |
+
return 0.0
|
| 102 |
+
hits = sum(1 for cue in valid_cues if _mentions_cue(reasoning, cue))
|
| 103 |
+
if hits >= 2:
|
| 104 |
+
return 0.10
|
| 105 |
+
if hits == 1:
|
| 106 |
+
return 0.05
|
| 107 |
+
return 0.0
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def grade(action_dict: dict[str, Any], task_id: str, temperature: float = 0.0, seed: int = 42) -> float:
|
| 111 |
+
del temperature, seed
|
| 112 |
+
if task_id not in TASK_REGISTRY:
|
| 113 |
+
return 0.0
|
| 114 |
+
try:
|
| 115 |
+
clip = _resolve_clip(action_dict, task_id)
|
| 116 |
+
if not clip:
|
| 117 |
+
return 0.0
|
| 118 |
+
|
| 119 |
+
action = _normalize_action(action_dict, clip)
|
| 120 |
+
reward = clip_grade(action, clip, _RUBRIC, _GT)
|
| 121 |
+
|
| 122 |
+
format_score = float(reward.format_score)
|
| 123 |
+
label_score = float(reward.label_score)
|
| 124 |
+
reasoning_score = float(reward.reasoning_score)
|
| 125 |
+
|
| 126 |
+
reasoning_score = min(0.30, reasoning_score + _cue_bonus(str(action.reasoning), clip))
|
| 127 |
+
task = TASK_REGISTRY.get(task_id, {})
|
| 128 |
+
difficulty = normalize_difficulty(str(task.get("difficulty", "")))
|
| 129 |
+
total = _clamp01(format_score + label_score + reasoning_score)
|
| 130 |
+
total = calibrate_total_score(total, difficulty=difficulty)
|
| 131 |
+
return round(total, 4)
|
| 132 |
+
except Exception:
|
| 133 |
+
return 0.0
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core>=0.2.3
|
| 2 |
+
fastapi>=0.104.0
|
| 3 |
+
uvicorn>=0.24.0
|
| 4 |
+
pydantic>=2.0.0
|
| 5 |
+
requests>=2.25.0
|
| 6 |
+
websockets>=12.0
|
| 7 |
+
gradio>=4.0.0
|
| 8 |
+
pandas>=2.0.0
|
server/tasks/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .task_easy import EASY_TASK
|
| 2 |
+
from .task_medium import MEDIUM_TASK
|
| 3 |
+
from .task_hard import HARD_TASK
|
| 4 |
+
|
| 5 |
+
TASK_REGISTRY = {
|
| 6 |
+
"task_easy": EASY_TASK,
|
| 7 |
+
"task_medium": MEDIUM_TASK,
|
| 8 |
+
"task_hard": HARD_TASK,
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
TASK_IDS = tuple(TASK_REGISTRY.keys())
|
| 12 |
+
|
| 13 |
+
__all__ = ["EASY_TASK", "MEDIUM_TASK", "HARD_TASK", "TASK_REGISTRY", "TASK_IDS"]
|
server/tasks/task_easy.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
EASY_TASK = {
|
| 2 |
+
"task_id": "task_easy",
|
| 3 |
+
"difficulty": "easy",
|
| 4 |
+
"description": (
|
| 5 |
+
"Classify clips with dominant quality evidence. "
|
| 6 |
+
"Return one label in {KEEP, BORDERLINE, REJECT} with brief metadata-grounded reasoning."
|
| 7 |
+
),
|
| 8 |
+
"data_corpus": [
|
| 9 |
+
{
|
| 10 |
+
"id": "easy_001",
|
| 11 |
+
"clip_id": "clip_0001",
|
| 12 |
+
"duration_s": 8.4,
|
| 13 |
+
"fps": 25,
|
| 14 |
+
"resolution": "1280x720",
|
| 15 |
+
"face_area_ratio": 0.43,
|
| 16 |
+
"face_confidence": 0.94,
|
| 17 |
+
"head_pose_yaw_deg": 6.2,
|
| 18 |
+
"head_pose_pitch_deg": 4.3,
|
| 19 |
+
"motion_score": 0.09,
|
| 20 |
+
"bg_complexity": "solid_dark",
|
| 21 |
+
"bg_complexity_score": 0.05,
|
| 22 |
+
"mouth_open_ratio": 0.36,
|
| 23 |
+
"blink_rate_hz": 0.22,
|
| 24 |
+
"audio_snr_db": 27.4,
|
| 25 |
+
"transcript_word_count": 28,
|
| 26 |
+
"transcript_confidence": 0.89,
|
| 27 |
+
"lighting_uniformity": 0.88,
|
| 28 |
+
"occlusion_present": False,
|
| 29 |
+
"environment_tag": "studio",
|
| 30 |
+
"framing": "front",
|
| 31 |
+
"expected_label": "KEEP",
|
| 32 |
+
"quality_cues": [
|
| 33 |
+
"high face_confidence and large face_area_ratio",
|
| 34 |
+
"low motion_score and clean audio_snr_db",
|
| 35 |
+
"uniform lighting with no occlusion",
|
| 36 |
+
],
|
| 37 |
+
"review_status": "pending",
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"id": "easy_002",
|
| 41 |
+
"clip_id": "clip_0004",
|
| 42 |
+
"duration_s": 7.1,
|
| 43 |
+
"fps": 25,
|
| 44 |
+
"resolution": "1280x720",
|
| 45 |
+
"face_area_ratio": 0.38,
|
| 46 |
+
"face_confidence": 0.90,
|
| 47 |
+
"head_pose_yaw_deg": 7.8,
|
| 48 |
+
"head_pose_pitch_deg": 5.0,
|
| 49 |
+
"motion_score": 0.12,
|
| 50 |
+
"bg_complexity": "simple_room",
|
| 51 |
+
"bg_complexity_score": 0.09,
|
| 52 |
+
"mouth_open_ratio": 0.33,
|
| 53 |
+
"blink_rate_hz": 0.21,
|
| 54 |
+
"audio_snr_db": 24.1,
|
| 55 |
+
"transcript_word_count": 24,
|
| 56 |
+
"transcript_confidence": 0.86,
|
| 57 |
+
"lighting_uniformity": 0.80,
|
| 58 |
+
"occlusion_present": False,
|
| 59 |
+
"environment_tag": "office",
|
| 60 |
+
"framing": "front",
|
| 61 |
+
"expected_label": "KEEP",
|
| 62 |
+
"quality_cues": [
|
| 63 |
+
"face_confidence is high",
|
| 64 |
+
"motion_score is low",
|
| 65 |
+
"audio_snr_db is comfortably above keep threshold",
|
| 66 |
+
],
|
| 67 |
+
"review_status": "pending",
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"id": "easy_003",
|
| 71 |
+
"clip_id": "clip_0007",
|
| 72 |
+
"duration_s": 8.0,
|
| 73 |
+
"fps": 25,
|
| 74 |
+
"resolution": "1280x720",
|
| 75 |
+
"face_area_ratio": 0.25,
|
| 76 |
+
"face_confidence": 0.80,
|
| 77 |
+
"head_pose_yaw_deg": 17.5,
|
| 78 |
+
"head_pose_pitch_deg": 8.2,
|
| 79 |
+
"motion_score": 0.26,
|
| 80 |
+
"bg_complexity": "simple_room",
|
| 81 |
+
"bg_complexity_score": 0.16,
|
| 82 |
+
"mouth_open_ratio": 0.25,
|
| 83 |
+
"blink_rate_hz": 0.29,
|
| 84 |
+
"audio_snr_db": 19.4,
|
| 85 |
+
"transcript_word_count": 19,
|
| 86 |
+
"transcript_confidence": 0.76,
|
| 87 |
+
"lighting_uniformity": 0.65,
|
| 88 |
+
"occlusion_present": False,
|
| 89 |
+
"environment_tag": "home_office",
|
| 90 |
+
"framing": "front",
|
| 91 |
+
"expected_label": "BORDERLINE",
|
| 92 |
+
"quality_cues": [
|
| 93 |
+
"face_area_ratio sits on the keep boundary",
|
| 94 |
+
"audio_snr_db and motion_score are borderline",
|
| 95 |
+
],
|
| 96 |
+
"review_status": "pending",
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "easy_004",
|
| 100 |
+
"clip_id": "clip_0014",
|
| 101 |
+
"duration_s": 3.4,
|
| 102 |
+
"fps": 25,
|
| 103 |
+
"resolution": "1280x720",
|
| 104 |
+
"face_area_ratio": 0.16,
|
| 105 |
+
"face_confidence": 0.56,
|
| 106 |
+
"head_pose_yaw_deg": 23.0,
|
| 107 |
+
"head_pose_pitch_deg": 11.2,
|
| 108 |
+
"motion_score": 0.54,
|
| 109 |
+
"bg_complexity": "busy_room",
|
| 110 |
+
"bg_complexity_score": 0.45,
|
| 111 |
+
"mouth_open_ratio": 0.15,
|
| 112 |
+
"blink_rate_hz": 0.38,
|
| 113 |
+
"audio_snr_db": 11.0,
|
| 114 |
+
"transcript_word_count": 8,
|
| 115 |
+
"transcript_confidence": 0.44,
|
| 116 |
+
"lighting_uniformity": 0.38,
|
| 117 |
+
"occlusion_present": True,
|
| 118 |
+
"environment_tag": "corridor",
|
| 119 |
+
"framing": "offgaze",
|
| 120 |
+
"expected_label": "REJECT",
|
| 121 |
+
"quality_cues": [
|
| 122 |
+
"occlusion_present is true",
|
| 123 |
+
"face_confidence is too low and motion_score is too high",
|
| 124 |
+
"audio_snr_db and lighting_uniformity are below reject thresholds",
|
| 125 |
+
],
|
| 126 |
+
"review_status": "pending",
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"id": "easy_005",
|
| 130 |
+
"clip_id": "clip_0016",
|
| 131 |
+
"duration_s": 5.0,
|
| 132 |
+
"fps": 25,
|
| 133 |
+
"resolution": "1280x720",
|
| 134 |
+
"face_area_ratio": 0.18,
|
| 135 |
+
"face_confidence": 0.63,
|
| 136 |
+
"head_pose_yaw_deg": 25.5,
|
| 137 |
+
"head_pose_pitch_deg": 12.4,
|
| 138 |
+
"motion_score": 0.47,
|
| 139 |
+
"bg_complexity": "busy_indoor",
|
| 140 |
+
"bg_complexity_score": 0.42,
|
| 141 |
+
"mouth_open_ratio": 0.17,
|
| 142 |
+
"blink_rate_hz": 0.35,
|
| 143 |
+
"audio_snr_db": 13.1,
|
| 144 |
+
"transcript_word_count": 10,
|
| 145 |
+
"transcript_confidence": 0.51,
|
| 146 |
+
"lighting_uniformity": 0.42,
|
| 147 |
+
"occlusion_present": False,
|
| 148 |
+
"environment_tag": "street_interview",
|
| 149 |
+
"framing": "left",
|
| 150 |
+
"expected_label": "REJECT",
|
| 151 |
+
"quality_cues": [
|
| 152 |
+
"face_confidence is below keep range",
|
| 153 |
+
"motion_score is reject-level",
|
| 154 |
+
"audio_snr_db is noisy and lighting is poor",
|
| 155 |
+
],
|
| 156 |
+
"review_status": "pending",
|
| 157 |
+
},
|
| 158 |
+
],
|
| 159 |
+
}
|
server/tasks/task_hard.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
HARD_TASK = {
|
| 2 |
+
"task_id": "task_hard",
|
| 3 |
+
"difficulty": "hard",
|
| 4 |
+
"description": (
|
| 5 |
+
"Classify difficult clips with conflicting quality cues. "
|
| 6 |
+
"Reasoning should explain trade-offs across visual and audio metadata."
|
| 7 |
+
),
|
| 8 |
+
"data_corpus": [
|
| 9 |
+
{
|
| 10 |
+
"id": "hard_001",
|
| 11 |
+
"clip_id": "clip_0012",
|
| 12 |
+
"duration_s": 8.6,
|
| 13 |
+
"fps": 25,
|
| 14 |
+
"resolution": "1280x720",
|
| 15 |
+
"face_area_ratio": 0.31,
|
| 16 |
+
"face_confidence": 0.88,
|
| 17 |
+
"head_pose_yaw_deg": 15.5,
|
| 18 |
+
"head_pose_pitch_deg": 9.0,
|
| 19 |
+
"motion_score": 0.43,
|
| 20 |
+
"bg_complexity": "busy_room",
|
| 21 |
+
"bg_complexity_score": 0.34,
|
| 22 |
+
"mouth_open_ratio": 0.31,
|
| 23 |
+
"blink_rate_hz": 0.33,
|
| 24 |
+
"audio_snr_db": 14.2,
|
| 25 |
+
"transcript_word_count": 26,
|
| 26 |
+
"transcript_confidence": 0.79,
|
| 27 |
+
"lighting_uniformity": 0.60,
|
| 28 |
+
"occlusion_present": False,
|
| 29 |
+
"environment_tag": "event_hall",
|
| 30 |
+
"framing": "front",
|
| 31 |
+
"expected_label": "BORDERLINE",
|
| 32 |
+
"quality_cues": [
|
| 33 |
+
"strong face_confidence conflicts with noisy audio_snr_db",
|
| 34 |
+
"motion_score is near reject but not over threshold",
|
| 35 |
+
"lighting is acceptable but not robust",
|
| 36 |
+
],
|
| 37 |
+
"review_status": "pending",
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"id": "hard_002",
|
| 41 |
+
"clip_id": "clip_0013",
|
| 42 |
+
"duration_s": 6.9,
|
| 43 |
+
"fps": 25,
|
| 44 |
+
"resolution": "1280x720",
|
| 45 |
+
"face_area_ratio": 0.29,
|
| 46 |
+
"face_confidence": 0.73,
|
| 47 |
+
"head_pose_yaw_deg": 21.8,
|
| 48 |
+
"head_pose_pitch_deg": 11.0,
|
| 49 |
+
"motion_score": 0.46,
|
| 50 |
+
"bg_complexity": "busy_room",
|
| 51 |
+
"bg_complexity_score": 0.38,
|
| 52 |
+
"mouth_open_ratio": 0.22,
|
| 53 |
+
"blink_rate_hz": 0.36,
|
| 54 |
+
"audio_snr_db": 16.6,
|
| 55 |
+
"transcript_word_count": 18,
|
| 56 |
+
"transcript_confidence": 0.71,
|
| 57 |
+
"lighting_uniformity": 0.49,
|
| 58 |
+
"occlusion_present": False,
|
| 59 |
+
"environment_tag": "street_interview",
|
| 60 |
+
"framing": "offgaze",
|
| 61 |
+
"expected_label": "BORDERLINE",
|
| 62 |
+
"quality_cues": [
|
| 63 |
+
"motion_score is high but audio_snr_db remains salvageable",
|
| 64 |
+
"face_confidence and lighting_uniformity are weak",
|
| 65 |
+
"multiple borderline cues create mixed confidence",
|
| 66 |
+
],
|
| 67 |
+
"review_status": "pending",
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"id": "hard_003",
|
| 71 |
+
"clip_id": "clip_0017",
|
| 72 |
+
"duration_s": 5.6,
|
| 73 |
+
"fps": 25,
|
| 74 |
+
"resolution": "1280x720",
|
| 75 |
+
"face_area_ratio": 0.22,
|
| 76 |
+
"face_confidence": 0.66,
|
| 77 |
+
"head_pose_yaw_deg": 18.2,
|
| 78 |
+
"head_pose_pitch_deg": 9.4,
|
| 79 |
+
"motion_score": 0.33,
|
| 80 |
+
"bg_complexity": "simple_room",
|
| 81 |
+
"bg_complexity_score": 0.20,
|
| 82 |
+
"mouth_open_ratio": 0.27,
|
| 83 |
+
"blink_rate_hz": 0.32,
|
| 84 |
+
"audio_snr_db": 20.2,
|
| 85 |
+
"transcript_word_count": 19,
|
| 86 |
+
"transcript_confidence": 0.79,
|
| 87 |
+
"lighting_uniformity": 0.57,
|
| 88 |
+
"occlusion_present": False,
|
| 89 |
+
"environment_tag": "home_office",
|
| 90 |
+
"framing": "left",
|
| 91 |
+
"expected_label": "REJECT",
|
| 92 |
+
"quality_cues": [
|
| 93 |
+
"face_confidence is near reject threshold",
|
| 94 |
+
"face_area_ratio is too small and motion is elevated",
|
| 95 |
+
"combined weak visual signals push toward reject",
|
| 96 |
+
],
|
| 97 |
+
"review_status": "pending",
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"id": "hard_004",
|
| 101 |
+
"clip_id": "clip_0018",
|
| 102 |
+
"duration_s": 11.8,
|
| 103 |
+
"fps": 25,
|
| 104 |
+
"resolution": "1280x720",
|
| 105 |
+
"face_area_ratio": 0.18,
|
| 106 |
+
"face_confidence": 0.64,
|
| 107 |
+
"head_pose_yaw_deg": 24.4,
|
| 108 |
+
"head_pose_pitch_deg": 12.1,
|
| 109 |
+
"motion_score": 0.41,
|
| 110 |
+
"bg_complexity": "busy_indoor",
|
| 111 |
+
"bg_complexity_score": 0.41,
|
| 112 |
+
"mouth_open_ratio": 0.19,
|
| 113 |
+
"blink_rate_hz": 0.39,
|
| 114 |
+
"audio_snr_db": 13.9,
|
| 115 |
+
"transcript_word_count": 14,
|
| 116 |
+
"transcript_confidence": 0.66,
|
| 117 |
+
"lighting_uniformity": 0.44,
|
| 118 |
+
"occlusion_present": False,
|
| 119 |
+
"environment_tag": "conference_room",
|
| 120 |
+
"framing": "offgaze",
|
| 121 |
+
"expected_label": "REJECT",
|
| 122 |
+
"quality_cues": [
|
| 123 |
+
"face_confidence is below reject ceiling",
|
| 124 |
+
"audio_snr_db is noisy and lighting is poor",
|
| 125 |
+
"duration_s long but quality remains low",
|
| 126 |
+
],
|
| 127 |
+
"review_status": "pending",
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"id": "hard_005",
|
| 131 |
+
"clip_id": "clip_0006",
|
| 132 |
+
"duration_s": 7.7,
|
| 133 |
+
"fps": 25,
|
| 134 |
+
"resolution": "1280x720",
|
| 135 |
+
"face_area_ratio": 0.30,
|
| 136 |
+
"face_confidence": 0.84,
|
| 137 |
+
"head_pose_yaw_deg": 10.3,
|
| 138 |
+
"head_pose_pitch_deg": 6.2,
|
| 139 |
+
"motion_score": 0.25,
|
| 140 |
+
"bg_complexity": "simple_room",
|
| 141 |
+
"bg_complexity_score": 0.13,
|
| 142 |
+
"mouth_open_ratio": 0.31,
|
| 143 |
+
"blink_rate_hz": 0.24,
|
| 144 |
+
"audio_snr_db": 21.1,
|
| 145 |
+
"transcript_word_count": 24,
|
| 146 |
+
"transcript_confidence": 0.86,
|
| 147 |
+
"lighting_uniformity": 0.72,
|
| 148 |
+
"occlusion_present": False,
|
| 149 |
+
"environment_tag": "studio",
|
| 150 |
+
"framing": "front",
|
| 151 |
+
"expected_label": "KEEP",
|
| 152 |
+
"quality_cues": [
|
| 153 |
+
"face metrics are above keep threshold",
|
| 154 |
+
"audio_snr_db is clean and motion_score is controlled",
|
| 155 |
+
"consistent lighting and framing support keep decision",
|
| 156 |
+
],
|
| 157 |
+
"review_status": "pending",
|
| 158 |
+
},
|
| 159 |
+
],
|
| 160 |
+
}
|
server/tasks/task_medium.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MEDIUM_TASK = {
|
| 2 |
+
"task_id": "task_medium",
|
| 3 |
+
"difficulty": "medium",
|
| 4 |
+
"description": (
|
| 5 |
+
"Classify clips with mixed or borderline evidence. "
|
| 6 |
+
"Use metadata trade-offs to justify KEEP/BORDERLINE/REJECT."
|
| 7 |
+
),
|
| 8 |
+
"data_corpus": [
|
| 9 |
+
{
|
| 10 |
+
"id": "med_001",
|
| 11 |
+
"clip_id": "clip_0008",
|
| 12 |
+
"duration_s": 6.2,
|
| 13 |
+
"fps": 25,
|
| 14 |
+
"resolution": "1280x720",
|
| 15 |
+
"face_area_ratio": 0.24,
|
| 16 |
+
"face_confidence": 0.78,
|
| 17 |
+
"head_pose_yaw_deg": 14.4,
|
| 18 |
+
"head_pose_pitch_deg": 9.3,
|
| 19 |
+
"motion_score": 0.29,
|
| 20 |
+
"bg_complexity": "simple_room",
|
| 21 |
+
"bg_complexity_score": 0.18,
|
| 22 |
+
"mouth_open_ratio": 0.23,
|
| 23 |
+
"blink_rate_hz": 0.30,
|
| 24 |
+
"audio_snr_db": 18.5,
|
| 25 |
+
"transcript_word_count": 20,
|
| 26 |
+
"transcript_confidence": 0.75,
|
| 27 |
+
"lighting_uniformity": 0.62,
|
| 28 |
+
"occlusion_present": False,
|
| 29 |
+
"environment_tag": "office",
|
| 30 |
+
"framing": "left",
|
| 31 |
+
"expected_label": "BORDERLINE",
|
| 32 |
+
"quality_cues": [
|
| 33 |
+
"face_confidence is slightly below keep threshold",
|
| 34 |
+
"audio_snr_db and motion_score are borderline",
|
| 35 |
+
"no hard reject signal",
|
| 36 |
+
],
|
| 37 |
+
"review_status": "pending",
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"id": "med_002",
|
| 41 |
+
"clip_id": "clip_0009",
|
| 42 |
+
"duration_s": 10.6,
|
| 43 |
+
"fps": 25,
|
| 44 |
+
"resolution": "1280x720",
|
| 45 |
+
"face_area_ratio": 0.23,
|
| 46 |
+
"face_confidence": 0.77,
|
| 47 |
+
"head_pose_yaw_deg": 16.1,
|
| 48 |
+
"head_pose_pitch_deg": 9.8,
|
| 49 |
+
"motion_score": 0.31,
|
| 50 |
+
"bg_complexity": "simple_room",
|
| 51 |
+
"bg_complexity_score": 0.19,
|
| 52 |
+
"mouth_open_ratio": 0.24,
|
| 53 |
+
"blink_rate_hz": 0.31,
|
| 54 |
+
"audio_snr_db": 17.4,
|
| 55 |
+
"transcript_word_count": 23,
|
| 56 |
+
"transcript_confidence": 0.73,
|
| 57 |
+
"lighting_uniformity": 0.59,
|
| 58 |
+
"occlusion_present": False,
|
| 59 |
+
"environment_tag": "podcast_corner",
|
| 60 |
+
"framing": "front",
|
| 61 |
+
"expected_label": "BORDERLINE",
|
| 62 |
+
"quality_cues": [
|
| 63 |
+
"duration_s is near upper keep band edge",
|
| 64 |
+
"face_area_ratio is borderline small",
|
| 65 |
+
"audio_snr_db is weak but not catastrophic",
|
| 66 |
+
],
|
| 67 |
+
"review_status": "pending",
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"id": "med_003",
|
| 71 |
+
"clip_id": "clip_0010",
|
| 72 |
+
"duration_s": 7.4,
|
| 73 |
+
"fps": 25,
|
| 74 |
+
"resolution": "1280x720",
|
| 75 |
+
"face_area_ratio": 0.27,
|
| 76 |
+
"face_confidence": 0.82,
|
| 77 |
+
"head_pose_yaw_deg": 11.2,
|
| 78 |
+
"head_pose_pitch_deg": 7.7,
|
| 79 |
+
"motion_score": 0.24,
|
| 80 |
+
"bg_complexity": "office",
|
| 81 |
+
"bg_complexity_score": 0.14,
|
| 82 |
+
"mouth_open_ratio": 0.29,
|
| 83 |
+
"blink_rate_hz": 0.26,
|
| 84 |
+
"audio_snr_db": 20.1,
|
| 85 |
+
"transcript_word_count": 22,
|
| 86 |
+
"transcript_confidence": 0.81,
|
| 87 |
+
"lighting_uniformity": 0.66,
|
| 88 |
+
"occlusion_present": False,
|
| 89 |
+
"environment_tag": "workstation",
|
| 90 |
+
"framing": "front",
|
| 91 |
+
"expected_label": "BORDERLINE",
|
| 92 |
+
"quality_cues": [
|
| 93 |
+
"face_confidence is good but face_area_ratio remains only moderate",
|
| 94 |
+
"motion_score and mouth_open_ratio are near boundary values",
|
| 95 |
+
],
|
| 96 |
+
"review_status": "pending",
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "med_004",
|
| 100 |
+
"clip_id": "clip_0011",
|
| 101 |
+
"duration_s": 4.3,
|
| 102 |
+
"fps": 25,
|
| 103 |
+
"resolution": "1280x720",
|
| 104 |
+
"face_area_ratio": 0.21,
|
| 105 |
+
"face_confidence": 0.69,
|
| 106 |
+
"head_pose_yaw_deg": 19.1,
|
| 107 |
+
"head_pose_pitch_deg": 10.6,
|
| 108 |
+
"motion_score": 0.39,
|
| 109 |
+
"bg_complexity": "busy_room",
|
| 110 |
+
"bg_complexity_score": 0.23,
|
| 111 |
+
"mouth_open_ratio": 0.21,
|
| 112 |
+
"blink_rate_hz": 0.34,
|
| 113 |
+
"audio_snr_db": 15.0,
|
| 114 |
+
"transcript_word_count": 13,
|
| 115 |
+
"transcript_confidence": 0.69,
|
| 116 |
+
"lighting_uniformity": 0.54,
|
| 117 |
+
"occlusion_present": False,
|
| 118 |
+
"environment_tag": "classroom",
|
| 119 |
+
"framing": "offgaze",
|
| 120 |
+
"expected_label": "BORDERLINE",
|
| 121 |
+
"quality_cues": [
|
| 122 |
+
"duration_s is barely acceptable",
|
| 123 |
+
"audio_snr_db and lighting_uniformity are borderline",
|
| 124 |
+
"motion_score is elevated but below hard reject cutoff",
|
| 125 |
+
],
|
| 126 |
+
"review_status": "pending",
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"id": "med_005",
|
| 130 |
+
"clip_id": "clip_0003",
|
| 131 |
+
"duration_s": 8.7,
|
| 132 |
+
"fps": 25,
|
| 133 |
+
"resolution": "1280x720",
|
| 134 |
+
"face_area_ratio": 0.36,
|
| 135 |
+
"face_confidence": 0.91,
|
| 136 |
+
"head_pose_yaw_deg": 8.1,
|
| 137 |
+
"head_pose_pitch_deg": 5.4,
|
| 138 |
+
"motion_score": 0.14,
|
| 139 |
+
"bg_complexity": "solid_dark",
|
| 140 |
+
"bg_complexity_score": 0.08,
|
| 141 |
+
"mouth_open_ratio": 0.34,
|
| 142 |
+
"blink_rate_hz": 0.24,
|
| 143 |
+
"audio_snr_db": 23.9,
|
| 144 |
+
"transcript_word_count": 29,
|
| 145 |
+
"transcript_confidence": 0.88,
|
| 146 |
+
"lighting_uniformity": 0.81,
|
| 147 |
+
"occlusion_present": False,
|
| 148 |
+
"environment_tag": "studio",
|
| 149 |
+
"framing": "front",
|
| 150 |
+
"expected_label": "KEEP",
|
| 151 |
+
"quality_cues": [
|
| 152 |
+
"strong face_confidence and face_area_ratio",
|
| 153 |
+
"clean audio and stable motion",
|
| 154 |
+
"well lit with no occlusion",
|
| 155 |
+
],
|
| 156 |
+
"review_status": "pending",
|
| 157 |
+
},
|
| 158 |
+
],
|
| 159 |
+
}
|
spaces_app.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
import gradio as gr
|
| 6 |
+
|
| 7 |
+
from models import Action
|
| 8 |
+
from server.environment import ClipQualityEnvironment
|
| 9 |
+
from server.tasks import TASK_REGISTRY
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _run_step(task_id: str, label: str, reasoning: str, confidence: float, clip_id: str) -> str:
|
| 13 |
+
env = ClipQualityEnvironment()
|
| 14 |
+
obs = env.reset(task_id=task_id)
|
| 15 |
+
payload = {
|
| 16 |
+
"label": label,
|
| 17 |
+
"reasoning": (reasoning or "No reasoning provided.").strip(),
|
| 18 |
+
"confidence": float(confidence),
|
| 19 |
+
}
|
| 20 |
+
if clip_id and clip_id.strip():
|
| 21 |
+
payload["clip_id"] = clip_id.strip()
|
| 22 |
+
|
| 23 |
+
next_obs = env.step(Action.model_validate(payload))
|
| 24 |
+
current_clip = next_obs.clip_metadata.clip_id
|
| 25 |
+
expected = next_obs.clip_metadata.expected_label
|
| 26 |
+
return (
|
| 27 |
+
f"Clip-quality task: {obs.task_id}\n"
|
| 28 |
+
f"Classified clip: {current_clip}\n"
|
| 29 |
+
f"Expected label: {expected}\n"
|
| 30 |
+
f"Step: {next_obs.step_count}\n"
|
| 31 |
+
f"Reward: {next_obs.reward:.4f}\n"
|
| 32 |
+
f"Done: {next_obs.done}\n"
|
| 33 |
+
f"Best quality score: {next_obs.info.get('best_score', 0.0):.4f}"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def build_demo() -> gr.Blocks:
|
| 38 |
+
with gr.Blocks(title="CLIP Quality Analyzer Dashboard") as demo:
|
| 39 |
+
gr.Markdown("# CLIP Quality Analyzer Dashboard")
|
| 40 |
+
gr.Markdown("Reference-style dashboard for clip-quality classification.")
|
| 41 |
+
|
| 42 |
+
task_id = gr.Dropdown(choices=list(TASK_REGISTRY.keys()), value="task_easy", label="Clip-Quality Scenario")
|
| 43 |
+
label = gr.Radio(
|
| 44 |
+
choices=["KEEP", "BORDERLINE", "REJECT"],
|
| 45 |
+
value="BORDERLINE",
|
| 46 |
+
label="Predicted Label",
|
| 47 |
+
)
|
| 48 |
+
reasoning = gr.Textbox(label="Reasoning (reference clip metadata)")
|
| 49 |
+
confidence = gr.Slider(minimum=0.0, maximum=1.0, value=0.5, step=0.01, label="Confidence")
|
| 50 |
+
clip_id = gr.Textbox(label="Clip ID override (optional)")
|
| 51 |
+
run_btn = gr.Button("Submit Classification Action")
|
| 52 |
+
output = gr.Textbox(label="Classification Result", lines=8)
|
| 53 |
+
run_btn.click(_run_step, inputs=[task_id, label, reasoning, confidence, clip_id], outputs=[output])
|
| 54 |
+
return demo
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _run_demo() -> None:
|
| 58 |
+
demo = build_demo()
|
| 59 |
+
demo.queue()
|
| 60 |
+
demo.launch(server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"), server_port=int(os.environ.get("PORT", "7860")))
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
_run_demo()
|
state/ground_truth.json
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"clip_0001": {
|
| 3 |
+
"confidence": null,
|
| 4 |
+
"episode": 0,
|
| 5 |
+
"label": "KEEP",
|
| 6 |
+
"reward": null,
|
| 7 |
+
"source": "seed"
|
| 8 |
+
},
|
| 9 |
+
"clip_0002": {
|
| 10 |
+
"confidence": null,
|
| 11 |
+
"episode": 0,
|
| 12 |
+
"label": "KEEP",
|
| 13 |
+
"reward": null,
|
| 14 |
+
"source": "seed"
|
| 15 |
+
},
|
| 16 |
+
"clip_0003": {
|
| 17 |
+
"confidence": null,
|
| 18 |
+
"episode": 0,
|
| 19 |
+
"label": "KEEP",
|
| 20 |
+
"reward": null,
|
| 21 |
+
"source": "seed"
|
| 22 |
+
},
|
| 23 |
+
"clip_0004": {
|
| 24 |
+
"confidence": null,
|
| 25 |
+
"episode": 0,
|
| 26 |
+
"label": "KEEP",
|
| 27 |
+
"reward": null,
|
| 28 |
+
"source": "seed"
|
| 29 |
+
},
|
| 30 |
+
"clip_0005": {
|
| 31 |
+
"confidence": null,
|
| 32 |
+
"episode": 0,
|
| 33 |
+
"label": "KEEP",
|
| 34 |
+
"reward": null,
|
| 35 |
+
"source": "seed"
|
| 36 |
+
},
|
| 37 |
+
"clip_0006": {
|
| 38 |
+
"confidence": null,
|
| 39 |
+
"episode": 0,
|
| 40 |
+
"label": "KEEP",
|
| 41 |
+
"reward": null,
|
| 42 |
+
"source": "seed"
|
| 43 |
+
},
|
| 44 |
+
"clip_0007": {
|
| 45 |
+
"confidence": null,
|
| 46 |
+
"episode": 0,
|
| 47 |
+
"label": "BORDERLINE",
|
| 48 |
+
"reward": null,
|
| 49 |
+
"source": "seed"
|
| 50 |
+
},
|
| 51 |
+
"clip_0008": {
|
| 52 |
+
"confidence": null,
|
| 53 |
+
"episode": 0,
|
| 54 |
+
"label": "BORDERLINE",
|
| 55 |
+
"reward": null,
|
| 56 |
+
"source": "seed"
|
| 57 |
+
},
|
| 58 |
+
"clip_0009": {
|
| 59 |
+
"confidence": null,
|
| 60 |
+
"episode": 0,
|
| 61 |
+
"label": "BORDERLINE",
|
| 62 |
+
"reward": null,
|
| 63 |
+
"source": "seed"
|
| 64 |
+
},
|
| 65 |
+
"clip_0010": {
|
| 66 |
+
"confidence": null,
|
| 67 |
+
"episode": 0,
|
| 68 |
+
"label": "BORDERLINE",
|
| 69 |
+
"reward": null,
|
| 70 |
+
"source": "seed"
|
| 71 |
+
},
|
| 72 |
+
"clip_0011": {
|
| 73 |
+
"confidence": null,
|
| 74 |
+
"episode": 0,
|
| 75 |
+
"label": "BORDERLINE",
|
| 76 |
+
"reward": null,
|
| 77 |
+
"source": "seed"
|
| 78 |
+
},
|
| 79 |
+
"clip_0012": {
|
| 80 |
+
"confidence": null,
|
| 81 |
+
"episode": 0,
|
| 82 |
+
"label": "BORDERLINE",
|
| 83 |
+
"reward": null,
|
| 84 |
+
"source": "seed"
|
| 85 |
+
},
|
| 86 |
+
"clip_0013": {
|
| 87 |
+
"confidence": null,
|
| 88 |
+
"episode": 0,
|
| 89 |
+
"label": "BORDERLINE",
|
| 90 |
+
"reward": null,
|
| 91 |
+
"source": "seed"
|
| 92 |
+
},
|
| 93 |
+
"clip_0014": {
|
| 94 |
+
"confidence": null,
|
| 95 |
+
"episode": 0,
|
| 96 |
+
"label": "REJECT",
|
| 97 |
+
"reward": null,
|
| 98 |
+
"source": "seed"
|
| 99 |
+
},
|
| 100 |
+
"clip_0015": {
|
| 101 |
+
"confidence": null,
|
| 102 |
+
"episode": 0,
|
| 103 |
+
"label": "REJECT",
|
| 104 |
+
"reward": null,
|
| 105 |
+
"source": "seed"
|
| 106 |
+
},
|
| 107 |
+
"clip_0016": {
|
| 108 |
+
"confidence": null,
|
| 109 |
+
"episode": 0,
|
| 110 |
+
"label": "REJECT",
|
| 111 |
+
"reward": null,
|
| 112 |
+
"source": "seed"
|
| 113 |
+
},
|
| 114 |
+
"clip_0017": {
|
| 115 |
+
"confidence": null,
|
| 116 |
+
"episode": 0,
|
| 117 |
+
"label": "REJECT",
|
| 118 |
+
"reward": null,
|
| 119 |
+
"source": "seed"
|
| 120 |
+
},
|
| 121 |
+
"clip_0018": {
|
| 122 |
+
"confidence": null,
|
| 123 |
+
"episode": 0,
|
| 124 |
+
"label": "REJECT",
|
| 125 |
+
"reward": null,
|
| 126 |
+
"source": "seed"
|
| 127 |
+
},
|
| 128 |
+
"clip_0019": {
|
| 129 |
+
"confidence": null,
|
| 130 |
+
"episode": 0,
|
| 131 |
+
"label": "REJECT",
|
| 132 |
+
"reward": null,
|
| 133 |
+
"source": "seed"
|
| 134 |
+
},
|
| 135 |
+
"clip_002": {
|
| 136 |
+
"confidence": 0.82,
|
| 137 |
+
"episode": 1,
|
| 138 |
+
"label": "REJECT",
|
| 139 |
+
"reward": 0.85,
|
| 140 |
+
"source": "agent_promoted"
|
| 141 |
+
},
|
| 142 |
+
"clip_0020": {
|
| 143 |
+
"confidence": null,
|
| 144 |
+
"episode": 0,
|
| 145 |
+
"label": "REJECT",
|
| 146 |
+
"reward": null,
|
| 147 |
+
"source": "seed"
|
| 148 |
+
},
|
| 149 |
+
"clip_003": {
|
| 150 |
+
"confidence": 0.82,
|
| 151 |
+
"episode": 1,
|
| 152 |
+
"label": "REJECT",
|
| 153 |
+
"reward": 0.85,
|
| 154 |
+
"source": "agent_promoted"
|
| 155 |
+
},
|
| 156 |
+
"clip_004": {
|
| 157 |
+
"confidence": 0.82,
|
| 158 |
+
"episode": 1,
|
| 159 |
+
"label": "REJECT",
|
| 160 |
+
"reward": 0.85,
|
| 161 |
+
"source": "agent_promoted"
|
| 162 |
+
},
|
| 163 |
+
"clip_008": {
|
| 164 |
+
"confidence": 0.85,
|
| 165 |
+
"episode": 21,
|
| 166 |
+
"label": "REJECT",
|
| 167 |
+
"reward": 0.856,
|
| 168 |
+
"source": "agent_promoted"
|
| 169 |
+
},
|
| 170 |
+
"clip_010": {
|
| 171 |
+
"confidence": 0.96,
|
| 172 |
+
"episode": 9,
|
| 173 |
+
"label": "REJECT",
|
| 174 |
+
"reward": 0.95,
|
| 175 |
+
"source": "agent_promoted"
|
| 176 |
+
},
|
| 177 |
+
"clip_012": {
|
| 178 |
+
"confidence": 0.82,
|
| 179 |
+
"episode": 1,
|
| 180 |
+
"label": "REJECT",
|
| 181 |
+
"reward": 0.95,
|
| 182 |
+
"source": "agent_promoted"
|
| 183 |
+
},
|
| 184 |
+
"clip_019": {
|
| 185 |
+
"confidence": 0.82,
|
| 186 |
+
"episode": 1,
|
| 187 |
+
"label": "KEEP",
|
| 188 |
+
"reward": 0.95,
|
| 189 |
+
"source": "agent_promoted"
|
| 190 |
+
},
|
| 191 |
+
"clip_020": {
|
| 192 |
+
"confidence": 0.82,
|
| 193 |
+
"episode": 1,
|
| 194 |
+
"label": "REJECT",
|
| 195 |
+
"reward": 0.95,
|
| 196 |
+
"source": "agent_promoted"
|
| 197 |
+
}
|
| 198 |
+
}
|
state/history.jsonl
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_e70a0096", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.25}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_333f90f4", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.9}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_2e4845a7", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.455, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 2 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_ac48dccf", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_7830fef5", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_b9961414", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.41, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 3 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_b373bc97", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_39a6f2aa", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_4858f1a1", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.47, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 4 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_b89db641", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_46ec41c6", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_61e09b7f", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.2, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 5 |
+
{"episode": 4, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_195c392b", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_65e03fc7", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_aace83de", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.41, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 6 |
+
{"episode": 5, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_9bbc2e2c", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_2fff2df8", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_cde64aa0", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.47, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 7 |
+
{"episode": 6, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_652d1f4e", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_e8c2e65a", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_2284b99a", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.68, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 8 |
+
{"episode": 7, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_92a79235", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_ad4dec94", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_597f5b02", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.47, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 9 |
+
{"episode": 8, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_c70d500a", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_b630f24a", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_4cf710c4", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.47, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 10 |
+
{"episode": 9, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_ba850490", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_1a2941bb", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_0dd7abbc", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.47, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 11 |
+
{"episode": 10, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_50f3f686", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_aafe5c00", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_c109cd91", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.41, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 12 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_24b3c30c", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_1f0d711b", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_83b8f2a8", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.41, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 13 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.68, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 14 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_0170e61e", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_446460c1", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_60287218", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.47, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 15 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.68, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 16 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_6f73370f", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_3c1e3756", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_1caa4633", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.2, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 17 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.68, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 18 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_0001", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "real_medium_0001", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_0001", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.68, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 19 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_0001", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "real_medium_0001", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_0001", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.68, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 20 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_009", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.32, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 21 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "clip_009", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.35}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.4125, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 22 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_012", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "clip_007", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.95}, {"step": 3, "difficulty": "hard", "clip_id": "clip_010", "label": "REJECT", "expected_label": "REJECT", "reward": 0.8}], "ep_reward": 0.8925, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 23 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_019", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_015", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "clip_008", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.87, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 24 |
+
{"episode": 4, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_017", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "clip_021", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "clip_003", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.64, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 25 |
+
{"episode": 5, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_018", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.9}, {"step": 2, "difficulty": "medium", "clip_id": "clip_014", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "clip_002", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.62, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 26 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "clip_009", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.35}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.4125, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 27 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_012", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "clip_007", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_010", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.41, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 28 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_019", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_015", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_008", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.8, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 29 |
+
{"episode": 4, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_017", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_021", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_003", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.53, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 30 |
+
{"episode": 5, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_018", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_014", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_002", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.53, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 31 |
+
{"episode": 6, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_001", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_023", "label": "REJECT", "expected_label": "REJECT", "reward": 0.8}], "ep_reward": 0.8, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 32 |
+
{"episode": 7, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_012", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "clip_011", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.35}, {"step": 3, "difficulty": "hard", "clip_id": "clip_005", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.6825, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 33 |
+
{"episode": 8, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_019", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "clip_013", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.95}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.6125, "gt_promoted": false, "gt_size": 20, "rubric_version": 1}
|
| 34 |
+
{"episode": 9, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_017", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "clip_022", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "clip_010", "label": "REJECT", "expected_label": "REJECT", "reward": 0.95}], "ep_reward": 0.9775, "gt_promoted": true, "gt_size": 21, "rubric_version": 1}
|
| 35 |
+
{"episode": 10, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_018", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "clip_016", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "clip_008", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.62, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 36 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_009", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.32, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 37 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_012", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "clip_007", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_010", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.41, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 38 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_019", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_015", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_008", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.8, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 39 |
+
{"episode": 4, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_017", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_021", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_003", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.53, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 40 |
+
{"episode": 5, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_018", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_014", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_002", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.53, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 41 |
+
{"episode": 6, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_001", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_023", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.53, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 42 |
+
{"episode": 7, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_012", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "clip_011", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "clip_005", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.47, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 43 |
+
{"episode": 8, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_019", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_013", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.53, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 44 |
+
{"episode": 9, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_017", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_022", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "clip_010", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.32, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 45 |
+
{"episode": 10, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_018", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_016", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "clip_008", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.59, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 46 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "clip_009", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.35}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.4125, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 47 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_012", "label": "REJECT", "expected_label": "REJECT", "reward": 0.9}, {"step": 2, "difficulty": "medium", "clip_id": "clip_007", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.95}, {"step": 3, "difficulty": "hard", "clip_id": "clip_010", "label": "REJECT", "expected_label": "REJECT", "reward": 0.95}], "ep_reward": 0.94, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 48 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "clip_009", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.35}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.4}], "ep_reward": 0.5025, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 49 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_009", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.35}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.3725, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 50 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_012", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "clip_007", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.95}, {"step": 3, "difficulty": "hard", "clip_id": "clip_010", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}], "ep_reward": 0.9825, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 51 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_019", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_015", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "clip_008", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.87, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 52 |
+
{"episode": 4, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_017", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_021", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "clip_003", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.6, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 53 |
+
{"episode": 5, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_018", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_014", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_002", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.53, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 54 |
+
{"episode": 6, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_020", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_001", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_023", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.53, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 55 |
+
{"episode": 7, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_012", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}, {"step": 2, "difficulty": "medium", "clip_id": "clip_011", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "clip_005", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.47, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 56 |
+
{"episode": 8, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_019", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_013", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "clip_004", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.53, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 57 |
+
{"episode": 9, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_017", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_022", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "clip_010", "label": "BORDERLINE", "expected_label": "REJECT", "reward": 0.2}], "ep_reward": 0.32, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 58 |
+
{"episode": 10, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "clip_018", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}, {"step": 2, "difficulty": "medium", "clip_id": "clip_016", "label": "BORDERLINE", "expected_label": "KEEP", "reward": 0.2}, {"step": 3, "difficulty": "hard", "clip_id": "clip_008", "label": "BORDERLINE", "expected_label": "BORDERLINE", "reward": 0.8}], "ep_reward": 0.59, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 59 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 60 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 61 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 62 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 63 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 64 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 65 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 66 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 67 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 68 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 69 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 70 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 71 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_02bc3f2a", "label": "REJECT", "expected_label": "REJECT", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_e7440a32", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_467b78ba", "label": "REJECT", "expected_label": "REJECT", "reward": 0.95}], "ep_reward": 0.8275, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 72 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_81082325", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_91e25287", "label": "REJECT", "expected_label": "REJECT", "reward": 0.8}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_133040f5", "label": "REJECT", "expected_label": "REJECT", "reward": 0.8}], "ep_reward": 0.84, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 73 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_041b3590", "label": "REJECT", "expected_label": "REJECT", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_cb64ce2a", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_e99ab288", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}], "ep_reward": 0.85, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 74 |
+
{"episode": 4, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_5950673c", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_7291cb9c", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_24f67412", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.68, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 75 |
+
{"episode": 5, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_a24a4892", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_fc846ffd", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_f717eeec", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}], "ep_reward": 0.85, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 76 |
+
{"episode": 6, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_5e6cc0b9", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_8629bbf7", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_139a4dbd", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}], "ep_reward": 0.86, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 77 |
+
{"episode": 7, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_ac0fbcc2", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_6c05fde8", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_4feb4a68", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.68, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 78 |
+
{"episode": 8, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_528650e8", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_7ad11dd3", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_2170965a", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.68, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 79 |
+
{"episode": 9, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_7d7b74b8", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_1ea581d7", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_763cc786", "label": "REJECT", "expected_label": "REJECT", "reward": 0.95}], "ep_reward": 0.8275, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 80 |
+
{"episode": 10, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "syn_easy_278c97de", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "syn_med_4b1e494a", "label": "REJECT", "expected_label": "REJECT", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "syn_hard_a774a48f", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.45}], "ep_reward": 0.7425, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 81 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 82 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 83 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 84 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 85 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 86 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 87 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 88 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 89 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 90 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 91 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 92 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 93 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 94 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 95 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 96 |
+
{"episode": 1, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 97 |
+
{"episode": 2, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
| 98 |
+
{"episode": 3, "steps": [{"step": 1, "difficulty": "easy", "clip_id": "real_easy_1", "label": "KEEP", "expected_label": "KEEP", "reward": 0.95}, {"step": 2, "difficulty": "medium", "clip_id": "real_med_1", "label": "KEEP", "expected_label": "KEEP", "reward": 1.0}, {"step": 3, "difficulty": "hard", "clip_id": "real_hard_1", "label": "KEEP", "expected_label": "BORDERLINE", "reward": 0.6}], "ep_reward": 0.81, "gt_promoted": false, "gt_size": 21, "rubric_version": 1}
|
tests/test_baseline_runs.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from server.baseline_runs import BaselineRunTracker, DEFAULT_RUN_TTL_SECONDS
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class _FakeClock:
|
| 7 |
+
def __init__(self, start: float = 0.0) -> None:
|
| 8 |
+
self.value = float(start)
|
| 9 |
+
|
| 10 |
+
def time(self) -> float:
|
| 11 |
+
return self.value
|
| 12 |
+
|
| 13 |
+
def advance(self, seconds: float) -> None:
|
| 14 |
+
self.value += float(seconds)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_create_run_defaults_to_running_state():
|
| 18 |
+
clock = _FakeClock(start=100.0)
|
| 19 |
+
tracker = BaselineRunTracker(time_fn=clock.time)
|
| 20 |
+
|
| 21 |
+
run_id = tracker.create_run()
|
| 22 |
+
run = tracker.get_run(run_id)
|
| 23 |
+
|
| 24 |
+
assert run is not None
|
| 25 |
+
assert run["run_id"] == run_id
|
| 26 |
+
assert run["status"] == "running"
|
| 27 |
+
assert run["partial"] == {}
|
| 28 |
+
assert run["result"] is None
|
| 29 |
+
assert run["error"] is None
|
| 30 |
+
assert run["created_at"] == 100.0
|
| 31 |
+
assert run["updated_at"] == 100.0
|
| 32 |
+
assert run["started_at"] == 100.0
|
| 33 |
+
assert run["completed_at"] is None
|
| 34 |
+
assert run["failed_at"] is None
|
| 35 |
+
assert run["expires_at"] == 100.0 + DEFAULT_RUN_TTL_SECONDS
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_update_partial_merges_and_refreshes_ttl():
|
| 39 |
+
clock = _FakeClock(start=10.0)
|
| 40 |
+
tracker = BaselineRunTracker(ttl_seconds=10.0, time_fn=clock.time)
|
| 41 |
+
|
| 42 |
+
run_id = tracker.create_run()
|
| 43 |
+
clock.advance(3.0)
|
| 44 |
+
tracker.update_partial(run_id, {"processed": 1})
|
| 45 |
+
|
| 46 |
+
first = tracker.get_run(run_id)
|
| 47 |
+
assert first is not None
|
| 48 |
+
assert first["status"] == "running"
|
| 49 |
+
assert first["partial"] == {"processed": 1}
|
| 50 |
+
assert first["expires_at"] == 23.0
|
| 51 |
+
|
| 52 |
+
clock.advance(2.0)
|
| 53 |
+
tracker.update_partial(run_id, {"total": 5})
|
| 54 |
+
second = tracker.get_run(run_id)
|
| 55 |
+
|
| 56 |
+
assert second is not None
|
| 57 |
+
assert second["partial"] == {"processed": 1, "total": 5}
|
| 58 |
+
assert second["expires_at"] == 25.0
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_status_transitions_failed_to_running_to_complete():
|
| 62 |
+
clock = _FakeClock(start=200.0)
|
| 63 |
+
tracker = BaselineRunTracker(time_fn=clock.time)
|
| 64 |
+
|
| 65 |
+
run_id = tracker.create_run()
|
| 66 |
+
clock.advance(1.0)
|
| 67 |
+
tracker.mark_failed(run_id, {"message": "transient error"})
|
| 68 |
+
|
| 69 |
+
failed = tracker.get_run(run_id)
|
| 70 |
+
assert failed is not None
|
| 71 |
+
assert failed["status"] == "failed"
|
| 72 |
+
assert failed["error"] == {"message": "transient error"}
|
| 73 |
+
assert failed["failed_at"] == 201.0
|
| 74 |
+
|
| 75 |
+
clock.advance(1.0)
|
| 76 |
+
tracker.mark_running(run_id, {"retry": 1})
|
| 77 |
+
running = tracker.get_run(run_id)
|
| 78 |
+
assert running is not None
|
| 79 |
+
assert running["status"] == "running"
|
| 80 |
+
assert running["error"] is None
|
| 81 |
+
assert running["failed_at"] is None
|
| 82 |
+
assert running["partial"] == {"retry": 1}
|
| 83 |
+
|
| 84 |
+
clock.advance(1.0)
|
| 85 |
+
tracker.mark_complete(run_id, {"average_score": 0.87})
|
| 86 |
+
completed = tracker.get_run(run_id)
|
| 87 |
+
assert completed is not None
|
| 88 |
+
assert completed["status"] == "completed"
|
| 89 |
+
assert completed["result"] == {"average_score": 0.87}
|
| 90 |
+
assert completed["error"] is None
|
| 91 |
+
assert completed["completed_at"] == 203.0
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def test_cleanup_expired_respects_ttl_and_touch_updates():
|
| 95 |
+
clock = _FakeClock(start=0.0)
|
| 96 |
+
tracker = BaselineRunTracker(ttl_seconds=10.0, time_fn=clock.time)
|
| 97 |
+
|
| 98 |
+
stale_run = tracker.create_run()
|
| 99 |
+
refreshed_run = tracker.create_run()
|
| 100 |
+
|
| 101 |
+
clock.advance(6.0)
|
| 102 |
+
tracker.update_partial(refreshed_run, {"progress": 0.5})
|
| 103 |
+
|
| 104 |
+
clock.advance(5.0)
|
| 105 |
+
removed = tracker.cleanup_expired()
|
| 106 |
+
assert removed == 1
|
| 107 |
+
assert tracker.get_run(stale_run) is None
|
| 108 |
+
assert tracker.get_run(refreshed_run) is not None
|
| 109 |
+
|
| 110 |
+
clock.advance(6.0)
|
| 111 |
+
assert tracker.cleanup_expired() == 1
|
| 112 |
+
assert tracker.get_run(refreshed_run) is None
|
tests/test_environment.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from statistics import mean
|
| 5 |
+
|
| 6 |
+
from clip_quality_env.env import ClipQualityEnvironment
|
| 7 |
+
from clip_quality_env.ground_truth import GTStore
|
| 8 |
+
from clip_quality_env.models import Action
|
| 9 |
+
from clip_quality_env.rubric import RubricState
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _action_for_clip(clip_id: str, label: str = "BORDERLINE") -> Action:
|
| 13 |
+
return Action.model_validate(
|
| 14 |
+
{
|
| 15 |
+
"label": label,
|
| 16 |
+
"reasoning": f"{label} decision for {clip_id} using clip metadata cues.",
|
| 17 |
+
"confidence": 0.8,
|
| 18 |
+
"clip_id": clip_id,
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_environment_reset_returns_task_observation():
|
| 24 |
+
env = ClipQualityEnvironment()
|
| 25 |
+
obs = env.reset(task_id="task_easy")
|
| 26 |
+
assert obs.task_id == "task_easy"
|
| 27 |
+
assert obs.step_count == 0
|
| 28 |
+
assert obs.corpus_size == obs.corpus_shown
|
| 29 |
+
assert len(obs.data_corpus) == obs.corpus_size
|
| 30 |
+
assert obs.clip_metadata.clip_id
|
| 31 |
+
assert obs.max_steps == 5
|
| 32 |
+
assert obs.info["steps_remaining"] == 5
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_environment_step_updates_state_and_reward():
|
| 36 |
+
env = ClipQualityEnvironment()
|
| 37 |
+
env.reset(task_id="task_easy")
|
| 38 |
+
action = Action.model_validate(
|
| 39 |
+
{
|
| 40 |
+
"label": "KEEP",
|
| 41 |
+
"reasoning": "face_confidence and lighting_uniformity are high, motion_score is low, so keep.",
|
| 42 |
+
"confidence": 0.9,
|
| 43 |
+
}
|
| 44 |
+
)
|
| 45 |
+
next_obs = env.step(action)
|
| 46 |
+
assert next_obs.step_count == 1
|
| 47 |
+
assert 0.0 <= next_obs.reward <= 1.0
|
| 48 |
+
assert next_obs.history
|
| 49 |
+
state = env.state
|
| 50 |
+
assert state.step_count == 1
|
| 51 |
+
assert state.actions_taken[-1] in {"KEEP", "BORDERLINE", "REJECT"}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_environment_observation_exposes_reward_decomposition():
|
| 55 |
+
env = ClipQualityEnvironment()
|
| 56 |
+
reset_obs = env.reset(task_id="task_easy")
|
| 57 |
+
|
| 58 |
+
assert reset_obs.info["format_score"] == 0.0
|
| 59 |
+
assert reset_obs.info["label_score"] == 0.0
|
| 60 |
+
assert reset_obs.info["reasoning_score"] == 0.0
|
| 61 |
+
assert reset_obs.info["raw_total"] == 0.0
|
| 62 |
+
assert reset_obs.info["calibrated_total"] == 0.0
|
| 63 |
+
assert reset_obs.info["reward_total"] == 0.0
|
| 64 |
+
assert reset_obs.info["total_reward"] == 0.0
|
| 65 |
+
assert reset_obs.info["reward_breakdown"] == {
|
| 66 |
+
"format_score": 0.0,
|
| 67 |
+
"label_score": 0.0,
|
| 68 |
+
"reasoning_score": 0.0,
|
| 69 |
+
"raw_total": 0.0,
|
| 70 |
+
"calibrated_total": 0.0,
|
| 71 |
+
"total_reward": 0.0,
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
action = Action.model_validate(
|
| 75 |
+
{
|
| 76 |
+
"label": "KEEP",
|
| 77 |
+
"reasoning": "face_confidence and lighting_uniformity are high, motion_score is low, so keep.",
|
| 78 |
+
"confidence": 0.9,
|
| 79 |
+
}
|
| 80 |
+
)
|
| 81 |
+
step_obs = env.step(action)
|
| 82 |
+
|
| 83 |
+
assert step_obs.info["format_score"] in {0.0, 0.1}
|
| 84 |
+
assert step_obs.info["label_score"] in {0.0, 0.25, 0.6}
|
| 85 |
+
assert 0.0 <= step_obs.info["reasoning_score"] <= 0.3
|
| 86 |
+
assert abs(
|
| 87 |
+
float(step_obs.info["raw_total"])
|
| 88 |
+
- (
|
| 89 |
+
float(step_obs.info["format_score"])
|
| 90 |
+
+ float(step_obs.info["label_score"])
|
| 91 |
+
+ float(step_obs.info["reasoning_score"])
|
| 92 |
+
)
|
| 93 |
+
) < 1e-9
|
| 94 |
+
assert abs(float(step_obs.info["calibrated_total"]) - float(step_obs.reward)) < 1e-9
|
| 95 |
+
assert abs(float(step_obs.info["reward_total"]) - float(step_obs.reward)) < 1e-9
|
| 96 |
+
assert abs(float(step_obs.info["total_reward"]) - float(env.state.total_reward)) < 1e-9
|
| 97 |
+
assert step_obs.info["reward_breakdown"] == {
|
| 98 |
+
"format_score": float(step_obs.info["format_score"]),
|
| 99 |
+
"label_score": float(step_obs.info["label_score"]),
|
| 100 |
+
"reasoning_score": float(step_obs.info["reasoning_score"]),
|
| 101 |
+
"raw_total": float(step_obs.info["raw_total"]),
|
| 102 |
+
"calibrated_total": float(step_obs.info["calibrated_total"]),
|
| 103 |
+
"total_reward": float(step_obs.info["reward_total"]),
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def test_environment_derives_expected_label_when_manifest_value_is_null(monkeypatch, tmp_path):
|
| 108 |
+
manifest_path = tmp_path / "manifest_null_expected.jsonl"
|
| 109 |
+
manifest_row = {
|
| 110 |
+
"difficulty": "easy",
|
| 111 |
+
"clip_id": "manifest_null_expected",
|
| 112 |
+
"expected_label": None,
|
| 113 |
+
"face_confidence": 0.88,
|
| 114 |
+
"motion_score": 0.12,
|
| 115 |
+
"audio_snr_db": 24.0,
|
| 116 |
+
"lighting_uniformity": 0.8,
|
| 117 |
+
"duration_s": 8.0,
|
| 118 |
+
}
|
| 119 |
+
manifest_path.write_text(json.dumps(manifest_row) + "\n", encoding="utf-8")
|
| 120 |
+
|
| 121 |
+
monkeypatch.setenv("REAL_CLIPS_MANIFEST", str(manifest_path))
|
| 122 |
+
monkeypatch.setenv("CLIP_CORPUS_SOURCE", "manifest")
|
| 123 |
+
|
| 124 |
+
env = ClipQualityEnvironment()
|
| 125 |
+
obs = env.reset(task_id="task_easy", seed=2026)
|
| 126 |
+
|
| 127 |
+
assert obs.info["corpus_source"] == "manifest:easy"
|
| 128 |
+
assert str(obs.clip_metadata.expected_label).upper() in {"KEEP", "BORDERLINE", "REJECT"}
|
| 129 |
+
assert str(obs.clip_metadata.expected_label).upper() != "NONE"
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def test_environment_can_force_task_registry_source(monkeypatch):
|
| 133 |
+
monkeypatch.setenv("CLIP_CORPUS_SOURCE", "task_registry")
|
| 134 |
+
|
| 135 |
+
env = ClipQualityEnvironment()
|
| 136 |
+
obs = env.reset(task_id="task_easy", seed=2026)
|
| 137 |
+
|
| 138 |
+
assert obs.info["corpus_mode"] == "task_registry"
|
| 139 |
+
assert obs.info["corpus_source"] == "task_registry:task_easy"
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def test_environment_observation_includes_full_unsliced_corpus():
|
| 143 |
+
env = ClipQualityEnvironment()
|
| 144 |
+
obs = env.reset(task_id="task_medium")
|
| 145 |
+
synthetic_corpus = []
|
| 146 |
+
template = dict(obs.data_corpus[0]) if obs.data_corpus else {
|
| 147 |
+
"expected_label": "BORDERLINE",
|
| 148 |
+
"review_status": "pending",
|
| 149 |
+
}
|
| 150 |
+
for idx in range(12):
|
| 151 |
+
item = dict(template)
|
| 152 |
+
item["id"] = f"synthetic_{idx:03d}"
|
| 153 |
+
item["clip_id"] = f"clip_synth_{idx:03d}"
|
| 154 |
+
item["expected_label"] = str(item.get("expected_label") or "BORDERLINE")
|
| 155 |
+
item["review_status"] = "pending"
|
| 156 |
+
synthetic_corpus.append(item)
|
| 157 |
+
env._episode_corpus[obs.task_id] = synthetic_corpus
|
| 158 |
+
|
| 159 |
+
full_obs = env._state_to_observation(reward=0.0, done=False)
|
| 160 |
+
|
| 161 |
+
assert full_obs.corpus_size == len(synthetic_corpus)
|
| 162 |
+
assert full_obs.corpus_shown == len(synthetic_corpus)
|
| 163 |
+
assert len(full_obs.data_corpus) == len(synthetic_corpus)
|
| 164 |
+
assert [item["clip_id"] for item in full_obs.data_corpus] == [
|
| 165 |
+
item["clip_id"] for item in synthetic_corpus
|
| 166 |
+
]
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def test_environment_step_updates_submitted_clip_review_status():
|
| 170 |
+
env = ClipQualityEnvironment()
|
| 171 |
+
obs = env.reset(task_id="task_medium")
|
| 172 |
+
current_clip_id = obs.clip_metadata.clip_id
|
| 173 |
+
|
| 174 |
+
current_row = next(item for item in obs.data_corpus if item["clip_id"] == current_clip_id)
|
| 175 |
+
assert str(current_row.get("review_status", "")).lower() == "pending"
|
| 176 |
+
|
| 177 |
+
action = Action.model_validate(
|
| 178 |
+
{
|
| 179 |
+
"label": "REJECT",
|
| 180 |
+
"reasoning": "multiple weak cues indicate this clip should be rejected.",
|
| 181 |
+
"confidence": 0.81,
|
| 182 |
+
"clip_id": current_clip_id,
|
| 183 |
+
}
|
| 184 |
+
)
|
| 185 |
+
next_obs = env.step(action)
|
| 186 |
+
updated_row = next(item for item in next_obs.data_corpus if item["clip_id"] == current_clip_id)
|
| 187 |
+
|
| 188 |
+
assert updated_row["review_status"] == "REJECT"
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def test_environment_instances_do_not_share_runtime_state():
|
| 192 |
+
env_a = ClipQualityEnvironment()
|
| 193 |
+
env_b = ClipQualityEnvironment()
|
| 194 |
+
|
| 195 |
+
env_a.reset(task_id="task_easy")
|
| 196 |
+
action = Action.model_validate(
|
| 197 |
+
{
|
| 198 |
+
"label": "KEEP",
|
| 199 |
+
"reasoning": "face_confidence and lighting_uniformity are high, motion_score is low, so keep.",
|
| 200 |
+
"confidence": 0.9,
|
| 201 |
+
}
|
| 202 |
+
)
|
| 203 |
+
env_a.step(action)
|
| 204 |
+
|
| 205 |
+
assert env_a is not env_b
|
| 206 |
+
assert env_a.state.step_count == 1
|
| 207 |
+
assert env_b.state.step_count == 0
|
| 208 |
+
assert env_b.state.actions_taken == []
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def test_environment_reset_plans_five_clips_from_selected_corpus():
|
| 212 |
+
env = ClipQualityEnvironment()
|
| 213 |
+
obs = env.reset(task_id="task_medium", seed=1234)
|
| 214 |
+
expected_source = obs.info["corpus_source"]
|
| 215 |
+
expected_data = list(obs.data_corpus)
|
| 216 |
+
expected_ids = {str(item["clip_id"]) for item in expected_data}
|
| 217 |
+
first_plan_ids = [str(item.clip.get("clip_id", "")) for item in env._episode_plan]
|
| 218 |
+
|
| 219 |
+
assert obs.max_steps == 5
|
| 220 |
+
assert obs.info["steps_remaining"] == 5
|
| 221 |
+
assert len(env._episode_plan) == 5
|
| 222 |
+
assert {item.task_id for item in env._episode_plan} == {"task_medium"}
|
| 223 |
+
assert set(first_plan_ids).issubset(expected_ids)
|
| 224 |
+
|
| 225 |
+
repeated = env.reset(task_id="task_medium", seed=1234)
|
| 226 |
+
repeated_plan_ids = [str(item.clip.get("clip_id", "")) for item in env._episode_plan]
|
| 227 |
+
assert repeated.max_steps == 5
|
| 228 |
+
assert repeated.info["steps_remaining"] == 5
|
| 229 |
+
assert repeated.info["corpus_source"] == expected_source
|
| 230 |
+
assert repeated.data_corpus == expected_data
|
| 231 |
+
assert repeated_plan_ids == first_plan_ids
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def test_environment_updates_review_status_across_queue_and_summary():
|
| 235 |
+
env = ClipQualityEnvironment()
|
| 236 |
+
obs = env.reset(task_id="task_hard", seed=2026)
|
| 237 |
+
|
| 238 |
+
assert obs.info["steps_remaining"] == 5
|
| 239 |
+
assert "episode_summary" not in obs.info
|
| 240 |
+
|
| 241 |
+
submitted: list[tuple[str, str]] = []
|
| 242 |
+
final_obs = obs
|
| 243 |
+
for step_index in range(1, 6):
|
| 244 |
+
current_clip_id = final_obs.clip_metadata.clip_id
|
| 245 |
+
label = "KEEP" if step_index % 2 else "BORDERLINE"
|
| 246 |
+
submitted.append((current_clip_id, label))
|
| 247 |
+
final_obs = env.step(_action_for_clip(current_clip_id, label=label))
|
| 248 |
+
queue_row = next(item for item in final_obs.data_corpus if item.get("clip_id") == current_clip_id)
|
| 249 |
+
assert str(queue_row.get("review_status")) == label
|
| 250 |
+
assert final_obs.info["steps_remaining"] == max(0, 5 - step_index)
|
| 251 |
+
|
| 252 |
+
assert final_obs.done is True
|
| 253 |
+
assert final_obs.step_count == 5
|
| 254 |
+
assert "episode_summary" in final_obs.info
|
| 255 |
+
summary = final_obs.info["episode_summary"]
|
| 256 |
+
assert summary["steps_completed"] == 5
|
| 257 |
+
assert summary["max_steps"] == 5
|
| 258 |
+
assert abs(float(summary["total_reward"]) - round(float(env.state.total_reward), 4)) < 1e-9
|
| 259 |
+
assert abs(float(final_obs.info["total_reward"]) - float(env.state.total_reward)) < 1e-9
|
| 260 |
+
assert abs(float(summary["average_reward"]) - round(float(env.state.total_reward) / 5.0, 4)) < 1e-9
|
| 261 |
+
|
| 262 |
+
final_status_map = {str(item["clip_id"]): str(item["review_status"]) for item in final_obs.data_corpus}
|
| 263 |
+
for clip_id, label in submitted:
|
| 264 |
+
assert final_status_map[clip_id] == label
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def test_environment_task_averages_follow_hard_medium_easy_order(monkeypatch, tmp_path):
|
| 268 |
+
monkeypatch.setenv("REAL_CLIPS_MANIFEST", str(tmp_path / "missing_manifest.jsonl"))
|
| 269 |
+
|
| 270 |
+
def run_task_average(task_id: str) -> float:
|
| 271 |
+
env = ClipQualityEnvironment()
|
| 272 |
+
env._rubric = RubricState(path=str(tmp_path / f"rubric_{task_id}.json"))
|
| 273 |
+
env._gt_store = GTStore(
|
| 274 |
+
seed_path="data/seed_gt.json",
|
| 275 |
+
state_path=str(tmp_path / f"ground_truth_{task_id}.json"),
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
obs = env.reset(task_id=task_id, seed=2026)
|
| 279 |
+
step_rewards: list[float] = []
|
| 280 |
+
while True:
|
| 281 |
+
expected_label = str(obs.clip_metadata.expected_label or "BORDERLINE")
|
| 282 |
+
action = Action.model_validate(
|
| 283 |
+
{
|
| 284 |
+
"label": expected_label,
|
| 285 |
+
"reasoning": (
|
| 286 |
+
"face_confidence, motion_score, audio_snr_db, and lighting_uniformity "
|
| 287 |
+
"support this decision."
|
| 288 |
+
),
|
| 289 |
+
"confidence": 0.9,
|
| 290 |
+
"clip_id": obs.clip_metadata.clip_id,
|
| 291 |
+
}
|
| 292 |
+
)
|
| 293 |
+
obs = env.step(action)
|
| 294 |
+
step_rewards.append(float(obs.reward))
|
| 295 |
+
if obs.done:
|
| 296 |
+
break
|
| 297 |
+
return float(mean(step_rewards))
|
| 298 |
+
|
| 299 |
+
easy_avg = run_task_average("task_easy")
|
| 300 |
+
medium_avg = run_task_average("task_medium")
|
| 301 |
+
hard_avg = run_task_average("task_hard")
|
| 302 |
+
|
| 303 |
+
assert hard_avg > medium_avg > easy_avg
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def test_environment_difficulty_score_ranges_are_strictly_ordered(monkeypatch, tmp_path):
|
| 307 |
+
monkeypatch.setenv("REAL_CLIPS_MANIFEST", str(tmp_path / "missing_manifest.jsonl"))
|
| 308 |
+
|
| 309 |
+
def run_task_range(task_id: str) -> tuple[float, float]:
|
| 310 |
+
task_scores: list[float] = []
|
| 311 |
+
labels = ("KEEP", "BORDERLINE", "REJECT")
|
| 312 |
+
reasoning_cases = (
|
| 313 |
+
"x",
|
| 314 |
+
"face_confidence, motion_score, audio_snr_db, and lighting_uniformity support this decision.",
|
| 315 |
+
)
|
| 316 |
+
confidence_cases = (0.0, 0.5, 1.0)
|
| 317 |
+
|
| 318 |
+
for label in labels:
|
| 319 |
+
for reasoning in reasoning_cases:
|
| 320 |
+
for confidence in confidence_cases:
|
| 321 |
+
env = ClipQualityEnvironment()
|
| 322 |
+
env._rubric = RubricState(path=str(tmp_path / f"rubric_range_{task_id}.json"))
|
| 323 |
+
env._gt_store = GTStore(
|
| 324 |
+
seed_path="data/seed_gt.json",
|
| 325 |
+
state_path=str(tmp_path / f"ground_truth_range_{task_id}.json"),
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
obs = env.reset(task_id=task_id, seed=2026)
|
| 329 |
+
while True:
|
| 330 |
+
action = Action.model_validate(
|
| 331 |
+
{
|
| 332 |
+
"label": label,
|
| 333 |
+
"reasoning": reasoning,
|
| 334 |
+
"confidence": confidence,
|
| 335 |
+
"clip_id": obs.clip_metadata.clip_id,
|
| 336 |
+
}
|
| 337 |
+
)
|
| 338 |
+
obs = env.step(action)
|
| 339 |
+
task_scores.append(float(obs.reward))
|
| 340 |
+
if obs.done:
|
| 341 |
+
break
|
| 342 |
+
|
| 343 |
+
return min(task_scores), max(task_scores)
|
| 344 |
+
|
| 345 |
+
easy_min, easy_max = run_task_range("task_easy")
|
| 346 |
+
medium_min, medium_max = run_task_range("task_medium")
|
| 347 |
+
hard_min, hard_max = run_task_range("task_hard")
|
| 348 |
+
|
| 349 |
+
assert easy_max < medium_min
|
| 350 |
+
assert medium_max < hard_min
|
| 351 |
+
|
| 352 |
+
assert 0.0 <= easy_min <= easy_max <= 1.0
|
| 353 |
+
assert 0.0 <= medium_min <= medium_max <= 1.0
|
| 354 |
+
assert 0.0 <= hard_min <= hard_max <= 1.0
|
tests/test_grader.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from statistics import mean
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
import server.grader as grader_module
|
| 8 |
+
from clip_quality_env.difficulty import DIFFICULTY_TOTAL_BANDS
|
| 9 |
+
from clip_quality_env.ground_truth import GTStore
|
| 10 |
+
from clip_quality_env.rubric import RubricState
|
| 11 |
+
from server.tasks import TASK_REGISTRY
|
| 12 |
+
from server.grader import grade
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@pytest.fixture
|
| 16 |
+
def isolated_grader_state(monkeypatch, tmp_path):
|
| 17 |
+
rubric = RubricState(path=str(tmp_path / "rubric.json"))
|
| 18 |
+
gt = GTStore(seed_path="data/seed_gt.json", state_path=str(tmp_path / "ground_truth.json"))
|
| 19 |
+
monkeypatch.setattr(grader_module, "_RUBRIC", rubric)
|
| 20 |
+
monkeypatch.setattr(grader_module, "_GT", gt)
|
| 21 |
+
return rubric, gt
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_grade_easy_clip_quality_scores_in_easy_band():
|
| 25 |
+
action = {
|
| 26 |
+
"label": "KEEP",
|
| 27 |
+
"clip_id": "clip_0001",
|
| 28 |
+
"reasoning": "face_confidence and audio_snr_db are high while motion_score is low, so this clip should be kept.",
|
| 29 |
+
"confidence": 0.9,
|
| 30 |
+
}
|
| 31 |
+
score = grade(action, "task_easy")
|
| 32 |
+
low, high = DIFFICULTY_TOTAL_BANDS["easy"]
|
| 33 |
+
assert low <= score <= high
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def test_grade_medium_clip_quality_scores_in_range():
|
| 37 |
+
action = {
|
| 38 |
+
"label": "BORDERLINE",
|
| 39 |
+
"clip_id": "clip_0008",
|
| 40 |
+
"reasoning": "face_area_ratio is borderline and motion_score is elevated, so borderline is safest.",
|
| 41 |
+
"confidence": 0.74,
|
| 42 |
+
}
|
| 43 |
+
score = grade(action, "task_medium")
|
| 44 |
+
low, high = DIFFICULTY_TOTAL_BANDS["medium"]
|
| 45 |
+
assert low <= score <= high
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_grade_hard_clip_quality_scores_in_range():
|
| 49 |
+
action = {
|
| 50 |
+
"label": "REJECT",
|
| 51 |
+
"clip_id": "clip_0017",
|
| 52 |
+
"reasoning": "face_confidence is weak, face_area_ratio is small, and motion_score is high enough to reject.",
|
| 53 |
+
"confidence": 0.83,
|
| 54 |
+
}
|
| 55 |
+
score = grade(action, "task_hard")
|
| 56 |
+
low, high = DIFFICULTY_TOTAL_BANDS["hard"]
|
| 57 |
+
assert low <= score <= high
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_grade_legacy_payload_is_supported_and_bounded():
|
| 61 |
+
action = {
|
| 62 |
+
"action_type": "propose_clarification",
|
| 63 |
+
"ambiguous_term": "appropriate",
|
| 64 |
+
"suggested_definition": "Keep clips with stable framing and clear speech; reject clips with severe occlusion.",
|
| 65 |
+
"justification": "Makes decisions consistent for borderline metadata combinations.",
|
| 66 |
+
}
|
| 67 |
+
score = grade(action, "task_easy")
|
| 68 |
+
assert 0.0 <= score <= 1.0
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_grade_task_averages_follow_hard_medium_easy_order(isolated_grader_state):
|
| 72 |
+
del isolated_grader_state
|
| 73 |
+
|
| 74 |
+
def task_average(task_id: str) -> float:
|
| 75 |
+
scores: list[float] = []
|
| 76 |
+
for clip in TASK_REGISTRY[task_id]["data_corpus"]:
|
| 77 |
+
action = {
|
| 78 |
+
"label": str(clip.get("expected_label", "BORDERLINE")),
|
| 79 |
+
"clip_id": str(clip.get("clip_id", "")),
|
| 80 |
+
"reasoning": (
|
| 81 |
+
"face_confidence, motion_score, audio_snr_db, and lighting_uniformity "
|
| 82 |
+
"support this decision."
|
| 83 |
+
),
|
| 84 |
+
"confidence": 0.9,
|
| 85 |
+
}
|
| 86 |
+
scores.append(grade(action, task_id))
|
| 87 |
+
return float(mean(scores))
|
| 88 |
+
|
| 89 |
+
easy_avg = task_average("task_easy")
|
| 90 |
+
medium_avg = task_average("task_medium")
|
| 91 |
+
hard_avg = task_average("task_hard")
|
| 92 |
+
|
| 93 |
+
assert hard_avg > medium_avg > easy_avg
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def test_grade_difficulty_bands_do_not_overlap(isolated_grader_state):
|
| 97 |
+
del isolated_grader_state
|
| 98 |
+
|
| 99 |
+
labels = ("KEEP", "BORDERLINE", "REJECT")
|
| 100 |
+
reasoning_cases = (
|
| 101 |
+
"x",
|
| 102 |
+
"face_confidence, motion_score, audio_snr_db, and lighting_uniformity support this decision.",
|
| 103 |
+
)
|
| 104 |
+
confidence_cases = (0.0, 0.5, 1.0)
|
| 105 |
+
|
| 106 |
+
ranges: dict[str, tuple[float, float]] = {}
|
| 107 |
+
for task_id in ("task_easy", "task_medium", "task_hard"):
|
| 108 |
+
task_scores: list[float] = []
|
| 109 |
+
for clip in TASK_REGISTRY[task_id]["data_corpus"]:
|
| 110 |
+
clip_id = str(clip.get("clip_id", ""))
|
| 111 |
+
for label in labels:
|
| 112 |
+
for reasoning in reasoning_cases:
|
| 113 |
+
for confidence in confidence_cases:
|
| 114 |
+
task_scores.append(
|
| 115 |
+
grade(
|
| 116 |
+
{
|
| 117 |
+
"label": label,
|
| 118 |
+
"clip_id": clip_id,
|
| 119 |
+
"reasoning": reasoning,
|
| 120 |
+
"confidence": confidence,
|
| 121 |
+
},
|
| 122 |
+
task_id,
|
| 123 |
+
)
|
| 124 |
+
)
|
| 125 |
+
ranges[task_id] = (min(task_scores), max(task_scores))
|
| 126 |
+
|
| 127 |
+
easy_min, easy_max = ranges["task_easy"]
|
| 128 |
+
medium_min, medium_max = ranges["task_medium"]
|
| 129 |
+
hard_min, hard_max = ranges["task_hard"]
|
| 130 |
+
|
| 131 |
+
assert easy_min >= DIFFICULTY_TOTAL_BANDS["easy"][0]
|
| 132 |
+
assert easy_max <= DIFFICULTY_TOTAL_BANDS["easy"][1]
|
| 133 |
+
assert medium_min >= DIFFICULTY_TOTAL_BANDS["medium"][0]
|
| 134 |
+
assert medium_max <= DIFFICULTY_TOTAL_BANDS["medium"][1]
|
| 135 |
+
assert hard_min >= DIFFICULTY_TOTAL_BANDS["hard"][0]
|
| 136 |
+
assert hard_max <= DIFFICULTY_TOTAL_BANDS["hard"][1]
|
| 137 |
+
|
| 138 |
+
assert easy_max < medium_min
|
| 139 |
+
assert medium_max < hard_min
|
tests/test_inference.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
import inference
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_inference_extract_json_handles_fenced_blocks():
|
| 9 |
+
raw = "```json\n{\"x\": 1}\n```"
|
| 10 |
+
parsed = inference._extract_json(raw)
|
| 11 |
+
assert parsed == {"x": 1}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_run_baseline_works_without_token(monkeypatch):
|
| 15 |
+
monkeypatch.delenv("HF_TOKEN", raising=False)
|
| 16 |
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
| 17 |
+
result = inference.run_baseline(task="task_easy")
|
| 18 |
+
assert result["detail"][0]["task_id"] == "task_easy"
|
| 19 |
+
assert result["detail"][0]["mode"] == "fallback"
|
| 20 |
+
assert result["detail"][0]["steps"] == 5
|
| 21 |
+
assert 0.0 <= result["detail"][0]["total_reward"] <= 5.0
|
| 22 |
+
assert 0.0 <= result["detail"][0]["final_reward"] <= 1.0
|
| 23 |
+
assert 0.0 <= result["detail"][0]["reward"] <= 1.0
|
| 24 |
+
assert 0.0 <= result["baseline_scores"]["overall_avg"] <= 1.0
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class _DummyMessage:
|
| 28 |
+
def __init__(self, content: str):
|
| 29 |
+
self.content = content
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class _DummyChoice:
|
| 33 |
+
def __init__(self, content: str):
|
| 34 |
+
self.message = _DummyMessage(content)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class _DummyResponse:
|
| 38 |
+
def __init__(self, content: str):
|
| 39 |
+
self.choices = [_DummyChoice(content)]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class _DummyCompletions:
|
| 43 |
+
def __init__(self, content: str):
|
| 44 |
+
self._content = content
|
| 45 |
+
|
| 46 |
+
def create(self, **_: Any):
|
| 47 |
+
return _DummyResponse(self._content)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class _DummyChat:
|
| 51 |
+
def __init__(self, content: str):
|
| 52 |
+
self.completions = _DummyCompletions(content)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class _DummyClient:
|
| 56 |
+
def __init__(self, content: str):
|
| 57 |
+
self.chat = _DummyChat(content)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_run_episode_preserves_logging_and_structure(capsys):
|
| 61 |
+
client = _DummyClient('{"label":"KEEP","reasoning":"face_confidence and motion_score indicate keep","confidence":0.9}')
|
| 62 |
+
result = inference.run_episode("task_easy", client, "dummy-model")
|
| 63 |
+
output = capsys.readouterr().out
|
| 64 |
+
assert "[START]" in output and "[STEP]" in output and "[END]" in output
|
| 65 |
+
assert "total_reward=" in output and "final_reward=" in output
|
| 66 |
+
assert result["task_id"] == "task_easy"
|
| 67 |
+
assert result["mode"] == "llm"
|
| 68 |
+
assert result["steps"] == 5
|
| 69 |
+
assert 0.0 <= result["final_reward"] <= 1.0
|
| 70 |
+
assert 0.0 <= result["total_reward"] <= 5.0
|
| 71 |
+
assert 0.0 <= result["reward"] <= 1.0
|
| 72 |
+
assert abs(result["reward"] * result["steps"] - result["total_reward"]) < 1e-9
|