Publish first RF4 fish quality upgrades
This commit is contained in:
@@ -136,6 +136,35 @@ def reconcile_queued_duplicates(path: Path) -> dict:
|
||||
return {"duplicates": duplicates, "queued": sum(item.get("status") == "queued" for item in manifest.get("assets", []))}
|
||||
|
||||
|
||||
def queue_quality_upgrades(path: Path, *, minimum_dimension: int = 256) -> dict:
|
||||
"""Queue alternatives to low-resolution published fish without unpublishing them."""
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
assets = manifest.get("assets", [])
|
||||
low_urls = {
|
||||
item.get("asset_url")
|
||||
for item in assets
|
||||
if item.get("status") == "approved"
|
||||
and item.get("entity_type") == "fish"
|
||||
and min(int(item.get("width") or 0), int(item.get("height") or 0)) < minimum_dimension
|
||||
}
|
||||
queued = 0
|
||||
for item in assets:
|
||||
if (
|
||||
item.get("status") == "duplicate"
|
||||
and item.get("entity_type") == "fish"
|
||||
and item.get("duplicate_of") in low_urls
|
||||
):
|
||||
item["status"] = "upgrade_queued"
|
||||
queued += 1
|
||||
manifest["updated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return {
|
||||
"minimum_dimension": minimum_dimension,
|
||||
"low_resolution_published": len(low_urls),
|
||||
"upgrade_queued": queued,
|
||||
}
|
||||
|
||||
|
||||
def approve_stored_assets(path: Path, *, note: str) -> dict:
|
||||
"""Publish every stored asset after an explicit owner-level approval."""
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
@@ -157,6 +186,49 @@ def approve_stored_assets(path: Path, *, note: str) -> dict:
|
||||
return {"approved": approved}
|
||||
|
||||
|
||||
def publish_quality_upgrades(path: Path, *, note: str, minimum_dimension: int = 256) -> dict:
|
||||
"""Atomically promote every reviewed quality candidate and retain its fallback."""
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
assets = manifest.get("assets", [])
|
||||
by_url = {item.get("asset_url"): item for item in assets}
|
||||
candidates = [item for item in assets if item.get("status") == "upgrade_stored"]
|
||||
|
||||
# Validate the complete set before changing any public mapping.
|
||||
for candidate in candidates:
|
||||
fallback = by_url.get(candidate.get("duplicate_of"))
|
||||
if not fallback or fallback.get("status") != "approved":
|
||||
raise ValueError(f"{candidate.get('asset_url')}: approved fallback is missing")
|
||||
if not fallback.get("entity_key"):
|
||||
raise ValueError(f"{candidate.get('asset_url')}: fallback has no entity_key")
|
||||
if min(int(candidate.get("width") or 0), int(candidate.get("height") or 0)) < minimum_dimension:
|
||||
raise ValueError(f"{candidate.get('asset_url')}: candidate is below {minimum_dimension}px")
|
||||
if not candidate.get("sha256") or not candidate.get("local_path"):
|
||||
raise ValueError(f"{candidate.get('asset_url')}: stored candidate metadata is incomplete")
|
||||
|
||||
reviewed_at = datetime.now(timezone.utc).isoformat()
|
||||
for candidate in candidates:
|
||||
fallback = by_url[candidate["duplicate_of"]]
|
||||
candidate.update({
|
||||
"status": "approved",
|
||||
"entity_key": fallback["entity_key"],
|
||||
"supersedes": fallback["asset_url"],
|
||||
"reviewed_at": reviewed_at,
|
||||
"review_note": note,
|
||||
})
|
||||
fallback.update({
|
||||
"status": "superseded",
|
||||
"replaced_by": candidate["asset_url"],
|
||||
"reviewed_at": reviewed_at,
|
||||
"review_note": note,
|
||||
})
|
||||
|
||||
manifest["updated_at"] = reviewed_at
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
temporary.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
temporary.replace(path)
|
||||
return {"published": len(candidates), "retained_fallbacks": len(candidates)}
|
||||
|
||||
|
||||
def reclassify_manifest(path: Path) -> dict:
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
for item in manifest.get("assets", []):
|
||||
@@ -200,7 +272,7 @@ def audit_media_catalog(root: Path) -> dict:
|
||||
status = str(item.get("status", "unknown"))
|
||||
statuses[status] = statuses.get(status, 0) + 1
|
||||
local_path = item.get("local_path")
|
||||
if status in {"stored", "approved"}:
|
||||
if status in {"stored", "upgrade_stored", "approved", "superseded"}:
|
||||
if not isinstance(local_path, str):
|
||||
issues.append(f"{item['asset_url']}: {status} asset has no local_path")
|
||||
continue
|
||||
@@ -262,7 +334,7 @@ def media_quality_report(root: Path, *, minimum_dimension: int = 256, display_di
|
||||
alternatives = [
|
||||
item for item in assets
|
||||
if item.get("entity_type") == "fish"
|
||||
and item.get("status") in {"duplicate", "queued"}
|
||||
and item.get("status") in {"duplicate", "queued", "upgrade_queued", "upgrade_stored"}
|
||||
and (item.get("duplicate_of") in low_urls or item.get("status") == "queued")
|
||||
]
|
||||
|
||||
@@ -290,6 +362,60 @@ def media_quality_report(root: Path, *, minimum_dimension: int = 256, display_di
|
||||
}
|
||||
|
||||
|
||||
def compare_quality_upgrades(root: Path, *, minimum_dimension: int = 256) -> dict:
|
||||
"""Compare stored upgrade candidates with their published fallbacks offline."""
|
||||
manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
|
||||
assets = manifest.get("assets", [])
|
||||
by_url = {item.get("asset_url"): item for item in assets}
|
||||
comparisons: list[dict] = []
|
||||
issues: list[str] = []
|
||||
for candidate in assets:
|
||||
if candidate.get("status") != "upgrade_stored":
|
||||
continue
|
||||
fallback = by_url.get(candidate.get("duplicate_of"))
|
||||
if not fallback or fallback.get("status") != "approved":
|
||||
issues.append(f"{candidate.get('asset_url')}: published fallback is missing")
|
||||
continue
|
||||
candidate_path = root / str(candidate.get("local_path") or "")
|
||||
fallback_path = root / str(fallback.get("local_path") or "")
|
||||
if not candidate_path.is_file() or not fallback_path.is_file():
|
||||
issues.append(f"{candidate.get('asset_url')}: comparison file is missing")
|
||||
continue
|
||||
with Image.open(candidate_path) as image:
|
||||
candidate_alpha = image.mode in {"LA", "RGBA"} or "transparency" in image.info
|
||||
with Image.open(fallback_path) as image:
|
||||
fallback_alpha = image.mode in {"LA", "RGBA"} or "transparency" in image.info
|
||||
candidate_width, candidate_height = int(candidate["width"]), int(candidate["height"])
|
||||
fallback_width, fallback_height = int(fallback["width"]), int(fallback["height"])
|
||||
candidate_ratio = candidate_width / candidate_height
|
||||
fallback_ratio = fallback_width / fallback_height
|
||||
comparisons.append({
|
||||
"label": candidate.get("label"),
|
||||
"candidate_url": candidate.get("asset_url"),
|
||||
"fallback_url": fallback.get("asset_url"),
|
||||
"candidate": {
|
||||
"width": candidate_width, "height": candidate_height,
|
||||
"bytes": candidate.get("bytes"), "content_type": candidate.get("content_type"),
|
||||
"alpha": candidate_alpha, "aspect_ratio": round(candidate_ratio, 4),
|
||||
},
|
||||
"fallback": {
|
||||
"width": fallback_width, "height": fallback_height,
|
||||
"bytes": fallback.get("bytes"), "content_type": fallback.get("content_type"),
|
||||
"alpha": fallback_alpha, "aspect_ratio": round(fallback_ratio, 4),
|
||||
},
|
||||
"meets_minimum": min(candidate_width, candidate_height) >= minimum_dimension,
|
||||
"aspect_ratio_matches": abs(candidate_ratio - fallback_ratio) < 0.01,
|
||||
})
|
||||
return {
|
||||
"minimum_dimension": minimum_dimension,
|
||||
"compared": len(comparisons),
|
||||
"meets_minimum": sum(item["meets_minimum"] for item in comparisons),
|
||||
"aspect_ratio_matches": sum(item["aspect_ratio_matches"] for item in comparisons),
|
||||
"issues": issues,
|
||||
"comparisons": comparisons,
|
||||
}
|
||||
|
||||
|
||||
def inspect_image(body: bytes) -> tuple[int, int, str]:
|
||||
try:
|
||||
with Image.open(io.BytesIO(body)) as image:
|
||||
|
||||
Reference in New Issue
Block a user