fix: count unique media coverage
This commit is contained in:
@@ -40,7 +40,7 @@ RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изо
|
||||
|
||||
Медиасборщик индексирует разрешённые изображения отдельно от публичного каталога: manifest хранит исходную страницу, URL, предполагаемый тип сущности и время обнаружения, а оригиналы сохраняются по SHA-256 без hotlink. Индексация страницы и загрузка каждого файла используют общий 30-минутный cooldown домена; непроверенный asset не публикуется автоматически. Локальный `media_cli --audit` без сетевых запросов проверяет хэши, файлы, MIME, размеры, approved-сопоставления и отсутствие бесхозных оригиналов.
|
||||
|
||||
`python -m rf4_research.media_cli --coverage` сравнивает manifest с датированным `data/media/catalog-baseline.json`: сейчас кандидатами не покрыты минимум 21 рыба и 17 водоёмов, а до ручного review не подтверждены 251 рыба и все 19 водоёмов. Общий target снастей остаётся `null`, пока разрешённый источник не отдаст проверяемый полный счётчик.
|
||||
`python -m rf4_research.media_cli --coverage` сравнивает manifest с датированным `data/media/catalog-baseline.json`: отдельно считает файлы, уникальные нормализованные подписи и кандидатов без подписи, поэтому дубли не завышают покрытие. Сейчас не покрыты минимум 21 рыба и 17 водоёмов, а до ручного review не подтверждены 251 рыба и все 19 водоёмов. Общий target снастей остаётся `null`, пока разрешённый источник не отдаст проверяемый полный счётчик.
|
||||
|
||||
Актуальный внешний ориентир — 19 водоёмов и 252 вида рыб; локальная альфа пока содержит 2+2 сущности. Media-manifest включает 452 кандидата: 231 изображение рыбы, 146 приманок, 2 карты и 73 справочных изображения. Вручную проверены и сопоставлены 1 рыба, 2 приманки и 1 официальная схема; 446 файлов остаются в очереди, 1 URL признан невалидным. Полное число «снастей» пока не заявляется: приманки — лишь одна часть каталога наряду с удилищами, катушками, лесками, крючками и оснастками.
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
- [x] **B08 · Семейства силуэтов рыб.** Три случайных hash-варианта заменены классификатором и отдельными формами `pike`, `salmonid`, `cyprinid`, `perch`, `catfish`, `eel`, `flatfish`, `marine`, плюс честный `generic`. Компонент допускает ручное переопределение family; название остаётся главным идентификатором.
|
||||
- [x] **B09 · Глифы снастей и приманок.** Добавлены SVG-глифы `spinner`, `wobbler`, `soft`, `boilie`, `worm`, `rig`, `unknown` и стабильная палитра по normalized name. Классификация срабатывает только по явным словам; глиф сопровождает текст в activity, лидере, уловах, рекордах и списке лучших приманок, не выдавая категорию за точную модель.
|
||||
- [x] **B10 · Визуальные отпечатки водоёмов.** Для каждого slug воспроизводимо выбираются один из восьми береговых контуров, число волн, положение точки и двухсимвольный индекс. Знак используется в каталоге и detail-hero; это явно абстрактный отпечаток, а не карта или игровая география.
|
||||
- [ ] **B11 · Разрешённый media pipeline — в работе.** Версионированный baseline на 12.09.2026 содержит 19 водоёмов и 252 рыбы; полное число снастей неизвестно и остаётся `null`, а не подменяется числом приманок. Воспроизводимый `media_cli --coverage` показывает: 231/252 fish-кандидатов (1 approved), 2/19 waterbody (0 approved), 146 tackle/bait (2 approved); ещё одна официальная Zig-Rig схема approved как reference. Следующие gaps: минимум 21 рыба, 17 водоёмов и все категории снастей за пределами найденных приманок; получить канонические перечни и устойчивые ID, затем загружать не более одного asset с домена за окно и вручную сопоставлять. RF4DB вернул 403 — повторять только после cooldown. Официальные `/media/fische/`, `/media/levels/` и `.bait_icon` records исследовать только через публичные контракты без обхода защиты.
|
||||
- [ ] **B11 · Разрешённый media pipeline — в работе.** Версионированный baseline на 12.09.2026 содержит 19 водоёмов и 252 рыбы; полное число снастей неизвестно и остаётся `null`, а не подменяется числом приманок. Воспроизводимый `media_cli --coverage` считает уникальные нормализованные подписи, а не файлы: 231/252 fish-кандидатов (1 approved), 2/19 waterbody (0 approved), 146 tackle/bait (2 approved); ещё одна официальная Zig-Rig схема approved как reference. Следующие gaps: минимум 21 рыба, 17 водоёмов и все категории снастей за пределами найденных приманок; получить канонические перечни и устойчивые ID, затем загружать не более одного asset с домена за окно и вручную сопоставлять. RF4DB вернул 403 — повторять только после cooldown. Официальные `/media/fische/`, `/media/levels/` и `.bait_icon` records исследовать только через публичные контракты без обхода защиты.
|
||||
- [x] **B12 · Эмблема сочетания.** Страница «водоём + рыба» получила составной атласный seal: собственный отпечаток водоёма пересекается со смысловым силуэтом рыбы. Так визуальная идентичность сопровождает всю иерархию каталога и не требует внешних изображений.
|
||||
- [x] **B13 · Навигационная леска атласа.** Разрозненные ссылки назад на detail-страницах заменены доступной breadcrumb-цепочкой с мотивом лески и узлов. Страница точки связывает главную, водоём и координаты; сочетание — каталог, водоём и рыбу. Текущий узел всегда подписан текстом и отмечен `aria-current`.
|
||||
- [x] **B14 · Атласные переходы сущностей.** Боковые списки рыб и водоёмов на detail-страницах получили компактные силуэты и отпечатки рядом с полным текстовым названием. Знаки продолжают систему каталога в рабочей навигации, а стрелка явно показывает переход к странице сочетания.
|
||||
|
||||
@@ -169,13 +169,20 @@ def media_coverage(root: Path) -> dict:
|
||||
result: dict[str, dict] = {}
|
||||
for entity_type, target in baseline["entities"].items():
|
||||
candidates = [item for item in manifest.get("assets", []) if item.get("entity_type") == entity_type]
|
||||
candidate_labels = {
|
||||
" ".join(item["label"].split()).casefold()
|
||||
for item in candidates
|
||||
if isinstance(item.get("label"), str) and item["label"].strip()
|
||||
}
|
||||
approved_keys = {item.get("entity_key") for item in candidates if item.get("status") == "approved" and item.get("entity_key")}
|
||||
expected = target.get("count")
|
||||
result[entity_type] = {
|
||||
"expected": expected,
|
||||
"candidates": len(candidates),
|
||||
"candidate_assets": len(candidates),
|
||||
"unique_candidates": len(candidate_labels),
|
||||
"unidentified_assets": sum(1 for item in candidates if not isinstance(item.get("label"), str) or not item["label"].strip()),
|
||||
"approved": len(approved_keys),
|
||||
"candidate_gap": max(expected - len(candidates), 0) if isinstance(expected, int) else None,
|
||||
"candidate_gap": max(expected - len(candidate_labels), 0) if isinstance(expected, int) else None,
|
||||
"approved_gap": max(expected - len(approved_keys), 0) if isinstance(expected, int) else None,
|
||||
}
|
||||
return {"baseline_date": baseline["verified_at"], "entities": result}
|
||||
|
||||
@@ -107,5 +107,20 @@ def test_media_coverage_keeps_unknown_tackle_total_honest(tmp_path: Path) -> Non
|
||||
encoding="utf-8",
|
||||
)
|
||||
report = media_coverage(tmp_path)["entities"]
|
||||
assert report["fish"] == {"expected": 2, "candidates": 2, "approved": 1, "candidate_gap": 0, "approved_gap": 1}
|
||||
assert report["fish"] == {
|
||||
"expected": 2, "candidate_assets": 2, "unique_candidates": 0,
|
||||
"unidentified_assets": 2, "approved": 1, "candidate_gap": 2, "approved_gap": 1,
|
||||
}
|
||||
assert report["tackle"]["candidate_gap"] is None
|
||||
|
||||
|
||||
def test_media_coverage_deduplicates_candidate_labels(tmp_path: Path) -> None:
|
||||
(tmp_path / "manifest.json").write_text(
|
||||
'{"assets":[{"entity_type":"fish","status":"queued","label":" Ерш "},'
|
||||
'{"entity_type":"fish","status":"queued","label":"ерш"}]}', encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "catalog-baseline.json").write_text(
|
||||
'{"verified_at":"2026-09-12","entities":{"fish":{"count":2}}}', encoding="utf-8",
|
||||
)
|
||||
fish = media_coverage(tmp_path)["entities"]["fish"]
|
||||
assert (fish["candidate_assets"], fish["unique_candidates"], fish["candidate_gap"]) == (2, 1, 1)
|
||||
|
||||
Reference in New Issue
Block a user