| """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 |
| 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) |
| |
| |
| 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") |
|
|