Download src/metadata/metadata_exporter.py from InternsProgressUpdate/Venket: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/InternsProgressUpdate/Venket/resolve/main/src/metadata/metadata_exporter.py
- Command line
-
hf download hf://InternsProgressUpdate/Venket/src/metadata/metadata_exporter.py
-
curl -L -o metadata_exporter.py https://huggingface.co/InternsProgressUpdate/Venket/resolve/main/src/metadata/metadata_exporter.py
10.2 kB
| """Assembles and writes the complete per-sample metadata JSON. | |
| The exported schema always includes every documented field -- augmentation | |
| parameters that were not applied are still present, with a neutral value | |
| (``0``/``null``/``false``) rather than being omitted, so downstream | |
| consumers can rely on a stable schema across the whole dataset. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from typing import Any | |
| import numpy as np | |
| from src.analysis.difficulty_analyzer import DifficultyResult | |
| from src.analysis.geometry_analyzer import GeometryResult | |
| from src.augmentation.augmentation_engine import AugmentationRecord | |
| from src.config.schema import QualityConfig | |
| from src.placement.placement_engine import PlacementResult | |
| from src.rendering.renderer import RenderResult | |
| from src.signatures.signature_dataset import SignatureRecord | |
| from src.utils.logging_utils import get_logger | |
| logger = get_logger(__name__) | |
| def _json_default(obj: Any) -> Any: | |
| if isinstance(obj, np.integer): | |
| return int(obj) | |
| if isinstance(obj, np.floating): | |
| return float(obj) | |
| if isinstance(obj, np.bool_): | |
| return bool(obj) | |
| if isinstance(obj, np.ndarray): | |
| return obj.tolist() | |
| if isinstance(obj, Path): | |
| return obj.as_posix() | |
| return str(obj) | |
| class MetadataExporter: | |
| """Builds and writes the full per-sample metadata JSON document. | |
| Example: | |
| >>> exporter = MetadataExporter() | |
| >>> metadata = exporter.build(...) | |
| >>> exporter.export(metadata, "output/sample_000001.json") | |
| """ | |
| def build( | |
| self, | |
| document_id: str, | |
| template_id: str, | |
| document_type: "str | None", | |
| page: int, | |
| page_size: tuple[int, int], | |
| dpi: int, | |
| signer_name: str, | |
| signer_role: str, | |
| signature_source: "SignatureRecord | None", | |
| placement: PlacementResult, | |
| render: RenderResult, | |
| signature_aug: dict[str, AugmentationRecord], | |
| document_aug: dict[str, AugmentationRecord], | |
| geometry: GeometryResult, | |
| difficulty: DifficultyResult, | |
| quality_cfg: QualityConfig, | |
| quality_values: dict[str, float], | |
| seed: int, | |
| sample_index: int, | |
| verification: "dict[str, Any] | None" = None, | |
| ) -> dict[str, Any]: | |
| """Assemble the complete metadata dict for one generated sample.""" | |
| degraded = self._compute_degraded(quality_cfg) | |
| augmentation_section = self._build_augmentation_section(placement, render, signature_aug, document_aug) | |
| metadata: dict[str, Any] = { | |
| "document_id": document_id, | |
| "template": template_id, | |
| "document_type": document_type, | |
| "page": page, | |
| "page_size": list(page_size), | |
| "dpi": dpi, | |
| "signer": { | |
| "name": signer_name, | |
| "role": signer_role, | |
| }, | |
| "signature_source": ( | |
| { | |
| "signature_id": signature_source.signature_id, | |
| "path": signature_source.path.as_posix(), | |
| "signer_group": signature_source.signer_group, | |
| "source_type": signature_source.source_type, | |
| "script": signature_source.script, | |
| } | |
| if signature_source is not None | |
| else {"signature_id": None, "path": None, "signer_group": None, "source_type": "none", "script": None} | |
| ), | |
| "placement": { | |
| "x_offset": placement.x_offset, | |
| "y_offset": placement.y_offset, | |
| "rotation_deg": round(placement.rotation_deg, 3), | |
| "scale_factor": round(placement.scale_factor, 4), | |
| "anchor_point": placement.anchor_point, | |
| "margin_target_box": list(placement.target_box), | |
| "misplaced": placement.misplaced, | |
| "misplacement_dx": round(placement.misplacement_dx, 2), | |
| "misplacement_dy": round(placement.misplacement_dy, 2), | |
| }, | |
| "rendering": { | |
| "blend_mode": render.blend_mode, | |
| "ink_opacity": render.ink_opacity, | |
| "blur_sigma": render.blur_sigma, | |
| "feather_px": render.feather_px, | |
| "scanner_noise": render.scanner_noise, | |
| }, | |
| "difficulty": { | |
| "score": difficulty.score, | |
| "tier": difficulty.tier, | |
| "misplacement": difficulty.misplacement, | |
| "whitespace": difficulty.whitespace, | |
| "faintness": difficulty.faintness, | |
| "smallness": difficulty.smallness, | |
| "low_texture": difficulty.low_texture, | |
| }, | |
| "quality": { | |
| "degraded": degraded, | |
| "case_type": quality_cfg.case_type, | |
| "notes": quality_cfg.notes, | |
| "scanner_quality": quality_values["scanner_quality"], | |
| "paper_quality": quality_values["paper_quality"], | |
| "render_quality": quality_values["render_quality"], | |
| }, | |
| "verification": verification or { | |
| "case_type": None, "verdict": None, "reject_code": None, | |
| "target_box_id": None, "expected_signer_group": None, "expected_signer_script": None, | |
| "actual_signer_group": None, "actual_source_type": None, "actual_signer_script": None, | |
| "companion": None, | |
| }, | |
| "augmentation": augmentation_section, | |
| "geometry": { | |
| "box_px": list(geometry.target_box), | |
| "signature_bbox_px": list(geometry.signature_bbox_px), | |
| "target_box": list(geometry.target_box), | |
| "actual_box": list(geometry.actual_box), | |
| "search_box_px": list(geometry.search_box), | |
| "signature_polygon_px": geometry.signature_polygon_px, | |
| "convex_hull_px": geometry.convex_hull_px, | |
| "min_rotated_rect": geometry.min_rotated_rect, | |
| "centroid_px": list(geometry.centroid_px), | |
| "orientation_deg": geometry.orientation_deg, | |
| "ink_pixels": geometry.ink_pixels, | |
| "detected": geometry.found, | |
| }, | |
| "metrics": { | |
| "box_coverage_pct": geometry.box_coverage_pct, | |
| "containment_pct": geometry.containment_pct, | |
| "ink_fill_ratio": geometry.ink_fill_ratio, | |
| "polygon_overlap_proxy": geometry.polygon_overlap_proxy, | |
| "distance_from_box_center_px": geometry.distance_from_box_center_px, | |
| "margin_utilization": geometry.margin_utilization, | |
| "mean_dark_intensity": geometry.mean_dark_intensity, | |
| "gray_stddev": geometry.gray_stddev, | |
| "signature_area_ratio": geometry.signature_area_ratio, | |
| }, | |
| "provenance": { | |
| "master_seed": seed, | |
| "sample_index": sample_index, | |
| }, | |
| } | |
| return metadata | |
| def _compute_degraded(quality_cfg: QualityConfig) -> bool: | |
| case_type_flag = quality_cfg.case_type == "1d" | |
| notes_flag = bool(quality_cfg.notes and "low-quality" in quality_cfg.notes.lower()) | |
| return bool(case_type_flag or notes_flag) | |
| def _build_augmentation_section( | |
| placement: PlacementResult, render: RenderResult, | |
| signature_aug: dict[str, AugmentationRecord], document_aug: dict[str, AugmentationRecord], | |
| ) -> dict[str, Any]: | |
| def rec(records: dict[str, AugmentationRecord], name: str) -> AugmentationRecord: | |
| return records[name] | |
| stroke_rec = rec(signature_aug, "stroke_dilation") | |
| noise_rec = rec(document_aug, "scanner_noise") | |
| stain_rec = rec(document_aug, "stain") | |
| shadow_rec = rec(document_aug, "shadow") | |
| coffee_rec = rec(document_aug, "coffee_stain") | |
| crumple_rec = rec(document_aug, "crumple") | |
| tilt_rec = rec(document_aug, "page_tilt") | |
| discoloration_rec = rec(document_aug, "discoloration") | |
| jpeg_rec = rec(document_aug, "jpeg_compression") | |
| effects: dict[str, Any] = {} | |
| for name, r in {**signature_aug, **document_aug}.items(): | |
| effects[name] = { | |
| "enabled": r.enabled, | |
| "applied": r.applied, | |
| "strength": r.strength, | |
| "probability": r.probability, | |
| "seed": r.seed, | |
| "params": r.params, | |
| } | |
| return { | |
| "rotate_deg": round(placement.rotation_deg, 3), | |
| "scale_factor": round(placement.scale_factor, 4), | |
| "stroke": stroke_rec.strength if stroke_rec.applied else 0.0, | |
| "noise_amount": noise_rec.strength if noise_rec.applied else 0.0, | |
| "stain_type": stain_rec.params.get("resolved_type") if stain_rec.applied else None, | |
| "stain_strength": stain_rec.strength if stain_rec.applied else 0.0, | |
| "shadow_type": shadow_rec.params.get("resolved_type") if shadow_rec.applied else None, | |
| "shadow_strength": shadow_rec.strength if shadow_rec.applied else 0.0, | |
| "coffee_stain_strength": coffee_rec.strength if coffee_rec.applied else 0.0, | |
| "crumple_strength": crumple_rec.strength if crumple_rec.applied else 0.0, | |
| "tilt_deg": tilt_rec.params.get("tilt_deg", 0.0) if tilt_rec.applied else 0.0, | |
| "discoloration_strength": discoloration_rec.strength if discoloration_rec.applied else 0.0, | |
| "blur_sigma": render.blur_sigma, | |
| "jpeg_quality": jpeg_rec.params.get("quality") if jpeg_rec.applied else None, | |
| "scanner_noise": render.scanner_noise, | |
| "effects": effects, | |
| } | |
| def export(self, metadata: dict[str, Any], path: "str | Path") -> None: | |
| """Write the metadata dict to disk as pretty-printed JSON.""" | |
| path = Path(path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(json.dumps(metadata, indent=2, default=_json_default), encoding="utf-8") | |