Metadata-Version: 2.4
Name: fktd
Version: 0.1.1
Summary: FKTD — Fraud KYC KTP Detector: OCR + strict cross-validation for Indonesian ID cards (KTP).
Project-URL: Homepage, https://github.com/rezayw/fktd
Project-URL: Issues, https://github.com/rezayw/fktd/issues
Author: rezayw4
License: MIT
License-File: LICENSE
Keywords: fraud-detection,indonesia,ktp,kyc,ocr,tesseract
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Requires-Python: >=3.9
Requires-Dist: easyocr>=1.7
Requires-Dist: numpy>=1.26
Requires-Dist: opencv-python>=4.10
Requires-Dist: pillow>=10.4
Requires-Dist: pydantic>=2.8
Requires-Dist: python-dateutil>=2.9
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: paddle
Requires-Dist: paddleocr>=2.8; extra == 'paddle'
Requires-Dist: paddlepaddle>=2.6; extra == 'paddle'
Description-Content-Type: text/markdown

# FKTD — Fraud KYC KTP Detector

[![PyPI version](https://img.shields.io/pypi/v/fktd.svg)](https://pypi.org/project/fktd/)
[![Python versions](https://img.shields.io/pypi/pyversions/fktd.svg)](https://pypi.org/project/fktd/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Multi-engine OCR plus strict cross-validation for Indonesian ID cards (KTP).

Most validators only check whether a field is empty. FKTD also decodes the 16-digit
NIK, which encodes the cardholder's birth date, sex, province, and regency, and checks
those against the printed fields. A card where the NIK says South Jakarta but the printed
city says West Jakarta will fail, even though every field is filled in.

This is structural validation, not identity verification. It does not talk to Dukcapil.
See [Disclaimer](#disclaimer).

## Contents

- [Pipeline](#pipeline)
- [Requirements](#requirements)
- [Install](#install)
- [Usage](#usage)
- [API](#api)
- [Extracted fields](#extracted-fields)
- [Fraud rules](#fraud-rules)
- [Pass criteria](#pass-criteria)
- [Region data](#region-data)
- [Disclaimer](#disclaimer)
- [License](#license)

## Pipeline

```
image -> OCR (EasyOCR / PaddleOCR) -> field extraction -> fraud rules -> FraudResult
```

`process()` runs OCR, parses the text into 19 fields, detects the photo and signature,
runs the rule engine, and returns a `FraudResult` with `is_passed`, a `risk_score`
(0–100), and the per-rule breakdown.

## Requirements

| | Minimum | Notes |
|---|---|---|
| Python | 3.9 | CPython, 64-bit. Tested through 3.12. |
| OS | macOS / Linux / Windows | Apple Silicon works without extra setup. |
| RAM | ~4 GB | EasyOCR loads PyTorch and model weights into memory. |
| Disk | ~2–3 GB | PyTorch and OpenCV wheels are large; EasyOCR weights add ~100 MB. |
| Network | once | EasyOCR downloads weights on first run. The region API is optional. |

Core dependencies (pulled in automatically): `easyocr>=1.7` (brings PyTorch),
`opencv-python>=4.10`, `Pillow>=10.4`, `numpy>=1.26`, `pydantic>=2.8`,
`python-dateutil>=2.9`.

The first `FKTD()` call downloads EasyOCR weights and is slow. After that it runs offline
unless you enable the live region API.

## Install

```bash
pip install fktd
```

EasyOCR is included and needs no system binaries. PaddleOCR is optional:

```bash
pip install "fktd[paddle]"   # paddlepaddle is unreliable on Apple Silicon
```

From source:

```bash
git clone https://github.com/rezayw/fktd.git
cd fktd
pip install -e ".[dev]"      # pytest + ruff
```

| Engine | Status | Notes |
|---|---|---|
| EasyOCR | included | Default. Indonesian out of the box, fine on macOS. |
| PaddleOCR | extra `[paddle]` | Higher accuracy/speed; best on Linux. |

## Usage

```python
from fktd import FKTD

detector = FKTD(engine="easyocr")        # or engine="paddle"
result = detector.process("ktp.jpg")

result.is_passed          # bool
result.risk_score         # 0 (safe) .. 100 (suspicious)
result.summary()          # failed checks as text
result.data.model_dump()  # all extracted fields
```

`summary()` looks like:

```
[FAIL] risk_score=55
  - (critical) nik.province_match: Provinsi di NIK (DKI JAKARTA) != field (JAWA BARAT)
  - (critical) vision.foto: Foto tidak terdeteksi
```

## API

### `FKTD(...)`

```python
FKTD(
    engine="easyocr",          # "easyocr" | "paddle" | an OCREngine instance
    lang=None,                 # OCR language override
    risk_threshold=25,         # pass requires risk_score < this
    min_confidence=60.0,       # OCR confidence floor for critical fields
    detect_vision=True,        # run photo + signature detection
    wilayah_provider="auto",   # "auto" | "api" | "local"
    **engine_kwargs,           # forwarded to the engine
)
```

| Method | Returns | Notes |
|---|---|---|
| `process(image, today=None)` | `FraudResult` | Full pipeline. |
| `extract_fields(image)` | `KTPData` | OCR + extraction only, no rules. |

`image` accepts a path, bytes, a PIL image, or a NumPy array. `today` overrides the
reference date used for age and birth-date checks (useful in tests).

### `FraudResult`

| Field | Type | Notes |
|---|---|---|
| `is_passed` | `bool` | Overall result. |
| `risk_score` | `int` | 0–100. |
| `checks` | `list[FraudCheck]` | Every rule, passed and failed. |
| `failed_checks` | `list[FraudCheck]` | Failed rules only (property). |
| `data` | `KTPData` | Extracted fields. |
| `summary()` | `str` | Status plus failed checks. |

`FraudCheck` carries `code`, `passed`, `severity` (`info` / `warning` / `critical`),
`message`, and `weight` (added to `risk_score` on failure).

## Extracted fields

19 text fields: Province, City/Regency, NIK, Name, Place of Birth, Date of Birth, Sex,
Blood Type, Address, RT/RW, Village (Kel/Desa), District (Kecamatan), Religion, Marital
Status, Occupation, Citizenship, Valid Until, Place Issued, Date Issued.

Two boolean flags from vision: `has_foto`, `has_tanda_tangan`.

## Fraud rules

Each rule emits a `FraudCheck`. A failure adds its weight to `risk_score`, capped at 100.

**Completeness** — 16 required fields. Empty is `CRITICAL`, weight 15 each. Blood type and
the issued-at/on fields are not required.

**NIK cross-check** (values encoded in the NIK):

| Check | Severity | Weight |
|---|---|---|
| NIK is exactly 16 digits | CRITICAL | 30 |
| Birth date in NIK == Date of Birth field | CRITICAL | 30 |
| Sex in NIK (day > 40 means female) == Sex field | CRITICAL | 25 |

**Location** (NIK codes vs. official region data):

| Check | Severity | Weight |
|---|---|---|
| Province code (2 digits) known and matches Province | CRITICAL | 25 |
| Regency code (4 digits) known and matches City/Regency | CRITICAL | 25 |

**Enums** (exact match):

| Field | Allowed | Severity | Weight |
|---|---|---|---|
| Sex | LAKI-LAKI, PEREMPUAN | CRITICAL | 20 |
| Religion | ISLAM, KRISTEN, KRISTEN PROTESTAN, KATOLIK, HINDU, BUDHA, KONGHUCU | CRITICAL | 20 |
| Citizenship | WNI, WNA | CRITICAL | 20 |
| Marital Status | BELUM KAWIN, KAWIN, CERAI HIDUP, CERAI MATI | WARNING | 10 |
| Blood Type | A, B, AB, O, - | WARNING | 5 |

**Text format:**

- Field values must be uppercase (`WARNING`, 10).
- Name must not contain digits (`CRITICAL`, 20).
- RT/RW matches `NNN/NNN`. Birth date must parse, not be in the future (`CRITICAL`, 25),
  and imply an age <= 120.
- "Valid Until" must be `SEUMUR HIDUP` (`CRITICAL`, 25). e-KTPs since 2016 are valid for
  life; an explicit expiry date means an old or invalid card.

**OCR confidence** — confidence below `min_confidence` on NIK, Name, or Date of Birth adds
`WARNING` (8).

**Vision** — missing photo is `CRITICAL` (20); missing signature is `WARNING` (10).

## Pass criteria

`is_passed` is true only when all of:

1. all required fields present,
2. no failed `CRITICAL` checks,
3. `risk_score < risk_threshold` (default 25).

## Region data

`wilayah_provider` controls where region codes are resolved:

| Mode | Behaviour |
|---|---|
| `auto` (default) | Local snapshot first, live API for codes the snapshot lacks. |
| `api` | Always hit the live region API (emsifa). |
| `local` | Snapshot only, no network. |

```python
FKTD(wilayah_provider="local")   # no outbound requests
```

## Disclaimer

**e-KTP only.** FKTD targets the electronic KTP (KTP-el), the standard since 2011, valid
for life. Rules like "Valid Until must be SEUMUR HIDUP" do not apply to pre-2011 cards that
carry an expiry date; those will fail because they are no longer valid.

**Structural, not official.** NIK and region checks validate the structure and internal
consistency of the data on the card. FKTD does not access Dukcapil, so it cannot confirm a
NIK is registered or belongs to a real person. `PASS` does not prove the card is genuine;
`FAIL` does not prove forgery (it can be a bad scan, an old card, or an OCR miss).
`risk_score` is an indicator, not a decision.

For production KYC, combine FKTD with official verification (Dukcapil or a licensed NIK
provider) and manual review where needed.

**Privacy.** A KTP holds personal data. Make sure your use complies with Indonesia's UU PDP
(Law No. 27/2022): lawful basis or consent, limited retention, secured storage. FKTD never
uploads card images; the only outbound traffic is the optional region API (province/regency
codes), which `wilayah_provider="local"` disables.

## License

[MIT](LICENSE)
