VietAIDetector — Complete Project Structure
Overview
A system for detecting AI-generated Vietnamese text using the VietBinoculars algorithm (PPL/X-PPL ratio) with the PhoGPT-4B model pair. Includes OCR support for scanned PDFs via Vintern-1B-v2 and long documents. Features a Gradio UI and is optimized for dual-GPU (e.g., Kaggle T4x2).
Algorithm
VietBinoculars Score = perplexity(performer) / cross_entropy(observer → performer)
Low score → AI-generated | High score → Human-written
Default threshold: YOUDEN_THRESHOLD $\approx 0.9280$ (Youden's J)
Directory Structure
VietAIDetector/
├── README.md ← Usage guide
├── requirements.txt ← Python dependencies
├── app.py ← Entry point: launch Gradio app
├── run_kaggle.sh ← Kaggle T4x2 deployment script
├── pyproject.toml ← Pytest configuration
├── mkdocs.yml ← MkDocs configuration
│
├── docs/ ← Documentation (MkDocs)
│ ├── STRUCTURE.md ← This file
│ ├── ARCHITECTURE.md ← Technical architecture & design
│ └── DEVELOPER_MANUAL.md ← Complete developer guide
│
├── diagram/ ← Architecture diagrams (Graphviz)
│
├── examples/ ← Sample documents for testing
│
├── config/
│ ├── __init__.py
│ └── settings.py ← Constants: models, thresholds, chunking, OCR config, devices
│
├── schemas/
│ ├── __init__.py
│ └── models.py ← Dataclasses: ChunkDetail, DetectionResult, PreprocessResult
│
├── core/
│ ├── __init__.py
│ ├── metrics.py ← perplexity() + cross_entropy() (from original Binoculars)
│ └── scorer.py ← VietBinocularsScorer: PhoGPT-4B dual-GPU scoring
│
├── preprocessing/
│ ├── __init__.py
│ ├── text_utils.py ← Regex constants, Vietnamese text helpers
│ ├── document_reader.py ← Read DOCX, PDF (native + scanned) → raw text
│ ├── normalizer.py ← Pipeline: de-hyphen, linebreaks, whitespace, filtering
│ └── ocr_engine.py ← VinternOCR: Vintern-1B-v2 for scanned PDF → text
│
├── processing/
│ ├── __init__.py
│ ├── chunker.py ← TextChunker: sliding window processing
│ └── aggregator.py ← ScoreAggregator: majority voting → final decision
│
├── reporting/
│ ├── __init__.py
│ └── pdf_report.py ← PDFReportGenerator: fpdf2 + NotoSans font
│
├── frontend/
│ ├── __init__.py
│ └── gradio_app.py ← Gradio UI: advanced settings, OCR progress, results
│
├── benchmarks/
│ ├── run_eval.py ← CLI benchmark evaluation script
│ ├── datasets/ ← LLM-generated test corpora (JSON)
│ └── train_datasets/ ← Update datasets/ with new data
│ ├── final_updated_dataset_20000.csv
│ ├── train_dataset_from_VietBinoculars.csv
│ └── *.jsonl
│
└── tests/
├── __init__.py
├── conftest.py ← Test configuration + sys.path setup
├── test_text_utils.py ← Unit tests: regex normalization
├── test_normalizer.py ← Unit tests: preprocessing pipeline
├── test_chunker.py ← Unit tests: sliding window chunking
├── test_aggregator.py ← Unit tests: majority voting aggregation
├── test_ocr_engine.py ← Unit tests: VinternOCR + scanned PDF handling
└── test_report_generator.py ← Unit tests: PDF generation
Data Flow Pipeline
[Gradio Frontend]
User inputs text / uploads file (.docx, .pdf, .txt)
Selects detection mode (Youden / Closest Point / Low FPR)
Adjusts Chunk Window and Overlap (Advanced Settings)
↓ Submit
[Backend Pipeline]
① DocumentReader
├─ Detect format (docx / pdf / plain)
├─ If PDF: attempt native text extraction (PyMuPDF)
│ ├─ Text found → "pdf_native" → continue
│ └─ No text layer → "pdf_scanned" → delegate to OCR
│ └── VinternOCR (Vintern-1B-v2 on cuda:1)
│ ├─ Convert pages to 300 DPI images
│ ├─ Dynamic high-resolution tiling (448×448)
│ ├─ Anti-hallucination: greedy decode, hard-coded prompt
│ ├─ Post-validation: skip pages < 10 chars
│ └─ Return concatenated page text
├─ If DOCX: extract via python-docx
└─ If plain: decode UTF-8
↓
② TextNormalizer
├─ De-hyphenation (fix broken words)
├─ Remove mid-sentence line breaks
├─ Collapse whitespace
└─ Filter short or noisy paragraphs
↓
③ TextChunker (PhoGPT tokenizer)
├─ Apply dynamic Window (W) and Overlap (O) from UI
├─ Calculate Stride (D = W - O)
├─ Tokenize full text → token IDs
├─ Sliding window generation
└─ Merge short trailing chunks
↓
④ VietBinocularsScorer (PhoGPT-4B + PhoGPT-4B-Chat)
├─ Observer (cuda:0): compute logits
├─ Performer (cuda:1): compute logits
├─ PPL = CE(performer logits, next tokens)
├─ X-PPL = CE(observer probs → performer logits)
└─ score = PPL / X-PPL (per chunk, batch_size=8)
↓
⑤ ScoreAggregator
├─ score < threshold → "AI"
├─ ai_pct = ai_chunks / total × 100
├─ ai_pct > 50% → "AI-generated"
├─ 0 < ai_pct ≤ 50% → "Human-written but contains AI-generated parts"
└─ ai_pct = 0% → "Human-written"
↓
⑥ PDFReportGenerator
└─ Generate PDF with AI (red) / Human (green) highlights
↓
[Gradio Frontend]
Render: Summary card (with source format) + chunk details table
Enable PDF report download
Models Used
| Role | Model | Default Device | VRAM (bf16) |
|---|---|---|---|
| Observer (base) | vinai/PhoGPT-4B | cuda:0 | ~9 GB |
| Performer (chat) | vinai/PhoGPT-4B-Chat | cuda:1 | ~9 GB |
| OCR (scanned PDF) | 5CD-AI/Vintern-1B-v2 | cuda:1 (lazy) | ~2.5 GB |
GPU Budget (Kaggle T4x2): cuda:0 = ~9/15 GB, cuda:1 = ~11.5/15 GB (when OCR is active)
Detection Thresholds (VietBinoculars Paper)
Centralized in config/settings.py (\(\text{Score} < \text{threshold} \implies \text{AI}\), \(\text{Score} \ge \text{threshold} \implies \text{Human}\)):
Mode (THRESHOLD_MODES) |
Constant | Value | Description |
|---|---|---|---|
| Youden's J (Default) | YOUDEN_THRESHOLD |
\(\approx 0.9280\) | Maximizes Youden's J (optimal balanced F1) |
| Closest Point | CLOSEST_POINT_THRESHOLD |
\(\approx 0.9251\) | Closest to \((0,1)\) on ROC curve |
| Low FPR (Fewer False Alarms) | FPR_THRESHOLD |
\(\approx 0.8993\) | Low False Positive Rate mode (\(\text{TPR@0.05FPR}\)) |
Dynamic Chunking Parameters
Values can be modified in the UI via the Advanced Settings accordion:
| Parameter | Default | Description |
|---|---|---|
| Window (W) | 450 | Max tokens per chunk. UI Slider: 100 - 1024. |
| Overlap (O) | 100 | Token overlap between chunks. UI Slider: 0 - 500. |
| Stride (D) | W - O | Automatically computed sliding step size. |
| Min Chunk | 50 | (Internal logic) If a chunk is smaller, it merges to the previous one. |
OCR Anti-Hallucination Configuration
| Parameter | Value | Rationale |
|---|---|---|
do_sample |
False |
Greedy decoding — no randomness |
temperature |
0.0 |
Eliminates sampling variability |
num_beams |
1 |
No beam search creativity |
repetition_penalty |
1.0 |
Preserves naturally repeated text |
max_new_tokens |
2048 |
Caps output, prevents runaway generation |
| Min page chars | 10 |
Pages below this threshold are skipped |