second pass. low

This commit is contained in:
Davide Scaini
2026-04-01 19:00:28 +02:00
parent 3d364c3992
commit bd5831c2fd
11 changed files with 277 additions and 62 deletions
+65 -21
View File
@@ -46,6 +46,9 @@ def _process_file(path: Path) -> dict:
"""Runs inside a worker process. Only receives a Path (tiny pickle).
All heavy shared data (_known_hashes, _strava_lookup, etc.) is already
in the worker's memory from the initializer — zero per-task overhead.
Writes to pending files (not final paths) so the main process can
arbitrate collisions and pick the best version.
"""
from bincio.extract.metrics import compute
from bincio.extract.parsers.factory import parse_file
@@ -80,11 +83,17 @@ def _process_file(path: Path) -> dict:
activity, metrics, _output_dir,
privacy=_privacy,
rdp_epsilon=_rdp_epsilon,
pending=True,
)
summary = build_summary(activity, metrics, activity_id, _privacy)
except Exception as exc:
return {"status": "error", "path": str(path), "error": str(exc)}
# Quality signals for the main process to compare competing results
sensor_channels = sum(1 for v in [
metrics.avg_hr_bpm, metrics.avg_power_w, metrics.avg_cadence_rpm,
] if v is not None)
return {
"status": "ok",
"summary": summary,
@@ -94,6 +103,8 @@ def _process_file(path: Path) -> dict:
"distance_m": metrics.distance_m,
"source": summary.get("source"),
"mmp": metrics.mmp,
"point_count": len(activity.points),
"sensor_channels": sensor_channels,
}
@@ -177,6 +188,8 @@ def extract(
summaries: list[dict] = []
errors: list[tuple[str, str]] = []
skipped = 0
# Collect all pending results, grouped by activity_id for collision arbitration
pending_by_id: dict[str, list[dict]] = {}
with Progress(
TextColumn("[progress.description]{task.description}"),
@@ -202,30 +215,61 @@ def extract(
elif result["status"] == "error":
errors.append((result["path"], result["error"]))
else:
# Near-duplicate check — must be sequential (stateful)
from datetime import datetime
started_at = datetime.fromisoformat(result["started_at"])
near_id = dedup.find_near_duplicate(started_at, result["distance_m"])
pending_by_id.setdefault(result["id"], []).append(result)
if near_id:
canonical = dedup.pick_canonical(near_id, result.get("source"))
if canonical != "__new__":
_patch_duplicate_of(cfg.output_dir, result["id"], near_id)
skipped += 1
continue
_patch_duplicate_of(cfg.output_dir, near_id, result["id"])
dedup._records[near_id].duplicate_of = result["id"]
# ── Arbitrate collisions and finalize pending files ───────────────────────
from bincio.extract.writer import (
activity_quality, cleanup_pending, finalize_pending, write_athlete_json, write_index,
)
dedup.register(ActivityRecord(
id=result["id"],
source_hash=result["hash"],
started_at=started_at,
distance_m=result["distance_m"],
source=result.get("source"),
))
summaries.append(result["summary"])
for activity_id, candidates in pending_by_id.items():
# Pick the best candidate by quality score
candidates.sort(key=activity_quality, reverse=True)
winner = candidates[0]
# Clean up losing candidates' pending files
for loser in candidates[1:]:
cleanup_pending(cfg.output_dir, activity_id, loser["hash"])
skipped += 1
# Near-duplicate check against already-known activities
from datetime import datetime
started_at = datetime.fromisoformat(winner["started_at"])
near_id = dedup.find_near_duplicate(started_at, winner["distance_m"])
if near_id:
canonical = dedup.pick_canonical(near_id, winner.get("source"))
if canonical != "__new__":
# Existing is better — finalize winner as duplicate, then patch it
final_id = finalize_pending(cfg.output_dir, activity_id, winner["hash"])
_patch_duplicate_of(cfg.output_dir, final_id, near_id)
skipped += 1
continue
# New is better — patch the existing one as duplicate
final_id = finalize_pending(cfg.output_dir, activity_id, winner["hash"])
_patch_duplicate_of(cfg.output_dir, near_id, final_id)
dedup._records[near_id].duplicate_of = final_id
else:
final_id = finalize_pending(cfg.output_dir, activity_id, winner["hash"])
# Update summary with the finalized ID (may include hash suffix)
summary = winner["summary"]
if final_id != activity_id:
summary = dict(summary)
summary["id"] = final_id
summary["detail_url"] = f"activities/{final_id}.json"
if summary.get("track_url"):
summary["track_url"] = f"activities/{final_id}.geojson"
dedup.register(ActivityRecord(
id=final_id,
source_hash=winner["hash"],
started_at=started_at,
distance_m=winner["distance_m"],
source=winner.get("source"),
))
summaries.append(summary)
from bincio.extract.writer import write_athlete_json, write_index
existing = _load_existing_summaries(cfg.output_dir)
merged = {s["id"]: s for s in existing}
for s in summaries: