""" AksaraFramework — API publik untuk AKSARA CLM. Ini adalah entry point utama untuk developer yang membangun model bahasa Indonesia berbasis pemahaman, grounding semantik, dan reasoning struktural. CARA PAKAI: from aksara.framework import AksaraFramework fw = AksaraFramework.dari_kbbi("kbbi_core_v2.json") state = fw.proses("Makanan tradisional khas Dompu sangat lezat.") print(state.ringkasan()) PRINSIP DASAR: Model prediktif token: developer berfokus pada pemetaan urutan linguistik AKSARA CLM: developer berfokus pada proses(kalimat) → AksaraState yang interpretatif Framework ini menjamin: 1. Unit dasar = morfem (bukan token arbitrer) 2. Representasi = state dinamis di manifold semantik (bukan vektor statis) 3. Mekanisme = constraint propagation (bukan attention O(n²)) 4. Pengetahuan = eksplisit dari KBBI (bukan implisit di bobot) 5. Interpretabilitas = setiap keputusan bisa dijelaskan 6. Update = patch leksikon, tanpa retrain """ from __future__ import annotations from pathlib import Path from typing import Dict, List, Optional import torch from aksara.base.state import ( AksaraState, ViolationSpan, ConstraintSatisfaction, PelanggaranConstraint ) from aksara.primitives.lps.parser import LPSParser from aksara.primitives.lps.morfem import Morfem from aksara.primitives.sfm.lexicon import LexiconLoader from aksara.primitives.sfm.manifold import SemanticManifold from aksara.primitives.cpe.engine import CPEngine from aksara.primitives.cmc.composer import CMComposer from aksara.primitives.tda.analyzer import TDAnalyzer from aksara.primitives.krl.layer import KRLayer from aksara.config import AksaraConfig class AksaraFramework: """ Framework inti untuk menghasilkan Cognitive Language Model (CLM) berbasis bahasa Indonesia. Pipeline: Kalimat → LPS → SFM → CPE → CMC → TDA → KRL → AksaraState Setiap tahap punya justifikasi linguistik eksplisit: LPS: dekomposisi morfem deterministik dari aturan TBBBI SFM: representasi semantik dari struktur KBBI CPE: evaluasi constraint linguistik Indonesia CMC: verifikasi komposisi makna via category theory TDA: deteksi anomali topologis multi-skala KRL: representasi proposisi + frame + referensi (Primitif 6) Semua oposisi terhadap Transformer/Mamba dipertahankan: - Tidak ada mekanisme inferensi berbasis scores - Tidak ada komposisi makna berbasis weighted-sum global - Tidak ada embedding statis di Euclidean space - Tidak ada pengetahuan tersembunyi di bobot """ def __init__( self, leksikon: Optional[LexiconLoader] = None, device: torch.device = torch.device("cpu"), aktif_cmc: bool = True, aktif_tda: bool = True, aktif_krl: bool = True, cpe_max_iter: int = 10, threshold_semantik: float = 1.5, config: Optional[AksaraConfig] = None, ): if leksikon is None: leksikon = LexiconLoader() self.leksikon = leksikon self.device = device self._aktif_cmc = aktif_cmc self._aktif_tda = aktif_tda self._aktif_krl = aktif_krl self.config = config or AksaraConfig.default() # Bangun leksikon dict untuk LPS leksikon_dict = {k: v.kelas for k, v in leksikon._entri.items()} # threshold dari config menang atas parameter langsung _threshold = self.config.threshold_semantik if config else threshold_semantik # Inisialisasi semua primitif — config diinjeksi ke CPE dan CMC self.lps = LPSParser(leksikon=leksikon_dict) self.sfm = SemanticManifold(leksikon) self.cpe = CPEngine( self.sfm, max_iter=cpe_max_iter, threshold_semantik=_threshold, config=self.config, ) self.cmc = CMComposer(leksikon, config=self.config) if aktif_cmc else None self.tda = TDAnalyzer( self.sfm.geodesic, threshold_edge=_threshold, ) if aktif_tda else None self.krl = KRLayer(leksikon) if aktif_krl else None @classmethod def dari_kbbi( cls, kbbi_path: str = "kbbi_core_v2.json", device: Optional[torch.device] = None, config: Optional[AksaraConfig] = None, **kwargs, ) -> "AksaraFramework": """ Factory method: bangun framework langsung dari file KBBI. Args: kbbi_path: path ke kbbi_core_v2.json device: torch device (default: cpu) config: AksaraConfig opsional untuk domain khusus (hukum, kesehatan, militer, pertanahan, dll.) Default: AksaraConfig.default() = bahasa Indonesia umum **kwargs: parameter tambahan ke AksaraFramework.__init__ Contoh: # Bahasa Indonesia umum fw = AksaraFramework.dari_kbbi('kbbi_core_v2.json') # Domain hukum from aksara.config import AksaraConfig fw = AksaraFramework.dari_kbbi( 'kbbi_core_v2.json', config=AksaraConfig.untuk_domain('hukum') ) """ if not Path(kbbi_path).exists(): if kbbi_path == "halo": kbbi_path = "kbbi_core_v2.json" else: raise FileNotFoundError(f"KBBI tidak ditemukan: {kbbi_path}") leksikon = LexiconLoader() n = leksikon.muat_kbbi(kbbi_path) if n == 0: raise ValueError(f"KBBI gagal dimuat dari: {kbbi_path}") device = device or torch.device("cpu") return cls(leksikon, device=device, config=config, **kwargs) def proses(self, kalimat: str) -> AksaraState: """ Proses satu kalimat melalui seluruh pipeline AKSARA. Pipeline: 1. LPS → dekomposisi morfem 2. SFM → encode ke tensor semantik 3. CPE → evaluasi constraint, hitung energi 4. CMC → verifikasi komposisi makna (jika aktif) 5. TDA → analisis topologis (jika aktif) 6. KRL → representasi proposisi + frame + referensi (jika aktif) 7. POST → bangun ViolationSpan + ConstraintSatisfaction Args: kalimat: string kalimat bahasa Indonesia Returns: AksaraState lengkap dengan skor_linguistik, violation_spans, constraint_satisfaction, dan penjelasan per dimensi """ # ── Tahap 1: LPS ───────────────────────────────────────────────────── morfem_list = self.lps.parse(kalimat) if not morfem_list: return self._state_kosong(kalimat) # ── Tahap 2: SFM ───────────────────────────────────────────────────── sfm_tensor = self.sfm.encode_kalimat(morfem_list, device=self.device) # ── Tahap 3: CPE ───────────────────────────────────────────────────── state = self.cpe(morfem_list, sfm_tensor=sfm_tensor, device=self.device) # ── Tahap 4: CMC ───────────────────────────────────────────────────── if self.cmc is not None and self._aktif_cmc: state = self.cmc.perkaya_state(state, morfem_list) # ── Tahap 5: TDA ───────────────────────────────────────────────────── if self.tda is not None and self._aktif_tda: state = self.tda.perkaya_state(state, morfem_list) # ── Tahap 6: KRL ────────────────────────────────────────────────────── if self.krl is not None and self._aktif_krl: state.krl_result = self.krl.proses(morfem_list, kalimat) # ── Tahap 7: Post-processing — Violation Localization & Satisfaction ── state.violation_spans = self._bangun_violation_spans( kalimat, morfem_list, state.pelanggaran ) state.constraint_satisfaction = self._bangun_constraint_satisfaction( state.energi_per_dimensi, state.pelanggaran, n_edge=state.metadata.get("n_edge", max(len(morfem_list) - 1, 1)) ) morfologi = [ { "teks": m.teks_asli, "root": m.root, "afiks": getattr(m, "afiks", []), "kelas_kata": getattr(m, "kelas", getattr(m, "kelas_kata", "")), "peran_gramatikal": getattr(m, "peran_gramatikal", ""), } for m in morfem_list ] state.morfologi = morfologi state.semantik = state.energi_per_dimensi state.interpretasi = state.jelaskan() state.kata = [m.teks_asli for m in morfem_list] state.tokens = state.kata state.pos = [getattr(m, "kelas", getattr(m, "kelas_kata", "")) for m in morfem_list] return state def __getattr__(self, name: str): if name == "tokens": return [] if name == "kata": return [] if name == "pos": return [] if name == "morfologi": return [] if name == "semantik": return {} if name == "interpretasi": return "" raise AttributeError(name) def _bangun_violation_spans( self, teks: str, morfem_list: List[Morfem], pelanggaran: List[PelanggaranConstraint], ) -> List[ViolationSpan]: """ Petakan pelanggaran ke posisi karakter di teks asli. Strategi: bangun indeks root→(mulai, akhir) dari posisi token di teks, lalu untuk setiap pelanggaran, cari token terlibat dan petakan ke span. """ # Bangun peta root → (mulai_char, akhir_char, teks_asli_token) root_to_span: Dict[str, tuple] = {} cursor = 0 teks_lower = teks.lower() for m in morfem_list: token = m.teks_asli token_lower = token.lower() # Cari posisi token mulai dari cursor pos = teks_lower.find(token_lower, cursor) if pos == -1: # Fallback: cari dari awal (untuk kasus normalisasi) pos = teks_lower.find(token_lower, 0) if pos != -1: root_to_span[m.root.lower()] = (pos, pos + len(token), token) root_to_span[token_lower] = (pos, pos + len(token), token) cursor = pos + len(token) spans: List[ViolationSpan] = [] seen_spans: set = set() for p in pelanggaran: for root in p.token_terlibat: root_key = root.lower() if root_key not in root_to_span: continue mulai, akhir, token_asli = root_to_span[root_key] span_key = (mulai, akhir, p.dimensi) if span_key in seen_spans: continue seen_spans.add(span_key) spans.append(ViolationSpan( mulai=mulai, akhir=akhir, token=token_asli, root=root, dimensi=p.dimensi, severitas=p.severitas, penjelasan=p.penjelasan, )) # Urutkan berdasarkan posisi di teks spans.sort(key=lambda s: s.mulai) return spans def _bangun_constraint_satisfaction( self, energi_per_dimensi: Dict[str, float], pelanggaran: List[PelanggaranConstraint], n_edge: int, ) -> ConstraintSatisfaction: """ Hitung constraint satisfaction per dimensi. Formula: satisfaction_d = 1.0 - energi_d di mana energi_d sudah dinormalisasi per edge oleh CPEngine (∈ [0,1]). Penalti tambahan per pelanggaran berat (severitas > 0.5): satisfaction_d -= 0.1 × n_pelang_berat_d / n_edge Ini memastikan kalimat dengan banyak pelanggaran mendapat skor lebih rendah bahkan jika energi rata-ratanya masih kecil. """ def sat(dim: str) -> float: e = energi_per_dimensi.get(dim, 0.0) # Hitung penalti dari pelanggaran berat di dimensi ini n_berat = sum( 1 for p in pelanggaran if p.dimensi == dim and p.severitas > 0.5 ) penalti = 0.1 * n_berat / max(n_edge, 1) return max(0.0, min(1.0, 1.0 - e - penalti)) return ConstraintSatisfaction( morfologis=sat("morfologis"), sintaktis =sat("sintaktis"), semantik =sat("semantik"), leksikal =sat("leksikal"), topologis =sat("topologis"), animasi =sat("animasi"), ) def proses_batch(self, kalimat_list: List[str]) -> List[AksaraState]: """Proses sekumpulan kalimat.""" return [self.proses(k) for k in kalimat_list] def tambah_kata( self, kata: str, kelas: str, domain: Optional[str] = None, sinonim: Optional[List[str]] = None, antonim: Optional[List[str]] = None, ) -> None: """ Tambah kata baru ke leksikon — update pengetahuan tanpa retrain. OPOSISI TRANSFORMER: Transformer: menambah pengetahuan = retrain seluruh model AKSARA: menambah pengetahuan = satu baris di sini, langsung berlaku Args: kata: lemma kata baru kelas: POS tag ('n', 'v', 'adj', dll.) domain: domain semantik (opsional) sinonim: list sinonim (opsional) antonim: list antonim (opsional) """ self.leksikon.tambah_entri( kata=kata, kelas=kelas, domain=domain, sinonim=sinonim, antonim=antonim, layer="custom", ) kata_lower = kata.lower() # Update leksikon dict di LPS parser self.lps.leksikon[kata_lower] = kelas # Invalidate SFM cache untuk kata ini dan semua yang berrelasi self.sfm._cache.pop(kata_lower, None) for s in (sinonim or []): self.sfm._cache.pop(s.lower(), None) # Invalidate geodesic cache untuk pasangan yang melibatkan kata ini keys_to_remove = [ k for k in self.sfm.geodesic._cache if kata_lower in k ] for k in keys_to_remove: self.sfm.geodesic._cache.pop(k, None) # Update leksikon_dict di sfm juga self.sfm.leksikon_dict[kata_lower] = kelas def info(self) -> Dict: """Ringkasan konfigurasi CLM.""" return { "leksikon_size": self.leksikon.ukuran, "n_domain": self.leksikon.n_domain, "sfm_dim": self.sfm.d_output, "cpe_max_iter": self.cpe.max_iter, "aktif_cmc": self._aktif_cmc, "aktif_tda": self._aktif_tda, "device": str(self.device), } def _state_kosong(self, teks: str) -> AksaraState: from dataclasses import replace return AksaraState( teks_asli=teks, morfem_states=[], energi_total=0.0, energi_per_dimensi={}, pelanggaran=[], register="formal", kelengkapan_struktur=0.0, )