"""Catatan satu kali jalan — bahan mentah untuk angka justifikasi biaya. Yang WAJIB tercatat dan gampang terlupa: backend yang benar-benar dipakai dan versi MinerU. Tanpa keduanya, angka CPU hari ini tidak sah dibandingkan dengan angka GPU bulan depan — padahal perbandingan itulah inti argumennya. Backend dibaca dari `_middle.json` keluaran MinerU (`_backend`), bukan dari config, supaya yang tercatat adalah kenyataan, bukan niat. """ from __future__ import annotations import json import platform from dataclasses import asdict, dataclass, field from datetime import datetime from pathlib import Path from typing import Any @dataclass class CatatanDokumen: doc_id: str source: str status: str # "ok" | "gagal" pages: int = 0 detik_parse: float = 0.0 detik_normalisasi: float = 0.0 dari_cache: bool = False jumlah_chunk: int = 0 backend_tercatat: str | None = None pemeriksaan: dict[str, Any] = field(default_factory=dict) galat: str | None = None @property def detik_per_halaman(self) -> float | None: return self.detik_parse / self.pages if self.pages else None @dataclass class Manifest: run_id: str dimulai: str config: dict[str, Any] mineru_version: str | None = None mesin: dict[str, str] = field(default_factory=dict) dokumen: list[CatatanDokumen] = field(default_factory=list) @classmethod def baru(cls, config: dict[str, Any]) -> Manifest: return cls( run_id=datetime.now().strftime("%Y%m%d-%H%M%S"), dimulai=datetime.now().isoformat(timespec="seconds"), config=config, mesin={ "os": platform.platform(), "python": platform.python_version(), "prosesor": platform.processor() or "?", }, ) def ringkasan(self) -> dict[str, Any]: ok = [d for d in self.dokumen if d.status == "ok"] halaman = sum(d.pages for d in ok) # Hanya run NYATA yang dihitung untuk kecepatan — hasil dari cache # akan membuat angkanya terlihat jauh lebih cepat dari kenyataan. nyata = [d for d in ok if not d.dari_cache] detik = sum(d.detik_parse for d in nyata) halaman_nyata = sum(d.pages for d in nyata) return { "dokumen_ok": len(ok), "dokumen_gagal": len(self.dokumen) - len(ok), "total_halaman": halaman, "halaman_terukur": halaman_nyata, "detik_parse_terukur": round(detik, 1), "detik_per_halaman": round(detik / halaman_nyata, 2) if halaman_nyata else None, "total_peringatan": sum( d.pemeriksaan.get("jumlah_peringatan", 0) for d in self.dokumen ), } def simpan(self, path: Path) -> None: isi = asdict(self) isi["ringkasan"] = self.ringkasan() path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(isi, indent=2, ensure_ascii=False), encoding="utf-8")