Spaces:
Running on Zero
Running on Zero
fix(ui): browser-playable overlay video (H.264) + light all dropdown option lists
#1
by BladeSzaSza - opened
- formscout/agents/report.py +18 -16
- scripts/hackathon_upload.sh +84 -0
- tests/test_phase2.py +20 -0
formscout/agents/report.py
CHANGED
|
@@ -10,9 +10,7 @@ Gated: no.
|
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
-
from formscout.types import
|
| 14 |
-
MovementResult, BiomechFeatures, ScoreResult, JudgeResult, ReportResult,
|
| 15 |
-
)
|
| 16 |
from formscout import config
|
| 17 |
|
| 18 |
# Bilateral tests that need L/R scoring
|
|
@@ -50,7 +48,10 @@ class ReportAgent:
|
|
| 50 |
else:
|
| 51 |
unilateral.append(entry)
|
| 52 |
|
| 53 |
-
# Process bilateral tests β
|
|
|
|
|
|
|
|
|
|
| 54 |
for test_name, entries in bilateral_groups.items():
|
| 55 |
scores = []
|
| 56 |
for entry in entries:
|
|
@@ -59,33 +60,34 @@ class ReportAgent:
|
|
| 59 |
score = judge.score if judge.score is not None else None
|
| 60 |
scores.append({"side": side, "score": score, "entry": entry})
|
| 61 |
|
| 62 |
-
# Find best entry per side
|
| 63 |
left = next((s for s in scores if s["side"] == "left"), None)
|
| 64 |
right = next((s for s in scores if s["side"] == "right"), None)
|
| 65 |
-
|
| 66 |
left_score = left["score"] if left else None
|
| 67 |
right_score = right["score"] if right else None
|
| 68 |
|
| 69 |
-
# Report lower
|
| 70 |
if left_score is not None and right_score is not None:
|
|
|
|
| 71 |
final_score = min(left_score, right_score)
|
| 72 |
-
delta = abs(left_score - right_score)
|
| 73 |
asymmetries.append({
|
| 74 |
"test": test_name,
|
| 75 |
"left_score": left_score,
|
| 76 |
"right_score": right_score,
|
| 77 |
-
"delta":
|
| 78 |
})
|
|
|
|
| 79 |
elif left_score is not None:
|
| 80 |
-
final_score = left_score
|
| 81 |
elif right_score is not None:
|
| 82 |
-
final_score = right_score
|
| 83 |
else:
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
per_test.append({
|
| 91 |
"test_name": test_name,
|
|
|
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
+
from formscout.types import ReportResult
|
|
|
|
|
|
|
| 14 |
from formscout import config
|
| 15 |
|
| 16 |
# Bilateral tests that need L/R scoring
|
|
|
|
| 48 |
else:
|
| 49 |
unilateral.append(entry)
|
| 50 |
|
| 51 |
+
# Process bilateral tests β report the lower side and emit an asymmetry
|
| 52 |
+
# when both sides exist; otherwise fall back to whatever was scored (e.g.
|
| 53 |
+
# a single clip submitted with side="na", the UI default) so the test
|
| 54 |
+
# still counts toward the composite instead of being dropped as unscored.
|
| 55 |
for test_name, entries in bilateral_groups.items():
|
| 56 |
scores = []
|
| 57 |
for entry in entries:
|
|
|
|
| 60 |
score = judge.score if judge.score is not None else None
|
| 61 |
scores.append({"side": side, "score": score, "entry": entry})
|
| 62 |
|
|
|
|
| 63 |
left = next((s for s in scores if s["side"] == "left"), None)
|
| 64 |
right = next((s for s in scores if s["side"] == "right"), None)
|
|
|
|
| 65 |
left_score = left["score"] if left else None
|
| 66 |
right_score = right["score"] if right else None
|
| 67 |
|
|
|
|
| 68 |
if left_score is not None and right_score is not None:
|
| 69 |
+
# Both sides present β report the lower and record the asymmetry.
|
| 70 |
final_score = min(left_score, right_score)
|
|
|
|
| 71 |
asymmetries.append({
|
| 72 |
"test": test_name,
|
| 73 |
"left_score": left_score,
|
| 74 |
"right_score": right_score,
|
| 75 |
+
"delta": abs(left_score - right_score),
|
| 76 |
})
|
| 77 |
+
primary = left["entry"] if left_score <= right_score else right["entry"]
|
| 78 |
elif left_score is not None:
|
| 79 |
+
final_score, primary = left_score, left["entry"]
|
| 80 |
elif right_score is not None:
|
| 81 |
+
final_score, primary = right_score, right["entry"]
|
| 82 |
else:
|
| 83 |
+
# No explicit L/R side: use the lowest available score across the
|
| 84 |
+
# group (covers side="na" and needs_human/unscored entries).
|
| 85 |
+
scored = [s for s in scores if s["score"] is not None]
|
| 86 |
+
if scored:
|
| 87 |
+
best = min(scored, key=lambda s: s["score"])
|
| 88 |
+
final_score, primary = best["score"], best["entry"]
|
| 89 |
+
else:
|
| 90 |
+
final_score, primary = None, entries[0]
|
| 91 |
|
| 92 |
per_test.append({
|
| 93 |
"test_name": test_name,
|
scripts/hackathon_upload.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Upload the FormScout source tree to the Build Small Hackathon Space, as a PR.
|
| 3 |
+
#
|
| 4 |
+
# Usage:
|
| 5 |
+
# ./scripts/hackathon_upload.sh # message from last git commit
|
| 6 |
+
# ./scripts/hackathon_upload.sh "feat: my change" # custom message
|
| 7 |
+
#
|
| 8 |
+
# Pushes (PR) to:
|
| 9 |
+
# spaces/build-small-hackathon/small-functional-movement-screening
|
| 10 |
+
#
|
| 11 |
+
# The Space already exists, so this script only uploads β it does not create the
|
| 12 |
+
# repo. Like hf_upload.sh, `hf upload` ignores .hfignore, so we parse it into
|
| 13 |
+
# --exclude globs ourselves. Above LARGE_THRESHOLD files it falls back to
|
| 14 |
+
# `hf upload-large-folder` (resumable, but commits to main directly β no PR).
|
| 15 |
+
set -euo pipefail
|
| 16 |
+
|
| 17 |
+
cd "$(dirname "$0")/.."
|
| 18 |
+
|
| 19 |
+
HACKATHON_OWNER="${FORMSCOUT_HF_HACKATHON_OWNER:-build-small-hackathon}"
|
| 20 |
+
REPO_NAME="small-functional-movement-screening"
|
| 21 |
+
SPACE_REPO="spaces/$HACKATHON_OWNER/$REPO_NAME"
|
| 22 |
+
MSG="${1:-$(git log -1 --pretty=%s)}"
|
| 23 |
+
LARGE_THRESHOLD="${FORMSCOUT_HF_LARGE_THRESHOLD:-500}"
|
| 24 |
+
|
| 25 |
+
# Belt-and-suspenders extras on top of .hfignore. `.cache/` is the resume
|
| 26 |
+
# state upload-large-folder writes into the folder being uploaded.
|
| 27 |
+
PATTERNS=(
|
| 28 |
+
"*.pdf"
|
| 29 |
+
"**/node_modules/**"
|
| 30 |
+
".cache/**"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Parse .hfignore into fnmatch-style globs. fnmatch's `*` crosses `/`, but a
|
| 34 |
+
# bare name like `.DS_Store` or `dir/` only matches at the root, so emit both
|
| 35 |
+
# the rooted and `**/`-prefixed forms.
|
| 36 |
+
while IFS= read -r line; do
|
| 37 |
+
line="${line%%#*}"
|
| 38 |
+
line="${line#"${line%%[![:space:]]*}"}"
|
| 39 |
+
line="${line%"${line##*[![:space:]]}"}"
|
| 40 |
+
[[ -z "$line" ]] && continue
|
| 41 |
+
if [[ "$line" == */ ]]; then
|
| 42 |
+
PATTERNS+=("${line}**" "**/${line}**")
|
| 43 |
+
else
|
| 44 |
+
PATTERNS+=("$line" "**/$line")
|
| 45 |
+
fi
|
| 46 |
+
done < .hfignore
|
| 47 |
+
|
| 48 |
+
EXCLUDES=()
|
| 49 |
+
for p in "${PATTERNS[@]}"; do
|
| 50 |
+
EXCLUDES+=(--exclude="$p")
|
| 51 |
+
done
|
| 52 |
+
|
| 53 |
+
# Count what would actually be uploaded, using the same filter the hub client
|
| 54 |
+
# applies, so the mode decision matches reality.
|
| 55 |
+
N_FILES=$(python3 - "${PATTERNS[@]}" <<'EOF'
|
| 56 |
+
import sys
|
| 57 |
+
from pathlib import Path
|
| 58 |
+
from huggingface_hub.utils import filter_repo_objects
|
| 59 |
+
|
| 60 |
+
patterns = sys.argv[1:]
|
| 61 |
+
files = (
|
| 62 |
+
str(p) for p in Path(".").rglob("*")
|
| 63 |
+
if p.is_file() and p.parts[0] != ".git"
|
| 64 |
+
)
|
| 65 |
+
print(len(list(filter_repo_objects(files, ignore_patterns=patterns))))
|
| 66 |
+
EOF
|
| 67 |
+
)
|
| 68 |
+
echo "ββ $N_FILES files to upload after .hfignore filtering"
|
| 69 |
+
|
| 70 |
+
if (( N_FILES == 0 )); then
|
| 71 |
+
echo "β nothing to upload β check .hfignore" >&2
|
| 72 |
+
exit 1
|
| 73 |
+
fi
|
| 74 |
+
|
| 75 |
+
if (( N_FILES > LARGE_THRESHOLD )); then
|
| 76 |
+
echo "ββ $SPACE_REPO: $N_FILES files > $LARGE_THRESHOLD, using upload-large-folder"
|
| 77 |
+
echo " (resumable; commits directly to main β no PR, no custom message)"
|
| 78 |
+
hf upload-large-folder "$SPACE_REPO" . "${EXCLUDES[@]}"
|
| 79 |
+
else
|
| 80 |
+
echo "ββ uploading (PR) to: $SPACE_REPO"
|
| 81 |
+
hf upload "$SPACE_REPO" . . "${EXCLUDES[@]}" --create-pr --commit-message="$MSG"
|
| 82 |
+
fi
|
| 83 |
+
|
| 84 |
+
echo "β done β PR opened on $SPACE_REPO (review & merge in the HF UI)"
|
tests/test_phase2.py
CHANGED
|
@@ -338,6 +338,26 @@ class TestReportAgent:
|
|
| 338 |
assert len(result.asymmetries) == 1
|
| 339 |
assert result.asymmetries[0]["delta"] == 1
|
| 340 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
def test_composite_none_when_unscored(self):
|
| 342 |
agent = ReportAgent()
|
| 343 |
entries = [{
|
|
|
|
| 338 |
assert len(result.asymmetries) == 1
|
| 339 |
assert result.asymmetries[0]["delta"] == 1
|
| 340 |
|
| 341 |
+
def test_bilateral_na_side_is_scored(self):
|
| 342 |
+
"""A bilateral test run once with side='na' (the UI default) must still
|
| 343 |
+
count toward the composite, not be dropped as unscored."""
|
| 344 |
+
agent = ReportAgent()
|
| 345 |
+
entries = [{
|
| 346 |
+
"movement": MovementResult(test_name="hurdle_step", side="na", confidence=1.0),
|
| 347 |
+
"features": _make_features("hurdle_step", side="na"),
|
| 348 |
+
"rubric_score": ScoreResult(score=2, rationale="", confidence=0.9),
|
| 349 |
+
"judge": JudgeResult(
|
| 350 |
+
score=2, rationale="", compensation_tags=[], corrective_hint="",
|
| 351 |
+
confidence=0.9,
|
| 352 |
+
),
|
| 353 |
+
"side": "na",
|
| 354 |
+
}]
|
| 355 |
+
result = agent.run(entries)
|
| 356 |
+
assert len(result.per_test) == 1
|
| 357 |
+
assert result.per_test[0]["score"] == 2
|
| 358 |
+
assert result.composite == 2
|
| 359 |
+
assert result.asymmetries == [] # no L/R pair β no asymmetry
|
| 360 |
+
|
| 361 |
def test_composite_none_when_unscored(self):
|
| 362 |
agent = ReportAgent()
|
| 363 |
entries = [{
|