feat: add explicit media review workflow
This commit is contained in:
@@ -95,6 +95,31 @@ def reclassify_manifest(path: Path) -> dict:
|
||||
return manifest
|
||||
|
||||
|
||||
def review_asset(
|
||||
path: Path, *, asset_url: str, decision: str, entity_type: str | None = None,
|
||||
entity_key: str | None = None, note: str | None = None,
|
||||
) -> dict:
|
||||
if decision not in {"approved", "rejected"}:
|
||||
raise ValueError("decision must be approved or rejected")
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
item = next((asset for asset in manifest.get("assets", []) if asset["asset_url"] == asset_url), None)
|
||||
if item is None:
|
||||
raise ValueError("asset URL is not present in manifest")
|
||||
if decision == "approved":
|
||||
if item.get("status") != "stored":
|
||||
raise ValueError("only a stored asset can be approved")
|
||||
if entity_type not in {"fish", "waterbody", "tackle", "reference"} or not entity_key:
|
||||
raise ValueError("approved asset requires entity type and canonical key")
|
||||
item.update({"entity_type": entity_type, "entity_key": entity_key})
|
||||
item.update({
|
||||
"status": decision,
|
||||
"reviewed_at": datetime.now(timezone.utc).isoformat(),
|
||||
"review_note": note or None,
|
||||
})
|
||||
path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return item
|
||||
|
||||
|
||||
def inspect_image(body: bytes) -> tuple[int, int, str]:
|
||||
try:
|
||||
with Image.open(io.BytesIO(body)) as image:
|
||||
|
||||
@@ -8,7 +8,7 @@ import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from .community_cli import USER_AGENT, _StrictRedirectHandler, _validate_url_before_io, check_and_reserve, fetch_html, fetch_site_key
|
||||
from .media_assets import extract_media_candidates, merge_manifest, reclassify_manifest, store_asset
|
||||
from .media_assets import extract_media_candidates, merge_manifest, reclassify_manifest, review_asset, store_asset
|
||||
|
||||
|
||||
DEFAULT_ROOT = Path("data/media")
|
||||
@@ -52,10 +52,21 @@ def main(argv: list[str] | None = None) -> int:
|
||||
parser.add_argument("url", nargs="?")
|
||||
parser.add_argument("--download-one", action="store_true", help="Store one queued asset while respecting site cooldown")
|
||||
parser.add_argument("--reclassify", action="store_true", help="Reapply current conservative classifier without network access")
|
||||
parser.add_argument("--review-url", help="Review an asset already present in the manifest")
|
||||
parser.add_argument("--decision", choices=("approved", "rejected"))
|
||||
parser.add_argument("--entity-type", choices=("fish", "waterbody", "tackle", "reference"))
|
||||
parser.add_argument("--entity-key")
|
||||
parser.add_argument("--note")
|
||||
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
|
||||
parser.add_argument("--state-file", type=Path, default=Path(".cache/community-fetch-state.json"))
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
if args.review_url:
|
||||
if not args.decision:
|
||||
parser.error("--decision is required with --review-url")
|
||||
item = review_asset(args.root / "manifest.json", asset_url=args.review_url, decision=args.decision, entity_type=args.entity_type, entity_key=args.entity_key, note=args.note)
|
||||
print(f"{item['status']} {item['asset_url']}")
|
||||
return 0
|
||||
if args.reclassify:
|
||||
manifest = reclassify_manifest(args.root / "manifest.json")
|
||||
print(f"reclassified {len(manifest['assets'])} candidates")
|
||||
|
||||
Reference in New Issue
Block a user